1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-17 16:42:48 +01:00

Support for multiple tax rates

This commit is contained in:
Hillel Coren 2016-03-31 12:29:01 +03:00
parent f91f32323f
commit be9e7beaed
13 changed files with 382 additions and 194 deletions

View File

@ -258,10 +258,11 @@ class InvoiceApiController extends BaseAPIController
// initialize the line items // initialize the line items
if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) { if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) {
$data['invoice_items'] = [self::prepareItem($data)]; $data['invoice_items'] = [self::prepareItem($data)];
// make sure the tax isn't applied twice (for the invoice and the line item) // make sure the tax isn't applied twice (for the invoice and the line item)
unset($data['invoice_items'][0]['tax_name']); unset($data['invoice_items'][0]['tax_name1']);
unset($data['invoice_items'][0]['tax_rate']); unset($data['invoice_items'][0]['tax_rate1']);
unset($data['invoice_items'][0]['tax_name2']);
unset($data['invoice_items'][0]['tax_rate2']);
} else { } else {
foreach ($data['invoice_items'] as $index => $item) { foreach ($data['invoice_items'] as $index => $item) {
$data['invoice_items'][$index] = self::prepareItem($item); $data['invoice_items'][$index] = self::prepareItem($item);

View File

@ -25,6 +25,13 @@ class Invoice extends EntityModel implements BalanceAffecting
protected $presenter = 'App\Ninja\Presenters\InvoicePresenter'; protected $presenter = 'App\Ninja\Presenters\InvoicePresenter';
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $fillable = [
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
];
protected $casts = [ protected $casts = [
'is_recurring' => 'boolean', 'is_recurring' => 'boolean',
'has_tasks' => 'boolean', 'has_tasks' => 'boolean',
@ -394,8 +401,10 @@ class Invoice extends EntityModel implements BalanceAffecting
'documents', 'documents',
'expenses', 'expenses',
'client', 'client',
'tax_name', 'tax_name1',
'tax_rate', 'tax_rate1',
'tax_name2',
'tax_rate2',
'account', 'account',
'invoice_design', 'invoice_design',
'invoice_design_id', 'invoice_design_id',
@ -476,8 +485,10 @@ class Invoice extends EntityModel implements BalanceAffecting
'custom_value2', 'custom_value2',
'cost', 'cost',
'qty', 'qty',
'tax_name', 'tax_name1',
'tax_rate', 'tax_rate1',
'tax_name2',
'tax_rate2',
]); ]);
} }
@ -843,52 +854,66 @@ class Invoice extends EntityModel implements BalanceAffecting
return $total; return $total;
} }
// if $calculatePaid is true we'll loop through each payment to
// determine the sum, otherwise we'll use the cached paid_to_date amount
public function getTaxes($calculatePaid = false) public function getTaxes($calculatePaid = false)
{ {
$taxes = []; $taxes = [];
$taxable = $this->getTaxable(); $taxable = $this->getTaxable();
$paidAmount = $this->getAmountPaid($calculatePaid);
if ($this->tax_rate && $this->tax_name) { if ($this->tax_name1) {
$taxAmount = $taxable * ($this->tax_rate / 100); $invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2);
$taxAmount = round($taxAmount, 2); $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount);
if ($taxAmount) {
$taxes[$this->tax_rate . ' ' . $this->tax_name] = [
'name' => $this->tax_name,
'rate' => $this->tax_rate+0,
'amount' => $taxAmount,
'paid' => round($this->getAmountPaid($calculatePaid) / $this->amount * $taxAmount, 2)
];
} }
if ($this->tax_name2) {
$invoiceTaxAmount = round($taxable * ($this->tax_rate2 / 100), 2);
$invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0;
$this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount);
} }
foreach ($this->invoice_items as $invoiceItem) { foreach ($this->invoice_items as $invoiceItem) {
if ( ! $invoiceItem->tax_rate || ! $invoiceItem->tax_name) { $itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable);
continue;
if ($invoiceItem->tax_name1) {
$itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2);
$itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount);
} }
$taxAmount = $this->getItemTaxable($invoiceItem, $taxable); if ($invoiceItem->tax_name2) {
$taxAmount = $taxable * ($invoiceItem->tax_rate / 100); $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate2 / 100), 2);
$taxAmount = round($taxAmount, 2); $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0;
$this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount);
}
}
if ($taxAmount) { return $taxes;
$key = $invoiceItem->tax_rate . ' ' . $invoiceItem->tax_name; }
private function calculateTax(&$taxes, $name, $rate, $amount, $paid)
{
if ( ! $amount) {
return;
}
$amount = round($amount, 2);
$paid = round($paid, 2);
$key = $rate . ' ' . $name;
if ( ! isset($taxes[$key])) { if ( ! isset($taxes[$key])) {
$taxes[$key] = [ $taxes[$key] = [
'name' => $name,
'rate' => $rate+0,
'amount' => 0, 'amount' => 0,
'paid' => 0 'paid' => 0
]; ];
} }
$taxes[$key]['amount'] += $taxAmount; $taxes[$key]['amount'] += $amount;
$taxes[$key]['paid'] += $this->amount && $taxAmount ? round($this->getAmountPaid($calculatePaid) / $this->amount * $taxAmount, 2) : 0; $taxes[$key]['paid'] += $paid;
$taxes[$key]['name'] = $invoiceItem->tax_name;
$taxes[$key]['rate'] = $invoiceItem->tax_rate+0;
}
}
return $taxes;
} }
public function hasDocuments(){ public function hasDocuments(){

View File

@ -7,6 +7,13 @@ class InvoiceItem extends EntityModel
use SoftDeletes; use SoftDeletes;
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $fillable = [
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
];
public function invoice() public function invoice()
{ {
return $this->belongsTo('App\Models\Invoice'); return $this->belongsTo('App\Models\Invoice');

View File

@ -221,6 +221,8 @@ class InvoiceRepository extends BaseRepository
$invoice = Invoice::scope($publicId)->firstOrFail(); $invoice = Invoice::scope($publicId)->firstOrFail();
} }
$invoice->fill($data);
if ((isset($data['set_default_terms']) && $data['set_default_terms']) if ((isset($data['set_default_terms']) && $data['set_default_terms'])
|| (isset($data['set_default_footer']) && $data['set_default_footer'])) { || (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']) {
@ -297,12 +299,10 @@ class InvoiceRepository extends BaseRepository
$invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id; $invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id;
if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) { // provide backwards compatability
$invoice->tax_rate = Utils::parseFloat($data['tax_rate']); if (isset($data['tax_name']) && isset($data['tax_rate'])) {
$invoice->tax_name = trim($data['tax_name']); $data['tax_name1'] = $data['tax_name'];
} else { $data['tax_rate1'] = $data['tax_rate'];
$invoice->tax_rate = 0;
$invoice->tax_name = '';
} }
$total = 0; $total = 0;
@ -323,10 +323,8 @@ class InvoiceRepository extends BaseRepository
foreach ($data['invoice_items'] as $item) { foreach ($data['invoice_items'] as $item) {
$item = (array) $item; $item = (array) $item;
if (isset($item['tax_rate']) && Utils::parseFloat($item['tax_rate']) > 0) {
$invoiceItemCost = round(Utils::parseFloat($item['cost']), 2); $invoiceItemCost = round(Utils::parseFloat($item['cost']), 2);
$invoiceItemQty = round(Utils::parseFloat($item['qty']), 2); $invoiceItemQty = round(Utils::parseFloat($item['qty']), 2);
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate']);
$lineTotal = $invoiceItemCost * $invoiceItemQty; $lineTotal = $invoiceItemCost * $invoiceItemQty;
if ($invoice->discount > 0) { if ($invoice->discount > 0) {
@ -337,6 +335,12 @@ class InvoiceRepository extends BaseRepository
} }
} }
if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
}
if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) {
$invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']);
$itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2);
} }
} }
@ -378,8 +382,9 @@ class InvoiceRepository extends BaseRepository
$total += $invoice->custom_value2; $total += $invoice->custom_value2;
} }
$total += $total * $invoice->tax_rate / 100; $taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2);
$total = round($total, 2); $taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2);
$total = round($total + $taxAmount1 + $taxAmount2, 2);
$total += $itemTax; $total += $itemTax;
// custom fields not charged taxes // custom fields not charged taxes
@ -502,7 +507,7 @@ class InvoiceRepository extends BaseRepository
$invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes'])); $invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes']));
$invoiceItem->cost = Utils::parseFloat($item['cost']); $invoiceItem->cost = Utils::parseFloat($item['cost']);
$invoiceItem->qty = Utils::parseFloat($item['qty']); $invoiceItem->qty = Utils::parseFloat($item['qty']);
$invoiceItem->tax_rate = 0; //$invoiceItem->tax_rate = 0;
if (isset($item['custom_value1'])) { if (isset($item['custom_value1'])) {
$invoiceItem->custom_value1 = $item['custom_value1']; $invoiceItem->custom_value1 = $item['custom_value1'];
@ -511,11 +516,14 @@ class InvoiceRepository extends BaseRepository
$invoiceItem->custom_value2 = $item['custom_value2']; $invoiceItem->custom_value2 = $item['custom_value2'];
} }
if (isset($item['tax_rate']) && isset($item['tax_name']) && $item['tax_name']) { // provide backwards compatability
$invoiceItem['tax_rate'] = Utils::parseFloat($item['tax_rate']); if (isset($item['tax_name']) && isset($item['tax_rate'])) {
$invoiceItem['tax_name'] = trim($item['tax_name']); $item['tax_name1'] = $item['tax_name'];
$item['tax_rate1'] = $item['tax_rate'];
} }
$invoiceItem->fill($item);
$invoice->invoice_items()->save($invoiceItem); $invoice->invoice_items()->save($invoiceItem);
} }
@ -562,8 +570,10 @@ class InvoiceRepository extends BaseRepository
'invoice_footer', 'invoice_footer',
'public_notes', 'public_notes',
'invoice_design_id', 'invoice_design_id',
'tax_name', 'tax_name1',
'tax_rate', 'tax_rate1',
'tax_name2',
'tax_rate2',
'amount', 'amount',
'is_quote', 'is_quote',
'custom_value1', 'custom_value1',
@ -597,8 +607,11 @@ class InvoiceRepository extends BaseRepository
'notes', 'notes',
'cost', 'cost',
'qty', 'qty',
'tax_name', 'tax_name1',
'tax_rate', ] as $field) { 'tax_rate1',
'tax_name2',
'tax_rate2',
] as $field) {
$cloneItem->$field = $item->$field; $cloneItem->$field = $item->$field;
} }
@ -689,8 +702,10 @@ class InvoiceRepository extends BaseRepository
$invoice->public_notes = Utils::processVariables($recurInvoice->public_notes); $invoice->public_notes = Utils::processVariables($recurInvoice->public_notes);
$invoice->terms = Utils::processVariables($recurInvoice->terms); $invoice->terms = Utils::processVariables($recurInvoice->terms);
$invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer); $invoice->invoice_footer = Utils::processVariables($recurInvoice->invoice_footer);
$invoice->tax_name = $recurInvoice->tax_name; $invoice->tax_name1 = $recurInvoice->tax_name1;
$invoice->tax_rate = $recurInvoice->tax_rate; $invoice->tax_rate1 = $recurInvoice->tax_rate1;
$invoice->tax_name2 = $recurInvoice->tax_name2;
$invoice->tax_rate2 = $recurInvoice->tax_rate2;
$invoice->invoice_design_id = $recurInvoice->invoice_design_id; $invoice->invoice_design_id = $recurInvoice->invoice_design_id;
$invoice->custom_value1 = $recurInvoice->custom_value1 ?: 0; $invoice->custom_value1 = $recurInvoice->custom_value1 ?: 0;
$invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0; $invoice->custom_value2 = $recurInvoice->custom_value2 ?: 0;
@ -709,8 +724,10 @@ class InvoiceRepository extends BaseRepository
$item->cost = $recurItem->cost; $item->cost = $recurItem->cost;
$item->notes = Utils::processVariables($recurItem->notes); $item->notes = Utils::processVariables($recurItem->notes);
$item->product_key = Utils::processVariables($recurItem->product_key); $item->product_key = Utils::processVariables($recurItem->product_key);
$item->tax_name = $recurItem->tax_name; $item->tax_name1 = $recurItem->tax_name1;
$item->tax_rate = $recurItem->tax_rate; $item->tax_rate1 = $recurItem->tax_rate1;
$item->tax_name2 = $recurItem->tax_name2;
$item->tax_rate2 = $recurItem->tax_rate2;
$invoice->invoice_items()->save($item); $invoice->invoice_items()->save($item);
} }

