Added support for partial payments

This commit is contained in:
Hillel Coren 2015-04-16 22:57:12 +03:00
parent caae008403
commit 96a71864ed
23 changed files with 128 additions and 33 deletions

View File

@ -262,7 +262,7 @@ class PaymentController extends BaseController
$card = new CreditCard($data);
return [
'amount' => ($invoice->partial ? $invoice->partial : $invoice->balance),
'amount' => $invoice->getRequestedAmount(),
'card' => $card,
'currency' => $currencyCode,
'returnUrl' => URL::to('complete'),
@ -304,7 +304,7 @@ class PaymentController extends BaseController
$data = [
'showBreadcrumbs' => false,
'url' => 'payment/'.$invitationKey,
'amount' => ($invoice->partial ? $invoice->partial : $invoice->balance),
'amount' => $invoice->getRequestedAmount(),
'invoiceNumber' => $invoice->invoice_number,
'client' => $client,
'contact' => $invitation->contact,
@ -603,7 +603,7 @@ class PaymentController extends BaseController
$payment->invitation_id = $invitation->id;
$payment->account_gateway_id = $accountGateway->id;
$payment->invoice_id = $invoice->id;
$payment->amount = $invoice->partial ? $invoice->partial : $invoice->balance;
$payment->amount = $invoice->getRequestedAmount();
$payment->client_id = $invoice->client_id;
$payment->contact_id = $invitation->contact_id;
$payment->transaction_reference = $ref;

View File

@ -228,6 +228,7 @@ class Account extends Eloquent
'subtotal',
'paid_to_date',
'balance_due',
'amount_due',
'terms',
'your_invoice',
'quote',

View File

@ -187,7 +187,7 @@ class Activity extends Eloquent
$diff = floatval($invoice->amount) - floatval($invoice->getOriginal('amount'));
$fieldChanged = false;
foreach (['invoice_number', 'po_number', 'invoice_date', 'due_date', 'terms', 'public_notes', 'invoice_footer'] as $field) {
foreach (['invoice_number', 'po_number', 'invoice_date', 'due_date', 'terms', 'public_notes', 'invoice_footer', 'partial'] as $field) {
if ($invoice->$field != $invoice->getOriginal($field)) {
$fieldChanged = true;
break;

View File

@ -9,7 +9,7 @@ class Invitation extends EntityModel
public function invoice()
{
return $this->belongsTo('App\Models\Invoice');
return $this->belongsTo('App\Models\Invoice')->withTrashed();
}
public function contact()

View File

@ -72,6 +72,11 @@ class Invoice extends EntityModel
return $this->invoice_status_id >= INVOICE_STATUS_PAID;
}
public function getRequestedAmount()
{
return $this->partial > 0 ? $this->partial : $this->balance;
}
public function hidePrivateFields()
{
$this->setVisible([

View File

@ -19,7 +19,7 @@ class ContactMailer extends Mailer
$subject = trans("texts.{$entityType}_subject", ['invoice' => $invoice->invoice_number, 'account' => $invoice->account->getDisplayName()]);
$accountName = $invoice->account->getDisplayName();
$emailTemplate = $invoice->account->getEmailTemplate($entityType);
$invoiceAmount = Utils::formatMoney($invoice->amount, $invoice->client->currency_id);
$invoiceAmount = Utils::formatMoney($invoice->getRequestedAmount(), $invoice->client->currency_id);
foreach ($invoice->invitations as $invitation) {
if (!$invitation->user || !$invitation->user->email) {

View File

@ -19,7 +19,7 @@ class InvoiceRepository
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->where('contacts.is_primary', '=', true)
->select('clients.public_id as client_public_id', 'invoice_number', 'invoice_status_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'quote_id', 'quote_invoice_id', 'invoices.deleted_at', 'invoices.is_deleted');
->select('clients.public_id as client_public_id', 'invoice_number', 'invoice_status_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'quote_id', 'quote_invoice_id', 'invoices.deleted_at', 'invoices.is_deleted', 'invoices.partial');
if (!\Session::get('show_trash:'.$entityType)) {
$query->where('invoices.deleted_at', '=', null);
@ -86,7 +86,7 @@ class InvoiceRepository
->where('invoices.is_deleted', '=', false)
->where('clients.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->select('invitation_key', 'invoice_number', 'invoice_date', 'invoices.balance as balance', 'due_date', 'clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'start_date', 'end_date', 'clients.currency_id');
->select('invitation_key', 'invoice_number', 'invoice_date', 'invoices.balance as balance', 'due_date', 'clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'amount', 'start_date', 'end_date', 'clients.currency_id', 'invoices.partial');
$table = \Datatable::query($query)
->addColumn('invoice_number', function ($model) use ($entityType) { return link_to('/view/'.$model->invitation_key, $model->invoice_number); })
@ -94,7 +94,11 @@ class InvoiceRepository
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); });
if ($entityType == ENTITY_INVOICE) {
$table->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); });
$table->addColumn('balance', function ($model) {
return $model->partial > 0 ?
trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) :
Utils::formatMoney($model->balance, $model->currency_id);
});
}
return $table->addColumn('due_date', function ($model) { return Utils::fromSqlDate($model->due_date); })
@ -122,7 +126,11 @@ class InvoiceRepository
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); });
if ($entityType == ENTITY_INVOICE) {
$table->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id); });
$table->addColumn('balance', function ($model) {
return $model->partial > 0 ?
trans('texts.partial_remaining', ['partial' => Utils::formatMoney($model->partial, $model->currency_id), 'balance' => Utils::formatMoney($model->balance, $model->currency_id)]) :
Utils::formatMoney($model->balance, $model->currency_id);
});
}
return $table->addColumn('due_date', function ($model) { return Utils::fromSqlDate($model->due_date); })

