Support marking expenses as paid #1400

This commit is contained in:
Hillel Coren 2017-04-19 17:18:24 +03:00
parent 432c8e94a8
commit 6351086c4e
12 changed files with 141 additions and 16 deletions

View File

@ -200,8 +200,11 @@ if (! defined('APP_NAME')) {
define('TASK_STATUS_PAID', 4); define('TASK_STATUS_PAID', 4);
define('EXPENSE_STATUS_LOGGED', 1); define('EXPENSE_STATUS_LOGGED', 1);
define('EXPENSE_STATUS_INVOICED', 2); define('EXPENSE_STATUS_PENDING', 2);
define('EXPENSE_STATUS_PAID', 3); define('EXPENSE_STATUS_INVOICED', 3);
define('EXPENSE_STATUS_BILLED', 4);
define('EXPENSE_STATUS_PAID', 5);
define('EXPENSE_STATUS_UNPAID', 6);
define('CUSTOM_DESIGN', 11); define('CUSTOM_DESIGN', 11);

View File

@ -98,8 +98,6 @@ class ExpenseController extends BaseController
{ {
$expense = $request->entity(); $expense = $request->entity();
$expense->expense_date = Utils::fromSqlDate($expense->expense_date);
$actions = []; $actions = [];
if ($expense->invoice) { if ($expense->invoice) {
$actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')]; $actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')];

View File

@ -371,6 +371,14 @@ class Account extends Eloquent
return $this->belongsTo('App\Models\TaxRate'); return $this->belongsTo('App\Models\TaxRate');
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function payment_type()
{
return $this->belongsTo('App\Models\PaymentType');
}
/** /**
* @return mixed * @return mixed
*/ */

View File

@ -47,6 +47,9 @@ class Expense extends EntityModel
'tax_name1', 'tax_name1',
'tax_rate2', 'tax_rate2',
'tax_name2', 'tax_name2',
'payment_date',
'payment_type_id',
'transaction_reference',
]; ];
public static function getImportColumns() public static function getImportColumns()
@ -129,6 +132,14 @@ class Expense extends EntityModel
return $this->hasMany('App\Models\Document')->orderBy('id'); return $this->hasMany('App\Models\Document')->orderBy('id');
} }
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function payment_type()
{
return $this->belongsTo('App\Models\PaymentType');
}
/** /**
* @return mixed * @return mixed
*/ */
@ -175,6 +186,14 @@ class Expense extends EntityModel
return $this->invoice_currency_id != $this->expense_currency_id || $this->exchange_rate != 1; return $this->invoice_currency_id != $this->expense_currency_id || $this->exchange_rate != 1;
} }
/**
* @return bool
*/
public function isPaid()
{
return $this->payment_date || $this->payment_type_id;
}
/** /**
* @return float * @return float
*/ */
@ -221,19 +240,23 @@ class Expense extends EntityModel
{ {
$statuses = []; $statuses = [];
$statuses[EXPENSE_STATUS_LOGGED] = trans('texts.logged'); $statuses[EXPENSE_STATUS_LOGGED] = trans('texts.logged');
$statuses[EXPENSE_STATUS_PENDING] = trans('texts.pending');
$statuses[EXPENSE_STATUS_INVOICED] = trans('texts.invoiced'); $statuses[EXPENSE_STATUS_INVOICED] = trans('texts.invoiced');
$statuses[EXPENSE_STATUS_BILLED] = trans('texts.billed');
$statuses[EXPENSE_STATUS_PAID] = trans('texts.paid'); $statuses[EXPENSE_STATUS_PAID] = trans('texts.paid');
$statuses[EXPENSE_STATUS_UNPAID] = trans('texts.unpaid');
return $statuses; return $statuses;
} }
public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance) public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance, $paymentDate)
{ {
if ($invoiceId) { if ($invoiceId) {
if (floatval($balance) > 0) { if (floatval($balance) > 0) {
$label = 'invoiced'; $label = 'invoiced';
} else { } else {
$label = 'paid'; $label = 'billed';
} }
} elseif ($shouldBeInvoiced) { } elseif ($shouldBeInvoiced) {
$label = 'pending'; $label = 'pending';
@ -241,7 +264,13 @@ class Expense extends EntityModel
$label = 'logged'; $label = 'logged';
} }
return trans("texts.{$label}"); $label = trans("texts.{$label}");
if ($paymentDate) {
$label = trans('texts.paid') . ' | ' . $label;
}
return $label;
} }
public static function calcStatusClass($shouldBeInvoiced, $invoiceId, $balance) public static function calcStatusClass($shouldBeInvoiced, $invoiceId, $balance)
@ -270,7 +299,7 @@ class Expense extends EntityModel
{ {
$balance = $this->invoice ? $this->invoice->balance : 0; $balance = $this->invoice ? $this->invoice->balance : 0;
return static::calcStatusLabel($this->should_be_invoiced, $this->invoice_id, $balance); return static::calcStatusLabel($this->should_be_invoiced, $this->invoice_id, $balance, $this->payment_date);
} }
} }

