diff --git a/LICENSE b/LICENSE index 81e4a1266..eaa9f1e36 100644 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,8 @@ open-source software. 1. Redistributions of source code, in whole or part and with or without modification requires the express permission of the author and must prominently -display "Powered by InvoiceNinja" in verifiable form with hyperlink to said site. +display "Powered by InvoiceNinja" or the Invoice Ninja logo in verifiable form +with hyperlink to said site. 2. Neither the name nor any trademark of the Author may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/app/commands/SendRecurringInvoices.php b/app/commands/SendRecurringInvoices.php index dda7bdc85..b527e7ae4 100755 --- a/app/commands/SendRecurringInvoices.php +++ b/app/commands/SendRecurringInvoices.php @@ -53,6 +53,7 @@ class SendRecurringInvoices extends Command $invoice->po_number = $recurInvoice->po_number; $invoice->public_notes = $recurInvoice->public_notes; $invoice->terms = $recurInvoice->terms; + $invoice->invoice_footer = $recurInvoice->invoice_footer; $invoice->tax_name = $recurInvoice->tax_name; $invoice->tax_rate = $recurInvoice->tax_rate; $invoice->invoice_design_id = $recurInvoice->invoice_design_id; diff --git a/app/controllers/AccountController.php b/app/controllers/AccountController.php index 70e1cbb63..52caaae72 100755 --- a/app/controllers/AccountController.php +++ b/app/controllers/AccountController.php @@ -597,6 +597,7 @@ class AccountController extends \BaseController { $account = Auth::user()->account; $account->invoice_terms = Input::get('invoice_terms'); + $account->invoice_footer = Input::get('invoice_footer'); $account->email_footer = Input::get('email_footer'); $account->save(); diff --git a/app/controllers/InvoiceApiController.php b/app/controllers/InvoiceApiController.php index 6e8696bfe..40c63ea30 100644 --- a/app/controllers/InvoiceApiController.php +++ b/app/controllers/InvoiceApiController.php @@ -87,7 +87,8 @@ class InvoiceApiController extends Controller $fields = [ 'discount' => 0, 'is_amount_discount' => false, - 'terms' => $account->invoice_terms, + 'terms' => '', + 'invoice_footer' => '', 'public_notes' => '', 'po_number' => '', 'invoice_design_id' => $account->invoice_design_id, diff --git a/app/lang/da/texts.php b/app/lang/da/texts.php index 71870293b..74ba37fec 100644 --- a/app/lang/da/texts.php +++ b/app/lang/da/texts.php @@ -535,5 +535,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/lang/de/texts.php b/app/lang/de/texts.php index 54351d10c..4d72f3ad1 100644 --- a/app/lang/de/texts.php +++ b/app/lang/de/texts.php @@ -525,5 +525,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/lang/en/texts.php b/app/lang/en/texts.php index 99d84e7d3..bd1b30a55 100644 --- a/app/lang/en/texts.php +++ b/app/lang/en/texts.php @@ -194,8 +194,8 @@ return array( 'email_paid' => 'Email me when an invoice is paid', 'site_updates' => 'Site Updates', 'custom_messages' => 'Custom Messages', - 'default_invoice_terms' => 'Set default invoice terms', - 'default_email_footer' => 'Set default email signature', + 'default_invoice_terms' => 'Set default invoice terms', + 'default_email_footer' => 'Set default email signature', 'import_clients' => 'Import Client Data', 'csv_file' => 'Select CSV file', 'export_clients' => 'Export Client Data', @@ -533,5 +533,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/lang/es/texts.php b/app/lang/es/texts.php index b1d5e79f7..752581c83 100644 --- a/app/lang/es/texts.php +++ b/app/lang/es/texts.php @@ -505,5 +505,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); \ No newline at end of file diff --git a/app/lang/fr/texts.php b/app/lang/fr/texts.php index ff2cf1b99..676fa1421 100644 --- a/app/lang/fr/texts.php +++ b/app/lang/fr/texts.php @@ -525,6 +525,9 @@ return array( 'order_overview' => 'Order overview', 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', - + + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); \ No newline at end of file diff --git a/app/lang/it/texts.php b/app/lang/it/texts.php index 264dd9c2e..68e9b5280 100644 --- a/app/lang/it/texts.php +++ b/app/lang/it/texts.php @@ -528,5 +528,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/lang/lt/texts.php b/app/lang/lt/texts.php index 77eb85a0e..43b28f4b9 100644 --- a/app/lang/lt/texts.php +++ b/app/lang/lt/texts.php @@ -536,6 +536,9 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/lang/nb_NO/texts.php b/app/lang/nb_NO/texts.php index 20a594bf2..68fdefc62 100644 --- a/app/lang/nb_NO/texts.php +++ b/app/lang/nb_NO/texts.php @@ -533,7 +533,10 @@ return array( 'order_overview' => 'Order overview', 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', - + + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); \ No newline at end of file diff --git a/app/lang/nl/texts.php b/app/lang/nl/texts.php index 60b8a2520..950e77bc9 100644 --- a/app/lang/nl/texts.php +++ b/app/lang/nl/texts.php @@ -529,6 +529,9 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); \ No newline at end of file diff --git a/app/lang/pt_BR/texts.php b/app/lang/pt_BR/texts.php index 4e02fde45..7a1213969 100644 --- a/app/lang/pt_BR/texts.php +++ b/app/lang/pt_BR/texts.php @@ -516,5 +516,8 @@ return array( 'match_address' => '*Address must match address accociated with credit card.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.', + 'default_invoice_footer' => 'Set default invoice footer', + 'invoice_footer' => 'Invoice footer', + 'save_as_default_footer' => 'Save as default footer', ); diff --git a/app/models/Activity.php b/app/models/Activity.php index f15506d43..c900ee1db 100755 --- a/app/models/Activity.php +++ b/app/models/Activity.php @@ -177,26 +177,32 @@ class Activity extends Eloquent } else { $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); - if ($diff == 0) { - return; + $fieldChanged = false; + foreach (['invoice_number', 'po_number', 'invoice_date', 'due_date', 'terms', 'public_notes', 'invoice_footer'] as $field) { + if ($invoice->$field != $invoice->getOriginal($field)) { + $fieldChanged = true; + break; + } } - $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id); + if ($diff > 0 || $fieldChanged) { + $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id); - if (!$invoice->is_quote && !$invoice->is_recurring) { - $client->balance = $client->balance + $diff; - $client->save(); + if ($diff > 0 && !$invoice->is_quote && !$invoice->is_recurring) { + $client->balance = $client->balance + $diff; + $client->save(); + } + + $activity = Activity::getBlank($invoice); + $activity->client_id = $invoice->client_id; + $activity->invoice_id = $invoice->id; + $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; + $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice); + $activity->balance = $client->balance; + $activity->adjustment = $invoice->is_quote || $invoice->is_recurring ? 0 : $diff; + $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); + $activity->save(); } - - $activity = Activity::getBlank($invoice); - $activity->client_id = $invoice->client_id; - $activity->invoice_id = $invoice->id; - $activity->activity_type_id = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; - $activity->message = Utils::encodeActivity(Auth::user(), 'updated', $invoice); - $activity->balance = $client->balance; - $activity->adjustment = $invoice->is_quote || $invoice->is_recurring ? 0 : $diff; - $activity->json_backup = $backupInvoice->hidePrivateFields()->toJSON(); - $activity->save(); } } diff --git a/app/models/Invoice.php b/app/models/Invoice.php index b5f7cad5b..0d7687d4a 100755 --- a/app/models/Invoice.php +++ b/app/models/Invoice.php @@ -77,6 +77,7 @@ class Invoice extends EntityModel 'invoice_date', 'due_date', 'terms', + 'invoice_footer', 'public_notes', 'amount', 'balance', diff --git a/app/ninja/repositories/InvoiceRepository.php b/app/ninja/repositories/InvoiceRepository.php index cef04eb2d..8624c3d9e 100755 --- a/app/ninja/repositories/InvoiceRepository.php +++ b/app/ninja/repositories/InvoiceRepository.php @@ -221,6 +221,8 @@ class InvoiceRepository } } + $account = \Auth::user()->account; + $invoice->client_id = $data['client_id']; $invoice->discount = round(Utils::parseFloat($data['discount']), 2); $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; @@ -240,7 +242,8 @@ class InvoiceRepository $invoice->end_date = null; } - $invoice->terms = trim($data['terms']); + $invoice->terms = trim($data['terms']) ? trim($data['terms']) : $account->invoice_terms; + $invoice->invoice_footer = trim($data['invoice_footer']) ? trim($data['invoice_footer']) : $account->invoice_footer; $invoice->public_notes = trim($data['public_notes']); $invoice->po_number = trim($data['po_number']); $invoice->invoice_design_id = $data['invoice_design_id']; @@ -357,9 +360,14 @@ class InvoiceRepository $invoice->invoice_items()->save($invoiceItem); } - if (isset($data['set_default_terms']) && $data['set_default_terms']) { - $account = \Auth::user()->account; - $account->invoice_terms = $invoice->terms; + if ((isset($data['set_default_terms']) && $data['set_default_terms']) + || (isset($data['set_default_footer']) && $data['set_default_footer'])) { + if (isset($data['set_default_terms']) && $data['set_default_terms']) { + $account->invoice_terms = trim($data['terms']); + } + if (isset($data['set_default_footer']) && $data['set_default_footer']) { + $account->invoice_footer = trim($data['invoice_footer']); + } $account->save(); } @@ -400,6 +408,7 @@ class InvoiceRepository 'start_date', 'end_date', 'terms', + 'invoice_footer', 'public_notes', 'invoice_design_id', 'tax_name', diff --git a/app/views/accounts/notifications.blade.php b/app/views/accounts/notifications.blade.php index 7cebf0efb..08e0c97b4 100755 --- a/app/views/accounts/notifications.blade.php +++ b/app/views/accounts/notifications.blade.php @@ -37,7 +37,8 @@ {{ Former::legend('custom_messages') }} - {{ Former::textarea('invoice_terms')->label(trans('texts.default_invoice_terms')) }} + {{ Former::textarea('invoice_terms')->label(trans('texts.default_invoice_terms')) }} + {{ Former::textarea('invoice_footer')->label(trans('texts.default_invoice_footer')) }} {{ Former::textarea('email_footer')->label(trans('texts.default_email_footer')) }} {{ Former::actions( Button::lg_success_submit(trans('texts.save'))->append_with_icon('floppy-disk') ) }} diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index 47aaefb21..1c5a0fc36 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -165,16 +165,36 @@
- {{ Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") - ->label(false)->placeholder(trans('texts.note_to_client'))->style('resize: none') }} - {{ Former::textarea('terms')->data_bind("value: wrapped_terms, valueUpdate: 'afterkeydown'") - ->label(false)->placeholder(trans('texts.invoice_terms'))->style('resize: none') - ->addGroupClass('less-space-bottom') }} - +
+ + + +
+
+ {{ Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") + ->label(null)->style('resize: none; min-width: 460px;')->rows(3) }} +
+
+ {{ Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: default_terms, valueUpdate: 'afterkeydown'") + ->label(false)->style('resize: none; min-width: 460px')->rows(3) + ->help('') }} +
+ +
+
+ - + {{ trans('texts.subtotal') }} @@ -243,7 +263,7 @@ - + {{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }} @@ -568,7 +588,7 @@ }); } - $('#terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount').change(function() { + $('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount').change(function() { setTimeout(function() { refreshPDF(); }, 1); @@ -618,8 +638,7 @@ var client = model.invoice().client(); setComboboxValue($('.client_select'), client.public_id(), - client.name.display()); - + client.name.display()); }); function applyComboboxListeners() { @@ -653,6 +672,13 @@ invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }}; invoice.contact = _.findWhere(invoice.client.contacts, {send_invoice: true}); + if (!invoice.terms) { + invoice.terms = "{{ $account->invoice_terms }}"; + } + if (!invoice.invoice_footer) { + invoice.invoice_footer = "{{ $account->invoice_footer }}"; + } + @if (file_exists($account->getLogoPath())) invoice.image = "{{ HTML::image_data($account->getLogoPath()) }}"; invoice.imageWidth = {{ $account->getLogoWidth() }}; @@ -1025,8 +1051,12 @@ self.is_amount_discount = ko.observable(0); self.frequency_id = ko.observable(''); //self.currency_id = ko.observable({{ $client && $client->currency_id ? $client->currency_id : Session::get(SESSION_CURRENCY) }}); - self.terms = ko.observable(wordWrapText('{{ str_replace(["\r\n","\r","\n"], '\n', addslashes($account->invoice_terms)) }}', 300)); - self.set_default_terms = ko.observable(false); + self.terms = ko.observable(''); + self.default_terms = ko.observable({{ $account->invoice_terms ? 'true' : 'false' }} ? wordWrapText('{{ str_replace(["\r\n","\r","\n"], '\n', addslashes($account->invoice_terms)) }}', 300) : "{{ trans('texts.invoice_terms') }}"); + self.set_default_terms = ko.observable(false); + self.invoice_footer = ko.observable(''); + self.default_footer = ko.observable({{ $account->invoice_footer ? 'true' : 'false' }} ? wordWrapText('{{ str_replace(["\r\n","\r","\n"], '\n', addslashes($account->invoice_footer)) }}', 600) : "{{ trans('texts.invoice_footer') }}"); + self.set_default_footer = ko.observable(false); self.public_notes = ko.observable(''); self.po_number = ko.observable(''); self.invoice_date = ko.observable('{{ Utils::today() }}'); @@ -1102,31 +1132,37 @@ self.wrapped_terms = ko.computed({ read: function() { - $('#terms').height(this.terms().split('\n').length * 36); return this.terms(); }, write: function(value) { value = wordWrapText(value, 300); self.terms(value); - $('#terms').height(value.split('\n').length * 36); }, owner: this }); - self.wrapped_notes = ko.computed({ - read: function() { - $('#public_notes').height(this.public_notes().split('\n').length * 36); - return this.public_notes(); - }, - write: function(value) { - value = wordWrapText(value, 300); - self.public_notes(value); - $('#public_notes').height(value.split('\n').length * 36); - }, - owner: this - }); + self.wrapped_notes = ko.computed({ + read: function() { + return this.public_notes(); + }, + write: function(value) { + value = wordWrapText(value, 300); + self.public_notes(value); + }, + owner: this + }); + self.wrapped_footer = ko.computed({ + read: function() { + return this.invoice_footer(); + }, + write: function(value) { + value = wordWrapText(value, 600); + self.invoice_footer(value); + }, + owner: this + }); self.removeItem = function(item) { self.invoice_items.remove(item); diff --git a/app/views/invoices/pdf.blade.php b/app/views/invoices/pdf.blade.php index 2c8c01671..feb2e8234 100644 --- a/app/views/invoices/pdf.blade.php +++ b/app/views/invoices/pdf.blade.php @@ -69,7 +69,7 @@ } else { window.accountLogo = "{{ HTML::image_data($account->getLogoPath()) }}"; } - @endif + @endif var NINJA = NINJA || {}; NINJA.primaryColor = "{{ $account->primary_color }}"; diff --git a/public/built.js b/public/built.js index 2ab5710be..9fce8d058 100644 --- a/public/built.js +++ b/public/built.js @@ -31603,6 +31603,16 @@ function GetPdf(invoice, javascript){ eval(javascript); + // add footer + if (invoice.invoice_footer) { + doc.setFontType('normal'); + doc.setFontSize('8'); + SetPdfColor('Black',doc); + var top = doc.internal.pageSize.height - layout.marginLeft; + var numLines = invoice.invoice_footer.split("\n").length - 1; + doc.text(layout.marginLeft, top - (numLines * 8), invoice.invoice_footer); + } + return doc; } @@ -31991,6 +32001,13 @@ if (window.ko) { if (value) $(element).datepicker('update', value); } }; + + ko.bindingHandlers.placeholder = { + init: function (element, valueAccessor, allBindingsAccessor) { + var underlyingObservable = valueAccessor(); + ko.applyBindingsToNode(element, { attr: { placeholder: underlyingObservable } } ); + } + }; } function wordWrapText(value, width) diff --git a/public/js/script.js b/public/js/script.js index 6b6ff47f4..ad7d7bc36 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -80,6 +80,16 @@ function GetPdf(invoice, javascript){ eval(javascript); + // add footer + if (invoice.invoice_footer) { + doc.setFontType('normal'); + doc.setFontSize('8'); + SetPdfColor('Black',doc); + var top = doc.internal.pageSize.height - layout.marginLeft; + var numLines = invoice.invoice_footer.split("\n").length - 1; + doc.text(layout.marginLeft, top - (numLines * 8), invoice.invoice_footer); + } + return doc; } @@ -468,6 +478,13 @@ if (window.ko) { if (value) $(element).datepicker('update', value); } }; + + ko.bindingHandlers.placeholder = { + init: function (element, valueAccessor, allBindingsAccessor) { + var underlyingObservable = valueAccessor(); + ko.applyBindingsToNode(element, { attr: { placeholder: underlyingObservable } } ); + } + }; } function wordWrapText(value, width)