Enabled setting an invoice footer

This commit is contained in:
Hillel Coren 2015-02-28 23:42:47 +02:00
parent dc117dbaff
commit cfe01e7e05
22 changed files with 178 additions and 57 deletions

View File

@ -13,7 +13,8 @@ open-source software.
1. Redistributions of source code, in whole or part and with or without 1. Redistributions of source code, in whole or part and with or without
modification requires the express permission of the author and must prominently 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 2. Neither the name nor any trademark of the Author may be used to
endorse or promote products derived from this software without specific endorse or promote products derived from this software without specific
prior written permission. prior written permission.

View File

@ -53,6 +53,7 @@ class SendRecurringInvoices extends Command
$invoice->po_number = $recurInvoice->po_number; $invoice->po_number = $recurInvoice->po_number;
$invoice->public_notes = $recurInvoice->public_notes; $invoice->public_notes = $recurInvoice->public_notes;
$invoice->terms = $recurInvoice->terms; $invoice->terms = $recurInvoice->terms;
$invoice->invoice_footer = $recurInvoice->invoice_footer;
$invoice->tax_name = $recurInvoice->tax_name; $invoice->tax_name = $recurInvoice->tax_name;
$invoice->tax_rate = $recurInvoice->tax_rate; $invoice->tax_rate = $recurInvoice->tax_rate;
$invoice->invoice_design_id = $recurInvoice->invoice_design_id; $invoice->invoice_design_id = $recurInvoice->invoice_design_id;

View File

@ -597,6 +597,7 @@ class AccountController extends \BaseController
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->invoice_terms = Input::get('invoice_terms'); $account->invoice_terms = Input::get('invoice_terms');
$account->invoice_footer = Input::get('invoice_footer');
$account->email_footer = Input::get('email_footer'); $account->email_footer = Input::get('email_footer');
$account->save(); $account->save();

View File

@ -87,7 +87,8 @@ class InvoiceApiController extends Controller
$fields = [ $fields = [
'discount' => 0, 'discount' => 0,
'is_amount_discount' => false, 'is_amount_discount' => false,
'terms' => $account->invoice_terms, 'terms' => '',
'invoice_footer' => '',
'public_notes' => '', 'public_notes' => '',
'po_number' => '', 'po_number' => '',
'invoice_design_id' => $account->invoice_design_id, 'invoice_design_id' => $account->invoice_design_id,

View File

@ -535,5 +535,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -525,5 +525,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -194,8 +194,8 @@ return array(
'email_paid' => 'Email me when an invoice is <b>paid</b>', 'email_paid' => 'Email me when an invoice is <b>paid</b>',
'site_updates' => 'Site Updates', 'site_updates' => 'Site Updates',
'custom_messages' => 'Custom Messages', 'custom_messages' => 'Custom Messages',
'default_invoice_terms' => 'Set default invoice terms', 'default_invoice_terms' => 'Set default <b>invoice terms</b>',
'default_email_footer' => 'Set default email signature', 'default_email_footer' => 'Set default <b>email signature</b>',
'import_clients' => 'Import Client Data', 'import_clients' => 'Import Client Data',
'csv_file' => 'Select CSV file', 'csv_file' => 'Select CSV file',
'export_clients' => 'Export Client Data', 'export_clients' => 'Export Client Data',
@ -533,5 +533,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', 'click_once' => '*Please click "PAY NOW" only once - transaction may take up to 1 minute to process.',
'default_invoice_footer' => 'Set default <b>invoice footer</b>',
'invoice_footer' => 'Invoice footer',
'save_as_default_footer' => 'Save as default footer',
); );

View File

@ -505,5 +505,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -526,5 +526,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -528,5 +528,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -536,6 +536,9 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -534,6 +534,9 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -529,6 +529,9 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -516,5 +516,8 @@ return array(
'match_address' => '*Address must match address accociated with credit card.', '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.', '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',
); );

View File

