Allow documents to be attached to expenses

This commit is contained in:
Joshua Dwire 2016-03-24 18:15:52 -04:00
parent bff6782026
commit 5e62d7d296
22 changed files with 426 additions and 85 deletions

View File

@ -99,7 +99,7 @@ class ExpenseController extends BaseController
public function edit($publicId) public function edit($publicId)
{ {
$expense = Expense::scope($publicId)->firstOrFail(); $expense = Expense::scope($publicId)->with('documents')->firstOrFail();
if(!$this->checkEditPermission($expense, $response)){ if(!$this->checkEditPermission($expense, $response)){
return $response; return $response;
@ -163,7 +163,14 @@ class ExpenseController extends BaseController
*/ */
public function update(UpdateExpenseRequest $request) public function update(UpdateExpenseRequest $request)
{ {
$expense = $this->expenseService->save($request->input()); $data = $request->input();
$data['documents'] = $request->file('documents');
if(!$this->checkUpdatePermission($data, $response)){
return $response;
}
$expense = $this->expenseService->save($data, true);
Session::flash('message', trans('texts.updated_expense')); Session::flash('message', trans('texts.updated_expense'));
@ -195,7 +202,6 @@ class ExpenseController extends BaseController
$expenses = Expense::scope($ids)->with('client')->get(); $expenses = Expense::scope($ids)->with('client')->get();
$clientPublicId = null; $clientPublicId = null;
$currencyId = null; $currencyId = null;
$data = [];
// Validate that either all expenses do not have a client or if there is a client, it is the same client // Validate that either all expenses do not have a client or if there is a client, it is the same client
foreach ($expenses as $expense) foreach ($expenses as $expense)
@ -220,19 +226,11 @@ class ExpenseController extends BaseController
Session::flash('error', trans('texts.expense_error_invoiced')); Session::flash('error', trans('texts.expense_error_invoiced'));
return Redirect::to('expenses'); return Redirect::to('expenses');
} }
$account = Auth::user()->account;
$data[] = [
'publicId' => $expense->public_id,
'description' => $expense->public_notes,
'qty' => 1,
'cost' => $expense->present()->converted_amount,
];
} }
return Redirect::to("invoices/create/{$clientPublicId}") return Redirect::to("invoices/create/{$clientPublicId}")
->with('expenseCurrencyId', $currencyId) ->with('expenseCurrencyId', $currencyId)
->with('expenses', $data); ->with('expenses', $ids);
break; break;
default: default:

View File

@ -17,6 +17,7 @@ use App\Models\Invoice;
use App\Models\Client; use App\Models\Client;
use App\Models\Account; use App\Models\Account;
use App\Models\Product; use App\Models\Product;
use App\Models\Expense;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Models\InvoiceDesign; use App\Models\InvoiceDesign;
use App\Models\Activity; use App\Models\Activity;
@ -91,7 +92,7 @@ class InvoiceController extends BaseController
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$invoice = Invoice::scope($publicId) $invoice = Invoice::scope($publicId)
->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'payments') ->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'payments')
->withTrashed() ->withTrashed()
->firstOrFail(); ->firstOrFail();
@ -242,6 +243,12 @@ class InvoiceController extends BaseController
$invoice = $account->createInvoice($entityType, $clientId); $invoice = $account->createInvoice($entityType, $clientId);
$invoice->public_id = 0; $invoice->public_id = 0;
$invoice->expenses = Expense::scope([2])->with('documents')->get();
if(Session::get('expenses')){
$invoice->expenses = Expense::scope(Session::get('expenses'))->with('documents')->get();
}
$clients = Client::scope()->with('contacts', 'country')->orderBy('name'); $clients = Client::scope()->with('contacts', 'country')->orderBy('name');
if(!Auth::user()->hasPermission('view_all')){ if(!Auth::user()->hasPermission('view_all')){
$clients = $clients->where('clients.user_id', '=', Auth::user()->id); $clients = $clients->where('clients.user_id', '=', Auth::user()->id);
@ -352,7 +359,6 @@ class InvoiceController extends BaseController
'recurringDueDateHelp' => $recurringDueDateHelp, 'recurringDueDateHelp' => $recurringDueDateHelp,
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null, 'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null,
'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null, 'expenseCurrencyId' => Session::get('expenseCurrencyId') ?: null,
]; ];
@ -537,7 +543,7 @@ class InvoiceController extends BaseController
public function invoiceHistory($publicId) public function invoiceHistory($publicId)
{ {
$invoice = Invoice::withTrashed()->scope($publicId)->firstOrFail(); $invoice = Invoice::withTrashed()->scope($publicId)->firstOrFail();
$invoice->load('user', 'invoice_items', 'documents', 'account.country', 'client.contacts', 'client.country'); $invoice->load('user', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'account.country', 'client.contacts', 'client.country');
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->is_pro = Auth::user()->isPro(); $invoice->is_pro = Auth::user()->isPro();

View File

@ -180,12 +180,6 @@ class PublicClientController extends BaseController
return $paymentTypes; return $paymentTypes;
} }
protected function humanFilesize($bytes, $decimals = 2) {
$size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$size[$factor];
}
public function download($invitationKey) public function download($invitationKey)
{ {
if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
@ -473,7 +467,13 @@ class PublicClientController extends BaseController
} }
protected function getInvoiceZipDocuments($invoice, &$size=0){ protected function getInvoiceZipDocuments($invoice, &$size=0){
$documents = $invoice->documents->sortBy('size'); $documents = $invoice->documents;
foreach($invoice->expenses as $expense){
$documents = $documents->merge($expense->documents);
}
$documents = $documents->sortBy('size');
$size = 0; $size = 0;
$maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000; $maxSize = MAX_ZIP_DOCUMENTS_SIZE * 1000;
@ -520,10 +520,6 @@ class PublicClientController extends BaseController
$invoice = $invitation->invoice; $invoice = $invitation->invoice;
if(!count($invoice->documents)){
return Response::view('error', array('error'=>'No documents'), 404);
}
$toZip = $this->getInvoiceZipDocuments($invoice); $toZip = $this->getInvoiceZipDocuments($invoice);
if(!count($toZip)){ if(!count($toZip)){

View File

@ -200,8 +200,10 @@ class Document extends EntityModel
public function toArray() public function toArray()
{ {
$array = parent::toArray(); $array = parent::toArray();
$array['url'] = $this->getUrl();
$array['preview_url'] = $this->getPreviewUrl(); if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl();
if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl();
return $array; return $array;
} }

View File

@ -53,6 +53,11 @@ class Expense extends EntityModel
return $this->belongsTo('App\Models\Invoice')->withTrashed(); return $this->belongsTo('App\Models\Invoice')->withTrashed();
} }
public function documents()
{
return $this->hasMany('App\Models\Document')->orderBy('id');
}
public function getName() public function getName()
{ {
if($this->expense_number) if($this->expense_number)
@ -80,6 +85,20 @@ class Expense extends EntityModel
{ {
return $this->invoice_currency_id != $this->expense_currency_id; return $this->invoice_currency_id != $this->expense_currency_id;
} }
public function convertedAmount()
{
return round($this->amount * $this->exchange_rate, 2);
}
public function toArray()
{
$array = parent::toArray();
if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['previewconverted_amount_url'] = $this->convertedAmount();
return $array;
}
} }
Expense::creating(function ($expense) { Expense::creating(function ($expense) {

View File

@ -2,6 +2,7 @@
use Utils; use Utils;
use DateTime; use DateTime;
use URL;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
use App\Models\BalanceAffecting; use App\Models\BalanceAffecting;
@ -391,6 +392,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'balance', 'balance',
'invoice_items', 'invoice_items',
'documents', 'documents',
'expenses',
'client', 'client',
'tax_name', 'tax_name',
'tax_rate', 'tax_rate',
@ -463,6 +465,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'custom_invoice_text_label2', 'custom_invoice_text_label2',
'custom_invoice_item_label1', 'custom_invoice_item_label1',
'custom_invoice_item_label2', 'custom_invoice_item_label2',
'invoice_embed_documents'
]); ]);
foreach ($this->invoice_items as $invoiceItem) { foreach ($this->invoice_items as $invoiceItem) {
@ -487,6 +490,26 @@ class Invoice extends EntityModel implements BalanceAffecting
]); ]);
} }
foreach ($this->documents as $document) {
$document->setVisible([
'public_id',
'name',
]);
}
foreach ($this->expenses as $expense) {
$expense->setVisible([
'documents',
]);
foreach ($expense->documents as $document) {
$document->setVisible([
'public_id',
'name',
]);
}
}
return $this; return $this;
} }
@ -867,6 +890,18 @@ class Invoice extends EntityModel implements BalanceAffecting
return $taxes; return $taxes;
} }
public function hasDocuments(){
if(count($this->documents))return true;
return $this->hasExpenseDocuments();
}
public function hasExpenseDocuments(){
foreach($this->expenses as $expense){
if(count($expense->documents))return true;
}
return false;
}
} }
Invoice::creating(function ($invoice) { Invoice::creating(function ($invoice) {

View File

@ -63,8 +63,14 @@ class ContactMailer extends Mailer
} }
$documentStrings = array(); $documentStrings = array();
if ($account->document_email_attachment && !empty($invoice->documents)) { if ($account->document_email_attachment && $invoice->hasDocuments()) {
$documents = $invoice->documents->sortBy('size'); $documents = $invoice->documents;
foreach($invoice->expenses as $expense){
$documents = $documents->merge($expense->documents);
}
$documents = $documents->sortBy('size');
$size = 0; $size = 0;
$maxSize = MAX_EMAIL_DOCUMENTS_SIZE * 1000; $maxSize = MAX_EMAIL_DOCUMENTS_SIZE * 1000;
@ -285,11 +291,16 @@ class ContactMailer extends Mailer
$passwordHTML = isset($data['password'])?'<p>'.trans('texts.password').': '.$data['password'].'<p>':false; $passwordHTML = isset($data['password'])?'<p>'.trans('texts.password').': '.$data['password'].'<p>':false;
$documentsHTML = ''; $documentsHTML = '';
if($account->isPro() && count($invoice->documents)){ if($account->isPro() && $invoice->hasDocuments()){
$documentsHTML .= trans('texts.email_documents_header').'<ul>'; $documentsHTML .= trans('texts.email_documents_header').'<ul>';
foreach($invoice->documents as $document){ foreach($invoice->documents as $document){
$documentsHTML .= '<li><a href="'.HTML::entities($document->getClientUrl($invitation)).'">'.HTML::entities($document->name).'</a></li>'; $documentsHTML .= '<li><a href="'.HTML::entities($document->getClientUrl($invitation)).'">'.HTML::entities($document->name).'</a></li>';
} }
foreach($invoice->expenses as $expense){
foreach($expense->documents as $document){
$documentsHTML .= '<li><a href="'.HTML::entities($document->getClientUrl($invitation)).'">'.HTML::entities($document->name).'</a></li>';
}
}
$documentsHTML .= '</ul>'; $documentsHTML .= '</ul>';
} }

View File

@ -16,14 +16,9 @@ class ExpensePresenter extends Presenter {
return Utils::fromSqlDate($this->entity->expense_date); return Utils::fromSqlDate($this->entity->expense_date);
} }
public function converted_amount()
{
return round($this->entity->amount * $this->entity->exchange_rate, 2);
}
public function invoiced_amount() public function invoiced_amount()
{ {
return $this->entity->invoice_id ? $this->converted_amount() : 0; return $this->entity->invoice_id ? $this->entity->convertedAmount() : 0;
} }
public function link() public function link()

View File

@ -59,7 +59,7 @@ class DocumentRepository extends BaseRepository
public function upload($uploaded, &$doc_array=null) public function upload($uploaded, &$doc_array=null)
{ {
$extension = strtolower($uploaded->extension()); $extension = strtolower($uploaded->getClientOriginalExtension());
if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){
$documentType = Document::$extraExtensions[$extension]; $documentType = Document::$extraExtensions[$extension];
} }
@ -68,7 +68,7 @@ class DocumentRepository extends BaseRepository
} }
if(empty(Document::$types[$documentType])){ if(empty(Document::$types[$documentType])){
return 'Unsupported extension'; return 'Unsupported file type';
} }
$documentTypeData = Document::$types[$documentType]; $documentTypeData = Document::$types[$documentType];
@ -180,10 +180,14 @@ class DocumentRepository extends BaseRepository
public function getClientDatatable($contactId, $entityType, $search) public function getClientDatatable($contactId, $entityType, $search)
{ {
$query = DB::table('invitations')
$query = DB::table('invitations')
->join('accounts', 'accounts.id', '=', 'invitations.account_id') ->join('accounts', 'accounts.id', '=', 'invitations.account_id')
->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id')
->join('documents', 'documents.invoice_id', '=', 'invitations.invoice_id') ->join('expenses', 'expenses.invoice_id', '=', 'invitations.invoice_id')
->join('documents', function($join){
$join->on('documents.invoice_id', '=', 'invitations.invoice_id')->orOn('documents.expense_id', '=', 'expenses.id');
})
->join('clients', 'clients.id', '=', 'invoices.client_id') ->join('clients', 'clients.id', '=', 'invoices.client_id')
->where('invitations.contact_id', '=', $contactId) ->where('invitations.contact_id', '=', $contactId)
->where('invitations.deleted_at', '=', null) ->where('invitations.deleted_at', '=', null)

View File

@ -4,17 +4,25 @@ use DB;
use Utils; use Utils;
use App\Models\Expense; use App\Models\Expense;
use App\Models\Vendor; use App\Models\Vendor;
use App\Models\Document;
use App\Ninja\Repositories\BaseRepository; use App\Ninja\Repositories\BaseRepository;
use Session; use Session;
class ExpenseRepository extends BaseRepository class ExpenseRepository extends BaseRepository
{ {
protected $documentRepo;
// Expenses // Expenses
public function getClassName() public function getClassName()
{ {
return 'App\Models\Expense'; return 'App\Models\Expense';
} }
public function __construct(DocumentRepository $documentRepo)
{
$this->documentRepo = $documentRepo;
}
public function all() public function all()
{ {
return Expense::scope() return Expense::scope()
@ -113,7 +121,7 @@ class ExpenseRepository extends BaseRepository
return $query; return $query;
} }
public function save($input) public function save($input, $checkSubPermissions=false)
{ {
$publicId = isset($input['public_id']) ? $input['public_id'] : false; $publicId = isset($input['public_id']) ? $input['public_id'] : false;
@ -145,6 +153,45 @@ class ExpenseRepository extends BaseRepository
$expense->exchange_rate = round($rate, 4); $expense->exchange_rate = round($rate, 4);
$expense->amount = round(Utils::parseFloat($input['amount']), 2); $expense->amount = round(Utils::parseFloat($input['amount']), 2);
// Documents
$document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();;
foreach ($document_ids as $document_id){
$document = Document::scope($document_id)->first();
if($document && !$checkSubPermissions || $document->canEdit()){
$document->invoice_id = null;
$document->expense_id = $expense->id;
$document->save();
}
}
if(!empty($input['documents']) && Document::canCreate()){
// Fallback upload
$doc_errors = array();
foreach($input['documents'] as $upload){
$result = $this->documentRepo->upload($upload);
if(is_string($result)){
$doc_errors[] = $result;
}
else{
$result->expense_id = $expense->id;
$result->save();
$document_ids[] = $result->public_id;
}
}
if(!empty($doc_errors)){
Session::flash('error', implode('<br>',array_map('htmlentities',$doc_errors)));
}
}
foreach ($expense->documents as $document){
if(!in_array($document->public_id, $document_ids)){
// Removed
if(!$checkSubPermissions || $document->canEdit()){
$document->delete();
}
}
}
$expense->save(); $expense->save();
return $expense; return $expense;

View File

@ -415,6 +415,7 @@ class InvoiceRepository extends BaseRepository
} }
$document->invoice_id = $invoice->id; $document->invoice_id = $invoice->id;
$document->expense_id = null;
$document->save(); $document->save();
} }
} }

View File

@ -28,7 +28,7 @@ class ExpenseService extends BaseService
return $this->expenseRepo; return $this->expenseRepo;
} }
public function save($data) public function save($data, $checkSubPermissions=false)
{ {
if (isset($data['client_id']) && $data['client_id']) { if (isset($data['client_id']) && $data['client_id']) {
$data['client_id'] = Client::getPrivateId($data['client_id']); $data['client_id'] = Client::getPrivateId($data['client_id']);
@ -38,7 +38,7 @@ class ExpenseService extends BaseService
$data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']); $data['vendor_id'] = Vendor::getPrivateId($data['vendor_id']);
} }
return $this->expenseRepo->save($data); return $this->expenseRepo->save($data, $checkSubPermissions);
} }
public function getDatatable($search) public function getDatatable($search)

