Added support for partial payments

This commit is contained in:
Hillel Coren 2015-04-16 20:12:56 +03:00
parent be327fbcbf
commit caae008403
21 changed files with 189 additions and 83 deletions

View File

@ -12,6 +12,7 @@ use Validator;
use View; use View;
use stdClass; use stdClass;
use Cache; use Cache;
use Response;
use App\Models\User; use App\Models\User;
use App\Models\Activity; use App\Models\Activity;

View File

@ -70,6 +70,8 @@ class AccountGatewayController extends BaseController
$data['title'] = trans('texts.edit_gateway') . ' - ' . $accountGateway->gateway->name; $data['title'] = trans('texts.edit_gateway') . ' - ' . $accountGateway->gateway->name;
$data['config'] = $configFields; $data['config'] = $configFields;
$data['paymentTypeId'] = $accountGateway->getPaymentType(); $data['paymentTypeId'] = $accountGateway->getPaymentType();
$data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get();
return View::make('accounts.account_gateway', $data); return View::make('accounts.account_gateway', $data);
} }
@ -94,6 +96,7 @@ class AccountGatewayController extends BaseController
$data['url'] = 'gateways'; $data['url'] = 'gateways';
$data['method'] = 'POST'; $data['method'] = 'POST';
$data['title'] = trans('texts.add_gateway'); $data['title'] = trans('texts.add_gateway');
$data['selectGateways'] = Gateway::where('payment_library_id', '=', 1)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->orderBy('name')->get();
return View::make('accounts.account_gateway', $data); return View::make('accounts.account_gateway', $data);
} }
@ -127,7 +130,6 @@ class AccountGatewayController extends BaseController
$account->load('account_gateways'); $account->load('account_gateways');
$currentGateways = $account->account_gateways; $currentGateways = $account->account_gateways;
$gateways = Gateway::where('payment_library_id', '=', 1)->orderBy('name')->get(); $gateways = Gateway::where('payment_library_id', '=', 1)->orderBy('name')->get();
$selectGateways = Gateway::where('payment_library_id', '=', 1)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->where('id', '!=', GATEWAY_PAYPAL_EXPRESS)->orderBy('name')->get();
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
$gateway->fields = $gateway->getFields(); $gateway->fields = $gateway->getFields();
@ -147,7 +149,6 @@ class AccountGatewayController extends BaseController
'accountGateway' => $accountGateway, 'accountGateway' => $accountGateway,
'config' => false, 'config' => false,
'gateways' => $gateways, 'gateways' => $gateways,
'selectGateways' => $selectGateways,
'creditCardTypes' => $creditCards, 'creditCardTypes' => $creditCards,
'tokenBillingOptions' => $tokenBillingOptions, 'tokenBillingOptions' => $tokenBillingOptions,
'showBreadcrumbs' => false, 'showBreadcrumbs' => false,
@ -174,8 +175,15 @@ class AccountGatewayController extends BaseController
public function save($accountGatewayPublicId = false) public function save($accountGatewayPublicId = false)
{ {
$rules = array(); $rules = array();
$paymentType = Input::get('payment_type_id');
$gatewayId = Input::get('gateway_id'); $gatewayId = Input::get('gateway_id');
if ($paymentType == PAYMENT_TYPE_PAYPAL) {
$gatewayId = GATEWAY_PAYPAL_EXPRESS;
} elseif ($paymentType == PAYMENT_TYPE_BITCOIN) {
$gatewayId = GATEWAY_BITPAY;
}
if (!$gatewayId) { if (!$gatewayId) {
Session::flash('error', trans('validation.required', ['attribute' => 'gateway'])); Session::flash('error', trans('validation.required', ['attribute' => 'gateway']));
return Redirect::to('gateways/create') return Redirect::to('gateways/create')
@ -206,25 +214,28 @@ class AccountGatewayController extends BaseController
->withInput(); ->withInput();
} else { } else {
$account = Account::with('account_gateways')->findOrFail(Auth::user()->account_id); $account = Account::with('account_gateways')->findOrFail(Auth::user()->account_id);
$oldConfig = null;
if ($accountGatewayPublicId) { if ($accountGatewayPublicId) {
$accountGateway = AccountGateway::scope($accountGatewayPublicId)->firstOrFail(); $accountGateway = AccountGateway::scope($accountGatewayPublicId)->firstOrFail();
$oldConfig = json_decode($accountGateway->config);
} else { } else {
$accountGateway = AccountGateway::createNew(); $accountGateway = AccountGateway::createNew();
$accountGateway->gateway_id = $gatewayId; $accountGateway->gateway_id = $gatewayId;
} }
$isMasked = false;
$config = new stdClass(); $config = new stdClass();
foreach ($fields as $field => $details) { foreach ($fields as $field => $details) {
$value = trim(Input::get($gateway->id.'_'.$field)); $value = trim(Input::get($gateway->id.'_'.$field));
// if the new value is masked use the original value
if ($value && $value === str_repeat('*', strlen($value))) { if ($value && $value === str_repeat('*', strlen($value))) {
$isMasked = true; $value = $oldConfig->$field;
}
if (!$value && ($field == 'testMode' || $field == 'developerMode')) {
// do nothing
} else {
$config->$field = $value;
} }
$config->$field = $value;
} }
$cardCount = 0; $cardCount = 0;
@ -234,19 +245,12 @@ class AccountGatewayController extends BaseController
} }
} }
// if the values haven't changed don't update the config $accountGateway->accepted_credit_cards = $cardCount;
if ($isMasked && $accountGatewayPublicId) { $accountGateway->config = json_encode($config);
$accountGateway->accepted_credit_cards = $cardCount;
if ($accountGatewayPublicId) {
$accountGateway->save(); $accountGateway->save();
// if there's an existing config for this gateway update it
} elseif (!$isMasked && $accountGatewayPublicId && $accountGateway->gateway_id == $gatewayId) {
$accountGateway->accepted_credit_cards = $cardCount;
$accountGateway->config = json_encode($config);
$accountGateway->save();
// otherwise, create a new gateway config
} else { } else {
$accountGateway->config = json_encode($config);
$accountGateway->accepted_credit_cards = $cardCount;
$account->account_gateways()->save($accountGateway); $account->account_gateways()->save($accountGateway);
} }