View File

@ -90,7 +90,7 @@ class ExpenseDatatable extends EntityDatatable
[ [
'status', 'status',
function ($model) { function ($model) {
return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced, $model->balance); return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced, $model->balance, $model->payment_date);
}, },
], ],
]; ];
@ -129,9 +129,9 @@ class ExpenseDatatable extends EntityDatatable
]; ];
} }
private function getStatusLabel($invoiceId, $shouldBeInvoiced, $balance) private function getStatusLabel($invoiceId, $shouldBeInvoiced, $balance, $paymentDate)
{ {
$label = Expense::calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance); $label = Expense::calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance, $paymentDate);
$class = Expense::calcStatusClass($shouldBeInvoiced, $invoiceId, $balance); $class = Expense::calcStatusClass($shouldBeInvoiced, $invoiceId, $balance);
return "<h4><div class=\"label label-{$class}\">$label</div></h4>"; return "<h4><div class=\"label label-{$class}\">$label</div></h4>";

View File

@ -26,6 +26,14 @@ class ExpensePresenter extends EntityPresenter
return Utils::fromSqlDate($this->entity->expense_date); return Utils::fromSqlDate($this->entity->expense_date);
} }
/**
* @return \DateTime|string
*/
public function payment_date()
{
return Utils::fromSqlDate($this->entity->payment_date);
}
public function month() public function month()
{ {
return Carbon::parse($this->entity->payment_date)->format('Y m'); return Carbon::parse($this->entity->payment_date)->format('Y m');

View File

@ -81,6 +81,7 @@ class ExpenseRepository extends BaseRepository
'expenses.user_id', 'expenses.user_id',
'expenses.tax_rate1', 'expenses.tax_rate1',
'expenses.tax_rate2', 'expenses.tax_rate2',
'expenses.payment_date',
'expense_categories.name as category', 'expense_categories.name as category',
'expense_categories.user_id as category_user_id', 'expense_categories.user_id as category_user_id',
'expense_categories.public_id as category_public_id', 'expense_categories.public_id as category_public_id',
@ -112,14 +113,24 @@ class ExpenseRepository extends BaseRepository
} }
if (in_array(EXPENSE_STATUS_INVOICED, $statuses)) { if (in_array(EXPENSE_STATUS_INVOICED, $statuses)) {
$query->orWhere('expenses.invoice_id', '>', 0); $query->orWhere('expenses.invoice_id', '>', 0);
if (! in_array(EXPENSE_STATUS_PAID, $statuses)) { if (! in_array(EXPENSE_STATUS_BILLED, $statuses)) {
$query->where('invoices.balance', '>', 0); $query->where('invoices.balance', '>', 0);
} }
} }
if (in_array(EXPENSE_STATUS_PAID, $statuses)) { if (in_array(EXPENSE_STATUS_BILLED, $statuses)) {
$query->orWhere('invoices.balance', '=', 0) $query->orWhere('invoices.balance', '=', 0)
->where('expenses.invoice_id', '>', 0); ->where('expenses.invoice_id', '>', 0);
} }
if (in_array(EXPENSE_STATUS_PAID, $statuses)) {
$query->orWhereNotNull('expenses.payment_date');
}
if (in_array(EXPENSE_STATUS_UNPAID, $statuses)) {
$query->orWhereNull('expenses.payment_date');
}
if (in_array(EXPENSE_STATUS_PENDING, $statuses)) {
$query->orWhere('expenses.should_be_invoiced', '=', 1)
->whereNull('expenses.invoice_id');
}
}); });
} }
@ -161,6 +172,9 @@ class ExpenseRepository extends BaseRepository
if (isset($input['expense_date'])) { if (isset($input['expense_date'])) {
$expense->expense_date = Utils::toSqlDate($input['expense_date']); $expense->expense_date = Utils::toSqlDate($input['expense_date']);
} }
if (isset($input['payment_date'])) {
$expense->payment_date = Utils::toSqlDate($input['payment_date']);
}
$expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false; $expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false;

