Bug fixes

This commit is contained in:
Hillel Coren 2015-05-05 12:48:23 +03:00
parent 5a28ff2612
commit b368e5589c
20 changed files with 484 additions and 336 deletions

View File

@ -1,5 +1,8 @@
<?php namespace App\Console\Commands;
use DB;
use DateTime;
use Carbon;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
@ -10,7 +13,7 @@ use Symfony\Component\Console\Input\InputArgument;
WARNING: Please backup your database before running this script
##################################################################
Since the application was released a number of bugs have (inevitably) been found.
Since the application was released a number of bugs have inevitably been found.
Although the bugs have always been fixed in some cases they've caused the client's
balance, paid to date and/or activity records to become inaccurate. This script will
check for errors and correct the data.
@ -100,13 +103,15 @@ class CheckData extends Command {
$clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at')
->orderBy('clients.id', 'DESC')
->get(['clients.id', 'clients.balance', 'clients.paid_to_date']);
->get(['clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]);
$this->info(count($clients) . ' clients with incorrect balance/activities');
foreach ($clients as $client) {
$this->info("=== Client:{$client->id} Balance:{$client->balance} ===");
$this->info("=== Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ===");
$foundProblem = false;
$lastBalance = 0;
$lastAdjustment = 0;
$lastCreatedAt = null;
$clientFix = false;
$activities = DB::table('activities')
->where('client_id', '=', $client->id)
@ -195,6 +200,11 @@ class CheckData extends Command {
$foundProblem = true;
$clientFix -= $activity->adjustment;
$activityFix = 0;
} else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) {
$this->info("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}");
$foundProblem = true;
$clientFix -= $activity->adjustment;
$activityFix = 0;
}
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) {
// **Fix for updating balance when updating a quote**
@ -231,19 +241,33 @@ class CheckData extends Command {
}
$lastBalance = $activity->balance;
$lastAdjustment = $activity->adjustment;
$lastCreatedAt = $activity->created_at;
}
if ($clientFix !== false) {
$balance = $activity->balance + $clientFix;
$data = ['balance' => $balance];
$this->info("Corrected balance:{$balance}");
if ($activity->balance + $clientFix != $client->actual_balance) {
$this->info("** Creating 'recovered update' activity **");
if ($this->option('fix') == 'true') {
DB::table('activities')->insert([
'created_at' => new Carbon,
'updated_at' => new Carbon,
'account_id' => $client->account_id,
'client_id' => $client->id,
'message' => 'Recovered update to invoice [<a href="https://github.com/hillelcoren/invoice-ninja/releases/tag/v1.7.1" target="_blank">details</a>]',
'adjustment' => $client->actual_balance - $activity->balance,
'balance' => $client->actual_balance,
]);
}
}
$data = ['balance' => $client->actual_balance];
$this->info("Corrected balance:{$client->actual_balance}");
if ($this->option('fix') == 'true') {
DB::table('clients')
->where('id', $client->id)
->update($data);
}
}
}
$this->info('Done');
}

View File

@ -51,6 +51,7 @@ class DashboardController extends BaseController
$activities = Activity::where('activities.account_id', '=', Auth::user()->account_id)
->where('activity_type_id', '>', 0)
->orderBy('created_at', 'desc')->take(6)->get();
$pastDue = Invoice::scope()

View File

@ -59,8 +59,6 @@ class ReportController extends BaseController
$enableChart = true;
}
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$datasets = [];
$labels = [];
$maxTotals = 0;
@ -155,7 +153,7 @@ class ReportController extends BaseController
if ($enableChart) {
foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) {
$records = DB::table($entityType.'s')
->select(DB::raw('sum(amount) as total, '.$groupBy.'('.$entityType.'_date) as '.$groupBy))
->select(DB::raw('sum(amount) as total, concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date)) as '.$groupBy))
->where('account_id', '=', Auth::user()->account_id)
->where($entityType.'s.is_deleted', '=', false)
->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d'))
@ -171,14 +169,17 @@ class ReportController extends BaseController
$dates = $records->lists($groupBy);
$data = array_combine($dates, $totals);
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
$period = new DatePeriod($startDate, $interval, $endDate);
$endDate->modify('-1 '.$padding);
$totals = [];
foreach ($period as $d) {
$dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n');
$date = $d->format($dateFormat);
$date = $d->format('Y'.$dateFormat);
$totals[] = isset($data[$date]) ? $data[$date] : 0;
if ($entityType == ENTITY_INVOICE) {
@ -228,7 +229,7 @@ class ReportController extends BaseController
'chartTypes' => $chartTypes,
'chartType' => $chartType,
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)),
'endDate' => $endDate->modify('-1'.$padding)->format(Session::get(SESSION_DATE_FORMAT)),
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)),
'groupBy' => $groupBy,
'feature' => ACCOUNT_CHART_BUILDER,
'displayData' => $displayData,

