1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-17 08:32:51 +01:00

Add client statements

This commit is contained in:
Hillel Coren 2017-01-23 17:00:44 +02:00
parent 3ed78fcaec
commit de6302d12a
14 changed files with 220 additions and 58 deletions

View File

@ -84,7 +84,10 @@ class ClientController extends BaseController
$user = Auth::user(); $user = Auth::user();
$actionLinks = []; $actionLinks = [];
if($user->can('create', ENTITY_TASK)){ if ($user->can('create', ENTITY_INVOICE)){
$actionLinks[] = ['label' => trans('texts.new_invoice'), 'url' => URL::to('/invoices/create/'.$client->public_id)];
}
if ($user->can('create', ENTITY_TASK)){
$actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)];
} }
if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) { if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) {
@ -215,4 +218,28 @@ class ClientController extends BaseController
return $this->returnBulk(ENTITY_CLIENT, $action, $ids); return $this->returnBulk(ENTITY_CLIENT, $action, $ids);
} }
public function statement()
{
$account = Auth::user()->account;
$client = Client::scope(request()->client_id)->with('contacts')->firstOrFail();
$invoice = $account->createInvoice(ENTITY_INVOICE);
$invoice->client = $client;
$invoice->date_format = $account->date_format ? $account->date_format->format_moment : 'MMM D, YYYY';
$invoice->invoice_items = Invoice::scope()
->with(['client'])
->whereClientId($client->id)
->invoices()
->whereIsPublic(true)
->where('balance', '>', 0)
->get();
$data = [
'showBreadcrumbs' => false,
'client' => $client,
'invoice' => $invoice,
];
return view('clients.statement', $data);
}
} }

View File

@ -133,6 +133,7 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('api/clients', 'ClientController@getDatatable'); Route::get('api/clients', 'ClientController@getDatatable');
Route::get('api/activities/{client_id?}', 'ActivityController@getDatatable'); Route::get('api/activities/{client_id?}', 'ActivityController@getDatatable');
Route::post('clients/bulk', 'ClientController@bulk'); Route::post('clients/bulk', 'ClientController@bulk');
Route::get('clients/statement/{client_id}', 'ClientController@statement');
Route::resource('tasks', 'TaskController'); Route::resource('tasks', 'TaskController');
Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable'); Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable');

View File

