New code for bills.

This commit is contained in:
James Cole
2015-02-25 15:19:14 +01:00
parent d83b508bbc
commit d36b2318fd
23 changed files with 1255 additions and 63 deletions

View File

@@ -0,0 +1,240 @@
<?php namespace FireflyIII\Http\Controllers;
use Auth;
use Carbon\Carbon;
use FireflyIII\Http\Requests;
use FireflyIII\Http\Requests\BillFormRequest;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use Redirect;
use Session;
use URL;
use View;
/**
* Class BillController
*
* @package FireflyIII\Http\Controllers
*/
class BillController extends Controller
{
public function __construct()
{
View::share('title', 'Bills');
View::share('mainTitleIcon', 'fa-calendar-o');
}
/**
* @return $this
*/
public function create()
{
$periods = \Config::get('firefly.periods_to_text');
return view('bills.create')->with('periods', $periods)->with('subTitle', 'Create new');
}
/**
* @param Bill $bill
*
* @return $this
*/
public function delete(Bill $bill)
{
return view('bills.delete')->with('bill', $bill)->with('subTitle', 'Delete "' . e($bill->name) . '"');
}
/**
* @param Bill $bill
*
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Bill $bill)
{
$bill->delete();
Session::flash('success', 'The bill was deleted.');
return Redirect::route('bills.index');
}
/**
* @param Bill $bill
*
* @return $this
*/
public function edit(Bill $bill)
{
$periods = \Config::get('firefly.periods_to_text');
return View::make('bills.edit')->with('periods', $periods)->with('bill', $bill)->with('subTitle', 'Edit "' . e($bill->name) . '"');
}
/**
* @param BillRepositoryInterface $repository
*
* @return \Illuminate\View\View
*/
public function index(BillRepositoryInterface $repository)
{
$bills = Auth::user()->bills()->get();
$bills->each(
function (Bill $bill) use ($repository) {
$bill->nextExpectedMatch = $repository->nextExpectedMatch($bill);
$last = $bill->transactionjournals()->orderBy('date', 'DESC')->first();
$bill->lastFoundMatch = null;
if ($last) {
$bill->lastFoundMatch = $last->date;
}
}
);
return View::make('bills.index', compact('bills'));
}
/**
* @param Bill $bill
*
* @return mixed
*/
public function rescan(Bill $bill, BillRepositoryInterface $repository)
{
if (intval($bill->active) == 0) {
Session::flash('warning', 'Inactive bills cannot be scanned.');
return Redirect::intended('/');
}
$set = \DB::table('transactions')->where('amount', '>', 0)->where('amount', '>=', $bill->amount_min)->where('amount', '<=', $bill->amount_max)->get(['transaction_journal_id']);
$ids = [];
/** @var Transaction $entry */
foreach ($set as $entry) {
$ids[] = intval($entry->transaction_journal_id);
}
if (count($ids) > 0) {
$journals = Auth::user()->transactionjournals()->whereIn('id',$ids)->get();
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
$repository->scan($bill, $journal);
}
}
Session::flash('success', 'Rescanned everything.');
return Redirect::to(URL::previous());
}
/**
* @param Bill $bill
*
* @return mixed
*/
public function show(Bill $bill, BillRepositoryInterface $repository)
{
$journals = $bill->transactionjournals()->withRelevantData()->orderBy('date', 'DESC')->get();
$bill->nextExpectedMatch = $repository->nextExpectedMatch($bill);
$hideBill = true;
return View::make('bills.show', compact('journals', 'hideBill', 'bill'))->with('subTitle', e($bill->name));
}
/**
* @return $this
*/
public function store(BillFormRequest $request, BillRepositoryInterface $repository)
{
var_dump($request->all());
$billData = [
'name' => $request->get('name'),
'match' => $request->get('match'),
'amount_min' => floatval($request->get('amount_min')),
'amount_currency_id' => floatval($request->get('amount_currency_id')),
'amount_max' => floatval($request->get('amount_max')),
'date' => new Carbon($request->get('date')),
'user' => Auth::user()->id,
'repeat_freq' => $request->get('repeat_freq'),
'skip' => intval($request->get('skip')),
'automatch' => intval($request->get('automatch')) === 1,
'active' => intval($request->get('active')) === 1,
];
$bill = $repository->store($billData);
Session::flash('success', 'Bill "' . e($bill->name) . '" stored.');
return Redirect::route('bills.index');
}
/**
* @param Bill $bill
*
* @return $this
*/
public function update(Bill $bill, BillFormRequest $request, BillRepositoryInterface $repository)
{
$billData = [
'name' => $request->get('name'),
'match' => $request->get('match'),
'amount_min' => floatval($request->get('amount_min')),
'amount_currency_id' => floatval($request->get('amount_currency_id')),
'amount_max' => floatval($request->get('amount_max')),
'date' => new Carbon($request->get('date')),
'user' => Auth::user()->id,
'repeat_freq' => $request->get('repeat_freq'),
'skip' => intval($request->get('skip')),
'automatch' => intval($request->get('automatch')) === 1,
'active' => intval($request->get('active')) === 1,
];
$bill = $repository->update($bill, $billData);
Session::flash('success', 'Bill "' . e($bill->name) . '" updated.');
return Redirect::route('bills.index');
// $data = Input::except('_token');
// $data['active'] = intval(Input::get('active'));
// $data['automatch'] = intval(Input::get('automatch'));
// $data['user_id'] = Auth::user()->id;
//
// // always validate:
// $messages = $this->_repository->validate($data);
//
// // flash messages:
// Session::flash('warnings', $messages['warnings']);
// Session::flash('successes', $messages['successes']);
// Session::flash('errors', $messages['errors']);
// if ($messages['errors']->count() > 0) {
// Session::flash('error', 'Could not update bill: ' . $messages['errors']->first());
//
// return Redirect::route('bills.edit', $bill->id)->withInput();
// }
//
// // return to update screen:
// if ($data['post_submit_action'] == 'validate_only') {
// return Redirect::route('bills.edit', $bill->id)->withInput();
// }
//
// // update
// $this->_repository->update($bill, $data);
// Session::flash('success', 'Bill "' . e($data['name']) . '" updated.');
//
// // go back to list
// if ($data['post_submit_action'] == 'update') {
// return Redirect::route('bills.index');
// }
//
// // go back to update screen.
// return Redirect::route('bills.edit', $bill->id)->withInput(['post_submit_action' => 'return_to_edit']);
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace FireflyIII\Http\Controllers;
use Auth;
use Carbon\Carbon;
use FireflyIII\Http\Requests;
use FireflyIII\Http\Requests\CategoryFormRequest;
use FireflyIII\Models\Category;
@@ -55,6 +56,26 @@ class CategoryController extends Controller
return view('categories.show', compact('category', 'journals', 'hideCategory'));
}
/**
* @return \Illuminate\View\View
*/
public function noCategory()
{
$start = Session::get('start', Carbon::now()->startOfMonth());
$end = Session::get('end', Carbon::now()->startOfMonth());
$list = Auth::user()
->transactionjournals()
->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->whereNull('category_transaction_journal.id')
->before($end)
->after($start)
->orderBy('transaction_journals.date')
->get(['transaction_journals.*']);
$subTitle = 'Transactions without a category in ' . $start->format('F Y');
return View::make('categories.noCategory', compact('list', 'subTitle'));
}
/**
* @param Category $category
*

View File

@@ -12,6 +12,7 @@ use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\LimitRepetition;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\Category;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use Grumpydictator\Gchart\GChart;
@@ -21,6 +22,7 @@ use Preferences;
use Response;
use Session;
use Steam;
use Navigation;
/**
* Class GoogleChartController
@@ -73,6 +75,50 @@ class GoogleChartController extends Controller
}
/**
* @param Bill $bill
*
* @return \Illuminate\Http\JsonResponse
*/
public function billOverview(Bill $bill, GChart $chart)
{
$chart->addColumn('Date', 'date');
$chart->addColumn('Max amount', 'number');
$chart->addColumn('Min amount', 'number');
$chart->addColumn('Current entry', 'number');
// get first transaction or today for start:
$first = $bill->transactionjournals()->orderBy('date', 'ASC')->first();
if ($first) {
$start = $first->date;
} else {
$start = new Carbon;
}
$end = new Carbon;
while ($start <= $end) {
$result = $bill->transactionjournals()->before($end)->after($start)->first();
if ($result) {
/** @var Transaction $tr */
foreach($result->transactions()->get() as $tr) {
if(floatval($tr->amount) > 0) {
$amount = floatval($tr->amount);
}
}
} else {
$amount = 0;
}
unset($result);
$chart->addRow(clone $start, $bill->amount_max, $bill->amount_min, $amount);
$start = Navigation::addPeriod($start, $bill->repeat_freq, 0);
}
$chart->generate();
return Response::json($chart->getData());
}
/**
* @param Account $account

View File

@@ -67,4 +67,6 @@ class PiggyBankController extends Controller {
return view('piggy-banks.index', compact('piggyBanks', 'accounts'));
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 25/02/15
* Time: 12:29
*/
namespace FireflyIII\Http\Requests;
use Auth;
use Input;
/**
* Class BillFormRequest
*
* @package FireflyIII\Http\Requests
*/
class BillFormRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged in users
return Auth::check();
}
/**
* @return array
*/
public function rules()
{
$nameRule = 'required|between:1,255|uniqueForUser:bills,name';
if(intval(Input::get('id')) > 0) {
$nameRule .= ','.intval(Input::get('id'));
}
$rules = [
'name' => $nameRule,
'match' => 'required|between:1,255',
'amount_min' => 'required|numeric|min:0.01',
'amount_max' => 'required|numeric|min:0.01',
'amount_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date',
'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly',
'skip' => 'required|between:0,31',
'automatch' => 'in:1',
'active' => 'in:1',
];
return $rules;
}
}