@ -177,13 +177,18 @@ class Activity extends Eloquent
} else { } else {
$diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount')); $diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));
if ($diff == 0) { $fieldChanged = false;
return; 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;
}
} }
if ($diff > 0 || $fieldChanged) {
$backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id); $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($invoice->id);
if (!$invoice->is_quote && !$invoice->is_recurring) { if ($diff > 0 && !$invoice->is_quote && !$invoice->is_recurring) {
$client->balance = $client->balance + $diff; $client->balance = $client->balance + $diff;
$client->save(); $client->save();
} }
@ -199,6 +204,7 @@ class Activity extends Eloquent
$activity->save(); $activity->save();
} }
} }
}
public static function viewInvoice($invitation) public static function viewInvoice($invitation)
{ {

View File

@ -77,6 +77,7 @@ class Invoice extends EntityModel
'invoice_date', 'invoice_date',
'due_date', 'due_date',
'terms', 'terms',
'invoice_footer',
'public_notes', 'public_notes',
'amount', 'amount',
'balance', 'balance',

View File

@ -221,6 +221,8 @@ class InvoiceRepository
} }
} }
$account = \Auth::user()->account;
$invoice->client_id = $data['client_id']; $invoice->client_id = $data['client_id'];
$invoice->discount = round(Utils::parseFloat($data['discount']), 2); $invoice->discount = round(Utils::parseFloat($data['discount']), 2);
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
@ -240,7 +242,8 @@ class InvoiceRepository
$invoice->end_date = null; $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->public_notes = trim($data['public_notes']);
$invoice->po_number = trim($data['po_number']); $invoice->po_number = trim($data['po_number']);
$invoice->invoice_design_id = $data['invoice_design_id']; $invoice->invoice_design_id = $data['invoice_design_id'];
@ -357,9 +360,14 @@ class InvoiceRepository
$invoice->invoice_items()->save($invoiceItem); $invoice->invoice_items()->save($invoiceItem);
} }
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']) { if (isset($data['set_default_terms']) && $data['set_default_terms']) {
$account = \Auth::user()->account; $account->invoice_terms = trim($data['terms']);
$account->invoice_terms = $invoice->terms; }
if (isset($data['set_default_footer']) && $data['set_default_footer']) {
$account->invoice_footer = trim($data['invoice_footer']);
}
$account->save(); $account->save();
} }
@ -400,6 +408,7 @@ class InvoiceRepository
'start_date', 'start_date',
'end_date', 'end_date',
'terms', 'terms',
'invoice_footer',
'public_notes', 'public_notes',
'invoice_design_id', 'invoice_design_id',
'tax_name', 'tax_name',

View File

@ -38,6 +38,7 @@
{{ Former::legend('custom_messages') }} {{ 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::textarea('email_footer')->label(trans('texts.default_email_footer')) }}
{{ Former::actions( Button::lg_success_submit(trans('texts.save'))->append_with_icon('floppy-disk') ) }} {{ Former::actions( Button::lg_success_submit(trans('texts.save'))->append_with_icon('floppy-disk') ) }}

View File

