Expense module

This commit is contained in:
steenrabol 2016-01-10 11:25:05 +01:00
parent d8cb1b436d
commit 3e9e28f06f
11 changed files with 287 additions and 140 deletions

View File

@ -111,7 +111,7 @@ class ExpenseController extends BaseController
$data['proPlanPaid'] = $account['pro_plan_paid'];
}
}
return View::make('expenses.edit', $data);
}
@ -124,26 +124,70 @@ class ExpenseController extends BaseController
public function update(UpdateExpenseRequest $request)
{
$client = $this->expenseRepo->save($request->input());
Session::flash('message', trans('texts.updated_expense'));
return redirect()->to('expenses');
}
public function store(CreateExpenseRequest $request)
{
$expense = $this->expenseRepo->save($request->input());
Session::flash('message', trans('texts.created_expense'));
return redirect()->to('expenses');
}
public function bulk()
{
$action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->expenseService->bulk($ids, $action);
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
switch($action)
{
case 'invoice':
$expenses = Expense::scope($ids)->get();
$clientPublicId = null;
$data = [];
// Validate that either all expenses do not have a client or if there is a client, it is the same client
foreach ($expenses as $expense)
{
if ($expense->client_id) {
if (!$clientPublicId) {
$clientPublicId = $expense->client_id;
} else if ($clientPublicId != $expense->client_id) {
Session::flash('error', trans('texts.expense_error_multiple_clients'));
return Redirect::to('expenses');
}
}
if ($expense->invoice_id) {
Session::flash('error', trans('texts.expense_error_invoiced'));
return Redirect::to('expenses');
}
if ($expense->should_be_invoiced == 0) {
Session::flash('error', trans('texts.expense_error_should_not_be_invoiced'));
return Redirect::to('expenses');
}
$account = Auth::user()->account;
$data[] = [
'publicId' => $expense->public_id,
'description' => $expense->public_notes,
'qty' => 1,
'cost' => $expense->amount,
];
}
return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data);
break;
default:
$count = $this->expenseService->bulk($ids, $action);
}
if ($count > 0) {
$message = Utils::pluralize($action.'d_expense', $count);
@ -152,7 +196,7 @@ class ExpenseController extends BaseController
return Redirect::to('expenses');
}
private static function getViewModel()
{
return [
@ -172,7 +216,7 @@ class ExpenseController extends BaseController
public function show($publicId)
{
$expense = Expense::withTrashed()->scope($publicId)->firstOrFail();
if($expense) {
Utils::trackViewed($expense->getDisplayName(), 'expense');
}
@ -191,5 +235,5 @@ class ExpenseController extends BaseController
);
return View::make('expenses.show', $data);
}
}
}

View File

@ -119,11 +119,11 @@ class InvoiceController extends BaseController
Session::put('invitation_key', $invitationKey); // track current invitation
$account->loadLocalizationSettings($client);
$invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date);
$invoice->due_date = Utils::fromSqlDate($invoice->due_date);
$invoice->is_pro = $account->isPro();
if ($invoice->invoice_design_id == CUSTOM_DESIGN) {
$invoice->invoice_design->javascript = $account->custom_design;
} else {
@ -204,7 +204,7 @@ class InvoiceController extends BaseController
->withTrashed()
->firstOrFail();
$entityType = $invoice->getEntityType();
$contactIds = DB::table('invitations')
->join('contacts', 'contacts.id', '=', 'invitations.contact_id')
->where('invitations.invoice_id', '=', $invoice->id)
@ -282,7 +282,7 @@ class InvoiceController extends BaseController
'actions' => $actions,
'lastSent' => $lastSent);
$data = array_merge($data, self::getViewModel());
if ($clone) {
$data['formIsChanged'] = true;
}
@ -318,14 +318,14 @@ class InvoiceController extends BaseController
$account = Auth::user()->account;
$entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE;
$clientId = null;
if ($clientPublicId) {
$clientId = Client::getPrivateId($clientPublicId);
}
$invoice = $account->createInvoice($entityType, $clientId);
$invoice->public_id = 0;
$data = [
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
'entityType' => $invoice->getEntityType(),
@ -335,7 +335,7 @@ class InvoiceController extends BaseController
'title' => trans('texts.new_invoice'),
];
$data = array_merge($data, self::getViewModel());
return View::make('invoices.edit', $data);
}
@ -380,6 +380,7 @@ class InvoiceController extends BaseController
'recurringHelp' => $recurringHelp,
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null,
];
}
@ -413,7 +414,7 @@ class InvoiceController extends BaseController
if ($action == 'email') {
return $this->emailInvoice($invoice, Input::get('pdfupload'));
}
return redirect()->to($invoice->getRoute());
}
@ -440,7 +441,7 @@ class InvoiceController extends BaseController
} elseif ($action == 'email') {
return $this->emailInvoice($invoice, Input::get('pdfupload'));
}
return redirect()->to($invoice->getRoute());
}

