diff --git a/app/Constants.php b/app/Constants.php index 799a4c1bb524..39522f175529 100644 --- a/app/Constants.php +++ b/app/Constants.php @@ -200,8 +200,11 @@ if (! defined('APP_NAME')) { define('TASK_STATUS_PAID', 4); define('EXPENSE_STATUS_LOGGED', 1); - define('EXPENSE_STATUS_INVOICED', 2); - define('EXPENSE_STATUS_PAID', 3); + define('EXPENSE_STATUS_PENDING', 2); + define('EXPENSE_STATUS_INVOICED', 3); + define('EXPENSE_STATUS_BILLED', 4); + define('EXPENSE_STATUS_PAID', 5); + define('EXPENSE_STATUS_UNPAID', 6); define('CUSTOM_DESIGN', 11); diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index d0be38fc8491..39ef8de542fb 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -98,8 +98,6 @@ class ExpenseController extends BaseController { $expense = $request->entity(); - $expense->expense_date = Utils::fromSqlDate($expense->expense_date); - $actions = []; if ($expense->invoice) { $actions[] = ['url' => URL::to("invoices/{$expense->invoice->public_id}/edit"), 'label' => trans('texts.view_invoice')]; diff --git a/app/Models/Account.php b/app/Models/Account.php index 879cc445dae9..82ef3564c5d6 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -371,6 +371,14 @@ class Account extends Eloquent return $this->belongsTo('App\Models\TaxRate'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function payment_type() + { + return $this->belongsTo('App\Models\PaymentType'); + } + /** * @return mixed */ diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 109c461b5d8a..a14625ea8390 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -47,6 +47,9 @@ class Expense extends EntityModel 'tax_name1', 'tax_rate2', 'tax_name2', + 'payment_date', + 'payment_type_id', + 'transaction_reference', ]; public static function getImportColumns() @@ -129,6 +132,14 @@ class Expense extends EntityModel 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 */ @@ -175,6 +186,14 @@ class Expense extends EntityModel 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 */ @@ -221,19 +240,23 @@ class Expense extends EntityModel { $statuses = []; $statuses[EXPENSE_STATUS_LOGGED] = trans('texts.logged'); + $statuses[EXPENSE_STATUS_PENDING] = trans('texts.pending'); $statuses[EXPENSE_STATUS_INVOICED] = trans('texts.invoiced'); + $statuses[EXPENSE_STATUS_BILLED] = trans('texts.billed'); $statuses[EXPENSE_STATUS_PAID] = trans('texts.paid'); + $statuses[EXPENSE_STATUS_UNPAID] = trans('texts.unpaid'); + return $statuses; } - public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance) + public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance, $paymentDate) { if ($invoiceId) { if (floatval($balance) > 0) { $label = 'invoiced'; } else { - $label = 'paid'; + $label = 'billed'; } } elseif ($shouldBeInvoiced) { $label = 'pending'; @@ -241,7 +264,13 @@ class Expense extends EntityModel $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) @@ -270,7 +299,7 @@ class Expense extends EntityModel { $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); } } diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php index d29408ab31e8..30ccffab4fd5 100644 --- a/app/Ninja/Datatables/ExpenseDatatable.php +++ b/app/Ninja/Datatables/ExpenseDatatable.php @@ -90,7 +90,7 @@ class ExpenseDatatable extends EntityDatatable [ 'status', 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); return "

$label

"; diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php index 9f8ac4c5506b..100a0de8cded 100644 --- a/app/Ninja/Presenters/ExpensePresenter.php +++ b/app/Ninja/Presenters/ExpensePresenter.php @@ -26,6 +26,14 @@ class ExpensePresenter extends EntityPresenter return Utils::fromSqlDate($this->entity->expense_date); } + /** + * @return \DateTime|string + */ + public function payment_date() + { + return Utils::fromSqlDate($this->entity->payment_date); + } + public function month() { return Carbon::parse($this->entity->payment_date)->format('Y m'); diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index 6a322e153d79..ba539eb254a7 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -81,6 +81,7 @@ class ExpenseRepository extends BaseRepository 'expenses.user_id', 'expenses.tax_rate1', 'expenses.tax_rate2', + 'expenses.payment_date', 'expense_categories.name as category', 'expense_categories.user_id as category_user_id', 'expense_categories.public_id as category_public_id', @@ -112,14 +113,24 @@ class ExpenseRepository extends BaseRepository } if (in_array(EXPENSE_STATUS_INVOICED, $statuses)) { $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); } } - if (in_array(EXPENSE_STATUS_PAID, $statuses)) { + if (in_array(EXPENSE_STATUS_BILLED, $statuses)) { $query->orWhere('invoices.balance', '=', 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'])) { $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; diff --git a/app/Providers/ComposerServiceProvider.php b/app/Providers/ComposerServiceProvider.php index f20211cee4bc..ef2512c796f0 100644 --- a/app/Providers/ComposerServiceProvider.php +++ b/app/Providers/ComposerServiceProvider.php @@ -20,6 +20,7 @@ class ComposerServiceProvider extends ServiceProvider 'vendors.edit', 'payments.edit', 'invoices.edit', + 'expenses.edit', 'accounts.localization', 'payments.credit_card', ], diff --git a/database/migrations/2017_04_16_101744_add_custom_contact_fields.php b/database/migrations/2017_04_16_101744_add_custom_contact_fields.php index 69971fdc1c08..0f4b1a67b1d4 100644 --- a/database/migrations/2017_04_16_101744_add_custom_contact_fields.php +++ b/database/migrations/2017_04_16_101744_add_custom_contact_fields.php @@ -21,7 +21,7 @@ class AddCustomContactFields extends Migration $table->string('custom_value1')->nullable(); $table->string('custom_value2')->nullable(); }); - + Schema::table('payment_methods', function ($table) { $table->unsignedInteger('account_gateway_token_id')->nullable()->change(); $table->dropForeign('payment_methods_account_gateway_token_id_foreign'); @@ -38,6 +38,13 @@ class AddCustomContactFields extends Migration Schema::table('payments', function ($table) { $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_value2'); }); + + Schema::table('expenses', function($table) { + $table->dropColumn('payment_type_id'); + $table->dropColumn('payment_date'); + $table->dropColumn('transaction_reference'); + }); } } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 451ecdde235f..163a285565ac 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -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.', 'datatable_info' => 'Showing :start to :end of :total entries', 'credit_total' => 'Credit Total', + 'mark_billable' => 'Mark Billable', + 'billed' => 'Billed', ); diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index e9d205bb7bb5..3bfcb346a914 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -78,12 +78,37 @@ @if (!$expense || ($expense && !$expense->invoice_id)) {!! Former::checkbox('should_be_invoiced') - ->text(trans('texts.billable')) + ->text(trans('texts.mark_billable')) ->data_bind('checked: should_be_invoiced()') ->label(' ') ->value(1) !!} @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 + +
+ {!! 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('') !!} + + {!! Former::text('transaction_reference') !!} +
+ @endif + @if (!$expense || ($expense && ! $expense->isExchanged())) {!! Former::checkbox('convert_currency') ->text(trans('texts.convert_currency')) @@ -302,7 +327,7 @@ 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() { toggleDatePicker('expense_date'); @@ -349,6 +374,23 @@ 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 dropzone = new Dropzone('#document-upload', { url:{!! json_encode(url('documents')) !!}, @@ -406,6 +448,7 @@ self.amount = ko.observable(); self.exchange_rate = ko.observable(1); 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.apply_taxes = ko.observable({{ ($expense && ($expense->tax_name1 || $expense->tax_name2)) ? 'true' : 'false' }}); diff --git a/resources/views/export/expenses.blade.php b/resources/views/export/expenses.blade.php index d1668eb6f78a..6de84218b9c0 100644 --- a/resources/views/export/expenses.blade.php +++ b/resources/views/export/expenses.blade.php @@ -7,8 +7,11 @@ {{ trans('texts.expense_date') }} {{ trans('texts.amount') }} {{ trans('texts.category') }} + {{ trans('texts.status') }} {{ trans('texts.public_notes') }} {{ trans('texts.private_notes') }} + {{ trans('texts.payment_date') }} + {{ trans('texts.transaction_reference') }} @foreach ($expenses as $expense) @@ -21,7 +24,10 @@ {{ $expense->present()->expense_date }} {{ $expense->present()->amount }} {{ $expense->present()->category }} + {{ $expense->statusLabel() }} {{ $expense->public_notes }} {{ $expense->private_notes }} + {{ $expense->present()->payment_date }} + {{ $expense->transaction_reference }} @endforeach