View File

@ -31396,13 +31396,19 @@ NINJA.invoiceLines = function(invoice) {
} }
NINJA.invoiceDocuments = function(invoice) { NINJA.invoiceDocuments = function(invoice) {
if(!invoice.documents || !invoice.account.invoice_embed_documents)return[]; if(!invoice.account.invoice_embed_documents)return[];
var stack = []; var stack = [];
var stackItem = null; var stackItem = null;
var j = 0; var j = 0;
for (var i = 0; i < invoice.documents.length; i++) { for (var i = 0; i < invoice.documents.length; i++)addDoc(invoice.documents[i]);
var document = invoice.documents[i];
for (var i = 0; i < invoice.expenses.length; i++) {
var expense = invoice.expenses[i];
for (var i = 0; i < expense.documents.length; i++)addDoc(expense.documents[i]);
}
function addDoc(document){
var path = document.base64; var path = document.base64;
if(!path)path = 'docs/'+document.public_id+'/'+document.name; if(!path)path = 'docs/'+document.public_id+'/'+document.name;
@ -31417,7 +31423,7 @@ NINJA.invoiceDocuments = function(invoice) {
} }
} }
return {stack:stack}; return stack.length?{stack:stack}:[];
} }
NINJA.subtotals = function(invoice, hideBalance) NINJA.subtotals = function(invoice, hideBalance)