View File

@ -1,8 +1,9 @@
<?php namespace app\Listeners;
use Carbon;
use App\Models\Credit;
use App\Models\Expense;
use App\Events\PaymentWasDeleted;
use App\Events\InvoiceWasDeleted;
use App\Ninja\Repositories\ExpenseRepository;
class ExpenseListener
@ -14,4 +15,11 @@ class ExpenseListener
{
$this->expenseRepo = $expenseRepo;
}
public function deletedInvoice(InvoiceWasDeleted $event)
{
// Release any tasks associated with the deleted invoice
Expense::where('invoice_id', '=', $event->invoice->id)
->update(['invoice_id' => null]);
}
}

View File

@ -28,6 +28,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'is_recurring' => 'boolean',
'has_tasks' => 'boolean',
'auto_bill' => 'boolean',
'has_expenses' => 'boolean',
];
// used for custom invoice numbers
@ -82,7 +83,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getDisplayName()
{
return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
}
public function affectsBalance()
@ -136,7 +137,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return ($this->amount - $this->balance);
}
public function trashed()
{
if ($this->client && $this->client->trashed()) {
@ -207,7 +208,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation->markSent($messageId);
// if the user marks it as sent rather than acually sending it
// if the user marks it as sent rather than acually sending it
// then we won't track it in the activity log
if (!$notify) {
return;
@ -358,6 +359,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'has_tasks',
'custom_text_value1',
'custom_text_value2',
'has_expenses',
]);
$this->client->setVisible([
@ -451,7 +453,7 @@ class Invoice extends EntityModel implements BalanceAffecting
// Fix for months with less than 31 days
$transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
$transformerConfig->enableLastDayOfMonthFix();
$transformer = new \Recurr\Transformer\ArrayTransformer();
$transformer->setConfig($transformerConfig);
$dates = $transformer->transform($rule);
@ -477,7 +479,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (count($schedule) < 2) {
return null;
}
return $schedule[1]->getStart();
}
@ -539,7 +541,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (!$nextSendDate = $this->getNextSendDate()) {
return false;
}
return $this->account->getDateTime() >= $nextSendDate;
}
*/

View File

@ -7,6 +7,7 @@ use App\Models\InvoiceItem;
use App\Models\Invitation;
use App\Models\Product;
use App\Models\Task;
use App\Models\Expense;
use App\Services\PaymentService;
use App\Ninja\Repositories\BaseRepository;
@ -175,7 +176,7 @@ class InvoiceRepository extends BaseRepository
$table->addColumn('balance', function ($model) {
return $model->partial > 0 ?
trans('texts.partial_remaining', [
'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id),
'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id),
'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)
]) :
Utils::formatMoney($model->balance, $model->currency_id, $model->country_id);
@ -204,6 +205,9 @@ class InvoiceRepository extends BaseRepository
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_tasks = true;
}
if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_expenses = true;
}
} else {
$invoice = Invoice::scope($publicId)->firstOrFail();
}
@ -265,7 +269,7 @@ class InvoiceRepository extends BaseRepository
if (isset($data['po_number'])) {
$invoice->po_number = trim($data['po_number']);
}
$invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id;
if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) {
@ -387,6 +391,14 @@ class InvoiceRepository extends BaseRepository
$task->save();
}
if (isset($item['expense_public_id']) && $item['expense_public_id']) {
$expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail();
$expense->invoice_id = $invoice->id;
$expense->invoice_client_id = $invoice->client_id;
$expense->is_invoiced = true;
$expense->save();
}
if ($item['product_key']) {
$productKey = trim($item['product_key']);
if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
@ -395,7 +407,10 @@ class InvoiceRepository extends BaseRepository
$product = Product::createNew();
$product->product_key = trim($item['product_key']);
}
$product->notes = $invoice->has_tasks ? '' : $item['notes'];
$product->notes = $invoice->has_expenses ? '' : $item['notes'];
$product->cost = $item['cost'];
$product->save();
}
@ -639,7 +654,7 @@ class InvoiceRepository extends BaseRepository
public function findNeedingReminding($account)
{
$dates = [];
for ($i=1; $i<=3; $i++) {
if ($date = $account->getReminderDate($i)) {
$field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';

View File

@ -23,7 +23,7 @@ class InvoiceTransformer extends EntityTransformer
protected $defaultIncludes = [
'invoice_items',
];
public function includeInvoiceItems(Invoice $invoice)
{
$transformer = new InvoiceItemTransformer($this->account, $this->serializer);
@ -70,6 +70,7 @@ class InvoiceTransformer extends EntityTransformer
'custom_value2' => $invoice->custom_value2,
'custom_taxes1' => (bool) $invoice->custom_taxes1,
'custom_taxes2' => (bool) $invoice->custom_taxes2,
'has_expenses' => (bool) $invoice->has_expenses,
];
}
}
}