View File

@ -19,8 +19,10 @@ class InvoiceItemTransformer extends EntityTransformer
'notes' => $item->notes, 'notes' => $item->notes,
'cost' => (float) $item->cost, 'cost' => (float) $item->cost,
'qty' => (float) $item->qty, 'qty' => (float) $item->qty,
'tax_name' => $item->tax_name, 'tax_name1' => $item->tax_name1,
'tax_rate' => (float) $item->tax_rate 'tax_rate1' => (float) $item->tax_rate1,
'tax_name2' => $item->tax_name2,
'tax_rate2' => (float) $item->tax_rate2,
]; ];
} }
} }

View File

@ -87,8 +87,10 @@ class InvoiceTransformer extends EntityTransformer
'end_date' => $invoice->end_date, 'end_date' => $invoice->end_date,
'last_sent_date' => $invoice->last_sent_date, 'last_sent_date' => $invoice->last_sent_date,
'recurring_invoice_id' => (int) $invoice->recurring_invoice_id, 'recurring_invoice_id' => (int) $invoice->recurring_invoice_id,
'tax_name' => $invoice->tax_name, 'tax_name1' => $invoice->tax_name1,
'tax_rate' => (float) $invoice->tax_rate, 'tax_rate1' => (float) $invoice->tax_rate1,
'tax_name2' => $invoice->tax_name2,
'tax_rate2' => (float) $invoice->tax_rate2,
'amount' => (float) $invoice->amount, 'amount' => (float) $invoice->amount,
'balance' => (float) $invoice->balance, 'balance' => (float) $invoice->balance,
'is_amount_discount' => (bool) $invoice->is_amount_discount, 'is_amount_discount' => (bool) $invoice->is_amount_discount,

