bug fixes

This commit is contained in:
Hillel Coren 2014-01-01 17:23:32 +02:00
parent fff1aa69d3
commit f0bf0a52ec
6 changed files with 437 additions and 273 deletions

View File

@ -317,6 +317,7 @@ class InvoiceController extends \BaseController {
$data = array( $data = array(
'account' => $invoice->account, 'account' => $invoice->account,
'invoice' => $invoice, 'invoice' => $invoice,
'data' => false,
'method' => 'PUT', 'method' => 'PUT',
'invitationContactIds' => $contactIds, 'invitationContactIds' => $contactIds,
'url' => 'invoices/' . $publicId, 'url' => 'invoices/' . $publicId,
@ -338,13 +339,13 @@ class InvoiceController extends \BaseController {
$data = array( $data = array(
'account' => $account, 'account' => $account,
'invoice' => null, 'invoice' => null,
'data' => Input::old('data'),
'invoiceNumber' => $invoiceNumber, 'invoiceNumber' => $invoiceNumber,
'method' => 'POST', 'method' => 'POST',
'url' => 'invoices', 'url' => 'invoices',
'title' => '- New Invoice', 'title' => '- New Invoice',
'client' => $client, 'client' => $client);
'items' => json_decode(Input::old('items')));
$data = array_merge($data, InvoiceController::getViewModel()); $data = array_merge($data, InvoiceController::getViewModel());
return View::make('invoices.edit', $data); return View::make('invoices.edit', $data);
} }
@ -393,20 +394,21 @@ class InvoiceController extends \BaseController {
} }
$input = json_decode(Input::get('data')); $input = json_decode(Input::get('data'));
$invoice = $input->invoice;
if (!$input->client->contacts[0]->email)
if ($errors = $this->invoiceRepo->getErrors($invoice))
{ {
return Redirect::to('invoices/create') return Redirect::to('invoices/create')
->withInput(); ->withInput()->withErrors($errors);
} }
else else
{ {
$this->taxRateRepo->save($input->tax_rates); $this->taxRateRepo->save($input->tax_rates);
$clientData = (array) $input->client; $clientData = (array) $invoice->client;
$client = $this->clientRepo->save($input->client->public_id, $clientData); $client = $this->clientRepo->save($invoice->client->public_id, $clientData);
$invoiceData = (array) $input; $invoiceData = (array) $invoice;
$invoiceData['client_id'] = $client->id; $invoiceData['client_id'] = $client->id;
$invoice = $this->invoiceRepo->save($publicId, $invoiceData); $invoice = $this->invoiceRepo->save($publicId, $invoiceData);
@ -454,7 +456,7 @@ class InvoiceController extends \BaseController {
} }
$message = ''; $message = '';
if ($input->client->public_id == '-1') if ($input->invoice->client->public_id == '-1')
{ {
$message = ' and created client'; $message = ' and created client';
$url = URL::to('clients/' . $client->public_id); $url = URL::to('clients/' . $client->public_id);

View File

@ -321,6 +321,7 @@ class ConfideSetupUsersTable extends Migration {
$t->unsignedInteger('public_id')->index(); $t->unsignedInteger('public_id')->index();
$t->unique( array('account_id','public_id') ); $t->unique( array('account_id','public_id') );
$t->unique( array('account_id','invoice_number') );
}); });

View File

@ -68,6 +68,29 @@ class InvoiceRepository
return $query; return $query;
} }
public function getErrors($input)
{
$contact = (array) $input->client->contacts[0];
$rules = ['email' => 'required|email'];
$validator = \Validator::make($contact, $rules);
if ($validator->fails())
{
return $validator;
}
$invoice = (array) $input;
$rules = ['invoice_number' => 'unique:invoices,invoice_number,' . $input->id];
$validator = \Validator::make($invoice, $rules);
if ($validator->fails())
{
return $validator;
}
return false;
}
public function save($publicId, $data) public function save($publicId, $data)
{ {
if ($publicId) if ($publicId)
@ -94,10 +117,10 @@ class InvoiceRepository
$invoice->po_number = trim($data['po_number']); $invoice->po_number = trim($data['po_number']);
$invoice->currency_id = $data['currency_id']; $invoice->currency_id = $data['currency_id'];
if (isset($data['tax']) && isset($data['tax']->rate) && floatval($data['tax']->rate) > 0) if (isset($data['tax_rate']) && floatval($data['tax_rate']) > 0)
{ {
$invoice->tax_rate = floatval($data['tax']->rate); $invoice->tax_rate = floatval($data['tax_rate']);
$invoice->tax_name = trim($data['tax']->name); $invoice->tax_name = trim($data['tax_name']);
} }
else else
{ {
@ -144,10 +167,10 @@ class InvoiceRepository
$invoiceItem->qty = floatval($item->qty); $invoiceItem->qty = floatval($item->qty);
$invoiceItem->tax_rate = 0; $invoiceItem->tax_rate = 0;
if ($item->tax && isset($item->tax->rate) && floatval($item->tax->rate) > 0) if (isset($item->tax_rate) && floatval($item->tax_rate) > 0)
{ {
$invoiceItem->tax_rate = floatval($item->tax->rate); $invoiceItem->tax_rate = floatval($item->tax_rate);
$invoiceItem->tax_name = trim($item->tax->name); $invoiceItem->tax_name = trim($item->tax_name);
} }
$invoice->invoice_items()->save($invoiceItem); $invoice->invoice_items()->save($invoiceItem);

View File

@ -10,7 +10,7 @@ class TaxRateRepository
foreach ($taxRates as $record) foreach ($taxRates as $record)
{ {
if (!isset($record->rate) || $record->is_deleted) if (!isset($record->rate) || (isset($record->is_deleted) && $record->is_deleted))
{ {
continue; continue;
} }

View File

@ -21,6 +21,7 @@
include_once(app_path().'/libraries/utils.php'); // TODO_FIX include_once(app_path().'/libraries/utils.php'); // TODO_FIX
include_once(app_path().'/handlers/UserEventHandler.php'); // TODO_FIX include_once(app_path().'/handlers/UserEventHandler.php'); // TODO_FIX
Route::get('/send_emails', function() { Route::get('/send_emails', function() {
Artisan::call('ninja:send-invoices'); Artisan::call('ninja:send-invoices');
}); });
@ -172,4 +173,9 @@ define('DEFAULT_CURRENCY', 1); // US Dollar
define('DEFAULT_DATE_FORMAT', 'M j, Y'); define('DEFAULT_DATE_FORMAT', 'M j, Y');
define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy'); define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy');
define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a'); define('DEFAULT_DATETIME_FORMAT', 'F j, Y, g:i a');
define('DEFAULT_QUERY_CACHE', 120); define('DEFAULT_QUERY_CACHE', 120);
if (Auth::check() && !Session::has(SESSION_TIMEZONE)) {
Event::fire('user.refresh');
}

View File

@ -9,14 +9,16 @@
@section('content') @section('content')
<p>&nbsp;</p> <p>&nbsp;</p>
{{ Former::open($url)->method($method)->addClass('main_form')->rules(array( {{ Former::open($url)->method($method)->addClass('main_form')->rules(array(
'client' => 'required', 'client' => 'required',
'email' => 'required', 'email' => 'required',
'product_key' => 'max:14', 'product_key' => 'max:14',
)); }} )); }}
<div data-bind="with: invoice">
<div class="row" style="min-height:195px" onkeypress="formEnterClick(event)"> <div class="row" style="min-height:195px" onkeypress="formEnterClick(event)">
<div class="col-md-5" id="col_1"> <div class="col-md-5" id="col_1">
@ -24,7 +26,7 @@
<div class="form-group"> <div class="form-group">
<label for="client" class="control-label col-lg-4 col-sm-4">Client</label> <label for="client" class="control-label col-lg-4 col-sm-4">Client</label>
<div class="col-lg-8 col-sm-8" style="padding-top: 7px"> <div class="col-lg-8 col-sm-8" style="padding-top: 7px">
<a href="#" data-bind="click: showClientForm">{{ $client->getDisplayName() }}</a> <a href="#" data-bind="click: $root.showClientForm">{{ $client->getDisplayName() }}</a>
</div> </div>
</div> </div>
<div style="display:none"> <div style="display:none">
@ -35,7 +37,7 @@
<div class="form-group" style="margin-bottom: 8px"> <div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4"> <div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4">
<a href="#" data-bind="click: showClientForm, text: showClientText"></a> <a href="#" data-bind="click: $root.showClientForm, text: client.linkText"></a>
</div> </div>
</div> </div>
@ -48,7 +50,7 @@
<div class="col-lg-8 col-lg-offset-4"> <div class="col-lg-8 col-lg-offset-4">
<label for="test" class="checkbox" data-bind="attr: {for: $index() + '_check'}"> <label for="test" class="checkbox" data-bind="attr: {for: $index() + '_check'}">
<input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check'}"> <input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check'}">
<span data-bind="text: displayName"/> <span data-bind="text: email.display"/>
</label> </label>
</div> </div>
</div> </div>
@ -89,7 +91,7 @@
<div class="form-group" style="margin-bottom: 8px"> <div class="form-group" style="margin-bottom: 8px">
<label for="recurring" class="control-label col-lg-4 col-sm-4">Taxes</label> <label for="recurring" class="control-label col-lg-4 col-sm-4">Taxes</label>
<div class="col-lg-8 col-sm-8" style="padding-top: 7px"> <div class="col-lg-8 col-sm-8" style="padding-top: 7px">
<a href="#" data-bind="click: showTaxesForm">Manage taxe rates</a> <a href="#" data-bind="click: $root.showTaxesForm">Manage taxe rates</a>
</div> </div>
</div> </div>
@ -98,7 +100,7 @@
<p>&nbsp;</p> <p>&nbsp;</p>
{{ Former::hidden('data')->data_bind("value: ko.toJSON(model)") }} {{ Former::hidden('data')->data_bind("value: ko.mapping.toJSON(model)") }}
<table class="table invoice-table" style="margin-bottom: 0px !important"> <table class="table invoice-table" style="margin-bottom: 0px !important">
<thead> <thead>
@ -108,7 +110,7 @@
<th>Description</th> <th>Description</th>
<th>Unit Cost</th> <th>Unit Cost</th>
<th>Quantity</th> <th>Quantity</th>
<th data-bind="visible: showInvoiceItemTaxes">Tax</th> <th data-bind="visible: $root.invoice_item_taxes.show">Tax</th>
<th>Line&nbsp;Total</th> <th>Line&nbsp;Total</th>
<th class="hide-border"></th> <th class="hide-border"></th>
</tr> </tr>
@ -131,11 +133,11 @@
<td style="min-width:120px"> <td style="min-width:120px">
<input onkeyup="onItemChange()" data-bind="value: prettyQty, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control"//> <input onkeyup="onItemChange()" data-bind="value: prettyQty, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control"//>
</td> </td>
<td style="min-width:120px; vertical-align:middle" data-bind="visible: $parent.showInvoiceItemTaxes"> <td style="min-width:120px; vertical-align:middle" data-bind="visible: $root.invoice_item_taxes.show">
<select class="form-control" style="width:100%" data-bind="value: tax, options: $parent.tax_rates, optionsText: 'displayName'"></select> <select class="form-control" style="width:100%" data-bind="value: tax, options: $root.tax_rates, optionsText: 'displayName'"></select>
</td> </td>
<td style="min-width:120px;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> <span data-bind="text: totals.total"></span>
</td> </td>
<td style="min-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"/> &nbsp;<i data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-minus-circle" title="Remove item"/>
@ -146,34 +148,34 @@
<tr> <tr>
<td class="hide-border"/> <td class="hide-border"/>
<td colspan="2"/> <td colspan="2"/>
<td data-bind="visible: showInvoiceItemTaxes"/> <td data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="2">Subtotal</td> <td colspan="2">Subtotal</td>
<td style="text-align: right"><span data-bind="text: subtotal"/></td> <td style="text-align: right"><span data-bind="text: totals.subtotal"/></td>
</tr> </tr>
<tr data-bind="visible: discount() > 0"> <tr data-bind="visible: discount() > 0">
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/> <td class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="2">Discount</td> <td colspan="2">Discount</td>
<td style="text-align: right"><span data-bind="text: discounted"/></td> <td style="text-align: right"><span data-bind="text: totals.discounted"/></td>
</tr> </tr>
<tr data-bind="visible: showInvoiceTaxes"> <tr data-bind="visible: $root.invoice_taxes.show">
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/> <td class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td style="vertical-align: middle">Tax</td> <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><select class="form-control" style="width:100%" data-bind="value: tax, options: $root.tax_rates, optionsText: 'displayName'"></select></td>
<td style="vertical-align: middle; text-align: right"><span data-bind="text: taxAmount"/></td> <td style="vertical-align: middle; text-align: right"><span data-bind="text: totals.taxAmount"/></td>
</tr> </tr>
<tr> <tr>
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/> <td class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="2">Paid to Date</td> <td colspan="2">Paid to Date</td>
<td style="text-align: right"></td> <td style="text-align: right"></td>
</tr> </tr>
<tr> <tr>
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td class="hide-border" data-bind="visible: showInvoiceItemTaxes"/> <td class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="2"><b>Balance Due</b></td> <td colspan="2"><b>Balance Due</b></td>
<td style="text-align: right"><span data-bind="text: total"/></td> <td style="text-align: right"><span data-bind="text: totals.total"/></td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@ -183,8 +185,9 @@
<div style="display:none"> <div style="display:none">
{{ Former::text('action') }} {{ Former::text('action') }}
@if ($invoice) @if ($invoice)
{{ Former::text('id') }} {{ Former::text('id') }}
{{ Former::populateField('id', $invoice->id) }}
@endif @endif
</div> </div>
@ -202,12 +205,12 @@
array('Delete Invoice', "javascript:onDeleteClick()"), array('Delete Invoice', "javascript:onDeleteClick()"),
) )
) )
, array('id'=>'actionDropDown', 'style'=>'text-align:left', 'data-bind'=>'css: saveButtonEnabled'))->split(); }} , array('id'=>'actionDropDown', 'style'=>'text-align:left', 'data-bind'=>'css: enable.save'))->split(); }}
@else @else
{{ Button::primary_submit('Save Invoice', array('data-bind'=>'css: saveButtonEnabled')) }} {{ Button::primary_submit('Save Invoice', array('data-bind'=>'css: enable.save')) }}
@endif @endif
{{ Button::primary('Send Email', array('id' => 'email_button', 'onclick' => 'onEmailClick()', 'data-bind' => 'css: emailButtonEnabled')) }} {{ Button::primary('Send Email', array('id' => 'email_button', 'onclick' => 'onEmailClick()', 'data-bind' => 'css: enable.email')) }}
</div> </div>
<p>&nbsp;</p> <p>&nbsp;</p>
@ -253,7 +256,7 @@
</div> </div>
{{ Former::legend('Additional Info') }} {{ Former::legend('Additional Info') }}
{{ Former::select('payment_terms')->addOption('','')->data_bind('value: payment_terms') {{ Former::select('payment_terms')->addOption('','0')->data_bind('value: payment_terms')
->fromQuery($paymentTerms, 'name', 'num_days') }} ->fromQuery($paymentTerms, 'name', 'num_days') }}
{{ Former::select('currency_id')->addOption('','')->label('Currency')->data_bind('value: currency_id') {{ Former::select('currency_id')->addOption('','')->label('Currency')->data_bind('value: currency_id')
->fromQuery($currencies, 'name', 'id') }} ->fromQuery($currencies, 'name', 'id') }}
@ -289,7 +292,7 @@
<div class="modal-footer" style="margin-top: 0px"> <div class="modal-footer" style="margin-top: 0px">
<span class="error-block" id="emailError" style="display:none;float:left;font-weight:bold">Please provide a valid email address.</span><span>&nbsp;</span> <span class="error-block" id="emailError" style="display:none;float:left;font-weight:bold">Please provide a valid email address.</span><span>&nbsp;</span>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bind="click: clientFormComplete">Done</button> <button type="button" class="btn btn-primary" data-bind="click: $root.clientFormComplete">Done</button>
</div> </div>
</div> </div>
@ -314,7 +317,7 @@
<th class="hide-border"></th> <th class="hide-border"></th>
</tr> </tr>
</thead> </thead>
<tbody data-bind="foreach: tax_rates"> <tbody data-bind="foreach: $root.tax_rates.filtered">
<tr data-bind="event: { mouseover: showActions, mouseout: hideActions }"> <tr data-bind="event: { mouseover: showActions, mouseout: hideActions }">
<td style="width:10px" class="hide-border"></td> <td style="width:10px" class="hide-border"></td>
<td style="width:60px"> <td style="width:60px">
@ -324,7 +327,7 @@
<input onkeyup="onTaxRateChange()" data-bind="value: prettyRate, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//> <input onkeyup="onTaxRateChange()" data-bind="value: prettyRate, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
</td> </td>
<td style="width:10px; cursor:pointer" class="hide-border td-icon"> <td style="width:10px; cursor:pointer" class="hide-border td-icon">
&nbsp;<i data-bind="click: $parent.removeTaxRate, visible: actionsVisible() &amp;&amp; $parent.tax_rates().length > 1" class="fa fa-minus-circle" title="Remove item"/> &nbsp;<i data-bind="click: $root.removeTaxRate, visible: actionsVisible() &amp;&amp; $root.tax_rates().length > 1" class="fa fa-minus-circle" title="Remove item"/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -332,15 +335,15 @@
&nbsp; &nbsp;
{{ Former::checkbox('invoice_taxes')->text('Enable specifying an <b>invoice tax</b>') {{ Former::checkbox('invoice_taxes')->text('Enable specifying an <b>invoice tax</b>')
->label('Settings')->data_bind('checked: invoice_taxes, enable: tax_rates().length > 1') }} ->label('Settings')->data_bind('checked: $root.invoice_taxes, enable: $root.tax_rates().length > 1') }}
{{ Former::checkbox('invoice_item_taxes')->text('Enable specifying <b>line item taxes</b>') {{ 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') }} ->label('&nbsp;')->data_bind('checked: $root.invoice_item_taxes, enable: $root.tax_rates().length > 1') }}
</div> </div>
<div class="modal-footer" style="margin-top: 0px"> <div class="modal-footer" style="margin-top: 0px">
<!-- <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> --> <!-- <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> -->
<button type="button" class="btn btn-primary" data-bind="click: taxFormComplete">Done</button> <button type="button" class="btn btn-primary" data-bind="click: $root.taxFormComplete">Done</button>
</div> </div>
</div> </div>
@ -349,6 +352,7 @@
{{ Former::close() }} {{ Former::close() }}
</div>
<script type="text/javascript"> <script type="text/javascript">
@ -463,7 +467,7 @@
} }
function createInvoiceModel() { function createInvoiceModel() {
var invoice = ko.toJS(model); var invoice = ko.toJS(model).invoice;
@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() }};
@ -573,192 +577,61 @@
} }
} }
function InvoiceModel(data) { function ViewModel() {
var self = this; var self = this;
this.client = new ClientModel(); self.invoice = new InvoiceModel();
self.discount = ko.observable('');
self.frequency_id = ko.observable('');
self.currency_id = ko.observable({{ Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY) }});
self.terms = ko.observable('');
self.public_notes = ko.observable('');
self.po_number = ko.observable('');
self.invoice_date = ko.observable('');
self.invoice_number = ko.observable('');
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.tax_rates = ko.observableArray();
self.loadClient = function(client) {
//console.log(client);
ko.mapping.fromJS(client, model.invoice.client.mapping, model.invoice.client);
}
self.invoice_taxes = ko.observable({{ Auth::user()->account->invoice_taxes ? 'true' : 'false' }}); 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.invoice_item_taxes = ko.observable({{ Auth::user()->account->invoice_item_taxes ? 'true' : 'false' }});
/*
self.mapping = { self.mapping = {
'invoice_items': { 'invoice': {
create: function(options) { create: function(options) {
return new ItemModel(options.data); return new InvoiceModel(options.data);
} }
} },
} 'tax_rates': {
self.loadClient = function(client) { create: function(options) {
//console.log(client); return new TaxRateModel(options.data);
ko.mapping.fromJS(client, model.client.mapping, model.client); }
},
} }
*/
self.wrapped_terms = ko.computed({ self.invoice_taxes.show = ko.computed(function() {
read: function() { if (self.tax_rates().length > 2 && self.invoice_taxes()) {
$('#terms').height(this.terms().split('\n').length * 36);
return this.terms();
},
write: function(value) {
value = wordWrapText(value, 340);
self.terms(value);
$('#terms').height(value.split('\n').length * 36);
},
owner: this
});
self.showInvoiceTaxes = ko.computed(function() {
if (self.tax_rates().length > 1 && self.invoice_taxes()) {
return true; return true;
} }
if (self.tax() && self.tax().rate() > 0) { if (self.invoice.tax_rate() > 0) {
return true; return true;
} }
return false; return false;
}); });
self.showInvoiceItemTaxes = ko.computed(function() { self.invoice_item_taxes.show = ko.computed(function() {
if (self.tax_rates().length > 1 && self.invoice_item_taxes()) { if (self.tax_rates().length > 2 && self.invoice_item_taxes()) {
return true; return true;
} }
for (var i=0; i<self.invoice_items().length; i++) { for (var i=0; i<self.invoice.invoice_items().length; i++) {
var item = self.invoice_items()[i]; var item = self.invoice.invoice_items()[i];
if (item.tax() && item.tax().rate() > 0) { if (item.tax_rate() > 0) {
return true; return true;
} }
} }
return false; return false;
}); });
self.tax_rates.filtered = ko.computed(function() {
self.wrapped_notes = ko.computed({ return self.tax_rates().slice(1, self.tax_rates().length);
read: function() {
$('#public_notes').height(this.public_notes().split('\n').length * 36);
return this.public_notes();
},
write: function(value) {
value = wordWrapText(value, 340);
self.public_notes(value);
$('#public_notes').height(value.split('\n').length * 36);
},
owner: this
}); });
self.showClientText = ko.computed(function() {
return self.client.public_id() ? 'Edit client details' : 'Create new client';
});
self.saveButtonEnabled = ko.computed(function() {
var isValid = false;
for (var i=0; i<self.client.contacts().length; i++) {
var contact = self.client.contacts()[i];
if (isValidEmailAddress(contact.email())) {
isValid = true;
} else {
isValid = false;
break;
}
}
return isValid ? "enabled" : "disabled";
});
self.emailButtonEnabled = ko.computed(function() {
var isValid = false;
var sendTo = false;
for (var i=0; i<self.client.contacts().length; i++) {
var contact = self.client.contacts()[i];
if (isValidEmailAddress(contact.email())) {
isValid = true;
if (contact.send_invoice()) {
sendTo = true;
}
} else {
isValid = false;
break;
}
}
return isValid && sendTo ? "enabled" : "disabled";
});
self.showTaxesForm = function() {
self.taxBackup = ko.mapping.toJS(self.tax_rates);
$('#taxModal').modal('show');
}
self.taxFormComplete = function() {
model.taxBackup = false;
$('#taxModal').modal('hide');
}
self.showClientForm = function() {
self.clientBackup = ko.mapping.toJS(self.client);
$('#emailError').css( "display", "none" );
$('#clientModal').modal('show');
}
self.clientFormComplete = function() {
var email = $('#email').val();
var firstName = $('#first_name').val();
var lastName = $('#last_name').val();
var name = $('#name').val();
if (!email || !isValidEmailAddress(email)) {
$('#emailError').css( "display", "inline" );
return;
}
if (self.client.public_id() == 0) {
self.client.public_id(-1);
}
if (name) {
//
} else if (firstName || lastName) {
name = firstName + ' ' + lastName;
} else {
name = email;
}
$('.client_select select').combobox('setSelected');
$('.client_select input.form-control').val(name);
$('.client_select .combobox-container').addClass('combobox-selected');
$('#emailError').css( "display", "none" );
//$('.client_select input.form-control').focus();
$('#invoice_number').focus();
refreshPDF();
model.clientBackup = false;
$('#clientModal').modal('hide');
}
self.removeItem = function(item) {
self.invoice_items.remove(item);
refreshPDF();
}
self.addItem = function() {
var itemModel = new ItemModel();
self.invoice_items.push(itemModel);
applyComboboxListeners();
}
self.removeTaxRate = function(taxRate) { self.removeTaxRate = function(taxRate) {
self.tax_rates.remove(taxRate); self.tax_rates.remove(taxRate);
@ -780,37 +653,239 @@
} }
} }
this.rawSubtotal = ko.computed(function() { self.getTaxRate = function(name, rate) {
for (var i=0; i<self.tax_rates().length; i++) {
var taxRate = self.tax_rates()[i];
if (taxRate.name() == name && taxRate.rate() == parseFloat(rate)) {
return taxRate;
}
}
var taxRate = new TaxRateModel();
taxRate.name(name);
taxRate.rate(parseFloat(rate));
taxRate.is_deleted(true);
model.tax_rates.push(taxRate);
return taxRate;
}
self.showTaxesForm = function() {
self.taxBackup = ko.mapping.toJS(self.tax_rates);
$('#taxModal').modal('show');
}
self.taxFormComplete = function() {
model.taxBackup = false;
$('#taxModal').modal('hide');
}
self.showClientForm = function() {
self.clientBackup = ko.mapping.toJS(self.invoice.client);
$('#emailError').css( "display", "none" );
$('#clientModal').modal('show');
}
self.clientFormComplete = function() {
var email = $('#email').val();
var firstName = $('#first_name').val();
var lastName = $('#last_name').val();
var name = $('#name').val();
if (!email || !isValidEmailAddress(email)) {
$('#emailError').css( "display", "inline" );
return;
}
if (self.invoice.client.public_id() == 0) {
self.invoice.client.public_id(-1);
}
if (name) {
//
} else if (firstName || lastName) {
name = firstName + ' ' + lastName;
} else {
name = email;
}
$('.client_select select').combobox('setSelected');
$('.client_select input.form-control').val(name);
$('.client_select .combobox-container').addClass('combobox-selected');
$('#emailError').css( "display", "none" );
//$('.client_select input.form-control').focus();
$('#invoice_number').focus();
refreshPDF();
model.clientBackup = false;
$('#clientModal').modal('hide');
}
}
function InvoiceModel(data) {
var self = this;
this.client = new ClientModel();
this.id = ko.observable('');
self.discount = ko.observable('');
self.frequency_id = ko.observable('');
self.currency_id = ko.observable({{ Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY) }});
self.terms = ko.observable(wordWrapText('{{ $account->invoice_terms }}', 340));
self.public_notes = ko.observable('');
self.po_number = ko.observable('');
self.invoice_date = ko.observable('{{ Utils::today() }}');
self.invoice_number = ko.observable('{{ isset($invoiceNumber) ? $invoiceNumber : '' }}');
self.due_date = ko.observable('');
self.start_date = ko.observable('{{ Utils::today() }}');
self.end_date = ko.observable('');
self.tax_name = ko.observable();
self.tax_rate = ko.observable();
self.is_recurring = ko.observable(false);
self.invoice_status_id = ko.observable(0);
self.invoice_items = ko.observableArray();
self.mapping = {
'invoice_items': {
create: function(options) {
return new ItemModel(options.data);
}
},
'tax': {
create: function(options) {
return new TaxRateModel(options.data);
}
},
}
self._tax = ko.observable();
this.tax = ko.computed({
read: function () {
return self._tax();
},
write: function(value) {
if (value) {
console.log("WRITE INVOICE TAX");
console.log(value.name());
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
} else {
self._tax(false);
self.tax_name('');
self.tax_rate(0);
}
}
})
self.wrapped_terms = ko.computed({
read: function() {
$('#terms').height(this.terms().split('\n').length * 36);
return this.terms();
},
write: function(value) {
value = wordWrapText(value, 340);
self.terms(value);
$('#terms').height(value.split('\n').length * 36);
},
owner: this
});
self.wrapped_notes = ko.computed({
read: function() {
$('#public_notes').height(this.public_notes().split('\n').length * 36);
return this.public_notes();
},
write: function(value) {
value = wordWrapText(value, 340);
self.public_notes(value);
$('#public_notes').height(value.split('\n').length * 36);
},
owner: this
});
self.client.linkText = ko.computed(function() {
return self.client.public_id() ? 'Edit client details' : 'Create new client';
});
self.enable = {};
self.enable.save = ko.computed(function() {
var isValid = false;
for (var i=0; i<self.client.contacts().length; i++) {
var contact = self.client.contacts()[i];
if (isValidEmailAddress(contact.email())) {
isValid = true;
} else {
isValid = false;
break;
}
}
return isValid ? "enabled" : "disabled";
});
self.enable.email = ko.computed(function() {
var isValid = false;
var sendTo = false;
for (var i=0; i<self.client.contacts().length; i++) {
var contact = self.client.contacts()[i];
if (isValidEmailAddress(contact.email())) {
isValid = true;
if (contact.send_invoice()) {
sendTo = true;
}
} else {
isValid = false;
break;
}
}
return isValid && sendTo ? "enabled" : "disabled";
});
self.removeItem = function(item) {
self.invoice_items.remove(item);
refreshPDF();
}
self.addItem = function() {
var itemModel = new ItemModel();
self.invoice_items.push(itemModel);
applyComboboxListeners();
}
this.totals = ko.observable();
this.totals.rawSubtotal = ko.computed(function() {
var total = 0; var total = 0;
for(var p = 0; p < self.invoice_items().length; ++p) for(var p = 0; p < self.invoice_items().length; ++p)
{ {
total += self.invoice_items()[p].rawTotal(); total += self.invoice_items()[p].totals.rawTotal();
} }
return total; return total;
}); });
this.subtotal = ko.computed(function() { this.totals.subtotal = ko.computed(function() {
var total = self.rawSubtotal(); var total = self.totals.rawSubtotal();
return total > 0 ? formatMoney(total, self.currency_id()) : ''; return total > 0 ? formatMoney(total, self.currency_id()) : '';
}); });
this.rawDiscounted = ko.computed(function() { this.totals.rawDiscounted = ko.computed(function() {
return self.rawSubtotal() * (self.discount()/100); return self.totals.rawSubtotal() * (self.discount()/100);
}); });
this.discounted = ko.computed(function() { this.totals.discounted = ko.computed(function() {
return formatMoney(self.rawDiscounted(), self.currency_id()); return formatMoney(self.totals.rawDiscounted(), self.currency_id());
}); });
self.taxAmount = ko.computed(function() { self.totals.taxAmount = ko.computed(function() {
var total = self.rawSubtotal(); var total = self.totals.rawSubtotal();
var discount = parseFloat(self.discount()); var discount = parseFloat(self.discount());
if (discount > 0) { if (discount > 0) {
total = total * ((100 - discount)/100); total = total * ((100 - discount)/100);
} }
var taxRate = self.tax() ? parseFloat(self.tax().rate()) : 0; var taxRate = parseFloat(self.tax_rate());
if (taxRate > 0) { if (taxRate > 0) {
var tax = total * (taxRate/100); var tax = total * (taxRate/100);
return formatMoney(tax, self.currency_id()); return formatMoney(tax, self.currency_id());
@ -820,15 +895,15 @@
}); });
this.total = ko.computed(function() { this.totals.total = ko.computed(function() {
var total = self.rawSubtotal(); var total = self.totals.rawSubtotal();
var discount = parseFloat(self.discount()); var discount = parseFloat(self.discount());
if (discount > 0) { if (discount > 0) {
total = total * ((100 - discount)/100); total = total * ((100 - discount)/100);
} }
var taxRate = self.tax() ? parseFloat(self.tax().rate()) : 0; var taxRate = parseFloat(self.tax_rate());
if (taxRate > 0) { if (taxRate > 0) {
total = parseFloat(total) + (total * (taxRate/100)); total = parseFloat(total) + (total * (taxRate/100));
} }
@ -857,7 +932,7 @@
self.client_industry_id = ko.observable(''); self.client_industry_id = ko.observable('');
self.currency_id = ko.observable(''); self.currency_id = ko.observable('');
self.website = ko.observable(''); self.website = ko.observable('');
self.payment_terms = ko.observable(); self.payment_terms = ko.observable(0);
self.contacts = ko.observableArray(); self.contacts = ko.observableArray();
self.mapping = { self.mapping = {
@ -885,6 +960,7 @@
self.contacts.remove(this); self.contacts.remove(this);
} }
/*
self.placeholderName = ko.computed(function() { self.placeholderName = ko.computed(function() {
if (self.contacts().length == 0) return; if (self.contacts().length == 0) return;
var contact = self.contacts()[0]; var contact = self.contacts()[0];
@ -894,6 +970,7 @@
return ''; return '';
} }
}); });
*/
if (data) { if (data) {
ko.mapping.fromJS(data, {}, this); ko.mapping.fromJS(data, {}, this);
@ -915,7 +992,7 @@
ko.mapping.fromJS(data, {}, this); ko.mapping.fromJS(data, {}, this);
} }
self.displayName = ko.computed(function() { self.email.display = ko.computed(function() {
return self.first_name() + ' ' + self.last_name() + ' - ' + self.email(); return self.first_name() + ' ' + self.last_name() + ' - ' + self.email();
}); });
} }
@ -923,12 +1000,14 @@
function TaxRateModel(data) { function TaxRateModel(data) {
var self = this; var self = this;
self.public_id = ko.observable(''); self.public_id = ko.observable('');
self.rate = ko.observable(); self.rate = ko.observable(0);
self.name = ko.observable(''); self.name = ko.observable('');
self.is_deleted = ko.observable(false); self.is_deleted = ko.observable(false);
self.actionsVisible = ko.observable(false); self.actionsVisible = ko.observable(false);
if (data) { if (data) {
console.log("NEW TAX MODEL");
console.log(data);
ko.mapping.fromJS(data, {}, this); ko.mapping.fromJS(data, {}, this);
} }
@ -943,10 +1022,16 @@
}); });
self.displayName = ko.computed(function() { self.displayName = ko.computed({
var name = self.name() ? self.name() : ''; read: function () {
var rate = self.rate() ? parseFloat(self.rate()) + '% -' : ''; var name = self.name() ? self.name() : '';
return rate + name; var rate = self.rate() ? parseFloat(self.rate()) + '% -' : '';
return rate + name;
},
write: function (value) {
// do nothing
},
owner: this
}); });
self.hideActions = function() { self.hideActions = function() {
@ -968,9 +1053,25 @@
this.notes = ko.observable(''); this.notes = ko.observable('');
this.cost = ko.observable(0); this.cost = ko.observable(0);
this.qty = ko.observable(0); this.qty = ko.observable(0);
this.tax = ko.observable(); self.tax_name = ko.observable('');
self.tax_rate = ko.observable(0);
this.actionsVisible = ko.observable(false); this.actionsVisible = ko.observable(false);
self._tax = ko.observable();
this.tax = ko.computed({
read: function () {
return self._tax();
},
write: function(value) {
console.log("TAX-WRITE");
console.log(value);
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
}
})
this.prettyQty = ko.computed({ this.prettyQty = ko.computed({
read: function () { read: function () {
return this.qty() ? parseFloat(this.qty()) : ''; return this.qty() ? parseFloat(this.qty()) : '';
@ -991,31 +1092,44 @@
owner: this owner: this
}); });
if (data) { self.mapping = {
ko.mapping.fromJS(data, {}, this); 'tax': {
if (this.cost()) this.cost(formatMoney(this.cost(), model.currency_id(), true)); create: function(options) {
console.log('CALLED');
return new TaxRateModel(options.data);
}
}
} }
if (data) {
ko.mapping.fromJS(data, self.mapping, this);
if (this.cost()) this.cost(formatMoney(this.cost(), model.invoice.currency_id(), true));
if (self.tax_rate()) {
self.tax(model.getTaxRate(self.tax_name(), self.tax_rate()));
}
}
/*
for (var i=0; i<model.tax_rates().length; i++) { for (var i=0; i<model.tax_rates().length; i++) {
var taxRate = model.tax_rates()[i]; var taxRate = model.tax_rates()[i];
if (data && (data.tax_name == taxRate.name() && data.tax_rate == taxRate.rate())) { if (data && (data.tax_name == taxRate.name() && data.tax_rate == taxRate.rate())) {
console.log("SETTING TAX: " + data.tax_name);
self.tax(taxRate); self.tax(taxRate);
break; break;
} else if ((!data || !data.tax_name) && !taxRate.name()) {
self.tax(taxRate);
break;
} }
} }
// if the tax was deleted but exists for the line item // if the tax was deleted but exists for the line item
if (data && data.tax_name && (parseFloat(data.tax_rate)) && !self.tax()) { if (data && data.tax_name && (parseFloat(data.tax_rate)) && !self.tax_rate()) {
var taxRate = new TaxRateModel(); var taxRate = new TaxRateModel();
taxRate.rate(parseFloat(data.tax_rate)); taxRate.rate(parseFloat(data.tax_rate));
taxRate.name(data.tax_name); taxRate.name(data.tax_name);
taxRate.is_deleted(true); taxRate.is_deleted(true);
model.tax_rates.push(taxRate); model.tax_rates.push(taxRate);
console.log("SETTING TAX: " + taxRate.name());
self.tax(taxRate); self.tax(taxRate);
} }
*/
self.wrapped_notes = ko.computed({ self.wrapped_notes = ko.computed({
read: function() { read: function() {
@ -1029,10 +1143,12 @@
owner: this owner: this
}); });
this.rawTotal = ko.computed(function() { this.totals = ko.observable();
this.totals.rawTotal = ko.computed(function() {
var cost = parseFloat(self.cost()); var cost = parseFloat(self.cost());
var qty = parseFloat(self.qty()); var qty = parseFloat(self.qty());
var taxRate = self.tax() ? parseFloat(self.tax().rate()) : 0; var taxRate = parseFloat(self.tax_rate());
var value = cost * qty; var value = cost * qty;
if (taxRate > 0) { if (taxRate > 0) {
value += value * (taxRate/100); value += value * (taxRate/100);
@ -1040,9 +1156,9 @@
return value ? value : ''; return value ? value : '';
}); });
this.total = ko.computed(function() { this.totals.total = ko.computed(function() {
var total = self.rawTotal(); var total = self.totals.rawTotal();
return total ? formatMoney(total, model.currency_id()) : ''; return total ? formatMoney(total, model.invoice.currency_id()) : '';
}); });
this.hideActions = function() { this.hideActions = function() {
@ -1065,15 +1181,15 @@
function onItemChange() function onItemChange()
{ {
var hasEmpty = false; var hasEmpty = false;
for(var i=0; i<model.invoice_items().length; i++) { for(var i=0; i<model.invoice.invoice_items().length; i++) {
var item = model.invoice_items()[i]; var item = model.invoice.invoice_items()[i];
if (item.isEmpty()) { if (item.isEmpty()) {
hasEmpty = true; hasEmpty = true;
} }
} }
if (!hasEmpty) { if (!hasEmpty) {
model.addItem(); model.invoice.addItem();
} }
$('.word-wrap').each(function(index, input) { $('.word-wrap').each(function(index, input) {
@ -1083,15 +1199,15 @@
function onTaxRateChange() function onTaxRateChange()
{ {
var hasEmpty = false; var emptyCount = 0;
for(var i=0; i<model.tax_rates().length; i++) { for(var i=0; i<model.tax_rates().length; i++) {
var taxRate = model.tax_rates()[i]; var taxRate = model.tax_rates()[i];
if (taxRate.isEmpty()) { if (taxRate.isEmpty()) {
hasEmpty = true; emptyCount++;
} }
} }
if (!hasEmpty) { if (emptyCount < 2) {
model.addTaxRate(); model.addTaxRate();
} }
} }
@ -1111,36 +1227,52 @@
$clientSelect.append(new Option(getClientDisplayName(client), client.public_id)); $clientSelect.append(new Option(getClientDisplayName(client), client.public_id));
} }
window.model = new InvoiceModel(); window.model = new ViewModel();
@foreach ($taxRates as $taxRate) @if (!$data)
model.addTaxRate({{ $taxRate }}); model.addTaxRate();
@endforeach @foreach ($taxRates as $taxRate)
model.addTaxRate(); model.addTaxRate({{ $taxRate }});
model.tax(model.getBlankTaxRate()); @endforeach
@if ($invoice) @endif
var invoice = {{ $invoice }}; //model.tax(model.getBlankTaxRate());
ko.mapping.fromJS(invoice, model.mapping, model); @if ($invoice || $data)
var invoice = {{ $invoice ? $invoice : $data }};
ko.mapping.fromJS(invoice, model.invoice.mapping, model.invoice);
var taxRate = model.getTaxRate(invoice.tax_name, invoice.tax_rate);
model.invoice.tax(taxRate);
/*
for (var i=0; i<model.tax_rates().length; i++) { for (var i=0; i<model.tax_rates().length; i++) {
var taxRate = model.tax_rates()[i]; var taxRate = model.tax_rates()[i];
if (model.tax_name() == taxRate.name() && model.tax_rate() == taxRate.rate()) { if (model.tax_name() == taxRate.name() && model.tax_rate() == taxRate.rate()) {
model.tax(taxRate); model.tax(taxRate);
break; break;
} }
}
// if the tax was deleted but exists for the line item
if (parseFloat(invoice.tax_rate) && !model.tax_rate()) {
var taxRate = new TaxRateModel();
taxRate.rate(parseFloat(invoice.tax_rate));
taxRate.name(invoice.tax_name);
taxRate.is_deleted(true);
model.tax_rates.push(taxRate);
model.tax(taxRate);
} }
if (!model.discount()) model.discount(''); @if ($invoice)
var invitationContactIds = {{ json_encode($invitationContactIds) }}; var invitationContactIds = {{ json_encode($invitationContactIds) }};
var client = clientMap[invoice.client.public_id]; var client = clientMap[invoice.client.public_id];
for (var i=0; i<client.contacts.length; i++) { for (var i=0; i<client.contacts.length; i++) {
var contact = client.contacts[i]; var contact = client.contacts[i];
contact.send_invoice = invitationContactIds.indexOf(contact.public_id) >= 0; contact.send_invoice = invitationContactIds.indexOf(contact.public_id) >= 0;
} }
@else @endif
model.invoice_date('{{ Utils::today() }}'); */
model.start_date('{{ Utils::today() }}'); if (!model.invoice.discount()) model.invoice.discount('');
model.invoice_number('{{ $invoiceNumber }}');
model.terms(wordWrapText('{{ $account->invoice_terms }}', 340)); @endif
@if (!$data)
model.addTaxRate();
model.invoice.addItem();
@endif @endif
model.addItem();
ko.applyBindings(model); ko.applyBindings(model);
</script> </script>