View File

@ -31601,6 +31601,12 @@ function GetPdf(invoice, javascript){
//set default style for report
doc.setFont('Helvetica','');
// For partial payments show "Amount Due" rather than "Balance Due"
if (!invoiceLabels.balance_due_orig) {
invoiceLabels.balance_due_orig = invoiceLabels.balance_due;
}
invoiceLabels.balance_due = NINJA.parseFloat(invoice.partial) ? invoiceLabels.amount_due : invoiceLabels.balance_due_orig;
eval(javascript);
// add footer
@ -32233,13 +32239,20 @@ function displayInvoice(doc, invoice, x, y, layout, rightAlignX) {
}
function getInvoiceDetails(invoice) {
return [
var fields = [
{'invoice_number': invoice.invoice_number},
{'po_number': invoice.po_number},
{'invoice_date': invoice.invoice_date},
{'due_date': invoice.due_date},
{'balance_due': formatMoney(invoice.balance_amount, invoice.client.currency_id)},
];
if (NINJA.parseFloat(invoice.partial)) {
fields.push({'total': formatMoney(invoice.total_amount, invoice.client.currency_id)});
}
fields.push({'balance_due': formatMoney(invoice.balance_amount, invoice.client.currency_id)})
return fields;
}
function getInvoiceDetailsHeight(invoice, layout) {
@ -32294,6 +32307,10 @@ function displaySubtotals(doc, layout, invoice, y, rightAlignTitleX)
data.push({'paid_to_date': formatMoney(paid, invoice.client.currency_id)});
}
if (NINJA.parseFloat(invoice.partial) && invoice.total_amount != invoice.subtotal_amount) {
data.push({'total': formatMoney(invoice.total_amount, invoice.client.currency_id)});
}
var options = {
hasheader: true,
rightAlignX: 550,
@ -32490,15 +32507,17 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2);
}
if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
}
invoice.total_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
invoice.discount_amount = discount;
invoice.tax_amount = tax;
invoice.has_taxes = hasTaxes;
if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = invoice.total_amount;
}
return invoice;
}

View File

