diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 910eac7db5..b671775f0c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -84,10 +84,11 @@ class AccountController extends Controller */ public function edit(Account $account, AccountRepositoryInterface $repository) { - $what = Config::get('firefly.shortNamesByFullName')[$account->accountType->type]; - $subTitle = 'Edit ' . strtolower(e($account->accountType->type)) . ' "' . e($account->name) . '"'; - $subTitleIcon = Config::get('firefly.subIconsByIdentifier.' . $what); - $openingBalance = $repository->openingBalanceTransaction($account); + + $what = Config::get('firefly.shortNamesByFullName')[$account->accountType->type]; + $subTitle = 'Edit ' . strtolower(e($account->accountType->type)) . ' "' . e($account->name) . '"'; + $subTitleIcon = Config::get('firefly.subIconsByIdentifier.' . $what); + $openingBalance = $repository->openingBalanceTransaction($account); // pre fill some useful values. @@ -100,10 +101,12 @@ class AccountController extends Controller } $preFilled = [ - 'accountRole' => $account->getMeta('accountRole'), - 'openingBalanceDate' => $openingBalance ? $openingBalance->date->format('Y-m-d') : null, - 'openingBalance' => $openingBalanceAmount, - 'virtualBalance' => floatval($account->virtual_balance) + 'accountRole' => $account->getMeta('accountRole'), + 'ccType' => $account->getMeta('ccType'), + 'ccMonthlyPaymentDate' => $account->getMeta('ccMonthlyPaymentDate'), + 'openingBalanceDate' => $openingBalance ? $openingBalance->date->format('Y-m-d') : null, + 'openingBalance' => $openingBalanceAmount, + 'virtualBalance' => floatval($account->virtual_balance) ]; Session::flash('preFilled', $preFilled); @@ -227,6 +230,8 @@ class AccountController extends Controller 'openingBalance' => floatval($request->input('openingBalance')), 'openingBalanceDate' => new Carbon($request->input('openingBalanceDate')), 'openingBalanceCurrency' => intval($request->input('balance_currency_id')), + 'ccType' => $request->input('ccType'), + 'ccMonthlyPaymentDate' => $request->input('ccMonthlyPaymentDate'), ]; $repository->update($account, $accountData); diff --git a/app/Http/Controllers/GoogleChartController.php b/app/Http/Controllers/GoogleChartController.php index 0a36801159..b939f396bc 100644 --- a/app/Http/Controllers/GoogleChartController.php +++ b/app/Http/Controllers/GoogleChartController.php @@ -355,6 +355,47 @@ class GoogleChartController extends Controller } } + /** + * Find credit card accounts and possibly unpaid credit card bills. + */ + $creditCards = Auth::user()->accounts() + ->hasMetaValue('accountRole', 'ccAsset') + ->hasMetaValue('ccType', 'monthlyFull') + ->get( + [ + 'accounts.*', + 'ccType.data as ccType', + 'accountRole.data as accountRole' + ] + ); + // if the balance is not zero, the monthly payment is still underway. + /** @var Account $creditCard */ + foreach ($creditCards as $creditCard) { + $balance = Steam::balance($creditCard, null, true); + $date = new Carbon($creditCard->getMeta('ccMonthlyPaymentDate')); + if ($balance < 0) { + // unpaid! + $unpaid['amount'] += $balance * -1; + $unpaid['items'][] = $creditCard->name . ' (expected on the ' . $date->format('jS') . ')'; + } + if ($balance == 0) { + // find a transfer TO the credit card which should account for + // anything paid. If not, the CC is not yet used. + $transactions = $creditCard->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->before($end)->after($start)->get(); + if ($transactions->count() > 0) { + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $journal = $transaction->transactionJournal; + if ($journal->transactionType->type == 'Transfer') { + $paid['amount'] += floatval($transaction->amount); + $paid['items'][] = $creditCard->name . ' (paid on the ' . $journal->date->format('jS') . ')'; + } + } + } + } + } $chart->addRow('Unpaid: ' . join(', ', $unpaid['items']), $unpaid['amount']); $chart->addRow('Paid: ' . join(', ', $paid['items']), $paid['amount']); $chart->generate(); diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index 0c3a23937a..f5c03fd7d7 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -28,8 +28,9 @@ class AccountFormRequest extends Request */ public function rules() { - $accountRoles = join(',', array_keys(Config::get('firefly.accountRoles'))); - $types = join(',', array_keys(Config::get('firefly.subTitlesByIdentifier'))); + $accountRoles = join(',', array_keys(Config::get('firefly.accountRoles'))); + $types = join(',', array_keys(Config::get('firefly.subTitlesByIdentifier'))); + $ccPaymentTypes = join(',', array_keys(Config::get('firefly.ccTypes'))); $nameRule = 'required|between:1,100|uniqueAccountForUser'; $idRule = ''; @@ -39,15 +40,17 @@ class AccountFormRequest extends Request } return [ - 'id' => $idRule, - 'name' => $nameRule, - 'openingBalance' => 'numeric', - 'virtualBalance' => 'numeric', - 'openingBalanceDate' => 'date', - 'accountRole' => 'in:' . $accountRoles, - 'active' => 'boolean', - 'balance_currency_id' => 'exists:transaction_currencies,id', - 'what' => 'in:' . $types + 'id' => $idRule, + 'name' => $nameRule, + 'openingBalance' => 'numeric', + 'virtualBalance' => 'numeric', + 'openingBalanceDate' => 'date', + 'accountRole' => 'in:' . $accountRoles, + 'active' => 'boolean', + 'ccType' => 'in:' . $ccPaymentTypes, + 'ccMonthlyPaymentDate' => 'date', + 'balance_currency_id' => 'exists:transaction_currencies,id', + 'what' => 'in:' . $types ]; } } diff --git a/app/Models/Account.php b/app/Models/Account.php index e3589ed0dc..d366a85c14 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -1,9 +1,11 @@ 'required|exists:users,id', 'account_type_id' => 'required|exists:account_types,id', 'name' => 'required|between:1,1024|uniqueAccountForUser', 'active' => 'required|boolean' ]; + /** + * @param array $fields + * + * @return Account|null + */ + public static function firstOrCreateEncrypted(array $fields) + { + // everything but the name: + $query = Account::orderBy('id'); + foreach ($fields as $name => $value) { + if ($name != 'name') { + $query->where($name, $value); + } + } + $set = $query->get(['accounts.*']); + /** @var Account $account */ + foreach ($set as $account) { + if ($account->name == $fields['name']) { + return $account; + } + } + // create it! + $account = Account::create($fields); + if (is_null($account->id)) { + // could not create account: + App::abort(500, 'Could not create new account with data: ' . json_encode($fields)); + + } + + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -65,7 +98,22 @@ class Account extends Model } + /** + * @param $value + * + * @return string + */ + public function getNameAttribute($value) + { + if (intval($this->encrypted) == 1) { + return Crypt::decrypt($value); + } + + // @codeCoverageIgnoreStart + return $value; + // @codeCoverageIgnoreEnd + } /** * @return \Illuminate\Database\Eloquent\Relations\HasMany @@ -88,6 +136,22 @@ class Account extends Model $query->whereIn('account_types.type', $types); } + /** + * @param EloquentBuilder $query + * @param string $name + * @param string $value + */ + public function scopeHasMetaValue(EloquentBuilder $query, $name, $value) + { + $joinName = str_replace('.', '_', $name); + $query->leftJoin( + 'account_meta as ' . $joinName, function (JoinClause $join) use ($joinName, $name) { + $join->on($joinName . '.account_id', '=', 'accounts.id')->where($joinName . '.name', '=', $name); + } + ); + $query->where($joinName . '.data', json_encode($value)); + } + /** * @param $value */ @@ -96,46 +160,6 @@ class Account extends Model $this->attributes['name'] = Crypt::encrypt($value); $this->attributes['encrypted'] = true; } - /** - * @param $value - * - * @return string - */ - public function getNameAttribute($value) - { - - if (intval($this->encrypted) == 1) { - return Crypt::decrypt($value); - } - - // @codeCoverageIgnoreStart - return $value; - // @codeCoverageIgnoreEnd - } - - /** - * @param array $fields - * @return Account|null - */ - public static function firstOrCreateEncrypted(array $fields) { - // everything but the name: - $query = Account::orderBy('id'); - foreach($fields as $name => $value) { - if($name != 'name') { - $query->where($name,$value); - } - } - $set = $query->get(['accounts.*']); - /** @var Account $account */ - foreach($set as $account) { - if($account->name == $fields['name']) { - return $account; - } - } - // create it! - return Account::create($fields); - - } /** * @return \Illuminate\Database\Eloquent\Relations\HasMany diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 61d059a11f..63a7e3979b 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -1,8 +1,8 @@ encrypted) == 1) { + return Crypt::decrypt($value); + } + + // @codeCoverageIgnoreStart + return $value; + // @codeCoverageIgnoreEnd + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ @@ -40,6 +57,15 @@ class Budget extends Model return $this->hasManyThrough('FireflyIII\Models\LimitRepetition', 'FireflyIII\Models\BudgetLimit', 'budget_id'); } + /** + * @param $value + */ + public function setNameAttribute($value) + { + $this->attributes['name'] = Crypt::encrypt($value); + $this->attributes['encrypted'] = true; + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ @@ -56,30 +82,5 @@ class Budget extends Model return $this->belongsTo('FireflyIII\User'); } - /** - * @param $value - */ - public function setNameAttribute($value) - { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['encrypted'] = true; - } - /** - * @param $value - * - * @return string - */ - public function getNameAttribute($value) - { - - if (intval($this->encrypted) == 1) { - return Crypt::decrypt($value); - } - - // @codeCoverageIgnoreStart - return $value; - // @codeCoverageIgnoreEnd - } - } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 90af507dae..ab1e04c0be 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -3,7 +3,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Watson\Validating\ValidatingTrait; - +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Carbon\Carbon; /** * Class Transaction * @@ -30,6 +31,28 @@ class Transaction extends Model return $this->belongsTo('FireflyIII\Models\Account'); } + /** + * @param EloquentBuilder $query + * @param Carbon $date + * + * @return mixed + */ + public function scopeAfter(EloquentBuilder $query, Carbon $date) + { + return $query->where('transaction_journals.date', '>=', $date->format('Y-m-d 00:00:00')); + } + + /** + * @param EloquentBuilder $query + * @param Carbon $date + * + * @return mixed + */ + public function scopeBefore(EloquentBuilder $query, Carbon $date) + { + return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d 00:00:00')); + } + /** * @return array */ diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 8b15de67e4..4d580a848e 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -321,17 +321,21 @@ class AccountRepository implements AccountRepositoryInterface */ protected function storeMetadata(Account $account, array $data) { - $metaData = new AccountMeta( - [ - 'account_id' => $account->id, - 'name' => 'accountRole', - 'data' => $data['accountRole'] - ] - ); - if (!$metaData->isValid()) { - App::abort(500); + $validFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType']; + foreach ($validFields as $field) { + if (isset($data[$field])) { + $metaData = new AccountMeta( + [ + 'account_id' => $account->id, + 'name' => $field, + 'data' => $data[$field] + ] + ); + $metaData->save(); + } + + } - $metaData->save(); } /** @@ -412,30 +416,28 @@ class AccountRepository implements AccountRepositoryInterface */ protected function updateMetadata(Account $account, array $data) { - $metaEntries = $account->accountMeta()->get(); + $validFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType']; $updated = false; - /** @var AccountMeta $entry */ - foreach ($metaEntries as $entry) { - if ($entry->name == 'accountRole') { - $entry->data = $data['accountRole']; - $updated = true; + foreach ($validFields as $field) { + $entry = $account->accountMeta()->where('name', $field)->first(); + + // update if new data is present: + if ($entry && isset($data[$field])) { + $entry->data = $data[$field]; $entry->save(); } - } - - if ($updated === false) { - $metaData = new AccountMeta( - [ - 'account_id' => $account->id, - 'name' => 'accountRole', - 'data' => $data['accountRole'] - ] - ); - if (!$metaData->isValid()) { - App::abort(500); + // no entry but data present? + if (!$entry && isset($data[$field])) { + $metaData = new AccountMeta( + [ + 'account_id' => $account->id, + 'name' => $field, + 'data' => $data[$field] + ] + ); + $metaData->save(); } - $metaData->save(); } } diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 851c879ed3..fdda6ae3d4 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -2,6 +2,7 @@ namespace FireflyIII\Repositories\Journal; +use App; use Auth; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -11,6 +12,7 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use Illuminate\Support\Collection; +use Log; /** * Class JournalRepository @@ -20,6 +22,16 @@ use Illuminate\Support\Collection; class JournalRepository implements JournalRepositoryInterface { + /** + * Get users first transaction journal + * + * @return TransactionJournal + */ + public function first() + { + return Auth::user()->transactionjournals()->orderBy('date', 'ASC')->first(['transaction_journals.*']); + } + /** * * Get the account_id, which is the asset account that paid for the transaction. @@ -149,7 +161,7 @@ class JournalRepository implements JournalRepositoryInterface 'amount' => $data['amount'] * -1 ] ); - $transaction = Transaction::create( // second transaction. + Transaction::create( // second transaction. [ 'account_id' => $to->id, 'transaction_journal_id' => $journal->id, @@ -226,7 +238,7 @@ class JournalRepository implements JournalRepositoryInterface protected function storeAccounts(TransactionType $type, array $data) { $from = null; - $to = null; + $to = null; switch ($type->type) { case 'Withdrawal': @@ -234,12 +246,14 @@ class JournalRepository implements JournalRepositoryInterface if (strlen($data['expense_account']) > 0) { $toType = AccountType::where('type', 'Expense account')->first(); - $to = Account::firstOrCreate( + $to = Account::firstOrCreateEncrypted( ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => $data['expense_account'], 'active' => 1] ); } else { $toType = AccountType::where('type', 'Cash account')->first(); - $to = Account::firstOrCreateEncrypted(['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1]); + $to = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] + ); } break; @@ -253,7 +267,9 @@ class JournalRepository implements JournalRepositoryInterface ); } else { $toType = AccountType::where('type', 'Cash account')->first(); - $from = Account::firstOrCreateEncrypted(['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1]); + $from = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] + ); } break; @@ -262,18 +278,16 @@ class JournalRepository implements JournalRepositoryInterface $to = Account::find($data['account_to_id']); break; } + if (is_null($to->id)) { + Log::error('"to"-account is null, so we cannot continue!'); + App::abort(500, '"to"-account is null, so we cannot continue!'); + } + if (is_null($from->id)) { + Log::error('"from"-account is null, so we cannot continue!'); + App::abort(500, '"from"-account is null, so we cannot continue!'); + } return [$from, $to]; } - /** - * Get users first transaction journal - * - * @return TransactionJournal - */ - public function first() - { - return Auth::user()->transactionjournals()->orderBy('date', 'ASC')->first(['transaction_journals.*']); - } - } diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index eb82ceb247..e0d93024c2 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -53,20 +53,22 @@ class ExpandedForm return $options['label']; } $labels = [ - 'amount_min' => 'Amount (min)', - 'amount_max' => 'Amount (max)', - 'match' => 'Matches on', - 'repeat_freq' => 'Repetition', - 'account_from_id' => 'Account from', - 'account_to_id' => 'Account to', - 'account_id' => 'Asset account', - 'budget_id' => 'Budget', - 'openingBalance' => 'Opening balance', - 'virtualBalance' => 'Virtual balance', - 'targetamount' => 'Target amount', - 'accountRole' => 'Account role', - 'openingBalanceDate' => 'Opening balance date', - 'piggy_bank_id' => 'Piggy bank']; + 'amount_min' => 'Amount (min)', + 'amount_max' => 'Amount (max)', + 'match' => 'Matches on', + 'repeat_freq' => 'Repetition', + 'account_from_id' => 'Account from', + 'account_to_id' => 'Account to', + 'account_id' => 'Asset account', + 'budget_id' => 'Budget', + 'openingBalance' => 'Opening balance', + 'virtualBalance' => 'Virtual balance', + 'targetamount' => 'Target amount', + 'accountRole' => 'Account role', + 'openingBalanceDate' => 'Opening balance date', + 'ccType' => 'Credit card payment plan', + 'ccMonthlyPaymentDate' => 'Credit card monthly payment date', + 'piggy_bank_id' => 'Piggy bank']; return isset($labels[$name]) ? $labels[$name] : str_replace('_', ' ', ucfirst($name)); @@ -192,6 +194,24 @@ class ExpandedForm return $html; } + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + */ + public function month($name, $value = null, array $options = []) + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $html = View::make('form.month', compact('classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + } + /** * @param $name * @param null $value diff --git a/config/firefly.php b/config/firefly.php index 6e66e64059..4996101cde 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -32,6 +32,9 @@ return [ '6M' => 'half year', 'custom' => '(custom)' ], + 'ccTypes' => [ + 'monthlyFull' => 'Full payment every month' + ], 'range_to_name' => [ '1D' => 'one day', '1W' => 'one week', diff --git a/resources/views/accounts/delete.blade.php b/resources/views/accounts/delete.blade.php index d32bc352f1..b45c9e86de 100644 --- a/resources/views/accounts/delete.blade.php +++ b/resources/views/accounts/delete.blade.php @@ -10,16 +10,19 @@
- Are you sure? + Are you sure that you want to delete the {{strtolower($account->accountType->type)}} "{{$account->name}}"?
@if($account->transactions()->count() > 0) -- Account "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it. - These will be deleted as well. +
+ {{ucfirst($account->accountType->type)}} "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it. These will be deleted as well. +
+ @endif + @if($account->piggyBanks()->count() > 0) ++ {{ucfirst($account->accountType->type)}} "{{{$account->name}}}" still has {{$account->piggyBanks()->count()}} piggy bank(s) associated to it. These will be deleted as well.
@endif -Cancel diff --git a/resources/views/accounts/edit.blade.php b/resources/views/accounts/edit.blade.php index 341cf70051..166aab8f0f 100644 --- a/resources/views/accounts/edit.blade.php +++ b/resources/views/accounts/edit.blade.php @@ -45,7 +45,8 @@ Credit card options
- Are you sure? + Are you sure that you want to delete bill "{{{$bill->name}}}"?
+ @if($bill->transactionjournals()->count() > 0) ++ Bill "{{{$bill->name}}}" still has {{$bill->transactionjournals()->count()}} transactions connected + to it. These will not be removed but will lose their connection to this bill. +
+ @endif +Cancel diff --git a/resources/views/budgets/delete.blade.php b/resources/views/budgets/delete.blade.php index d4acb2d795..c5f4870785 100644 --- a/resources/views/budgets/delete.blade.php +++ b/resources/views/budgets/delete.blade.php @@ -10,9 +10,16 @@
- Are you sure? + Are you sure that you want to delete budget "{{{$budget->name}}}"?
+ @if($budget->transactionjournals()->count() > 0) ++ Budget "{{{$budget->name}}}" still has {{$budget->transactionjournals()->count()}} transactions connected + to it. These will not be removed but will lose their connection to this budget. +
+ @endif +Cancel diff --git a/resources/views/categories/delete.blade.php b/resources/views/categories/delete.blade.php index 77353b9faf..04bd21f0fd 100644 --- a/resources/views/categories/delete.blade.php +++ b/resources/views/categories/delete.blade.php @@ -10,9 +10,16 @@
- Are you sure? + Are you sure that you want to delete category "{{$category->name}}"?
+ @if($category->transactionjournals()->count() > 0) ++ Category "{{{$category->name}}}" still has {{$category->transactionjournals()->count()}} transactions connected + to it. These will not be removed but will lose their connection to this category. +
+ @endif +Cancel diff --git a/resources/views/currency/delete.blade.php b/resources/views/currency/delete.blade.php index b3c0e70e64..3245be5894 100644 --- a/resources/views/currency/delete.blade.php +++ b/resources/views/currency/delete.blade.php @@ -10,7 +10,7 @@
- Are you sure? + Are you sure that you want to delete currency "{{{$currency->name}}}"?
diff --git a/resources/views/form/date.blade.php b/resources/views/form/date.blade.php index 58c3c1e5e3..2eb2c909c5 100644 --- a/resources/views/form/date.blade.php +++ b/resources/views/form/date.blade.php @@ -2,6 +2,7 @@
{{$options['helpText']}}
+@endif \ No newline at end of file diff --git a/resources/views/form/month.blade.php b/resources/views/form/month.blade.php new file mode 100644 index 0000000000..587ce62ff6 --- /dev/null +++ b/resources/views/form/month.blade.php @@ -0,0 +1,8 @@ +{{$options['helpText']}}
- @endif + @include('form.help') @include('form.feedback')