15
public/css/built.css vendored
View File

@ -3193,10 +3193,21 @@ div.panel-body div.panel-body {
} }
/* Attached Documents */ /* Attached Documents */
.dropzone { #document-upload {
border:1px solid #ebe7e7; border:1px solid #ebe7e7;
background:#f9f9f9 !important; background:#f9f9f9 !important;
border-radius:3px; border-radius:3px;
padding:20px;
}
.invoice-table #document-upload{
max-width:560px;
}
#document-upload .dropzone{
background:none;
border:none;
padding:0;
} }
.dropzone .dz-preview.dz-image-preview{ .dropzone .dz-preview.dz-image-preview{
@ -3204,8 +3215,6 @@ div.panel-body div.panel-body {
} }
.dropzone .dz-preview .dz-image{ .dropzone .dz-preview .dz-image{
width:119px;
height:119px;
border-radius:5px!important; border-radius:5px!important;
} }

15
public/css/style.css vendored
View File

@ -1064,10 +1064,21 @@ div.panel-body div.panel-body {
} }
/* Attached Documents */ /* Attached Documents */
.dropzone { #document-upload {
border:1px solid #ebe7e7; border:1px solid #ebe7e7;
background:#f9f9f9 !important; background:#f9f9f9 !important;
border-radius:3px; border-radius:3px;
padding:20px;
}
.invoice-table #document-upload{
max-width:560px;
}
#document-upload .dropzone{
background:none;
border:none;
padding:0;
} }
.dropzone .dz-preview.dz-image-preview{ .dropzone .dz-preview.dz-image-preview{
@ -1075,8 +1086,6 @@ div.panel-body div.panel-body {
} }
.dropzone .dz-preview .dz-image{ .dropzone .dz-preview .dz-image{
width:119px;
height:119px;
border-radius:5px!important; border-radius:5px!important;
} }

View File

@ -404,13 +404,19 @@ NINJA.invoiceLines = function(invoice) {
} }
NINJA.invoiceDocuments = function(invoice) { NINJA.invoiceDocuments = function(invoice) {
if(!invoice.documents || !invoice.account.invoice_embed_documents)return[]; if(!invoice.account.invoice_embed_documents)return[];
var stack = []; var stack = [];
var stackItem = null; var stackItem = null;
var j = 0; var j = 0;
for (var i = 0; i < invoice.documents.length; i++) { for (var i = 0; i < invoice.documents.length; i++)addDoc(invoice.documents[i]);
var document = invoice.documents[i];
for (var i = 0; i < invoice.expenses.length; i++) {
var expense = invoice.expenses[i];
for (var i = 0; i < expense.documents.length; i++)addDoc(expense.documents[i]);
}
function addDoc(document){
var path = document.base64; var path = document.base64;
if(!path)path = 'docs/'+document.public_id+'/'+document.name; if(!path)path = 'docs/'+document.public_id+'/'+document.name;
@ -425,7 +431,7 @@ NINJA.invoiceDocuments = function(invoice) {
} }
} }
return {stack:stack}; return stack.length?{stack:stack}:[];
} }
NINJA.subtotals = function(invoice, hideBalance) NINJA.subtotals = function(invoice, hideBalance)

