1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-18 00:53:10 +01:00

Bug fixes

This commit is contained in:
Hillel Coren 2013-12-27 11:10:32 +02:00
parent aa8151edaf
commit 130a176888
8 changed files with 243 additions and 51 deletions

View File

@ -10,6 +10,7 @@ class ConfideSetupUsersTable extends Migration {
*/
public function up()
{
Schema::dropIfExists('tax_rates');
Schema::dropIfExists('themes');
Schema::dropIfExists('credits');
Schema::dropIfExists('activities');
@ -348,6 +349,9 @@ class ConfideSetupUsersTable extends Migration {
$t->decimal('cost', 10, 2);
$t->decimal('qty', 10, 2);
$t->string('tax_name');
$t->decimal('tax_rate', 10, 2);
$t->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
$t->foreign('product_id')->references('id')->on('products');
$t->foreign('user_id')->references('id')->on('users');
@ -408,6 +412,24 @@ class ConfideSetupUsersTable extends Migration {
$t->unique( array('account_id','public_id') );
});
Schema::create('tax_rates', function($t)
{
$t->increments('id');
$t->unsignedInteger('account_id')->index();
$t->unsignedInteger('user_id');
$t->timestamps();
$t->softDeletes();
$t->string('name');
$t->decimal('rate', 10, 2);
$t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$t->foreign('user_id')->references('id')->on('users');
$t->unsignedInteger('public_id');
$t->unique( array('account_id','public_id') );
});
Schema::create('activities', function($t)
{
$t->increments('id');
@ -439,6 +461,7 @@ class ConfideSetupUsersTable extends Migration {
*/
public function down()
{
Schema::dropIfExists('tax_rates');
Schema::dropIfExists('themes');
Schema::dropIfExists('credits');
Schema::dropIfExists('activities');

6
app/models/TaxRate.php Executable file
View File

@ -0,0 +1,6 @@
<?php
class TaxRate extends EntityModel
{
}

28
app/ninja/repositories/AccountRepository.php Normal file → Executable file
View File

@ -9,35 +9,45 @@ class AccountRepository
{
$clients = \DB::table('clients')
->where('clients.deleted_at', '=', null)
->select(\DB::raw("'Clients' as type, clients.public_id, clients.name"));
->select(\DB::raw("'Clients' as type, clients.public_id, clients.name, '' as token"));
$contacts = \DB::table('clients')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->where('clients.deleted_at', '=', null)
->select(\DB::raw("'Contacts' as type, clients.public_id, CONCAT(contacts.first_name, ' ', contacts.last_name) as name"));
->select(\DB::raw("'Contacts' as type, clients.public_id, CONCAT(contacts.first_name, ' ', contacts.last_name, ': ', clients.name) as name, '' as token"));
$invoices = \DB::table('clients')
->join('invoices', 'invoices.client_id', '=', 'clients.id')
->where('clients.deleted_at', '=', null)
->where('invoices.deleted_at', '=', null)
->select(\DB::raw("'Invoices' as type, invoices.public_id, CONCAT(invoices.invoice_number, ': ', clients.name) as name"));
->select(\DB::raw("'Invoices' as type, invoices.public_id, CONCAT(invoices.invoice_number, ': ', clients.name) as name, invoices.invoice_number as token"));
$data = [];
foreach ($clients->union($contacts)->union($invoices)->get() as $row)
{
if (!isset($data[$row->type]))
$type = $row->type;
if (!isset($data[$type]))
{
$data[$row->type] = [];
$data[$type] = [];
}
$tokens = explode(' ', $row->name);
$tokens[] = $type;
if ($type == 'Invoices')
{
$tokens[] = intVal($row->token) . '';
}
$data[$row->type][] = [
$data[$type][] = [
'value' => $row->name,
'public_id' => $row->public_id
'public_id' => $row->public_id,
'tokens' => $tokens
];
}
return $data;
}
}

View File

@ -30,7 +30,6 @@
<link rel="stylesheet" type="text/css" href="{{ asset('css/datepicker.css') }}"/>
<script src="{{ asset('js/typeahead.js') }}" type="text/javascript"></script>
<script src="{{ asset('js/hogan-2.0.0.js') }}" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="{{ asset('css/typeahead.js-bootstrap.css') }}"/>
<script src="{{ asset('js/script.js') }}" type="text/javascript"></script>
@ -501,7 +500,7 @@
if (!data.hasOwnProperty(type)) continue;
datasets.push({
name: type,
header: '&nbsp;<b>' + type + '</b>',
header: '&nbsp;<b>' + type + '</b>',
local: data[type]
});
}

View File

@ -67,6 +67,14 @@
{{ Former::text('po_number')->label('PO&nbsp;number')->data_bind("value: po_number, valueUpdate: 'afterkeydown'") }}
{{ Former::text('discount')->data_bind("value: discount, valueUpdate: 'afterkeydown'") }}
{{ Former::text('currency')->data_bind("value: discount, valueUpdate: 'afterkeydown'") }}
<div class="form-group" style="margin-bottom: 8px">
<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">
<a href="#" data-bind="click: showTaxesForm">Manage taxe rates</a>
</div>
</div>
</div>
</div>
@ -82,6 +90,7 @@
<th>Description</th>
<th>Unit Cost</th>
<th>Quantity</th>
<th data-bind="visible: tax_rates().length > 1">Tax</th>
<th>Line&nbsp;Total</th>
<th class="hide-border"></th>
</tr>
@ -92,24 +101,22 @@
<i data-bind="visible: actionsVisible() &amp;&amp; $parent.invoice_items().length > 1" class="fa fa-sort"></i>
</td>
<td style="width:120px">
{{ Former::text('product_key')->useDatalist(Product::getProductKeys($products), 'key')->onkeyup('onChange()')
{{ 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">
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown'" rows="1" cols="60" style="resize: none;" class="form-control word-wrap" onchange="refreshPDF()"></textarea>
</td>
<td style="width:100px">
<input onkeyup="onChange()" data-bind="value: cost, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
<input onkeyup="onItemChange()" data-bind="value: cost, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
</td>
<td style="width:80px">
<input onkeyup="onChange()" data-bind="value: qty, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
<input onkeyup="onItemChange()" data-bind="value: qty, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
</td>
<!--
<td style="width:100px">
<input data-bind="value: tax, valueUpdate: 'afterkeydown'"/>
<td style="width:80px; vertical-align:middle" data-bind="visible: $parent.tax_rates().length > 1">
<select style="width:100%" data-bind="options: $parent.tax_rates"></select>
</td>
-->
<td style="width:100px;text-align: right;padding-top:9px !important">
<td style="width:100px;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">
@ -119,26 +126,22 @@
</tbody>
<tfoot>
<tr>
<td class="hide-border"></td>
<td colspan="2"/>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td colspan="2">Subtotal</td>
<td style="text-align: right"><span data-bind="text: subtotal"/></td>
</tr>
<tr>
<td class="hide-border"></td>
<td colspan="2" class="hide-border"/>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td colspan="2">Paid to Date</td>
<td style="text-align: right"></td>
</tr>
<tr data-bind="visible: discount() > 0">
<td class="hide-border"></td>
<td colspan="2" class="hide-border"/>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td colspan="2">Discount</td>
<td style="text-align: right"><span data-bind="text: discounted"/></td>
</tr>
<tr>
<td class="hide-border"></td>
<td colspan="2" class="hide-border"/>
<td class="hide-border" data-bind="attr: {colspan: tax_rates().length > 1 ? 4 : 3}"/>
<td colspan="2"><b>Balance Due</b></td>
<td style="text-align: right"><span data-bind="text: total"/></td>
</tr>
@ -184,16 +187,17 @@
<canvas id="theCanvas" style="display:none;width:100%;border:solid 1px #CCCCCC;"></canvas>
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal fade" id="clientModal" tabindex="-1" role="dialog" aria-labelledby="clientModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:1000px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="myModalLabel">New Client</h4>
<h4 class="modal-title" id="clientModalLabel">New Client</h4>
</div>
<div class="row" data-bind="with: client" style="padding-left:16px;padding-right:16px" onkeypress="modalEnterClick(event)">
<div class="col-md-6">
<div class="container" style="width: 100%">
<div style="background-color: #F6F6F6" class="row" data-bind="with: client" onkeypress="clientModalEnterClick(event)">
<div class="col-md-6" style="margin-left:0px;margin-right:0px" >
{{ Former::legend('Organization') }}
{{ Former::text('name')->data_bind("value: name, valueUpdate: 'afterkeydown'") }}
@ -211,7 +215,7 @@
->fromQuery($countries, 'name', 'id')->data_bind("dropdown: country_id") }}
</div>
<div class="col-md-6">
<div class="col-md-6" style="margin-left:0px;margin-right:0px" >
{{ Former::legend('Contacts') }}
<div data-bind='template: { foreach: contacts,
@ -244,18 +248,64 @@
</div>
</div>
</div>
<div class="modal-footer">
<div class="modal-footer" style="margin-top: 0px">
<span class="error-block" id="nameError" style="display:none;float:left">Please provide a value for the name field.</span><span>&nbsp;</span>
<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>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="taxModal" tabindex="-1" role="dialog" aria-labelledby="taxModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="taxModalLabel">Tax Rates</h4>
</div>
<div style="background-color: #F6F6F6" onkeypress="taxModalEnterClick(event)">
<table class="table invoice-table sides-padded" style="margin-bottom: 0px !important">
<thead>
<tr>
<th class="hide-border"></th>
<th class="hide-border">Name</th>
<th class="hide-border">Rate</th>
<th class="hide-border"></th>
</tr>
</thead>
<tbody data-bind="foreach: tax_rates">
<tr data-bind="event: { mouseover: showActions, mouseout: hideActions }">
<td style="width:10px" class="hide-border"></td>
<td style="width:60px">
<input onkeyup="onTaxRateChange()" data-bind="value: name, valueUpdate: 'afterkeydown'" class="form-control" onchange="refreshPDF()"//>
</td>
<td style="width:60px">
<input onkeyup="onTaxRateChange()" data-bind="value: rate, valueUpdate: 'afterkeydown'" style="text-align: right" class="form-control" onchange="refreshPDF()"//>
</td>
<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"/>
</td>
</tr>
</tbody>
</table>
&nbsp;
</div>
<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-primary" data-bind="click: taxFormComplete">Done</button>
</div>
</div>
</div>
</div>
{{ Former::close() }}
<script type="text/javascript">
@ -302,7 +352,7 @@
//$('[name="client_combobox"]').focus();
@endif
$('#myModal').on('hidden.bs.modal', function () {
$('#clientModal').on('hidden.bs.modal', function () {
if (model.clientBackup) {
console.log("Loading backup");
//console.log(model.clientBackup);
@ -312,10 +362,14 @@
})
$('#myModal').on('shown.bs.modal', function () {
$('#clientModal').on('shown.bs.modal', function () {
$('#name').focus();
})
$('#taxModal').on('shown.bs.modal', function () {
$('#taxModal input:first').focus();
})
$('#actionDropDown > button:first').click(function() {
onSaveClick();
});
@ -438,7 +492,7 @@
}
}
function modalEnterClick(event) {
function clientModalEnterClick(event) {
if (event.keyCode === 13){
event.preventDefault();
model.clientFormComplete();
@ -446,6 +500,14 @@
}
}
function taxModalEnterClick(event) {
if (event.keyCode === 13){
event.preventDefault();
model.taxFormComplete();
return false;
}
}
function InvoiceModel() {
var self = this;
this.client = new ClientModel();
@ -460,7 +522,9 @@
self.end_date = 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.mapping = {
'invoice_items': {
@ -491,17 +555,27 @@
return self.client.public_id() ? 'Edit client details' : 'Create new client';
});
self.showTaxesForm = function() {
$('#taxModal').modal('show');
}
self.taxFormComplete = function() {
$('#taxModal').modal('hide');
}
self.showClientForm = function() {
self.clientBackup = ko.mapping.toJS(self.client);
console.log(self.clientBackup);
//console.log(self.clientBackup);
if (self.client.public_id() == 0) {
$('#myModal input').val('');
$('#myModal #country_id').val('');
$('#clientModal input').val('');
$('#clientModal #country_id').val('');
}
$('#nameError').css( "display", "none" );
$('#myModal').modal('show');
$('#clientModal').modal('show');
}
self.clientFormComplete = function() {
@ -523,7 +597,7 @@
refreshPDF();
model.clientBackup = false;
$('#myModal').modal('hide');
$('#clientModal').modal('hide');
}
self.removeItem = function(item) {
@ -536,6 +610,16 @@
applyComboboxListeners();
}
self.removeTaxRate = function(taxRate) {
self.tax_rates.remove(taxRate);
//refreshPDF();
}
self.addTaxRate = function() {
self.tax_rates.push(new TaxRateModel());
applyComboboxListeners();
}
this.rawSubtotal = ko.computed(function() {
var total = 0;
for(var p = 0; p < self.invoice_items().length; ++p)
@ -639,13 +723,33 @@
});
}
function TaxRateModel(data) {
var self = this;
this.rate = ko.observable();
this.name = ko.observable('');
this.actionsVisible = ko.observable(false);
this.hideActions = function() {
this.actionsVisible(false);
}
this.showActions = function() {
this.actionsVisible(true);
}
this.isEmpty = function() {
return !self.rate() && !self.name();
}
}
function ItemModel(data) {
var self = this;
this.product_key = ko.observable('');
this.notes = ko.observable('');
this.cost = ko.observable();
this.qty = ko.observable();
this.tax = ko.observable();
this.tax_rate = ko.observable();
this.tax_name = ko.observable('');
this.actionsVisible = ko.observable(false);
if (data) {
@ -659,7 +763,7 @@
write: function(value) {
value = wordWrapText(value);
self.notes(value);
onChange();
onItemChange();
},
owner: this
});
@ -667,9 +771,9 @@
this.rawTotal = ko.computed(function() {
var cost = parseFloat(self.cost());
var qty = parseFloat(self.qty());
var tax = parseFloat(self.tax());
var tax = parseFloat(self.tax_rate());
var value = cost * qty;
if (self.tax() > 0) {
if (tax > 0) {
//value = value * ((100 - this.tax())/100);
}
return value ? value : '';
@ -693,7 +797,7 @@
}
}
function onChange()
function onItemChange()
{
var hasEmpty = false;
for(var i=0; i<model.invoice_items().length; i++) {
@ -702,6 +806,7 @@
hasEmpty = true;
}
}
if (!hasEmpty) {
model.addItem();
}
@ -711,6 +816,21 @@
});
}
function onTaxRateChange()
{
var hasEmpty = false;
for(var i=0; i<model.tax_rates().length; i++) {
var taxRate = model.tax_rates()[i];
if (taxRate.isEmpty()) {
hasEmpty = true;
}
}
if (!hasEmpty) {
model.addTaxRate();
}
}
var products = {{ $products }};
var clients = {{ $clients }};
var clientMap = {};
@ -740,7 +860,8 @@
model.invoice_number('{{ $invoiceNumber }}');
model.terms(wordWrapText('{{ $account->invoice_terms }}', 250));
@endif
model.invoice_items.push(new ItemModel());
model.addItem();
model.addTaxRate();
ko.applyBindings(model);

View File

@ -14,7 +14,9 @@
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.js" type="text/javascript"></script>
<!-- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.js" type="text/javascript"></script> -->
<script src="{{ asset('js/jquery.js') }}" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="{{ asset('css/bootstrap.css') }}"/>

View File

@ -23,6 +23,11 @@ div.panel {
text-align: center;
}
.sides-padded {
margin-left: 8px !important;
margin-right: 8px !important;
}
/*
.form-horizontal {
max-width: 750px;

View File

@ -15,8 +15,28 @@ function generatePDF(invoice) {
var descriptionLeft = 140;
var unitCostRight = 400;
var qtyRight = 470;
var taxRight = 470;
var lineTotalRight = 540;
var hasTaxes = true;
for (var i=0; i<invoice.invoice_items.length; i++)
{
var item = invoice.invoice_items[i];
if (item.tax_rate > 0) {
hasTaxes = true;
break;
}
}
if (hasTaxes)
{
descriptionLeft -= 20;
unitCostRight -= 40;
qtyRight -= 40;
}
var doc = new jsPDF('p', 'pt');
doc.setFont('Helvetica','');
doc.setFontSize(10);
@ -86,6 +106,7 @@ function generatePDF(invoice) {
var costX = unitCostRight - (doc.getStringUnitWidth('Unit Cost') * doc.internal.getFontSize());
var qtyX = qtyRight - (doc.getStringUnitWidth('Quantity') * doc.internal.getFontSize());
var taxX = taxRight - (doc.getStringUnitWidth('Tax') * doc.internal.getFontSize());
var totalX = lineTotalRight - (doc.getStringUnitWidth('Line Total') * doc.internal.getFontSize());
doc.text(tableLeft, tableTop, 'Item');
@ -94,13 +115,18 @@ function generatePDF(invoice) {
doc.text(qtyX, tableTop, 'Quantity');
doc.text(totalX, tableTop, 'Line Total');
if (hasTaxes)
{
doc.text(taxX, tableTop, 'Tax');
}
/* line items */
doc.setFontType("normal");
var line = 1;
var total = 0;
var shownItem = false;
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 cost = formatNumber(item.cost);
var qty = item.qty ? parseFloat(item.qty) + '' : '';