View File

@ -66,8 +66,7 @@ class StartupCheck
$count = Session::get(SESSION_COUNTER, 0);
Session::put(SESSION_COUNTER, ++$count);
//if (!Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) {
if (true) {
if (!Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) {
$data = false;
if (Utils::isNinja()) {
$data = Utils::getNewsFeedResponse();

View File

@ -236,7 +236,11 @@ class Utils
$currencyId = Session::get(SESSION_CURRENCY);
}
$currency = Currency::find($currencyId);
foreach (Cache::get('currencies') as $currency) {
if ($currency->id == $currencyId) {
break;
}
}
if (!$currency) {
$currency = Currency::find(1);
@ -486,7 +490,7 @@ class Utils
public static function encodeActivity($person = null, $action, $entity = null, $otherPerson = null)
{
$person = $person ? $person->getDisplayName() : '<i>System</i>';
$entity = $entity ? '['.$entity->getActivityKey().']' : '';
$entity = $entity ? $entity->getActivityKey() : '';
$otherPerson = $otherPerson ? 'to '.$otherPerson->getDisplayName() : '';
$token = Session::get('token_id') ? ' ('.trans('texts.token').')' : '';

View File

@ -44,7 +44,7 @@ class EntityModel extends Eloquent
public function getActivityKey()
{
return $this->getEntityType().':'.$this->public_id.':'.$this->getName();
return '[' . $this->getEntityType().':'.$this->public_id.':'.$this->getName() . ']';
}
/*

View File

@ -44,7 +44,7 @@ class AccountRepository
}
$user->confirmed = !Utils::isNinja();
$user->registered = !Utils::isNinja();
$user->registered = !Utils::isNinja() && $user->email;
if (!$user->confirmed) {
$user->confirmation_code = str_random(RANDOM_KEY_LENGTH);

View File

@ -270,9 +270,12 @@ class InvoiceRepository
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
$invoice->invoice_number = trim($data['invoice_number']);
$invoice->partial = round(Utils::parseFloat($data['partial']), 2);
$invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false;
$invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']);
if (!$publicId) {
$invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false;
}
if ($invoice->is_recurring) {
$invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0;
$invoice->start_date = Utils::toSqlDate($data['start_date']);

View File

@ -20,7 +20,7 @@ class AddPartialAmountToInvoices extends Migration {
Schema::table('accounts', function($table)
{
$table->boolean('utf8_invoices')->default(false);
$table->boolean('auto_wrap')->default(true);
$table->boolean('auto_wrap')->default(false);
$table->string('subdomain')->nullable();
});
}

View File

@ -2421,7 +2421,7 @@ display: block;
width: 100%;
height: 40px;
padding: 9px 12px;
font-size: 14px;
font-size: 16px;
line-height: 1.42857143;
color: #000 !important;
background: #f9f9f9 !important;

View File

@ -37,7 +37,7 @@ display: block;
width: 100%;
height: 40px;
padding: 9px 12px;
font-size: 14px;
font-size: 16px;
line-height: 1.42857143;
color: #000 !important;
background: #f9f9f9 !important;

View File

@ -33231,7 +33231,7 @@ function subtotals(invoice)
function accountDetails(account) {
var data = [];
if(account.name) data.push({text:account.name, style:'accountDetails'});
if(account.name) data.push({text:account.name, style:'accountName'});
if(account.id_number) data.push({text:account.id_number, style:'accountDetails'});
if(account.vat_number) data.push({text:account.vat_number, style:'accountDetails'});
if(account.work_email) data.push({text:account.work_email, style:'accountDetails'});
@ -33240,15 +33240,66 @@ function subtotals(invoice)
}
function accountAddress(account) {
var address = '';
if (account.city || account.state || account.postal_code) {
address = ((account.city ? account.city + ', ' : '') + account.state + ' ' + account.postal_code).trim();
}
var data = [];
if(account.address1) data.push({text:account.address1, style:'accountDetails'});
if(account.address2) data.push({text:account.address2, style:'accountDetails'});
if(account.city) data.push({text:account.city, style:'accountDetails'});
if(account.state) data.push({text:account.state, style:'accountDetails'});
if(account.postal_code) data.push({text:account.postal_code, style:'accountDetails'});
if(address) data.push({text:address, style:'accountDetails'});
if(account.country) data.push({text:account.country.name, style: 'accountDetails'});
return data;
}
function invoiceDetails(invoice) {
var data = [
[
invoice.is_quote ? invoiceLabels.quote_number : invoiceLabels.invoice_number,
{style: 'bold', text: invoice.invoice_number},
],
[
invoice.is_quote ? invoiceLabels.quote_date : invoiceLabels.invoice_date,
invoice.invoice_date,
],
[
invoice.is_quote ? invoiceLabels.total : invoiceLabels.balance_due,
formatMoney(invoice.balance_amount, invoice.client.currency_id),
],
];
return data;
}
function clientDetails(invoice) {
var client = invoice.client;
if (!client) {
return;
}
var fields = [
getClientDisplayName(client),
client.id_number,
client.vat_number,
concatStrings(client.address1, client.address2),
concatStrings(client.city, client.state, client.postal_code),
client.country ? client.country.name : false,
invoice.contact && getClientDisplayName(client) != invoice.contact.email ? invoice.contact.email : false,
invoice.client.custom_value1 ? invoice.account['custom_client_label1'] + ' ' + invoice.client.custom_value1 : false,
invoice.client.custom_value2 ? invoice.account['custom_client_label2'] + ' ' + invoice.client.custom_value2 : false,
];
var data = [];
for (var i=0; i<fields.length; i++) {
var field = fields[i];
if (!field) {
continue;
}
data.push(field);
}
return data;
}
function primaryColor( defaultColor) {
return NINJA.primaryColor?NINJA.primaryColor:defaultColor;
}

View File

@ -139,7 +139,7 @@ function subtotals(invoice)
function accountDetails(account) {
var data = [];
if(account.name) data.push({text:account.name, style:'accountDetails'});
if(account.name) data.push({text:account.name, style:'accountName'});
if(account.id_number) data.push({text:account.id_number, style:'accountDetails'});
if(account.vat_number) data.push({text:account.vat_number, style:'accountDetails'});
if(account.work_email) data.push({text:account.work_email, style:'accountDetails'});
@ -148,15 +148,66 @@ function subtotals(invoice)
}
function accountAddress(account) {
var address = '';
if (account.city || account.state || account.postal_code) {
address = ((account.city ? account.city + ', ' : '') + account.state + ' ' + account.postal_code).trim();
}
var data = [];
if(account.address1) data.push({text:account.address1, style:'accountDetails'});
if(account.address2) data.push({text:account.address2, style:'accountDetails'});
if(account.city) data.push({text:account.city, style:'accountDetails'});
if(account.state) data.push({text:account.state, style:'accountDetails'});
if(account.postal_code) data.push({text:account.postal_code, style:'accountDetails'});
if(address) data.push({text:address, style:'accountDetails'});
if(account.country) data.push({text:account.country.name, style: 'accountDetails'});
return data;
}
function invoiceDetails(invoice) {
var data = [
[
invoice.is_quote ? invoiceLabels.quote_number : invoiceLabels.invoice_number,
{style: 'bold', text: invoice.invoice_number},
],
[
invoice.is_quote ? invoiceLabels.quote_date : invoiceLabels.invoice_date,
invoice.invoice_date,
],
[
invoice.is_quote ? invoiceLabels.total : invoiceLabels.balance_due,
formatMoney(invoice.balance_amount, invoice.client.currency_id),
],
];
return data;
}
function clientDetails(invoice) {
var client = invoice.client;
if (!client) {
return;
}
var fields = [
getClientDisplayName(client),
client.id_number,
client.vat_number,
concatStrings(client.address1, client.address2),
concatStrings(client.city, client.state, client.postal_code),
client.country ? client.country.name : false,
invoice.contact && getClientDisplayName(client) != invoice.contact.email ? invoice.contact.email : false,
invoice.client.custom_value1 ? invoice.account['custom_client_label1'] + ' ' + invoice.client.custom_value1 : false,
invoice.client.custom_value2 ? invoice.account['custom_client_label2'] + ' ' + invoice.client.custom_value2 : false,
];
var data = [];
for (var i=0; i<fields.length; i++) {
var field = fields[i];
if (!field) {
continue;
}
data.push(field);
}
return data;
}
function primaryColor( defaultColor) {
return NINJA.primaryColor?NINJA.primaryColor:defaultColor;
}

View File

@ -20,23 +20,29 @@ var dd = {
},
{
text:(invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice).toUpperCase(),
margin: [8, 16, 8, 16],
style: 'primaryColor'
margin: [8, 70, 8, 16],
style: 'primaryColor',
fontSize: 11
},
{
style: 'tableExample',
table: {
headerRows: 1,
widths: ['auto', 'auto', '*'],
body: [
[invoice.is_quote ? invoiceLabels.quote_number:invoiceLabels.invoice_number, {style: 'bold', text: invoice.invoice_number}, ""],
[invoice.is_quote ? invoiceLabels.quote_date:invoiceLabels.invoice_date, invoice.invoice_date, ""],
[invoice.is_quote ? invoiceLabels.total : invoiceLabels.balance_due, formatMoney(invoice.balance_amount, invoice.client.currency_id), ""],
]
body: [[
{
table: {
body: invoiceDetails(invoice),
},
layout: 'noBorders',
},
clientDetails(invoice),
''
]]
},
layout: {
hLineWidth: function (i, node) {
return (i === 0 || i === node.table.body.length) ? 1 : 0;
return (i === 0 || i === node.table.body.length) ? .5 : 0;
},
vLineWidth: function (i, node) {
return 0;//(i === 0 || i === node.table.widths.length) ? 2 : 1;
@ -57,12 +63,12 @@ var dd = {
{
table: {
headerRows: 1,
widths: ['auto', '*', 'auto', 'auto', 'auto', 'auto'],
widths: ['15%', '*', 'auto', 'auto', 'auto', 'auto'],
body:invoiceLines(invoice),
},
layout: {
hLineWidth: function (i, node) {
return i === 0 ? 0 : 1;
return i === 0 ? 0 : .5;
},
vLineWidth: function (i, node) {
return 0;
@ -104,12 +110,12 @@ var dd = {
],
footer: function(){
f = [{ text:invoice.invoice_footer?invoice.invoice_footer:"", margin: [72, 0]}]
f = [{ text:invoice.invoice_footer?invoice.invoice_footer:"", margin: [40, 0]}]
if (!invoice.is_pro && logoImages.imageLogo1) {
f.push({
image: logoImages.imageLogo1,
width: 150,
margin: [72,0]
margin: [40,0]
});
}
return f;
@ -124,6 +130,10 @@ var dd = {
primaryColor:{
color: primaryColor('#299CC2')
},
accountName: {
margin: [4, 2, 4, 2],
color:primaryColor('#299CC2')
},
accountDetails: {
margin: [4, 2, 4, 2],
color: '#AAA9A9'
@ -178,5 +188,5 @@ var dd = {
margin: [0, 10, 0, 4]
}
},
pageMargins: [72, 40, 40, 80]
pageMargins: [40, 40, 40, 40]
};

View File

@ -650,5 +650,9 @@ return array(
'export' => 'Export',
'documentation' => 'Documentation',
'zapier' => 'Zapier <sup>Beta</sup>',
'recurring' => 'Recurring',
'last_invoice_sent' => 'Last invoice sent :date',
);

View File

@ -98,8 +98,10 @@
</div>
<div class="panel-body">
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
{!! Former::checkbox('auto_wrap')->text(trans('texts.enable')) !!}
{!! Former::checkbox('utf8_invoices')->text(trans('texts.enable')) !!}
<div style="display:none">
{!! Former::checkbox('auto_wrap')->text(trans('texts.enable')) !!}
</div>
</div>
</div>
</div>

View File

@ -14,7 +14,9 @@
<div class="pull-right">
{!! Button::normal(trans('texts.documentation'))->asLinkTo(NINJA_WEB_URL.'/knowledgebase/api-documentation/')->withAttributes(['target' => '_blank']) !!}
@if (Utils::isNinja())
{!! Button::normal(trans('texts.zapier'))->asLinkTo(ZAPIER_URL)->withAttributes(['target' => '_blank']) !!}
@endif
@if (Utils::isPro())
{!! Button::primary(trans('texts.add_token'))->asLinkTo('/tokens/create')->appendIcon(Icon::create('plus-sign')) !!}
@endif

View File

@ -90,7 +90,7 @@
@foreach ($pastDue as $invoice)
@if (!$invoice->client->trashed())
<tr>
<td>{{ $invoice->getLink() }}</td>
<td>{!! $invoice->getLink() !!}</td>
<td>{{ $invoice->client->getDisplayName() }}</td>
<td>{{ Utils::fromSqlDate($invoice->due_date) }}</td>
<td>{{ Utils::formatMoney($invoice->balance, $invoice->client->currency_id) }}</td>

View File

@ -19,6 +19,12 @@
}
}
@media screen and (max-width: 768px) {
body {
padding-top: 56px;
}
}
</style>
@include('script')

View File

@ -11,14 +11,6 @@
<script src="{{ asset('js/vfs_fonts.js') }}" type="text/javascript"></script>
@endif
<style type="text/css">
.partial div.checkbox {
display: inline;
}
.partial span.input-group-addon {
padding-right: 30px;
}
</style>
@stop
@section('content')
@ -86,9 +78,7 @@
{!! Former::text('due_date')->data_bind("datePicker: due_date, valueUpdate: 'afterkeydown'")
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'due_date\')"></i>') !!}
{!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown', enable: is_partial")
->onchange('onPartialChange()')->addGroupClass('partial')->append(Former::checkbox('is_partial')->raw()
->data_bind('checked: is_partial')->onclick('onPartialEnabled()') . '&nbsp;' . (trans('texts.enable'))) !!}
{!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown'")->onchange('onPartialChange()') !!}
</div>
@if ($entityType == ENTITY_INVOICE)
<div data-bind="visible: is_recurring" style="display: none">
@ -104,8 +94,24 @@
</div>
@else
<div data-bind="visible: invoice_status_id() === 0">
{!! Former::checkbox('recurring')->onclick('onRecurringEnabled()')->text(trans('texts.enable').' &nbsp;&nbsp; <a href="#" onclick="showLearnMore()"><i class="glyphicon glyphicon-question-sign"></i> '.trans('texts.learn_more').'</a>')->data_bind("checked: is_recurring")
->inlineHelp($invoice && $invoice->last_sent_date ? 'Last invoice sent ' . Utils::dateToString($invoice->last_sent_date) : '') !!}
<div class="form-group">
<label for="" class="control-label col-lg-4 col-sm-4">
{{ trans('texts.recurring') }}
</label>
<div class="col-lg-8 col-sm-8">
<div class="checkbox">
<label for="recurring" class="">
<input onclick="onRecurringEnabled()" data-bind="checked: is_recurring" id="recurring" type="checkbox" name="recurring" value="1">{{ trans('texts.enable') }} &nbsp;&nbsp;
<a href="#" onclick="showLearnMore()"><i class="glyphicon glyphicon-question-sign"></i> {{ trans('texts.learn_more') }}</a>
</label>
</div>
</div>
</div>
@if ($invoice && $invoice->last_sent_date)
<div class="pull-right">
{{ trans('texts.last_invoice_sent', ['date' => Utils::dateToString($invoice->last_sent_date)]) }}
</div>
@endif
</div>
@endif
@endif
@ -1130,7 +1136,6 @@
self.balance = ko.observable(0);
self.invoice_design_id = ko.observable({{ $account->utf8_invoices ? 1 : $account->invoice_design_id }});
self.partial = ko.observable(0);
self.is_partial = ko.observable(false);
self.custom_value1 = ko.observable(0);
self.custom_value2 = ko.observable(0);
@ -1336,7 +1341,7 @@
});
self.totals.total = ko.computed(function() {
return formatMoney(self.is_partial() ? self.partial() : self.totals.rawTotal(), self.client().currency_id());
return formatMoney(self.partial() ? self.partial() : self.totals.rawTotal(), self.client().currency_id());
});
self.onDragged = function(item) {
@ -1647,19 +1652,7 @@
{
var val = NINJA.parseFloat($('#partial').val());
val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0);
$('#partial').val(val);
}
function onPartialEnabled()
{
model.invoice().partial('');
refreshPDF();
if ($('#is_partial').prop('checked')) {
setTimeout(function() {
$('#partial').focus();
}, 1);
}
$('#partial').val(val || '');
}
function onRecurringEnabled()
@ -1702,9 +1695,6 @@
@if ($invoice)
var invoice = {!! $invoice !!};
ko.mapping.fromJS(invoice, model.invoice().mapping, model.invoice);
if (NINJA.parseFloat(model.invoice().partial())) {
model.invoice().is_partial(true);
}
var invitationContactIds = {!! json_encode($invitationContactIds) !!};
var client = clientMap[invoice.client.public_id];
if (client) { // in case it's deleted