View File

@ -1103,11 +1103,13 @@ $LANG = array(
'email_documents_example_1' => 'Widgets Receipt.pdf', 'email_documents_example_1' => 'Widgets Receipt.pdf',
'email_documents_example_2' => 'Final Deliverable.zip', 'email_documents_example_2' => 'Final Deliverable.zip',
'invoice_documents' => 'Attached Documents', 'invoice_documents' => 'Attached Documents',
'expense_documents' => 'Attached Documents',
'document_upload_message' => 'Drop files here or click to upload.', 'document_upload_message' => 'Drop files here or click to upload.',
'invoice_embed_documents' => 'Embed Documents', 'invoice_embed_documents' => 'Embed Documents',
'invoice_embed_documents_help' => 'Include attached images in the invoice.', 'invoice_embed_documents_help' => 'Include attached images in the invoice.',
'document_email_attachment' => 'Attach Documents', 'document_email_attachment' => 'Attach Documents',
'download_documents' => 'Download Documents (:size)', 'download_documents' => 'Download Documents (:size)',
'documents_from_expenses' => 'From Expenses:',
'documents' => 'Documents', 'documents' => 'Documents',
'document_date' => 'Document Date', 'document_date' => 'Document Date',
'document_size' => 'Size', 'document_size' => 'Size',

View File

@ -105,6 +105,27 @@
</div> </div>
</div> </div>
@if ($account->isPro())
<div clas="row">
<div class="col-md-2 col-sm-4"><div class="control-label" style="margin-bottom:10px;">{{trans('texts.expense_documents')}}</div></div>
<div class="col-md-12 col-sm-8">
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload" class="dropzone">
<div class="fallback">
<input name="documents[]" type="file" multiple />
</div>
<div data-bind="foreach: documents">
<div class="fallback-doc">
<a href="#" class="fallback-doc-remove" data-bind="click: $parent.removeDocument"><i class="fa fa-close"></i></a>
<span data-bind="text:name"></span>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
</div> </div>
</div> </div>
@ -122,6 +143,7 @@
{!! Former::close() !!} {!! Former::close() !!}
<script type="text/javascript"> <script type="text/javascript">
Dropzone.autoDiscover = false;
var vendors = {!! $vendors !!}; var vendors = {!! $vendors !!};
var clients = {!! $clients !!}; var clients = {!! $clients !!};
@ -194,6 +216,54 @@
@else @else
$('#amount').focus(); $('#amount').focus();
@endif @endif
@if (Auth::user()->account->isPro())
$('.main-form').submit(function(){
if($('#document-upload .fallback input').val())$(this).attr('enctype', 'multipart/form-data')
else $(this).removeAttr('enctype')
})
// Initialize document upload
dropzone = new Dropzone('#document-upload', {
url:{!! json_encode(url('document')) !!},
params:{
_token:"{{ Session::getToken() }}"
},
acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!},
addRemoveLinks:true,
maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}},
dictDefaultMessage:{!! json_encode(trans('texts.document_upload_message')) !!}
});
if(dropzone instanceof Dropzone){
dropzone.on("addedfile",handleDocumentAdded);
dropzone.on("removedfile",handleDocumentRemoved);
dropzone.on("success",handleDocumentUploaded);
for (var i=0; i<model.documents().length; i++) {
var document = model.documents()[i];
var mockFile = {
name:document.name(),
size:document.size(),
type:document.type(),
public_id:document.public_id(),
status:Dropzone.SUCCESS,
accepted:true,
url:document.preview_url()||document.url(),
mock:true,
index:i
};
dropzone.emit('addedfile', mockFile);
dropzone.emit('complete', mockFile);
if(document.preview_url()){
dropzone.emit('thumbnail', mockFile, document.preview_url()||document.url());
}
else if(document.type()=='jpeg' || document.type()=='png' || document.type()=='svg'){
dropzone.emit('thumbnail', mockFile, document.url());
}
dropzone.files.push(mockFile);
}
}
@endif
}); });
var ViewModel = function(data) { var ViewModel = function(data) {
@ -206,8 +276,16 @@
self.should_be_invoiced = ko.observable(); self.should_be_invoiced = ko.observable();
self.convert_currency = ko.observable(false); self.convert_currency = ko.observable(false);
self.mapping = {
'documents': {
create: function(options) {
return new DocumentModel(options.data);
}
}
}
if (data) { if (data) {
ko.mapping.fromJS(data, {}, this); ko.mapping.fromJS(data, self.mapping, this);
} }
self.account_currency_id = ko.observable({{ $account->getCurrencyId() }}); self.account_currency_id = ko.observable({{ $account->getCurrencyId() }});
@ -250,8 +328,57 @@
|| invoiceCurrencyId != self.account_currency_id() || invoiceCurrencyId != self.account_currency_id()
|| expenseCurrencyId != self.account_currency_id(); || expenseCurrencyId != self.account_currency_id();
}) })
};
self.addDocument = function() {
var documentModel = new DocumentModel();
self.documents.push(documentModel);
return documentModel;
}
self.removeDocument = function(doc) {
var public_id = doc.public_id?doc.public_id():doc;
self.documents.remove(function(document) {
return document.public_id() == public_id;
});
}
};
function DocumentModel(data) {
var self = this;
self.public_id = ko.observable(0);
self.size = ko.observable(0);
self.name = ko.observable('');
self.type = ko.observable('');
self.url = ko.observable('');
self.update = function(data){
ko.mapping.fromJS(data, {}, this);
}
if (data) {
self.update(data);
}
}
@if (Auth::user()->account->isPro())
function handleDocumentAdded(file){
if(file.mock)return;
file.index = model.documents().length;
model.addDocument({name:file.name, size:file.size, type:file.type});
}
function handleDocumentRemoved(file){
model.removeDocument(file.public_id);
}
function handleDocumentUploaded(file, response){
file.public_id = response.document.public_id
model.documents()[file.index].update(response.document);
if(response.document.preview_url){
dropzone.emit('thumbnail', file, response.document.preview_url);
}
}
@endif
</script> </script>
@stop @stop

