Add inclusive tax rates #552

This commit is contained in:
Hillel Coren 2017-01-02 13:38:58 +02:00
parent 4307bb2984
commit c4cdc45a93
14 changed files with 199 additions and 47 deletions

View File

@ -253,7 +253,7 @@ class ExpenseController extends BaseController
'customLabel1' => Auth::user()->account->custom_vendor_label1,
'customLabel2' => Auth::user()->account->custom_vendor_label2,
'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(),
];
}

View File

@ -328,7 +328,11 @@ class InvoiceController extends BaseController
$defaultTax = false;
foreach ($rates as $rate) {
$options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%';
$name = $rate->name . ' ' . ($rate->rate+0) . '%';
if ($rate->is_inclusive) {
$name .= ' - ' . trans('texts.inclusive');
}
$options[($rate->is_inclusive ? '1 ' : '0 ') . $rate->rate . ' ' . $rate->name] = $name;
// load default invoice tax
if ($rate->id == $account->default_tax_rate_id) {
@ -342,7 +346,7 @@ class InvoiceController extends BaseController
if (isset($options[$key])) {
continue;
}
$options[$key] = $rate['name'] . ' ' . $rate['rate'] . '%';
$options['0 ' . $key] = $rate['name'] . ' ' . $rate['rate'] . '%';
}
}

View File

@ -74,7 +74,7 @@ class ProductController extends BaseController
$data = [
'account' => $account,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']) : null,
'product' => $product,
'entity' => $product,
'method' => 'PUT',
@ -94,7 +94,7 @@ class ProductController extends BaseController
$data = [
'account' => $account,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']) : null,
'product' => null,
'method' => 'POST',
'url' => 'products',

View File

@ -18,7 +18,8 @@ class TaxRate extends EntityModel
*/
protected $fillable = [
'name',
'rate'
'rate',
'is_inclusive',
];
/**

View File

@ -20,6 +20,12 @@ class TaxRateDatatable extends EntityDatatable
function ($model) {
return $model->rate . '%';
}
],
[
'type',
function ($model) {
return $model->is_inclusive ? trans('texts.inclusive') : trans('texts.exclusive');
}
]
];
}

View File

@ -16,7 +16,13 @@ class TaxRateRepository extends BaseRepository
return DB::table('tax_rates')
->where('tax_rates.account_id', '=', $accountId)
->where('tax_rates.deleted_at', '=', null)
->select('tax_rates.public_id', 'tax_rates.name', 'tax_rates.rate', 'tax_rates.deleted_at');
->select(
'tax_rates.public_id',
'tax_rates.name',
'tax_rates.rate',
'tax_rates.deleted_at',
'tax_rates.is_inclusive'
);
}
public function save($data, $taxRate = null)

View File

@ -13,6 +13,7 @@ class TaxRateTransformer extends EntityTransformer
* @SWG\Property(property="name", type="string", example="GST")
* @SWG\Property(property="account_key", type="string", example="asimplestring", readOnly=true)
* @SWG\Property(property="rate", type="float", example=17.5)
* @SWG\Property(property="is_inclusive", type="boolean", example=false)
* @SWG\Property(property="updated_at", type="date-time", example="2016-01-01 12:10:00")
* @SWG\Property(property="archived_at", type="date-time", example="2016-01-01 12:10:00")
*/
@ -23,6 +24,7 @@ class TaxRateTransformer extends EntityTransformer
'id' => (int) $taxRate->public_id,
'name' => $taxRate->name,
'rate' => (float) $taxRate->rate,
'is_inclusive' => (bool) $taxRate->is_inclusive,
'updated_at' => $this->getTimestamp($taxRate->updated_at),
'archived_at' => $this->getTimestamp($taxRate->deleted_at),
]);

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddInclusiveTaxes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tax_rates', function($table)
{
$table->boolean('is_inclusive')->default(false);
});
Schema::table('companies', function ($table)
{
$table->enum('bluevine_status', ['ignored', 'signed_up'])->nullable();
});
DB::statement('UPDATE companies
LEFT JOIN accounts ON accounts.company_id = companies.id AND accounts.bluevine_status IS NOT NULL
SET companies.bluevine_status = accounts.bluevine_status');
Schema::table('accounts', function($table)
{
$table->dropColumn('bluevine_status');
$table->text('bcc_email')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tax_rates', function($table)
{
$table->dropColumn('is_inclusive');
});
Schema::table('companies', function($table)
{
$table->dropColumn('bluevine_status');
});
Schema::table('accounts', function ($table)
{
$table->enum('bluevine_status', ['ignored', 'signed_up'])->nullable();
$table->dropColumn('bcc_email');
});
}
}