@ -128,7 +128,7 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
public function getFullName() public function getFullName()
{ {
if ($this->first_name || $this->last_name) { if ($this->first_name || $this->last_name) {
return $this->first_name.' '.$this->last_name; return trim($this->first_name.' '.$this->last_name);
} else { } else {
return ''; return '';
} }

View File

@ -209,6 +209,8 @@ trait PresentsInvoice
'adjustment', 'adjustment',
'tax_invoice', 'tax_invoice',
'tax_quote', 'tax_quote',
'statement',
'statement_date',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {

View File

@ -41,6 +41,11 @@ class AddInclusiveTaxes extends Migration
{ {
$table->text('notes')->nullable(); $table->text('notes')->nullable();
}); });
Schema::table('date_formats', function ($table)
{
$table->string('format_moment')->nullable();
});
} }
/** /**
@ -75,5 +80,10 @@ class AddInclusiveTaxes extends Migration
{ {
$table->dropColumn('notes'); $table->dropColumn('notes');
}); });
Schema::table('date_formats', function ($table)
{
$table->dropColumn('format_moment');
});
} }
} }

View File

@ -11,19 +11,19 @@ class DateFormatsSeeder extends Seeder
// Date formats // Date formats
$formats = [ $formats = [
['format' => 'd/M/Y', 'picker_format' => 'dd/M/yyyy'], ['format' => 'd/M/Y', 'picker_format' => 'dd/M/yyyy', 'format_moment' => 'DD/MMM/YYYY'],
['format' => 'd-M-Y', 'picker_format' => 'dd-M-yyyy'], ['format' => 'd-M-Y', 'picker_format' => 'dd-M-yyyy', 'format_moment' => 'DD-MMM-YYYY'],
['format' => 'd/F/Y', 'picker_format' => 'dd/MM/yyyy'], ['format' => 'd/F/Y', 'picker_format' => 'dd/MM/yyyy', 'format_moment' => 'DD/MMMM/YYYY'],
['format' => 'd-F-Y', 'picker_format' => 'dd-MM-yyyy'], ['format' => 'd-F-Y', 'picker_format' => 'dd-MM-yyyy', 'format_moment' => 'DD-MMMM-YYYY'],
['format' => 'M j, Y', 'picker_format' => 'M d, yyyy'], ['format' => 'M j, Y', 'picker_format' => 'M d, yyyy', 'format_moment' => 'MMM D, YYYY'],
['format' => 'F j, Y', 'picker_format' => 'MM d, yyyy'], ['format' => 'F j, Y', 'picker_format' => 'MM d, yyyy', 'format_moment' => 'MMMM D, YYYY'],
['format' => 'D M j, Y', 'picker_format' => 'D MM d, yyyy'], ['format' => 'D M j, Y', 'picker_format' => 'D MM d, yyyy', 'format_moment' => 'ddd MMM Do, YYYY'],
['format' => 'Y-m-d', 'picker_format' => 'yyyy-mm-dd'], ['format' => 'Y-m-d', 'picker_format' => 'yyyy-mm-dd', 'format_moment' => 'YYYY-MM-DD'],
['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy'], ['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy', 'format_moment' => 'DD-MM-YYYY'],
['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy'], ['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy', 'format_moment' => 'MM/DD/YYYY'],
['format' => 'd.m.Y', 'picker_format' => 'dd.mm.yyyy'], ['format' => 'd.m.Y', 'picker_format' => 'dd.mm.yyyy', 'format_moment' => 'D.MM.YYYY'],
['format' => 'j. M. Y', 'picker_format' => 'd. M. yyyy'], ['format' => 'j. M. Y', 'picker_format' => 'd. M. yyyy', 'format_moment' => 'DD. MMM. YYYY'],
['format' => 'j. F Y', 'picker_format' => 'd. MM yyyy'] ['format' => 'j. F Y', 'picker_format' => 'd. MM yyyy', 'format_moment' => 'DD. MMMM YYYY']
]; ];
foreach ($formats as $format) { foreach ($formats as $format) {
@ -31,6 +31,7 @@ class DateFormatsSeeder extends Seeder
$record = DateFormat::whereRaw("BINARY `format`= ?", array($format['format']))->first(); $record = DateFormat::whereRaw("BINARY `format`= ?", array($format['format']))->first();
if ($record) { if ($record) {
$record->picker_format = $format['picker_format']; $record->picker_format = $format['picker_format'];
$record->format_moment = $format['format_moment'];
$record->save(); $record->save();
} else { } else {
DateFormat::create($format); DateFormat::create($format);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -161,23 +161,23 @@ NINJA.decodeJavascript = function(invoice, javascript)
'accountAddress': NINJA.accountAddress(invoice), 'accountAddress': NINJA.accountAddress(invoice),
'invoiceDetails': NINJA.invoiceDetails(invoice), 'invoiceDetails': NINJA.invoiceDetails(invoice),
'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16, 'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16,
'invoiceLineItems': NINJA.invoiceLines(invoice), 'invoiceLineItems': invoice.is_statement ? NINJA.statementLines(invoice) : NINJA.invoiceLines(invoice),
'invoiceLineItemColumns': NINJA.invoiceColumns(invoice), 'invoiceLineItemColumns': invoice.is_statement ? NINJA.statementColumns(invoice) : NINJA.invoiceColumns(invoice),
'invoiceDocuments' : isEdge ? [] : NINJA.invoiceDocuments(invoice), 'invoiceDocuments' : isEdge ? [] : NINJA.invoiceDocuments(invoice),
'quantityWidth': NINJA.quantityWidth(invoice), 'quantityWidth': NINJA.quantityWidth(invoice),
'taxWidth': NINJA.taxWidth(invoice), 'taxWidth': NINJA.taxWidth(invoice),
'clientDetails': NINJA.clientDetails(invoice), 'clientDetails': NINJA.clientDetails(invoice),
'notesAndTerms': NINJA.notesAndTerms(invoice), 'notesAndTerms': NINJA.notesAndTerms(invoice),
'subtotals': NINJA.subtotals(invoice), 'subtotals': invoice.is_statement ? NINJA.statementSubtotals(invoice) : NINJA.subtotals(invoice),
'subtotalsHeight': (NINJA.subtotals(invoice).length * 16) + 16, 'subtotalsHeight': (NINJA.subtotals(invoice).length * 16) + 16,
'subtotalsWithoutBalance': NINJA.subtotals(invoice, true), 'subtotalsWithoutBalance': NINJA.subtotals(invoice, true),
'subtotalsBalance': NINJA.subtotalsBalance(invoice), 'subtotalsBalance': NINJA.subtotalsBalance(invoice),
'balanceDue': formatMoneyInvoice(invoice.balance_amount, invoice), 'balanceDue': formatMoneyInvoice(invoice.balance_amount, invoice),
'invoiceFooter': NINJA.invoiceFooter(invoice), 'invoiceFooter': NINJA.invoiceFooter(invoice),
'invoiceNumber': invoice.invoice_number || ' ', 'invoiceNumber': invoice.invoice_number || ' ',
'entityType': invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice, 'entityType': invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice,
'entityTypeUC': (invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice).toUpperCase(), 'entityTypeUC': (invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice).toUpperCase(),
'entityTaxType': invoice.is_quote ? invoiceLabels.tax_quote : invoiceLabels.tax_invoice, 'entityTaxType': invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.tax_quote : invoiceLabels.tax_invoice,
'fontSize': NINJA.fontSize, 'fontSize': NINJA.fontSize,
'fontSizeLarger': NINJA.fontSize + 1, 'fontSizeLarger': NINJA.fontSize + 1,
'fontSizeLargest': NINJA.fontSize + 2, 'fontSizeLargest': NINJA.fontSize + 2,
@ -281,6 +281,35 @@ NINJA.notesAndTerms = function(invoice)
return NINJA.prepareDataList(data, 'notesAndTerms'); return NINJA.prepareDataList(data, 'notesAndTerms');
} }
NINJA.statementColumns = function(invoice)
{
return ["22%", "22%", "22%", "17%", "17%"];
}
NINJA.statementLines = function(invoice)
{
var grid = [[]];
grid[0].push({text: invoiceLabels.invoice_number, style: ['tableHeader', 'invoiceNumberTableHeader']});
grid[0].push({text: invoiceLabels.invoice_date, style: ['tableHeader', 'invoiceDateTableHeader']});
grid[0].push({text: invoiceLabels.due_date, style: ['tableHeader', 'dueDateTableHeader']});
grid[0].push({text: invoiceLabels.total, style: ['tableHeader', 'totalTableHeader']});
grid[0].push({text: invoiceLabels.balance, style: ['tableHeader', 'balanceTableHeader']});
for (var i = 0; i < invoice.invoice_items.length; i++) {
var item = invoice.invoice_items[i];
var row = [];
grid.push([
{text: item.invoice_number, style:['invoiceNumber']},
{text: item.invoice_date && item.invoice_date != '0000-00-00' ? moment(item.invoice_date).format(invoice.date_format) : ' ', style:['invoiceDate']},
{text: item.due_date && item.due_date != '0000-00-00' ? moment(item.due_date).format(invoice.date_format) : ' ', style:['dueDate']},
{text: formatMoneyInvoice(item.amount, invoice), style:['subtotals']},
{text: formatMoneyInvoice(item.balance, invoice), style:['lineTotal']},
]);
}
return NINJA.prepareDataTable(grid, 'invoiceItems');
}
NINJA.invoiceColumns = function(invoice) NINJA.invoiceColumns = function(invoice)
{ {
var account = invoice.account; var account = invoice.account;
@ -489,6 +518,16 @@ NINJA.invoiceDocuments = function(invoice) {
return stack.length?{stack:stack}:[]; return stack.length?{stack:stack}:[];
} }
NINJA.statementSubtotals = function(invoice)
{
var data = [[
{ text: invoiceLabels.balance_due, style: ['subtotalsLabel', 'balanceDueLabel'] },
{ text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['subtotals', 'balanceDue'] }
]];
return NINJA.prepareDataPairs(data, 'subtotals');
}
NINJA.subtotals = function(invoice, hideBalance) NINJA.subtotals = function(invoice, hideBalance)
{ {
if (!invoice) { if (!invoice) {
@ -629,10 +668,14 @@ NINJA.renderInvoiceField = function(invoice, field) {
var account = invoice.account; var account = invoice.account;
if (field == 'invoice.invoice_number') { if (field == 'invoice.invoice_number') {
return [ if (invoice.is_statement) {
{text: (invoice.is_quote ? invoiceLabels.quote_number : invoiceLabels.invoice_number), style: ['invoiceNumberLabel']}, return false;
{text: invoice.invoice_number, style: ['invoiceNumber']} } else {
]; return [
{text: (invoice.is_quote ? invoiceLabels.quote_number : invoiceLabels.invoice_number), style: ['invoiceNumberLabel']},
{text: invoice.invoice_number, style: ['invoiceNumber']}
];
}
} else if (field == 'invoice.po_number') { } else if (field == 'invoice.po_number') {
return [ return [
{text: invoiceLabels.po_number}, {text: invoiceLabels.po_number},
@ -640,7 +683,7 @@ NINJA.renderInvoiceField = function(invoice, field) {
]; ];
} else if (field == 'invoice.invoice_date') { } else if (field == 'invoice.invoice_date') {
return [ return [
{text: (invoice.is_quote ? invoiceLabels.quote_date : invoiceLabels.invoice_date)}, {text: (invoice.is_statement ? invoiceLabels.statement_date : invoice.is_quote ? invoiceLabels.quote_date : invoiceLabels.invoice_date)},
{text: invoice.invoice_date} {text: invoice.invoice_date}
]; ];
} else if (field == 'invoice.due_date') { } else if (field == 'invoice.due_date') {

View File

@ -632,7 +632,7 @@ function calculateAmounts(invoice) {
// sum line item // sum line item
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 item = invoice.invoice_items[i];
var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty)); var lineTotal = invoice.is_statement ? roundToTwo(NINJA.parseFloat(item.balance)) : roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty));
lineTotal = roundToTwo(lineTotal); lineTotal = roundToTwo(lineTotal);
if (lineTotal) { if (lineTotal) {
total += lineTotal; total += lineTotal;

View File

@ -2341,7 +2341,9 @@ $LANG = array(
'group_when_sorted' => 'Group When Sorted', 'group_when_sorted' => 'Group When Sorted',
'group_dates_by' => 'Group Dates By', 'group_dates_by' => 'Group Dates By',
'year' => 'Year', 'year' => 'Year',
'view_statement' => 'View Statement',
'statement' => 'Statement',
'statement_date' => 'Statement Date',
); );
return $LANG; return $LANG;

View File

@ -57,7 +57,7 @@
@endcan @endcan
@if ( ! $client->trashed()) @if ( ! $client->trashed())
@can('create', ENTITY_INVOICE) @can('create', ENTITY_INVOICE)
{!! DropdownButton::primary(trans('texts.new_invoice')) {!! DropdownButton::primary(trans('texts.view_statement'))
->withAttributes(['class'=>'primaryDropDown']) ->withAttributes(['class'=>'primaryDropDown'])
->withContents($actionLinks)->split() !!} ->withContents($actionLinks)->split() !!}
@endcan @endcan
@ -292,7 +292,7 @@
window.location = '{{ URL::to('clients/' . $client->public_id . '/edit') }}'; window.location = '{{ URL::to('clients/' . $client->public_id . '/edit') }}';
}); });
$('.primaryDropDown:not(.dropdown-toggle)').click(function() { $('.primaryDropDown:not(.dropdown-toggle)').click(function() {
window.location = '{{ URL::to('invoices/create/' . $client->public_id ) }}'; window.location = '{{ URL::to('clients/statement/' . $client->public_id ) }}';
}); });
// load datatable data when tab is shown and remember last tab selected // load datatable data when tab is shown and remember last tab selected

View File

@ -0,0 +1,74 @@
@extends('header')
@section('head')
@parent
@include('money_script')
@foreach (Auth::user()->account->getFontFolders() as $font)
<script src="{{ asset('js/vfs_fonts/'.$font.'.js') }}" type="text/javascript"></script>
@endforeach
<script src="{{ asset('pdf.built.js') }}?no_cache={{ NINJA_VERSION }}" type="text/javascript"></script>
<script>
var invoiceDesigns = {!! \App\Models\InvoiceDesign::getDesigns() !!};
var invoiceFonts = {!! Cache::get('fonts') !!};
var currentInvoice = {!! $invoice !!};
var invoice = {!! $invoice !!};
function getPDFString(cb) {
invoice.is_statement = true;
invoice.image = window.accountLogo;
invoice.features = {
customize_invoice_design:{{ Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) ? 'true' : 'false' }},
remove_created_by:{{ Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }},
invoice_settings:{{ Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }}
};
/*
var invoiceDesignId = parseInt(invoice.invoice_design_id);
var invoiceDesign = _.findWhere(invoiceDesigns, {id: invoiceDesignId});
if (!invoiceDesign) {
invoiceDesign = invoiceDesigns[0];
}
*/
var invoiceDesign = invoiceDesigns[0];
generatePDF(invoice, invoiceDesign.javascript, true, cb);
}
$(function() {
refreshPDF();
});
function onDownloadClick() {
var doc = generatePDF(invoice, invoiceDesigns[0].javascript, true);
doc.save("{{ str_replace(' ', '_', trim($client->getDisplayName())) . '-' . trans('texts.statement') }}" + '.pdf');
}
</script>
@stop
@section('content')
<div class="pull-right">
{!! Button::normal(trans('texts.download_pdf'))
->withAttributes(['onclick' => 'onDownloadClick()'])
->appendIcon(Icon::create('download-alt')) !!}
{!! Button::primary(trans('texts.view_client'))
->asLinkTo($client->present()->url) !!}
</div>
<ol class="breadcrumb pull-left">
<li>{{ link_to('/clients', trans('texts.clients')) }}</li>
<li class='active'>{{ $client->getDisplayName() }}</li>
</ol>
<p>&nbsp;</p>
<p>&nbsp;</p>
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
@stop

View File

@ -515,7 +515,9 @@
]) ])
@endforeach @endforeach
@endif @endif
@include('partials.navigation_option', ['option' => 'reports']) @if (Auth::user()->is_admin)
@include('partials.navigation_option', ['option' => 'reports'])
@endif
@include('partials.navigation_option', ['option' => 'settings']) @include('partials.navigation_option', ['option' => 'settings'])
<li style="width:100%;"> <li style="width:100%;">
<div class="nav-footer"> <div class="nav-footer">