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();
$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)];
}
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);
}
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/activities/{client_id?}', 'ActivityController@getDatatable');
Route::post('clients/bulk', 'ClientController@bulk');
Route::get('clients/statement/{client_id}', 'ClientController@statement');
Route::resource('tasks', 'TaskController');
Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable');

View File

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

View File

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

View File

@ -41,6 +41,11 @@ class AddInclusiveTaxes extends Migration
{
$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');
});
Schema::table('date_formats', function ($table)
{
$table->dropColumn('format_moment');
});
}
}

View File

@ -11,19 +11,19 @@ class DateFormatsSeeder extends Seeder
// Date formats
$formats = [
['format' => 'd/M/Y', 'picker_format' => 'dd/M/yyyy'],
['format' => 'd-M-Y', 'picker_format' => 'dd-M-yyyy'],
['format' => 'd/F/Y', 'picker_format' => 'dd/MM/yyyy'],
['format' => 'd-F-Y', 'picker_format' => 'dd-MM-yyyy'],
['format' => 'M j, Y', 'picker_format' => 'M d, yyyy'],
['format' => 'F j, Y', 'picker_format' => 'MM d, yyyy'],
['format' => 'D M j, Y', 'picker_format' => 'D MM d, yyyy'],
['format' => 'Y-m-d', 'picker_format' => 'yyyy-mm-dd'],
['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy'],
['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy'],
['format' => 'd.m.Y', 'picker_format' => 'dd.mm.yyyy'],
['format' => 'j. M. Y', 'picker_format' => 'd. M. yyyy'],
['format' => 'j. F Y', 'picker_format' => 'd. MM 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_moment' => 'DD-MMM-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_moment' => 'DD-MMMM-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_moment' => 'MMMM 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_moment' => 'YYYY-MM-DD'],
['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy', 'format_moment' => 'DD-MM-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_moment' => 'D.MM.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_moment' => 'DD. MMMM YYYY']
];
foreach ($formats as $format) {
@ -31,6 +31,7 @@ class DateFormatsSeeder extends Seeder
$record = DateFormat::whereRaw("BINARY `format`= ?", array($format['format']))->first();
if ($record) {
$record->picker_format = $format['picker_format'];
$record->format_moment = $format['format_moment'];
$record->save();
} else {
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),
'invoiceDetails': NINJA.invoiceDetails(invoice),
'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16,
'invoiceLineItems': NINJA.invoiceLines(invoice),
'invoiceLineItemColumns': NINJA.invoiceColumns(invoice),
'invoiceLineItems': invoice.is_statement ? NINJA.statementLines(invoice) : NINJA.invoiceLines(invoice),
'invoiceLineItemColumns': invoice.is_statement ? NINJA.statementColumns(invoice) : NINJA.invoiceColumns(invoice),
'invoiceDocuments' : isEdge ? [] : NINJA.invoiceDocuments(invoice),
'quantityWidth': NINJA.quantityWidth(invoice),
'taxWidth': NINJA.taxWidth(invoice),
'clientDetails': NINJA.clientDetails(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,
'subtotalsWithoutBalance': NINJA.subtotals(invoice, true),
'subtotalsBalance': NINJA.subtotalsBalance(invoice),
'balanceDue': formatMoneyInvoice(invoice.balance_amount, invoice),
'invoiceFooter': NINJA.invoiceFooter(invoice),
'invoiceNumber': invoice.invoice_number || ' ',
'entityType': invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice,
'entityTypeUC': (invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice).toUpperCase(),
'entityTaxType': invoice.is_quote ? invoiceLabels.tax_quote : invoiceLabels.tax_invoice,
'entityType': invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice,
'entityTypeUC': (invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.quote : invoiceLabels.invoice).toUpperCase(),
'entityTaxType': invoice.is_statement ? invoiceLabels.statement : invoice.is_quote ? invoiceLabels.tax_quote : invoiceLabels.tax_invoice,
'fontSize': NINJA.fontSize,
'fontSizeLarger': NINJA.fontSize + 1,
'fontSizeLargest': NINJA.fontSize + 2,
@ -281,6 +281,35 @@ NINJA.notesAndTerms = function(invoice)
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)
{
var account = invoice.account;
@ -489,6 +518,16 @@ NINJA.invoiceDocuments = function(invoice) {
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)
{
if (!invoice) {
@ -629,10 +668,14 @@ NINJA.renderInvoiceField = function(invoice, field) {
var account = invoice.account;
if (field == 'invoice.invoice_number') {
return [
{text: (invoice.is_quote ? invoiceLabels.quote_number : invoiceLabels.invoice_number), style: ['invoiceNumberLabel']},
{text: invoice.invoice_number, style: ['invoiceNumber']}
];
if (invoice.is_statement) {
return false;
} 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') {
return [
{text: invoiceLabels.po_number},
@ -640,7 +683,7 @@ NINJA.renderInvoiceField = function(invoice, field) {
];
} else if (field == 'invoice.invoice_date') {
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}
];
} else if (field == 'invoice.due_date') {

View File

@ -632,7 +632,7 @@ function calculateAmounts(invoice) {
// sum line item
for (var i=0; i<invoice.invoice_items.length; 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);
if (lineTotal) {
total += lineTotal;

View File

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

View File

@ -57,7 +57,7 @@
@endcan
@if ( ! $client->trashed())
@can('create', ENTITY_INVOICE)
{!! DropdownButton::primary(trans('texts.new_invoice'))
{!! DropdownButton::primary(trans('texts.view_statement'))
->withAttributes(['class'=>'primaryDropDown'])
->withContents($actionLinks)->split() !!}
@endcan
@ -292,7 +292,7 @@
window.location = '{{ URL::to('clients/' . $client->public_id . '/edit') }}';
});
$('.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

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
@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'])
<li style="width:100%;">
<div class="nav-footer">