View File

@ -95,6 +95,7 @@ class AppController extends BaseController
// Artisan::call('migrate:rollback', array('--force' => true)); // Debug Purposes // Artisan::call('migrate:rollback', array('--force' => true)); // Debug Purposes
Artisan::call('migrate', array('--force' => true)); Artisan::call('migrate', array('--force' => true));
Artisan::call('db:seed', array('--force' => true)); Artisan::call('db:seed', array('--force' => true));
Artisan::call('optimize', array('--force' => true));
$firstName = trim(Input::get('first_name')); $firstName = trim(Input::get('first_name'));
$lastName = trim(Input::get('last_name')); $lastName = trim(Input::get('last_name'));
@ -159,6 +160,7 @@ class AppController extends BaseController
try { try {
Artisan::call('migrate', array('--force' => true)); Artisan::call('migrate', array('--force' => true));
Artisan::call('db:seed', array('--force' => true)); Artisan::call('db:seed', array('--force' => true));
Artisan::call('optimize', array('--force' => true));
} catch (Exception $e) { } catch (Exception $e) {
Response::make($e->getMessage(), 500); Response::make($e->getMessage(), 500);
} }
@ -172,6 +174,7 @@ class AppController extends BaseController
if (!Utils::isNinja()) { if (!Utils::isNinja()) {
try { try {
Artisan::call('migrate', array('--force' => true)); Artisan::call('migrate', array('--force' => true));
Artisan::call('optimize', array('--force' => true));
Cache::flush(); Cache::flush();
} catch (Exception $e) { } catch (Exception $e) {
Response::make($e->getMessage(), 500); Response::make($e->getMessage(), 500);

View File

@ -204,7 +204,8 @@ class ClientController extends BaseController
private function save($publicId = null) private function save($publicId = null)
{ {
$rules = array( $rules = array(
'email' => 'required', 'email' => 'email|required_without:first_name',
'first_name' => 'required_without:email',
); );
$validator = Validator::make(Input::all(), $rules); $validator = Validator::make(Input::all(), $rules);

View File

@ -262,7 +262,7 @@ class PaymentController extends BaseController
$card = new CreditCard($data); $card = new CreditCard($data);
return [ return [
'amount' => $invoice->balance, 'amount' => ($invoice->partial ? $invoice->partial : $invoice->balance),
'card' => $card, 'card' => $card,
'currency' => $currencyCode, 'currency' => $currencyCode,
'returnUrl' => URL::to('complete'), 'returnUrl' => URL::to('complete'),
@ -304,7 +304,7 @@ class PaymentController extends BaseController
$data = [ $data = [
'showBreadcrumbs' => false, 'showBreadcrumbs' => false,
'url' => 'payment/'.$invitationKey, 'url' => 'payment/'.$invitationKey,
'amount' => $invoice->balance, 'amount' => ($invoice->partial ? $invoice->partial : $invoice->balance),
'invoiceNumber' => $invoice->invoice_number, 'invoiceNumber' => $invoice->invoice_number,
'client' => $client, 'client' => $client,
'contact' => $invitation->contact, 'contact' => $invitation->contact,
@ -603,12 +603,17 @@ class PaymentController extends BaseController
$payment->invitation_id = $invitation->id; $payment->invitation_id = $invitation->id;
$payment->account_gateway_id = $accountGateway->id; $payment->account_gateway_id = $accountGateway->id;
$payment->invoice_id = $invoice->id; $payment->invoice_id = $invoice->id;
$payment->amount = $invoice->balance; $payment->amount = $invoice->partial ? $invoice->partial : $invoice->balance;
$payment->client_id = $invoice->client_id; $payment->client_id = $invoice->client_id;
$payment->contact_id = $invitation->contact_id; $payment->contact_id = $invitation->contact_id;
$payment->transaction_reference = $ref; $payment->transaction_reference = $ref;
$payment->payment_date = date_create()->format('Y-m-d'); $payment->payment_date = date_create()->format('Y-m-d');
if ($invoice->partial) {
$invoice->partial = 0;
$invoice->save();
}
if ($payerId) { if ($payerId) {
$payment->payer_id = $payerId; $payment->payer_id = $payerId;
} }

View File

@ -99,6 +99,7 @@ class Invoice extends EntityModel
'custom_value2', 'custom_value2',
'custom_taxes1', 'custom_taxes1',
'custom_taxes2', 'custom_taxes2',
'partial',
]); ]);
$this->client->setVisible([ $this->client->setVisible([

View File

@ -34,7 +34,10 @@ class ClientRepository
public function getErrors($data) public function getErrors($data)
{ {
$contact = isset($data['contacts']) ? (array) $data['contacts'][0] : (isset($data['contact']) ? $data['contact'] : []); $contact = isset($data['contacts']) ? (array) $data['contacts'][0] : (isset($data['contact']) ? $data['contact'] : []);
$validator = \Validator::make($contact, ['email' => 'required|email']); $validator = \Validator::make($contact, [
'email' => 'email|required_without:first_name',
'first_name' => 'required_without:email',
]);
if ($validator->fails()) { if ($validator->fails()) {
return $validator->messages(); return $validator->messages();
} }

View File

@ -182,7 +182,10 @@ class InvoiceRepository
public function getErrors($input) public function getErrors($input)
{ {
$contact = (array) $input->client->contacts[0]; $contact = (array) $input->client->contacts[0];
$rules = ['email' => 'required|email']; $rules = [
'email' => 'email|required_without:first_name',
'first_name' => 'required_without:email',
];
$validator = \Validator::make($contact, $rules); $validator = \Validator::make($contact, $rules);
if ($validator->fails()) { if ($validator->fails()) {
@ -238,6 +241,7 @@ class InvoiceRepository
$invoice->discount = round(Utils::parseFloat($data['discount']), 2); $invoice->discount = round(Utils::parseFloat($data['discount']), 2);
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
$invoice->invoice_number = trim($data['invoice_number']); $invoice->invoice_number = trim($data['invoice_number']);
$invoice->partial = round(Utils::parseFloat($data['partial']), 2);
$invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false; $invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false;
$invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']); $invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']);

View File

@ -17,7 +17,7 @@ class AddCompanyVatNumber extends Migration {
$table->string('vat_number')->nullable(); $table->string('vat_number')->nullable();
}); });
Schema::table('clients', function($table) Schema::table('clients', function($table)
{ {
$table->string('vat_number')->nullable(); $table->string('vat_number')->nullable();
}); });
@ -34,7 +34,8 @@ class AddCompanyVatNumber extends Migration {
{ {
$table->dropColumn('vat_number'); $table->dropColumn('vat_number');
}); });
Schema::table('clients', function($table)
Schema::table('clients', function($table)
{ {
$table->dropColumn('vat_number'); $table->dropColumn('vat_number');
}); });

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddPartialAmountToInvoices extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('invoices', function($table)
{
$table->decimal('partial', 13, 2)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('invoices', function($table)
{
$table->dropColumn('partial');
});
}
}

View File

@ -2900,6 +2900,7 @@ body.modal-open { overflow:inherit; padding-right:inherit !important; }
.radio input[type="radio"], .radio input[type="radio"],
.checkbox input[type="checkbox"] { .checkbox input[type="checkbox"] {
margin-left: 0; margin-left: 0;
padding-left: 0px !important;
margin-right: 5px; margin-right: 5px;
height: inherit; height: inherit;
width: inherit; width: inherit;
@ -2908,3 +2909,7 @@ body.modal-open { overflow:inherit; padding-right:inherit !important; }
position: relative; position: relative;
margin-top: 3px; margin-top: 3px;
} }
div.checkbox > label {
padding-left: 0px !important;
}

View File

@ -792,6 +792,7 @@ body.modal-open { overflow:inherit; padding-right:inherit !important; }
.radio input[type="radio"], .radio input[type="radio"],
.checkbox input[type="checkbox"] { .checkbox input[type="checkbox"] {
margin-left: 0; margin-left: 0;
padding-left: 0px !important;
margin-right: 5px; margin-right: 5px;
height: inherit; height: inherit;
width: inherit; width: inherit;
@ -800,3 +801,7 @@ body.modal-open { overflow:inherit; padding-right:inherit !important; }
position: relative; position: relative;
margin-top: 3px; margin-top: 3px;
} }
div.checkbox > label {
padding-left: 0px !important;
}

View File

@ -32490,7 +32490,11 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2); total += roundToTwo(invoice.custom_value2);
} }
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance)); if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
}
invoice.discount_amount = discount; invoice.discount_amount = discount;
invoice.tax_amount = tax; invoice.tax_amount = tax;
invoice.has_taxes = hasTaxes; invoice.has_taxes = hasTaxes;

View File

@ -967,7 +967,11 @@ function calculateAmounts(invoice) {
total += roundToTwo(invoice.custom_value2); total += roundToTwo(invoice.custom_value2);
} }
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance)); if (NINJA.parseFloat(invoice.partial)) {
invoice.balance_amount = roundToTwo(invoice.partial);
} else {
invoice.balance_amount = roundToTwo(total) - (roundToTwo(invoice.amount) - roundToTwo(invoice.balance));
}
invoice.discount_amount = discount; invoice.discount_amount = discount;
invoice.tax_amount = tax; invoice.tax_amount = tax;
invoice.has_taxes = hasTaxes; invoice.has_taxes = hasTaxes;

View File

@ -589,5 +589,7 @@ return array(
'payment_type_credit_card' => 'Credit card', 'payment_type_credit_card' => 'Credit card',
'payment_type_paypal' => 'PayPal', 'payment_type_paypal' => 'PayPal',
'payment_type_bitcoin' => 'Bitcoin', 'payment_type_bitcoin' => 'Bitcoin',
'knowledge_base' => 'Knowledge Base',
'partial' => 'Partial',
); );

