1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 00:11:35 +02:00

Working on taxes

This commit is contained in:
Hillel Coren 2013-12-31 21:49:54 +02:00
parent 7a90856025
commit 309442cab3
8 changed files with 212 additions and 61 deletions

View File

@ -134,7 +134,7 @@ class InvoiceController extends \BaseController {
return View::make('invoices.deleted');
}
if ($invoice->invoice_status_id < INVOICE_STATUS_VIEWED)
if (!$invoice->isViewed())
{
$invoice->invoice_status_id = INVOICE_STATUS_VIEWED;
$invoice->save();
@ -241,15 +241,13 @@ class InvoiceController extends \BaseController {
}
else
{
return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>');
}
}
catch (\Exception $e)
{
exit('Sorry, there was an error processing your payment. Please try again later.<p>'.$e);
return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>'.$e);
}
exit;
}
public function do_payment()
@ -274,9 +272,12 @@ class InvoiceController extends \BaseController {
$payment->transaction_reference = $ref;
$payment->save();
if ($payment->amount >= $invoice->amount) {
if ($payment->amount >= $invoice->amount)
{
$invoice->invoice_status_id = INVOICE_STATUS_PAID;
} else {
}
else
{
$invoice->invoice_status_id = INVOICE_STATUS_PARTIAL;
}
$invoice->save();
@ -286,12 +287,12 @@ class InvoiceController extends \BaseController {
}
else
{
exit($response->getMessage());
return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>'.$response->getMessage());
}
}
catch (\Exception $e)
{
exit('Sorry, there was an error processing your payment. Please try again later.' . $e);
return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.<p>'.$e);
}
}
@ -404,11 +405,16 @@ class InvoiceController extends \BaseController {
$invoiceData['client_id'] = $client->id;
$invoice = $this->invoiceRepo->save($publicId, $invoiceData);
$account = Auth::user()->account;
if ($account->invoice_taxes != $input->invoice_taxes || $account->invoice_item_taxes != $input->invoice_item_taxes)
{
$account->invoice_taxes = $input->invoice_taxes;
$account->invoice_item_taxes = $input->invoice_item_taxes;
$account->save();
}
if ($action == 'email' && $invoice->invoice_status_id == INVOICE_STATUS_DRAFT)
{
$invoice->invoice_status_id = INVOICE_STATUS_SENT;
$invoice->save();
$client->balance = $client->balance + $invoice->amount;
$client->save();
}

View File

@ -125,6 +125,9 @@ class ConfideSetupUsersTable extends Migration {
$t->unsignedInteger('country_id')->nullable();
$t->text('invoice_terms');
$t->boolean('invoice_taxes')->default(true);
$t->boolean('invoice_item_taxes')->default(false);
$t->foreign('timezone_id')->references('id')->on('timezones');
$t->foreign('date_format_id')->references('id')->on('date_formats');
$t->foreign('datetime_format_id')->references('id')->on('datetime_formats');
@ -303,6 +306,9 @@ class ConfideSetupUsersTable extends Migration {
$t->timestamp('last_sent_date')->nullable();
$t->unsignedInteger('recurring_invoice_id')->index()->nullable();
$t->string('tax_name');
$t->decimal('tax_rate', 13, 4);
$t->decimal('amount', 13, 4);
$t->decimal('balance', 13, 4);

View File

@ -2,6 +2,12 @@
class Utils
{
public static function fatalError($error)
{
Log::error($error);
return View::make('error')->with('error', $error);
}
public static function formatPhoneNumber($phoneNumber)
{
$phoneNumber = preg_replace('/[^0-9]/','',$phoneNumber);

View File

@ -42,9 +42,15 @@ class Invoice extends EntityModel
return $this->invoice_status_id >= INVOICE_STATUS_SENT;
}
public function isViewed()
{
return $this->invoice_status_id >= INVOICE_STATUS_VIEWED;
}
public function hidePrivateFields()
{
$this->setVisible(['invoice_number', 'discount', 'po_number', 'invoice_date', 'due_date', 'terms', 'currency_id', 'public_notes', 'amount', 'balance', 'invoice_items', 'client']);
$this->setVisible(['invoice_number', 'discount', 'po_number', 'invoice_date', 'due_date', 'terms', 'currency_id', 'public_notes', 'amount', 'balance', 'invoice_items', 'client', 'tax_name', 'tax_rate']);
$this->client->setVisible(['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'work_phone', 'payment_terms', 'contacts']);
foreach ($this->invoice_items as $invoiceItem)

View File

@ -80,7 +80,7 @@ class InvoiceRepository
}
$invoice->client_id = $data['client_id'];
$invoice->discount = $data['discount'];
$invoice->discount = floatval($data['discount']);
$invoice->invoice_number = trim($data['invoice_number']);
$invoice->invoice_date = Utils::toSqlDate($data['invoice_date']);
$invoice->due_date = Utils::toSqlDate($data['due_date']);
@ -93,30 +93,19 @@ class InvoiceRepository
$invoice->public_notes = trim($data['public_notes']);
$invoice->po_number = trim($data['po_number']);
$invoice->currency_id = $data['currency_id'];
$invoice->tax_rate = 0;
if (isset($data['tax']) && isset($data['tax']->rate) && floatval($data['tax']->rate) > 0)
{
$invoice->tax_rate = floatval($data['tax']->rate);
$invoice->tax_name = trim($data['tax']->name);
}
$invoice->save();
$invoice->invoice_items()->forceDelete();
$total = 0;
foreach ($data['invoice_items'] as $item)
{
if (!isset($item->cost))
{
$item->cost = 0;
}
if (!isset($item->qty))
{
$item->qty = 0;
}
$total += floatval($item->qty) * floatval($item->cost);
}
$invoice->amount = $total;
$invoice->balance = $total;
$invoice->save();
$invoice->invoice_items()->forceDelete();
foreach ($data['invoice_items'] as $item)
{
if (!$item->cost && !$item->qty && !$item->product_key && !$item->notes)
@ -149,17 +138,31 @@ class InvoiceRepository
$invoiceItem->notes = trim($item->notes);
$invoiceItem->cost = floatval($item->cost);
$invoiceItem->qty = floatval($item->qty);
$invoiceItem->tax_rate = 0;
if ($item->tax && isset($item->tax->rate) && isset($item->tax->name))
if ($item->tax && isset($item->tax->rate) && floatval($item->tax->rate) > 0)
{
$invoiceItem->tax_rate = floatval($item->tax->rate);
$invoiceItem->tax_name = trim($item->tax->name);
}
$invoice->invoice_items()->save($invoiceItem);
$total += floatval($item->qty) * floatval($item->cost);
$lineTotal = $invoiceItem->cost * $invoiceItem->qty;
$total += $lineTotal + ($lineTotal * $invoiceItem->tax_rate / 100);
}
if ($invoice->discount > 0)
{
$total *= (100 - $invoice->discount) / 100;
}
$total += $total * $invoice->tax_rate / 100;
$invoice->amount = $total;
$invoice->balance = $total;
$invoice->save();
return $invoice;
}
}

7
app/views/error.blade.php Executable file
View File

@ -0,0 +1,7 @@
@extends('header')
@section('content')
{{ $error }}
@stop

View File

@ -108,36 +108,36 @@
<th>Description</th>
<th>Unit Cost</th>
<th>Quantity</th>
<th data-bind="visible: tax_rates().length > 1">Tax</th>
<th data-bind="visible: showInvoiceItemTaxes">Tax</th>
<th>Line&nbsp;Total</th>
<th class="hide-border"></th>
</tr>
</thead>
<tbody data-bind="sortable: { data: invoice_items, afterMove: onDragged }">
<tr data-bind="event: { mouseover: showActions, mouseout: hideActions }" class="sortable-row">
<td style="width:20px;" class="hide-border td-icon">
<td style="min-width:20px;" class="hide-border td-icon">
<i data-bind="visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-sort"></i>
</td>
<td style="width:120px">
<td style="min-width:120px">
{{ Former::text('product_key')->useDatalist(Product::getProductKeys($products), 'key')->onkeyup('onItemChange()')
->raw()->data_bind("value: product_key, valueUpdate: 'afterkeydown'")->addClass('datalist') }}
</td>
<td style="width:300px">
<td style="width:100%">
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown'" rows="1" cols="60" style="resize: none;" class="form-control word-wrap"></textarea>
</td>
<td style="width:100px">
<td style="min-width:120px">
<input onkeyup="onItemChange()" data-bind="value: prettyCost, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control"//>
</td>
<td style="width:80px">
<td style="min-width:120px">
<input onkeyup="onItemChange()" data-bind="value: prettyQty, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control"//>
</td>
<td style="width:120px; vertical-align:middle" data-bind="visible: $parent.tax_rates().length > 1">
<td style="min-width:120px; vertical-align:middle" data-bind="visible: $parent.showInvoiceItemTaxes">
<select class="form-control" style="width:100%" data-bind="value: tax, options: $parent.tax_rates, optionsText: 'displayName'"></select>
</td>
<td style="width:100px;text-align: right;padding-top:9px !important">
<td style="min-width:120px;text-align: right;padding-top:9px !important">
<span data-bind="text: total"></span>
</td>
<td style="width:20px; cursor:pointer" class="hide-border td-icon">
<td style="min-width:20px; cursor:pointer" class="hide-border td-icon">
&nbsp;<i data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-minus-circle" title="Remove item"/>
</td>
</tr>
@ -145,22 +145,33 @@
<tfoot>
<tr>
<td class="hide-border"/>
<td data-bind="attr: {colspan: tax_rates().length > 1 ? 3 : 2}"/>
<td colspan="2"/>
<td data-bind="visible: showInvoiceItemTaxes"/>
<td colspan="2">Subtotal</td>
<td style="text-align: right"><span data-bind="text: subtotal"/></td>
</tr>
<tr data-bind="visible: discount() > 0">
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/>
<td colspan="2">Discount</td>
<td style="text-align: right"><span data-bind="text: discounted"/></td>
</tr>
<tr data-bind="visible: showInvoiceTaxes">
<td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/>
<td style="vertical-align: middle">Tax</td>
<td><select class="form-control" style="width:100%" data-bind="value: tax, options: tax_rates, optionsText: 'displayName'"></select></td>
<td style="vertical-align: middle; text-align: right"><span data-bind="text: taxAmount"/></td>
</tr>
<tr>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/>
<td colspan="2">Paid to Date</td>
<td style="text-align: right"></td>
</tr>
<tr>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/>
<td colspan="2"><b>Balance Due</b></td>
<td style="text-align: right"><span data-bind="text: total"/></td>
</tr>
@ -319,6 +330,12 @@
</tbody>
</table>
&nbsp;
{{ Former::checkbox('invoice_taxes')->text('Enable specifying an <b>invoice tax</b>')
->label('Settings')->data_bind('checked: invoice_taxes, enable: tax_rates().length > 1') }}
{{ Former::checkbox('invoice_item_taxes')->text('Enable specifying <b>line item taxes</b>')
->label('&nbsp;')->data_bind('checked: invoice_item_taxes, enable: tax_rates().length > 1') }}
</div>
<div class="modal-footer" style="margin-top: 0px">
@ -394,9 +411,24 @@
$('#taxModal').on('shown.bs.modal', function () {
$('#taxModal input:first').focus();
}).on('hidden.bs.modal', function () {
console.log('TAX HIDDEN: %s %s', model.invoice_taxes(), model.invoice_item_taxes())
/*
var blank = model.getBlankTaxRate();
if (!model.invoice_taxes()) {
model.tax(blank);
}
if (!model.invoice_item_taxes()) {
for (var i=0; i<model.invoice_items().length; i++) {
var item = model.invoice_items()[i];
item.tax(blank);
}
}
*/
/*
if (model.taxBackup) {
}
*/
})
$('#actionDropDown > button:first').click(function() {
@ -558,12 +590,16 @@
self.due_date = ko.observable('');
self.start_date = ko.observable('');
self.end_date = ko.observable('');
self.tax = ko.observable('');
self.is_recurring = ko.observable(false);
self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray();
self.tax_rates = ko.observableArray();
self.invoice_taxes = ko.observable({{ Auth::user()->account->invoice_taxes ? 'true' : 'false' }});
self.invoice_item_taxes = ko.observable({{ Auth::user()->account->invoice_item_taxes ? 'true' : 'false' }});
self.mapping = {
'invoice_items': {
create: function(options) {
@ -590,6 +626,30 @@
owner: this
});
self.showInvoiceTaxes = ko.computed(function() {
if (self.tax_rates().length > 1 && self.invoice_taxes()) {
return true;
}
if (self.tax() && self.tax().rate() > 0) {
return true;
}
return false;
});
self.showInvoiceItemTaxes = ko.computed(function() {
if (self.tax_rates().length > 1 && self.invoice_item_taxes()) {
return true;
}
for (var i=0; i<self.invoice_items().length; i++) {
var item = self.invoice_items()[i];
if (item.tax() && item.tax().rate() > 0) {
return true;
}
}
return false;
});
self.wrapped_notes = ko.computed({
read: function() {
$('#public_notes').height(this.public_notes().split('\n').length * 36);
@ -653,7 +713,7 @@
$('#emailError').css( "display", "none" );
//$('.client_select input.form-control').focus();
$('#terms').focus();
$('#invoice_number').focus();
refreshPDF();
model.clientBackup = false;
@ -682,6 +742,15 @@
applyComboboxListeners();
}
self.getBlankTaxRate = function() {
for (var i=0; i<self.tax_rates().length; i++) {
var taxRate = self.tax_rates()[i];
if (!taxRate.name()) {
return taxRate;
}
}
}
this.rawSubtotal = ko.computed(function() {
var total = 0;
for(var p = 0; p < self.invoice_items().length; ++p)
@ -696,12 +765,32 @@
return total > 0 ? formatMoney(total, self.currency_id()) : '';
});
this.rawDiscounted = ko.computed(function() {
return self.rawSubtotal() * (self.discount()/100);
});
this.discounted = ko.computed(function() {
var total = self.rawSubtotal() * (self.discount()/100);
return formatMoney(total, self.currency_id());
return formatMoney(self.rawDiscounted(), self.currency_id());
});
self.taxAmount = ko.computed(function() {
var total = self.rawSubtotal();
var discount = parseFloat(self.discount());
if (discount > 0) {
total = total * ((100 - discount)/100);
}
var taxRate = self.tax() ? parseFloat(self.tax().rate()) : 0;
if (taxRate > 0) {
var tax = total * (taxRate/100);
return formatMoney(tax, self.currency_id());
} else {
return formatMoney(0);
}
});
this.total = ko.computed(function() {
var total = self.rawSubtotal();
@ -710,6 +799,11 @@
total = total * ((100 - discount)/100);
}
var taxRate = self.tax() ? parseFloat(self.tax().rate()) : 0;
if (taxRate > 0) {
total = parseFloat(total) + (total * (taxRate/100));
}
return total > 0 ? formatMoney(total, self.currency_id()) : '';
});
@ -821,9 +915,9 @@
self.displayName = ko.computed(function() {
var name = self.name() ? self.name() : false;
var rate = self.rate() ? parseFloat(self.rate()) : false;
return (name && rate) ? (rate + '%' + ' - ' + name) : '';
var name = self.name() ? self.name() : '';
var rate = self.rate() ? parseFloat(self.rate()) + '% -' : '';
return rate + name;
});
self.hideActions = function() {

View File

@ -32,7 +32,7 @@ function generatePDF(invoice) {
for (var i=0; i<invoice.invoice_items.length; i++)
{
var item = invoice.invoice_items[i];
if (item.tax && item.tax.rate > 0) {
if ((item.tax && item.tax.rate > 0) || (item.tax_rate && parseFloat(item.tax_rate) > 0)) {
hasTaxes = true;
break;
}
@ -141,7 +141,12 @@ function generatePDF(invoice) {
var qty = item.qty ? parseFloat(item.qty) + '' : '';
var notes = item.notes;
var productKey = item.product_key;
var tax = item.tax && parseFloat(item.tax.rate) ? parseFloat(item.tax.rate) + '%' : false;
var tax = 0;
if (item.tax && parseFloat(item.tax.rate)) {
tax = parseFloat(item.tax.rate);
} else if (item.tax_rate && parseFloat(item.tax_rate)) {
tax = parseFloat(item.tax_rate);
}
// show at most one blank line
if (shownItem && (!cost || cost == '0.00') && !qty && !notes && !productKey) {
@ -155,7 +160,7 @@ function generatePDF(invoice) {
var lineTotal = item.cost * item.qty;
if (tax) {
lineTotal += lineTotal * parseFloat(item.tax.rate) / 100;
lineTotal += lineTotal * tax / 100;
}
if (lineTotal) {
total += lineTotal;
@ -164,7 +169,7 @@ function generatePDF(invoice) {
var costX = unitCostRight - (doc.getStringUnitWidth(cost) * doc.internal.getFontSize());
var qtyX = qtyRight - (doc.getStringUnitWidth(qty) * doc.internal.getFontSize());
var taxX = taxRight - (doc.getStringUnitWidth(tax) * doc.internal.getFontSize());
var taxX = taxRight - (doc.getStringUnitWidth(tax+'%') * doc.internal.getFontSize());
var totalX = lineTotalRight - (doc.getStringUnitWidth(lineTotal) * doc.internal.getFontSize());
var x = tableTop + (line * tableRowHeight) + 6;
if (i==0) x -= 4;
@ -176,7 +181,7 @@ function generatePDF(invoice) {
doc.text(totalX, x, lineTotal);
if (tax) {
doc.text(taxX, x, tax);
doc.text(taxX, x, tax+'%');
}
line += doc.splitTextToSize(item.notes, 200).length;
@ -206,12 +211,30 @@ function generatePDF(invoice) {
x += 16;
doc.text(footerLeft, x, 'Discount');
var discount = formatMoney(total * (invoice.discount/100), currencyId, true);
var discount = total * (invoice.discount/100);
total -= discount;
discount = formatMoney(discount, currencyId, true);
var discountX = headerRight - (doc.getStringUnitWidth(discount) * doc.internal.getFontSize());
doc.text(discountX, x, discount);
}
var tax = 0;
if (invoice.tax && parseFloat(invoice.tax.rate)) {
tax = parseFloat(invoice.tax.rate);
} else if (invoice.tax_rate && parseFloat(invoice.tax_rate)) {
tax = parseFloat(invoice.tax_rate);
}
if (tax) {
x += 16;
doc.text(footerLeft, x, 'Tax ' + tax + '%');
var tax = total * (tax/100);
total = parseFloat(total) + parseFloat(tax);
tax = formatMoney(tax, currencyId, true);
var taxX = headerRight - (doc.getStringUnitWidth(tax) * doc.internal.getFontSize());
doc.text(taxX, x, tax);
}
x += 16;
doc.text(footerLeft, x, 'Paid to Date');
var paid = formatMoney(0, currencyId, true);