View File

@ -2291,7 +2291,9 @@ $LANG = array(
'iphone_app_message' => 'Consider downloading our :link',
'iphone_app' => 'iPhone app',
'logged_in' => 'Logged In',
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.'
'switch_to_primary' => 'Switch to your primary company (:name) to manage your plan.',
'inclusive' => 'Inclusive',
'exclusive' => 'Exclusive',
);

View File

@ -34,7 +34,6 @@
trans('texts.currency_symbol') . ': <span id="currency_symbol_example"/>' => array('name' => 'show_currency_code', 'value' => 0),
trans('texts.currency_code') . ': <span id="currency_code_example"/>' => array('name' => 'show_currency_code', 'value' => 1),
])->inline()
->check('timer')
->label('&nbsp;')
->addGroupClass('currrency_radio') !!}
<br/>

View File

@ -21,11 +21,20 @@
@if ($taxRate)
{{ Former::populate($taxRate) }}
{{ Former::populateField('is_inclusive', intval($taxRate->is_inclusive)) }}
@endif
{!! Former::text('name')->label('texts.name') !!}
{!! Former::text('rate')->label('texts.rate')->append('%') !!}
{!! Former::radios('is_inclusive')->radios([
trans('texts.exclusive') => array('name' => 'is_inclusive', 'value' => 0),
trans('texts.inclusive') => array('name' => 'is_inclusive', 'value' => 1),
])->inline()
->check(0)
->label('type') !!}
</div>
</div>

View File

@ -65,12 +65,13 @@
->addColumn(
trans('texts.name'),
trans('texts.rate'),
trans('texts.type'),
trans('texts.action'))
->setUrl(url('api/tax_rates/'))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->setOptions('bAutoWidth', false)
->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]])
->setOptions('aoColumns', [[ "sWidth"=> "25%" ], [ "sWidth"=> "25%" ], ["sWidth"=> "25%"], ["sWidth"=> "25%"]])
->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]])
->render('datatable') !!}

View File

@ -300,7 +300,7 @@
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax1')
->data_bind('value: tax1, event:{change:onTax1Change}')
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->raw() !!}
<input type="text" data-bind="value: tax_name1, attr: {name: 'invoice_items[' + $index() + '][tax_name1]'}" style="display:none">
@ -309,7 +309,7 @@
{!! Former::select('')
->addOption('', '')
->options($taxRateOptions)
->data_bind('value: tax2')
->data_bind('value: tax2, event:{change:onTax2Change}')
->addClass('tax-select')
->raw() !!}
</div>
@ -452,7 +452,7 @@
->addOption('', '')
->options($taxRateOptions)
->addClass($account->enable_second_tax_rate ? 'tax-select' : '')
->data_bind('value: tax1')
->data_bind('value: tax1, event:{change:onTax1Change}')
->raw() !!}
<input type="text" name="tax_name1" data-bind="value: tax_name1" style="display:none">
<input type="text" name="tax_rate1" data-bind="value: tax_rate1" style="display:none">
@ -461,7 +461,7 @@
->addOption('', '')
->options($taxRateOptions)
->addClass('tax-select')
->data_bind('value: tax2')
->data_bind('value: tax2, event:{change:onTax2Change}')
->raw() !!}
</div>
<input type="text" name="tax_name2" data-bind="value: tax_name2" style="display:none">

View File