View File

@ -21,6 +21,8 @@
@endif @endif
@endforeach @endforeach
@endif @endif
@else
{!! Former::populateField('gateway_id', GATEWAY_AUTHORIZE_NET) !!}
@endif @endif
{!! Former::select('payment_type_id') {!! Former::select('payment_type_id')
@ -28,7 +30,7 @@
->addGroupClass('payment-type-option') ->addGroupClass('payment-type-option')
->onchange('setPaymentType()') !!} ->onchange('setPaymentType()') !!}
{!! Former::select('gateway_id')->addOption('', '') {!! Former::select('gateway_id')
->dataClass('gateway-dropdown') ->dataClass('gateway-dropdown')
->addGroupClass('gateway-option') ->addGroupClass('gateway-option')
->fromQuery($selectGateways, 'name', 'id') ->fromQuery($selectGateways, 'name', 'id')
@ -42,7 +44,7 @@
@if (in_array($field, ['solutionType', 'landingPage', 'headerImageUrl', 'brandName'])) @if (in_array($field, ['solutionType', 'landingPage', 'headerImageUrl', 'brandName']))
{{-- do nothing --}} {{-- do nothing --}}
@elseif ($field == 'testMode' || $field == 'developerMode') @elseif ($field == 'testMode' || $field == 'developerMode')
{{-- Former::checkbox($gateway->id.'_'.$field)->label(Utils::toSpaceCase($field))->text('Enable') --}} {!! Former::checkbox($gateway->id.'_'.$field)->label(Utils::toSpaceCase($field))->text('Enable')->value('true') !!}
@elseif ($field == 'username' || $field == 'password') @elseif ($field == 'username' || $field == 'password')
{!! Former::text($gateway->id.'_'.$field)->label('API '. ucfirst(Utils::toSpaceCase($field))) !!} {!! Former::text($gateway->id.'_'.$field)->label('API '. ucfirst(Utils::toSpaceCase($field))) !!}
@else @else

View File

@ -72,6 +72,7 @@
<p class="link"> <p class="link">
{!! link_to('/forgot', trans('texts.forgot_password')) !!} {!! link_to('/forgot', trans('texts.forgot_password')) !!}
{!! link_to(NINJA_WEB_URL.'/knowledgebase/', trans('texts.knowledge_base'), ['target' => '_blank', 'class' => 'pull-right']) !!}
</p> </p>

View File

@ -4,8 +4,8 @@
{!! Former::legend('Organization') !!} {!! Former::legend('Organization') !!}
{!! Former::text('name') !!} {!! Former::text('name') !!}
{!! Former::text('id_number') !!} {!! Former::text('id_number') !!}
{!! Former::text('vat_number') !!} {!! Former::text('vat_number') !!}
{!! Former::text('work_phone')->label('Phone') !!} {!! Former::text('work_phone')->label('Phone') !!}
{!! Former::textarea('notes') !!} {!! Former::textarea('notes') !!}

View File

@ -7,11 +7,8 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<!--<h3>{{ $title }} Client</h3>-->
{!! Former::open($url)->addClass('col-md-12 warn-on-exit')->method($method)->rules(array( {!! Former::open($url)->addClass('col-md-12 warn-on-exit')->method($method) !!}
'email' => 'email|required'
)); !!}
@if ($client) @if ($client)
{!! Former::populate($client) !!} {!! Former::populate($client) !!}

View File

@ -5,6 +5,15 @@
<script src="{{ asset('js/pdf_viewer.js') }}" type="text/javascript"></script> <script src="{{ asset('js/pdf_viewer.js') }}" type="text/javascript"></script>
<script src="{{ asset('js/compatibility.js') }}" type="text/javascript"></script> <script src="{{ asset('js/compatibility.js') }}" type="text/javascript"></script>
<style type="text/css">
.partial div.checkbox {
display: inline;
}
.partial span.input-group-addon {
padding-right: 30px;
}
</style>
@stop @stop
@section('content') @section('content')
@ -18,7 +27,6 @@
{!! Former::open($url)->method($method)->addClass('warn-on-exit')->rules(array( {!! Former::open($url)->method($method)->addClass('warn-on-exit')->rules(array(
'client' => 'required', 'client' => 'required',
'email' => 'required',
'product_key' => 'max:20' 'product_key' => 'max:20'
)) !!} )) !!}
@ -51,7 +59,7 @@
@endif @endif
<div data-bind="with: client"> <div data-bind="with: client">
<div style="display:none" class="form-group" data-bind="visible: contacts().length > 0 &amp;&amp; contacts()[0].email(), foreach: contacts"> <div style="display:none" class="form-group" data-bind="visible: contacts().length > 0 &amp;&amp; (contacts()[0].email() || contacts()[0].first_name()), foreach: contacts">
<div class="col-lg-8 col-lg-offset-4"> <div class="col-lg-8 col-lg-offset-4">
<label class="checkbox" data-bind="attr: {for: $index() + '_check'}" onclick="refreshPDF()"> <label class="checkbox" data-bind="attr: {for: $index() + '_check'}" onclick="refreshPDF()">
<input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check'}"> <input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check'}">
@ -68,6 +76,10 @@
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'invoice_date\')"></i>') !!} ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'invoice_date\')"></i>') !!}
{!! Former::text('due_date')->data_bind("datePicker: due_date, valueUpdate: 'afterkeydown'") {!! Former::text('due_date')->data_bind("datePicker: due_date, valueUpdate: 'afterkeydown'")
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'due_date\')"></i>') !!} ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->append('<i class="glyphicon glyphicon-calendar" onclick="toggleDatePicker(\'due_date\')"></i>') !!}
{!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown', enable: is_partial")
->addGroupClass('partial')->append(Former::checkbox('is_partial')->raw()
->data_bind('checked: is_partial')->onclick('onPartialEnabled()') . '&nbsp;' . (trans('texts.enable'))) !!}
</div> </div>
@if ($entityType == ENTITY_INVOICE) @if ($entityType == ENTITY_INVOICE)
<div data-bind="visible: is_recurring" style="display: none"> <div data-bind="visible: is_recurring" style="display: none">
@ -83,7 +95,7 @@
</div> </div>
@else @else
<div data-bind="visible: invoice_status_id() === 0"> <div data-bind="visible: invoice_status_id() === 0">
{!! Former::checkbox('recurring')->onclick('setEmailEnabled()')->text(trans('texts.enable').' &nbsp;&nbsp; <a href="#" onclick="showLearnMore()"><i class="glyphicon glyphicon-question-sign"></i> '.trans('texts.learn_more').'</a>')->data_bind("checked: is_recurring") {!! Former::checkbox('recurring')->onclick('onRecurringEnabled()')->text(trans('texts.enable').' &nbsp;&nbsp; <a href="#" onclick="showLearnMore()"><i class="glyphicon glyphicon-question-sign"></i> '.trans('texts.learn_more').'</a>')->data_bind("checked: is_recurring")
->inlineHelp($invoice && $invoice->last_sent_date ? 'Last invoice sent ' . Utils::dateToString($invoice->last_sent_date) : '') !!} ->inlineHelp($invoice && $invoice->last_sent_date ? 'Last invoice sent ' . Utils::dateToString($invoice->last_sent_date) : '') !!}
</div> </div>
@endif @endif
@ -265,7 +277,7 @@
<td class="hide-border" colspan="3"/> <td class="hide-border" colspan="3"/>
<td style="display:none" data-bind="visible: $root.invoice_item_taxes.show"/> <td style="display:none" data-bind="visible: $root.invoice_item_taxes.show"/>
<td colspan="{{ $account->hide_quantity ? 1 : 2 }}"><b>{{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }}</b></td> <td colspan="{{ $account->hide_quantity ? 1 : 2 }}"><b>{{ trans($entityType == ENTITY_INVOICE ? 'texts.balance_due' : 'texts.total') }}</b></td>
<td style="text-align: right"><span data-bind="text: totals.total"/></td> <td style="text-align: right"><span data-bind="text: totals.total"></span></td>
</tr> </tr>
</tfoot> </tfoot>
@ -588,7 +600,7 @@
}); });
} }
$('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount').change(function() { $('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial').change(function() {
setTimeout(function() { setTimeout(function() {
refreshPDF(); refreshPDF();
}, 1); }, 1);
@ -752,7 +764,7 @@
var isValid = false; var isValid = false;
for (var i=0; i<model.invoice().client().contacts().length; i++) { for (var i=0; i<model.invoice().client().contacts().length; i++) {
var contact = model.invoice().client().contacts()[i]; var contact = model.invoice().client().contacts()[i];
if (isValidEmailAddress(contact.email())) { if (isValidEmailAddress(contact.email()) || contact.first_name()) {
isValid = true; isValid = true;
} else { } else {
isValid = false; isValid = false;
@ -984,10 +996,23 @@
self.clientFormComplete = function() { self.clientFormComplete = function() {
trackUrl('/save_client_form'); trackUrl('/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; var isValid = true;
$("input[name='email']").each(function(item, value) { $("input[name='email']").each(function(item, value) {
var email = $(value).val(); var email = $(value).val();
if (!email || !isValidEmailAddress(email)) { if (!name && (!email || !isValidEmailAddress(email))) {
isValid = false; isValid = false;
} }
}); });
@ -996,25 +1021,12 @@
return; return;
} }
var email = $('#email0').val();
var firstName = $('#first_name').val();
var lastName = $('#last_name').val();
var name = $('#name').val();
if (self.invoice().client().public_id() == 0) { if (self.invoice().client().public_id() == 0) {
self.invoice().client().public_id(-1); self.invoice().client().public_id(-1);
} }
model.setDueDate(); model.setDueDate();
if (name) {
//
} else if (firstName || lastName) {
name = firstName + ' ' + lastName;
} else {
name = email;
}
setComboboxValue($('.client_select'), -1, name); setComboboxValue($('.client_select'), -1, name);
//$('.client_select select').combobox('setSelected'); //$('.client_select select').combobox('setSelected');
@ -1078,6 +1090,8 @@
self.amount = ko.observable(0); self.amount = ko.observable(0);
self.balance = ko.observable(0); self.balance = ko.observable(0);
self.invoice_design_id = ko.observable({{ $account->invoice_design_id }}); self.invoice_design_id = ko.observable({{ $account->invoice_design_id }});
self.partial = ko.observable(0);
self.is_partial = ko.observable(false);
self.custom_value1 = ko.observable(0); self.custom_value1 = ko.observable(0);
self.custom_value2 = ko.observable(0); self.custom_value2 = ko.observable(0);
@ -1111,7 +1125,7 @@
applyComboboxListeners(); applyComboboxListeners();
} }
if (data) { if (data) {
ko.mapping.fromJS(data, self.mapping, self); ko.mapping.fromJS(data, self.mapping, self);
self.is_recurring(parseInt(data.is_recurring)); self.is_recurring(parseInt(data.is_recurring));
} else { } else {
@ -1237,27 +1251,20 @@
} }
}); });
this.totals.rawPaidToDate = ko.computed(function() { self.totals.rawPaidToDate = ko.computed(function() {
return accounting.toFixed(self.amount(),2) - accounting.toFixed(self.balance(),2); return accounting.toFixed(self.amount(),2) - accounting.toFixed(self.balance(),2);
}); });
this.totals.paidToDate = ko.computed(function() { self.totals.paidToDate = ko.computed(function() {
var total = self.totals.rawPaidToDate(); var total = self.totals.rawPaidToDate();
return formatMoney(total, self.client().currency_id()); return formatMoney(total, self.client().currency_id());
}); });
this.totals.total = 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;
/*
var discount = parseFloat(self.discount());
if (discount > 0) {
total = roundToTwo(total * ((100 - discount)/100));
}
*/
var customValue1 = roundToTwo(self.custom_value1()); var customValue1 = roundToTwo(self.custom_value1());
var customValue2 = roundToTwo(self.custom_value2()); var customValue2 = roundToTwo(self.custom_value2());
var customTaxes1 = self.custom_taxes1() == 1; var customTaxes1 = self.custom_taxes1() == 1;
@ -1287,9 +1294,13 @@
total -= paid; total -= paid;
} }
return formatMoney(total, self.client().currency_id()); return total;
}); });
self.totals.total = ko.computed(function() {
return formatMoney(self.is_partial() ? self.partial() : self.totals.rawTotal(), self.client().currency_id());
});
self.onDragged = function(item) { self.onDragged = function(item) {
refreshPDF(); refreshPDF();
} }
@ -1388,11 +1399,13 @@
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/>';
} }
str += self.email(); if (self.email()) {
str += self.email() + '<br/>';
}
@if (Utils::isConfirmed()) @if (Utils::isConfirmed())
if (self.invitation_link()) { if (self.invitation_link()) {
str += '<br/><a href="' + self.invitation_link() + '" target="_blank">{{ trans('texts.view_as_recipient') }}</a>'; str += '<a href="' + self.invitation_link() + '" target="_blank">{{ trans('texts.view_as_recipient') }}</a>';
} }
@endif @endif
@ -1593,10 +1606,22 @@
} }
} }
function setEmailEnabled() function onPartialEnabled()
{
if ($('#is_partial').prop('checked')) {
model.invoice().partial(model.invoice().totals.rawTotal() || '');
} else {
model.invoice().partial('');
}
refreshPDF();
}
function onRecurringEnabled()
{ {
if ($('#recurring').prop('checked')) { if ($('#recurring').prop('checked')) {
$('#email_button').attr('disabled', true); $('#email_button').attr('disabled', true);
model.invoice().partial('');
} else { } else {
$('#email_button').removeAttr('disabled'); $('#email_button').removeAttr('disabled');
} }
@ -1622,12 +1647,12 @@
} }
@if ($data) @if ($data)
window.model = new ViewModel({{ $data }}); window.model = new ViewModel({{ $data }});
@else @else
window.model = new ViewModel(); window.model = new ViewModel();
model.addTaxRate(); model.addTaxRate();
@foreach ($taxRates as $taxRate) @foreach ($taxRates as $taxRate)
model.addTaxRate({{ $taxRate }}); model.addTaxRate({!! $taxRate !!});
@endforeach @endforeach
@if ($invoice) @if ($invoice)
var invoice = {!! $invoice !!}; var invoice = {!! $invoice !!};
@ -1635,7 +1660,10 @@
if (model.invoice().is_recurring() === '0') { if (model.invoice().is_recurring() === '0') {
model.invoice().is_recurring(false); model.invoice().is_recurring(false);
} }
var invitationContactIds = {!! json_encode($invitationContactIds) !!}; if (NINJA.parseFloat(model.invoice().partial())) {
model.invoice().is_partial(true);
}
var invitationContactIds = {!! json_encode($invitationContactIds) !!};
var client = clientMap[invoice.client.public_id]; var client = clientMap[invoice.client.public_id];
if (client) { // in case it's deleted if (client) { // in case it's deleted
for (var i=0; i<client.contacts.length; i++) { for (var i=0; i<client.contacts.length; i++) {
@ -1666,6 +1694,7 @@
// display blank instead of '0' // display blank instead of '0'
if (!NINJA.parseFloat(model.invoice().discount())) model.invoice().discount(''); if (!NINJA.parseFloat(model.invoice().discount())) model.invoice().discount('');
if (!NINJA.parseFloat(model.invoice().partial())) model.invoice().partial('');
if (!model.invoice().custom_value1()) model.invoice().custom_value1(''); if (!model.invoice().custom_value1()) model.invoice().custom_value1('');
if (!model.invoice().custom_value2()) model.invoice().custom_value2(''); if (!model.invoice().custom_value2()) model.invoice().custom_value2('');

View File

@ -9,9 +9,9 @@
<script> <script>
var invoiceDesigns = {{ $invoiceDesigns }}; var invoiceDesigns = {!! $invoiceDesigns !!};
var currentInvoice = {{ $invoice }}; var currentInvoice = {!! $invoice !!};
var versionsJson = {{ $versionsJson }}; var versionsJson = {!! $versionsJson !!};
function getPDFString() { function getPDFString() {
@ -49,10 +49,10 @@
@section('content') @section('content')
{{ Former::open()->addClass('form-inline')->onchange('refreshPDF()') }} {!! Former::open()->addClass('form-inline')->onchange('refreshPDF()') !!}
{{ Former::select('version')->options($versionsSelect)->label(trans('select_version')) }} {!! Former::select('version')->options($versionsSelect)->label(trans('select_version')) !!}
{{ Button::success_link(URL::to($invoice->getEntityType() . 's/' . $invoice->public_id . '/edit'), trans('texts.edit_' . $invoice->getEntityType()), array('class' => 'pull-right')) }} {!! Button::success(trans('texts.edit_' . $invoice->getEntityType()))->asLinkTo('/' . $invoice->getEntityType() . 's/' . $invoice->public_id . '/edit')->withAttributes(array('class' => 'pull-right')) !!}
{{ Former::close() }} {!! Former::close() !!}
<br/>&nbsp;<br/> <br/>&nbsp;<br/>