@ -165,16 +165,36 @@
<td class="hide-border"/> <td class="hide-border"/>
<td colspan="2" rowspan="6" style="vertical-align:top"> <td colspan="2" rowspan="6" style="vertical-align:top">
<br/> <br/>
<div role="tabpanel">
<ul class="nav nav-tabs" role="tablist" style="border: none">
<li role="presentation" class="active"><a href="#notes" aria-controls="notes" role="tab" data-toggle="tab">{{ trans('texts.note_to_client') }}</a></li>
<li role="presentation"><a href="#terms" aria-controls="terms" role="tab" data-toggle="tab">{{ trans('texts.invoice_terms') }}</a></li>
<li role="presentation"><a href="#footer" aria-controls="footer" role="tab" data-toggle="tab">{{ trans('texts.invoice_footer') }}</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px">
{{ Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") {{ Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'")
->label(false)->placeholder(trans('texts.note_to_client'))->style('resize: none') }} ->label(null)->style('resize: none; min-width: 460px;')->rows(3) }}
{{ Former::textarea('terms')->data_bind("value: wrapped_terms, valueUpdate: 'afterkeydown'") </div>
->label(false)->placeholder(trans('texts.invoice_terms'))->style('resize: none') <div role="tabpanel" class="tab-pane" id="terms">
->addGroupClass('less-space-bottom') }} {{ Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: default_terms, valueUpdate: 'afterkeydown'")
<label class="checkbox" style="width: 200px"> ->label(false)->style('resize: none; min-width: 460px')->rows(3)
<input type="checkbox" style="width: 24px" data-bind="checked: set_default_terms"/>{{ trans('texts.save_as_default_terms') }} ->help('<label class="checkbox" style="width: 200px">
</label> <input type="checkbox" style="width: 24px" data-bind="checked: set_default_terms"/>'.trans('texts.save_as_default_terms').'</label>') }}
</div>
<div role="tabpanel" class="tab-pane" id="footer">
{{ Former::textarea('invoice_footer')->data_bind("value:wrapped_footer, placeholder: default_footer, valueUpdate: 'afterkeydown'")
->label(false)->style('resize: none; min-width: 460px')->rows(3)
->help('<label class="checkbox" style="width: 200px">
<input type="checkbox" style="width: 24px" data-bind="checked: set_default_footer"/>'.trans('texts.save_as_default_footer').'</label>') }}
</div>
</div>
</div>
</td> </td>
<td style="display:none" data-bind="visible: $root.invoice_item_taxes.show"/> <td class="hide-border" style="display:none" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="{{ $account->hide_quantity ? 1 : 2 }}">{{ trans('texts.subtotal') }}</td> <td colspan="{{ $account->hide_quantity ? 1 : 2 }}">{{ trans('texts.subtotal') }}</td>
<td style="text-align: right"><span data-bind="text: totals.subtotal"/></td> <td style="text-align: right"><span data-bind="text: totals.subtotal"/></td>
</tr> </tr>
@ -243,7 +263,7 @@
<tr> <tr>
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td style="display:none" class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/> <td style="display:none" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="{{ $account->hide_quantity ? 1 : 2 }}"><b>{{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }}</b></td> <td colspan="{{ $account->hide_quantity ? 1 : 2 }}"><b>{{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }}</b></td>
<td style="text-align: right"><span data-bind="text: totals.total"/></td> <td style="text-align: right"><span data-bind="text: totals.total"/></td>
</tr> </tr>
@ -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() { setTimeout(function() {
refreshPDF(); refreshPDF();
}, 1); }, 1);
@ -619,7 +639,6 @@
setComboboxValue($('.client_select'), setComboboxValue($('.client_select'),
client.public_id(), client.public_id(),
client.name.display()); client.name.display());
}); });
function applyComboboxListeners() { function applyComboboxListeners() {
@ -653,6 +672,13 @@
invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }}; invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }};
invoice.contact = _.findWhere(invoice.client.contacts, {send_invoice: true}); 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())) @if (file_exists($account->getLogoPath()))
invoice.image = "{{ HTML::image_data($account->getLogoPath()) }}"; invoice.image = "{{ HTML::image_data($account->getLogoPath()) }}";
invoice.imageWidth = {{ $account->getLogoWidth() }}; invoice.imageWidth = {{ $account->getLogoWidth() }};
@ -1025,8 +1051,12 @@
self.is_amount_discount = ko.observable(0); self.is_amount_discount = ko.observable(0);
self.frequency_id = ko.observable(''); self.frequency_id = ko.observable('');
//self.currency_id = ko.observable({{ $client && $client->currency_id ? $client->currency_id : Session::get(SESSION_CURRENCY) }}); //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.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.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.public_notes = ko.observable('');
self.po_number = ko.observable(''); self.po_number = ko.observable('');
self.invoice_date = ko.observable('{{ Utils::today() }}'); self.invoice_date = ko.observable('{{ Utils::today() }}');
@ -1102,13 +1132,11 @@
self.wrapped_terms = ko.computed({ self.wrapped_terms = ko.computed({
read: function() { read: function() {
$('#terms').height(this.terms().split('\n').length * 36);
return this.terms(); return this.terms();
}, },
write: function(value) { write: function(value) {
value = wordWrapText(value, 300); value = wordWrapText(value, 300);
self.terms(value); self.terms(value);
$('#terms').height(value.split('\n').length * 36);
}, },
owner: this owner: this
}); });
@ -1116,17 +1144,25 @@
self.wrapped_notes = ko.computed({ self.wrapped_notes = ko.computed({
read: function() { read: function() {
$('#public_notes').height(this.public_notes().split('\n').length * 36);
return this.public_notes(); return this.public_notes();
}, },
write: function(value) { write: function(value) {
value = wordWrapText(value, 300); value = wordWrapText(value, 300);
self.public_notes(value); self.public_notes(value);
$('#public_notes').height(value.split('\n').length * 36);
}, },
owner: this 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.removeItem = function(item) {
self.invoice_items.remove(item); self.invoice_items.remove(item);

View File

@ -31603,6 +31603,16 @@ function GetPdf(invoice, javascript){
eval(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; return doc;
} }
@ -31991,6 +32001,13 @@ if (window.ko) {
if (value) $(element).datepicker('update', value); 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) function wordWrapText(value, width)

View File

@ -80,6 +80,16 @@ function GetPdf(invoice, javascript){
eval(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; return doc;
} }
@ -468,6 +478,13 @@ if (window.ko) {
if (value) $(element).datepicker('update', value); 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) function wordWrapText(value, width)