Expanded to credit cards.

This commit is contained in:
James Cole
2015-04-03 22:54:21 +02:00
parent 78d04230d5
commit b2254875b2
20 changed files with 330 additions and 159 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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
];
}
}

View File

@@ -1,9 +1,11 @@
<?php namespace FireflyIII\Models;
use App;
use Crypt;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\JoinClause;
use Watson\Validating\ValidatingTrait;
/**
@@ -15,15 +17,46 @@ class Account extends Model
{
use SoftDeletes, ValidatingTrait;
protected $fillable = ['user_id', 'account_type_id', 'name', 'active','virtual_balance'];
protected $fillable = ['user_id', 'account_type_id', 'name', 'active', 'virtual_balance'];
protected $rules
= [
= [
'user_id' => '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

View File

@@ -1,8 +1,8 @@
<?php namespace FireflyIII\Models;
use Crypt;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Crypt;
/**
* Class Budget
@@ -32,6 +32,23 @@ class Budget extends Model
return ['created_at', 'updated_at', 'deleted_at'];
}
/**
* @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\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
}
}

View File

@@ -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
*/

View File

@@ -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();
}
}

View File

@@ -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.*']);
}
}

View File

@@ -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

View File

@@ -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',

View File

@@ -10,16 +10,19 @@
</div>
<div class="panel-body">
<p>
Are you sure?
Are you sure that you want to delete the {{strtolower($account->accountType->type)}} "{{$account->name}}"?
</p>
@if($account->transactions()->count() > 0)
<p class="text-info">
Account "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it.
These will be deleted as well.
<p class="text-danger">
{{ucfirst($account->accountType->type)}} "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it. These will be deleted as well.
</p>
@endif
@if($account->piggyBanks()->count() > 0)
<p class="text-danger">
{{ucfirst($account->accountType->type)}} "{{{$account->name}}}" still has {{$account->piggyBanks()->count()}} piggy bank(s) associated to it. These will be deleted as well.
</p>
@endif
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>
<a href="{{URL::previous()}}" class="btn-default btn">Cancel</a >

View File

@@ -45,7 +45,8 @@
<i class="fa fa-credit-card"></i> Credit card options
</div>
<div class="panel-body">
Will be here.
{!! ExpandedForm::select('ccType',Config::get('firefly.ccTypes')) !!}
{!! ExpandedForm::date('ccMonthlyPaymentDate',null,['helpText' => 'Select any year and any month, it will be ignored anway. Only the day of the month is relevant.']) !!}
</div>
</div>
@endif

View File

@@ -10,9 +10,16 @@
</div>
<div class="panel-body">
<p>
Are you sure?
Are you sure that you want to delete bill "{{{$bill->name}}}"?
</p>
@if($bill->transactionjournals()->count() > 0)
<p class="text-info">
Bill "{{{$bill->name}}}" still has {{$bill->transactionjournals()->count()}} transactions connected
to it. These will <strong>not</strong> be removed but will lose their connection to this bill.
</p>
@endif
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>
<a href="{{URL::previous()}}" class="btn-default btn">Cancel</a >

View File

@@ -10,9 +10,16 @@
</div>
<div class="panel-body">
<p>
Are you sure?
Are you sure that you want to delete budget "{{{$budget->name}}}"?
</p>
@if($budget->transactionjournals()->count() > 0)
<p class="text-info">
Budget "{{{$budget->name}}}" still has {{$budget->transactionjournals()->count()}} transactions connected
to it. These will <strong>not</strong> be removed but will lose their connection to this budget.
</p>
@endif
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>
<a href="{{URL::previous()}}" class="btn-default btn">Cancel</a >

View File

@@ -10,9 +10,16 @@
</div>
<div class="panel-body">
<p>
Are you sure?
Are you sure that you want to delete category "{{$category->name}}"?
</p>
@if($category->transactionjournals()->count() > 0)
<p class="text-info">
Category "{{{$category->name}}}" still has {{$category->transactionjournals()->count()}} transactions connected
to it. These will <strong>not</strong> be removed but will lose their connection to this category.
</p>
@endif
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>
<a href="{{URL::previous()}}" class="btn-default btn">Cancel</a >

View File

@@ -10,7 +10,7 @@
</div>
<div class="panel-body">
<p>
Are you sure?
Are you sure that you want to delete currency "{{{$currency->name}}}"?
</p>
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>

View File

@@ -2,6 +2,7 @@
<label for="{{{$options['id']}}}" class="col-sm-4 control-label">{{{$label}}}</label>
<div class="col-sm-8">
{!! Form::input('date', $name, $value, $options) !!}
@include('form.help')
@include('form.feedback')
</div>
</div>

View File

@@ -0,0 +1,3 @@
@if(isset($options['helpText']))
<p class="help-block">{{$options['helpText']}}</p>
@endif

View File

@@ -0,0 +1,8 @@
<div class="{{{$classes}}}">
<label for="{{{$options['id']}}}" class="col-sm-4 control-label">{{{$label}}}</label>
<div class="col-sm-8">
{!! Form::input('month', $name, $value, $options) !!}
@include('form.help')
@include('form.feedback')
</div>
</div>

View File

@@ -2,9 +2,7 @@
<label for="{{{$options['id']}}}" class="col-sm-4 control-label">{{{$label}}}</label>
<div class="col-sm-8">
{!! Form::select($name, $list, $selected , $options ) !!}
@if(isset($options['helpText']))
<p class="help-block">{{$options['helpText']}}</p>
@endif
@include('form.help')
@include('form.feedback')
</div>