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:
parent
3ed78fcaec
commit
de6302d12a
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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 '';
|
||||
}
|
||||
|
@ -209,6 +209,8 @@ trait PresentsInvoice
|
||||
'adjustment',
|
||||
'tax_invoice',
|
||||
'tax_quote',
|
||||
'statement',
|
||||
'statement_date',
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
@ -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') {
|
||||
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') {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
74
resources/views/clients/statement.blade.php
Normal file
74
resources/views/clients/statement.blade.php
Normal 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> </p>
|
||||
<p> </p>
|
||||
|
||||
@include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800])
|
||||
|
||||
@stop
|
@ -515,7 +515,9 @@
|
||||
])
|
||||
@endforeach
|
||||
@endif
|
||||
@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">
|
||||
|
Loading…
Reference in New Issue
Block a user