View File

@ -19,6 +19,20 @@ class SupportMultipleTaxRates extends Migration
Schema::table('invoice_items', function($table) { Schema::table('invoice_items', function($table) {
$table->decimal('tax_rate', 13, 3)->change(); $table->decimal('tax_rate', 13, 3)->change();
}); });
Schema::table('invoices', function($table) {
$table->renameColumn('tax_rate', 'tax_rate1');
$table->renameColumn('tax_name', 'tax_name1');
$table->string('tax_name2')->nullable();
$table->decimal('tax_rate2', 13, 3);
});
Schema::table('invoice_items', function($table) {
$table->renameColumn('tax_rate', 'tax_rate1');
$table->renameColumn('tax_name', 'tax_name1');
$table->string('tax_name2')->nullable();
$table->decimal('tax_rate2', 13, 3);
});
} }
/** /**
* Reverse the migrations. * Reverse the migrations.
@ -28,11 +42,19 @@ class SupportMultipleTaxRates extends Migration
public function down() public function down()
{ {
Schema::table('invoices', function($table) { Schema::table('invoices', function($table) {
$table->decimal('tax_rate', 13, 2)->change(); $table->decimal('tax_rate1', 13, 2)->change();
$table->renameColumn('tax_rate1', 'tax_rate');
$table->renameColumn('tax_name1', 'tax_name');
$table->dropColumn('tax_name2');
$table->dropColumn('tax_rate2');
}); });
Schema::table('invoice_items', function($table) { Schema::table('invoice_items', function($table) {
$table->decimal('tax_rate', 13, 2)->change(); $table->decimal('tax_rate1', 13, 2)->change();
$table->renameColumn('tax_rate1', 'tax_rate');
$table->renameColumn('tax_name1', 'tax_name');
$table->dropColumn('tax_name2');
$table->dropColumn('tax_rate2');
}); });
} }
} }

View File

@ -30497,8 +30497,10 @@ function calculateAmounts(invoice) {
for (var i=0; i<invoice.invoice_items.length; i++) { for (var i=0; i<invoice.invoice_items.length; i++) {
var item = invoice.invoice_items[i]; var item = invoice.invoice_items[i];
var taxRate = 0; var taxRate1 = 0;
var taxName = ''; var taxName1 = '';
var taxRate2 = 0;
var taxName2 = '';
if (item.product_key) { if (item.product_key) {
invoice.has_product_key = true; invoice.has_product_key = true;
@ -30506,9 +30508,14 @@ function calculateAmounts(invoice) {
invoice.has_product_key = true; invoice.has_product_key = true;
} }
if (item.tax_rate && parseFloat(item.tax_rate)) { if (item.tax_rate1 && parseFloat(item.tax_rate1)) {
taxRate = parseFloat(item.tax_rate); taxRate1 = parseFloat(item.tax_rate1);
taxName = item.tax_name; taxName1 = item.tax_name1;
}
if (item.tax_rate2 && parseFloat(item.tax_rate2)) {
taxRate2 = parseFloat(item.tax_rate2);
taxName2 = item.tax_name2;
} }
// calculate line item tax // calculate line item tax
@ -30520,18 +30527,28 @@ function calculateAmounts(invoice) {
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100)); lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
} }
} }
var taxAmount = roundToTwo(lineTotal * taxRate / 100);
if (taxAmount) { var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
var key = taxName + taxRate; if (taxAmount1) {
var key = taxName1 + taxRate1;
if (taxes.hasOwnProperty(key)) { if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount; taxes[key].amount += taxAmount1;
} else { } else {
taxes[key] = {name: taxName, rate:taxRate, amount:taxAmount}; taxes[key] = {name: taxName1, rate:taxRate1, amount:taxAmount1};
} }
} }
if (item.tax_name) { var taxAmount2 = roundToTwo(lineTotal * taxRate2 / 100);
if (taxAmount2) {
var key = taxName2 + taxRate2;
if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount2;
} else {
taxes[key] = {name: taxName2, rate:taxRate2, amount:taxAmount2};
}
}
if (item.tax_name1 || item.tax_name2) {
hasTaxes = true; hasTaxes = true;
} }
} }
@ -30556,15 +30573,17 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2); total += roundToTwo(invoice.custom_value2);
} }
var tax = 0; taxRate1 = 0;
if (invoice.tax_rate && parseFloat(invoice.tax_rate)) { taxRate2 = 0;
tax = parseFloat(invoice.tax_rate); if (invoice.tax_rate1 && parseFloat(invoice.tax_rate1)) {
taxRate1 = parseFloat(invoice.tax_rate1);
} }
if (invoice.tax_rate2 && parseFloat(invoice.tax_rate2)) {
if (tax) { taxRate2 = parseFloat(invoice.tax_rate2);
var tax = roundToTwo(total * (tax/100));
total = parseFloat(total) + parseFloat(tax);
} }
taxAmount1 = roundToTwo(total * (taxRate1/100));
taxAmount2 = roundToTwo(total * (taxRate2/100));
total = total + taxAmount1 + taxAmount2;
for (var key in taxes) { for (var key in taxes) {
if (taxes.hasOwnProperty(key)) { if (taxes.hasOwnProperty(key)) {
@ -30582,7 +30601,8 @@ function calculateAmounts(invoice) {
invoice.total_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.discount_amount = discount;
invoice.tax_amount = tax; invoice.tax_amount1 = taxAmount1;
invoice.tax_amount2 = taxAmount2;
invoice.item_taxes = taxes; invoice.item_taxes = taxes;
if (NINJA.parseFloat(invoice.partial)) { if (NINJA.parseFloat(invoice.partial)) {
@ -30594,14 +30614,6 @@ function calculateAmounts(invoice) {
return invoice; return invoice;
} }
function getInvoiceTaxRate(invoice) {
var tax = 0;
if (invoice.tax_rate && parseFloat(invoice.tax_rate)) {
tax = parseFloat(invoice.tax_rate);
}
return tax;
}
// http://stackoverflow.com/questions/11941876/correctly-suppressing-warnings-in-datatables // http://stackoverflow.com/questions/11941876/correctly-suppressing-warnings-in-datatables
window.alert = (function() { window.alert = (function() {
var nativeAlert = window.alert; var nativeAlert = window.alert;
@ -31337,13 +31349,15 @@ NINJA.invoiceLines = function(invoice) {
var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : ''; var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : '';
var notes = item.notes; var notes = item.notes;
var productKey = item.product_key; var productKey = item.product_key;
var tax = ''; var tax1 = '';
var tax2 = '';
if (showItemTaxes) { if (showItemTaxes) {
if (item.tax && parseFloat(item.tax.rate)) { if (item.tax_name1) {
tax = parseFloat(item.tax.rate); tax1 = parseFloat(item.tax_rate1);
} else if (item.tax_rate && parseFloat(item.tax_rate)) { }
tax = parseFloat(item.tax_rate); if (item.tax_name2) {
tax2 = parseFloat(item.tax_rate2);
} }
} }
@ -31380,7 +31394,14 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '}); row.push({style:["quantity", rowStyle], text:qty || ' '});
} }
if (showItemTaxes) { if (showItemTaxes) {
row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '}); var str = '';
if (tax1) {
str += tax1.toString() + '% ';
}
if (tax2) {
str += tax2.toString() + '% ';
}
row.push({style:["tax", rowStyle], text:str});
} }
row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});
@ -31452,9 +31473,13 @@ NINJA.subtotals = function(invoice, hideBalance)
} }
} }
if (invoice.tax_amount) { if (invoice.tax_amount1) {
var taxStr = invoice.tax_name + ' ' + (invoice.tax_rate*1).toString() + '%'; var taxStr = invoice.tax_name1 + ' ' + (invoice.tax_rate1*1).toString() + '%';
data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount, invoice)}]); data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount1, invoice)}]);
}
if (invoice.tax_amount2) {
var taxStr = invoice.tax_name2 + ' ' + (invoice.tax_rate2*1).toString() + '%';
data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount2, invoice)}]);
} }
if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') { if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') {

View File

@ -350,13 +350,15 @@ NINJA.invoiceLines = function(invoice) {
var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : ''; var qty = NINJA.parseFloat(item.qty) ? roundToTwo(NINJA.parseFloat(item.qty)) + '' : '';
var notes = item.notes; var notes = item.notes;
var productKey = item.product_key; var productKey = item.product_key;
var tax = ''; var tax1 = '';
var tax2 = '';
if (showItemTaxes) { if (showItemTaxes) {
if (item.tax && parseFloat(item.tax.rate)) { if (item.tax_name1) {
tax = parseFloat(item.tax.rate); tax1 = parseFloat(item.tax_rate1);
} else if (item.tax_rate && parseFloat(item.tax_rate)) { }
tax = parseFloat(item.tax_rate); if (item.tax_name2) {
tax2 = parseFloat(item.tax_rate2);
} }
} }
@ -393,7 +395,14 @@ NINJA.invoiceLines = function(invoice) {
row.push({style:["quantity", rowStyle], text:qty || ' '}); row.push({style:["quantity", rowStyle], text:qty || ' '});
} }
if (showItemTaxes) { if (showItemTaxes) {
row.push({style:["tax", rowStyle], text:tax ? (tax.toString() + '%') : ' '}); var str = '';
if (tax1) {
str += tax1.toString() + '% ';
}
if (tax2) {
str += tax2.toString() + '% ';
}
row.push({style:["tax", rowStyle], text:str});
} }
row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '}); row.push({style:["lineTotal", rowStyle], text:lineTotal || ' '});
@ -465,9 +474,13 @@ NINJA.subtotals = function(invoice, hideBalance)
} }
} }
if (invoice.tax_amount) { if (invoice.tax_amount1) {
var taxStr = invoice.tax_name + ' ' + (invoice.tax_rate*1).toString() + '%'; var taxStr = invoice.tax_name1 + ' ' + (invoice.tax_rate1*1).toString() + '%';
data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount, invoice)}]); data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount1, invoice)}]);
}
if (invoice.tax_amount2) {
var taxStr = invoice.tax_name2 + ' ' + (invoice.tax_rate2*1).toString() + '%';
data.push([{text: taxStr}, {text: formatMoneyInvoice(invoice.tax_amount2, invoice)}]);
} }
if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') { if (NINJA.parseFloat(invoice.custom_value1) && invoice.custom_taxes1 != '1') {

View File

@ -603,8 +603,10 @@ function calculateAmounts(invoice) {
for (var i=0; i<invoice.invoice_items.length; i++) { for (var i=0; i<invoice.invoice_items.length; i++) {
var item = invoice.invoice_items[i]; var item = invoice.invoice_items[i];
var taxRate = 0; var taxRate1 = 0;
var taxName = ''; var taxName1 = '';
var taxRate2 = 0;
var taxName2 = '';
if (item.product_key) { if (item.product_key) {
invoice.has_product_key = true; invoice.has_product_key = true;
@ -612,9 +614,14 @@ function calculateAmounts(invoice) {
invoice.has_product_key = true; invoice.has_product_key = true;
} }
if (item.tax_rate && parseFloat(item.tax_rate)) { if (item.tax_rate1 && parseFloat(item.tax_rate1)) {
taxRate = parseFloat(item.tax_rate); taxRate1 = parseFloat(item.tax_rate1);
taxName = item.tax_name; taxName1 = item.tax_name1;
}
if (item.tax_rate2 && parseFloat(item.tax_rate2)) {
taxRate2 = parseFloat(item.tax_rate2);
taxName2 = item.tax_name2;
} }
// calculate line item tax // calculate line item tax
@ -626,18 +633,28 @@ function calculateAmounts(invoice) {
lineTotal -= roundToTwo(lineTotal * (invoice.discount/100)); lineTotal -= roundToTwo(lineTotal * (invoice.discount/100));
} }
} }
var taxAmount = roundToTwo(lineTotal * taxRate / 100);
if (taxAmount) { var taxAmount1 = roundToTwo(lineTotal * taxRate1 / 100);
var key = taxName + taxRate; if (taxAmount1) {
var key = taxName1 + taxRate1;
if (taxes.hasOwnProperty(key)) { if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount; taxes[key].amount += taxAmount1;
} else { } else {
taxes[key] = {name: taxName, rate:taxRate, amount:taxAmount}; taxes[key] = {name: taxName1, rate:taxRate1, amount:taxAmount1};
} }
} }
if (item.tax_name) { var taxAmount2 = roundToTwo(lineTotal * taxRate2 / 100);
if (taxAmount2) {
var key = taxName2 + taxRate2;
if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount2;
} else {
taxes[key] = {name: taxName2, rate:taxRate2, amount:taxAmount2};
}
}
if (item.tax_name1 || item.tax_name2) {
hasTaxes = true; hasTaxes = true;
} }
} }
@ -662,15 +679,17 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2); total += roundToTwo(invoice.custom_value2);
} }
var tax = 0; taxRate1 = 0;
if (invoice.tax_rate && parseFloat(invoice.tax_rate)) { taxRate2 = 0;
tax = parseFloat(invoice.tax_rate); if (invoice.tax_rate1 && parseFloat(invoice.tax_rate1)) {
taxRate1 = parseFloat(invoice.tax_rate1);
} }
if (invoice.tax_rate2 && parseFloat(invoice.tax_rate2)) {
if (tax) { taxRate2 = parseFloat(invoice.tax_rate2);
var tax = roundToTwo(total * (tax/100));
total = parseFloat(total) + parseFloat(tax);
} }
taxAmount1 = roundToTwo(total * (taxRate1/100));
taxAmount2 = roundToTwo(total * (taxRate2/100));
total = total + taxAmount1 + taxAmount2;
for (var key in taxes) { for (var key in taxes) {
if (taxes.hasOwnProperty(key)) { if (taxes.hasOwnProperty(key)) {
@ -688,7 +707,8 @@ function calculateAmounts(invoice) {
invoice.total_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.discount_amount = discount;
invoice.tax_amount = tax; invoice.tax_amount1 = taxAmount1;
invoice.tax_amount2 = taxAmount2;
invoice.item_taxes = taxes; invoice.item_taxes = taxes;
if (NINJA.parseFloat(invoice.partial)) { if (NINJA.parseFloat(invoice.partial)) {
@ -700,14 +720,6 @@ function calculateAmounts(invoice) {
return invoice; return invoice;
} }
function getInvoiceTaxRate(invoice) {
var tax = 0;
if (invoice.tax_rate && parseFloat(invoice.tax_rate)) {
tax = parseFloat(invoice.tax_rate);
}
return tax;
}
// http://stackoverflow.com/questions/11941876/correctly-suppressing-warnings-in-datatables // http://stackoverflow.com/questions/11941876/correctly-suppressing-warnings-in-datatables
window.alert = (function() { window.alert = (function() {
var nativeAlert = window.alert; var nativeAlert = window.alert;

View File

@ -17,6 +17,11 @@
label.control-label[for=invoice_number] { label.control-label[for=invoice_number] {
font-weight: normal !important; font-weight: normal !important;
} }
select.tax-select {
width: 50%;
float: left;
}
</style> </style>
@stop @stop
@ -204,7 +209,7 @@
@endif @endif
<th style="min-width:120px" data-bind="text: costLabel">{{ $invoiceLabels['unit_cost'] }}</th> <th style="min-width:120px" data-bind="text: costLabel">{{ $invoiceLabels['unit_cost'] }}</th>
<th style="{{ $account->hide_quantity ? 'display:none' : 'min-width:120px' }}" data-bind="text: qtyLabel">{{ $invoiceLabels['quantity'] }}</th> <th style="{{ $account->hide_quantity ? 'display:none' : 'min-width:120px' }}" data-bind="text: qtyLabel">{{ $invoiceLabels['quantity'] }}</th>
<th style="min-width:120px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th> <th style="min-width:180px;display:none;" data-bind="visible: $root.invoice_item_taxes.show">{{ trans('texts.tax') }}</th>
<th style="min-width:120px;">{{ trans('texts.line_total') }}</th> <th style="min-width:120px;">{{ trans('texts.line_total') }}</th>
<th style="min-width:32px;" class="hide-border"></th> <th style="min-width:32px;" class="hide-border"></th>
</tr> </tr>
@ -246,10 +251,19 @@
{!! Former::select('') {!! Former::select('')
->addOption('', '') ->addOption('', '')
->options($taxRateOptions) ->options($taxRateOptions)
->data_bind('value: tax') ->data_bind('value: tax1')
->addClass('tax-select')
->raw() !!} ->raw() !!}
<input type="text" data-bind="value: tax_name, attr: {name: 'invoice_items[' + $index() + '][tax_name]'}" style="display:none"> <input type="text" data-bind="value: tax_name1, attr: {name: 'invoice_items[' + $index() + '][tax_name1]'}" style="display:none">
<input type="text" data-bind="value: tax_rate, attr: {name: 'invoice_items[' + $index() + '][tax_rate]'}" style="display:none"> <input type="text" data-bind="value: tax_rate1, attr: {name: 'invoice_items[' + $index() + '][tax_rate1]'}" style="display:none">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax2')
->addClass('tax-select')
->raw() !!}
<input type="text" data-bind="value: tax_name2, attr: {name: 'invoice_items[' + $index() + '][tax_name2]'}" style="display:none">
<input type="text" data-bind="value: tax_rate2, attr: {name: 'invoice_items[' + $index() + '][tax_rate2]'}" style="display:none">
</td> </td>
<td style="text-align:right;padding-top:9px !important" nowrap> <td style="text-align:right;padding-top:9px !important" nowrap>
<div class="line-total" data-bind="text: totals.total"></div> <div class="line-total" data-bind="text: totals.total"></div>
@ -387,10 +401,19 @@
{!! Former::select('') {!! Former::select('')
->addOption('', '') ->addOption('', '')
->options($taxRateOptions) ->options($taxRateOptions)
->data_bind('value: tax') ->addClass('tax-select')
->data_bind('value: tax1')
->raw() !!} ->raw() !!}
<input type="text" name="tax_name" data-bind="value: tax_name" style="display:none"> <input type="text" name="tax_name1" data-bind="value: tax_name1" style="display:none">
<input type="text" name="tax_rate" data-bind="value: tax_rate" style="display:none"> <input type="text" name="tax_rate1" data-bind="value: tax_rate1" style="display:none">
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->data_bind('value: tax2')
->raw() !!}
<input type="text" name="tax_name2" data-bind="value: tax_name2" style="display:none">
<input type="text" name="tax_rate2" data-bind="value: tax_rate2" style="display:none">
</td> </td>
<td style="text-align: right"><span data-bind="text: totals.taxAmount"/></td> <td style="text-align: right"><span data-bind="text: totals.taxAmount"/></td>
</tr> </tr>
@ -770,8 +793,8 @@
// set the default account tax rate // set the default account tax rate
@if ($account->invoice_taxes && ! empty($defaultTax)) @if ($account->invoice_taxes && ! empty($defaultTax))
var defaultTax = {!! $defaultTax !!}; var defaultTax = {!! $defaultTax !!};
model.invoice().tax_rate(defaultTax.rate); model.invoice().tax_rate1(defaultTax.rate);
model.invoice().tax_name(defaultTax.name); model.invoice().tax_name1(defaultTax.name);
@endif @endif
@endif @endif

View File

@ -52,10 +52,11 @@ function ViewModel(data) {
} }
self.invoice_taxes.show = ko.computed(function() { self.invoice_taxes.show = ko.computed(function() {
if (self.invoice_taxes()) { if (self.invoice().tax_name1() || self.invoice().tax_name2()) {
return true; return true;
} }
return false;
return self.invoice_taxes() && {{ count($taxRateOptions) ? 'true' : 'false' }};
}); });
self.invoice_item_taxes.show = ko.computed(function() { self.invoice_item_taxes.show = ko.computed(function() {
@ -64,7 +65,7 @@ function ViewModel(data) {
} }
for (var i=0; i<self.invoice().invoice_items().length; i++) { for (var i=0; i<self.invoice().invoice_items().length; i++) {
var item = self.invoice().invoice_items()[i]; var item = self.invoice().invoice_items()[i];
if (item.tax_rate() > 0) { if (item.tax_name1() || item.tax_name2()) {
return true; return true;
} }
} }
@ -175,8 +176,10 @@ function InvoiceModel(data) {
self.start_date = ko.observable(''); self.start_date = ko.observable('');
self.end_date = ko.observable(''); self.end_date = ko.observable('');
self.last_sent_date = ko.observable(''); self.last_sent_date = ko.observable('');
self.tax_name = ko.observable(); self.tax_name1 = ko.observable();
self.tax_rate = ko.observable(); self.tax_rate1 = ko.observable();
self.tax_name2 = ko.observable();
self.tax_rate2 = ko.observable();
self.is_recurring = ko.observable(0); self.is_recurring = ko.observable(0);
self.is_quote = ko.observable({{ $entityType == ENTITY_QUOTE ? '1' : '0' }}); self.is_quote = ko.observable({{ $entityType == ENTITY_QUOTE ? '1' : '0' }});
self.auto_bill = ko.observable(); self.auto_bill = ko.observable();
@ -258,15 +261,27 @@ function InvoiceModel(data) {
return self.has_tasks() ? invoiceLabels['rate'] : invoiceLabels['unit_cost']; return self.has_tasks() ? invoiceLabels['rate'] : invoiceLabels['unit_cost'];
}, this); }, this);
this.tax = ko.computed({ this.tax1 = ko.computed({
read: function () { read: function () {
return self.tax_rate() + ' ' + self.tax_name(); return self.tax_rate1() + ' ' + self.tax_name1();
}, },
write: function(value) { write: function(value) {
var rate = value.substr(0, value.indexOf(' ')); var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1); var name = value.substr(value.indexOf(' ') + 1);
self.tax_name(name); self.tax_name1(name);
self.tax_rate(rate); self.tax_rate1(rate);
}
})
this.tax2 = ko.computed({
read: function () {
return self.tax_rate2() + ' ' + self.tax_name2();
},
write: function(value) {
var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1);
self.tax_name2(name);
self.tax_rate2(rate);
} }
}) })
@ -359,15 +374,13 @@ function InvoiceModel(data) {
total = NINJA.parseFloat(total) + customValue2; total = NINJA.parseFloat(total) + customValue2;
} }
var taxRate = parseFloat(self.tax_rate()); var taxRate1 = parseFloat(self.tax_rate1());
//if (taxRate > 0) { var tax1 = roundToTwo(total * (taxRate1/100));
// var tax = roundToTwo(total * (taxRate/100));
// return self.formatMoney(tax); var taxRate2 = parseFloat(self.tax_rate2());
//} else { var tax2 = roundToTwo(total * (taxRate2/100));
// return self.formatMoney(0);
//} return self.formatMoney(tax1 + tax2);
var tax = roundToTwo(total * (taxRate/100));
return self.formatMoney(tax);
}); });
self.totals.itemTaxes = ko.computed(function() { self.totals.itemTaxes = ko.computed(function() {
@ -383,13 +396,24 @@ function InvoiceModel(data) {
lineTotal -= roundToTwo(lineTotal * (self.discount()/100)); lineTotal -= roundToTwo(lineTotal * (self.discount()/100));
} }
} }
var taxAmount = roundToTwo(lineTotal * item.tax_rate() / 100);
var taxAmount = roundToTwo(lineTotal * item.tax_rate1() / 100);
if (taxAmount) { if (taxAmount) {
var key = item.tax_name() + item.tax_rate(); var key = item.tax_name1() + item.tax_rate1();
if (taxes.hasOwnProperty(key)) { if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount; taxes[key].amount += taxAmount;
} else { } else {
taxes[key] = {name:item.tax_name(), rate:item.tax_rate(), amount:taxAmount}; taxes[key] = {name:item.tax_name1(), rate:item.tax_rate1(), amount:taxAmount};
}
}
var taxAmount = roundToTwo(lineTotal * item.tax_rate2() / 100);
if (taxAmount) {
var key = item.tax_name2() + item.tax_rate2();
if (taxes.hasOwnProperty(key)) {
taxes[key].amount += taxAmount;
} else {
taxes[key] = {name:item.tax_name2(), rate:item.tax_rate2(), amount:taxAmount};
} }
} }
} }
@ -455,8 +479,9 @@ function InvoiceModel(data) {
total = NINJA.parseFloat(total) + customValue2; total = NINJA.parseFloat(total) + customValue2;
} }
var taxRate = parseFloat(self.tax_rate()); var taxAmount1 = roundToTwo(total * (parseFloat(self.tax_rate1())/100));
total = NINJA.parseFloat(total) + roundToTwo(total * (taxRate/100)); var taxAmount2 = roundToTwo(total * (parseFloat(self.tax_rate2())/100));
total = NINJA.parseFloat(total) + taxAmount1 + taxAmount2;
total = roundToTwo(total); total = roundToTwo(total);
var taxes = self.totals.itemTaxes(); var taxes = self.totals.itemTaxes();
@ -641,21 +666,35 @@ function ItemModel(data) {
self.qty = ko.observable(0); self.qty = ko.observable(0);
self.custom_value1 = ko.observable(''); self.custom_value1 = ko.observable('');
self.custom_value2 = ko.observable(''); self.custom_value2 = ko.observable('');
self.tax_name = ko.observable(''); self.tax_name1 = ko.observable('');
self.tax_rate = ko.observable(0); self.tax_rate1 = ko.observable(0);
self.tax_name2 = ko.observable('');
self.tax_rate2 = ko.observable(0);
self.task_public_id = ko.observable(''); self.task_public_id = ko.observable('');
self.expense_public_id = ko.observable(''); self.expense_public_id = ko.observable('');
self.actionsVisible = ko.observable(false); self.actionsVisible = ko.observable(false);
this.tax = ko.computed({ this.tax1 = ko.computed({
read: function () { read: function () {
return self.tax_rate() + ' ' + self.tax_name(); return self.tax_rate1() + ' ' + self.tax_name1();
}, },
write: function(value) { write: function(value) {
var rate = value.substr(0, value.indexOf(' ')); var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1); var name = value.substr(value.indexOf(' ') + 1);
self.tax_name(name); self.tax_name1(name);
self.tax_rate(rate); self.tax_rate1(rate);
}
})
this.tax2 = ko.computed({
read: function () {
return self.tax_rate2() + ' ' + self.tax_name2();
},
write: function(value) {
var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1);
self.tax_name2(name);
self.tax_rate2(rate);
} }
}) })
@ -794,9 +833,9 @@ ko.bindingHandlers.typeahead = {
} }
@if ($account->invoice_item_taxes) @if ($account->invoice_item_taxes)
if (datum.default_tax_rate) { if (datum.default_tax_rate) {
model.tax_rate(datum.default_tax_rate.rate); model.tax_rate1(datum.default_tax_rate.rate);
model.tax_name(datum.default_tax_rate.name); model.tax_name1(datum.default_tax_rate.name);
model.tax(datum.default_tax_rate.rate + ' ' + datum.default_tax_rate.name); model.tax1(datum.default_tax_rate.rate + ' ' + datum.default_tax_rate.name);
} }
@endif @endif
@endif @endif