mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 10:17:30 -04:00
Custom invoice/quote numbers
This commit is contained in:
parent
ab3de15381
commit
7ca98c9080
@ -157,6 +157,8 @@ class AccountController extends BaseController
|
|||||||
return self::showLocalization();
|
return self::showLocalization();
|
||||||
} elseif ($section == ACCOUNT_PAYMENTS) {
|
} elseif ($section == ACCOUNT_PAYMENTS) {
|
||||||
return self::showOnlinePayments();
|
return self::showOnlinePayments();
|
||||||
|
} elseif ($section == ACCOUNT_INVOICE_SETTINGS) {
|
||||||
|
return self::showInvoiceSettings();
|
||||||
} elseif ($section == ACCOUNT_IMPORT_EXPORT) {
|
} elseif ($section == ACCOUNT_IMPORT_EXPORT) {
|
||||||
return View::make('accounts.import_export', ['title' => trans('texts.import_export')]);
|
return View::make('accounts.import_export', ['title' => trans('texts.import_export')]);
|
||||||
} elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) {
|
} elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) {
|
||||||
@ -177,6 +179,29 @@ class AccountController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function showInvoiceSettings()
|
||||||
|
{
|
||||||
|
$account = Auth::user()->account;
|
||||||
|
$recurringHours = [];
|
||||||
|
|
||||||
|
for ($i=0; $i<24; $i++) {
|
||||||
|
if ($account->military_time) {
|
||||||
|
$format = 'H:i';
|
||||||
|
} else {
|
||||||
|
$format = 'g:i a';
|
||||||
|
}
|
||||||
|
$recurringHours[$i] = date($format, strtotime("{$i}:00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'account' => Account::with('users')->findOrFail(Auth::user()->account_id),
|
||||||
|
'title' => trans("texts.invoice_settings"),
|
||||||
|
'section' => ACCOUNT_INVOICE_SETTINGS,
|
||||||
|
'recurringHours' => $recurringHours
|
||||||
|
];
|
||||||
|
return View::make("accounts.invoice_settings", $data);
|
||||||
|
}
|
||||||
|
|
||||||
private function showCompanyDetails()
|
private function showCompanyDetails()
|
||||||
{
|
{
|
||||||
$data = [
|
$data = [
|
||||||
@ -487,18 +512,36 @@ class AccountController extends BaseController
|
|||||||
$account->custom_invoice_text_label1 = trim(Input::get('custom_invoice_text_label1'));
|
$account->custom_invoice_text_label1 = trim(Input::get('custom_invoice_text_label1'));
|
||||||
$account->custom_invoice_text_label2 = trim(Input::get('custom_invoice_text_label2'));
|
$account->custom_invoice_text_label2 = trim(Input::get('custom_invoice_text_label2'));
|
||||||
|
|
||||||
$account->invoice_number_prefix = Input::get('invoice_number_prefix');
|
|
||||||
$account->invoice_number_counter = Input::get('invoice_number_counter');
|
$account->invoice_number_counter = Input::get('invoice_number_counter');
|
||||||
$account->quote_number_prefix = Input::get('quote_number_prefix');
|
$account->quote_number_prefix = Input::get('quote_number_prefix');
|
||||||
$account->share_counter = Input::get('share_counter') ? true : false;
|
$account->share_counter = Input::get('share_counter') ? true : false;
|
||||||
|
|
||||||
$account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false;
|
$account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false;
|
||||||
$account->auto_wrap = Input::get('auto_wrap') ? true : false;
|
|
||||||
|
if (Input::has('recurring_hour')) {
|
||||||
|
$account->recurring_hour = Input::get('recurring_hour');
|
||||||
|
}
|
||||||
|
|
||||||
if (!$account->share_counter) {
|
if (!$account->share_counter) {
|
||||||
$account->quote_number_counter = Input::get('quote_number_counter');
|
$account->quote_number_counter = Input::get('quote_number_counter');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Input::get('invoice_number_type') == 'prefix') {
|
||||||
|
$account->invoice_number_prefix = trim(Input::get('invoice_number_prefix'));
|
||||||
|
$account->invoice_number_pattern = null;
|
||||||
|
} else {
|
||||||
|
$account->invoice_number_pattern = trim(Input::get('invoice_number_pattern'));
|
||||||
|
$account->invoice_number_prefix = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input::get('quote_number_type') == 'prefix') {
|
||||||
|
$account->quote_number_prefix = trim(Input::get('quote_number_prefix'));
|
||||||
|
$account->quote_number_pattern = null;
|
||||||
|
} else {
|
||||||
|
$account->quote_number_pattern = trim(Input::get('quote_number_pattern'));
|
||||||
|
$account->quote_number_prefix = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) {
|
if (!$account->share_counter && $account->invoice_number_prefix == $account->quote_number_prefix) {
|
||||||
Session::flash('error', trans('texts.invalid_counter'));
|
Session::flash('error', trans('texts.invalid_counter'));
|
||||||
return Redirect::to('settings/' . ACCOUNT_INVOICE_SETTINGS)->withInput();
|
return Redirect::to('settings/' . ACCOUNT_INVOICE_SETTINGS)->withInput();
|
||||||
|
@ -258,6 +258,13 @@ class ClientController extends BaseController
|
|||||||
$client->payment_terms = Input::get('payment_terms') ?: 0;
|
$client->payment_terms = Input::get('payment_terms') ?: 0;
|
||||||
$client->website = trim(Input::get('website'));
|
$client->website = trim(Input::get('website'));
|
||||||
|
|
||||||
|
if (Input::has('invoice_number_counter')) {
|
||||||
|
$client->invoice_number_counter = (int) Input::get('invoice_number_counter');
|
||||||
|
}
|
||||||
|
if (Input::has('quote_number_counter')) {
|
||||||
|
$client->invoice_number_counter = (int) Input::get('quote_number_counter');
|
||||||
|
}
|
||||||
|
|
||||||
$client->save();
|
$client->save();
|
||||||
|
|
||||||
$data = json_decode(Input::get('data'));
|
$data = json_decode(Input::get('data'));
|
||||||
|
@ -59,16 +59,6 @@ class InvoiceApiController extends Controller
|
|||||||
$data = Input::all();
|
$data = Input::all();
|
||||||
$error = null;
|
$error = null;
|
||||||
|
|
||||||
// check if the invoice number is set and unique
|
|
||||||
if (!isset($data['invoice_number']) && !isset($data['id'])) {
|
|
||||||
$data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber();
|
|
||||||
} else if (isset($data['invoice_number'])) {
|
|
||||||
$invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first();
|
|
||||||
if ($invoice) {
|
|
||||||
$error = trans('validation.unique', ['attribute' => 'texts.invoice_number']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($data['email'])) {
|
if (isset($data['email'])) {
|
||||||
$client = Client::scope()->whereHas('contacts', function($query) use ($data) {
|
$client = Client::scope()->whereHas('contacts', function($query) use ($data) {
|
||||||
$query->where('email', '=', $data['email']);
|
$query->where('email', '=', $data['email']);
|
||||||
@ -95,6 +85,16 @@ class InvoiceApiController extends Controller
|
|||||||
$client = Client::scope($data['client_id'])->first();
|
$client = Client::scope($data['client_id'])->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if the invoice number is set and unique
|
||||||
|
if (!isset($data['invoice_number']) && !isset($data['id'])) {
|
||||||
|
$data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber(false, '', $client);
|
||||||
|
} else if (isset($data['invoice_number'])) {
|
||||||
|
$invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first();
|
||||||
|
if ($invoice) {
|
||||||
|
$error = trans('validation.unique', ['attribute' => 'texts.invoice_number']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$error) {
|
if (!$error) {
|
||||||
if (!isset($data['client_id']) && !isset($data['email'])) {
|
if (!isset($data['client_id']) && !isset($data['email'])) {
|
||||||
$error = trans('validation.', ['attribute' => 'client_id or email']);
|
$error = trans('validation.', ['attribute' => 'client_id or email']);
|
||||||
|
@ -235,6 +235,7 @@ class InvoiceController extends BaseController
|
|||||||
|
|
||||||
public function edit($publicId, $clone = false)
|
public function edit($publicId, $clone = false)
|
||||||
{
|
{
|
||||||
|
$account = Auth::user()->account;
|
||||||
$invoice = Invoice::scope($publicId)->withTrashed()->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items')->firstOrFail();
|
$invoice = Invoice::scope($publicId)->withTrashed()->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items')->firstOrFail();
|
||||||
$entityType = $invoice->getEntityType();
|
$entityType = $invoice->getEntityType();
|
||||||
|
|
||||||
@ -247,7 +248,7 @@ class InvoiceController extends BaseController
|
|||||||
|
|
||||||
if ($clone) {
|
if ($clone) {
|
||||||
$invoice->id = null;
|
$invoice->id = null;
|
||||||
$invoice->invoice_number = Auth::user()->account->getNextInvoiceNumber($invoice->is_quote);
|
$invoice->invoice_number = $account->getNextInvoiceNumber($invoice->is_quote, '', $invoice->client);
|
||||||
$invoice->balance = $invoice->amount;
|
$invoice->balance = $invoice->amount;
|
||||||
$invoice->invoice_status_id = 0;
|
$invoice->invoice_status_id = 0;
|
||||||
$invoice->invoice_date = date_create()->format('Y-m-d');
|
$invoice->invoice_date = date_create()->format('Y-m-d');
|
||||||
@ -349,7 +350,7 @@ class InvoiceController extends BaseController
|
|||||||
public function create($clientPublicId = 0, $isRecurring = false)
|
public function create($clientPublicId = 0, $isRecurring = false)
|
||||||
{
|
{
|
||||||
$client = null;
|
$client = null;
|
||||||
$invoiceNumber = $isRecurring ? microtime(true) : Auth::user()->account->getNextInvoiceNumber();
|
$invoiceNumber = $isRecurring ? microtime(true) : Auth::user()->account->getDefaultInvoiceNumber();
|
||||||
|
|
||||||
if ($clientPublicId) {
|
if ($clientPublicId) {
|
||||||
$client = Client::scope($clientPublicId)->firstOrFail();
|
$client = Client::scope($clientPublicId)->firstOrFail();
|
||||||
|
@ -82,7 +82,7 @@ class QuoteController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$client = null;
|
$client = null;
|
||||||
$invoiceNumber = Auth::user()->account->getNextInvoiceNumber(true);
|
$invoiceNumber = Auth::user()->account->getDefaultInvoiceNumber(true);
|
||||||
$account = Account::with('country')->findOrFail(Auth::user()->account_id);
|
$account = Account::with('country')->findOrFail(Auth::user()->account_id);
|
||||||
|
|
||||||
if ($clientPublicId) {
|
if ($clientPublicId) {
|
||||||
|
@ -408,6 +408,7 @@ if (!defined('CONTACT_EMAIL')) {
|
|||||||
define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/');
|
define('OUTDATE_BROWSER_URL', 'http://browsehappy.com/');
|
||||||
define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html');
|
define('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html');
|
||||||
define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/single/browser/v1/');
|
define('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/single/browser/v1/');
|
||||||
|
define('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php');
|
||||||
define('REFERRAL_PROGRAM_URL', false);
|
define('REFERRAL_PROGRAM_URL', false);
|
||||||
|
|
||||||
define('COUNT_FREE_DESIGNS', 4);
|
define('COUNT_FREE_DESIGNS', 4);
|
||||||
|
@ -243,9 +243,88 @@ class Account extends Eloquent
|
|||||||
return $height;
|
return $height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNextInvoiceNumber($isQuote = false, $prefix = '')
|
public function hasNumberPattern($isQuote)
|
||||||
{
|
{
|
||||||
$counter = $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter;
|
return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNumberPattern($isQuote, $client = null)
|
||||||
|
{
|
||||||
|
$pattern = $isQuote ? $this->quote_number_pattern : $this->invoice_number_pattern;
|
||||||
|
|
||||||
|
if (!$pattern) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = ['{$year}'];
|
||||||
|
$replace = [date('Y')];
|
||||||
|
|
||||||
|
$search[] = '{$counter}';
|
||||||
|
$replace[] = str_pad($this->getCounter($isQuote), 4, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$matches = false;
|
||||||
|
preg_match('/{\$date:(.*?)}/', $pattern, $matches);
|
||||||
|
if (count($matches) > 1) {
|
||||||
|
$format = $matches[1];
|
||||||
|
$search[] = $matches[0];
|
||||||
|
$replace[] = str_replace($format, date($format), $matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pattern = str_replace($search, $replace, $pattern);
|
||||||
|
|
||||||
|
if ($client) {
|
||||||
|
$pattern = $this->getClientInvoiceNumber($pattern, $isQuote, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClientInvoiceNumber($pattern, $isQuote, $client)
|
||||||
|
{
|
||||||
|
if (!$client) {
|
||||||
|
return $pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = [
|
||||||
|
//'{$clientId}',
|
||||||
|
'{$userId}',
|
||||||
|
'{$custom1}',
|
||||||
|
'{$custom2}',
|
||||||
|
];
|
||||||
|
|
||||||
|
$replace = [
|
||||||
|
//str_pad($client->public_id, 3, '0', STR_PAD_LEFT),
|
||||||
|
str_pad($client->user->public_id, 2, '0', STR_PAD_LEFT),
|
||||||
|
$client->custom_value1,
|
||||||
|
$client->custom_value2,
|
||||||
|
];
|
||||||
|
|
||||||
|
return str_replace($search, $replace, $pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're using a pattern we don't know the next number until a client
|
||||||
|
// is selected, to support this the default value is blank
|
||||||
|
public function getDefaultInvoiceNumber($isQuote = false, $prefix = '')
|
||||||
|
{
|
||||||
|
if ($this->getNumberPattern($isQuote)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getNextInvoiceNumber($isQuote = false, $prefix = '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCounter($isQuote)
|
||||||
|
{
|
||||||
|
return $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNextInvoiceNumber($isQuote = false, $prefix = '', $client = null)
|
||||||
|
{
|
||||||
|
if ($this->hasNumberPattern($isQuote)) {
|
||||||
|
return $this->getNumberPattern($isQuote, $client);
|
||||||
|
}
|
||||||
|
|
||||||
|
$counter = $this->getCounter($isQuote);
|
||||||
$prefix .= $isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix;
|
$prefix .= $isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix;
|
||||||
$counterOffset = 0;
|
$counterOffset = 0;
|
||||||
|
|
||||||
@ -271,9 +350,9 @@ class Account extends Eloquent
|
|||||||
return $number;
|
return $number;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function incrementCounter($isQuote = false)
|
public function incrementCounter($invoice)
|
||||||
{
|
{
|
||||||
if ($isQuote && !$this->share_counter) {
|
if ($invoice->is_quote && !$this->share_counter) {
|
||||||
$this->quote_number_counter += 1;
|
$this->quote_number_counter += 1;
|
||||||
} else {
|
} else {
|
||||||
$this->invoice_number_counter += 1;
|
$this->invoice_number_counter += 1;
|
||||||
|
@ -25,6 +25,11 @@ class Client extends EntityModel
|
|||||||
return $this->belongsTo('App\Models\Account');
|
return $this->belongsTo('App\Models\Account');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Models\User');
|
||||||
|
}
|
||||||
|
|
||||||
public function invoices()
|
public function invoices()
|
||||||
{
|
{
|
||||||
return $this->hasMany('App\Models\Invoice');
|
return $this->hasMany('App\Models\Invoice');
|
||||||
@ -168,6 +173,11 @@ class Client extends EntityModel
|
|||||||
|
|
||||||
return $this->account->currency_id ?: DEFAULT_CURRENCY;
|
return $this->account->currency_id ?: DEFAULT_CURRENCY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCounter($isQuote)
|
||||||
|
{
|
||||||
|
return $isQuote ? $this->quote_number_counter : $this->invoice_number_counter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -15,6 +15,16 @@ class Invoice extends EntityModel
|
|||||||
'auto_bill' => 'boolean',
|
'auto_bill' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static $patternFields = [
|
||||||
|
'counter',
|
||||||
|
'custom1',
|
||||||
|
'custom2',
|
||||||
|
'userId',
|
||||||
|
//'clientId', // need to update after saving
|
||||||
|
'year',
|
||||||
|
'date:',
|
||||||
|
];
|
||||||
|
|
||||||
public function account()
|
public function account()
|
||||||
{
|
{
|
||||||
return $this->belongsTo('App\Models\Account');
|
return $this->belongsTo('App\Models\Account');
|
||||||
@ -223,7 +233,7 @@ class Invoice extends EntityModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
$startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date');
|
$startDate = $this->getOriginal('last_sent_date') ?: $this->getOriginal('start_date');
|
||||||
$startDate .= ' ' . DEFAULT_SEND_RECURRING_HOUR . ':00:00';
|
$startDate .= ' ' . $this->account->recurring_hour . ':00:00';
|
||||||
$startDate = $this->account->getDateTime($startDate);
|
$startDate = $this->account->getDateTime($startDate);
|
||||||
$endDate = $this->end_date ? $this->account->getDateTime($this->getOriginal('end_date')) : null;
|
$endDate = $this->end_date ? $this->account->getDateTime($this->getOriginal('end_date')) : null;
|
||||||
$timezone = $this->account->getTimezone();
|
$timezone = $this->account->getTimezone();
|
||||||
@ -249,7 +259,7 @@ class Invoice extends EntityModel
|
|||||||
public function getNextSendDate()
|
public function getNextSendDate()
|
||||||
{
|
{
|
||||||
if ($this->start_date && !$this->last_sent_date) {
|
if ($this->start_date && !$this->last_sent_date) {
|
||||||
$startDate = $this->getOriginal('start_date') . ' ' . DEFAULT_SEND_RECURRING_HOUR . ':00:00';
|
$startDate = $this->getOriginal('start_date') . ' ' . $this->account->recurring_hour . ':00:00';
|
||||||
return $this->account->getDateTime($startDate);
|
return $this->account->getDateTime($startDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,7 +442,7 @@ class Invoice extends EntityModel
|
|||||||
|
|
||||||
Invoice::creating(function ($invoice) {
|
Invoice::creating(function ($invoice) {
|
||||||
if (!$invoice->is_recurring) {
|
if (!$invoice->is_recurring) {
|
||||||
$invoice->account->incrementCounter($invoice->is_quote);
|
$invoice->account->incrementCounter($invoice);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -478,7 +478,7 @@ class InvoiceRepository
|
|||||||
}
|
}
|
||||||
$clone->invoice_number = $account->invoice_number_prefix.$invoiceNumber;
|
$clone->invoice_number = $account->invoice_number_prefix.$invoiceNumber;
|
||||||
} else {
|
} else {
|
||||||
$clone->invoice_number = $account->getNextInvoiceNumber();
|
$clone->invoice_number = $account->getNextInvoiceNumber($invoice->is_quote, '', $invoice->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
@ -631,7 +631,7 @@ class InvoiceRepository
|
|||||||
$invoice = Invoice::createNew($recurInvoice);
|
$invoice = Invoice::createNew($recurInvoice);
|
||||||
$invoice->client_id = $recurInvoice->client_id;
|
$invoice->client_id = $recurInvoice->client_id;
|
||||||
$invoice->recurring_invoice_id = $recurInvoice->id;
|
$invoice->recurring_invoice_id = $recurInvoice->id;
|
||||||
$invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber(false, 'R');
|
$invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber(false, 'R', $recurInvoice->client);
|
||||||
$invoice->amount = $recurInvoice->amount;
|
$invoice->amount = $recurInvoice->amount;
|
||||||
$invoice->balance = $recurInvoice->amount;
|
$invoice->balance = $recurInvoice->amount;
|
||||||
$invoice->invoice_date = date_create()->format('Y-m-d');
|
$invoice->invoice_date = date_create()->format('Y-m-d');
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddInvoiceNumberPattern extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('accounts', function ($table) {
|
||||||
|
$table->string('invoice_number_pattern')->nullable();
|
||||||
|
$table->string('quote_number_pattern')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('clients', function ($table) {
|
||||||
|
$table->integer('invoice_number_counter')->default(1)->nullable();
|
||||||
|
$table->integer('quote_number_counter')->default(1)->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('accounts', function ($table) {
|
||||||
|
$table->dropColumn('invoice_number_pattern');
|
||||||
|
$table->dropColumn('quote_number_pattern');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('accounts', function ($table) {
|
||||||
|
$table->dropColumn('invoice_number_counter');
|
||||||
|
$table->dropColumn('quote_number_counter');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -32060,6 +32060,7 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var account = invoice.account;
|
||||||
var contact = client.contacts[0];
|
var contact = client.contacts[0];
|
||||||
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email);
|
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email);
|
||||||
var clientEmail = client.contacts[0].email == clientName ? '' : client.contacts[0].email;
|
var clientEmail = client.contacts[0].email == clientName ? '' : client.contacts[0].email;
|
||||||
@ -32070,6 +32071,11 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
|
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a custom field is used in the invoice/quote number then we'll hide it from the PDF
|
||||||
|
var pattern = invoice.is_quote ? account.quote_number_pattern : account.invoice_number_pattern;
|
||||||
|
var custom1InPattern = (pattern && pattern.indexOf('{$custom1}') >= 0);
|
||||||
|
var custom2InPattern = (pattern && pattern.indexOf('{$custom2}') >= 0);
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
{text:clientName || ' ', style: ['clientName']},
|
{text:clientName || ' ', style: ['clientName']},
|
||||||
{text:client.id_number},
|
{text:client.id_number},
|
||||||
@ -32079,8 +32085,8 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
{text:cityStatePostal},
|
{text:cityStatePostal},
|
||||||
{text:client.country ? client.country.name : ''},
|
{text:client.country ? client.country.name : ''},
|
||||||
{text:clientEmail},
|
{text:clientEmail},
|
||||||
{text: invoice.client.custom_value1 ? invoice.account.custom_client_label1 + ' ' + invoice.client.custom_value1 : false},
|
{text: client.custom_value1 && !custom1InPattern ? account.custom_client_label1 + ' ' + client.custom_value1 : false},
|
||||||
{text: invoice.client.custom_value2 ? invoice.account.custom_client_label2 + ' ' + invoice.client.custom_value2 : false}
|
{text: client.custom_value2 && !custom2InPattern ? account.custom_client_label2 + ' ' + client.custom_value2 : false}
|
||||||
];
|
];
|
||||||
|
|
||||||
return NINJA.prepareDataList(data, 'clientDetails');
|
return NINJA.prepareDataList(data, 'clientDetails');
|
||||||
|
@ -487,6 +487,7 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var account = invoice.account;
|
||||||
var contact = client.contacts[0];
|
var contact = client.contacts[0];
|
||||||
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email);
|
var clientName = client.name || (contact.first_name || contact.last_name ? (contact.first_name + ' ' + contact.last_name) : contact.email);
|
||||||
var clientEmail = client.contacts[0].email == clientName ? '' : client.contacts[0].email;
|
var clientEmail = client.contacts[0].email == clientName ? '' : client.contacts[0].email;
|
||||||
@ -497,6 +498,11 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
|
cityStatePostal = formatAddress(client.city, client.state, client.postal_code, swap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a custom field is used in the invoice/quote number then we'll hide it from the PDF
|
||||||
|
var pattern = invoice.is_quote ? account.quote_number_pattern : account.invoice_number_pattern;
|
||||||
|
var custom1InPattern = (pattern && pattern.indexOf('{$custom1}') >= 0);
|
||||||
|
var custom2InPattern = (pattern && pattern.indexOf('{$custom2}') >= 0);
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
{text:clientName || ' ', style: ['clientName']},
|
{text:clientName || ' ', style: ['clientName']},
|
||||||
{text:client.id_number},
|
{text:client.id_number},
|
||||||
@ -506,8 +512,8 @@ NINJA.clientDetails = function(invoice) {
|
|||||||
{text:cityStatePostal},
|
{text:cityStatePostal},
|
||||||
{text:client.country ? client.country.name : ''},
|
{text:client.country ? client.country.name : ''},
|
||||||
{text:clientEmail},
|
{text:clientEmail},
|
||||||
{text: invoice.client.custom_value1 ? invoice.account.custom_client_label1 + ' ' + invoice.client.custom_value1 : false},
|
{text: client.custom_value1 && !custom1InPattern ? account.custom_client_label1 + ' ' + client.custom_value1 : false},
|
||||||
{text: invoice.client.custom_value2 ? invoice.account.custom_client_label2 + ' ' + invoice.client.custom_value2 : false}
|
{text: client.custom_value2 && !custom2InPattern ? account.custom_client_label2 + ' ' + client.custom_value2 : false}
|
||||||
];
|
];
|
||||||
|
|
||||||
return NINJA.prepareDataList(data, 'clientDetails');
|
return NINJA.prepareDataList(data, 'clientDetails');
|
||||||
|
@ -840,6 +840,16 @@ return array(
|
|||||||
'archived_tax_rate' => 'Successfully archived the tax rate',
|
'archived_tax_rate' => 'Successfully archived the tax rate',
|
||||||
'default_tax_rate_id' => 'Default Tax Rate',
|
'default_tax_rate_id' => 'Default Tax Rate',
|
||||||
'tax_rate' => 'Tax Rate',
|
'tax_rate' => 'Tax Rate',
|
||||||
|
'recurring_hour' => 'Recurring Hour',
|
||||||
|
'pattern' => 'Pattern',
|
||||||
|
'pattern_help_title' => 'Pattern Help',
|
||||||
|
'pattern_help_1' => 'Create custom invoice and quote numbers by specifying a pattern',
|
||||||
|
'pattern_help_2' => 'Available variables:',
|
||||||
|
'pattern_help_3' => 'For example, :example would be converted to :value',
|
||||||
|
'see_options' => 'See options',
|
||||||
|
'invoice_counter' => 'Invoice Counter',
|
||||||
|
'quote_counter' => 'Quote Counter',
|
||||||
|
'type' => 'Type',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
.input-group-addon div.checkbox {
|
.input-group-addon div.checkbox {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
.tab-content span.input-group-addon {
|
.tab-content .pad-checkbox span.input-group-addon {
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
@parent
|
@parent
|
||||||
@include('accounts.nav', ['selected' => ACCOUNT_INVOICE_SETTINGS, 'advanced' => true])
|
@include('accounts.nav', ['selected' => ACCOUNT_INVOICE_SETTINGS, 'advanced' => true])
|
||||||
|
|
||||||
{!! Former::open()->addClass('warn-on-exit') !!}
|
{!! Former::open()->rules(['iframe_url' => 'url'])->addClass('warn-on-exit') !!}
|
||||||
{{ Former::populate($account) }}
|
{{ Former::populate($account) }}
|
||||||
{{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }}
|
{{ Former::populateField('custom_invoice_taxes1', intval($account->custom_invoice_taxes1)) }}
|
||||||
{{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }}
|
{{ Former::populateField('custom_invoice_taxes2', intval($account->custom_invoice_taxes2)) }}
|
||||||
@ -34,7 +34,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body form-padding-right">
|
<div class="panel-body form-padding-right">
|
||||||
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
|
{!! Former::checkbox('pdf_email_attachment')->text(trans('texts.enable')) !!}
|
||||||
@if (Utils::isNinja())
|
|
||||||
|
{{-- Former::select('recurring_hour')->options($recurringHours) --}}
|
||||||
|
|
||||||
{!! Former::inline_radios('custom_invoice_link')
|
{!! Former::inline_radios('custom_invoice_link')
|
||||||
->onchange('onCustomLinkChange()')
|
->onchange('onCustomLinkChange()')
|
||||||
->radios([
|
->radios([
|
||||||
@ -42,17 +44,18 @@
|
|||||||
trans('texts.website') => ['value' => 'website', 'name' => 'custom_link'],
|
trans('texts.website') => ['value' => 'website', 'name' => 'custom_link'],
|
||||||
])->check($account->iframe_url ? 'website' : 'subdomain') !!}
|
])->check($account->iframe_url ? 'website' : 'subdomain') !!}
|
||||||
{{ Former::setOption('capitalize_translations', false) }}
|
{{ Former::setOption('capitalize_translations', false) }}
|
||||||
|
|
||||||
{!! Former::text('subdomain')
|
{!! Former::text('subdomain')
|
||||||
->placeholder(trans('texts.www'))
|
->placeholder(trans('texts.www'))
|
||||||
->onchange('onSubdomainChange()')
|
->onchange('onSubdomainChange()')
|
||||||
->addGroupClass('subdomain')
|
->addGroupClass('subdomain')
|
||||||
->label(' ') !!}
|
->label(' ') !!}
|
||||||
|
|
||||||
{!! Former::text('iframe_url')
|
{!! Former::text('iframe_url')
|
||||||
->placeholder('http://www.example.com/invoice')
|
->placeholder('http://www.example.com/invoice')
|
||||||
->appendIcon('question-sign')
|
->appendIcon('question-sign')
|
||||||
->addGroupClass('iframe_url')
|
->addGroupClass('iframe_url')
|
||||||
->label(' ') !!}
|
->label(' ') !!}
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -61,6 +64,7 @@
|
|||||||
<h3 class="panel-title">{!! trans('texts.invoice_quote_number') !!}</h3>
|
<h3 class="panel-title">{!! trans('texts.invoice_quote_number') !!}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body form-padding-right">
|
<div class="panel-body form-padding-right">
|
||||||
|
|
||||||
<div role="tabpanel">
|
<div role="tabpanel">
|
||||||
<ul class="nav nav-tabs" role="tablist" style="border: none">
|
<ul class="nav nav-tabs" role="tablist" style="border: none">
|
||||||
<li role="presentation" class="active"><a href="#invoiceNumber" aria-controls="invoiceNumber" role="tab" data-toggle="tab">{{ trans('texts.invoice_number') }}</a></li>
|
<li role="presentation" class="active"><a href="#invoiceNumber" aria-controls="invoiceNumber" role="tab" data-toggle="tab">{{ trans('texts.invoice_number') }}</a></li>
|
||||||
@ -70,18 +74,56 @@
|
|||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="invoiceNumber">
|
<div role="tabpanel" class="tab-pane active" id="invoiceNumber">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{!! Former::text('invoice_number_prefix')->label(trans('texts.prefix')) !!}
|
{!! Former::inline_radios('invoice_number_type')
|
||||||
{!! Former::text('invoice_number_counter')->label(trans('texts.counter')) !!}
|
->onchange('onInvoiceNumberTypeChange()')
|
||||||
|
->label(trans('texts.type'))
|
||||||
|
->radios([
|
||||||
|
trans('texts.prefix') => ['value' => 'prefix', 'name' => 'invoice_number_type'],
|
||||||
|
trans('texts.pattern') => ['value' => 'pattern', 'name' => 'invoice_number_type'],
|
||||||
|
])->check($account->invoice_number_pattern ? 'pattern' : 'prefix') !!}
|
||||||
|
|
||||||
|
{!! Former::text('invoice_number_prefix')
|
||||||
|
->addGroupClass('invoice-prefix')
|
||||||
|
->label(' ') !!}
|
||||||
|
{!! Former::text('invoice_number_pattern')
|
||||||
|
->appendIcon('question-sign')
|
||||||
|
->addGroupClass('invoice-pattern')
|
||||||
|
->label(' ')
|
||||||
|
->addGroupClass('number-pattern') !!}
|
||||||
|
{!! Former::text('invoice_number_counter')
|
||||||
|
->label(trans('texts.counter')) !!}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane" id="quoteNumber">
|
<div role="tabpanel" class="tab-pane" id="quoteNumber">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{!! Former::text('quote_number_prefix')->label(trans('texts.prefix')) !!}
|
{!! Former::inline_radios('quote_number_type')
|
||||||
{!! Former::text('quote_number_counter')->label(trans('texts.counter'))
|
->onchange('onQuoteNumberTypeChange()')
|
||||||
->append(Former::checkbox('share_counter')->raw()->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) !!}
|
->label(trans('texts.type'))
|
||||||
|
->radios([
|
||||||
|
trans('texts.prefix') => ['value' => 'prefix', 'name' => 'quote_number_type'],
|
||||||
|
trans('texts.pattern') => ['value' => 'pattern', 'name' => 'quote_number_type'],
|
||||||
|
])->check($account->quote_number_pattern ? 'pattern' : 'prefix') !!}
|
||||||
|
|
||||||
|
{!! Former::text('quote_number_prefix')
|
||||||
|
->addGroupClass('quote-prefix')
|
||||||
|
->label(' ') !!}
|
||||||
|
{!! Former::text('quote_number_pattern')
|
||||||
|
->appendIcon('question-sign')
|
||||||
|
->addGroupClass('quote-pattern')
|
||||||
|
->addGroupClass('number-pattern')
|
||||||
|
->label(' ') !!}
|
||||||
|
{!! Former::text('quote_number_counter')
|
||||||
|
->label(trans('texts.counter'))
|
||||||
|
->addGroupClass('pad-checkbox')
|
||||||
|
->append(Former::checkbox('share_counter')->raw()
|
||||||
|
->onclick('setQuoteNumberEnabled()') . ' ' . trans('texts.share_invoice_counter')) !!}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -131,10 +173,16 @@
|
|||||||
<div role="tabpanel" class="tab-pane" id="invoiceCharges">
|
<div role="tabpanel" class="tab-pane" id="invoiceCharges">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
|
||||||
{!! Former::text('custom_invoice_label1')->label(trans('texts.field_label'))
|
{!! Former::text('custom_invoice_label1')
|
||||||
->append(Former::checkbox('custom_invoice_taxes1')->raw() . trans('texts.charge_taxes')) !!}
|
->label(trans('texts.field_label'))
|
||||||
{!! Former::text('custom_invoice_label2')->label(trans('texts.field_label'))
|
->addGroupClass('pad-checkbox')
|
||||||
->append(Former::checkbox('custom_invoice_taxes2')->raw() . trans('texts.charge_taxes')) !!}
|
->append(Former::checkbox('custom_invoice_taxes1')
|
||||||
|
->raw() . trans('texts.charge_taxes')) !!}
|
||||||
|
{!! Former::text('custom_invoice_label2')
|
||||||
|
->label(trans('texts.field_label'))
|
||||||
|
->addGroupClass('pad-checkbox')
|
||||||
|
->append(Former::checkbox('custom_invoice_taxes2')
|
||||||
|
->raw() . trans('texts.charge_taxes')) !!}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -179,6 +227,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="patternHelpModal" tabindex="-1" role="dialog" aria-labelledby="patternHelpModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" style="min-width:150px">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title" id="patternHelpModalLabel">{{ trans('texts.pattern_help_title') }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ trans('texts.pattern_help_1') }}</p>
|
||||||
|
<p>{{ trans('texts.pattern_help_2') }}</p>
|
||||||
|
<ul>
|
||||||
|
@foreach (\App\Models\Invoice::$patternFields as $field)
|
||||||
|
@if ($field == 'date:')
|
||||||
|
<li>$date:format ({!! link_to(PHP_DATE_FORMATS, trans('texts.see_options'), ['target' => '_blank']) !!})</li>
|
||||||
|
@else
|
||||||
|
<li>${{ $field }}</li>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<p>{{ trans('texts.pattern_help_3', [
|
||||||
|
'example' => '{$year}-{$counter}',
|
||||||
|
'value' => date('Y') . '-0001'
|
||||||
|
]) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
{!! Former::close() !!}
|
{!! Former::close() !!}
|
||||||
|
|
||||||
@ -210,13 +292,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInvoiceNumberTypeChange() {
|
||||||
|
var val = $('input[name=invoice_number_type]:checked').val()
|
||||||
|
if (val == 'prefix') {
|
||||||
|
$('.invoice-prefix').show();
|
||||||
|
$('.invoice-pattern').hide();
|
||||||
|
} else {
|
||||||
|
$('.invoice-prefix').hide();
|
||||||
|
$('.invoice-pattern').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuoteNumberTypeChange() {
|
||||||
|
var val = $('input[name=quote_number_type]:checked').val()
|
||||||
|
if (val == 'prefix') {
|
||||||
|
$('.quote-prefix').show();
|
||||||
|
$('.quote-pattern').hide();
|
||||||
|
} else {
|
||||||
|
$('.quote-prefix').hide();
|
||||||
|
$('.quote-pattern').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('.iframe_url .input-group-addon').click(function() {
|
$('.iframe_url .input-group-addon').click(function() {
|
||||||
$('#iframeHelpModal').modal('show');
|
$('#iframeHelpModal').modal('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.number-pattern .input-group-addon').click(function() {
|
||||||
|
$('#patternHelpModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
setQuoteNumberEnabled();
|
setQuoteNumberEnabled();
|
||||||
onCustomLinkChange();
|
onCustomLinkChange();
|
||||||
|
onInvoiceNumberTypeChange();
|
||||||
|
onQuoteNumberTypeChange();
|
||||||
|
|
||||||
$('#subdomain').change(function() {
|
$('#subdomain').change(function() {
|
||||||
$('#iframe_url').val('');
|
$('#iframe_url').val('');
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
{!! Former::open_for_files()->addClass('warn-on-exit') !!}
|
{!! Former::open_for_files()->addClass('warn-on-exit') !!}
|
||||||
{{ Former::populate($account) }}
|
{{ Former::populate($account) }}
|
||||||
|
{{ Former::populateField('military_time', intval($account->military_time)) }}
|
||||||
|
|
||||||
@include('accounts.nav', ['selected' => ACCOUNT_LOCALIZATION])
|
@include('accounts.nav', ['selected' => ACCOUNT_LOCALIZATION])
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
)) !!}
|
)) !!}
|
||||||
|
|
||||||
{{ Former::populate($account) }}
|
{{ Former::populate($account) }}
|
||||||
{{ Former::populateField('military_time', intval($account->military_time)) }}
|
|
||||||
{{ Former::populateField('first_name', $user->first_name) }}
|
{{ Former::populateField('first_name', $user->first_name) }}
|
||||||
{{ Former::populateField('last_name', $user->last_name) }}
|
{{ Former::populateField('last_name', $user->last_name) }}
|
||||||
{{ Former::populateField('email', $user->email) }}
|
{{ Former::populateField('email', $user->email) }}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
@extends('header')
|
@extends('header')
|
||||||
|
|
||||||
|
|
||||||
@ -115,6 +116,7 @@
|
|||||||
{!! Former::select('industry_id')->addOption('','')
|
{!! Former::select('industry_id')->addOption('','')
|
||||||
->fromQuery($industries, 'name', 'id') !!}
|
->fromQuery($industries, 'name', 'id') !!}
|
||||||
{!! Former::textarea('private_notes') !!}
|
{!! Former::textarea('private_notes') !!}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
797
resources/views/invoices/knockout.blade.php
Normal file
797
resources/views/invoices/knockout.blade.php
Normal file
@ -0,0 +1,797 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function ViewModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.showMore = ko.observable(false);
|
||||||
|
|
||||||
|
//self.invoice = data ? false : new InvoiceModel();
|
||||||
|
self.invoice = ko.observable(data ? false : new InvoiceModel());
|
||||||
|
self.tax_rates = ko.observableArray();
|
||||||
|
self.tax_rates.push(new TaxRateModel()); // add blank row
|
||||||
|
|
||||||
|
|
||||||
|
self.loadClient = function(client) {
|
||||||
|
ko.mapping.fromJS(client, model.invoice().client().mapping, model.invoice().client);
|
||||||
|
@if (!$invoice)
|
||||||
|
self.setDueDate();
|
||||||
|
@endif
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showMoreFields = function() {
|
||||||
|
self.showMore(!self.showMore());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setDueDate = function() {
|
||||||
|
@if ($entityType == ENTITY_INVOICE)
|
||||||
|
var paymentTerms = parseInt(self.invoice().client().payment_terms());
|
||||||
|
if (paymentTerms && paymentTerms != 0 && !self.invoice().due_date())
|
||||||
|
{
|
||||||
|
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($('#due_date').val());
|
||||||
|
}
|
||||||
|
@endif
|
||||||
|
}
|
||||||
|
|
||||||
|
self.invoice_taxes = ko.observable({{ Auth::user()->account->invoice_taxes ? 'true' : 'false' }});
|
||||||
|
self.invoice_item_taxes = ko.observable({{ Auth::user()->account->invoice_item_taxes ? 'true' : 'false' }});
|
||||||
|
self.show_item_taxes = ko.observable({{ Auth::user()->account->show_item_taxes ? 'true' : 'false' }});
|
||||||
|
|
||||||
|
self.mapping = {
|
||||||
|
'invoice': {
|
||||||
|
create: function(options) {
|
||||||
|
return new InvoiceModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'tax_rates': {
|
||||||
|
create: function(options) {
|
||||||
|
return new TaxRateModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, self.mapping, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.invoice_taxes.show = ko.computed(function() {
|
||||||
|
if (self.invoice_taxes() && self.tax_rates().length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (self.invoice().tax_rate() > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.invoice_item_taxes.show = ko.computed(function() {
|
||||||
|
if (self.invoice_item_taxes()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (var i=0; i<self.invoice().invoice_items().length; i++) {
|
||||||
|
var item = self.invoice().invoice_items()[i];
|
||||||
|
if (item.tax_rate() > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addTaxRate = function(data) {
|
||||||
|
var itemModel = new TaxRateModel(data);
|
||||||
|
self.tax_rates.push(itemModel);
|
||||||
|
applyComboboxListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.getTaxRateById = function(id) {
|
||||||
|
for (var i=0; i<self.tax_rates().length; i++) {
|
||||||
|
var taxRate = self.tax_rates()[i];
|
||||||
|
if (taxRate.public_id() == id) {
|
||||||
|
return taxRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.getTaxRate = function(name, rate) {
|
||||||
|
for (var i=0; i<self.tax_rates().length; i++) {
|
||||||
|
var taxRate = self.tax_rates()[i];
|
||||||
|
if (taxRate.name() == name && taxRate.rate() == parseFloat(rate)) {
|
||||||
|
return taxRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxRate = new TaxRateModel();
|
||||||
|
taxRate.name(name);
|
||||||
|
taxRate.rate(parseFloat(rate));
|
||||||
|
if (name) {
|
||||||
|
taxRate.is_deleted(true);
|
||||||
|
self.tax_rates.push(taxRate);
|
||||||
|
}
|
||||||
|
return taxRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showClientForm = function() {
|
||||||
|
trackEvent('/activity', '/view_client_form');
|
||||||
|
self.clientBackup = ko.mapping.toJS(self.invoice().client);
|
||||||
|
|
||||||
|
$('#emailError').css( "display", "none" );
|
||||||
|
$('#clientModal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clientFormComplete = function() {
|
||||||
|
trackEvent('/activity', '/save_client_form');
|
||||||
|
|
||||||
|
var email = $('#email0').val();
|
||||||
|
var firstName = $('#first_name').val();
|
||||||
|
var lastName = $('#last_name').val();
|
||||||
|
var name = $('#name').val();
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
//
|
||||||
|
} else if (firstName || lastName) {
|
||||||
|
name = firstName + ' ' + lastName;
|
||||||
|
} else {
|
||||||
|
name = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isValid = true;
|
||||||
|
$("input[name='email']").each(function(item, value) {
|
||||||
|
var email = $(value).val();
|
||||||
|
if (!name && (!email || !isValidEmailAddress(email))) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
$('#emailError').css( "display", "inline" );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.invoice().client().public_id() == 0) {
|
||||||
|
self.invoice().client().public_id(-1);
|
||||||
|
self.invoice().client().invoice_number_counter = 1;
|
||||||
|
self.invoice().client().quote_number_counter = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.setDueDate();
|
||||||
|
setComboboxValue($('.client_select'), -1, name);
|
||||||
|
|
||||||
|
var client = $.parseJSON(ko.toJSON(self.invoice().client()));
|
||||||
|
setInvoiceNumber(client);
|
||||||
|
|
||||||
|
//$('.client_select select').combobox('setSelected');
|
||||||
|
//$('.client_select input.form-control').val(name);
|
||||||
|
//$('.client_select .combobox-container').addClass('combobox-selected');
|
||||||
|
|
||||||
|
$('#emailError').css( "display", "none" );
|
||||||
|
|
||||||
|
refreshPDF(true);
|
||||||
|
model.clientBackup = false;
|
||||||
|
$('#clientModal').modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clientLinkText = ko.computed(function() {
|
||||||
|
if (self.invoice().client().public_id())
|
||||||
|
{
|
||||||
|
return "{{ trans('texts.edit_client') }}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (clients.length > {{ Auth::user()->getMaxNumClients() }})
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "{{ trans('texts.create_new_client') }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function InvoiceModel(data) {
|
||||||
|
var self = this;
|
||||||
|
this.client = ko.observable(data ? false : new ClientModel());
|
||||||
|
self.account = {!! $account !!};
|
||||||
|
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.terms = ko.observable('');
|
||||||
|
self.default_terms = ko.observable(account.invoice_terms);
|
||||||
|
self.terms_placeholder = ko.observable({{ !$invoice && $account->invoice_terms ? 'account.invoice_terms' : false}});
|
||||||
|
self.set_default_terms = ko.observable(false);
|
||||||
|
self.invoice_footer = ko.observable('');
|
||||||
|
self.default_footer = ko.observable(account.invoice_footer);
|
||||||
|
self.footer_placeholder = ko.observable({{ !$invoice && $account->invoice_footer ? 'account.invoice_footer' : false}});
|
||||||
|
self.set_default_footer = ko.observable(false);
|
||||||
|
self.public_notes = ko.observable('');
|
||||||
|
self.po_number = ko.observable('');
|
||||||
|
self.invoice_date = ko.observable('{{ Utils::today() }}');
|
||||||
|
self.invoice_number = ko.observable('{{ isset($invoiceNumber) ? $invoiceNumber : '' }}');
|
||||||
|
self.due_date = ko.observable('');
|
||||||
|
self.start_date = ko.observable('{{ Utils::today() }}');
|
||||||
|
self.end_date = ko.observable('');
|
||||||
|
self.last_sent_date = ko.observable('');
|
||||||
|
self.tax_name = ko.observable();
|
||||||
|
self.tax_rate = ko.observable();
|
||||||
|
self.is_recurring = ko.observable({{ $isRecurring ? 'true' : 'false' }});
|
||||||
|
self.auto_bill = ko.observable();
|
||||||
|
self.invoice_status_id = ko.observable(0);
|
||||||
|
self.invoice_items = ko.observableArray();
|
||||||
|
self.amount = ko.observable(0);
|
||||||
|
self.balance = ko.observable(0);
|
||||||
|
self.invoice_design_id = ko.observable({{ $account->invoice_design_id }});
|
||||||
|
self.partial = ko.observable(0);
|
||||||
|
self.has_tasks = ko.observable(false);
|
||||||
|
|
||||||
|
self.custom_value1 = ko.observable(0);
|
||||||
|
self.custom_value2 = ko.observable(0);
|
||||||
|
self.custom_taxes1 = ko.observable(false);
|
||||||
|
self.custom_taxes2 = ko.observable(false);
|
||||||
|
self.custom_text_value1 = ko.observable();
|
||||||
|
self.custom_text_value2 = ko.observable();
|
||||||
|
|
||||||
|
self.mapping = {
|
||||||
|
'client': {
|
||||||
|
create: function(options) {
|
||||||
|
return new ClientModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'invoice_items': {
|
||||||
|
create: function(options) {
|
||||||
|
return new ItemModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'tax': {
|
||||||
|
create: function(options) {
|
||||||
|
return new TaxRateModel(options.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addItem = function() {
|
||||||
|
var itemModel = new ItemModel();
|
||||||
|
@if ($account->hide_quantity)
|
||||||
|
itemModel.qty(1);
|
||||||
|
@endif
|
||||||
|
self.invoice_items.push(itemModel);
|
||||||
|
applyComboboxListeners();
|
||||||
|
return itemModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, self.mapping, self);
|
||||||
|
} else {
|
||||||
|
self.addItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.productLabel = ko.computed(function() {
|
||||||
|
return self.has_tasks() ? invoiceLabels['date'] : invoiceLabels['item'];
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
self.qtyLabel = ko.computed(function() {
|
||||||
|
return self.has_tasks() ? invoiceLabels['hours'] : invoiceLabels['quantity'];
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
self.costLabel = ko.computed(function() {
|
||||||
|
return self.has_tasks() ? invoiceLabels['rate'] : invoiceLabels['unit_cost'];
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
self._tax = ko.observable();
|
||||||
|
this.tax = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return self._tax();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
if (value) {
|
||||||
|
self._tax(value);
|
||||||
|
self.tax_name(value.name());
|
||||||
|
self.tax_rate(value.rate());
|
||||||
|
} else {
|
||||||
|
self._tax(false);
|
||||||
|
self.tax_name('');
|
||||||
|
self.tax_rate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.wrapped_terms = ko.computed({
|
||||||
|
read: function() {
|
||||||
|
return this.terms();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
value = wordWrapText(value, 300);
|
||||||
|
self.terms(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
self.wrapped_notes = ko.computed({
|
||||||
|
read: function() {
|
||||||
|
return this.public_notes();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
value = wordWrapText(value, 300);
|
||||||
|
self.public_notes(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
self.wrapped_footer = ko.computed({
|
||||||
|
read: function() {
|
||||||
|
return this.invoice_footer();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
value = wordWrapText(value, 600);
|
||||||
|
self.invoice_footer(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
self.removeItem = function(item) {
|
||||||
|
self.invoice_items.remove(item);
|
||||||
|
refreshPDF(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.totals = ko.observable();
|
||||||
|
|
||||||
|
self.totals.rawSubtotal = ko.computed(function() {
|
||||||
|
var total = 0;
|
||||||
|
for(var p=0; p < self.invoice_items().length; ++p) {
|
||||||
|
var item = self.invoice_items()[p];
|
||||||
|
total += item.totals.rawTotal();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.subtotal = ko.computed(function() {
|
||||||
|
var total = self.totals.rawSubtotal();
|
||||||
|
return total > 0 ? formatMoney(total, self.client().currency_id()) : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.rawDiscounted = ko.computed(function() {
|
||||||
|
if (parseInt(self.is_amount_discount())) {
|
||||||
|
return roundToTwo(self.discount());
|
||||||
|
} else {
|
||||||
|
return roundToTwo(self.totals.rawSubtotal() * (self.discount()/100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.discounted = ko.computed(function() {
|
||||||
|
return formatMoney(self.totals.rawDiscounted(), self.client().currency_id());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.taxAmount = ko.computed(function() {
|
||||||
|
var total = self.totals.rawSubtotal();
|
||||||
|
var discount = self.totals.rawDiscounted();
|
||||||
|
total -= discount;
|
||||||
|
|
||||||
|
var customValue1 = roundToTwo(self.custom_value1());
|
||||||
|
var customValue2 = roundToTwo(self.custom_value2());
|
||||||
|
var customTaxes1 = self.custom_taxes1() == 1;
|
||||||
|
var customTaxes2 = self.custom_taxes2() == 1;
|
||||||
|
|
||||||
|
if (customValue1 && customTaxes1) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue1;
|
||||||
|
}
|
||||||
|
if (customValue2 && customTaxes2) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxRate = parseFloat(self.tax_rate());
|
||||||
|
if (taxRate > 0) {
|
||||||
|
var tax = roundToTwo(total * (taxRate/100));
|
||||||
|
return formatMoney(tax, self.client().currency_id());
|
||||||
|
} else {
|
||||||
|
return formatMoney(0, self.client().currency_id());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.itemTaxes = ko.computed(function() {
|
||||||
|
var taxes = {};
|
||||||
|
var total = self.totals.rawSubtotal();
|
||||||
|
for(var i=0; i<self.invoice_items().length; i++) {
|
||||||
|
var item = self.invoice_items()[i];
|
||||||
|
var lineTotal = item.totals.rawTotal();
|
||||||
|
if (self.discount()) {
|
||||||
|
if (parseInt(self.is_amount_discount())) {
|
||||||
|
lineTotal -= roundToTwo((lineTotal/total) * self.discount());
|
||||||
|
} else {
|
||||||
|
lineTotal -= roundToTwo(lineTotal * (self.discount()/100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var taxAmount = roundToTwo(lineTotal * item.tax_rate() / 100);
|
||||||
|
if (taxAmount) {
|
||||||
|
var key = item.tax_name() + item.tax_rate();
|
||||||
|
if (taxes.hasOwnProperty(key)) {
|
||||||
|
taxes[key].amount += taxAmount;
|
||||||
|
} else {
|
||||||
|
taxes[key] = {name:item.tax_name(), rate:item.tax_rate(), amount:taxAmount};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taxes;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.hasItemTaxes = ko.computed(function() {
|
||||||
|
var count = 0;
|
||||||
|
var taxes = self.totals.itemTaxes();
|
||||||
|
for (var key in taxes) {
|
||||||
|
if (taxes.hasOwnProperty(key)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.itemTaxRates = ko.computed(function() {
|
||||||
|
var taxes = self.totals.itemTaxes();
|
||||||
|
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 = [];
|
||||||
|
for (var key in taxes) {
|
||||||
|
if (taxes.hasOwnProperty(key)) {
|
||||||
|
parts.push(formatMoney(taxes[key].amount, self.client().currency_id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join('<br/>');
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.rawPaidToDate = ko.computed(function() {
|
||||||
|
return accounting.toFixed(self.amount(),2) - accounting.toFixed(self.balance(),2);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.paidToDate = ko.computed(function() {
|
||||||
|
var total = self.totals.rawPaidToDate();
|
||||||
|
return formatMoney(total, self.client().currency_id());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.rawTotal = ko.computed(function() {
|
||||||
|
var total = accounting.toFixed(self.totals.rawSubtotal(),2);
|
||||||
|
var discount = self.totals.rawDiscounted();
|
||||||
|
total -= discount;
|
||||||
|
|
||||||
|
var customValue1 = roundToTwo(self.custom_value1());
|
||||||
|
var customValue2 = roundToTwo(self.custom_value2());
|
||||||
|
var customTaxes1 = self.custom_taxes1() == 1;
|
||||||
|
var customTaxes2 = self.custom_taxes2() == 1;
|
||||||
|
|
||||||
|
if (customValue1 && customTaxes1) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue1;
|
||||||
|
}
|
||||||
|
if (customValue2 && customTaxes2) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxRate = parseFloat(self.tax_rate());
|
||||||
|
if (taxRate > 0) {
|
||||||
|
total = NINJA.parseFloat(total) + roundToTwo((total * (taxRate/100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var taxes = self.totals.itemTaxes();
|
||||||
|
for (var key in taxes) {
|
||||||
|
if (taxes.hasOwnProperty(key)) {
|
||||||
|
total += taxes[key].amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customValue1 && !customTaxes1) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue1;
|
||||||
|
}
|
||||||
|
if (customValue2 && !customTaxes2) {
|
||||||
|
total = NINJA.parseFloat(total) + customValue2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paid = self.totals.rawPaidToDate();
|
||||||
|
if (paid > 0) {
|
||||||
|
total -= paid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.totals.total = ko.computed(function() {
|
||||||
|
return formatMoney(self.partial() ? self.partial() : self.totals.rawTotal(), self.client().currency_id());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.onDragged = function(item) {
|
||||||
|
refreshPDF(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClientModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.public_id = ko.observable(0);
|
||||||
|
self.name = ko.observable('');
|
||||||
|
self.id_number = ko.observable('');
|
||||||
|
self.vat_number = ko.observable('');
|
||||||
|
self.work_phone = ko.observable('');
|
||||||
|
self.custom_value1 = ko.observable('');
|
||||||
|
self.custom_value2 = ko.observable('');
|
||||||
|
self.private_notes = ko.observable('');
|
||||||
|
self.address1 = ko.observable('');
|
||||||
|
self.address2 = ko.observable('');
|
||||||
|
self.city = ko.observable('');
|
||||||
|
self.state = ko.observable('');
|
||||||
|
self.postal_code = ko.observable('');
|
||||||
|
self.country_id = ko.observable('');
|
||||||
|
self.size_id = ko.observable('');
|
||||||
|
self.industry_id = ko.observable('');
|
||||||
|
self.currency_id = ko.observable('');
|
||||||
|
self.language_id = ko.observable('');
|
||||||
|
self.website = ko.observable('');
|
||||||
|
self.payment_terms = ko.observable(0);
|
||||||
|
self.contacts = ko.observableArray();
|
||||||
|
|
||||||
|
self.mapping = {
|
||||||
|
'contacts': {
|
||||||
|
create: function(options) {
|
||||||
|
var model = new ContactModel(options.data);
|
||||||
|
model.send_invoice(options.data.send_invoice == '1');
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showContact = function(elem) { if (elem.nodeType === 1) $(elem).hide().slideDown() }
|
||||||
|
self.hideContact = function(elem) { if (elem.nodeType === 1) $(elem).slideUp(function() { $(elem).remove(); }) }
|
||||||
|
|
||||||
|
self.addContact = function() {
|
||||||
|
var contact = new ContactModel();
|
||||||
|
contact.send_invoice(true);
|
||||||
|
self.contacts.push(contact);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.removeContact = function() {
|
||||||
|
self.contacts.remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.name.display = ko.computed(function() {
|
||||||
|
if (self.name()) {
|
||||||
|
return self.name();
|
||||||
|
}
|
||||||
|
if (self.contacts().length == 0) return;
|
||||||
|
var contact = self.contacts()[0];
|
||||||
|
if (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 '';
|
||||||
|
var contact = self.contacts()[0];
|
||||||
|
if (contact.first_name() || contact.last_name()) {
|
||||||
|
return contact.first_name() + ' ' + contact.last_name();
|
||||||
|
} else {
|
||||||
|
return contact.email();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
} else {
|
||||||
|
self.addContact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContactModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.public_id = ko.observable('');
|
||||||
|
self.first_name = ko.observable('');
|
||||||
|
self.last_name = ko.observable('');
|
||||||
|
self.email = ko.observable('');
|
||||||
|
self.phone = ko.observable('');
|
||||||
|
self.send_invoice = ko.observable(false);
|
||||||
|
self.invitation_link = ko.observable('');
|
||||||
|
self.invitation_status = ko.observable('');
|
||||||
|
self.invitation_viewed = ko.observable(false);
|
||||||
|
self.email_error = ko.observable('');
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.displayName = ko.computed(function() {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.email.display = ko.computed(function() {
|
||||||
|
var str = '';
|
||||||
|
if (self.first_name() || self.last_name()) {
|
||||||
|
str += self.first_name() + ' ' + self.last_name() + '<br/>';
|
||||||
|
}
|
||||||
|
if (self.email()) {
|
||||||
|
str += self.email() + '<br/>';
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.view_as_recipient = ko.computed(function() {
|
||||||
|
var str = '';
|
||||||
|
@if (Utils::isConfirmed())
|
||||||
|
if (self.invitation_link()) {
|
||||||
|
str += '<a href="' + self.invitation_link() + '" target="_blank">{{ trans('texts.view_as_recipient') }}</a>';
|
||||||
|
}
|
||||||
|
@endif
|
||||||
|
|
||||||
|
return str;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaxRateModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.public_id = ko.observable('');
|
||||||
|
self.rate = ko.observable(0);
|
||||||
|
self.name = ko.observable('');
|
||||||
|
self.is_deleted = ko.observable(false);
|
||||||
|
self.is_blank = ko.observable(false);
|
||||||
|
self.actionsVisible = ko.observable(false);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prettyRate = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return this.rate() ? roundToTwo(this.rate()) : '';
|
||||||
|
},
|
||||||
|
write: function (value) {
|
||||||
|
this.rate(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
self.displayName = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
var name = self.name() ? self.name() : '';
|
||||||
|
var rate = self.rate() ? parseFloat(self.rate()) + '%' : '';
|
||||||
|
return name + ' ' + rate;
|
||||||
|
},
|
||||||
|
write: function (value) {
|
||||||
|
// do nothing
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
self.hideActions = function() {
|
||||||
|
self.actionsVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.showActions = function() {
|
||||||
|
self.actionsVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isEmpty = function() {
|
||||||
|
return !self.rate() && !self.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.product_key = ko.observable('');
|
||||||
|
self.notes = ko.observable('');
|
||||||
|
self.cost = ko.observable(0);
|
||||||
|
self.qty = ko.observable(0);
|
||||||
|
self.tax_name = ko.observable('');
|
||||||
|
self.tax_rate = ko.observable(0);
|
||||||
|
self.task_public_id = ko.observable('');
|
||||||
|
self.actionsVisible = ko.observable(false);
|
||||||
|
|
||||||
|
self._tax = ko.observable();
|
||||||
|
this.tax = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return self._tax();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
self._tax(value);
|
||||||
|
self.tax_name(value.name());
|
||||||
|
self.tax_rate(value.rate());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.prettyQty = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return NINJA.parseFloat(this.qty()) ? NINJA.parseFloat(this.qty()) : '';
|
||||||
|
},
|
||||||
|
write: function (value) {
|
||||||
|
this.qty(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
this.prettyCost = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return this.cost() ? this.cost() : '';
|
||||||
|
},
|
||||||
|
write: function (value) {
|
||||||
|
this.cost(value);
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
self.mapping = {
|
||||||
|
'tax': {
|
||||||
|
create: function(options) {
|
||||||
|
return new TaxRateModel(options.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, self.mapping, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wrapped_notes = ko.computed({
|
||||||
|
read: function() {
|
||||||
|
return this.notes();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
value = wordWrapText(value, 235);
|
||||||
|
self.notes(value);
|
||||||
|
onItemChange();
|
||||||
|
},
|
||||||
|
owner: this
|
||||||
|
});
|
||||||
|
|
||||||
|
this.totals = ko.observable();
|
||||||
|
|
||||||
|
this.totals.rawTotal = ko.computed(function() {
|
||||||
|
var cost = roundToTwo(NINJA.parseFloat(self.cost()));
|
||||||
|
var qty = roundToTwo(NINJA.parseFloat(self.qty()));
|
||||||
|
var value = cost * qty;
|
||||||
|
return value ? roundToTwo(value) : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.totals.total = ko.computed(function() {
|
||||||
|
var total = self.totals.rawTotal();
|
||||||
|
if (window.hasOwnProperty('model') && model.invoice && model.invoice() && model.invoice().client()) {
|
||||||
|
return total ? formatMoney(total, model.invoice().client().currency_id()) : '';
|
||||||
|
} else {
|
||||||
|
return total ? formatMoney(total, 1) : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hideActions = function() {
|
||||||
|
this.actionsVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showActions = function() {
|
||||||
|
this.actionsVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isEmpty = function() {
|
||||||
|
return !self.product_key() && !self.notes() && !self.cost() && (!self.qty() || {{ $account->hide_quantity ? 'true' : 'false' }});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onSelect = function() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
@ -167,10 +167,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setBulkActionsEnabled() {
|
function setBulkActionsEnabled() {
|
||||||
var checked = $('tbody :checkbox:checked').length > 0;
|
var buttonLabel = "{{ trans('texts.archive') }}";
|
||||||
$('button.archive, button.invoice').prop('disabled', !checked);
|
var count = $('tbody :checkbox:checked').length;
|
||||||
|
$('button.archive, button.invoice').prop('disabled', !count);
|
||||||
|
if (count) {
|
||||||
|
buttonLabel += ' (' + count + ')';
|
||||||
|
}
|
||||||
|
$('button.archive').not('.dropdown-toggle').text(buttonLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user