View File

@ -23,6 +23,7 @@ class CreateExpensesTable extends Migration
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('vendor_id')->nullable();
$table->unsignedInteger('user_id');
$table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('invoice_client_id')->nullable();
$table->boolean('is_deleted')->default(false);
$table->decimal('amount', 13, 2);
@ -38,7 +39,7 @@ class CreateExpensesTable extends Migration
// Relations
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
// Indexes
$table->unsignedInteger('public_id')->index();
$table->unique( array('account_id','public_id') );

View File

@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddInvoicesHasExpenses extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('invoices', function(Blueprint $table)
{
$table->boolean('has_expenses')->default(false);
});
$invoices = DB::table('invoices')
->join('expenses', 'expenses.invoice_id', '=', 'invoices.id')
->selectRaw('DISTINCT invoices.id')
->get();
foreach ($invoices as $invoice) {
DB::table('invoices')
->where('id', $invoice->id)
->update(['has_tasks' => true]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('invoices', function(Blueprint $table)
{
$table->dropColumn('has_expenses');
});
}
}

View File

@ -263,7 +263,7 @@ return array(
'deleted_vendor' => 'Successfully deleted vendor',
'deleted_vendors' => 'Successfully deleted :count vendors',
// Emails
'confirmation_subject' => 'Invoice Ninja Account Confirmation',
'confirmation_header' => 'Account Confirmation',
@ -911,7 +911,7 @@ return array(
'default_invoice_footer' => 'Default Invoice Footer',
'quote_footer' => 'Quote Footer',
'free' => 'Free',
'quote_is_approved' => 'This quote is approved',
'apply_credit' => 'Apply Credit',
'system_settings' => 'System Settings',
@ -1026,7 +1026,7 @@ return array(
'archive_vendor' => 'Archive vendor',
'delete_vendor' => 'Delete vendor',
'view_vendor' => 'View vendor',
// Expenses
'expense_amount' => 'Expense amount',
'expense_balance' => 'Expense balance',
@ -1051,11 +1051,14 @@ return array(
'view' => 'View',
'restore_expense' => 'Restore expense',
'invoice_expense' => 'Invoice',
'expense_error_multiple_clients' =>'The expenses can\'t belong to different clients',
'expense_error_invoiced' => 'Expense have already been invoiced',
'expense_error_should_not_be_invoiced' => 'Expense maked not to be invoiced',
// Payment terms
'num_days' => 'Number of days',
'create_payment_term' => 'Create payment term',
'edit_payment_terms' => 'Edit payment term',
'edit_payment_term' => 'Edit payment term',
'archive_payment_term' => 'Archive payment term',
);

View File

@ -28,7 +28,7 @@
<li>{!! link_to(($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'), trans('texts.' . ($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'))) !!}</li>
<li class="active">{{ $invoice->invoice_number }}</li>
@endif
</ol>
</ol>
@endif
{!! Former::open($url)
@ -39,7 +39,7 @@
'client' => 'required',
'invoice_number' => 'required',
'product_key' => 'max:255'
)) !!}
)) !!}
@include('partials.autocomplete_fix')
@ -60,7 +60,7 @@
<a id="editClientLink" class="pointer" data-bind="click: $root.showClientForm">{{ trans('texts.edit_client') }}</a> |
{!! link_to('/clients/'.$invoice->client->public_id, trans('texts.view_client'), ['target' => '_blank']) !!}
</div>
</div>
</div>
<div style="display:none">
@endif
@ -69,7 +69,7 @@
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4">
<a id="createClientLink" class="pointer" data-bind="click: $root.showClientForm, html: $root.clientLinkText"></a>
<span data-bind="visible: $root.invoice().client().public_id() > 0" style="display:none">|
<span data-bind="visible: $root.invoice().client().public_id() > 0" style="display:none">|
<a data-bind="attr: {href: '{{ url('/clients') }}/' + $root.invoice().client().public_id()}" target="_blank">{{ trans('texts.view_client') }}</a>
</span>
</div>
@ -85,20 +85,20 @@
<label class="checkbox" data-bind="attr: {for: $index() + '_check'}" onclick="refreshPDF(true)">
<input type="hidden" value="0" data-bind="attr: {name: 'client[contacts][' + $index() + '][send_invoice]'}">
<input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check', name: 'client[contacts][' + $index() + '][send_invoice]'}">
<span data-bind="html: email.display"></span>
<span data-bind="html: email.display"></span>
</label>
<span data-bind="html: $data.view_as_recipient"></span>&nbsp;&nbsp;
@if (Utils::isConfirmed())
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
style: {color: $data.hasOwnProperty('invitation_viewed') &amp;&amp; $data.invitation_viewed() ? '#57D172':'#B1B5BA'}"></span>
@endif
</div>
</div>
</div>
</div>
<div class="col-md-4" id="col_2">
<div data-bind="visible: !is_recurring()">
@ -106,7 +106,7 @@
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('invoice_date') !!}
{!! Former::text('due_date')->data_bind("datePicker: due_date, valueUpdate: 'afterkeydown'")->label(trans("texts.{$entityType}_due_date"))
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('due_date') !!}
{!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown'")->onchange('onPartialChange()')
->rel('tooltip')->data_toggle('tooltip')->data_placement('bottom')->title(trans('texts.partial_value')) !!}
</div>
@ -127,7 +127,7 @@
@if ($entityType == ENTITY_INVOICE)
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
@if ($invoice->recurring_invoice)
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
@elseif ($invoice->id)
@ -151,7 +151,7 @@
{!! Former::text('invoice_number')
->label(trans("texts.{$entityType}_number_short"))
->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!}
</span>
</span>
<span data-bind="visible: is_recurring()" style="display: none">
{!! Former::checkbox('auto_bill')
->label(trans('texts.auto_bill'))
@ -163,7 +163,7 @@
->addGroupClass('discount-group')->type('number')->min('0')->step('any')->append(
Former::select('is_amount_discount')->addOption(trans('texts.discount_percent'), '0')
->addOption(trans('texts.discount_amount'), '1')->data_bind("value: is_amount_discount")->raw()
) !!}
) !!}
@if ($account->showCustomField('custom_invoice_text_label2', $invoice))
{!! Former::text('custom_text_value2')->label($account->custom_invoice_text_label2)->data_bind("value: custom_text_value2, valueUpdate: 'afterkeydown'") !!}
@ -200,16 +200,17 @@
!!}
</td>
<td>
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
rows="1" cols="60" style="resize: vertical" class="form-control word-wrap"></textarea>
<input type="text" data-bind="value: task_public_id, attr: {name: 'invoice_items[' + $index() + '][task_public_id]'}" style="display: none"/>
<input type="text" data-bind="value: expense_public_id, attr: {name: 'invoice_items[' + $index() + '][expense_public_id]'}" style="display: none"/>
</td>
<td>
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
style="text-align: right" class="form-control invoice-item"/>
</td>
<td style="{{ $account->hide_quantity ? 'display:none' : '' }}">
<input data-bind="value: prettyQty, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][qty]'}"
<input data-bind="value: prettyQty, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][qty]'}"
style="text-align: right" class="form-control invoice-item" name="quantity"/>
</td>
<td style="display:none;" data-bind="visible: $root.invoice_item_taxes.show">
@ -221,7 +222,7 @@
<div class="line-total" data-bind="text: totals.total"></div>
</td>
<td style="cursor:pointer" class="hide-border td-icon">
<i style="padding-left:2px" data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp;
<i style="padding-left:2px" data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp;
$index() < ($parent.invoice_items().length - 1) &amp;&amp;
$parent.invoice_items().length > 1" class="fa fa-minus-circle redlink" title="Remove item"/>
</td>
@ -245,7 +246,7 @@
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px">
{!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'")
->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!}
->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!}
</div>
<div role="tabpanel" class="tab-pane" id="terms">
{!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'")
@ -316,7 +317,7 @@
<tr style="display:none" data-bind="visible: $root.invoice_taxes.show">
<td class="hide-border" colspan="3"/>
<td style="display:none" class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td style="display:none" class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
@if (!$account->hide_quantity)
<td>{{ trans('texts.tax') }}</td>
@endif
@ -369,7 +370,7 @@
</div>
</div>
</div>
<p>&nbsp;</p>
<div class="form-actions">
@ -383,18 +384,19 @@
{!! Former::text('is_quote')->data_bind('value: is_quote') !!}
{!! Former::text('has_tasks')->data_bind('value: has_tasks') !!}
{!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!}
{!! Former::text('has_expenses')->data_bind('value: has_expenses') !!}
{!! Former::text('pdfupload') !!}
</div>
@if (!Utils::isPro() || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST)
{!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id")->addOption(trans('texts.more_designs') . '...', '-1') !!}
@else
@else
{!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!}
@endif
{!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!}
{!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!}
@if ($invoice->isClientTrashed())
<!-- do nothing -->
@elseif ($invoice->trashed())
@ -448,7 +450,7 @@
{!! Former::text('client[vat_number]')
->label('vat_number')
->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('client[website]')
->label('website')
->data_bind("value: website, valueUpdate: 'afterkeydown'") !!}
@ -457,7 +459,7 @@
->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!}
</span>
@if (Auth::user()->isPro())
@if (Auth::user()->isPro())
@if ($account->custom_client_label1)
{!! Former::text('client[custom_value1]')
->label($account->custom_client_label1)
@ -503,11 +505,11 @@
{!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][public_id]'}") !!}
{!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown',
{!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][first_name]'}") !!}
{!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][last_name]'}") !!}
{!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown',
{!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][email]', id:'email'+\$index()}")
->addClass('client-email') !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown',
@ -517,7 +519,7 @@
<div class="col-lg-8 col-lg-offset-4">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">
{!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!}
</span>
</span>
<span data-bind="visible: $index() === ($parent.contacts().length - 1)" class="pull-right greenlink bold">
{!! link_to('#', trans('texts.add_contact').' +', array('data-bind'=>'click: $parent.addContact')) !!}
</span>
@ -540,7 +542,7 @@
->placeholder($account->language ? $account->language->name : '')
->label(trans('texts.language_id'))
->data_bind('value: language_id')
->fromQuery($languages, 'name', 'id') !!}
->fromQuery($languages, 'name', 'id') !!}
{!! Former::select('client[payment_terms]')->addOption('','')->data_bind('value: payment_terms')
->fromQuery($paymentTerms, 'name', 'num_days')
->label(trans('texts.payment_terms'))
@ -565,9 +567,9 @@
<span class="error-block" id="emailError" style="display:none;float:left;font-weight:bold">{{ trans('texts.provide_name_or_email') }}</span><span>&nbsp;</span>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="button" class="btn btn-default" data-bind="click: $root.showMoreFields, text: $root.showMore() ? '{{ trans('texts.less_fields') }}' : '{{ trans('texts.more_fields') }}'"></button>
<button id="clientDoneButton" type="button" class="btn btn-primary" data-bind="click: $root.clientFormComplete">{{ trans('texts.done') }}</button>
<button id="clientDoneButton" type="button" class="btn btn-primary" data-bind="click: $root.clientFormComplete">{{ trans('texts.done') }}</button>
</div>
</div>
</div>
</div>
@ -587,7 +589,7 @@
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
@ -607,9 +609,9 @@
@include('invoices.knockout')
<script type="text/javascript">
var products = {!! $products !!};
var clients = {!! $clients !!};
var clients = {!! $clients !!};
var account = {!! Auth::user()->account !!};
var clientMap = {};
@ -628,17 +630,17 @@
contact.send_invoice = true;
}
if (clientName != contactName) {
$clientSelect.append(new Option(contactName, client.public_id));
$clientSelect.append(new Option(contactName, client.public_id));
}
}
clientMap[client.public_id] = client;
$clientSelect.append(new Option(clientName, client.public_id));
$clientSelect.append(new Option(clientName, client.public_id));
}
@if ($data)
// this means we failed so we'll reload the previous state
window.model = new ViewModel({!! $data !!});
@else
@else
// otherwise create blank model
window.model = new ViewModel();
@ -674,7 +676,7 @@
// move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop();
var tasks = {!! $tasks !!};
for (var i=0; i<tasks.length; i++) {
var task = tasks[i];
var item = model.invoice().addItem();
@ -685,6 +687,24 @@
model.invoice().invoice_items.push(blank);
model.invoice().has_tasks(true);
@endif
@if (isset($expenses) && $expenses)
// move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop();
var expenses = {!! $expenses !!};
for (var i=0; i<expenses.length; i++) {
var expense = expenses[i];
var item = model.invoice().addItem();
item.notes(expense.description);
item.qty(expense.qty);
item.expense_public_id(expense.publicId);
item.cost(expense.cost);
}
model.invoice().invoice_items.push(blank);
model.invoice().has_expenses(true);
@endif
@endif
model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate()));
@ -702,7 +722,7 @@
ko.applyBindings(model);
onItemChange();
$('#country_id').combobox().on('change', function(e) {
var countryId = $('input[name=country_id]').val();
@ -723,15 +743,15 @@
@if ($invoice->client && !$invoice->id)
$('input[name=client]').val({{ $invoice->client->public_id }});
@endif
var $input = $('select#client');
$input.combobox().on('change', function(e) {
var oldId = model.invoice().client().public_id();
var clientId = parseInt($('input[name=client]').val(), 10) || 0;
if (clientId > 0) {
if (clientId > 0) {
var selected = clientMap[clientId];
model.loadClient(selected);
// we enable searching by contact but the selection must be the client
// we enable searching by contact but the selection must be the client
$('.client-input').val(getClientDisplayName(selected));
// if there's an invoice number pattern we'll apply it now
setInvoiceNumber(selected);
@ -748,7 +768,7 @@
$('.client_select input.form-control').on('click', function() {
model.showClientForm();
});
}
}
$('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #start_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial, #custom_text_value1, #custom_text_value2').change(function() {
setTimeout(function() {
@ -766,7 +786,7 @@
(function (_field) {
$('.' + _field + ' .input-group-addon').click(function() {
toggleDatePicker(_field);
});
});
})(field);
}
@ -775,7 +795,7 @@
@else
$('.client_select input.form-control').focus();
@endif
$('#clientModal').on('shown.bs.modal', function () {
$('#client\\[name\\]').focus();
}).on('hidden.bs.modal', function () {
@ -784,30 +804,34 @@
refreshPDF(true);
}
})
$('#relatedActions > button:first').click(function() {
onPaymentClick();
});
$('label.radio').addClass('radio-inline');
@if ($invoice->client->id)
$input.trigger('change');
@else
@else
refreshPDF(true);
@endif
var client = model.invoice().client();
setComboboxValue($('.client_select'),
client.public_id(),
client.name.display());
setComboboxValue($('.client_select'),
client.public_id(),
client.name.display());
@if (isset($tasks) && $tasks)
NINJA.formIsChanged = true;
@endif
@if (isset($expenses) && $expenses)
NINJA.formIsChanged = true;
@endif
applyComboboxListeners();
});
});
function applyComboboxListeners() {
var selectorStr = '.invoice-table input, .invoice-table textarea';
@ -980,7 +1004,7 @@
var invoice = createInvoiceModel();
var design = getDesignJavascript();
if (!design) return;
doc = generatePDF(invoice, design, true);
doc.getDataUrl( function(pdfString){
$('#pdfupload').val(pdfString);
@ -1016,7 +1040,7 @@
}
return isValid;
}
function isEmailValid() {
var isValid = true;
var sendTo = false;
@ -1046,7 +1070,7 @@
}
function onConvertClick() {
submitAction('convert');
submitAction('convert');
}
@if ($invoice->id)
@ -1058,7 +1082,7 @@
window.location = '{{ URL::to('credits/create/' . $invoice->client->public_id . '/' . $invoice->public_id ) }}';
}
@endif
function onArchiveClick() {
submitBulkAction('archive');
}
@ -1066,7 +1090,7 @@
function onDeleteClick() {
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
submitBulkAction('delete');
}
}
}
function formEnterClick(event) {
@ -1084,9 +1108,9 @@
}
}
function clientModalEnterClick(event) {
function clientModalEnterClick(event) {
if (event.keyCode === 13){
event.preventDefault();
event.preventDefault();
model.clientFormComplete();
return false;
}
@ -1113,7 +1137,7 @@
var oldVal = val;
val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0);
model.invoice().partial(val || '');
if (!silent && val != oldVal) {
$('#partial').tooltip('show');
setTimeout(function() {

View File

@ -29,8 +29,8 @@ function ViewModel(data) {
if (paymentTerms == -1) paymentTerms = 0;
var dueDate = $('#invoice_date').datepicker('getDate');
dueDate.setDate(dueDate.getDate() + paymentTerms);
self.invoice().due_date(dueDate);
// We're using the datepicker to handle the date formatting
self.invoice().due_date(dueDate);
// We're using the datepicker to handle the date formatting
self.invoice().due_date($('#due_date').val());
}
@endif
@ -51,7 +51,7 @@ function ViewModel(data) {
return new TaxRateModel(options.data);
}
},
}
}
if (data) {
ko.mapping.fromJS(data, self.mapping, self);
@ -63,7 +63,7 @@ function ViewModel(data) {
}
if (self.invoice().tax_rate() > 0) {
return true;
}
}
return false;
});
@ -82,7 +82,7 @@ function ViewModel(data) {
self.addTaxRate = function(data) {
var itemModel = new TaxRateModel(data);
self.tax_rates.push(itemModel);
self.tax_rates.push(itemModel);
applyComboboxListeners();
}
@ -200,7 +200,7 @@ function InvoiceModel(data) {
self.id = ko.observable('');
self.discount = ko.observable('');
self.is_amount_discount = ko.observable(0);
self.frequency_id = ko.observable(4); // default to monthly
self.frequency_id = ko.observable(4); // default to monthly
self.terms = ko.observable('');
self.default_terms = ko.observable(account.{{ $entityType }}_terms);
self.terms_placeholder = ko.observable({{ !$invoice->id && $account->{"{$entityType}_terms"} ? "account.{$entityType}_terms" : false}});
@ -229,6 +229,7 @@ function InvoiceModel(data) {
self.invoice_design_id = ko.observable(1);
self.partial = ko.observable(0);
self.has_tasks = ko.observable();
self.has_expenses = ko.observable();
self.custom_value1 = ko.observable(0);
self.custom_value2 = ko.observable(0);
@ -260,7 +261,7 @@ function InvoiceModel(data) {
@if ($account->hide_quantity)
itemModel.qty(1);
@endif
self.invoice_items.push(itemModel);
self.invoice_items.push(itemModel);
applyComboboxListeners();
return itemModel;
}
@ -286,11 +287,11 @@ function InvoiceModel(data) {
},
write: function(value) {
if (value) {
self._tax(value);
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
} else {
self._tax(false);
self._tax(false);
self.tax_name('');
self.tax_rate(0);
}
@ -310,7 +311,7 @@ function InvoiceModel(data) {
self.wrapped_notes = ko.computed({
read: function() {
read: function() {
return this.public_notes();
},
write: function(value) {
@ -361,7 +362,7 @@ function InvoiceModel(data) {
if (parseInt(self.is_amount_discount())) {
return roundToTwo(self.discount());
} else {
return roundToTwo(self.totals.rawSubtotal() * (self.discount()/100));
return roundToTwo(self.totals.rawSubtotal() * (self.discount()/100));
}
});
@ -416,7 +417,7 @@ function InvoiceModel(data) {
} else {
taxes[key] = {name:item.tax_name(), rate:item.tax_rate(), amount:taxAmount};
}
}
}
}
return taxes;
});
@ -432,24 +433,24 @@ function InvoiceModel(data) {
return count > 0;
});
self.totals.itemTaxRates = ko.computed(function() {
self.totals.itemTaxRates = ko.computed(function() {
var taxes = self.totals.itemTaxes();
var parts = [];
var parts = [];
for (var key in taxes) {
if (taxes.hasOwnProperty(key)) {
parts.push(taxes[key].name + ' ' + (taxes[key].rate*1) + '%');
}
}
}
return parts.join('<br/>');
});
self.totals.itemTaxAmounts = ko.computed(function() {
var taxes = self.totals.itemTaxes();
var parts = [];
var parts = [];
for (var key in taxes) {
if (taxes.hasOwnProperty(key)) {
parts.push(self.formatMoney(taxes[key].amount));
}
}
}
return parts.join('<br/>');
});
@ -464,7 +465,7 @@ function InvoiceModel(data) {
});
self.totals.rawTotal = ko.computed(function() {
var total = accounting.toFixed(self.totals.rawSubtotal(),2);
var total = accounting.toFixed(self.totals.rawSubtotal(),2);
var discount = self.totals.rawDiscounted();
total -= discount;
@ -509,7 +510,7 @@ function InvoiceModel(data) {
self.totals.total = ko.computed(function() {
return self.formatMoney(self.partial() ? self.partial() : self.totals.rawTotal());
});
});
self.onDragged = function(item) {
refreshPDF(true);
@ -569,7 +570,7 @@ function ClientModel(data) {
}
self.removeContact = function() {
self.contacts.remove(this);
self.contacts.remove(this);
}
self.name.display = ko.computed(function() {
@ -577,13 +578,13 @@ function ClientModel(data) {
return self.name();
}
if (self.contacts().length == 0) return;
var contact = self.contacts()[0];
var contact = self.contacts()[0];
if (contact.first_name() || contact.last_name()) {
return contact.first_name() + ' ' + contact.last_name();
return contact.first_name() + ' ' + contact.last_name();
} else {
return contact.email();
}
});
});
self.name.placeholder = ko.computed(function() {
if (self.contacts().length == 0) return '';
@ -593,13 +594,13 @@ function ClientModel(data) {
} else {
return contact.email();
}
});
});
if (data) {
ko.mapping.fromJS(data, {}, this);
} else {
self.addContact();
}
}
}
function ContactModel(data) {
@ -608,7 +609,7 @@ function ContactModel(data) {
self.first_name = ko.observable('');
self.last_name = ko.observable('');
self.email = ko.observable('');
self.phone = ko.observable('');
self.phone = ko.observable('');
self.send_invoice = ko.observable(false);
self.invitation_link = ko.observable('');
self.invitation_status = ko.observable('');
@ -623,10 +624,10 @@ function ContactModel(data) {
var str = '';
if (self.first_name() || self.last_name()) {
str += self.first_name() + ' ' + self.last_name() + '\n';
}
}
if (self.email()) {
str += self.email() + '\n';
}
}
return str;
});
@ -635,9 +636,9 @@ function ContactModel(data) {
var str = '';
if (self.first_name() || self.last_name()) {
str += self.first_name() + ' ' + self.last_name() + '<br/>';
}
}
if (self.email()) {
str += self.email() + '<br/>';
str += self.email() + '<br/>';
}
return str;
});
@ -651,7 +652,7 @@ function ContactModel(data) {
@endif
return str;
});
});
}
function TaxRateModel(data) {
@ -675,7 +676,7 @@ function TaxRateModel(data) {
this.rate(value);
},
owner: this
});
});
self.displayName = ko.computed({
@ -687,8 +688,8 @@ function TaxRateModel(data) {
write: function (value) {
// do nothing
},
owner: this
});
owner: this
});
self.hideActions = function() {
self.actionsVisible(false);
@ -696,15 +697,15 @@ function TaxRateModel(data) {
self.showActions = function() {
self.actionsVisible(true);
}
}
self.isEmpty = function() {
return !self.rate() && !self.name();
}
}
}
function ItemModel(data) {
var self = this;
var self = this;
self.product_key = ko.observable('');
self.notes = ko.observable('');
self.cost = ko.observable(0);
@ -712,6 +713,7 @@ function ItemModel(data) {
self.tax_name = ko.observable('');
self.tax_rate = ko.observable(0);
self.task_public_id = ko.observable('');
self.expense_public_id = ko.observable('');
self.actionsVisible = ko.observable(false);
self._tax = ko.observable();
@ -720,7 +722,7 @@ function ItemModel(data) {
return self._tax();
},
write: function(value) {
self._tax(value);
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
}
@ -734,7 +736,7 @@ function ItemModel(data) {
this.qty(value);
},
owner: this
});
});
this.prettyCost = ko.computed({
read: function () {
@ -744,7 +746,7 @@ function ItemModel(data) {
this.cost(value);
},
owner: this
});
});
self.mapping = {
'tax': {
@ -755,7 +757,7 @@ function ItemModel(data) {
}
if (data) {
ko.mapping.fromJS(data, self.mapping, this);
ko.mapping.fromJS(data, self.mapping, this);
}
self.wrapped_notes = ko.computed({
@ -775,7 +777,7 @@ function ItemModel(data) {
this.totals.rawTotal = ko.computed(function() {
var cost = roundToTwo(NINJA.parseFloat(self.cost()));
var qty = roundToTwo(NINJA.parseFloat(self.qty()));
var value = cost * qty;
var value = cost * qty;
return value ? roundToTwo(value) : 0;
});
@ -806,4 +808,4 @@ function ItemModel(data) {
this.onSelect = function() {}
}
</script>
</script>