View File

@@ -4,6 +4,7 @@ use DaveJamesMiller\Breadcrumbs\Generator;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Category;
use FireflyIII\Models\LimitRepetition;
use FireflyIII\Models\PiggyBank;

View File

@@ -95,12 +95,16 @@ Route::group(
* Bills Controller
*/
Route::get('/bills', ['uses' => 'BillController@index', 'as' => 'bills.index']);
//Route::get('/bills/rescan/{bill}', ['uses' => 'BillController@rescan', 'as' => 'bills.rescan']); # rescan for matching.
Route::get('/bills/rescan/{bill}', ['uses' => 'BillController@rescan', 'as' => 'bills.rescan']); # rescan for matching.
Route::get('/bills/create', ['uses' => 'BillController@create', 'as' => 'bills.create']);
//Route::get('/bills/edit/{bill}', ['uses' => 'BillController@edit', 'as' => 'bills.edit']);
// Route::get('/bills/delete/{bill}', ['uses' => 'BillController@delete', 'as' => 'bills.delete']);
Route::get('/bills/edit/{bill}', ['uses' => 'BillController@edit', 'as' => 'bills.edit']);
Route::get('/bills/delete/{bill}', ['uses' => 'BillController@delete', 'as' => 'bills.delete']);
Route::get('/bills/show/{bill}', ['uses' => 'BillController@show', 'as' => 'bills.show']);
Route::post('/bills/store', ['uses' => 'BillController@store', 'as' => 'bills.store']);
Route::post('/bills/update/{bill}', ['uses' => 'BillController@update', 'as' => 'bills.update']);
Route::post('/bills/destroy/{bill}', ['uses' => 'BillController@destroy', 'as' => 'bills.destroy']);
/**
* Budget Controller
*/
@@ -155,7 +159,7 @@ Route::group(
Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']);
Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']);
//Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']);
Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']);
// JSON controller
Route::get('/json/expense-accounts', ['uses' => 'JsonController@expenseAccounts', 'as' => 'json.expense-accounts']);

View File

@@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Model;
class Bill extends Model
{
protected $fillable = ['name', 'match', 'amount_min','user_id', 'amount_max', 'date', 'repeat_freq', 'skip', 'automatch', 'active',];
/**
* @return array
*/

View File

@@ -60,6 +60,7 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind('FireflyIII\Repositories\Budget\BudgetRepositoryInterface', 'FireflyIII\Repositories\Budget\BudgetRepository');
$this->app->bind('FireflyIII\Repositories\Category\CategoryRepositoryInterface', 'FireflyIII\Repositories\Category\CategoryRepository');
$this->app->bind('FireflyIII\Repositories\Journal\JournalRepositoryInterface', 'FireflyIII\Repositories\Journal\JournalRepository');
$this->app->bind('FireflyIII\Repositories\Bill\BillRepositoryInterface', 'FireflyIII\Repositories\Bill\BillRepository');
$this->app->bind('FireflyIII\Helpers\Report\ReportHelperInterface', 'FireflyIII\Helpers\Report\ReportHelper');
$this->app->bind('FireflyIII\Helpers\Report\ReportQueryInterface', 'FireflyIII\Helpers\Report\ReportQuery');

View File

@@ -0,0 +1,198 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 25/02/15
* Time: 07:40
*/
namespace FireflyIII\Repositories\Bill;
use Carbon\Carbon;
use FireflyIII\Models\Bill;
use FireflyIII\Models\TransactionJournal;
use Navigation;
use Log;
/**
* Class BillRepository
*
* @package FireflyIII\Repositories\Bill
*/
class BillRepository implements BillRepositoryInterface
{
/**
* @param Bill $bill
*
* @return Carbon
*/
public function nextExpectedMatch(Bill $bill)
{
$finalDate = null;
if ($bill->active == 0) {
return $finalDate;
}
/*
* $today is the start of the next period, to make sure FF3 won't miss anything
* when the current period has a transaction journal.
*/
$today = Navigation::addPeriod(new Carbon, $bill->repeat_freq, 0);
$skip = $bill->skip + 1;
$start = Navigation::startOfPeriod(new Carbon, $bill->repeat_freq);
/*
* go back exactly one month/week/etc because FF3 does not care about 'next'
* bills if they're too far into the past.
*/
$counter = 0;
while ($start <= $today) {
if (($counter % $skip) == 0) {
// do something.
$end = Navigation::endOfPeriod(clone $start, $bill->repeat_freq);
$journalCount = $bill->transactionjournals()->before($end)->after($start)->count();
if ($journalCount == 0) {
$finalDate = clone $start;
break;
}
}
// add period for next round!
$start = Navigation::addPeriod($start, $bill->repeat_freq, 0);
$counter++;
}
return $finalDate;
}
/**
* @param Bill $bill
* @param TransactionJournal $journal
*
* @return bool
*/
public function scan(Bill $bill, TransactionJournal $journal)
{
/*
* Match words.
*/
$wordMatch = false;
$matches = explode(',', $bill->match);
$description = strtolower($journal->description);
Log::debug('Now scanning ' . $description);
/*
* Attach expense account to description for more narrow matching.
*/
if (count($journal->transactions) < 2) {
$transactions = $journal->transactions()->get();
} else {
$transactions = $journal->transactions;
}
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var Account $account */
$account = $transaction->account()->first();
/** @var AccountType $type */
$type = $account->accountType()->first();
if ($type->type == 'Expense account' || $type->type == 'Beneficiary account') {
$description .= ' ' . strtolower($account->name);
}
}
Log::debug('Final description: ' . $description);
Log::debug('Matches searched: ' . join(':', $matches));
$count = 0;
foreach ($matches as $word) {
if (!(strpos($description, strtolower($word)) === false)) {
$count++;
}
}
if ($count >= count($matches)) {
$wordMatch = true;
Log::debug('word match is true');
} else {
Log::debug('Count: ' . $count.', count(matches): ' . count($matches));
}
/*
* Match amount.
*/
$amountMatch = false;
if (count($transactions) > 1) {
$amount = max(floatval($transactions[0]->amount), floatval($transactions[1]->amount));
$min = floatval($bill->amount_min);
$max = floatval($bill->amount_max);
if ($amount >= $min && $amount <= $max) {
$amountMatch = true;
Log::debug('Amount match is true!');
}
}
/*
* If both, update!
*/
if ($wordMatch && $amountMatch) {
Log::debug('TOTAL match is true!');
$journal->bill()->associate($bill);
$journal->save();
}
}
/**
* @param array $data
*
* @return Bill
*/
public function store(array $data)
{
$bill = Bill::create(
[
'name' => $data['name'],
'match' => $data['match'],
'amount_min' => $data['amount_min'],
'user_id' => $data['user'],
'amount_max' => $data['amount_max'],
'date' => $data['date'],
'repeat_freq' => $data['repeat_freq'],
'skip' => $data['skip'],
'automatch' => $data['automatch'],
'active' => $data['active'],
]
);
return $bill;
}
/**
* @param Bill $bill
* @param array $data
*
* @return Bill|static
*/
public function update(Bill $bill, array $data)
{
$bill->name = $data['name'];
$bill->match = $data['match'];
$bill->amount_min = $data['amount_min'];
$bill->amount_max = $data['amount_max'];
$bill->date = $data['date'];
$bill->repeat_freq = $data['repeat_freq'];
$bill->skip = $data['skip'];
$bill->automatch = $data['automatch'];
$bill->active = $data['active'];
$bill->save();
return $bill;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 25/02/15
* Time: 07:40
*/
namespace FireflyIII\Repositories\Bill;
use FireflyIII\Models\Bill;
use FireflyIII\Models\TransactionJournal;
/**
* Interface BillRepositoryInterface
*
* @package FireflyIII\Repositories\Bill
*/
interface BillRepositoryInterface {
/**
* @param Bill $bill
*
* @return Carbon|null
*/
public function nextExpectedMatch(Bill $bill);
/**
* @param array $data
*
* @return Bill
*/
public function store(array $data);
/**
* @param Bill $bill
* @param array $data
*
* @return mixed
*/
public function update(Bill $bill, array $data);
/**
* @param Bill $bill
* @param TransactionJournal $journal
*
* @return bool
*/
public function scan(Bill $bill, TransactionJournal $journal);
}

View File

@@ -16,6 +16,47 @@ use View;
*/
class ExpandedForm
{
/**
* @param $name
* @param null $value
* @param array $options
*
* @return string
*/
public function integer($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);
$options['step'] = '1';
$html = \View::make('form.integer', compact('classes', 'name', 'label', 'value', 'options'))->render();
return $html;
}
/**
* @param $name
* @param null $value
* @param array $options
*
* @return string
*/
public function tags($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);
$options['data-role'] = 'tagsinput';
$html = \View::make('form.tags', compact('classes', 'name', 'label', 'value', 'options'))->render();
return $html;
}
/**
* @param $name
* @param null $value

View File

@@ -15,31 +15,93 @@ class Navigation
/**
* @param Carbon $date
* @param $repeatFrequency
* @param Carbon $theDate
* @param $repeatFreq
* @param $skip
*
* @return string
* @return \Carbon\Carbon
* @throws FireflyException
*/
public function periodShow(Carbon $date, $repeatFrequency)
public function addPeriod(Carbon $theDate, $repeatFreq, $skip)
{
$formatMap = [
'daily' => 'j F Y',
'week' => '\W\e\e\k W, Y',
'weekly' => '\W\e\e\k W, Y',
'quarter' => 'F Y',
'month' => 'F Y',
'monthly' => 'F Y',
'year' => 'Y',
'yearly' => 'Y',
$date = clone $theDate;
$add = ($skip + 1);
$functionMap = [
'daily' => 'addDays',
'weekly' => 'addWeeks',
'week' => 'addWeeks',
'month' => 'addMonths',
'monthly' => 'addMonths',
'quarter' => 'addMonths',
'quarterly' => 'addMonths',
'half-year' => 'addMonths',
'year' => 'addYears',
'yearly' => 'addYears',
];
if (isset($formatMap[$repeatFrequency])) {
return $date->format($formatMap[$repeatFrequency]);
$modifierMap = [
'quarter' => 3,
'quarterly' => 3,
'half-year' => 6,
];
if (!isset($functionMap[$repeatFreq])) {
throw new FireflyException('Cannot do addPeriod for $repeat_freq "' . $repeatFreq . '"');
}
throw new FireflyException('No date formats for frequency "' . $repeatFrequency . '"!');
if (isset($modifierMap[$repeatFreq])) {
$add = $add * $modifierMap[$repeatFreq];
}
$function = $functionMap[$repeatFreq];
$date->$function($add);
return $date;
}
/**
* @param Carbon $theCurrentEnd
* @param $repeatFreq
*
* @return Carbon
* @throws FireflyException
*/
public function endOfPeriod(Carbon $theCurrentEnd, $repeatFreq)
{
$currentEnd = clone $theCurrentEnd;
$functionMap = [
'daily' => 'addDay',
'week' => 'addWeek',
'weekly' => 'addWeek',
'month' => 'addMonth',
'monthly' => 'addMonth',
'quarter' => 'addMonths',
'quarterly' => 'addMonths',
'half-year' => 'addMonths',
'year' => 'addYear',
'yearly' => 'addYear',
];
$modifierMap = [
'quarter' => 3,
'quarterly' => 3,
'half-year' => 6,
];
$subDay = ['week', 'weekly', 'month', 'monthly', 'quarter', 'quarterly', 'half-year', 'year', 'yearly'];
if (!isset($functionMap[$repeatFreq])) {
throw new FireflyException('Cannot do endOfPeriod for $repeat_freq ' . $repeatFreq);
}
$function = $functionMap[$repeatFreq];
if (isset($modifierMap[$repeatFreq])) {
$currentEnd->$function($modifierMap[$repeatFreq]);
} else {
$currentEnd->$function();
}
if (in_array($repeatFreq, $subDay)) {
$currentEnd->subDay();
}
return $currentEnd;
}
/**
* @param $range
@@ -154,6 +216,72 @@ class Navigation
throw new FireflyException('No _periodName() for range "' . $range . '"');
}
/**
* @param Carbon $date
* @param $repeatFrequency
*
* @return string
* @throws FireflyException
*/
public function periodShow(Carbon $date, $repeatFrequency)
{
$formatMap = [
'daily' => 'j F Y',
'week' => '\W\e\e\k W, Y',
'weekly' => '\W\e\e\k W, Y',
'quarter' => 'F Y',
'month' => 'F Y',
'monthly' => 'F Y',
'year' => 'Y',
'yearly' => 'Y',
];
if (isset($formatMap[$repeatFrequency])) {
return $date->format($formatMap[$repeatFrequency]);
}
throw new FireflyException('No date formats for frequency "' . $repeatFrequency . '"!');
}
/**
* @param Carbon $theDate
* @param $repeatFreq
*
* @return Carbon
* @throws FireflyException
*/
public function startOfPeriod(Carbon $theDate, $repeatFreq)
{
$date = clone $theDate;
$functionMap = [
'daily' => 'startOfDay',
'week' => 'startOfWeek',
'weekly' => 'startOfWeek',
'month' => 'startOfMonth',
'monthly' => 'startOfMonth',
'quarter' => 'firstOfQuarter',
'quartly' => 'firstOfQuarter',
'year' => 'startOfYear',
'yearly' => 'startOfYear',
];
if (isset($functionMap[$repeatFreq])) {
$function = $functionMap[$repeatFreq];
$date->$function();
return $date;
}
if ($repeatFreq == 'half-year') {
$month = intval($date->format('m'));
$date->startOfYear();
if ($month >= 7) {
$date->addMonths(6);
}
return $date;
}
throw new FireflyException('Cannot do startOfPeriod for $repeat_freq ' . $repeatFreq);
}
/**
* @param $range
* @param Carbon $start
@@ -224,47 +352,5 @@ class Navigation
throw new FireflyException('updateStartDate cannot handle $range ' . $range);
}
/**
* @param Carbon $theDate
* @param $repeatFreq
* @param $skip
*
* @return \Carbon\Carbon
* @throws FireflyException
*/
public function addPeriod(Carbon $theDate, $repeatFreq, $skip)
{
$date = clone $theDate;
$add = ($skip + 1);
$functionMap = [
'daily' => 'addDays',
'weekly' => 'addWeeks',
'week' => 'addWeeks',
'month' => 'addMonths',
'monthly' => 'addMonths',
'quarter' => 'addMonths',
'quarterly' => 'addMonths',
'half-year' => 'addMonths',
'year' => 'addYears',
'yearly' => 'addYears',
];
$modifierMap = [
'quarter' => 3,
'quarterly' => 3,
'half-year' => 6,
];
if (!isset($functionMap[$repeatFreq])) {
throw new FireflyException('Cannot do addPeriod for $repeat_freq "' . $repeatFreq . '"');
}
if (isset($modifierMap[$repeatFreq])) {
$add = $add * $modifierMap[$repeatFreq];
}
$function = $functionMap[$repeatFreq];
$date->$function($add);
return $date;
}
}

View File

@@ -392,7 +392,7 @@ class TestDataSeeder extends Seeder
$user = User::whereEmail('thegrumpydictator@gmail.com')->first();
// bill
Bill::create(
['user_id' => $user->id, 'name' => 'Rent', 'match' => 'rent,landlord', 'amount_min' => 700, 'amount_max' => 900, 'date' => $this->som,
['user_id' => $user->id, 'name' => 'Rent', 'match' => 'rent,land,lord', 'amount_min' => 700, 'amount_max' => 900, 'date' => $this->som,
'active' => 1, 'automatch' => 1, 'repeat_freq' => 'monthly', 'skip' => 0,]
);

47
public/css/bootstrap-tagsinput.css vendored Normal file
View File

@@ -0,0 +1,47 @@
.bootstrap-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
margin-bottom: 10px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
width:100%;
line-height: 22px;
cursor: text;
}
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0;
margin: 0;
width: auto !important;
max-width: inherit;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .tag {
margin-right: 2px;
color: white;
}
.bootstrap-tagsinput .tag [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.bootstrap-tagsinput .tag [data-role="remove"]:after {
content: "x";
padding: 0px 2px;
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

7
public/js/bills.js Normal file
View File

@@ -0,0 +1,7 @@
$(document).ready(function () {
if (typeof(googleComboChart) === 'function' && typeof(billID) !== 'undefined') {
googleComboChart('chart/bills/' + billID, 'bill-overview');
}
}
);

7
public/js/bootstrap-tagsinput.min.js vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,63 @@
@extends('layouts.default')
@section('content')
{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName()) !!}
{!! Form::open(['class' => 'form-horizontal','id' => 'store','url' => route('bills.store')]) !!}
<div class="row">
<div class="col-lg-6 col-md-12 col-sm-6">
<!-- panel for mandatory fields -->
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-exclamation-circle"></i> Mandatory fields
</div>
<div class="panel-body">
{!! ExpandedForm::text('name') !!}
{!! ExpandedForm::tags('match') !!}
{!! ExpandedForm::amount('amount_min') !!}
{!! ExpandedForm::amount('amount_max') !!}
{!! ExpandedForm::date('date',Carbon\Carbon::now()->addDay()->format('Y-m-d')) !!}
{!! ExpandedForm::select('repeat_freq',$periods,'monthly') !!}
</div>
</div>
<p>
<button type="submit" class="btn btn-lg btn-success">
<i class="fa fa-plus-circle"></i> Store new bill
</button>
</p>
</div>
<div class="col-lg-6 col-md-12 col-sm-6">
<!-- panel for optional fields -->
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-smile-o"></i> Optional fields
</div>
<div class="panel-body">
{!! ExpandedForm::integer('skip',0) !!}
{!! ExpandedForm::checkbox('automatch',1,true) !!}
{!! ExpandedForm::checkbox('active',1,true) !!}
</div>
</div>
<!-- panel for options -->
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-bolt"></i> Options
</div>
<div class="panel-body">
{!! ExpandedForm::optionsList('create','bill') !!}
</div>
</div>
</div>
</div>
{!! Form::close() !!}
@stop
@section('styles')
<link href="css/bootstrap-tagsinput.css" type="text/css" rel="stylesheet" media="all">
@stop
@section('scripts')
<script type="text/javascript" src="js/bootstrap-tagsinput.min.js"></script>
@stop

View File

@@ -0,0 +1,37 @@
@extends('layouts.default')
@section('content')
{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $bill) !!}
{!! Form::open(['class' => 'form-horizontal','id' => 'destroy','url' => route('bills.destroy',$bill->id)]) !!}
<div class="row">
<div class="col-lg-6 col-md-12 col-sm-12">
<div class="panel panel-red">
<div class="panel-heading">
Delete bill "{{{$bill->name}}}"
</div>
<div class="panel-body">
<p>
Are you sure?
</p>
<p>
<button type="submit" class="btn btn-default btn-danger">Delete permanently</button>
<a href="{{URL::previous()}}" class="btn-default btn">Cancel</a >
</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="form-group">
<div class="col-sm-8">
</div>
</div>
</div>
</div>
{!! Form::close() !!}
@stop

View File

@@ -0,0 +1,64 @@
@extends('layouts.default')
@section('content')
{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $bill) !!}
{!! Form::model($bill, ['class' => 'form-horizontal','id' => 'update','url' => route('bills.update', $bill->id)]) !!}
<input type="hidden" name="id" value="{{$bill->id}}" />
<div class="row">
<div class="col-lg-6 col-md-12 col-sm-6">
<!-- panel for mandatory fields -->
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-exclamation-circle"></i> Mandatory fields
</div>
<div class="panel-body">
{!! ExpandedForm::text('name') !!}
{!! ExpandedForm::tags('match') !!}
{!! ExpandedForm::amount('amount_min') !!}
{!! ExpandedForm::amount('amount_max') !!}
{!! ExpandedForm::date('date',$bill->date->format('Y-m-d')) !!}
{!! ExpandedForm::select('repeat_freq',$periods) !!}
</div>
</div>
<p>
<button type="submit" class="btn btn-lg btn-success">
<i class="fa fa-plus-circle"></i> Update bill
</button>
</p>
</div>
<div class="col-lg-6 col-md-12 col-sm-6">
<!-- panel for optional fields -->
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-smile-o"></i> Optional fields
</div>
<div class="panel-body">
{!! ExpandedForm::integer('skip') !!}
{!! ExpandedForm::checkbox('automatch',1) !!}
{!! ExpandedForm::checkbox('active',1) !!}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-bolt"></i> Options
</div>
<div class="panel-body">
{!! ExpandedForm::optionsList('update','bill') !!}
</div>
</div>
</div>
</div>
{!! Form::close() !!}
@stop
@section('styles')
<link href="css/bootstrap-tagsinput.css" type="text/css" rel="stylesheet" media="all">
@stop
@section('scripts')
<script type="text/javascript" src="js/bootstrap-tagsinput.min.js"></script>
@stop

View File

@@ -0,0 +1,32 @@
@extends('layouts.default')
@section('content')
{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName()) !!}
<div class="row">
<div class="col-lg-12 col-sm-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa {{$mainTitleIcon}}"></i> {{{$title}}}
<!-- ACTIONS MENU -->
<div class="pull-right">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
Actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right" role="menu">
<li><a href="{{route('bills.create')}}"><i class="fa fa-plus fa-fw"></i> New bill</a></li>
</ul>
</div>
</div>
</div>
<div class="panel-body">
@include('list.bills')
</div>
</div>
</div>
</div>
@stop
@section('scripts')
@stop

View File

@@ -0,0 +1,116 @@
@extends('layouts.default')
@section('content')
{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $bill) !!}
<div class="row">
<div class="col-lg-6 col-sm-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-rotate-right"></i> {{{$bill->name}}}
@if($bill->active)
<span class="glyphicon glyphicon-ok" title="Active"></span>
@else
<span class="glyphicon glyphicon-remove" title="Inactive"></span>
@endif
@if($bill->automatch)
<span class="glyphicon glyphicon-ok" title="Automatically matched by Firefly"></span>
@else
<span class="glyphicon glyphicon-remove" title="Not automatically matched by Firefly"></span>
@endif
<!-- ACTIONS MENU -->
<div class="pull-right">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
Actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right" role="menu">
<li><a href="{{route('bills.edit',$bill->id)}}"><span class="glyphicon glyphicon-pencil"></span> edit</a></li>
<li><a href="{{route('bills.delete',$bill->id)}}"><span class="glyphicon glyphicon-trash"></span> delete</a></li>
</ul>
</div>
</div>
</div>
<div class="panel-body">
<table class="table">
<tr>
<td colspan="2">
Matching on
@foreach(explode(',',$bill->match) as $word)
<span class="label label-info">{{{$word}}}</span>
@endforeach
between {!! Amount::format($bill->amount_min) !!} and {!! Amount::format($bill->amount_max) !!}.
Repeats {!! $bill->repeat_freq !!}.</td>
</tr>
<tr>
<td>Next expected match</td>
<td>
@if($bill->nextExpectedMatch)
{{$bill->nextExpectedMatch->format('j F Y')}}
@else
<em>Unknown</em>
@endif
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-lg-6 col-sm-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
More
</div>
<div class="panel-body">
<p>
<a href="{{route('bills.rescan',$bill->id)}}" class="btn btn-default">Rescan old transactions</a>
</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-sm-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Chart
</div>
<div class="panel-body">
<div id="bill-overview"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-sm-12 col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
Connected transaction journals
</div>
<div class="panel-body">
@include('list.journals-full')
</div>
</div>
</div>
</div>
@stop
@section('scripts')
<script type="text/javascript">
var billID = {{{$bill->id}}};
var currencyCode = '{{Amount::getCurrencyCode()}}';
</script>
<!-- load the libraries and scripts necessary for Google Charts: -->
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript" src="js/gcharts.options.js"></script>
<script type="text/javascript" src="js/gcharts.js"></script>
<script type="text/javascript" src="js/bills.js"></script>
@stop

View File

@@ -0,0 +1,71 @@
<table class="table table-bordered table-striped">
<tr>
<th>&nbsp;</th>
<th>Name</th>
<th>Matches on</th>
<th>Matching amount</th>
<th>Last seen match</th>
<th>Next expected match</th>
<th>Is active</th>
<th>Will be automatched</th>
<th>Repeats every</th>
</tr>
@foreach($bills as $entry)
<tr>
<td>
<div class="btn-group btn-group-xs">
<a href="{{route('bills.edit',$entry->id)}}" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span></a>
<a href="{{route('bills.delete',$entry->id)}}" class="btn btn-danger btn-xs"><span class="glyphicon glyphicon-trash"></span></a>
</div>
</td>
<td>
<a href="{{route('bills.show',$entry->id)}}" title="{{{$entry->name}}}">{{{$entry->name}}}</a>
</td>
<td>
@foreach(explode(',',$entry->match) as $match)
<span class="label label-info">{{{$match}}}</span>
@endforeach
</td>
<td>
{!! Amount::format($entry->amount_min) !!}
&mdash;
{!! Amount::format($entry->amount_max) !!}
</td>
<td>
@if($entry->lastFoundMatch)
{{$entry->lastFoundMatch->format('j F Y')}}
@else
<em>Unknown</em>
@endif
</td>
<td>
@if($entry->nextExpectedMatch)
{{$entry->nextExpectedMatch->format('j F Y')}}
@else
<em>Unknown</em>
@endif
</td>
<td>
@if($entry->active)
<i class="fa fa-fw fa-check"></i>
@else
<i class="fa fa-fw fa-ban"></i>
@endif
</td>
<td>
@if($entry->automatch)
<i class="fa fa-fw fa-check"></i>
@else
<i class="fa fa-fw fa-ban"></i>
@endif
</td>
<td>
{{{$entry->repeat_freq}}}
@if($entry->skip > 0)
skips over {{$entry->skip}}
@endif
</td>
</tr>
@endforeach
</table>