View File

@ -20,6 +20,7 @@ class ComposerServiceProvider extends ServiceProvider
'vendors.edit', 'vendors.edit',
'payments.edit', 'payments.edit',
'invoices.edit', 'invoices.edit',
'expenses.edit',
'accounts.localization', 'accounts.localization',
'payments.credit_card', 'payments.credit_card',
], ],

View File

@ -21,7 +21,7 @@ class AddCustomContactFields extends Migration
$table->string('custom_value1')->nullable(); $table->string('custom_value1')->nullable();
$table->string('custom_value2')->nullable(); $table->string('custom_value2')->nullable();
}); });
Schema::table('payment_methods', function ($table) { Schema::table('payment_methods', function ($table) {
$table->unsignedInteger('account_gateway_token_id')->nullable()->change(); $table->unsignedInteger('account_gateway_token_id')->nullable()->change();
$table->dropForeign('payment_methods_account_gateway_token_id_foreign'); $table->dropForeign('payment_methods_account_gateway_token_id_foreign');
@ -38,6 +38,13 @@ class AddCustomContactFields extends Migration
Schema::table('payments', function ($table) { Schema::table('payments', function ($table) {
$table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade'); $table->foreign('payment_method_id')->references('id')->on('payment_methods')->onDelete('cascade');
}); });
Schema::table('expenses', function($table) {
$table->unsignedInteger('payment_type_id')->nullable();
$table->date('payment_date')->nullable();
$table->string('transaction_reference')->nullable();
$table->foreign('payment_type_id')->references('id')->on('payment_types');
});
} }
/** /**
@ -56,5 +63,11 @@ class AddCustomContactFields extends Migration
$table->dropColumn('custom_value1'); $table->dropColumn('custom_value1');
$table->dropColumn('custom_value2'); $table->dropColumn('custom_value2');
}); });
Schema::table('expenses', function($table) {
$table->dropColumn('payment_type_id');
$table->dropColumn('payment_date');
$table->dropColumn('transaction_reference');
});
} }
} }

View File

@ -2481,6 +2481,8 @@ $LANG = array(
'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.', 'custom_contact_fields_help' => 'Add a field when creating a contact and display the label and value on the PDF.',
'datatable_info' => 'Showing :start to :end of :total entries', 'datatable_info' => 'Showing :start to :end of :total entries',
'credit_total' => 'Credit Total', 'credit_total' => 'Credit Total',
'mark_billable' => 'Mark Billable',
'billed' => 'Billed',
); );

View File

@ -78,12 +78,37 @@
@if (!$expense || ($expense && !$expense->invoice_id)) @if (!$expense || ($expense && !$expense->invoice_id))
{!! Former::checkbox('should_be_invoiced') {!! Former::checkbox('should_be_invoiced')
->text(trans('texts.billable')) ->text(trans('texts.mark_billable'))
->data_bind('checked: should_be_invoiced()') ->data_bind('checked: should_be_invoiced()')
->label(' ') ->label(' ')
->value(1) !!} ->value(1) !!}
@endif @endif
@if (! $expense || ! $expense->transaction_id)
@if (! $expense || ! $expense->isPaid())
{!! Former::checkbox('mark_paid')
->data_bind('checked: mark_paid')
->text(trans('texts.mark_paid'))
->label(' ')
->value(1) !!}
@endif
<div style="display:none" data-bind="visible: mark_paid">
{!! Former::select('payment_type_id')
->addOption('','')
->fromQuery($paymentTypes, 'name', 'id')
->addGroupClass('payment-type-select') !!}
{!! Former::text('payment_date')
->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT))
->addGroupClass('payment_date')
->append('<i class="glyphicon glyphicon-calendar"></i>') !!}
{!! Former::text('transaction_reference') !!}
</div>
@endif
@if (!$expense || ($expense && ! $expense->isExchanged())) @if (!$expense || ($expense && ! $expense->isExchanged()))
{!! Former::checkbox('convert_currency') {!! Former::checkbox('convert_currency')
->text(trans('texts.convert_currency')) ->text(trans('texts.convert_currency'))
@ -302,7 +327,7 @@
setComboboxValue($('.expense-category-select'), category.public_id, category.name); setComboboxValue($('.expense-category-select'), category.public_id, category.name);
} }
$('#expense_date').datepicker('update', '{{ $expense ? $expense->expense_date : 'new Date()' }}'); $('#expense_date').datepicker('update', '{{ $expense ? Utils::fromSqlDate($expense->expense_date) : 'new Date()' }}');
$('.expense_date .input-group-addon').click(function() { $('.expense_date .input-group-addon').click(function() {
toggleDatePicker('expense_date'); toggleDatePicker('expense_date');
@ -349,6 +374,23 @@
else $(this).removeAttr('enctype') else $(this).removeAttr('enctype')
}) })
$('#payment_type_id').combobox();
$('#mark_paid').click(function(event) {
if ($('#mark_paid').is(':checked')) {
$('#payment_date').datepicker('update', new Date());
@if ($account->payment_type_id)
setComboboxValue($('.payment-type-select'), {{ $account->payment_type_id }}, "{{ trans('texts.payment_type_' . $account->payment_type->name) }}");
@endif
} else {
$('#payment_date').datepicker('update', false);
setComboboxValue($('.payment-type-select'), '', '');
}
})
@if ($expense && $expense->payment_date)
$('#payment_date').datepicker('update', '{{ Utils::fromSqlDate($expense->payment_date) }}');
@endif
// Initialize document upload // Initialize document upload
dropzone = new Dropzone('#document-upload', { dropzone = new Dropzone('#document-upload', {
url:{!! json_encode(url('documents')) !!}, url:{!! json_encode(url('documents')) !!},
@ -406,6 +448,7 @@
self.amount = ko.observable(); self.amount = ko.observable();
self.exchange_rate = ko.observable(1); self.exchange_rate = ko.observable(1);
self.should_be_invoiced = ko.observable(); self.should_be_invoiced = ko.observable();
self.mark_paid = ko.observable({{ $expense && $expense->isPaid() ? 'true' : 'false' }});
self.convert_currency = ko.observable({{ ($expense && $expense->isExchanged()) ? 'true' : 'false' }}); self.convert_currency = ko.observable({{ ($expense && $expense->isExchanged()) ? 'true' : 'false' }});
self.apply_taxes = ko.observable({{ ($expense && ($expense->tax_name1 || $expense->tax_name2)) ? 'true' : 'false' }}); self.apply_taxes = ko.observable({{ ($expense && ($expense->tax_name1 || $expense->tax_name2)) ? 'true' : 'false' }});

View File

@ -7,8 +7,11 @@
<td>{{ trans('texts.expense_date') }}</td> <td>{{ trans('texts.expense_date') }}</td>
<td>{{ trans('texts.amount') }}</td> <td>{{ trans('texts.amount') }}</td>
<td>{{ trans('texts.category') }}</td> <td>{{ trans('texts.category') }}</td>
<td>{{ trans('texts.status') }}</td>
<td>{{ trans('texts.public_notes') }}</td> <td>{{ trans('texts.public_notes') }}</td>
<td>{{ trans('texts.private_notes') }}</td> <td>{{ trans('texts.private_notes') }}</td>
<td>{{ trans('texts.payment_date') }}</td>
<td>{{ trans('texts.transaction_reference') }}</td>
</tr> </tr>
@foreach ($expenses as $expense) @foreach ($expenses as $expense)
@ -21,7 +24,10 @@
<td>{{ $expense->present()->expense_date }}</td> <td>{{ $expense->present()->expense_date }}</td>
<td>{{ $expense->present()->amount }}</td> <td>{{ $expense->present()->amount }}</td>
<td>{{ $expense->present()->category }}</td> <td>{{ $expense->present()->category }}</td>
<td>{{ $expense->statusLabel() }}</td>
<td>{{ $expense->public_notes }}</td> <td>{{ $expense->public_notes }}</td>
<td>{{ $expense->private_notes }}</td> <td>{{ $expense->private_notes }}</td>
<td>{{ $expense->present()->payment_date }}</td>
<td>{{ $expense->transaction_reference }}</td>
</tr> </tr>
@endforeach @endforeach