@ -78,6 +78,12 @@ function GetPdf(invoice, javascript){
//set default style for report
doc.setFont('Helvetica','');
// For partial payments show "Amount Due" rather than "Balance Due"
if (!invoiceLabels.balance_due_orig) {
invoiceLabels.balance_due_orig = invoiceLabels.balance_due;
}
invoiceLabels.balance_due = NINJA.parseFloat(invoice.partial) ? invoiceLabels.amount_due : invoiceLabels.balance_due_orig;
eval(javascript);
// add footer
@ -710,13 +716,20 @@ function displayInvoice(doc, invoice, x, y, layout, rightAlignX) {
}
function getInvoiceDetails(invoice) {
return [
var fields = [
{'invoice_number': invoice.invoice_number},
{'po_number': invoice.po_number},
{'invoice_date': invoice.invoice_date},
{'due_date': invoice.due_date},
{'balance_due': formatMoney(invoice.balance_amount, invoice.client.currency_id)},
];
if (NINJA.parseFloat(invoice.partial)) {
fields.push({'total': formatMoney(invoice.total_amount, invoice.client.currency_id)});
}
fields.push({'balance_due': formatMoney(invoice.balance_amount, invoice.client.currency_id)})
return fields;
}
function getInvoiceDetailsHeight(invoice, layout) {
@ -771,6 +784,10 @@ function displaySubtotals(doc, layout, invoice, y, rightAlignTitleX)
data.push({'paid_to_date': formatMoney(paid, invoice.client.currency_id)});
}
if (NINJA.parseFloat(invoice.partial) && invoice.total_amount != invoice.subtotal_amount) {
data.push({'total': formatMoney(invoice.total_amount, invoice.client.currency_id)});
}
var options = {
hasheader: true,
rightAlignX: 550,
@ -967,15 +984,17 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2);
}
if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
}
invoice.total_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
invoice.discount_amount = discount;
invoice.tax_amount = tax;
invoice.has_taxes = hasTaxes;
if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = invoice.total_amount;
}
return invoice;
}

View File

@ -591,5 +591,8 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -582,6 +582,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -591,5 +591,7 @@ return array(
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -561,6 +561,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',

View File

@ -590,6 +590,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -582,6 +582,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -584,6 +584,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -592,6 +592,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',

View File

@ -590,6 +590,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -585,6 +585,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',

View File

@ -585,6 +585,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -588,6 +588,9 @@ return array(
'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
'partial_remaining' => ':partial of :balance',
);

View File

@ -11,7 +11,7 @@
</div>
@if ($gatewayLink)
{!! Button::link($gatewayLink, trans('texts.view_in_stripe'), ['target' => '_blank']) !!}
{!! Button::normal(trans('texts.view_in_stripe'))->asLinkTo($gatewayLink)->withAttributes(['target' => '_blank']) !!}
@endif
@if ($client->trashed())

View File

@ -78,7 +78,7 @@
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'due_date\')"></i>') !!}
{!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown', enable: is_partial")
->addGroupClass('partial')->append(Former::checkbox('is_partial')->raw()
->onchange('onPartialChange()')->addGroupClass('partial')->append(Former::checkbox('is_partial')->raw()
->data_bind('checked: is_partial')->onclick('onPartialEnabled()') . '&nbsp;' . (trans('texts.enable'))) !!}
</div>
@if ($entityType == ENTITY_INVOICE)
@ -1606,15 +1606,23 @@
}
}
function onPartialChange()
{
var val = NINJA.parseFloat($('#partial').val());
val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0);
$('#partial').val(val);
}
function onPartialEnabled()
{
if ($('#is_partial').prop('checked')) {
model.invoice().partial(model.invoice().totals.rawTotal() || '');
} else {
model.invoice().partial('');
}
model.invoice().partial('');
refreshPDF();
if ($('#is_partial').prop('checked')) {
setTimeout(function() {
$('#partial').focus();
}, 1);
}
}
function onRecurringEnabled()