View File

@ -38,9 +38,8 @@
{!! Former::open($url) {!! Former::open($url)
->method($method) ->method($method)
->addClass('warn-on-exit') ->addClass('warn-on-exit main-form')
->autocomplete('off') ->autocomplete('off')
->attributes(array('enctype'=>'multipart/form-data'))
->onsubmit('return onFormSubmit(event)') ->onsubmit('return onFormSubmit(event)')
->rules(array( ->rules(array(
'client' => 'required', 'client' => 'required',
@ -307,17 +306,27 @@
</div> </div>
@if ($account->isPro()) @if ($account->isPro())
<div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9"> <div role="tabpanel" class="tab-pane" id="attached-documents" style="position:relative;z-index:9">
<div id="document-upload" class="dropzone"> <div id="document-upload">
<div class="fallback"> <div class="dropzone">
<input name="documents[]" type="file" multiple /> <div class="fallback">
</div> <input name="documents[]" type="file" multiple />
<div data-bind="foreach: documents"> </div>
<div class="fallback-doc"> <div data-bind="foreach: documents">
<a href="#" class="fallback-doc-remove" data-bind="click: $parent.removeDocument"><i class="fa fa-close"></i></a> <div class="fallback-doc">
<span data-bind="text:name"></span> <a href="#" class="fallback-doc-remove" data-bind="click: $parent.removeDocument"><i class="fa fa-close"></i></a>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/> <span data-bind="text:name"></span>
<input type="hidden" name="document_ids[]" data-bind="value: public_id"/>
</div>
</div> </div>
</div> </div>
@if ($invoice->hasExpenseDocuments())
<h4>{{trans('texts.documents_from_expenses')}}</h4>
@foreach($invoice->expenses as $expense)
@foreach($expense->documents as $document)
<div>{{$document->name}}</div>
@endforeach
@endforeach
@endif
</div> </div>
</div> </div>
@endif @endif
@ -778,24 +787,24 @@
model.invoice().has_tasks(true); model.invoice().has_tasks(true);
@endif @endif
@if (isset($expenses) && $expenses) if(model.invoice().expenses() && !model.invoice().public_id()){
model.expense_currency_id({{ $expenseCurrencyId }}); model.expense_currency_id({{ $expenseCurrencyId }});
// move the blank invoice line item to the end // move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop(); var blank = model.invoice().invoice_items.pop();
var expenses = {!! $expenses !!}; var expenses = model.invoice().expenses();
for (var i=0; i<expenses.length; i++) { for (var i=0; i<expenses.length; i++) {
var expense = expenses[i]; var expense = expenses[i];
var item = model.invoice().addItem(); var item = model.invoice().addItem();
item.notes(expense.description); item.notes(expense.public_notes());
item.qty(expense.qty); item.qty(1);
item.expense_public_id(expense.publicId); item.expense_public_id(expense.public_id());
item.cost(expense.cost); item.cost(expense.converted_amount());
} }
model.invoice().invoice_items.push(blank); model.invoice().invoice_items.push(blank);
model.invoice().has_expenses(true); model.invoice().has_expenses(true);
@endif }
@endif @endif
@ -929,8 +938,13 @@
applyComboboxListeners(); applyComboboxListeners();
@if (Auth::user()->account->isPro()) @if (Auth::user()->account->isPro())
$('.main-form').submit(function(){
if($('#document-upload .dropzone .fallback input').val())$(this).attr('enctype', 'multipart/form-data')
else $(this).removeAttr('enctype')
})
// Initialize document upload // Initialize document upload
dropzone = new Dropzone('#document-upload', { dropzone = new Dropzone('#document-upload .dropzone', {
url:{!! json_encode(url('document')) !!}, url:{!! json_encode(url('document')) !!},
params:{ params:{
_token:"{{ Session::getToken() }}" _token:"{{ Session::getToken() }}"
@ -1360,6 +1374,13 @@
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script> <script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif @endif
@endforeach @endforeach
@foreach ($invoice->expenses as $expense)
@foreach ($expense->documents as $document)
@if($document->isPDFEmbeddable())
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif
@endforeach
@endforeach
@endif @endif
@stop @stop

View File

@ -57,11 +57,18 @@
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800]) @include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
@if (Auth::user()->account->isPro() && Auth::user()->account->invoice_embed_documents) @if (Utils::isPro() && $invoice->account->invoice_embed_documents)
@foreach ($invoice->documents as $document) @foreach ($invoice->documents as $document)
@if($document->isPDFEmbeddable()) @if($document->isPDFEmbeddable())
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script> <script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif @endif
@endforeach @endforeach
@foreach ($invoice->expenses as $expense)
@foreach ($expense->documents as $document)
@if($document->isPDFEmbeddable())
<script src="{{ $document->getVFSJSUrl() }}" type="text/javascript" async></script>
@endif
@endforeach
@endforeach
@endif @endif
@stop @stop

View File

@ -227,6 +227,7 @@ function InvoiceModel(data) {
self.invoice_status_id = ko.observable(0); self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray(); self.invoice_items = ko.observableArray();
self.documents = ko.observableArray(); self.documents = ko.observableArray();
self.expenses = ko.observableArray();
self.amount = ko.observable(0); self.amount = ko.observable(0);
self.balance = ko.observable(0); self.balance = ko.observable(0);
self.invoice_design_id = ko.observable(1); self.invoice_design_id = ko.observable(1);
@ -257,6 +258,11 @@ function InvoiceModel(data) {
return new DocumentModel(options.data); return new DocumentModel(options.data);
} }
}, },
'expenses': {
create: function(options) {
return new ExpenseModel(options.data);
}
},
'tax': { 'tax': {
create: function(options) { create: function(options) {
return new TaxRateModel(options.data); return new TaxRateModel(options.data);
@ -847,6 +853,28 @@ function DocumentModel(data) {
} }
} }
var ExpenseModel = function(data) {
var self = this;
self.mapping = {
'documents': {
create: function(options) {
return new DocumentModel(options.data);
}
}
}
self.description = ko.observable('');
self.qty = ko.observable(0);
self.public_id = ko.observable(0);
self.amount = ko.observable();
self.converted_amount = ko.observable();
if (data) {
ko.mapping.fromJS(data, self.mapping, this);
}
};
/* Custom binding for product key typeahead */ /* Custom binding for product key typeahead */
ko.bindingHandlers.typeahead = { ko.bindingHandlers.typeahead = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {

View File

@ -49,13 +49,18 @@
@endif @endif
<div class="clearfix"></div><p>&nbsp;</p> <div class="clearfix"></div><p>&nbsp;</p>
@if ($account->isPro() && count($invoice->documents)) @if ($account->isPro() && $invoice->hasDocuments())
<div class="invoice-documents"> <div class="invoice-documents">
<h3>{{ trans('texts.documents_header') }}</h3> <h3>{{ trans('texts.documents_header') }}</h3>
<ul> <ul>
@foreach ($invoice->documents as $document) @foreach ($invoice->documents as $document)
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li> <li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li>
@endforeach @endforeach
@foreach ($invoice->expenses as $expense)
@foreach ($expense->documents as $document)
<li><a target="_blank" href="{{ $document->getClientUrl($invitation) }}">{{$document->name}} ({{Form::human_filesize($document->size)}})</a></li>
@endforeach
@endforeach
</ul> </ul>
</div> </div>
@endif @endif
@ -63,9 +68,16 @@
@if ($account->isPro() && $account->invoice_embed_documents) @if ($account->isPro() && $account->invoice_embed_documents)
@foreach ($invoice->documents as $document) @foreach ($invoice->documents as $document)
@if($document->isPDFEmbeddable()) @if($document->isPDFEmbeddable())
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" {{ Input::has('phantomjs')?'':'async' }}></script> <script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" async></script>
@endif @endif
@endforeach @endforeach
@foreach ($invoice->expenses as $expense)
@foreach ($expense->documents as $document)
@if($document->isPDFEmbeddable())
<script src="{{ $document->getClientVFSJSUrl() }}" type="text/javascript" async></script>
@endif
@endforeach
@endforeach
@endif @endif
<script type="text/javascript"> <script type="text/javascript">