@ -179,8 +179,10 @@ function InvoiceModel(data) {
self.last_sent_date = ko.observable('');
self.tax_name1 = ko.observable();
self.tax_rate1 = ko.observable();
self.tax_rate1IsInclusive = ko.observable(0);
self.tax_name2 = ko.observable();
self.tax_rate2 = ko.observable();
self.tax_rate2IsInclusive = ko.observable(0);
self.is_recurring = ko.observable(0);
self.is_quote = ko.observable({{ $entityType == ENTITY_QUOTE ? '1' : '0' }});
self.auto_bill = ko.observable(0);
@ -268,25 +270,25 @@ function InvoiceModel(data) {
this.tax1 = ko.computed({
read: function () {
return self.tax_rate1() + ' ' + self.tax_name1();
return self.tax_rate1IsInclusive() + ' ' + self.tax_rate1() + ' ' + self.tax_name1();
},
write: function(value) {
var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1);
self.tax_name1(name);
self.tax_rate1(rate);
var parts = value.split(' ');
self.tax_rate1IsInclusive(parts.shift());
self.tax_rate1(parts.shift());
self.tax_name1(parts.join(' '));
}
})
this.tax2 = ko.computed({
read: function () {
return self.tax_rate2() + ' ' + self.tax_name2();
return self.tax_rate2IsInclusive() + ' ' + 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);
var parts = value.split(' ');
self.tax_rate2IsInclusive(parts.shift());
self.tax_rate2(parts.shift());
self.tax_name2(parts.join(' '));
}
})
@ -498,6 +500,37 @@ function InvoiceModel(data) {
self.showResetFooter = function() {
return self.default_footer() && self.invoice_footer() != self.default_footer();
}
self.applyInclusivTax = function(taxRate) {
for (var i=0; i<self.invoice_items().length; i++) {
var item = self.invoice_items()[i];
item.applyInclusivTax(taxRate);
}
}
self.onTax1Change = function(obj, event) {
if ( ! event.originalEvent) {
return;
}
var taxKey = $(event.currentTarget).val();
var taxRate = parseFloat(self.tax_rate1());
if (taxKey.substr(0, 1) != 1) {
return;
}
self.applyInclusivTax(taxRate);
}
self.onTax2Change = function(obj, event) {
if ( ! event.originalEvent) {
return;
}
var taxKey = $(event.currentTarget).val();
var taxRate = parseFloat(self.tax_rate2());
if (taxKey.substr(0, 1) != 1) {
return;
}
self.applyInclusivTax(taxRate);
}
}
function ClientModel(data) {
@ -666,33 +699,35 @@ function ItemModel(data) {
self.custom_value2 = ko.observable('');
self.tax_name1 = ko.observable('');
self.tax_rate1 = ko.observable(0);
self.tax_rate1IsInclusive = ko.observable(0);
self.tax_name2 = ko.observable('');
self.tax_rate2 = ko.observable(0);
self.tax_rate2IsInclusive = ko.observable(0);
self.task_public_id = ko.observable('');
self.expense_public_id = ko.observable('');
self.actionsVisible = ko.observable(false);
this.tax1 = ko.computed({
read: function () {
return self.tax_rate1() + ' ' + self.tax_name1();
return self.tax_rate1IsInclusive() + ' ' + self.tax_rate1() + ' ' + self.tax_name1();
},
write: function(value) {
var rate = value.substr(0, value.indexOf(' '));
var name = value.substr(value.indexOf(' ') + 1);
self.tax_name1(name);
self.tax_rate1(rate);
var parts = value.split(' ');
self.tax_rate1IsInclusive(parts.shift());
self.tax_rate1(parts.shift());
self.tax_name1(parts.join(' '));
}
})
this.tax2 = ko.computed({
read: function () {
return self.tax_rate2() + ' ' + self.tax_name2();
return self.tax_rate2IsInclusive() + ' ' + 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);
var parts = value.split(' ');
self.tax_rate2IsInclusive(parts.shift());
self.tax_rate2(parts.shift());
self.tax_name2(parts.join(' '));
}
})
@ -747,6 +782,34 @@ function ItemModel(data) {
}
this.onSelect = function() {}
self.applyInclusivTax = function(taxRate) {
if ( ! taxRate) {
return;
}
var cost = self.cost() / (100 + taxRate) * 100;
self.cost(roundToTwo(cost));
}
self.onTax1Change = function (obj, event) {
if (event.originalEvent) {
var taxKey = $(event.currentTarget).val();
var taxRate = parseFloat(self.tax_rate1());
if (taxKey.substr(0, 1) == 1) {
self.applyInclusivTax(taxRate);
}
}
}
self.onTax2Change = function (obj, event) {
if (event.originalEvent) {
var taxKey = $(event.currentTarget).val();
var taxRate = parseFloat(self.tax_rate2());
if (taxKey.substr(0, 1) == 1) {
self.applyInclusivTax(taxRate);
}
}
}
}
function DocumentModel(data) {
@ -838,9 +901,8 @@ ko.bindingHandlers.productTypeahead = {
}
@if ($account->invoice_item_taxes)
if (datum.default_tax_rate) {
model.tax_rate1(datum.default_tax_rate.rate);
model.tax_name1(datum.default_tax_rate.name);
model.tax1(datum.default_tax_rate.rate + ' ' + datum.default_tax_rate.name);
var $select = $(this).parentsUntil('tbody').find('select').first();
$select.val('0 ' + datum.default_tax_rate.rate + ' ' + datum.default_tax_rate.name).trigger('change');
}
@endif
@endif