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']; $data['proPlanPaid'] = $account['pro_plan_paid'];
} }
} }
return View::make('expenses.edit', $data); return View::make('expenses.edit', $data);
} }
@ -124,26 +124,70 @@ class ExpenseController extends BaseController
public function update(UpdateExpenseRequest $request) public function update(UpdateExpenseRequest $request)
{ {
$client = $this->expenseRepo->save($request->input()); $client = $this->expenseRepo->save($request->input());
Session::flash('message', trans('texts.updated_expense')); Session::flash('message', trans('texts.updated_expense'));
return redirect()->to('expenses'); return redirect()->to('expenses');
} }
public function store(CreateExpenseRequest $request) public function store(CreateExpenseRequest $request)
{ {
$expense = $this->expenseRepo->save($request->input()); $expense = $this->expenseRepo->save($request->input());
Session::flash('message', trans('texts.created_expense')); Session::flash('message', trans('texts.created_expense'));
return redirect()->to('expenses'); return redirect()->to('expenses');
} }
public function bulk() public function bulk()
{ {
$action = Input::get('action'); $action = Input::get('action');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->expenseService->bulk($ids, $action);
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) { if ($count > 0) {
$message = Utils::pluralize($action.'d_expense', $count); $message = Utils::pluralize($action.'d_expense', $count);
@ -152,7 +196,7 @@ class ExpenseController extends BaseController
return Redirect::to('expenses'); return Redirect::to('expenses');
} }
private static function getViewModel() private static function getViewModel()
{ {
return [ return [
@ -172,7 +216,7 @@ class ExpenseController extends BaseController
public function show($publicId) public function show($publicId)
{ {
$expense = Expense::withTrashed()->scope($publicId)->firstOrFail(); $expense = Expense::withTrashed()->scope($publicId)->firstOrFail();
if($expense) { if($expense) {
Utils::trackViewed($expense->getDisplayName(), 'expense'); Utils::trackViewed($expense->getDisplayName(), 'expense');
} }
@ -191,5 +235,5 @@ class ExpenseController extends BaseController
); );
return View::make('expenses.show', $data); return View::make('expenses.show', $data);
} }
} }

View File

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

View File

@ -1,8 +1,9 @@
<?php namespace app\Listeners; <?php namespace app\Listeners;
use Carbon; use Carbon;
use App\Models\Credit; use App\Models\Expense;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;
use App\Events\InvoiceWasDeleted;
use App\Ninja\Repositories\ExpenseRepository; use App\Ninja\Repositories\ExpenseRepository;
class ExpenseListener class ExpenseListener
@ -14,4 +15,11 @@ class ExpenseListener
{ {
$this->expenseRepo = $expenseRepo; $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', 'is_recurring' => 'boolean',
'has_tasks' => 'boolean', 'has_tasks' => 'boolean',
'auto_bill' => 'boolean', 'auto_bill' => 'boolean',
'has_expenses' => 'boolean',
]; ];
// used for custom invoice numbers // used for custom invoice numbers
@ -82,7 +83,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getDisplayName() 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() public function affectsBalance()
@ -136,7 +137,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return ($this->amount - $this->balance); return ($this->amount - $this->balance);
} }
public function trashed() public function trashed()
{ {
if ($this->client && $this->client->trashed()) { if ($this->client && $this->client->trashed()) {
@ -207,7 +208,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation->markSent($messageId); $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 // then we won't track it in the activity log
if (!$notify) { if (!$notify) {
return; return;
@ -358,6 +359,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'has_tasks', 'has_tasks',
'custom_text_value1', 'custom_text_value1',
'custom_text_value2', 'custom_text_value2',
'has_expenses',
]); ]);
$this->client->setVisible([ $this->client->setVisible([
@ -451,7 +453,7 @@ class Invoice extends EntityModel implements BalanceAffecting
// Fix for months with less than 31 days // Fix for months with less than 31 days
$transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig(); $transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
$transformerConfig->enableLastDayOfMonthFix(); $transformerConfig->enableLastDayOfMonthFix();
$transformer = new \Recurr\Transformer\ArrayTransformer(); $transformer = new \Recurr\Transformer\ArrayTransformer();
$transformer->setConfig($transformerConfig); $transformer->setConfig($transformerConfig);
$dates = $transformer->transform($rule); $dates = $transformer->transform($rule);
@ -477,7 +479,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (count($schedule) < 2) { if (count($schedule) < 2) {
return null; return null;
} }
return $schedule[1]->getStart(); return $schedule[1]->getStart();
} }
@ -539,7 +541,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (!$nextSendDate = $this->getNextSendDate()) { if (!$nextSendDate = $this->getNextSendDate()) {
return false; return false;
} }
return $this->account->getDateTime() >= $nextSendDate; return $this->account->getDateTime() >= $nextSendDate;
} }
*/ */

View File

@ -7,6 +7,7 @@ use App\Models\InvoiceItem;
use App\Models\Invitation; use App\Models\Invitation;
use App\Models\Product; use App\Models\Product;
use App\Models\Task; use App\Models\Task;
use App\Models\Expense;
use App\Services\PaymentService; use App\Services\PaymentService;
use App\Ninja\Repositories\BaseRepository; use App\Ninja\Repositories\BaseRepository;
@ -175,7 +176,7 @@ class InvoiceRepository extends BaseRepository
$table->addColumn('balance', function ($model) { $table->addColumn('balance', function ($model) {
return $model->partial > 0 ? return $model->partial > 0 ?
trans('texts.partial_remaining', [ 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) 'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)
]) : ]) :
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)) { if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_tasks = true; $invoice->has_tasks = true;
} }
if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_expenses = true;
}
} else { } else {
$invoice = Invoice::scope($publicId)->firstOrFail(); $invoice = Invoice::scope($publicId)->firstOrFail();
} }
@ -265,7 +269,7 @@ class InvoiceRepository extends BaseRepository
if (isset($data['po_number'])) { if (isset($data['po_number'])) {
$invoice->po_number = trim($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; $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']) { if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) {
@ -387,6 +391,14 @@ class InvoiceRepository extends BaseRepository
$task->save(); $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']) { if ($item['product_key']) {
$productKey = trim($item['product_key']); $productKey = trim($item['product_key']);
if (\Auth::user()->account->update_products && ! strtotime($productKey)) { if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
@ -395,7 +407,10 @@ class InvoiceRepository extends BaseRepository
$product = Product::createNew(); $product = Product::createNew();
$product->product_key = trim($item['product_key']); $product->product_key = trim($item['product_key']);
} }
$product->notes = $invoice->has_tasks ? '' : $item['notes']; $product->notes = $invoice->has_tasks ? '' : $item['notes'];
$product->notes = $invoice->has_expenses ? '' : $item['notes'];
$product->cost = $item['cost']; $product->cost = $item['cost'];
$product->save(); $product->save();
} }
@ -639,7 +654,7 @@ class InvoiceRepository extends BaseRepository
public function findNeedingReminding($account) public function findNeedingReminding($account)
{ {
$dates = []; $dates = [];
for ($i=1; $i<=3; $i++) { for ($i=1; $i<=3; $i++) {
if ($date = $account->getReminderDate($i)) { if ($date = $account->getReminderDate($i)) {
$field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date'; $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 = [ protected $defaultIncludes = [
'invoice_items', 'invoice_items',
]; ];
public function includeInvoiceItems(Invoice $invoice) public function includeInvoiceItems(Invoice $invoice)
{ {
$transformer = new InvoiceItemTransformer($this->account, $this->serializer); $transformer = new InvoiceItemTransformer($this->account, $this->serializer);
@ -70,6 +70,7 @@ class InvoiceTransformer extends EntityTransformer
'custom_value2' => $invoice->custom_value2, 'custom_value2' => $invoice->custom_value2,
'custom_taxes1' => (bool) $invoice->custom_taxes1, 'custom_taxes1' => (bool) $invoice->custom_taxes1,
'custom_taxes2' => (bool) $invoice->custom_taxes2, '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('account_id')->index();
$table->unsignedInteger('vendor_id')->nullable(); $table->unsignedInteger('vendor_id')->nullable();
$table->unsignedInteger('user_id'); $table->unsignedInteger('user_id');
$table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('invoice_client_id')->nullable(); $table->unsignedInteger('invoice_client_id')->nullable();
$table->boolean('is_deleted')->default(false); $table->boolean('is_deleted')->default(false);
$table->decimal('amount', 13, 2); $table->decimal('amount', 13, 2);
@ -38,7 +39,7 @@ class CreateExpensesTable extends Migration
// Relations // Relations
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
// Indexes // Indexes
$table->unsignedInteger('public_id')->index(); $table->unsignedInteger('public_id')->index();
$table->unique( array('account_id','public_id') ); $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_vendor' => 'Successfully deleted vendor',
'deleted_vendors' => 'Successfully deleted :count vendors', 'deleted_vendors' => 'Successfully deleted :count vendors',
// Emails // Emails
'confirmation_subject' => 'Invoice Ninja Account Confirmation', 'confirmation_subject' => 'Invoice Ninja Account Confirmation',
'confirmation_header' => 'Account Confirmation', 'confirmation_header' => 'Account Confirmation',
@ -911,7 +911,7 @@ return array(
'default_invoice_footer' => 'Default Invoice Footer', 'default_invoice_footer' => 'Default Invoice Footer',
'quote_footer' => 'Quote Footer', 'quote_footer' => 'Quote Footer',
'free' => 'Free', 'free' => 'Free',
'quote_is_approved' => 'This quote is approved', 'quote_is_approved' => 'This quote is approved',
'apply_credit' => 'Apply Credit', 'apply_credit' => 'Apply Credit',
'system_settings' => 'System Settings', 'system_settings' => 'System Settings',
@ -1026,7 +1026,7 @@ return array(
'archive_vendor' => 'Archive vendor', 'archive_vendor' => 'Archive vendor',
'delete_vendor' => 'Delete vendor', 'delete_vendor' => 'Delete vendor',
'view_vendor' => 'View vendor', 'view_vendor' => 'View vendor',
// Expenses // Expenses
'expense_amount' => 'Expense amount', 'expense_amount' => 'Expense amount',
'expense_balance' => 'Expense balance', 'expense_balance' => 'Expense balance',
@ -1051,11 +1051,14 @@ return array(
'view' => 'View', 'view' => 'View',
'restore_expense' => 'Restore expense', 'restore_expense' => 'Restore expense',
'invoice_expense' => 'Invoice', '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 // Payment terms
'num_days' => 'Number of days', 'num_days' => 'Number of days',
'create_payment_term' => 'Create payment term', 'create_payment_term' => 'Create payment term',
'edit_payment_terms' => 'Edit payment term', 'edit_payment_terms' => 'Edit payment term',
'edit_payment_term' => 'Edit payment term', 'edit_payment_term' => 'Edit payment term',
'archive_payment_term' => 'Archive 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>{!! link_to(($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'), trans('texts.' . ($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'))) !!}</li>
<li class="active">{{ $invoice->invoice_number }}</li> <li class="active">{{ $invoice->invoice_number }}</li>
@endif @endif
</ol> </ol>
@endif @endif
{!! Former::open($url) {!! Former::open($url)
@ -39,7 +39,7 @@
'client' => 'required', 'client' => 'required',
'invoice_number' => 'required', 'invoice_number' => 'required',
'product_key' => 'max:255' 'product_key' => 'max:255'
)) !!} )) !!}
@include('partials.autocomplete_fix') @include('partials.autocomplete_fix')
@ -60,7 +60,7 @@
<a id="editClientLink" class="pointer" data-bind="click: $root.showClientForm">{{ trans('texts.edit_client') }}</a> | <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']) !!} {!! link_to('/clients/'.$invoice->client->public_id, trans('texts.view_client'), ['target' => '_blank']) !!}
</div> </div>
</div> </div>
<div style="display:none"> <div style="display:none">
@endif @endif
@ -69,7 +69,7 @@
<div class="form-group" style="margin-bottom: 8px"> <div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4"> <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> <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> <a data-bind="attr: {href: '{{ url('/clients') }}/' + $root.invoice().client().public_id()}" target="_blank">{{ trans('texts.view_client') }}</a>
</span> </span>
</div> </div>
@ -85,20 +85,20 @@
<label class="checkbox" data-bind="attr: {for: $index() + '_check'}" onclick="refreshPDF(true)"> <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="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]'}"> <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> </label>
<span data-bind="html: $data.view_as_recipient"></span>&nbsp;&nbsp; <span data-bind="html: $data.view_as_recipient"></span>&nbsp;&nbsp;
@if (Utils::isConfirmed()) @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> data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign" <span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true}, 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> style: {color: $data.hasOwnProperty('invitation_viewed') &amp;&amp; $data.invitation_viewed() ? '#57D172':'#B1B5BA'}"></span>
@endif @endif
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4" id="col_2"> <div class="col-md-4" id="col_2">
<div data-bind="visible: !is_recurring()"> <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') !!} ->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")) {!! 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') !!} ->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()') {!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown'")->onchange('onPartialChange()')
->rel('tooltip')->data_toggle('tooltip')->data_placement('bottom')->title(trans('texts.partial_value')) !!} ->rel('tooltip')->data_toggle('tooltip')->data_placement('bottom')->title(trans('texts.partial_value')) !!}
</div> </div>
@ -127,7 +127,7 @@
@if ($entityType == ENTITY_INVOICE) @if ($entityType == ENTITY_INVOICE)
<div class="form-group" style="margin-bottom: 8px"> <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) @if ($invoice->recurring_invoice)
{!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!} {!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!}
@elseif ($invoice->id) @elseif ($invoice->id)
@ -151,7 +151,7 @@
{!! Former::text('invoice_number') {!! Former::text('invoice_number')
->label(trans("texts.{$entityType}_number_short")) ->label(trans("texts.{$entityType}_number_short"))
->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!} ->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!}
</span> </span>
<span data-bind="visible: is_recurring()" style="display: none"> <span data-bind="visible: is_recurring()" style="display: none">
{!! Former::checkbox('auto_bill') {!! Former::checkbox('auto_bill')
->label(trans('texts.auto_bill')) ->label(trans('texts.auto_bill'))
@ -163,7 +163,7 @@
->addGroupClass('discount-group')->type('number')->min('0')->step('any')->append( ->addGroupClass('discount-group')->type('number')->min('0')->step('any')->append(
Former::select('is_amount_discount')->addOption(trans('texts.discount_percent'), '0') Former::select('is_amount_discount')->addOption(trans('texts.discount_percent'), '0')
->addOption(trans('texts.discount_amount'), '1')->data_bind("value: is_amount_discount")->raw() ->addOption(trans('texts.discount_amount'), '1')->data_bind("value: is_amount_discount")->raw()
) !!} ) !!}
@if ($account->showCustomField('custom_invoice_text_label2', $invoice)) @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'") !!} {!! 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>
<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> 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: 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>
<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"/> style="text-align: right" class="form-control invoice-item"/>
</td> </td>
<td style="{{ $account->hide_quantity ? 'display:none' : '' }}"> <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"/> style="text-align: right" class="form-control invoice-item" name="quantity"/>
</td> </td>
<td style="display:none;" data-bind="visible: $root.invoice_item_taxes.show"> <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> <div class="line-total" data-bind="text: totals.total"></div>
</td> </td>
<td style="cursor:pointer" class="hide-border td-icon"> <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; $index() < ($parent.invoice_items().length - 1) &amp;&amp;
$parent.invoice_items().length > 1" class="fa fa-minus-circle redlink" title="Remove item"/> $parent.invoice_items().length > 1" class="fa fa-minus-circle redlink" title="Remove item"/>
</td> </td>
@ -245,7 +246,7 @@
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px"> <div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px">
{!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") {!! 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>
<div role="tabpanel" class="tab-pane" id="terms"> <div role="tabpanel" class="tab-pane" id="terms">
{!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'") {!! 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"> <tr style="display:none" data-bind="visible: $root.invoice_taxes.show">
<td class="hide-border" colspan="3"/> <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) @if (!$account->hide_quantity)
<td>{{ trans('texts.tax') }}</td> <td>{{ trans('texts.tax') }}</td>
@endif @endif
@ -369,7 +370,7 @@
</div> </div>
</div> </div>
</div> </div>
<p>&nbsp;</p> <p>&nbsp;</p>
<div class="form-actions"> <div class="form-actions">
@ -383,18 +384,19 @@
{!! Former::text('is_quote')->data_bind('value: is_quote') !!} {!! Former::text('is_quote')->data_bind('value: is_quote') !!}
{!! Former::text('has_tasks')->data_bind('value: has_tasks') !!} {!! Former::text('has_tasks')->data_bind('value: has_tasks') !!}
{!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!} {!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!}
{!! Former::text('has_expenses')->data_bind('value: has_expenses') !!}
{!! Former::text('pdfupload') !!} {!! Former::text('pdfupload') !!}
</div> </div>
@if (!Utils::isPro() || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST) @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') !!} {!! 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") !!} {!! 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 @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()) @if ($invoice->isClientTrashed())
<!-- do nothing --> <!-- do nothing -->
@elseif ($invoice->trashed()) @elseif ($invoice->trashed())
@ -448,7 +450,7 @@
{!! Former::text('client[vat_number]') {!! Former::text('client[vat_number]')
->label('vat_number') ->label('vat_number')
->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!} ->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!}
{!! Former::text('client[website]') {!! Former::text('client[website]')
->label('website') ->label('website')
->data_bind("value: website, valueUpdate: 'afterkeydown'") !!} ->data_bind("value: website, valueUpdate: 'afterkeydown'") !!}
@ -457,7 +459,7 @@
->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!} ->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!}
</span> </span>
@if (Auth::user()->isPro()) @if (Auth::user()->isPro())
@if ($account->custom_client_label1) @if ($account->custom_client_label1)
{!! Former::text('client[custom_value1]') {!! Former::text('client[custom_value1]')
->label($account->custom_client_label1) ->label($account->custom_client_label1)
@ -503,11 +505,11 @@
{!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][public_id]'}") !!} 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]'}") !!} attr: {name: 'client[contacts][' + \$index() + '][first_name]'}") !!}
{!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown',
attr: {name: 'client[contacts][' + \$index() + '][last_name]'}") !!} 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()}") attr: {name: 'client[contacts][' + \$index() + '][email]', id:'email'+\$index()}")
->addClass('client-email') !!} ->addClass('client-email') !!}
{!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown',
@ -517,7 +519,7 @@
<div class="col-lg-8 col-lg-offset-4"> <div class="col-lg-8 col-lg-offset-4">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1"> <span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">
{!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!} {!! 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"> <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')) !!} {!! link_to('#', trans('texts.add_contact').' +', array('data-bind'=>'click: $parent.addContact')) !!}
</span> </span>
@ -540,7 +542,7 @@
->placeholder($account->language ? $account->language->name : '') ->placeholder($account->language ? $account->language->name : '')
->label(trans('texts.language_id')) ->label(trans('texts.language_id'))
->data_bind('value: 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') {!! Former::select('client[payment_terms]')->addOption('','')->data_bind('value: payment_terms')
->fromQuery($paymentTerms, 'name', 'num_days') ->fromQuery($paymentTerms, 'name', 'num_days')
->label(trans('texts.payment_terms')) ->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> <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-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 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>
</div> </div>
</div> </div>
@ -587,7 +589,7 @@
<div class="modal-footer" style="margin-top: 0px"> <div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button> <button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -607,9 +609,9 @@
@include('invoices.knockout') @include('invoices.knockout')
<script type="text/javascript"> <script type="text/javascript">
var products = {!! $products !!}; var products = {!! $products !!};
var clients = {!! $clients !!}; var clients = {!! $clients !!};
var account = {!! Auth::user()->account !!}; var account = {!! Auth::user()->account !!};
var clientMap = {}; var clientMap = {};
@ -628,17 +630,17 @@
contact.send_invoice = true; contact.send_invoice = true;
} }
if (clientName != contactName) { if (clientName != contactName) {
$clientSelect.append(new Option(contactName, client.public_id)); $clientSelect.append(new Option(contactName, client.public_id));
} }
} }
clientMap[client.public_id] = client; clientMap[client.public_id] = client;
$clientSelect.append(new Option(clientName, client.public_id)); $clientSelect.append(new Option(clientName, client.public_id));
} }
@if ($data) @if ($data)
// this means we failed so we'll reload the previous state // this means we failed so we'll reload the previous state
window.model = new ViewModel({!! $data !!}); window.model = new ViewModel({!! $data !!});
@else @else
// otherwise create blank model // otherwise create blank model
window.model = new ViewModel(); window.model = new ViewModel();
@ -674,7 +676,7 @@
// move the blank invoice line item to the end // move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop(); var blank = model.invoice().invoice_items.pop();
var tasks = {!! $tasks !!}; var tasks = {!! $tasks !!};
for (var i=0; i<tasks.length; i++) { for (var i=0; i<tasks.length; i++) {
var task = tasks[i]; var task = tasks[i];
var item = model.invoice().addItem(); var item = model.invoice().addItem();
@ -685,6 +687,24 @@
model.invoice().invoice_items.push(blank); model.invoice().invoice_items.push(blank);
model.invoice().has_tasks(true); model.invoice().has_tasks(true);
@endif @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 @endif
model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate())); model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate()));
@ -702,7 +722,7 @@
ko.applyBindings(model); ko.applyBindings(model);
onItemChange(); onItemChange();
$('#country_id').combobox().on('change', function(e) { $('#country_id').combobox().on('change', function(e) {
var countryId = $('input[name=country_id]').val(); var countryId = $('input[name=country_id]').val();
@ -723,15 +743,15 @@
@if ($invoice->client && !$invoice->id) @if ($invoice->client && !$invoice->id)
$('input[name=client]').val({{ $invoice->client->public_id }}); $('input[name=client]').val({{ $invoice->client->public_id }});
@endif @endif
var $input = $('select#client'); var $input = $('select#client');
$input.combobox().on('change', function(e) { $input.combobox().on('change', function(e) {
var oldId = model.invoice().client().public_id(); var oldId = model.invoice().client().public_id();
var clientId = parseInt($('input[name=client]').val(), 10) || 0; var clientId = parseInt($('input[name=client]').val(), 10) || 0;
if (clientId > 0) { if (clientId > 0) {
var selected = clientMap[clientId]; var selected = clientMap[clientId];
model.loadClient(selected); 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)); $('.client-input').val(getClientDisplayName(selected));
// if there's an invoice number pattern we'll apply it now // if there's an invoice number pattern we'll apply it now
setInvoiceNumber(selected); setInvoiceNumber(selected);
@ -748,7 +768,7 @@
$('.client_select input.form-control').on('click', function() { $('.client_select input.form-control').on('click', function() {
model.showClientForm(); 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() { $('#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() { setTimeout(function() {
@ -766,7 +786,7 @@
(function (_field) { (function (_field) {
$('.' + _field + ' .input-group-addon').click(function() { $('.' + _field + ' .input-group-addon').click(function() {
toggleDatePicker(_field); toggleDatePicker(_field);
}); });
})(field); })(field);
} }
@ -775,7 +795,7 @@
@else @else
$('.client_select input.form-control').focus(); $('.client_select input.form-control').focus();
@endif @endif
$('#clientModal').on('shown.bs.modal', function () { $('#clientModal').on('shown.bs.modal', function () {
$('#client\\[name\\]').focus(); $('#client\\[name\\]').focus();
}).on('hidden.bs.modal', function () { }).on('hidden.bs.modal', function () {
@ -784,30 +804,34 @@
refreshPDF(true); refreshPDF(true);
} }
}) })
$('#relatedActions > button:first').click(function() { $('#relatedActions > button:first').click(function() {
onPaymentClick(); onPaymentClick();
}); });
$('label.radio').addClass('radio-inline'); $('label.radio').addClass('radio-inline');
@if ($invoice->client->id) @if ($invoice->client->id)
$input.trigger('change'); $input.trigger('change');
@else @else
refreshPDF(true); refreshPDF(true);
@endif @endif
var client = model.invoice().client(); var client = model.invoice().client();
setComboboxValue($('.client_select'), setComboboxValue($('.client_select'),
client.public_id(), client.public_id(),
client.name.display()); client.name.display());
@if (isset($tasks) && $tasks) @if (isset($tasks) && $tasks)
NINJA.formIsChanged = true; NINJA.formIsChanged = true;
@endif @endif
@if (isset($expenses) && $expenses)
NINJA.formIsChanged = true;
@endif
applyComboboxListeners(); applyComboboxListeners();
}); });
function applyComboboxListeners() { function applyComboboxListeners() {
var selectorStr = '.invoice-table input, .invoice-table textarea'; var selectorStr = '.invoice-table input, .invoice-table textarea';
@ -980,7 +1004,7 @@
var invoice = createInvoiceModel(); var invoice = createInvoiceModel();
var design = getDesignJavascript(); var design = getDesignJavascript();
if (!design) return; if (!design) return;
doc = generatePDF(invoice, design, true); doc = generatePDF(invoice, design, true);
doc.getDataUrl( function(pdfString){ doc.getDataUrl( function(pdfString){
$('#pdfupload').val(pdfString); $('#pdfupload').val(pdfString);
@ -1016,7 +1040,7 @@
} }
return isValid; return isValid;
} }
function isEmailValid() { function isEmailValid() {
var isValid = true; var isValid = true;
var sendTo = false; var sendTo = false;
@ -1046,7 +1070,7 @@
} }
function onConvertClick() { function onConvertClick() {
submitAction('convert'); submitAction('convert');
} }
@if ($invoice->id) @if ($invoice->id)
@ -1058,7 +1082,7 @@
window.location = '{{ URL::to('credits/create/' . $invoice->client->public_id . '/' . $invoice->public_id ) }}'; window.location = '{{ URL::to('credits/create/' . $invoice->client->public_id . '/' . $invoice->public_id ) }}';
} }
@endif @endif
function onArchiveClick() { function onArchiveClick() {
submitBulkAction('archive'); submitBulkAction('archive');
} }
@ -1066,7 +1090,7 @@
function onDeleteClick() { function onDeleteClick() {
if (confirm('{!! trans("texts.are_you_sure") !!}')) { if (confirm('{!! trans("texts.are_you_sure") !!}')) {
submitBulkAction('delete'); submitBulkAction('delete');
} }
} }
function formEnterClick(event) { function formEnterClick(event) {
@ -1084,9 +1108,9 @@
} }
} }
function clientModalEnterClick(event) { function clientModalEnterClick(event) {
if (event.keyCode === 13){ if (event.keyCode === 13){
event.preventDefault(); event.preventDefault();
model.clientFormComplete(); model.clientFormComplete();
return false; return false;
} }
@ -1113,7 +1137,7 @@
var oldVal = val; var oldVal = val;
val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0); val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0);
model.invoice().partial(val || ''); model.invoice().partial(val || '');
if (!silent && val != oldVal) { if (!silent && val != oldVal) {
$('#partial').tooltip('show'); $('#partial').tooltip('show');
setTimeout(function() { setTimeout(function() {

View File

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