Merge branch 'release/3.5.4'

This commit is contained in:
James Cole 2015-12-18 08:13:28 +01:00
commit 53b3f7f821
95 changed files with 3777 additions and 1813 deletions

View File

@ -11,6 +11,8 @@ DB_PASSWORD=secret
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_DRIVER=file SESSION_DRIVER=file
DEFAULT_CURRENCY=EUR
EMAIL_SMTP= EMAIL_SMTP=
EMAIL_DRIVER=smtp EMAIL_DRIVER=smtp
EMAIL_USERNAME= EMAIL_USERNAME=
@ -22,3 +24,5 @@ SITE_OWNER=mail@example.com
SENDGRID_USERNAME= SENDGRID_USERNAME=
SENDGRID_PASSWORD= SENDGRID_PASSWORD=
BLOCKED_DOMAINS=

2
.gitignore vendored
View File

@ -5,7 +5,7 @@ Thumbs.db
.idea/ .idea/
tests/_output/* tests/_output/*
_ide_helper.php _ide_helper.php
/build/logs/clover.xml /build/logs
index.html* index.html*
app/storage/firefly-export* app/storage/firefly-export*
.vagrant .vagrant

View File

@ -10,6 +10,9 @@ install:
- composer update - composer update
- php artisan env - php artisan env
- mv -v .env.testing .env - mv -v .env.testing .env
- touch storage/database/testing.db
- php artisan migrate --env=testing
- php artisan migrate --seed --env=testing
script: script:
- phpunit - phpunit

View File

@ -61,19 +61,19 @@ Everything is organised:
_Please note that everything in these screenshots is fictional and may not be realistic._ _Please note that everything in these screenshots is fictional and may not be realistic._
![Index](https://i.nder.be/c6hz06d3) ![Index](https://i.nder.be/hmp5mhw5)
![Accounts](https://i.nder.be/gzxxyz6n) ![Accounts](https://i.nder.be/hf5k02g9)
![Budgets](https://i.nder.be/hhu3krqk) ![Budgets](https://i.nder.be/gzv635mz)
![Reports 1](https://i.nder.be/cc3yspf6) ![Reports 1](https://i.nder.be/g0w698s3)
![Reports 2](https://i.nder.be/h6fp7xkb) ![Reports 2](https://i.nder.be/cr77nyxq)
![Bills](https://i.nder.be/c30zkcpv) ![Bills](https://i.nder.be/c7sugsz5)
![Piggy banks](https://i.nder.be/g20k0mdq) ![Piggy banks](https://i.nder.be/gy2nk0y4)
## Running and installing ## Running and installing
@ -82,7 +82,13 @@ If you're still interested please read [the installation guide](https://github.c
and the **[first use guide](https://github.com/JC5/firefly-iii/wiki/First-use)**. and the **[first use guide](https://github.com/JC5/firefly-iii/wiki/First-use)**.
If you want to try out Firefly III, you can do so on [this dedicated website](https://geld.nder.be/). If you want to try out Firefly III, you can do so on [this dedicated website](https://geld.nder.be/).
This site always runs the latest version of Firefly III. If you want to use it, please read the [privacy considerations](https://github.com/JC5/firefly-iii/wiki/Privacy-on-demo-site) for this demo-site. Accounts on the demo sites will stop working after one week. This site always runs the latest version of Firefly III. If you want to use it, please read the [privacy considerations](https://github.com/JC5/firefly-iii/wiki/Privacy-on-demo-site) for this demo-site. Accounts on the demo sites will stop working after one month. It's a trial.
## Security
You should always run Firefly III on a site with TLS enabled (https://). Please note that although some parts of the
database are encrypted (transaction descriptions, names, etc.) some parts are _not_ (amounts, dates, etc). If you need
more security, you must enable transparent database encryption or a comparable technology.
## Credits ## Credits

View File

@ -18,6 +18,13 @@ interface BudgetChartGenerator
*/ */
public function budget(Collection $entries); public function budget(Collection $entries);
/**
* @param Collection $entries
*
* @return array
*/
public function multiYear(Collection $entries);
/** /**
* @param Collection $entries * @param Collection $entries
* *

View File

@ -141,4 +141,37 @@ class ChartJsBudgetChartGenerator implements BudgetChartGenerator
return $data; return $data;
} }
/**
* @param Collection $entries
*
* @return array
*/
public function multiYear(Collection $entries)
{
// dataset:
$data = [
'count' => 0,
'labels' => [],
'datasets' => [],
];
// get labels from one of the budgets (assuming there's at least one):
$first = $entries->first();
foreach ($first['budgeted'] as $year => $noInterest) {
$data['labels'][] = strval($year);
}
// then, loop all entries and create datasets:
foreach ($entries as $entry) {
$name = $entry['name'];
$spent = $entry['spent'];
$budgeted = $entry['budgeted'];
$data['datasets'][] = ['label' => 'Spent on ' . $name, 'data' => array_values($spent)];
$data['datasets'][] = ['label' => 'Budgeted for ' . $name, 'data' => array_values($budgeted)];
}
$data['count'] = count($data['datasets']);
return $data;
}
} }

View File

@ -19,6 +19,13 @@ interface CategoryChartGenerator
*/ */
public function all(Collection $entries); public function all(Collection $entries);
/**
* @param Collection $entries
*
* @return array
*/
public function multiYear(Collection $entries);
/** /**
* @param Collection $entries * @param Collection $entries
* *

View File

@ -158,4 +158,41 @@ class ChartJsCategoryChartGenerator implements CategoryChartGenerator
return $data; return $data;
} }
/**
* @param Collection $entries
*
* @return array
*/
public function multiYear(Collection $entries)
{
// dataset:
$data = [
'count' => 0,
'labels' => [],
'datasets' => [],
];
// get labels from one of the categories (assuming there's at least one):
$first = $entries->first();
foreach ($first['spent'] as $year => $noInterest) {
$data['labels'][] = strval($year);
}
// then, loop all entries and create datasets:
foreach ($entries as $entry) {
$name = $entry['name'];
$spent = $entry['spent'];
$earned = $entry['earned'];
if (array_sum(array_values($spent)) != 0) {
$data['datasets'][] = ['label' => 'Spent in category ' . $name, 'data' => array_values($spent)];
}
if (array_sum(array_values($earned)) != 0) {
$data['datasets'][] = ['label' => 'Earned in category ' . $name, 'data' => array_values($earned)];
}
}
$data['count'] = count($data['datasets']);
return $data;
}
} }

View File

@ -49,6 +49,39 @@ class ChartJsReportChartGenerator implements ReportChartGenerator
return $data; return $data;
} }
/**
* Same as above but other translations.
*
* @param Collection $entries
*
* @return array
*/
public function multiYearInOut(Collection $entries)
{
$data = [
'count' => 2,
'labels' => [],
'datasets' => [
[
'label' => trans('firefly.income'),
'data' => []
],
[
'label' => trans('firefly.expenses'),
'data' => []
]
],
];
foreach ($entries as $entry) {
$data['labels'][] = $entry[0]->formatLocalized('%Y');
$data['datasets'][0]['data'][] = round($entry[1], 2);
$data['datasets'][1]['data'][] = round($entry[2], 2);
}
return $data;
}
/** /**
* @param string $income * @param string $income
* @param string $expense * @param string $expense
@ -80,4 +113,35 @@ class ChartJsReportChartGenerator implements ReportChartGenerator
return $data; return $data;
} }
/**
* @param string $income
* @param string $expense
* @param int $count
*
* @return array
*/
public function multiYearInOutSummarized($income, $expense, $count)
{
$data = [
'count' => 2,
'labels' => [trans('firefly.sum_of_years'), trans('firefly.average_of_years')],
'datasets' => [
[
'label' => trans('firefly.income'),
'data' => []
],
[
'label' => trans('firefly.expenses'),
'data' => []
]
],
];
$data['datasets'][0]['data'][] = round($income, 2);
$data['datasets'][1]['data'][] = round( $expense, 2);
$data['datasets'][0]['data'][] = round(($income / $count), 2);
$data['datasets'][1]['data'][] = round(( $expense / $count), 2);
return $data;
}
} }

View File

@ -19,6 +19,13 @@ interface ReportChartGenerator
*/ */
public function yearInOut(Collection $entries); public function yearInOut(Collection $entries);
/**
* @param Collection $entries
*
* @return array
*/
public function multiYearInOut(Collection $entries);
/** /**
* @param string $income * @param string $income
* @param string $expense * @param string $expense
@ -28,4 +35,13 @@ interface ReportChartGenerator
*/ */
public function yearInOutSummarized($income, $expense, $count); public function yearInOutSummarized($income, $expense, $count);
/**
* @param string $income
* @param string $expense
* @param int $count
*
* @return array
*/
public function multiYearInOutSummarized($income, $expense, $count);
} }

View File

@ -150,24 +150,4 @@ class BalanceLine
{ {
$this->balanceEntries = $balanceEntries; $this->balanceEntries = $balanceEntries;
} }
/**
* If the BalanceEntries for a BalanceLine have a "left" value, the amount
* of money left in the entire BalanceLine is returned here:
*
* @return float
*/
public function sumOfLeft()
{
$sum = '0';
bcscale(2);
/** @var BalanceEntry $balanceEntry */
foreach ($this->getBalanceEntries() as $balanceEntry) {
$sum = bcadd($sum, $balanceEntry->getLeft());
}
return $sum;
}
} }

View File

@ -37,6 +37,7 @@ class Category
// spent is minus zero for an expense report: // spent is minus zero for an expense report:
if ($category->spent < 0) { if ($category->spent < 0) {
$this->categories->push($category); $this->categories->push($category);
$this->addTotal($category->spent);
} }
} }
@ -55,7 +56,7 @@ class Category
*/ */
public function getCategories() public function getCategories()
{ {
$set = $this->categories->sortByDesc( $set = $this->categories->sortBy(
function (CategoryModel $category) { function (CategoryModel $category) {
return $category->spent; return $category->spent;
} }

View File

@ -33,18 +33,24 @@ class Expense
*/ */
public function addOrCreateExpense(TransactionJournal $entry) public function addOrCreateExpense(TransactionJournal $entry)
{ {
bcscale(2);
$accountId = $entry->account_id; $accountId = $entry->account_id;
$amount = strval(round($entry->amount, 2));
if (bccomp('0', $amount) === -1) {
$amount = bcmul($amount, '-1');
}
if (!$this->expenses->has($accountId)) { if (!$this->expenses->has($accountId)) {
$newObject = new stdClass; $newObject = new stdClass;
$newObject->amount = strval(round($entry->amount_positive, 2)); $newObject->amount = $amount;
$newObject->name = $entry->name; $newObject->name = $entry->name;
$newObject->count = 1; $newObject->count = 1;
$newObject->id = $accountId; $newObject->id = $accountId;
$this->expenses->put($accountId, $newObject); $this->expenses->put($accountId, $newObject);
} else { } else {
bcscale(2);
$existing = $this->expenses->get($accountId); $existing = $this->expenses->get($accountId);
$existing->amount = bcadd($existing->amount, $entry->amount_positive); $existing->amount = bcadd($existing->amount, $amount);
$existing->count++; $existing->count++;
$this->expenses->put($accountId, $existing); $this->expenses->put($accountId, $existing);
} }
@ -55,8 +61,18 @@ class Expense
*/ */
public function addToTotal($add) public function addToTotal($add)
{ {
$add = strval(round($add, 2));
bcscale(2); bcscale(2);
$add = strval(round($add, 2));
if (bccomp('0', $add) === -1) {
$add = bcmul($add, '-1');
}
// if amount is positive, the original transaction
// was a transfer. But since this is an expense report,
// that amount must be negative.
$this->total = bcadd($this->total, $add); $this->total = bcadd($this->total, $add);
} }
@ -65,7 +81,7 @@ class Expense
*/ */
public function getExpenses() public function getExpenses()
{ {
$set = $this->expenses->sortByDesc( $set = $this->expenses->sortBy(
function (stdClass $object) { function (stdClass $object) {
return $object->amount; return $object->amount;
} }

View File

@ -296,7 +296,7 @@ class Importer
// some debug info: // some debug info:
$journalId = $journal->id; $journalId = $journal->id;
$type = $journal->transactionType->type; $type = $journal->getTransactionType();
/** @var Account $asset */ /** @var Account $asset */
$asset = $this->importData['asset-account-object']; $asset = $this->importData['asset-account-object'];
/** @var Account $opposing */ /** @var Account $opposing */
@ -314,13 +314,13 @@ class Importer
*/ */
protected function getTransactionType() protected function getTransactionType()
{ {
$transactionType = TransactionType::where('type', 'Deposit')->first(); $transactionType = TransactionType::where('type', TransactionType::DEPOSIT)->first();
if ($this->importData['amount'] < 0) { if ($this->importData['amount'] < 0) {
$transactionType = TransactionType::where('type', 'Withdrawal')->first(); $transactionType = TransactionType::where('type', TransactionType::WITHDRAWAL)->first();
} }
if (in_array($this->importData['opposing-account-object']->accountType->type, ['Asset account', 'Default account'])) { if (in_array($this->importData['opposing-account-object']->accountType->type, ['Asset account', 'Default account'])) {
$transactionType = TransactionType::where('type', 'Transfer')->first(); $transactionType = TransactionType::where('type', TransactionType::TRANSFER)->first();
} }
return $transactionType; return $transactionType;

View File

@ -24,7 +24,7 @@ class Currency implements PostProcessorInterface
// fix currency // fix currency
if (is_null($this->data['currency'])) { if (is_null($this->data['currency'])) {
$currencyPreference = Preferences::get('currencyPreference', 'EUR'); $currencyPreference = Preferences::get('currencyPreference', env('DEFAULT_CURRENCY', 'EUR'));
$this->data['currency'] = TransactionCurrency::whereCode($currencyPreference->data)->first(); $this->data['currency'] = TransactionCurrency::whereCode($currencyPreference->data)->first();
} }

View File

@ -19,6 +19,8 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\Bill; use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget as BudgetModel; use FireflyIII\Models\Budget as BudgetModel;
use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\LimitRepetition;
use Illuminate\Support\Collection;
use Steam;
/** /**
* Class ReportHelper * Class ReportHelper
@ -43,48 +45,113 @@ class ReportHelper implements ReportHelperInterface
} }
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return CategoryCollection
*/
public function getCategoryReport(Carbon $start, Carbon $end, Collection $accounts)
{
$object = new CategoryCollection;
/**
* GET CATEGORIES:
*/
/** @var \FireflyIII\Repositories\Category\CategoryRepositoryInterface $repository */
$repository = app('FireflyIII\Repositories\Category\CategoryRepositoryInterface');
$set = $repository->getCategories();
foreach ($set as $category) {
$spent = $repository->balanceInPeriodForList($category, $start, $end, $accounts);
$category->spent = $spent;
$object->addCategory($category);
}
return $object;
}
/**
* @param Carbon $date
*
* @return array
*/
public function listOfMonths(Carbon $date)
{
$start = clone $date;
$end = Carbon::now();
$months = [];
while ($start <= $end) {
$year = $start->year;
if (!isset($months[$year])) {
$months[$year] = [
'start' => Carbon::createFromDate($year, 1, 1)->format('Y-m-d'),
'end' => Carbon::createFromDate($year, 12, 31)->format('Y-m-d'),
'months' => [],
];
}
$currentEnd = clone $start;
$currentEnd->endOfMonth();
$months[$year]['months'][] = [
'formatted' => $start->formatLocalized('%B %Y'),
'start' => $start->format('Y-m-d'),
'end' => $currentEnd->format('Y-m-d'),
'month' => $start->month,
'year' => $year,
];
$start->addMonth();
}
return $months;
}
/** /**
* This method generates a full report for the given period on all * This method generates a full report for the given period on all
* the users asset and cash accounts. * given accounts
* *
* @param Carbon $date * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param $shared * @param Collection $accounts
* *
* @return AccountCollection * @return AccountCollection
*/ */
public function getAccountReport(Carbon $date, Carbon $end, $shared) public function getAccountReport(Carbon $start, Carbon $end, Collection $accounts)
{ {
$startAmount = '0';
$endAmount = '0';
$accounts = $this->query->getAllAccounts($date, $end, $shared);
$start = '0';
$end = '0';
$diff = '0'; $diff = '0';
bcscale(2); bcscale(2);
// remove cash account, if any: $accounts->each(
$accounts = $accounts->filter( function (Account $account) use ($start, $end) {
function (Account $account) { /**
if ($account->accountType->type != 'Cash account') { * The balance for today always incorporates transactions
return $account; * made on today. So to get todays "start" balance, we sub one
} * day.
*/
$yesterday = clone $start;
$yesterday->subDay();
return null; /** @noinspection PhpParamsInspection */
$account->startBalance = Steam::balance($account, $yesterday);
$account->endBalance = Steam::balance($account, $end);
} }
); );
// summarize: // summarize:
foreach ($accounts as $account) { foreach ($accounts as $account) {
$start = bcadd($start, $account->startBalance); $startAmount = bcadd($startAmount, $account->startBalance);
$end = bcadd($end, $account->endBalance); $endAmount = bcadd($endAmount, $account->endBalance);
$diff = bcadd($diff, bcsub($account->endBalance, $account->startBalance)); $diff = bcadd($diff, bcsub($account->endBalance, $account->startBalance));
} }
$object = new AccountCollection; $object = new AccountCollection;
$object->setStart($start); $object->setStart($startAmount);
$object->setEnd($end); $object->setEnd($endAmount);
$object->setDifference($diff); $object->setDifference($diff);
$object->setAccounts($accounts); $object->setAccounts($accounts);
@ -92,27 +159,128 @@ class ReportHelper implements ReportHelperInterface
} }
/** /**
* * Get a full report on the users incomes during the period for the given accounts.
* The balance report contains a Balance object which in turn contains:
*
* A BalanceHeader object which contains all relevant user asset accounts for the report.
*
* A number of BalanceLine objects, which hold:
* - A budget
* - A number of BalanceEntry objects.
*
* The BalanceEntry object holds:
* - The same budget (again)
* - A user asset account as mentioned in the BalanceHeader
* - The amount of money spent on the budget by the user asset account
* *
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
*
* @return Income
*/
public function getIncomeReport($start, $end, Collection $accounts)
{
$object = new Income;
$set = $this->query->incomeInPeriod($start, $end, $accounts);
foreach ($set as $entry) {
$object->addToTotal($entry->amount_positive);
$object->addOrCreateIncome($entry);
}
return $object;
}
/**
* Get a full report on the users expenses during the period for a list of accounts.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return Expense
*/
public function getExpenseReport($start, $end, Collection $accounts)
{
$object = new Expense;
$set = $this->query->expenseInPeriod($start, $end, $accounts);
foreach ($set as $entry) {
$object->addToTotal($entry->amount); // can be positive, if it's a transfer
$object->addOrCreateExpense($entry);
}
return $object;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return BudgetCollection
*/
public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts)
{
$object = new BudgetCollection;
/** @var \FireflyIII\Repositories\Budget\BudgetRepositoryInterface $repository */
$repository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface');
$set = $repository->getBudgets();
bcscale(2);
foreach ($set as $budget) {
$repetitions = $repository->getBudgetLimitRepetitions($budget, $start, $end);
// no repetition(s) for this budget:
if ($repetitions->count() == 0) {
$spent = $repository->balanceInPeriodForList($budget, $start, $end, $accounts);
$budgetLine = new BudgetLine;
$budgetLine->setBudget($budget);
$budgetLine->setOverspent($spent);
$object->addOverspent($spent);
$object->addBudgetLine($budgetLine);
continue;
}
// one or more repetitions for budget:
/** @var LimitRepetition $repetition */
foreach ($repetitions as $repetition) {
$budgetLine = new BudgetLine;
$budgetLine->setBudget($budget);
$budgetLine->setRepetition($repetition);
$expenses = $repository->balanceInPeriodForList($budget, $start, $end, $accounts);
// 200 en -100 is 100, vergeleken met 0 === 1
// 200 en -200 is 0, vergeleken met 0 === 0
// 200 en -300 is -100, vergeleken met 0 === -1
$left = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? bcadd($repetition->amount, $expenses) : 0;
$spent = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? $expenses : '0';
$overspent = bccomp(bcadd($repetition->amount, $expenses), '0') === 1 ? '0' : bcadd($expenses, $repetition->amount);
$budgetLine->setLeft($left);
$budgetLine->setSpent($spent);
$budgetLine->setOverspent($overspent);
$budgetLine->setBudgeted($repetition->amount);
$object->addBudgeted($repetition->amount);
$object->addSpent($spent);
$object->addLeft($left);
$object->addOverspent($overspent);
$object->addBudgetLine($budgetLine);
}
}
// stuff outside of budgets:
$noBudget = $repository->getWithoutBudgetSum($start, $end);
$budgetLine = new BudgetLine;
$budgetLine->setOverspent($noBudget);
$budgetLine->setSpent($noBudget);
$object->addOverspent($noBudget);
$object->addBudgetLine($budgetLine);
return $object;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @return Balance * @return Balance
*/ */
public function getBalanceReport(Carbon $start, Carbon $end, $shared) public function getBalanceReport(Carbon $start, Carbon $end, Collection $accounts)
{ {
$repository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); $repository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface');
$tagRepository = app('FireflyIII\Repositories\Tag\TagRepositoryInterface'); $tagRepository = app('FireflyIII\Repositories\Tag\TagRepositoryInterface');
@ -120,8 +288,6 @@ class ReportHelper implements ReportHelperInterface
// build a balance header: // build a balance header:
$header = new BalanceHeader; $header = new BalanceHeader;
$accounts = $this->query->getAllAccounts($start, $end, $shared);
$budgets = $repository->getBudgets(); $budgets = $repository->getBudgets();
foreach ($accounts as $account) { foreach ($accounts as $account) {
$header->addAccount($account); $header->addAccount($account);
@ -134,6 +300,7 @@ class ReportHelper implements ReportHelperInterface
// get budget amount for current period: // get budget amount for current period:
$rep = $repository->getCurrentRepetition($budget, $start, $end); $rep = $repository->getCurrentRepetition($budget, $start, $end);
// could be null?
$line->setRepetition($rep); $line->setRepetition($rep);
// loop accounts: // loop accounts:
@ -142,7 +309,7 @@ class ReportHelper implements ReportHelperInterface
$balanceEntry->setAccount($account); $balanceEntry->setAccount($account);
// get spent: // get spent:
$spent = $this->query->spentInBudgetCorrected($account, $budget, $start, $end); // I think shared is irrelevant. $spent = $this->query->spentInBudget($account, $budget, $start, $end); // I think shared is irrelevant.
$balanceEntry->setSpent($spent); $balanceEntry->setSpent($spent);
$line->addBalanceEntry($balanceEntry); $line->addBalanceEntry($balanceEntry);
@ -153,6 +320,7 @@ class ReportHelper implements ReportHelperInterface
// then a new line for without budget. // then a new line for without budget.
// and one for the tags: // and one for the tags:
// and one for "left unbalanced".
$empty = new BalanceLine; $empty = new BalanceLine;
$tags = new BalanceLine; $tags = new BalanceLine;
$diffLine = new BalanceLine; $diffLine = new BalanceLine;
@ -164,7 +332,7 @@ class ReportHelper implements ReportHelperInterface
$spent = $this->query->spentNoBudget($account, $start, $end); $spent = $this->query->spentNoBudget($account, $start, $end);
$left = $tagRepository->coveredByBalancingActs($account, $start, $end); $left = $tagRepository->coveredByBalancingActs($account, $start, $end);
bcscale(2); bcscale(2);
$diff = bcsub($spent, $left); $diff = bcadd($spent, $left);
// budget // budget
$budgetEntry = new BalanceEntry; $budgetEntry = new BalanceEntry;
@ -199,16 +367,19 @@ class ReportHelper implements ReportHelperInterface
* This method generates a full report for the given period on all * This method generates a full report for the given period on all
* the users bills and their payments. * the users bills and their payments.
* *
* Excludes bills which have not had a payment on the mentioned accounts.
*
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param Collection $accounts
* *
* @return BillCollection * @return BillCollection
*/ */
public function getBillReport(Carbon $start, Carbon $end) public function getBillReport(Carbon $start, Carbon $end, Collection $accounts)
{ {
/** @var \FireflyIII\Repositories\Bill\BillRepositoryInterface $repository */ /** @var \FireflyIII\Repositories\Bill\BillRepositoryInterface $repository */
$repository = app('FireflyIII\Repositories\Bill\BillRepositoryInterface'); $repository = app('FireflyIII\Repositories\Bill\BillRepositoryInterface');
$bills = $repository->getBills(); $bills = $repository->getBillsForAccounts($accounts);
$collection = new BillCollection; $collection = new BillCollection;
/** @var Bill $bill */ /** @var Bill $bill */
@ -238,168 +409,5 @@ class ReportHelper implements ReportHelperInterface
} }
return $collection; return $collection;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param boolean $shared
*
* @return BudgetCollection
*/
public function getBudgetReport(Carbon $start, Carbon $end, $shared)
{
$object = new BudgetCollection;
/** @var \FireflyIII\Repositories\Budget\BudgetRepositoryInterface $repository */
$repository = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface');
$set = $repository->getBudgets();
bcscale(2);
foreach ($set as $budget) {
$repetitions = $repository->getBudgetLimitRepetitions($budget, $start, $end);
// no repetition(s) for this budget:
if ($repetitions->count() == 0) {
$spent = $repository->balanceInPeriod($budget, $start, $end, $shared);
$budgetLine = new BudgetLine;
$budgetLine->setBudget($budget);
$budgetLine->setOverspent($spent);
$object->addOverspent($spent);
$object->addBudgetLine($budgetLine);
continue;
}
// one or more repetitions for budget:
/** @var LimitRepetition $repetition */
foreach ($repetitions as $repetition) {
$budgetLine = new BudgetLine;
$budgetLine->setBudget($budget);
$budgetLine->setRepetition($repetition);
$expenses = $repository->balanceInPeriod($budget, $repetition->startdate, $repetition->enddate, $shared);
$expenses = $expenses * -1;
$left = $expenses < $repetition->amount ? bcsub($repetition->amount, $expenses) : 0;
$spent = $expenses > $repetition->amount ? 0 : $expenses;
$overspent = $expenses > $repetition->amount ? bcsub($expenses, $repetition->amount) : 0;
$budgetLine->setLeft($left);
$budgetLine->setSpent($spent);
$budgetLine->setOverspent($overspent);
$budgetLine->setBudgeted($repetition->amount);
$object->addBudgeted($repetition->amount);
$object->addSpent($spent);
$object->addLeft($left);
$object->addOverspent($overspent);
$object->addBudgetLine($budgetLine);
}
}
// stuff outside of budgets:
$noBudget = $repository->getWithoutBudgetSum($start, $end);
$budgetLine = new BudgetLine;
$budgetLine->setOverspent($noBudget);
$object->addOverspent($noBudget);
$object->addBudgetLine($budgetLine);
return $object;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param boolean $shared
*
* @return CategoryCollection
*/
public function getCategoryReport(Carbon $start, Carbon $end, $shared)
{
$object = new CategoryCollection;
/**
* GET CATEGORIES:
*/
/** @var \FireflyIII\Repositories\Category\CategoryRepositoryInterface $repository */
$repository = app('FireflyIII\Repositories\Category\CategoryRepositoryInterface');
$set = $repository->getCategories();
foreach ($set as $category) {
$spent = $repository->balanceInPeriod($category, $start, $end, $shared);
$category->spent = $spent;
$object->addCategory($category);
$object->addTotal($spent);
}
return $object;
}
/**
* Get a full report on the users expenses during the period.
*
* @param Carbon $start
* @param Carbon $end
* @param boolean $shared
*
* @return Expense
*/
public function getExpenseReport($start, $end, $shared)
{
$object = new Expense;
$set = $this->query->expenseInPeriodCorrected($start, $end, $shared);
foreach ($set as $entry) {
$object->addToTotal($entry->amount_positive);
$object->addOrCreateExpense($entry);
}
return $object;
}
/**
* Get a full report on the users incomes during the period.
*
* @param Carbon $start
* @param Carbon $end
* @param boolean $shared
*
* @return Income
*/
public function getIncomeReport($start, $end, $shared)
{
$object = new Income;
$set = $this->query->incomeInPeriodCorrected($start, $end, $shared);
foreach ($set as $entry) {
$object->addToTotal($entry->amount_positive);
$object->addOrCreateIncome($entry);
}
return $object;
}
/**
* @param Carbon $date
*
* @return array
*/
public function listOfMonths(Carbon $date)
{
$start = clone $date;
$end = Carbon::now();
$months = [];
while ($start <= $end) {
$year = $start->year;
$months[$year][] = [
'formatted' => $start->formatLocalized('%B %Y'),
'month' => $start->month,
'year' => $year,
];
$start->addMonth();
}
return $months;
} }
} }

View File

@ -10,6 +10,7 @@ use FireflyIII\Helpers\Collection\Budget as BudgetCollection;
use FireflyIII\Helpers\Collection\Category as CategoryCollection; use FireflyIII\Helpers\Collection\Category as CategoryCollection;
use FireflyIII\Helpers\Collection\Expense; use FireflyIII\Helpers\Collection\Expense;
use FireflyIII\Helpers\Collection\Income; use FireflyIII\Helpers\Collection\Income;
use Illuminate\Support\Collection;
/** /**
* Interface ReportHelperInterface * Interface ReportHelperInterface
@ -21,75 +22,78 @@ interface ReportHelperInterface
/** /**
* This method generates a full report for the given period on all * This method generates a full report for the given period on all
* the users asset and cash accounts. * given accounts
* *
* @param Carbon $date * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return AccountCollection * @return AccountCollection
*/ */
public function getAccountReport(Carbon $date, Carbon $end, $shared); public function getAccountReport(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* This method generates a full report for the given period on all * This method generates a full report for the given period on all
* the users bills and their payments. * the users bills and their payments.
* *
* Excludes bills which have not had a payment on the mentioned accounts.
*
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param Collection $accounts
* *
* @return BillCollection * @return BillCollection
*/ */
public function getBillReport(Carbon $start, Carbon $end); public function getBillReport(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return Balance * @return Balance
*/ */
public function getBalanceReport(Carbon $start, Carbon $end, $shared); public function getBalanceReport(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return BudgetCollection * @return BudgetCollection
*/ */
public function getBudgetReport(Carbon $start, Carbon $end, $shared); public function getBudgetReport(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return CategoryCollection * @return CategoryCollection
*/ */
public function getCategoryReport(Carbon $start, Carbon $end, $shared); public function getCategoryReport(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* Get a full report on the users expenses during the period. * Get a full report on the users expenses during the period for a list of accounts.
* *
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return Expense * @return Expense
*/ */
public function getExpenseReport($start, $end, $shared); public function getExpenseReport($start, $end, Collection $accounts);
/** /**
* Get a full report on the users incomes during the period. * Get a full report on the users incomes during the period for the given accounts.
* *
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param boolean $shared * @param Collection $accounts
* *
* @return Income * @return Income
*/ */
public function getIncomeReport($start, $end, $shared); public function getIncomeReport($start, $end, Collection $accounts);
/** /**
* @param Carbon $date * @param Carbon $date

View File

@ -8,10 +8,10 @@ use Crypt;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Steam;
/** /**
* Class ReportQuery * Class ReportQuery
@ -20,178 +20,6 @@ use Steam;
*/ */
class ReportQuery implements ReportQueryInterface class ReportQuery implements ReportQueryInterface
{ {
/**
* See ReportQueryInterface::incomeInPeriodCorrected.
*
* This method's length is caused mainly by the query build stuff. Therefor:
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*
* @param Carbon $start
* @param Carbon $end
* @param bool $includeShared
*
* @return Collection
*
*/
public function expenseInPeriodCorrected(Carbon $start, Carbon $end, $includeShared = false)
{
$query = $this->queryJournalsWithTransactions($start, $end);
if ($includeShared === false) {
$query->where(
function (Builder $query) {
$query->where(
function (Builder $q) { // only get withdrawals not from a shared account
$q->where('transaction_types.type', 'Withdrawal');
$q->where('acm_from.data', '!=', '"sharedAsset"');
}
);
$query->orWhere(
function (Builder $q) { // and transfers from a shared account.
$q->where('transaction_types.type', 'Transfer');
$q->where('acm_to.data', '=', '"sharedAsset"');
$q->where('acm_from.data', '!=', '"sharedAsset"');
}
);
}
);
} else {
$query->where('transaction_types.type', 'Withdrawal'); // any withdrawal is fine.
}
$query->orderBy('transaction_journals.date');
$data = $query->get( // get everything
['transaction_journals.*', 'transaction_types.type', 'ac_to.name as name', 'ac_to.id as account_id', 'ac_to.encrypted as account_encrypted']
);
$data->each(
function (TransactionJournal $journal) {
if (intval($journal->account_encrypted) == 1) {
$journal->name = Crypt::decrypt($journal->name);
}
}
);
return $data;
}
/**
* Get a users accounts combined with various meta-data related to the start and end date.
*
* @param Carbon $start
* @param Carbon $end
* @param bool $includeShared
*
* @return Collection
*/
public function getAllAccounts(Carbon $start, Carbon $end, $includeShared = false)
{
$query = Auth::user()->accounts()->orderBy('accounts.name', 'ASC')
->accountTypeIn(['Default account', 'Asset account', 'Cash account']);
if ($includeShared === false) {
$query->leftJoin(
'account_meta', function (JoinClause $join) {
$join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole');
}
)
->where(
function (Builder $query) {
$query->where('account_meta.data', '!=', '"sharedAsset"');
$query->orWhereNull('account_meta.data');
}
);
}
$set = $query->get(['accounts.*']);
$set->each(
function (Account $account) use ($start, $end) {
/**
* The balance for today always incorporates transactions
* made on today. So to get todays "start" balance, we sub one
* day.
*/
$yesterday = clone $start;
$yesterday->subDay();
/** @noinspection PhpParamsInspection */
$account->startBalance = Steam::balance($account, $yesterday);
$account->endBalance = Steam::balance($account, $end);
}
);
return $set;
}
/**
* This method works the same way as ReportQueryInterface::incomeInPeriod does, but instead of returning results
* will simply list the transaction journals only. This should allow any follow up counting to be accurate with
* regards to tags.
*
* This method returns all "income" journals in a certain period, which are both transfers from a shared account
* and "ordinary" deposits. The query used is almost equal to ReportQueryInterface::journalsByRevenueAccount but it does
* not group and returns different fields.
*
* @param Carbon $start
* @param Carbon $end
* @param bool $includeShared
*
* @return Collection
*/
public function incomeInPeriodCorrected(Carbon $start, Carbon $end, $includeShared = false)
{
$query = $this->queryJournalsWithTransactions($start, $end);
if ($includeShared === false) {
// only get deposits not to a shared account
// and transfers to a shared account.
$query->where(
function (Builder $query) {
$query->where(
function (Builder $q) {
$q->where('transaction_types.type', 'Deposit');
$q->where('acm_to.data', '!=', '"sharedAsset"');
}
);
$query->orWhere(
function (Builder $q) {
$q->where('transaction_types.type', 'Transfer');
$q->where('acm_from.data', '=', '"sharedAsset"');
$q->where('acm_to.data','!=','"sharedAsset"');
}
);
}
);
} else {
// any deposit is fine.
$query->where('transaction_types.type', 'Deposit');
}
$query->orderBy('transaction_journals.date');
// get everything
$data = $query->get(
['transaction_journals.*', 'transaction_types.type', 'ac_from.name as name', 'ac_from.id as account_id', 'ac_from.encrypted as account_encrypted']
);
$data->each(
function (TransactionJournal $journal) {
if (intval($journal->account_encrypted) == 1) {
$journal->name = Crypt::decrypt($journal->name);
}
}
);
$data = $data->filter(
function (TransactionJournal $journal) {
if ($journal->amount != 0) {
return $journal;
}
return null;
}
);
return $data;
}
/** /**
* Covers tags * Covers tags
* *
@ -202,22 +30,18 @@ class ReportQuery implements ReportQueryInterface
* *
* @return float * @return float
*/ */
public function spentInBudgetCorrected(Account $account, Budget $budget, Carbon $start, Carbon $end) public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end)
{ {
bcscale(2); return Auth::user()->transactionjournals()
return bcmul(
Auth::user()->transactionjournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->transactionTypes(['Withdrawal']) ->transactionTypes([TransactionType::WITHDRAWAL])
->where('transactions.account_id', $account->id) ->where('transactions.account_id', $account->id)
->before($end) ->before($end)
->after($start) ->after($start)
->where('budget_transaction_journal.budget_id', $budget->id) ->where('budget_transaction_journal.budget_id', $budget->id)
->get(['transaction_journals.*'])->sum('amount'), -1 ->get(['transaction_journals.*'])->sum('amount');
);
} }
/** /**
@ -233,7 +57,7 @@ class ReportQuery implements ReportQueryInterface
Auth::user()->transactionjournals() Auth::user()->transactionjournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->transactionTypes(['Withdrawal']) ->transactionTypes([TransactionType::WITHDRAWAL])
->where('transactions.account_id', $account->id) ->where('transactions.account_id', $account->id)
->before($end) ->before($end)
->after($start) ->after($start)
@ -276,4 +100,137 @@ class ReportQuery implements ReportQueryInterface
return $query; return $query;
} }
/**
* This method works the same way as ReportQueryInterface::incomeInPeriod does, but instead of returning results
* will simply list the transaction journals only. This should allow any follow up counting to be accurate with
* regards to tags. It will only get the incomes to the specified accounts.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return Collection
*/
public function incomeInPeriod(Carbon $start, Carbon $end, Collection $accounts)
{
$query = $this->queryJournalsWithTransactions($start, $end);
$ids = [];
/** @var Account $account */
foreach ($accounts as $account) {
$ids[] = $account->id;
}
// OR is a deposit
// OR any transfer TO the accounts in $accounts, not FROM any of the accounts in $accounts.
$query->where(
function (Builder $query) use ($ids) {
$query->where(
function (Builder $q) {
$q->where('transaction_types.type', TransactionType::DEPOSIT);
}
);
$query->orWhere(
function (Builder $q) use ($ids) {
$q->where('transaction_types.type', TransactionType::TRANSFER);
$q->whereNotIn('ac_from.id',$ids);
$q->whereIn('ac_to.id', $ids);
}
);
}
);
// only include selected accounts.
$query->whereIn('ac_to.id', $ids);
$query->orderBy('transaction_journals.date');
// get everything
$data = $query->get(
['transaction_journals.*', 'transaction_types.type', 'ac_from.name as name', 'ac_from.id as account_id', 'ac_from.encrypted as account_encrypted']
);
$data->each(
function (TransactionJournal $journal) {
if (intval($journal->account_encrypted) == 1) {
$journal->name = Crypt::decrypt($journal->name);
}
}
);
$data = $data->filter(
function (TransactionJournal $journal) {
if ($journal->amount != 0) {
return $journal;
}
return null;
}
);
return $data;
}
/**
* See ReportQueryInterface::incomeInPeriod
*
* This method returns all "expense" journals in a certain period, which are both transfers to a shared account
* and "ordinary" withdrawals. The query used is almost equal to ReportQueryInterface::journalsByRevenueAccount but it does
* not group and returns different fields.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return Collection
*
*/
public function expenseInPeriod(Carbon $start, Carbon $end, Collection $accounts)
{
$ids = [];
/** @var Account $account */
foreach ($accounts as $account) {
$ids[] = $account->id;
}
$query = $this->queryJournalsWithTransactions($start, $end);
// withdrawals from any account are an expense.
// transfers away, from an account in the list, to an account not in the list, are an expense.
$query->where(
function (Builder $query) use ($ids) {
$query->where(
function (Builder $q) {
$q->where('transaction_types.type', TransactionType::WITHDRAWAL);
}
);
$query->orWhere(
function (Builder $q) use ($ids) {
$q->where('transaction_types.type', TransactionType::TRANSFER);
$q->whereIn('ac_from.id', $ids);
$q->whereNotIn('ac_to.id', $ids);
}
);
}
);
// expense goes from the selected accounts:
$query->whereIn('ac_from.id', $ids);
$query->orderBy('transaction_journals.date');
$data = $query->get( // get everything
['transaction_journals.*', 'transaction_types.type', 'ac_to.name as name', 'ac_to.id as account_id', 'ac_to.encrypted as account_encrypted']
);
$data->each(
function (TransactionJournal $journal) {
if (intval($journal->account_encrypted) == 1) {
$journal->name = Crypt::decrypt($journal->name);
}
}
);
return $data;
}
} }

View File

@ -16,7 +16,7 @@ interface ReportQueryInterface
{ {
/** /**
* See ReportQueryInterface::incomeInPeriodCorrected * See ReportQueryInterface::incomeInPeriod
* *
* This method returns all "expense" journals in a certain period, which are both transfers to a shared account * This method returns all "expense" journals in a certain period, which are both transfers to a shared account
* and "ordinary" withdrawals. The query used is almost equal to ReportQueryInterface::journalsByRevenueAccount but it does * and "ordinary" withdrawals. The query used is almost equal to ReportQueryInterface::journalsByRevenueAccount but it does
@ -24,36 +24,25 @@ interface ReportQueryInterface
* *
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param bool $includeShared * @param Collection $accounts
* *
* @return Collection * @return Collection
* *
*/ */
public function expenseInPeriodCorrected(Carbon $start, Carbon $end, $includeShared = false); public function expenseInPeriod(Carbon $start, Carbon $end, Collection $accounts);
/**
* Get a users accounts combined with various meta-data related to the start and end date.
*
* @param Carbon $start
* @param Carbon $end
* @param bool $includeShared
*
* @return Collection
*/
public function getAllAccounts(Carbon $start, Carbon $end, $includeShared = false);
/** /**
* This method works the same way as ReportQueryInterface::incomeInPeriod does, but instead of returning results * This method works the same way as ReportQueryInterface::incomeInPeriod does, but instead of returning results
* will simply list the transaction journals only. This should allow any follow up counting to be accurate with * will simply list the transaction journals only. This should allow any follow up counting to be accurate with
* regards to tags. * regards to tags. It will only get the incomes to the specified accounts.
* *
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* @param bool $includeShared * @param Collection $accounts
* *
* @return Collection * @return Collection
*/ */
public function incomeInPeriodCorrected(Carbon $start, Carbon $end, $includeShared = false); public function incomeInPeriod(Carbon $start, Carbon $end, Collection $accounts);
/** /**
* Covers tags as well. * Covers tags as well.
@ -65,7 +54,7 @@ interface ReportQueryInterface
* *
* @return float * @return float
*/ */
public function spentInBudgetCorrected(Account $account, Budget $budget, Carbon $start, Carbon $end); public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end);
/** /**
* @param Account $account * @param Account $account

View File

@ -1,6 +1,7 @@
<?php namespace FireflyIII\Http\Controllers\Auth; <?php namespace FireflyIII\Http\Controllers\Auth;
use Auth; use Auth;
use Config;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Role; use FireflyIII\Models\Role;
use FireflyIII\User; use FireflyIII\User;
@ -8,12 +9,12 @@ use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Foundation\Auth\ThrottlesLogins; use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Log;
use Mail; use Mail;
use Request as Rq; use Request as Rq;
use Session; use Session;
use Twig; use Twig;
use Validator; use Validator;
use Log;
/** /**
* Class AuthController * Class AuthController
@ -32,9 +33,8 @@ class AuthController extends Controller
public function getLogout() public function getLogout()
{ {
Auth::logout(); Auth::logout();
Log::debug('Logout and redirect to root.');
return redirect('/login'); return redirect('/auth/login');
} }
/** /**
@ -89,7 +89,7 @@ class AuthController extends Controller
if (!is_null($foundUser)) { if (!is_null($foundUser)) {
// if it exists, show message: // if it exists, show message:
$code = $foundUser->blocked_code; $code = $foundUser->blocked_code;
if(strlen($code) == 0) { if (strlen($code) == 0) {
$code = 'general_blocked'; $code = 'general_blocked';
} }
$message = trans('firefly.' . $code . '_error', ['email' => $credentials['email']]); $message = trans('firefly.' . $code . '_error', ['email' => $credentials['email']]);
@ -160,21 +160,35 @@ class AuthController extends Controller
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
$data = $request->all(); $data = $request->all();
$data['password'] = bcrypt($data['password']); $data['password'] = bcrypt($data['password']);
// is user email domain blocked?
if ($this->isBlockedDomain($data['email'])) {
$validator->getMessageBag()->add('email', trans('validation.invalid_domain'));
$this->throwValidationException(
$request, $validator
);
}
Auth::login($this->create($data)); Auth::login($this->create($data));
// get the email address // get the email address
if (Auth::user() instanceof User) { if (Auth::user() instanceof User) {
$email = Auth::user()->email; $email = Auth::user()->email;
$address = route('index'); $address = route('index');
$ipAddress = $request->ip();
// send email. // send email.
try {
Mail::send( Mail::send(
['emails.registered-html', 'emails.registered'], ['address' => $address], function (Message $message) use ($email) { ['emails.registered-html', 'emails.registered'], ['address' => $address, 'ip' => $ipAddress], function (Message $message) use ($email) {
$message->to($email, $email)->subject('Welcome to Firefly III! '); $message->to($email, $email)->subject('Welcome to Firefly III! ');
} }
); );
} catch (\Swift_TransportException $e) {
Log::error($e->getMessage());
}
// set flash message // set flash message
Session::flash('success', 'You have registered successfully!'); Session::flash('success', 'You have registered successfully!');
@ -197,6 +211,32 @@ class AuthController extends Controller
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
/**
* @return array
*/
protected function getBlockedDomains() {
$set = Config::get('mail.blocked_domains');
$domains = [];
foreach($set as $entry) {
$domain = trim($entry);
if(strlen($domain) > 0) {
$domains[] = $domain;
}
}
return $domains;
}
protected function isBlockedDomain($email)
{
$parts = explode('@', $email);
$blocked = $this->getBlockedDomains();
if (isset($parts[1]) && in_array($parts[1], $blocked)) {
return true;
}
return false;
}
/** /**
* Get a validator for an incoming registration request. * Get a validator for an incoming registration request.
* *

View File

@ -2,7 +2,12 @@
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use FireflyIII\User;
use Illuminate\Http\Request;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Class PasswordController * Class PasswordController
* *
@ -41,4 +46,35 @@ class PasswordController extends Controller
$this->middleware('guest'); $this->middleware('guest');
} }
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$user = User::whereEmail($request->get('email'))->first();
if (!is_null($user) && intval($user->blocked) === 1) {
$response = 'passwords.blocked';
} else {
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
}
switch ($response) {
case Password::RESET_LINK_SENT:
return redirect()->back()->with('status', trans($response));
case Password::INVALID_USER:
case 'passwords.blocked':
return redirect()->back()->withErrors(['email' => trans($response)]);
}
}
} }

View File

@ -6,6 +6,7 @@ use Carbon\Carbon;
use FireflyIII\Http\Requests\BudgetFormRequest; use FireflyIII\Http\Requests\BudgetFormRequest;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\LimitRepetition;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use Input; use Input;
use Navigation; use Navigation;
@ -135,7 +136,7 @@ class BudgetController extends Controller
* *
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function index(BudgetRepositoryInterface $repository) public function index(BudgetRepositoryInterface $repository, AccountRepositoryInterface $accountRepository)
{ {
$budgets = $repository->getActiveBudgets(); $budgets = $repository->getActiveBudgets();
$inactive = $repository->getInactiveBudgets(); $inactive = $repository->getInactiveBudgets();
@ -147,6 +148,8 @@ class BudgetController extends Controller
$key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd');
$budgetIncomeTotal = Preferences::get($key, 1000)->data; $budgetIncomeTotal = Preferences::get($key, 1000)->data;
$period = Navigation::periodShow($start, $range); $period = Navigation::periodShow($start, $range);
$accounts = $accountRepository->getAccounts(['Default account', 'Asset account', 'Cash account']);
bcscale(2); bcscale(2);
/** /**
* Do some cleanup: * Do some cleanup:
@ -156,7 +159,7 @@ class BudgetController extends Controller
// loop the budgets: // loop the budgets:
/** @var Budget $budget */ /** @var Budget $budget */
foreach ($budgets as $budget) { foreach ($budgets as $budget) {
$budget->spent = $repository->balanceInPeriod($budget, $start, $end); $budget->spent = $repository->balanceInPeriodForList($budget, $start, $end, $accounts);
$budget->currentRep = $repository->getCurrentRepetition($budget, $start, $end); $budget->currentRep = $repository->getCurrentRepetition($budget, $start, $end);
if ($budget->currentRep) { if ($budget->currentRep) {
$budgeted = bcadd($budgeted, $budget->currentRep->amount); $budgeted = bcadd($budgeted, $budget->currentRep->amount);
@ -170,7 +173,7 @@ class BudgetController extends Controller
$defaultCurrency = Amount::getDefaultCurrency(); $defaultCurrency = Amount::getDefaultCurrency();
return view( return view(
'budgets.index', compact('budgetMaximum','period', 'range', 'budgetIncomeTotal', 'defaultCurrency', 'inactive', 'budgets', 'spent', 'budgeted') 'budgets.index', compact('budgetMaximum', 'period', 'range', 'budgetIncomeTotal', 'defaultCurrency', 'inactive', 'budgets', 'spent', 'budgeted')
); );
} }
@ -279,7 +282,7 @@ class BudgetController extends Controller
{ {
$budgetData = [ $budgetData = [
'name' => $request->input('name'), 'name' => $request->input('name'),
'active' => intval($request->input('active')) == 1 'active' => intval($request->input('active')) == 1,
]; ];
$repository->update($budget, $budgetData); $repository->update($budget, $budgetData);

View File

@ -79,6 +79,38 @@ class AccountController extends Controller
return Response::json($data); return Response::json($data);
} }
/**
* Shows the balances for a given set of dates and accounts.
*
* TODO fix parameters.
*
* @param AccountRepositoryInterface $repository
*
* @param $url
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function report($report_type, Carbon $start, Carbon $end, Collection $accounts)
{
// chart properties for cache:
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('all');
$cache->addProperty('accounts');
$cache->addProperty('default');
$cache->addProperty($accounts);
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
// make chart:
$data = $this->generator->all($accounts, $start, $end);
$cache->store($data);
return Response::json($data);
}
/** /**
* Shows the balances for all the user's expense accounts. * Shows the balances for all the user's expense accounts.
* *

View File

@ -6,6 +6,7 @@ use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\LimitRepetition;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -35,13 +36,86 @@ class BudgetController extends Controller
$this->generator = app('FireflyIII\Generator\Chart\Budget\BudgetChartGenerator'); $this->generator = app('FireflyIII\Generator\Chart\Budget\BudgetChartGenerator');
} }
/**
* @param BudgetRepositoryInterface $repository
* @param $report_type
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* @param Collection $budgets
*/
public function multiYear(BudgetRepositoryInterface $repository, $report_type, Carbon $start, Carbon $end, Collection $accounts, Collection $budgets)
{
// chart properties for cache:
$cache = new CacheProperties();
$cache->addProperty($report_type);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($accounts);
$cache->addProperty($budgets);
$cache->addProperty('multiYearBudget');
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
/**
* budget
* year:
* spent: x
* budgeted: x
* year
* spent: x
* budgeted: x
*/
$entries = new Collection;
// go by budget, not by year.
foreach ($budgets as $budget) {
$entry = ['name' => '', 'spent' => [], 'budgeted' => []];
$currentStart = clone $start;
while ($currentStart < $end) {
// fix the date:
$currentEnd = clone $currentStart;
$currentEnd->endOfYear();
// get data:
if (is_null($budget->id)) {
$name = trans('firefly.noBudget');
$sum = $repository->getWithoutBudgetSum($currentStart, $currentEnd);
$budgeted = 0;
} else {
$name = $budget->name;
$sum = $repository->balanceInPeriodForList($budget, $currentStart, $currentEnd, $accounts);
$budgeted = $repository->getBudgetLimitRepetitions($budget, $currentStart, $currentEnd)->sum('amount');
}
// save to array:
$year = $currentStart->year;
$entry['name'] = $name;
$entry['spent'][$year] = ($sum * -1);
$entry['budgeted'][$year] = $budgeted;
// jump to next year.
$currentStart = clone $currentEnd;
$currentStart->addDay();
}
$entries->push($entry);
}
// generate chart with data:
$data = $this->generator->multiYear($entries);
return Response::json($data);
}
/** /**
* @param BudgetRepositoryInterface $repository * @param BudgetRepositoryInterface $repository
* @param Budget $budget * @param Budget $budget
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function budget(BudgetRepositoryInterface $repository, Budget $budget) public function budget(BudgetRepositoryInterface $repository, AccountRepositoryInterface $accountRepository, Budget $budget)
{ {
// dates and times // dates and times
@ -51,6 +125,8 @@ class BudgetController extends Controller
$final = clone $last; $final = clone $last;
$final->addYears(2); $final->addYears(2);
$last = Navigation::endOfX($last, $range, $final); $last = Navigation::endOfX($last, $range, $final);
$accounts = $accountRepository->getAccounts(['Default account', 'Asset account', 'Cash account']);
// chart properties for cache: // chart properties for cache:
$cache = new CacheProperties(); $cache = new CacheProperties();
@ -68,7 +144,7 @@ class BudgetController extends Controller
$end->subDay(); $end->subDay();
$chartDate = clone $end; $chartDate = clone $end;
$chartDate->startOfMonth(); $chartDate->startOfMonth();
$spent = $repository->balanceInPeriod($budget, $first, $end) * -1; $spent = $repository->balanceInPeriodForList($budget, $first, $end, $accounts) * -1;
$entries->push([$chartDate, $spent]); $entries->push([$chartDate, $spent]);
$first = Navigation::addPeriod($first, $range, 0); $first = Navigation::addPeriod($first, $range, 0);
} }
@ -113,7 +189,7 @@ class BudgetController extends Controller
/* /*
* Sum of expenses on this day: * Sum of expenses on this day:
*/ */
$sum = $repository->expensesOnDayCorrected($budget, $start); $sum = $repository->expensesOnDay($budget, $start);
$amount = bcadd($amount, $sum); $amount = bcadd($amount, $sum);
$entries->push([clone $start, $amount]); $entries->push([clone $start, $amount]);
$start->addDay(); $start->addDay();
@ -133,12 +209,13 @@ class BudgetController extends Controller
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function frontpage(BudgetRepositoryInterface $repository) public function frontpage(BudgetRepositoryInterface $repository, AccountRepositoryInterface $accountRepository)
{ {
$budgets = $repository->getBudgets(); $budgets = $repository->getBudgets();
$start = Session::get('start', Carbon::now()->startOfMonth()); $start = Session::get('start', Carbon::now()->startOfMonth());
$end = Session::get('end', Carbon::now()->endOfMonth()); $end = Session::get('end', Carbon::now()->endOfMonth());
$allEntries = new Collection; $allEntries = new Collection;
$accounts = $accountRepository->getAccounts(['Default account', 'Asset account', 'Cash account']);
// chart properties for cache: // chart properties for cache:
$cache = new CacheProperties(); $cache = new CacheProperties();
@ -156,13 +233,13 @@ class BudgetController extends Controller
foreach ($budgets as $budget) { foreach ($budgets as $budget) {
$repetitions = $repository->getBudgetLimitRepetitions($budget, $start, $end); $repetitions = $repository->getBudgetLimitRepetitions($budget, $start, $end);
if ($repetitions->count() == 0) { if ($repetitions->count() == 0) {
$expenses = $repository->balanceInPeriod($budget, $start, $end, true) * -1; $expenses = $repository->balanceInPeriodForList($budget, $start, $end, $accounts) * -1;
$allEntries->push([$budget->name, 0, 0, $expenses, 0, 0]); $allEntries->push([$budget->name, 0, 0, $expenses, 0, 0]);
continue; continue;
} }
/** @var LimitRepetition $repetition */ /** @var LimitRepetition $repetition */
foreach ($repetitions as $repetition) { foreach ($repetitions as $repetition) {
$expenses = $repository->balanceInPeriod($budget, $repetition->startdate, $repetition->enddate, true) * -1; $expenses = $repository->balanceInPeriodForList($budget, $repetition->startdate, $repetition->enddate, $accounts) * -1;
// $left can be less than zero. // $left can be less than zero.
// $overspent can be more than zero ( = overspending) // $overspent can be more than zero ( = overspending)
@ -197,11 +274,8 @@ class BudgetController extends Controller
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function year(BudgetRepositoryInterface $repository, $year, $shared = false) public function year(BudgetRepositoryInterface $repository, $report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
$start = new Carbon($year . '-01-01');
$end = new Carbon($year . '-12-31');
$shared = $shared == 'shared' ? true : false;
$allBudgets = $repository->getBudgets(); $allBudgets = $repository->getBudgets();
$budgets = new Collection; $budgets = new Collection;
@ -218,7 +292,7 @@ class BudgetController extends Controller
// filter empty budgets: // filter empty budgets:
foreach ($allBudgets as $budget) { foreach ($allBudgets as $budget) {
$spent = $repository->balanceInPeriod($budget, $start, $end, $shared); $spent = $repository->balanceInPeriodForList($budget, $start, $end, $accounts);
if ($spent != 0) { if ($spent != 0) {
$budgets->push($budget); $budgets->push($budget);
} }
@ -234,7 +308,7 @@ class BudgetController extends Controller
// each budget, fill the row: // each budget, fill the row:
foreach ($budgets as $budget) { foreach ($budgets as $budget) {
$spent = $repository->balanceInPeriod($budget, $start, $month, $shared); $spent = $repository->balanceInPeriodForList($budget, $start, $month, $accounts);
$row[] = $spent * -1; $row[] = $spent * -1;
} }
$entries->push($row); $entries->push($row);

View File

@ -107,7 +107,7 @@ class CategoryController extends Controller
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$array = $repository->getCategoriesAndExpensesCorrected($start, $end); $array = $repository->getCategoriesAndExpenses($start, $end);
// sort by callback: // sort by callback:
uasort( uasort(
$array, $array,
@ -121,6 +121,84 @@ class CategoryController extends Controller
); );
$set = new Collection($array); $set = new Collection($array);
$data = $this->generator->frontpage($set); $data = $this->generator->frontpage($set);
$cache->store($data);
return Response::json($data);
}
/**
* @param CategoryRepositoryInterface $repository
* @param $report_type
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* @param Collection $categories
*/
public function multiYear(CategoryRepositoryInterface $repository, $report_type, Carbon $start, Carbon $end, Collection $accounts, Collection $categories)
{
// chart properties for cache:
$cache = new CacheProperties();
$cache->addProperty($report_type);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($accounts);
$cache->addProperty($categories);
$cache->addProperty('multiYearCategory');
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
/**
* category
* year:
* spent: x
* earned: x
* year
* spent: x
* earned: x
*/
$entries = new Collection;
// go by budget, not by year.
/** @var Category $category */
foreach ($categories as $category) {
$entry = ['name' => '', 'spent' => [], 'earned' => []];
$currentStart = clone $start;
while ($currentStart < $end) {
// fix the date:
$currentEnd = clone $currentStart;
$currentEnd->endOfYear();
// get data:
if (is_null($category->id)) {
$name = trans('firefly.noCategory');
$spent = $repository->spentNoCategoryForAccounts($accounts, $currentStart, $currentEnd);
$earned = $repository->earnedNoCategoryForAccounts($accounts, $currentStart, $currentEnd);
} else {
$name = $category->name;
$spent = $repository->spentInPeriodForAccounts($category, $accounts, $currentStart, $currentEnd);
$earned = $repository->earnedInPeriodForAccounts($category, $accounts, $currentStart, $currentEnd);
}
// save to array:
$year = $currentStart->year;
$entry['name'] = $name;
$entry['spent'][$year] = ($spent * -1);
$entry['earned'][$year] = $earned;
// jump to next year.
$currentStart = clone $currentEnd;
$currentStart->addDay();
}
$entries->push($entry);
}
// generate chart with data:
$data = $this->generator->multiYear($entries);
$cache->store($data);
return Response::json($data); return Response::json($data);
@ -151,8 +229,8 @@ class CategoryController extends Controller
while ($start <= $end) { while ($start <= $end) {
$spent = $repository->spentOnDaySumCorrected($category, $start); $spent = $repository->spentOnDaySum($category, $start);
$earned = $repository->earnedOnDaySumCorrected($category, $start); $earned = $repository->earnedOnDaySum($category, $start);
$date = Navigation::periodShow($start, '1D'); $date = Navigation::periodShow($start, '1D');
$entries->push([clone $start, $date, $spent, $earned]); $entries->push([clone $start, $date, $spent, $earned]);
$start->addDay(); $start->addDay();
@ -194,8 +272,8 @@ class CategoryController extends Controller
while ($start <= $end) { while ($start <= $end) {
$spent = $repository->spentOnDaySumCorrected($category, $start); $spent = $repository->spentOnDaySum($category, $start);
$earned = $repository->earnedOnDaySumCorrected($category, $start); $earned = $repository->earnedOnDaySum($category, $start);
$theDate = Navigation::periodShow($start, '1D'); $theDate = Navigation::periodShow($start, '1D');
$entries->push([clone $start, $theDate, $spent, $earned]); $entries->push([clone $start, $theDate, $spent, $earned]);
$start->addDay(); $start->addDay();
@ -213,15 +291,15 @@ class CategoryController extends Controller
* This chart will only show expenses. * This chart will only show expenses.
* *
* @param CategoryRepositoryInterface $repository * @param CategoryRepositoryInterface $repository
* @param $year * @param $report_type
* @param bool $shared * @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Illuminate\Http\JsonResponse
*/ */
public function spentInYear(CategoryRepositoryInterface $repository, $year, $shared = false) public function spentInYear(CategoryRepositoryInterface $repository, $report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
$start = new Carbon($year . '-01-01');
$end = new Carbon($year . '-12-31');
$cache = new CacheProperties; // chart properties for cache: $cache = new CacheProperties; // chart properties for cache:
$cache->addProperty($start); $cache->addProperty($start);
@ -232,12 +310,11 @@ class CategoryController extends Controller
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$shared = $shared == 'shared' ? true : false;
$allCategories = $repository->getCategories(); $allCategories = $repository->getCategories();
$entries = new Collection; $entries = new Collection;
$categories = $allCategories->filter( $categories = $allCategories->filter(
function (Category $category) use ($repository, $start, $end, $shared) { function (Category $category) use ($repository, $start, $end, $accounts) {
$spent = $repository->balanceInPeriod($category, $start, $end, $shared); $spent = $repository->balanceInPeriodForList($category, $start, $end, $accounts);
if ($spent < 0) { if ($spent < 0) {
return $category; return $category;
} }
@ -252,7 +329,7 @@ class CategoryController extends Controller
$row = [clone $start]; // make a row: $row = [clone $start]; // make a row:
foreach ($categories as $category) { // each budget, fill the row foreach ($categories as $category) { // each budget, fill the row
$spent = $repository->balanceInPeriod($category, $start, $month, $shared); $spent = $repository->balanceInPeriodForList($category, $start, $month, $accounts);
if ($spent < 0) { if ($spent < 0) {
$row[] = $spent * -1; $row[] = $spent * -1;
} else { } else {
@ -272,16 +349,15 @@ class CategoryController extends Controller
* This chart will only show income. * This chart will only show income.
* *
* @param CategoryRepositoryInterface $repository * @param CategoryRepositoryInterface $repository
* @param $year * @param $report_type
* @param bool $shared * @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Illuminate\Http\JsonResponse
*/ */
public function earnedInYear(CategoryRepositoryInterface $repository, $year, $shared = false) public function earnedInYear(CategoryRepositoryInterface $repository, $report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
$start = new Carbon($year . '-01-01');
$end = new Carbon($year . '-12-31');
$cache = new CacheProperties; // chart properties for cache: $cache = new CacheProperties; // chart properties for cache:
$cache->addProperty($start); $cache->addProperty($start);
$cache->addProperty($end); $cache->addProperty($end);
@ -291,12 +367,11 @@ class CategoryController extends Controller
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$shared = $shared == 'shared' ? true : false;
$allCategories = $repository->getCategories(); $allCategories = $repository->getCategories();
$allEntries = new Collection; $allEntries = new Collection;
$categories = $allCategories->filter( $categories = $allCategories->filter(
function (Category $category) use ($repository, $start, $end, $shared) { function (Category $category) use ($repository, $start, $end, $accounts) {
$spent = $repository->balanceInPeriod($category, $start, $end, $shared); $spent = $repository->balanceInPeriodForList($category, $start, $end, $accounts);
if ($spent > 0) { if ($spent > 0) {
return $category; return $category;
} }
@ -311,7 +386,7 @@ class CategoryController extends Controller
$row = [clone $start]; // make a row: $row = [clone $start]; // make a row:
foreach ($categories as $category) { // each budget, fill the row foreach ($categories as $category) { // each budget, fill the row
$spent = $repository->balanceInPeriod($category, $start, $month, $shared); $spent = $repository->balanceInPeriodForList($category, $start, $month, $accounts);
if ($spent > 0) { if ($spent > 0) {
$row[] = $spent; $row[] = $spent;
} else { } else {

View File

@ -9,7 +9,6 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Response; use Response;
use Log;
/** /**
* Class ReportController * Class ReportController
@ -37,34 +36,57 @@ class ReportController extends Controller
* Summarizes all income and expenses, per month, for a given year. * Summarizes all income and expenses, per month, for a given year.
* *
* @param ReportQueryInterface $query * @param ReportQueryInterface $query
* @param $year * @param $report_type
* @param bool $shared * @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Illuminate\Http\JsonResponse
*/ */
public function yearInOut(ReportQueryInterface $query, $year, $shared = false) public function yearInOut(ReportQueryInterface $query, $report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
// get start and end of year
$start = new Carbon($year . '-01-01');
$end = new Carbon($year . '-12-31');
$shared = $shared == 'shared' ? true : false;
// chart properties for cache: // chart properties for cache:
$cache = new CacheProperties; $cache = new CacheProperties;
$cache->addProperty('yearInOut'); $cache->addProperty('yearInOut');
$cache->addProperty($year); $cache->addProperty($start);
$cache->addProperty($shared); $cache->addProperty($accounts);
$cache->addProperty($end);
if ($cache->has()) { if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
// per year?
if ($start->diffInMonths($end) > 12) {
$entries = new Collection;
while ($start < $end) {
$startOfYear = clone $start;
$startOfYear->startOfYear();
$endOfYear = clone $startOfYear;
$endOfYear->endOfYear();
// total income and total expenses:
$incomeSum = $query->incomeInPeriod($startOfYear, $endOfYear, $accounts)->sum('amount_positive');
$expenseSum = $query->expenseInPeriod($startOfYear, $endOfYear, $accounts)->sum('amount_positive');
$entries->push([clone $start, $incomeSum, $expenseSum]);
$start->addYear();
}
$data = $this->generator->multiYearInOut($entries);
$cache->store($data);
} else {
// per month:
$entries = new Collection; $entries = new Collection;
while ($start < $end) { while ($start < $end) {
$month = clone $start; $month = clone $start;
$month->endOfMonth(); $month->endOfMonth();
// total income and total expenses: // total income and total expenses:
$incomeSum = $query->incomeInPeriodCorrected($start, $month, $shared)->sum('amount_positive'); $incomeSum = $query->incomeInPeriod($start, $month, $accounts)->sum('amount_positive');
$expenseSum = $query->expenseInPeriodCorrected($start, $month, $shared)->sum('amount_positive'); $expenseSum = $query->expenseInPeriod($start, $month, $accounts)->sum('amount_positive');
$entries->push([clone $start, $incomeSum, $expenseSum]); $entries->push([clone $start, $incomeSum, $expenseSum]);
$start->addMonth(); $start->addMonth();
@ -72,6 +94,8 @@ class ReportController extends Controller
$data = $this->generator->yearInOut($entries); $data = $this->generator->yearInOut($entries);
$cache->store($data); $cache->store($data);
}
return Response::json($data); return Response::json($data);
@ -81,54 +105,71 @@ class ReportController extends Controller
* Summarizes all income and expenses for a given year. Gives a total and an average. * Summarizes all income and expenses for a given year. Gives a total and an average.
* *
* @param ReportQueryInterface $query * @param ReportQueryInterface $query
* @param $year * @param $report_type
* @param bool $shared * @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Illuminate\Http\JsonResponse
*/ */
public function yearInOutSummarized(ReportQueryInterface $query, $year, $shared = false) public function yearInOutSummarized(ReportQueryInterface $query, $report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
// chart properties for cache: // chart properties for cache:
$cache = new CacheProperties; $cache = new CacheProperties;
$cache->addProperty('yearInOutSummarized'); $cache->addProperty('yearInOutSummarized');
$cache->addProperty($year); $cache->addProperty($start);
$cache->addProperty($shared); $cache->addProperty($end);
$cache->addProperty($accounts);
if ($cache->has()) { if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$start = new Carbon($year . '-01-01');
$end = new Carbon($year . '-12-31');
$shared = $shared == 'shared' ? true : false;
$income = '0'; $income = '0';
$expense = '0'; $expense = '0';
$count = 0; $count = 0;
bcscale(2); bcscale(2);
if ($start->diffInMonths($end) > 12) {
// per year
while ($start < $end) {
$startOfYear = clone $start;
$startOfYear->startOfYear();
$endOfYear = clone $startOfYear;
$endOfYear->endOfYear();
// total income and total expenses:
$currentIncome = $query->incomeInPeriod($startOfYear, $endOfYear, $accounts)->sum('amount_positive');
$currentExpense = $query->expenseInPeriod($startOfYear, $endOfYear, $accounts)->sum('amount_positive');
$income = bcadd($income, $currentIncome);
$expense = bcadd($expense, $currentExpense);
$count++;
$start->addYear();
}
$data = $this->generator->multiYearInOutSummarized($income, $expense, $count);
$cache->store($data);
} else {
// per month!
while ($start < $end) { while ($start < $end) {
$month = clone $start; $month = clone $start;
$month->endOfMonth(); $month->endOfMonth();
// total income and total expenses: // total income and total expenses:
$currentIncome = $query->incomeInPeriodCorrected($start, $month, $shared)->sum('amount_positive'); $currentIncome = $query->incomeInPeriod($start, $month, $accounts)->sum('amount_positive');
$currentExpense = $query->expenseInPeriodCorrected($start, $month, $shared)->sum('amount_positive'); $currentExpense = $query->expenseInPeriod($start, $month, $accounts)->sum('amount_positive');
Log::debug('Date ['.$month->format('M Y').']: income = ['.$income.' + '.$currentIncome.'], out = ['.$expense.' + '.$currentExpense.']');
$income = bcadd($income, $currentIncome); $income = bcadd($income, $currentIncome);
$expense = bcadd($expense, $currentExpense); $expense = bcadd($expense, $currentExpense);
$count++; $count++;
$start->addMonth(); $start->addMonth();
} }
$data = $this->generator->yearInOutSummarized($income, $expense, $count); $data = $this->generator->yearInOutSummarized($income, $expense, $count);
$cache->store($data); $cache->store($data);
}
return Response::json($data); return Response::json($data);

View File

@ -147,7 +147,7 @@ class CurrencyController extends Controller
public function index(CurrencyRepositoryInterface $repository) public function index(CurrencyRepositoryInterface $repository)
{ {
$currencies = $repository->get(); $currencies = $repository->get();
$defaultCurrency = $repository->getCurrencyByPreference(Preferences::get('currencyPreference', 'EUR')); $defaultCurrency = $repository->getCurrencyByPreference(Preferences::get('currencyPreference', env('DEFAULT_CURRENCY','EUR')));
if (!Auth::user()->hasRole('owner')) { if (!Auth::user()->hasRole('owner')) {

View File

@ -178,7 +178,7 @@ class JsonController extends Controller
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function boxIn(ReportQueryInterface $reportQuery) public function boxIn(ReportQueryInterface $reportQuery, AccountRepositoryInterface $accountRepository)
{ {
$start = Session::get('start', Carbon::now()->startOfMonth()); $start = Session::get('start', Carbon::now()->startOfMonth());
$end = Session::get('end', Carbon::now()->endOfMonth()); $end = Session::get('end', Carbon::now()->endOfMonth());
@ -191,8 +191,8 @@ class JsonController extends Controller
if ($cache->has()) { if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$accounts = $accountRepository->getAccounts(['Default account', 'Asset account', 'Cash account']);
$amount = $reportQuery->incomeInPeriodCorrected($start, $end, true)->sum('amount'); $amount = $reportQuery->incomeInPeriod($start, $end, $accounts)->sum('amount');
$data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; $data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
$cache->store($data); $cache->store($data);
@ -205,11 +205,12 @@ class JsonController extends Controller
* *
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function boxOut(ReportQueryInterface $reportQuery) public function boxOut(ReportQueryInterface $reportQuery, AccountRepositoryInterface $accountRepository)
{ {
$start = Session::get('start', Carbon::now()->startOfMonth()); $start = Session::get('start', Carbon::now()->startOfMonth());
$end = Session::get('end', Carbon::now()->endOfMonth()); $end = Session::get('end', Carbon::now()->endOfMonth());
$accounts = $accountRepository->getAccounts(['Default account', 'Asset account', 'Cash account']);
// works for json too! // works for json too!
$cache = new CacheProperties; $cache = new CacheProperties;
@ -220,7 +221,7 @@ class JsonController extends Controller
return Response::json($cache->get()); // @codeCoverageIgnore return Response::json($cache->get()); // @codeCoverageIgnore
} }
$amount = $reportQuery->expenseInPeriodCorrected($start, $end, true)->sum('amount'); $amount = $reportQuery->expenseInPeriod($start, $end, $accounts)->sum('amount');
$amount = $amount * -1; $amount = $amount * -1;
$data = ['box' => 'out', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; $data = ['box' => 'out', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];

View File

@ -4,6 +4,7 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Report\ReportHelperInterface; use FireflyIII\Helpers\Report\ReportHelperInterface;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use Illuminate\Support\Collection;
use Session; use Session;
use View; use View;
@ -46,106 +47,175 @@ class ReportController extends Controller
// does the user have shared accounts? // does the user have shared accounts?
$accounts = $repository->getAccounts(['Default account', 'Asset account']); $accounts = $repository->getAccounts(['Default account', 'Asset account']);
$hasShared = false; // get id's for quick links:
$accountIds = [];
/** @var Account $account */ /** @var Account $account */
foreach ($accounts as $account) { foreach($accounts as $account) {
if ($account->getMeta('accountRole') == 'sharedAsset') { $accountIds [] = $account->id;
$hasShared = true;
}
} }
$accountList = join(',',$accountIds);
return view('reports.index', compact('months', 'hasShared')); return view('reports.index', compact('months', 'accounts', 'start','accountList'));
} }
/** /**
* @param string $year * @param $report_type
* @param string $month * @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* *
* @param bool $shared * @return View
*
* @return \Illuminate\View\View
*/ */
public function month($year = '2014', $month = '1', $shared = false) public function defaultYear($report_type, Carbon $start, Carbon $end, Collection $accounts)
{ {
$start = new Carbon($year . '-' . $month . '-01');
$subTitle = trans('firefly.reportForMonth', ['month' => $start->formatLocalized($this->monthFormat)]);
$subTitleIcon = 'fa-calendar';
$end = clone $start;
$incomeTopLength = 8;
$expenseTopLength = 8;
if ($shared == 'shared') {
$shared = true;
$subTitle = trans('firefly.reportForMonthShared', ['month' => $start->formatLocalized($this->monthFormat)]);
}
$end->endOfMonth();
$accounts = $this->helper->getAccountReport($start, $end, $shared);
$incomes = $this->helper->getIncomeReport($start, $end, $shared);
$expenses = $this->helper->getExpenseReport($start, $end, $shared);
$budgets = $this->helper->getBudgetReport($start, $end, $shared);
$categories = $this->helper->getCategoryReport($start, $end, $shared);
$balance = $this->helper->getBalanceReport($start, $end, $shared);
$bills = $this->helper->getBillReport($start, $end);
Session::flash('gaEventCategory', 'report');
Session::flash('gaEventAction', 'month');
Session::flash('gaEventLabel', $start->format('F Y'));
return view(
'reports.month',
compact(
'start', 'shared',
'subTitle', 'subTitleIcon',
'accounts',
'incomes', 'incomeTopLength',
'expenses', 'expenseTopLength',
'budgets', 'balance',
'categories',
'bills'
)
);
}
/**
* @param $year
*
* @param bool $shared
*
* @return $this
*/
public function year($year, $shared = false)
{
$start = new Carbon('01-01-' . $year);
$end = clone $start;
$subTitle = trans('firefly.reportForYear', ['year' => $year]);
$subTitleIcon = 'fa-bar-chart';
$incomeTopLength = 8; $incomeTopLength = 8;
$expenseTopLength = 8; $expenseTopLength = 8;
if ($shared == 'shared') { $accountReport = $this->helper->getAccountReport($start, $end, $accounts);
$shared = true; $incomes = $this->helper->getIncomeReport($start, $end, $accounts);
$subTitle = trans('firefly.reportForYearShared', ['year' => $year]); $expenses = $this->helper->getExpenseReport($start, $end, $accounts);
}
$end->endOfYear();
$accounts = $this->helper->getAccountReport($start, $end, $shared);
$incomes = $this->helper->getIncomeReport($start, $end, $shared);
$expenses = $this->helper->getExpenseReport($start, $end, $shared);
Session::flash('gaEventCategory', 'report'); Session::flash('gaEventCategory', 'report');
Session::flash('gaEventAction', 'year'); Session::flash('gaEventAction', 'year');
Session::flash('gaEventLabel', $start->format('Y')); Session::flash('gaEventLabel', $start->format('Y'));
// and some id's, joined:
$accountIds = [];
/** @var Account $account */
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
$accountIds = join(',', $accountIds);
return view( return view(
'reports.year', 'reports.default.year',
compact('start', 'shared', 'accounts', 'incomes', 'expenses', 'subTitle', 'subTitleIcon', 'incomeTopLength', 'expenseTopLength') compact(
'start', 'accountReport', 'incomes', 'report_type', 'accountIds', 'end',
'expenses', 'incomeTopLength', 'expenseTopLength'
)
); );
} }
/**
* @param $report_type
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return View
*/
public function defaultMonth($report_type, Carbon $start, Carbon $end, Collection $accounts)
{
$incomeTopLength = 8;
$expenseTopLength = 8;
// get report stuff!
$accountReport = $this->helper->getAccountReport($start, $end, $accounts);
$incomes = $this->helper->getIncomeReport($start, $end, $accounts);
$expenses = $this->helper->getExpenseReport($start, $end, $accounts);
$budgets = $this->helper->getBudgetReport($start, $end, $accounts);
$categories = $this->helper->getCategoryReport($start, $end, $accounts);
$balance = $this->helper->getBalanceReport($start, $end, $accounts);
$bills = $this->helper->getBillReport($start, $end, $accounts);
// and some id's, joined:
$accountIds = [];
/** @var Account $account */
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
$accountIds = join(',', $accountIds);
// continue!
return view(
'reports.default.month',
compact(
'start', 'end', 'report_type',
'accountReport',
'incomes', 'incomeTopLength',
'expenses', 'expenseTopLength',
'budgets', 'balance',
'categories',
'bills',
'accountIds', 'report_type'
)
);
}
public function defaultMultiYear($report_type, $start, $end, $accounts)
{
// list of users stuff:
$budgets = app('FireflyIII\Repositories\Budget\BudgetRepositoryInterface')->getActiveBudgets();
$categories = app('FireflyIII\Repositories\Category\CategoryRepositoryInterface')->getCategories();
// and some id's, joined:
$accountIds = [];
/** @var Account $account */
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
$accountIds = join(',', $accountIds);
return view(
'reports.default.multi-year', compact('budgets', 'accounts', 'categories', 'start', 'end', 'accountIds', 'report_type')
);
}
/**
* @param $report_type
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return View
*/
public function report($report_type, Carbon $start, Carbon $end, Collection $accounts)
{
// throw an error if necessary.
if ($end < $start) {
return view('error')->with('message', 'End date cannot be before start date, silly!');
}
// lower threshold
if ($start < Session::get('first')) {
$start = Session::get('first');
}
switch ($report_type) {
default:
case 'default':
View::share(
'subTitle', trans(
'firefly.report_default',
[
'start' => $start->formatLocalized($this->monthFormat),
'end' => $end->formatLocalized($this->monthFormat)
]
)
);
View::share('subTitleIcon', 'fa-calendar');
// more than one year date difference means year report.
if ($start->diffInMonths($end) > 12) {
// return view('error')->with('message', 'No report yet for this time period.');
return $this->defaultMultiYear($report_type, $start, $end, $accounts);
}
// more than two months date difference means year report.
if ($start->diffInMonths($end) > 1) {
return $this->defaultYear($report_type, $start, $end, $accounts);
}
return $this->defaultMonth($report_type, $start, $end, $accounts);
break;
}
}
} }

View File

@ -12,6 +12,7 @@ use FireflyIII\Http\Requests\JournalFormRequest;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use Input; use Input;
@ -45,8 +46,9 @@ class TransactionController extends Controller
* *
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function create(AccountRepositoryInterface $repository, $what = 'deposit') public function create(AccountRepositoryInterface $repository, $what = TransactionType::DEPOSIT)
{ {
$what = strtolower($what);
$maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize')); $maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize'));
$maxPostSize = Steam::phpBytes(ini_get('post_max_size')); $maxPostSize = Steam::phpBytes(ini_get('post_max_size'));
$uploadSize = min($maxFileSize, $maxPostSize); $uploadSize = min($maxFileSize, $maxPostSize);
@ -95,7 +97,7 @@ class TransactionController extends Controller
*/ */
public function delete(TransactionJournal $journal) public function delete(TransactionJournal $journal)
{ {
$what = strtolower($journal->transactionType->type); $what = strtolower($journal->getTransactionType());
$subTitle = trans('firefly.delete_' . $what, ['description' => $journal->description]); $subTitle = trans('firefly.delete_' . $what, ['description' => $journal->description]);
// put previous url in session // put previous url in session
@ -137,7 +139,7 @@ class TransactionController extends Controller
public function edit(AccountRepositoryInterface $repository, TransactionJournal $journal) public function edit(AccountRepositoryInterface $repository, TransactionJournal $journal)
{ {
// cannot edit opening balance // cannot edit opening balance
if ($journal->transactionType->type == 'Opening balance') { if ($journal->isOpeningBalance()) {
return view('error')->with('message', 'Cannot edit this transaction. Edit the account instead!'); return view('error')->with('message', 'Cannot edit this transaction. Edit the account instead!');
} }
@ -145,7 +147,7 @@ class TransactionController extends Controller
$maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize')); $maxFileSize = Steam::phpBytes(ini_get('upload_max_filesize'));
$maxPostSize = Steam::phpBytes(ini_get('post_max_size')); $maxPostSize = Steam::phpBytes(ini_get('post_max_size'));
$uploadSize = min($maxFileSize, $maxPostSize); $uploadSize = min($maxFileSize, $maxPostSize);
$what = strtolower($journal->transactionType->type); $what = strtolower($journal->getTransactionType());
$accounts = ExpandedForm::makeSelectList($repository->getAccounts(['Default account', 'Asset account'])); $accounts = ExpandedForm::makeSelectList($repository->getAccounts(['Default account', 'Asset account']));
$budgets = ExpandedForm::makeSelectList(Auth::user()->budgets()->get()); $budgets = ExpandedForm::makeSelectList(Auth::user()->budgets()->get());
$budgets[0] = trans('form.noBudget'); $budgets[0] = trans('form.noBudget');
@ -181,7 +183,7 @@ class TransactionController extends Controller
$preFilled['amount'] = $journal->amount_positive; $preFilled['amount'] = $journal->amount_positive;
if ($journal->transactionType->type == 'Withdrawal') { if ($journal->isWithdrawal()) {
$preFilled['account_id'] = $journal->source_account->id; $preFilled['account_id'] = $journal->source_account->id;
$preFilled['expense_account'] = $journal->destination_account->name_for_editform; $preFilled['expense_account'] = $journal->destination_account->name_for_editform;
} else { } else {
@ -269,8 +271,8 @@ class TransactionController extends Controller
$t->after = bcadd($t->before, $t->amount); $t->after = bcadd($t->before, $t->amount);
} }
); );
$what = strtolower($journal->transactionType->type); $what = strtolower($journal->getTransactionType());
$subTitle = trans('firefly.' . $journal->transactionType->type) . ' "' . e($journal->description) . '"'; $subTitle = trans('firefly.' . $journal->getTransactionType()) . ' "' . e($journal->description) . '"';
return view('transactions.show', compact('journal', 'subTitle', 'what')); return view('transactions.show', compact('journal', 'subTitle', 'what'));
} }
@ -287,7 +289,7 @@ class TransactionController extends Controller
$journalData = $request->getJournalData(); $journalData = $request->getJournalData();
// if not withdrawal, unset budgetid. // if not withdrawal, unset budgetid.
if ($journalData['what'] != 'withdrawal') { if ($journalData['what'] != strtolower(TransactionType::WITHDRAWAL)) {
$journalData['budget_id'] = 0; $journalData['budget_id'] = 0;
} }
@ -308,7 +310,7 @@ class TransactionController extends Controller
// rescan journal, UpdateJournalConnection // rescan journal, UpdateJournalConnection
event(new JournalSaved($journal)); event(new JournalSaved($journal));
if ($journal->transactionType->type == 'Transfer' && intval($request->get('piggy_bank_id')) > 0) { if ($journal->isTransfer() && intval($request->get('piggy_bank_id')) > 0) {
event(new JournalCreated($journal, intval($request->get('piggy_bank_id')))); event(new JournalCreated($journal, intval($request->get('piggy_bank_id'))));
} }
@ -340,7 +342,7 @@ class TransactionController extends Controller
{ {
// cannot edit opening balance // cannot edit opening balance
if ($journal->transactionType->type == 'Opening balance') { if ($journal->isOpeningBalance()) {
return view('error')->with('message', 'Cannot edit this transaction. Edit the account instead!'); return view('error')->with('message', 'Cannot edit this transaction. Edit the account instead!');
} }

View File

@ -64,6 +64,7 @@ class Authenticate
Carbon::setLocale($pref->data); Carbon::setLocale($pref->data);
setlocale(LC_TIME, Config::get('firefly.locales.' . $pref->data)); setlocale(LC_TIME, Config::get('firefly.locales.' . $pref->data));
setlocale(LC_MONETARY, Config::get('firefly.locales.' . $pref->data));
return $next($request); return $next($request);
} }

View File

@ -5,6 +5,7 @@ namespace FireflyIII\Http\Requests;
use Auth; use Auth;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use FireflyIII\Models\TransactionType;
use Input; use Input;
/** /**
@ -65,7 +66,7 @@ class JournalFormRequest extends Request
]; ];
switch ($what) { switch ($what) {
case 'withdrawal': case strtolower(TransactionType::WITHDRAWAL):
$rules['account_id'] = 'required|exists:accounts,id|belongsToUser:accounts'; $rules['account_id'] = 'required|exists:accounts,id|belongsToUser:accounts';
$rules['expense_account'] = 'between:1,255'; $rules['expense_account'] = 'between:1,255';
$rules['category'] = 'between:1,255'; $rules['category'] = 'between:1,255';
@ -73,12 +74,12 @@ class JournalFormRequest extends Request
$rules['budget_id'] = 'exists:budgets,id|belongsToUser:budgets'; $rules['budget_id'] = 'exists:budgets,id|belongsToUser:budgets';
} }
break; break;
case 'deposit': case strtolower(TransactionType::DEPOSIT):
$rules['category'] = 'between:1,255'; $rules['category'] = 'between:1,255';
$rules['account_id'] = 'required|exists:accounts,id|belongsToUser:accounts'; $rules['account_id'] = 'required|exists:accounts,id|belongsToUser:accounts';
$rules['revenue_account'] = 'between:1,255'; $rules['revenue_account'] = 'between:1,255';
break; break;
case 'transfer': case strtolower(TransactionType::TRANSFER):
$rules['account_from_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:account_to_id'; $rules['account_from_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:account_to_id';
$rules['account_to_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:account_from_id'; $rules['account_to_id'] = 'required|exists:accounts,id|belongsToUser:accounts|different:account_from_id';
$rules['category'] = 'between:1,255'; $rules['category'] = 'between:1,255';

View File

@ -351,30 +351,14 @@ Breadcrumbs::register(
); );
Breadcrumbs::register( Breadcrumbs::register(
'reports.year', function (Generator $breadcrumbs, Carbon $date, $shared) { 'reports.report', function (Generator $breadcrumbs, Carbon $start, Carbon $end, $reportType, $accountIds) {
$breadcrumbs->parent('reports.index'); $breadcrumbs->parent('reports.index');
if ($shared) {
$title = trans('breadcrumbs.yearly_report_shared', ['date' => $date->year]);
} else {
$title = trans('breadcrumbs.yearly_report', ['date' => $date->year]);
}
$breadcrumbs->push($title, route('reports.year', [$date->year]));
}
);
Breadcrumbs::register( $pref = Preferences::get('language', 'en')->data;
'reports.month', function (Generator $breadcrumbs, Carbon $date, $shared) { $monthFormat = Config::get('firefly.monthAndDay.' . $pref);
$breadcrumbs->parent('reports.year', $date, $shared); $title = trans('firefly.report_default', ['start' => $start->formatLocalized($monthFormat), 'end' => $end->formatLocalized($monthFormat)]);
$language = Preferences::get('language', 'en')->data;
$format = Config::get('firefly.month.' . $language);
if ($shared) { $breadcrumbs->push($title, route('reports.report', ['url' => 'abcde']));
$title = trans('breadcrumbs.monthly_report_shared', ['date' => $date->formatLocalized($format)]);
} else {
$title = trans('breadcrumbs.monthly_report', ['date' => $date->formatLocalized($format)]);
}
$breadcrumbs->push($title, route('reports.month', [$date->year, $date->month]));
} }
); );
@ -416,7 +400,7 @@ Breadcrumbs::register(
Breadcrumbs::register( Breadcrumbs::register(
'transactions.show', function (Generator $breadcrumbs, TransactionJournal $journal) { 'transactions.show', function (Generator $breadcrumbs, TransactionJournal $journal) {
$breadcrumbs->parent('transactions.index', strtolower($journal->transactionType->type)); $breadcrumbs->parent('transactions.index', strtolower($journal->getTransactionType()));
$breadcrumbs->push($journal->description, route('transactions.show', [$journal->id])); $breadcrumbs->push($journal->description, route('transactions.show', [$journal->id]));
} }

View File

@ -1,4 +1,5 @@
<?php <?php
use Carbon\Carbon;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Attachment; use FireflyIII\Models\Attachment;
use FireflyIII\Models\Bill; use FireflyIII\Models\Bill;
@ -29,6 +30,111 @@ Route::bind(
throw new NotFoundHttpException; throw new NotFoundHttpException;
} }
); );
// account list! Yay!
Route::bind(
'accountList',
function ($value) {
if (Auth::check()) {
$ids = explode(',', $value);
/** @var \Illuminate\Support\Collection $object */
$object = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->where('account_types.editable', 1)
->whereIn('accounts.id', $ids)
->where('user_id', Auth::user()->id)
->get(['accounts.*']);
if ($object->count() > 0) {
return $object;
}
}
throw new NotFoundHttpException;
}
);
// budget list
Route::bind(
'budgetList',
function ($value) {
if (Auth::check()) {
$ids = explode(',', $value);
/** @var \Illuminate\Support\Collection $object */
$object = Budget::where('active', 1)
->whereIn('id', $ids)
->where('user_id', Auth::user()->id)
->get();
// add empty budget if applicable.
if (in_array('0', $ids)) {
$object->push(new Budget);
}
if ($object->count() > 0) {
return $object;
}
}
throw new NotFoundHttpException;
}
);
// category list
Route::bind(
'categoryList',
function ($value) {
if (Auth::check()) {
$ids = explode(',', $value);
/** @var \Illuminate\Support\Collection $object */
$object = Category::whereIn('id', $ids)
->where('user_id', Auth::user()->id)
->get();
// add empty budget if applicable.
if (in_array('0', $ids)) {
$object->push(new Category);
}
if ($object->count() > 0) {
return $object;
}
}
throw new NotFoundHttpException;
}
);
// Date
Route::bind(
'start_date',
function ($value) {
if (Auth::check()) {
try {
$date = new Carbon($value);
} catch (Exception $e) {
Log::error('Could not parse date "' . $value . '" for user #' . Auth::user()->id);
throw new NotFoundHttpException;
}
return $date;
}
throw new NotFoundHttpException;
}
);
// Date
Route::bind(
'end_date',
function ($value) {
if (Auth::check()) {
try {
$date = new Carbon($value);
} catch (Exception $e) {
Log::error('Could not parse date "' . $value . '" for user #' . Auth::user()->id);
throw new NotFoundHttpException;
}
return $date;
}
throw new NotFoundHttpException;
}
);
Route::bind( Route::bind(
'tj', function ($value) { 'tj', function ($value) {
@ -287,6 +393,7 @@ Route::group(
Route::get('/chart/account/month/{year}/{month}/{shared?}', ['uses' => 'Chart\AccountController@all'])->where( Route::get('/chart/account/month/{year}/{month}/{shared?}', ['uses' => 'Chart\AccountController@all'])->where(
['year' => '[0-9]{4}', 'month' => '[0-9]{1,2}', 'shared' => 'shared'] ['year' => '[0-9]{4}', 'month' => '[0-9]{1,2}', 'shared' => 'shared']
); );
Route::get('/chart/account/report/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\AccountController@report']);
Route::get('/chart/account/{account}', ['uses' => 'Chart\AccountController@single']); Route::get('/chart/account/{account}', ['uses' => 'Chart\AccountController@single']);
@ -296,18 +403,24 @@ Route::group(
// budgets: // budgets:
Route::get('/chart/budget/frontpage', ['uses' => 'Chart\BudgetController@frontpage']); Route::get('/chart/budget/frontpage', ['uses' => 'Chart\BudgetController@frontpage']);
Route::get('/chart/budget/year/{year}/{shared?}', ['uses' => 'Chart\BudgetController@year'])->where(['year' => '[0-9]{4}', 'shared' => 'shared']);
// this chart is used in reports:
Route::get('/chart/budget/year/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\BudgetController@year']);
Route::get('/chart/budget/multi-year/{report_type}/{start_date}/{end_date}/{accountList}/{budgetList}', ['uses' => 'Chart\BudgetController@multiYear']);
Route::get('/chart/budget/{budget}/{limitrepetition}', ['uses' => 'Chart\BudgetController@budgetLimit']); Route::get('/chart/budget/{budget}/{limitrepetition}', ['uses' => 'Chart\BudgetController@budgetLimit']);
Route::get('/chart/budget/{budget}', ['uses' => 'Chart\BudgetController@budget']); Route::get('/chart/budget/{budget}', ['uses' => 'Chart\BudgetController@budget']);
// categories: // categories:
Route::get('/chart/category/frontpage', ['uses' => 'Chart\CategoryController@frontpage']); Route::get('/chart/category/frontpage', ['uses' => 'Chart\CategoryController@frontpage']);
Route::get('/chart/category/spent-in-year/{year}/{shared?}', ['uses' => 'Chart\CategoryController@spentInYear'])->where(
['year' => '[0-9]{4}', 'shared' => 'shared'] // these three charts are for reports:
); Route::get('/chart/category/spent-in-year/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\CategoryController@spentInYear']);
Route::get('/chart/category/earned-in-year/{year}/{shared?}', ['uses' => 'Chart\CategoryController@earnedInYear'])->where( Route::get('/chart/category/earned-in-year/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\CategoryController@earnedInYear']);
['year' => '[0-9]{4}', 'shared' => 'shared'] Route::get(
'/chart/category/multi-year/{report_type}/{start_date}/{end_date}/{accountList}/{categoryList}', ['uses' => 'Chart\CategoryController@multiYear']
); );
Route::get('/chart/category/{category}/period', ['uses' => 'Chart\CategoryController@currentPeriod']); Route::get('/chart/category/{category}/period', ['uses' => 'Chart\CategoryController@currentPeriod']);
Route::get('/chart/category/{category}/period/{date}', ['uses' => 'Chart\CategoryController@specificPeriod']); Route::get('/chart/category/{category}/period/{date}', ['uses' => 'Chart\CategoryController@specificPeriod']);
Route::get('/chart/category/{category}/all', ['uses' => 'Chart\CategoryController@all']); Route::get('/chart/category/{category}/all', ['uses' => 'Chart\CategoryController@all']);
@ -316,8 +429,10 @@ Route::group(
Route::get('/chart/piggyBank/{piggyBank}', ['uses' => 'Chart\PiggyBankController@history']); Route::get('/chart/piggyBank/{piggyBank}', ['uses' => 'Chart\PiggyBankController@history']);
// reports: // reports:
Route::get('/chart/report/in-out/{year}/{shared?}', ['uses' => 'Chart\ReportController@yearInOut'])->where(['year' => '[0-9]{4}', 'shared' => 'shared']); Route::get('/chart/report/in-out/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\ReportController@yearInOut'])->where(
Route::get('/chart/report/in-out-sum/{year}/{shared?}', ['uses' => 'Chart\ReportController@yearInOutSummarized'])->where( ['year' => '[0-9]{4}', 'shared' => 'shared']
);
Route::get('/chart/report/in-out-sum/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'Chart\ReportController@yearInOutSummarized'])->where(
['year' => '[0-9]{4}', 'shared' => 'shared'] ['year' => '[0-9]{4}', 'shared' => 'shared']
); );
@ -385,10 +500,10 @@ Route::group(
* Report Controller * Report Controller
*/ */
Route::get('/reports', ['uses' => 'ReportController@index', 'as' => 'reports.index']); Route::get('/reports', ['uses' => 'ReportController@index', 'as' => 'reports.index']);
Route::get('/reports/{year}/{shared?}', ['uses' => 'ReportController@year', 'as' => 'reports.year'])->where(['year' => '[0-9]{4}', 'shared' => 'shared']); // Route::post('/reports/select', ['uses' => 'ReportController@select', 'as' => 'reports.select']);
Route::get('/reports/{year}/{month}/{shared?}', ['uses' => 'ReportController@month', 'as' => 'reports.month'])->where( Route::get('/reports/report/{report_type}/{start_date}/{end_date}/{accountList}', ['uses' => 'ReportController@report', 'as' => 'reports.report']);
['year' => '[0-9]{4}', 'month' => '[0-9]{1,2}', 'shared' => 'shared'] // Route::get('/reports/{year}/{shared?}', ['uses' => 'ReportController@year', 'as' => 'reports.year'])->where(['year' => '[0-9]{4}', 'shared' => 'shared']);
); // Route::get('/reports/{year}/{month}/{shared?}', ['uses' => 'ReportController@month', 'as' => 'reports.month'])->where(['year' => '[0-9]{4}', 'month' => '[0-9]{1,2}', 'shared' => 'shared']);
// pop ups for budget report: // pop ups for budget report:

View File

@ -147,10 +147,9 @@ class TransactionJournal extends Model
} }
bcscale(2); bcscale(2);
$type = $this->transactionType->type;
$transaction = $this->transactions->sortByDesc('amount')->first(); $transaction = $this->transactions->sortByDesc('amount')->first();
$amount = $transaction->amount; $amount = $transaction->amount;
if ($type == 'Withdrawal') { if ($this->isWithdrawal()) {
$amount = $amount * -1; $amount = $amount * -1;
} }
$cache->store($amount); $cache->store($amount);
@ -167,15 +166,15 @@ class TransactionJournal extends Model
*/ */
protected function amountByTagAdvancePayment(Tag $tag, $amount) protected function amountByTagAdvancePayment(Tag $tag, $amount)
{ {
if ($this->transactionType->type == 'Withdrawal') { if ($this->isWithdrawal()) {
$others = $tag->transactionJournals()->transactionTypes(['Deposit'])->get(); $others = $tag->transactionJournals()->transactionTypes([TransactionType::DEPOSIT])->get();
foreach ($others as $other) { foreach ($others as $other) {
$amount = bcsub($amount, $other->amount_positive); $amount = bcsub($amount, $other->amount_positive);
} }
return $amount; return $amount;
} }
if ($this->transactionType->type == 'Deposit') { if ($this->isDeposit()) {
return '0'; return '0';
} }
@ -190,8 +189,8 @@ class TransactionJournal extends Model
*/ */
protected function amountByTagBalancingAct($tag, $amount) protected function amountByTagBalancingAct($tag, $amount)
{ {
if ($this->transactionType->type == 'Withdrawal') { if ($this->isWithdrawal()) {
$transfer = $tag->transactionJournals()->transactionTypes(['Transfer'])->first(); $transfer = $tag->transactionJournals()->transactionTypes([TransactionType::TRANSFER])->first();
if ($transfer) { if ($transfer) {
$amount = bcsub($amount, $transfer->amount_positive); $amount = bcsub($amount, $transfer->amount_positive);
@ -491,4 +490,43 @@ class TransactionJournal extends Model
return $this->belongsTo('FireflyIII\User'); return $this->belongsTo('FireflyIII\User');
} }
/**
* @return string
*/
public function getTransactionType()
{
return $this->transactionType->type;
}
/**
* @return bool
*/
public function isWithdrawal()
{
return $this->transactionType->isWithdrawal();
}
/**
* @return bool
*/
public function isDeposit()
{
return $this->transactionType->isDeposit();
}
/**
* @return bool
*/
public function isTransfer()
{
return $this->transactionType->isTransfer();
}
/**
* @return bool
*/
public function isOpeningBalance()
{
return $this->transactionType->isOpeningBalance();
}
} }

View File

@ -23,6 +23,11 @@ class TransactionType extends Model
{ {
use SoftDeletes; use SoftDeletes;
const WITHDRAWAL = 'Withdrawal';
const DEPOSIT = 'Deposit';
const TRANSFER = 'Transfer';
const OPENING_BALANCE = 'Opening balance';
/** /**
* @return array * @return array
*/ */
@ -38,4 +43,36 @@ class TransactionType extends Model
{ {
return $this->hasMany('FireflyIII\Models\TransactionJournal'); return $this->hasMany('FireflyIII\Models\TransactionJournal');
} }
/**
* @return bool
*/
public function isWithdrawal()
{
return $this->type === TransactionType::WITHDRAWAL;
}
/**
* @return bool
*/
public function isDeposit()
{
return $this->type === TransactionType::DEPOSIT;
}
/**
* @return bool
*/
public function isTransfer()
{
return $this->type === TransactionType::TRANSFER;
}
/**
* @return bool
*/
public function isOpeningBalance()
{
return $this->type === TransactionType::OPENING_BALANCE;
}
} }

View File

@ -234,7 +234,7 @@ class AccountRepository implements AccountRepositoryInterface
$ids = array_unique($ids); $ids = array_unique($ids);
if (count($ids) > 0) { if (count($ids) > 0) {
$accounts = Auth::user()->accounts()->whereIn('id', $ids)->get(); $accounts = Auth::user()->accounts()->whereIn('id', $ids)->where('accounts.active', 1)->get();
} }
bcscale(2); bcscale(2);
@ -273,6 +273,7 @@ class AccountRepository implements AccountRepositoryInterface
$accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC') $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')
->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id')
->where('account_meta.name', 'accountRole') ->where('account_meta.name', 'accountRole')
->where('accounts.active', 1)
->where('account_meta.data', '"savingAsset"') ->where('account_meta.data', '"savingAsset"')
->get(['accounts.*']); ->get(['accounts.*']);
$start = clone Session::get('start', new Carbon); $start = clone Session::get('start', new Carbon);
@ -329,7 +330,7 @@ class AccountRepository implements AccountRepositoryInterface
->where('transaction_journals.user_id', Auth::user()->id) ->where('transaction_journals.user_id', Auth::user()->id)
->where('transaction_journals.date', '>=', $start->format('Y-m-d')) ->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->where('transaction_types.type', 'Transfer'); ->where('transaction_types.type', TransactionType::TRANSFER);
} }
)->get(); )->get();
@ -375,7 +376,7 @@ class AccountRepository implements AccountRepositoryInterface
return TransactionJournal return TransactionJournal
::orderBy('transaction_journals.date', 'ASC') ::orderBy('transaction_journals.date', 'ASC')
->accountIs($account) ->accountIs($account)
->transactionTypes(['Opening balance']) ->transactionTypes([TransactionType::OPENING_BALANCE])
->orderBy('created_at', 'ASC') ->orderBy('created_at', 'ASC')
->first(['transaction_journals.*']); ->first(['transaction_journals.*']);
} }
@ -548,7 +549,7 @@ class AccountRepository implements AccountRepositoryInterface
*/ */
protected function storeInitialBalance(Account $account, Account $opposing, array $data) protected function storeInitialBalance(Account $account, Account $opposing, array $data)
{ {
$transactionType = TransactionType::whereType('Opening balance')->first(); $transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
$journal = TransactionJournal::create( $journal = TransactionJournal::create(
[ [
'user_id' => $data['user'], 'user_id' => $data['user'],
@ -642,4 +643,14 @@ class AccountRepository implements AccountRepositoryInterface
return $journal; return $journal;
} }
/**
* @param $accountId
*
* @return Account
*/
public function find($accountId)
{
return Auth::user()->accounts()->findOrNew($accountId);
}
} }

View File

@ -24,6 +24,13 @@ interface AccountRepositoryInterface
*/ */
public function countAccounts(array $types); public function countAccounts(array $types);
/**
* @param $accountId
*
* @return Account
*/
public function find($accountId);
/** /**
* @param Account $account * @param Account $account
* @param Account $moveTo * @param Account $moveTo

View File

@ -5,6 +5,7 @@ namespace FireflyIII\Repositories\Bill;
use Auth; use Auth;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
use FireflyIII\Models\Account;
use FireflyIII\Models\Bill; use FireflyIII\Models\Bill;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@ -108,6 +109,46 @@ class BillRepository implements BillRepositoryInterface
return $set; return $set;
} }
/**
* @param Collection $accounts
*
* @return Collection
*/
public function getBillsForAccounts(Collection $accounts)
{
/** @var Collection $set */
$set = Auth::user()->bills()->orderBy('name', 'ASC')->get();
$ids = [];
/** @var Account $account */
foreach ($accounts as $account) {
$ids[] = $account->id;
}
$set = $set->filter(
function (Bill $bill) use ($ids) {
// get transaction journals from or to any of the mentioned accounts.
// if zero, return null.
$journals = $bill->transactionjournals()->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereIn('transactions.account_id', $ids)->count();
return ($journals > 0);
}
);
$set = $set->sortBy(
function (Bill $bill) {
$int = $bill->active == 1 ? 0 : 1;
return $int . strtolower($bill->name);
}
);
return $set;
}
/** /**
* @param Bill $bill * @param Bill $bill
* *
@ -154,7 +195,7 @@ class BillRepository implements BillRepositoryInterface
} }
$journals = new Collection; $journals = new Collection;
if (count($ids) > 0) { if (count($ids) > 0) {
$journals = Auth::user()->transactionjournals()->transactionTypes(['Withdrawal'])->whereIn('transaction_journals.id', $ids)->get( $journals = Auth::user()->transactionjournals()->transactionTypes([TransactionType::WITHDRAWAL])->whereIn('transaction_journals.id', $ids)->get(
['transaction_journals.*'] ['transaction_journals.*']
); );
} }
@ -276,12 +317,21 @@ class BillRepository implements BillRepositoryInterface
*/ */
public function scan(Bill $bill, TransactionJournal $journal) public function scan(Bill $bill, TransactionJournal $journal)
{ {
/*
* Can only support withdrawals.
*/
if (false === $journal->isWithdrawal()) {
return false;
}
$matches = explode(',', $bill->match); $matches = explode(',', $bill->match);
$description = strtolower($journal->description) . ' ' . strtolower($journal->destination_account->name); $description = strtolower($journal->description) . ' ' . strtolower($journal->destination_account->name);
$wordMatch = $this->doWordMatch($matches, $description); $wordMatch = $this->doWordMatch($matches, $description);
$amountMatch = $this->doAmountMatch($journal->amount_positive, $bill->amount_min, $bill->amount_max); $amountMatch = $this->doAmountMatch($journal->amount_positive, $bill->amount_min, $bill->amount_max);
Log::debug('Journal #' . $journal->id . ' has description "' . $description . '"'); Log::debug('Journal #' . $journal->id . ' has description "' . $description . '"');
/* /*
* If both, update! * If both, update!
*/ */

View File

@ -77,6 +77,15 @@ interface BillRepositoryInterface
*/ */
public function getBills(); public function getBills();
/**
* Gets the bills which have some kind of relevance to the accounts mentioned.
*
* @param Collection $accounts
*
* @return Collection
*/
public function getBillsForAccounts(Collection $accounts);
/** /**
* @param Bill $bill * @param Bill $bill
* *

View File

@ -7,12 +7,14 @@ use Carbon\Carbon;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\LimitRepetition;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Shared\ComponentRepository; use FireflyIII\Repositories\Shared\ComponentRepository;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Input; use Input;
use Log;
/** /**
* Class BudgetRepository * Class BudgetRepository
@ -50,10 +52,10 @@ class BudgetRepository extends ComponentRepository implements BudgetRepositoryIn
* *
* @return float * @return float
*/ */
public function expensesOnDayCorrected(Budget $budget, Carbon $date) public function expensesOnDay(Budget $budget, Carbon $date)
{ {
bcscale(2); bcscale(2);
$sum = $budget->transactionjournals()->transactionTypes(['Withdrawal'])->onDate($date)->get(['transaction_journals.*'])->sum('amount'); $sum = $budget->transactionjournals()->transactionTypes([TransactionType::WITHDRAWAL])->onDate($date)->get(['transaction_journals.*'])->sum('amount');
return $sum; return $sum;
} }
@ -139,9 +141,11 @@ class BudgetRepository extends ComponentRepository implements BudgetRepositoryIn
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }
$data = $budget->limitrepetitions() $data = $budget->limitrepetitions()
->where('limit_repetitions.startdate', $start) ->where('limit_repetitions.startdate', $start->format('Y-m-d 00:00:00'))
->where('limit_repetitions.enddate', $end) ->where('limit_repetitions.enddate', $end->format('Y-m-d 00:00:00'))
->first(['limit_repetitions.*']); ->first(['limit_repetitions.*']);
//Log::debug('Looking for limit reps for budget #' . $budget->id . ' start [' . $start . '] and end [' . $end . '].');
//Log::debug(DB::getQueryLog())
$cache->store($data); $cache->store($data);
return $data; return $data;
@ -303,12 +307,23 @@ class BudgetRepository extends ComponentRepository implements BudgetRepositoryIn
) )
->after($start) ->after($start)
->before($end) ->before($end)
->transactionTypes(['Withdrawal']) ->transactionTypes([TransactionType::WITHDRAWAL])
->get(['transaction_journals.*'])->sum('amount'); ->get(['transaction_journals.*'])->sum('amount');
bcscale(2); return $noBudgetSet;
}
return bcmul($noBudgetSet, -1); /**
* @param Budget $budget
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return string
*/
public function balanceInPeriodForList(Budget $budget, Carbon $start, Carbon $end, Collection $accounts)
{
return $this->commonBalanceInPeriodForList($budget, $start, $end, $accounts);
} }
/** /**

View File

@ -35,7 +35,7 @@ interface BudgetRepositoryInterface
* *
* @return float * @return float
*/ */
public function expensesOnDayCorrected(Budget $budget, Carbon $date); public function expensesOnDay(Budget $budget, Carbon $date);
/** /**
* @return Collection * @return Collection
@ -139,6 +139,19 @@ interface BudgetRepositoryInterface
*/ */
public function balanceInPeriod(Budget $budget, Carbon $start, Carbon $end, $shared = true); public function balanceInPeriod(Budget $budget, Carbon $start, Carbon $end, $shared = true);
/**
*
* Same as ::spentInPeriod but corrects journals for a set of accounts
*
* @param Budget $budget
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return string
*/
public function balanceInPeriodForList(Budget $budget, Carbon $start, Carbon $end, Collection $accounts);
/** /**
* @param array $data * @param array $data
* *

View File

@ -7,6 +7,7 @@ use Carbon\Carbon;
use Crypt; use Crypt;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Shared\ComponentRepository; use FireflyIII\Repositories\Shared\ComponentRepository;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -58,6 +59,71 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
return $set; return $set;
} }
/**
* Returns the amount earned without category by accounts in period.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function earnedNoCategoryForAccounts(Collection $accounts, Carbon $start, Carbon $end)
{
$accountIds = [];
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
// is deposit AND account_from is in the list of $accounts
// not from any of the accounts in the list?
return 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)
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereIn('transactions.account_id', $accountIds)
->transactionTypes([TransactionType::DEPOSIT])
->get(['transaction_journals.*'])->sum('amount');
}
/**
* Returns the amount spent without category by accounts in period.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentNoCategoryForAccounts(Collection $accounts, Carbon $start, Carbon $end)
{
$accountIds = [];
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
// is withdrawal or transfer AND account_from is in the list of $accounts
return 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)
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereIn('transactions.account_id', $accountIds)
->transactionTypes([TransactionType::WITHDRAWAL])
->get(['transaction_journals.*'])->sum('amount');
}
/** /**
* *
@ -66,7 +132,7 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
* *
* @return array * @return array
*/ */
public function getCategoriesAndExpensesCorrected(Carbon $start, Carbon $end) public function getCategoriesAndExpenses(Carbon $start, Carbon $end)
{ {
$set = Auth::user()->transactionjournals() $set = Auth::user()->transactionjournals()
->leftJoin( ->leftJoin(
@ -76,7 +142,7 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
->before($end) ->before($end)
->where('categories.user_id', Auth::user()->id) ->where('categories.user_id', Auth::user()->id)
->after($start) ->after($start)
->transactionTypes(['Withdrawal']) ->transactionTypes([TransactionType::WITHDRAWAL])
->get(['categories.id as category_id', 'categories.encrypted as category_encrypted', 'categories.name', 'transaction_journals.*']); ->get(['categories.id as category_id', 'categories.encrypted as category_encrypted', 'categories.name', 'transaction_journals.*']);
bcscale(2); bcscale(2);
@ -190,6 +256,19 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
return $this->commonBalanceInPeriod($category, $start, $end, $shared); return $this->commonBalanceInPeriod($category, $start, $end, $shared);
} }
/**
* @param Category $category
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return string
*/
public function balanceInPeriodForList(Category $category, Carbon $start, Carbon $end, Collection $accounts)
{
return $this->commonBalanceInPeriodForList($category, $start, $end, $accounts);
}
/** /**
* Corrected for tags * Corrected for tags
* *
@ -198,9 +277,9 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
* *
* @return string * @return string
*/ */
public function spentOnDaySumCorrected(Category $category, Carbon $date) public function spentOnDaySum(Category $category, Carbon $date)
{ {
return $category->transactionjournals()->transactionTypes(['Withdrawal'])->onDate($date)->get(['transaction_journals.*'])->sum('amount'); return $category->transactionjournals()->transactionTypes([TransactionType::WITHDRAWAL])->onDate($date)->get(['transaction_journals.*'])->sum('amount');
} }
/** /**
@ -285,7 +364,8 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }
$sum = $category->transactionjournals()->transactionTypes(['Withdrawal'])->before($end)->after($start)->get(['transaction_journals.*'])->sum( $sum = $category->transactionjournals()->transactionTypes([TransactionType::WITHDRAWAL])->before($end)->after($start)->get(['transaction_journals.*'])
->sum(
'amount' 'amount'
); );
@ -315,7 +395,8 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }
$sum = $category->transactionjournals()->transactionTypes(['Deposit'])->before($end)->after($start)->get(['transaction_journals.*'])->sum( $sum = $category->transactionjournals()->transactionTypes([TransactionType::DEPOSIT])->before($end)->after($start)->get(['transaction_journals.*'])
->sum(
'amount' 'amount'
); );
@ -365,8 +446,69 @@ class CategoryRepository extends ComponentRepository implements CategoryReposito
* *
* @return float * @return float
*/ */
public function earnedOnDaySumCorrected(Category $category, Carbon $date) public function earnedOnDaySum(Category $category, Carbon $date)
{ {
return $category->transactionjournals()->transactionTypes(['Deposit'])->onDate($date)->get(['transaction_journals.*'])->sum('amount'); return $category->transactionjournals()->transactionTypes([TransactionType::DEPOSIT])->onDate($date)->get(['transaction_journals.*'])->sum('amount');
}
/**
* Calculates how much is spent in this period.
*
* @param Category $category
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentInPeriodForAccounts(Category $category, Collection $accounts, Carbon $start, Carbon $end)
{
$accountIds = [];
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
$sum
= $category
->transactionjournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->after($start)
->before($end)
->whereIn('transactions.account_id', $accountIds)
->transactionTypes([TransactionType::WITHDRAWAL])
->get(['transaction_journals.*'])
->sum('amount');
return $sum;
}
/**
* Calculate how much is earned in this period.
*
* @param Category $category
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function earnedInPeriodForAccounts(Category $category, Collection $accounts, Carbon $start, Carbon $end)
{
$accountIds = [];
foreach ($accounts as $account) {
$accountIds[] = $account->id;
}
$sum
= $category
->transactionjournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->before($end)
->whereIn('transactions.account_id', $accountIds)
->transactionTypes([TransactionType::DEPOSIT])
->after($start)
->get(['transaction_journals.*'])
->sum('amount');
return $sum;
} }
} }

View File

@ -39,6 +39,53 @@ interface CategoryRepositoryInterface
*/ */
public function getCategories(); public function getCategories();
/**
* Calculates how much is spent in this period.
*
* @param Category $category
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentInPeriodForAccounts(Category $category, Collection $accounts, Carbon $start, Carbon $end);
/**
* Calculate how much is earned in this period.
*
* @param Category $category
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function earnedInPeriodForAccounts(Category $category, Collection $accounts, Carbon $start, Carbon $end);
/**
* Returns the amount spent without category by accounts in period.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function spentNoCategoryForAccounts(Collection $accounts, Carbon $start, Carbon $end);
/**
* Returns the amount earned without category by accounts in period.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function earnedNoCategoryForAccounts(Collection $accounts, Carbon $start, Carbon $end);
/** /**
* Corrected for tags. * Corrected for tags.
* *
@ -47,7 +94,7 @@ interface CategoryRepositoryInterface
* *
* @return array * @return array
*/ */
public function getCategoriesAndExpensesCorrected(Carbon $start, Carbon $end); public function getCategoriesAndExpenses(Carbon $start, Carbon $end);
/** /**
* @param Category $category * @param Category $category
@ -112,6 +159,19 @@ interface CategoryRepositoryInterface
*/ */
public function balanceInPeriod(Category $category, Carbon $start, Carbon $end, $shared = false); public function balanceInPeriod(Category $category, Carbon $start, Carbon $end, $shared = false);
/**
* Corrected for tags.
*
* @param Category $category
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
* @param Collection $accounts
*
* @return string
*/
public function balanceInPeriodForList(Category $category, Carbon $start, Carbon $end, Collection $accounts);
/** /**
* @param Category $category * @param Category $category
* @param \Carbon\Carbon $start * @param \Carbon\Carbon $start
@ -143,7 +203,7 @@ interface CategoryRepositoryInterface
* *
* @return float * @return float
*/ */
public function spentOnDaySumCorrected(Category $category, Carbon $date); public function spentOnDaySum(Category $category, Carbon $date);
/** /**
* *
@ -154,7 +214,7 @@ interface CategoryRepositoryInterface
* *
* @return float * @return float
*/ */
public function earnedOnDaySumCorrected(Category $category, Carbon $date); public function earnedOnDaySum(Category $category, Carbon $date);
/** /**
* @param array $data * @param array $data

View File

@ -327,15 +327,15 @@ class JournalRepository implements JournalRepositoryInterface
$fromAccount = null; $fromAccount = null;
$toAccount = null; $toAccount = null;
switch ($type->type) { switch ($type->type) {
case 'Withdrawal': case TransactionType::WITHDRAWAL:
list($fromAccount, $toAccount) = $this->storeWithdrawalAccounts($data); list($fromAccount, $toAccount) = $this->storeWithdrawalAccounts($data);
break; break;
case 'Deposit': case TransactionType::DEPOSIT:
list($fromAccount, $toAccount) = $this->storeDepositAccounts($data); list($fromAccount, $toAccount) = $this->storeDepositAccounts($data);
break; break;
case 'Transfer': case TransactionType::TRANSFER:
$fromAccount = Account::find($data['account_from_id']); $fromAccount = Account::find($data['account_from_id']);
$toAccount = Account::find($data['account_to_id']); $toAccount = Account::find($data['account_to_id']);
break; break;

View File

@ -3,8 +3,11 @@
namespace FireflyIII\Repositories\Shared; namespace FireflyIII\Repositories\Shared;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Models\TransactionType;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
/** /**
* Class ComponentRepository * Class ComponentRepository
@ -39,7 +42,10 @@ class ComponentRepository
} }
if ($shared === true) { // shared is true: always ignore transfers between accounts! if ($shared === true) { // shared is true: always ignore transfers between accounts!
$sum = $object->transactionjournals()->transactionTypes(['Withdrawal', 'Deposit', 'Opening balance'])->before($end)->after($start) $sum = $object->transactionjournals()
->transactionTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
->before($end)
->after($start)
->get(['transaction_journals.*'])->sum('amount'); ->get(['transaction_journals.*'])->sum('amount');
} else { } else {
// do something else, SEE budgets. // do something else, SEE budgets.
@ -47,7 +53,7 @@ class ComponentRepository
$sum = $object->transactionjournals()->before($end)->after($start) $sum = $object->transactionjournals()->before($end)->after($start)
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->transactionTypes(['Withdrawal', 'Deposit', 'Opening balance']) ->transactionTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
->leftJoin( ->leftJoin(
'account_meta', function (JoinClause $join) { 'account_meta', function (JoinClause $join) {
$join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole');
@ -59,4 +65,46 @@ class ComponentRepository
return $sum; return $sum;
} }
/**
* @param $object
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return string
*/
protected function commonBalanceInPeriodForList($object, Carbon $start, Carbon $end, Collection $accounts)
{
$cache = new CacheProperties; // we must cache this.
$cache->addProperty($object->id);
$cache->addProperty(get_class($object));
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($accounts);
$cache->addProperty('balanceInPeriodList');
$ids = [];
/** @var Account $account */
foreach ($accounts as $account) {
$ids[] = $account->id;
}
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
$sum = $object->transactionjournals()
->transactionTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
->before($end)
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->whereIn('accounts.id', $ids)
->after($start)
->get(['transaction_journals.*'])->sum('amount');
$cache->store($sum);
return $sum;
}
} }

View File

@ -77,7 +77,7 @@ class TagRepository implements TagRepositoryInterface
/** @var Tag $tag */ /** @var Tag $tag */
foreach ($tags as $tag) { foreach ($tags as $tag) {
$journals = $tag->transactionjournals()->after($start)->before($end)->transactionTypes(['Transfer'])->get(['transaction_journals.*']); $journals = $tag->transactionjournals()->after($start)->before($end)->transactionTypes([TransactionType::TRANSFER])->get(['transaction_journals.*']);
/** @var TransactionJournal $journal */ /** @var TransactionJournal $journal */
foreach ($journals as $journal) { foreach ($journals as $journal) {
@ -158,7 +158,7 @@ class TagRepository implements TagRepositoryInterface
if ($tag->tagMode == 'balancingAct' || $tag->tagMode == 'nothing') { if ($tag->tagMode == 'balancingAct' || $tag->tagMode == 'nothing') {
foreach ($tag->transactionjournals as $journal) { foreach ($tag->transactionjournals as $journal) {
if ($journal->transactionType->type == 'Transfer') { if ($journal->isTransfer()) {
return false; return false;
} }
} }
@ -169,7 +169,7 @@ class TagRepository implements TagRepositoryInterface
*/ */
$count = 0; $count = 0;
foreach ($tag->transactionjournals as $journal) { foreach ($tag->transactionjournals as $journal) {
if ($journal->transactionType->type == 'Withdrawal') { if ($journal->isWithdrawal()) {
$count++; $count++;
} }
} }
@ -201,7 +201,7 @@ class TagRepository implements TagRepositoryInterface
* If any transaction is a deposit, cannot become a balancing act. * If any transaction is a deposit, cannot become a balancing act.
*/ */
foreach ($tag->transactionjournals as $journal) { foreach ($tag->transactionjournals as $journal) {
if ($journal->transactionType->type == 'Deposit') { if ($journal->isDeposit()) {
return false; return false;
} }
} }
@ -239,10 +239,10 @@ class TagRepository implements TagRepositoryInterface
protected function connectBalancingAct(TransactionJournal $journal, Tag $tag) protected function connectBalancingAct(TransactionJournal $journal, Tag $tag)
{ {
/** @var TransactionType $withdrawal */ /** @var TransactionType $withdrawal */
$withdrawal = TransactionType::whereType('Withdrawal')->first(); $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first();
$withdrawals = $tag->transactionjournals()->where('transaction_type_id', $withdrawal->id)->count(); $withdrawals = $tag->transactionjournals()->where('transaction_type_id', $withdrawal->id)->count();
/** @var TransactionType $transfer */ /** @var TransactionType $transfer */
$transfer = TransactionType::whereType('Transfer')->first(); $transfer = TransactionType::whereType(TransactionType::TRANSFER)->first();
$transfers = $tag->transactionjournals()->where('transaction_type_id', $transfer->id)->count(); $transfers = $tag->transactionjournals()->where('transaction_type_id', $transfer->id)->count();
@ -275,11 +275,11 @@ class TagRepository implements TagRepositoryInterface
protected function connectAdvancePayment(TransactionJournal $journal, Tag $tag) protected function connectAdvancePayment(TransactionJournal $journal, Tag $tag)
{ {
/** @var TransactionType $transfer */ /** @var TransactionType $transfer */
$transfer = TransactionType::whereType('Transfer')->first(); $transfer = TransactionType::whereType(TransactionType::TRANSFER)->first();
/** @var TransactionType $withdrawal */ /** @var TransactionType $withdrawal */
$withdrawal = TransactionType::whereType('Withdrawal')->first(); $withdrawal = TransactionType::whereType(TransactionType::WITHDRAWAL)->first();
/** @var TransactionType $deposit */ /** @var TransactionType $deposit */
$deposit = TransactionType::whereType('Deposit')->first(); $deposit = TransactionType::whereType(TransactionType::DEPOSIT)->first();
$withdrawals = $tag->transactionjournals()->where('transaction_type_id', $withdrawal->id)->count(); $withdrawals = $tag->transactionjournals()->where('transaction_type_id', $withdrawal->id)->count();
$deposits = $tag->transactionjournals()->where('transaction_type_id', $deposit->id)->count(); $deposits = $tag->transactionjournals()->where('transaction_type_id', $deposit->id)->count();
@ -326,10 +326,10 @@ class TagRepository implements TagRepositoryInterface
foreach ($tag->transactionjournals as $check) { foreach ($tag->transactionjournals as $check) {
// $checkAccount is the source_account for a withdrawal // $checkAccount is the source_account for a withdrawal
// $checkAccount is the destination_account for a deposit // $checkAccount is the destination_account for a deposit
if ($check->transactionType->type == 'Withdrawal' && $check->source_account->id != $journal->destination_account->id) { if ($check->isWithdrawal() && $check->source_account->id != $journal->destination_account->id) {
$match = false; $match = false;
} }
if ($check->transactionType->type == 'Deposit' && $check->destination_account->id != $journal->destination_account->id) { if ($check->isDeposit() && $check->destination_account->id != $journal->destination_account->id) {
$match = false; $match = false;
} }

View File

@ -40,7 +40,7 @@ class Amount
if ($cache->has()) { if ($cache->has()) {
return $cache->get(); return $cache->get();
} else { } else {
$currencyPreference = Prefs::get('currencyPreference', 'EUR'); $currencyPreference = Prefs::get('currencyPreference', env('DEFAULT_CURRENCY','EUR'));
$currency = TransactionCurrency::whereCode($currencyPreference->data)->first(); $currency = TransactionCurrency::whereCode($currencyPreference->data)->first();
$cache->store($currency->symbol); $cache->store($currency->symbol);
@ -60,7 +60,7 @@ class Amount
{ {
$amount = floatval($amount); $amount = floatval($amount);
$amount = round($amount, 2); $amount = round($amount, 2);
$string = number_format($amount, 2, ',', '.'); $string = money_format('%!.2n', $amount);
if ($coloured === true) { if ($coloured === true) {
if ($amount === 0.0) { if ($amount === 0.0) {
@ -100,13 +100,13 @@ class Amount
$symbol = $journal->symbol; $symbol = $journal->symbol;
} }
if ($journal->transactionType->type == 'Transfer' && $coloured) { if ($journal->isTransfer() && $coloured) {
$txt = '<span class="text-info">' . $this->formatWithSymbol($symbol, $journal->amount_positive, false) . '</span>'; $txt = '<span class="text-info">' . $this->formatWithSymbol($symbol, $journal->amount_positive, false) . '</span>';
$cache->store($txt); $cache->store($txt);
return $txt; return $txt;
} }
if ($journal->transactionType->type == 'Transfer' && !$coloured) { if ($journal->isTransfer() && !$coloured) {
$txt = $this->formatWithSymbol($symbol, $journal->amount_positive, false); $txt = $this->formatWithSymbol($symbol, $journal->amount_positive, false);
$cache->store($txt); $cache->store($txt);
@ -152,7 +152,7 @@ class Amount
if ($cache->has()) { if ($cache->has()) {
return $cache->get(); return $cache->get();
} else { } else {
$currencyPreference = Prefs::get('currencyPreference', 'EUR'); $currencyPreference = Prefs::get('currencyPreference', env('DEFAULT_CURRENCY','EUR'));
$currency = TransactionCurrency::whereCode($currencyPreference->data)->first(); $currency = TransactionCurrency::whereCode($currencyPreference->data)->first();
if ($currency) { if ($currency) {
@ -161,9 +161,9 @@ class Amount
return $currency->code; return $currency->code;
} }
$cache->store('EUR'); $cache->store(env('DEFAULT_CURRENCY','EUR'));
return 'EUR'; // @codeCoverageIgnore return env('DEFAULT_CURRENCY','EUR'); // @codeCoverageIgnore
} }
} }

View File

@ -23,10 +23,10 @@ class Budget extends Twig_Extension
{ {
$functions = []; $functions = [];
$functions[] = new Twig_SimpleFunction( $functions[] = new Twig_SimpleFunction(
'spentInRepetitionCorrected', function (LimitRepetition $repetition) { 'spentInRepetition', function (LimitRepetition $repetition) {
$cache = new CacheProperties; $cache = new CacheProperties;
$cache->addProperty($repetition->id); $cache->addProperty($repetition->id);
$cache->addProperty('spentInRepetitionCorrected'); $cache->addProperty('spentInRepetition');
if ($cache->has()) { if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }

View File

@ -68,19 +68,17 @@ class Journal extends Twig_Extension
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }
$type = $journal->transactionType->type; switch (true) {
case $journal->isWithdrawal():
switch ($type) {
case 'Withdrawal':
$txt = '<i class="fa fa-long-arrow-left fa-fw" title="' . trans('firefly.withdrawal') . '"></i>'; $txt = '<i class="fa fa-long-arrow-left fa-fw" title="' . trans('firefly.withdrawal') . '"></i>';
break; break;
case 'Deposit': case $journal->isDeposit():
$txt = '<i class="fa fa-long-arrow-right fa-fw" title="' . trans('firefly.deposit') . '"></i>'; $txt = '<i class="fa fa-long-arrow-right fa-fw" title="' . trans('firefly.deposit') . '"></i>';
break; break;
case 'Transfer': case $journal->isTransfer():
$txt = '<i class="fa fa-fw fa-exchange" title="' . trans('firefly.transfer') . '"></i>'; $txt = '<i class="fa fa-fw fa-exchange" title="' . trans('firefly.transfer') . '"></i>';
break; break;
case 'Opening balance': case $journal->isOpeningBalance():
$txt = '<i class="fa-fw fa fa-ban" title="' . trans('firefly.openingBalance') . '"></i>'; $txt = '<i class="fa-fw fa fa-ban" title="' . trans('firefly.openingBalance') . '"></i>';
break; break;
default: default:
@ -189,7 +187,7 @@ class Journal extends Twig_Extension
} }
if ($tag->tagMode == 'advancePayment') { if ($tag->tagMode == 'advancePayment') {
if ($journal->transactionType->type == 'Deposit') { if ($journal->isDeposit()) {
$amount = app('amount')->formatJournal($journal, false); $amount = app('amount')->formatJournal($journal, false);
$string = '<a href="' . route('tags.show', [$tag->id]) . '" class="label label-success" title="' . $amount $string = '<a href="' . route('tags.show', [$tag->id]) . '" class="label label-success" title="' . $amount
. '"><i class="fa fa-fw fa-sort-numeric-desc"></i> ' . $tag->tag . '</a>'; . '"><i class="fa fa-fw fa-sort-numeric-desc"></i> ' . $tag->tag . '</a>';
@ -201,7 +199,7 @@ class Journal extends Twig_Extension
* AdvancePayment with a withdrawal will show the amount with a link to * AdvancePayment with a withdrawal will show the amount with a link to
* the tag. The TransactionJournal should properly calculate the amount. * the tag. The TransactionJournal should properly calculate the amount.
*/ */
if ($journal->transactionType->type == 'Withdrawal') { if ($journal->isWithdrawal()) {
$amount = app('amount')->formatJournal($journal); $amount = app('amount')->formatJournal($journal);
$string = '<a href="' . route('tags.show', [$tag->id]) . '">' . $amount . '</a>'; $string = '<a href="' . route('tags.show', [$tag->id]) . '">' . $amount . '</a>';

737
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -139,8 +139,8 @@ return [
'TwigBridge\ServiceProvider', 'TwigBridge\ServiceProvider',
'DaveJamesMiller\Breadcrumbs\ServiceProvider', 'DaveJamesMiller\Breadcrumbs\ServiceProvider',
//'Barryvdh\Debugbar\ServiceProvider', // 'Barryvdh\Debugbar\ServiceProvider',
//'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider', // 'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
'Zizaco\Entrust\EntrustServiceProvider', 'Zizaco\Entrust\EntrustServiceProvider',
/* /*

View File

@ -49,7 +49,7 @@ return [
'sqlite' => [ 'sqlite' => [
'driver' => 'sqlite', 'driver' => 'sqlite',
'database' => __DIR__ . '/../storage/database/testing.db', 'database' => storage_path('database/testing.db'),
'prefix' => '', 'prefix' => '',
], ],

View File

@ -2,7 +2,7 @@
return [ return [
'chart' => 'chartjs', 'chart' => 'chartjs',
'version' => '3.5.3', 'version' => '3.5.4',
'index_periods' => ['1D', '1W', '1M', '3M', '6M', '1Y', 'custom'], 'index_periods' => ['1D', '1W', '1M', '3M', '6M', '1Y', 'custom'],
'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], 'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'],
'csv_import_enabled' => true, 'csv_import_enabled' => true,

View File

@ -15,6 +15,7 @@ return [
| |
*/ */
'blocked_domains' => explode(',', env('BLOCKED_DOMAINS')),
'driver' => env('EMAIL_DRIVER', 'smtp'), 'driver' => env('EMAIL_DRIVER', 'smtp'),
/* /*

View File

@ -49,7 +49,7 @@ class TestDataSeeder extends Seeder
$this->createPiggybanks(); $this->createPiggybanks();
// dates: // dates:
$start = Carbon::now()->subyear()->startOfMonth(); $start = Carbon::now()->subYears(5)->startOfMonth();
$end = Carbon::now()->endOfDay(); $end = Carbon::now()->endOfDay();

View File

@ -12,10 +12,10 @@ class TransactionTypeSeeder extends Seeder
DB::table('transaction_types')->delete(); DB::table('transaction_types')->delete();
TransactionType::create(['type' => 'Withdrawal']); TransactionType::create(['type' => TransactionType::WITHDRAWAL]);
TransactionType::create(['type' => 'Deposit']); TransactionType::create(['type' => TransactionType::DEPOSIT]);
TransactionType::create(['type' => 'Transfer']); TransactionType::create(['type' => TransactionType::TRANSFER]);
TransactionType::create(['type' => 'Opening balance']); TransactionType::create(['type' => TransactionType::OPENING_BALANCE]);
} }
} }

4
public/js/accounting.min.js vendored Normal file
View File

@ -0,0 +1,4 @@
/*!
* accounting.js v0.4.2, copyright 2014 Open Exchange Rates, MIT license, http://openexchangerates.github.io/accounting.js
*/
(function(p,z){function q(a){return!!(""===a||a&&a.charCodeAt&&a.substr)}function m(a){return u?u(a):"[object Array]"===v.call(a)}function r(a){return"[object Object]"===v.call(a)}function s(a,b){var d,a=a||{},b=b||{};for(d in b)b.hasOwnProperty(d)&&null==a[d]&&(a[d]=b[d]);return a}function j(a,b,d){var c=[],e,h;if(!a)return c;if(w&&a.map===w)return a.map(b,d);for(e=0,h=a.length;e<h;e++)c[e]=b.call(d,a[e],e,a);return c}function n(a,b){a=Math.round(Math.abs(a));return isNaN(a)?b:a}function x(a){var b=c.settings.currency.format;"function"===typeof a&&(a=a());return q(a)&&a.match("%v")?{pos:a,neg:a.replace("-","").replace("%v","-%v"),zero:a}:!a||!a.pos||!a.pos.match("%v")?!q(b)?b:c.settings.currency.format={pos:b,neg:b.replace("%v","-%v"),zero:b}:a}var c={version:"0.4.1",settings:{currency:{symbol:"$",format:"%s%v",decimal:".",thousand:",",precision:2,grouping:3},number:{precision:0,grouping:3,thousand:",",decimal:"."}}},w=Array.prototype.map,u=Array.isArray,v=Object.prototype.toString,o=c.unformat=c.parse=function(a,b){if(m(a))return j(a,function(a){return o(a,b)});a=a||0;if("number"===typeof a)return a;var b=b||".",c=RegExp("[^0-9-"+b+"]",["g"]),c=parseFloat((""+a).replace(/\((.*)\)/,"-$1").replace(c,"").replace(b,"."));return!isNaN(c)?c:0},y=c.toFixed=function(a,b){var b=n(b,c.settings.number.precision),d=Math.pow(10,b);return(Math.round(c.unformat(a)*d)/d).toFixed(b)},t=c.formatNumber=c.format=function(a,b,d,i){if(m(a))return j(a,function(a){return t(a,b,d,i)});var a=o(a),e=s(r(b)?b:{precision:b,thousand:d,decimal:i},c.settings.number),h=n(e.precision),f=0>a?"-":"",g=parseInt(y(Math.abs(a||0),h),10)+"",l=3<g.length?g.length%3:0;return f+(l?g.substr(0,l)+e.thousand:"")+g.substr(l).replace(/(\d{3})(?=\d)/g,"$1"+e.thousand)+(h?e.decimal+y(Math.abs(a),h).split(".")[1]:"")},A=c.formatMoney=function(a,b,d,i,e,h){if(m(a))return j(a,function(a){return A(a,b,d,i,e,h)});var a=o(a),f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format);return(0<a?g.pos:0>a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal))};c.formatColumn=function(a,b,d,i,e,h){if(!a)return[];var f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format),l=g.pos.indexOf("%s")<g.pos.indexOf("%v")?!0:!1,k=0,a=j(a,function(a){if(m(a))return c.formatColumn(a,f);a=o(a);a=(0<a?g.pos:0>a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal));if(a.length>k)k=a.length;return a});return j(a,function(a){return q(a)&&a.length<k?l?a.replace(f.symbol,f.symbol+Array(k-a.length+1).join(" ")):Array(k-a.length+1).join(" ")+a:a})};if("undefined"!==typeof exports){if("undefined"!==typeof module&&module.exports)exports=module.exports=c;exports.accounting=c}else"function"===typeof define&&define.amd?define([],function(){return c}):(c.noConflict=function(a){return function(){p.accounting=a;c.noConflict=z;return c}}(p.accounting),p.accounting=c)})(this);

View File

@ -24,6 +24,23 @@ var colourSet = [
]; ];
// Settings object that controls default parameters for library methods:
accounting.settings = {
currency: {
symbol : currencySymbol, // default currency symbol is '$'
format: "%s %v", // controls output: %s = symbol, %v = value/number (can be object: see below)
decimal : ",", // decimal point separator
thousand: ".", // thousands separator
precision : 2 // decimal places
},
number: {
precision : 0, // default precision on numbers is 0
thousand: ",",
decimal : "."
}
}
var fillColors = []; var fillColors = [];
var strokePointHighColors = []; var strokePointHighColors = [];
@ -45,9 +62,9 @@ var defaultAreaOptions = {
animation: false, animation: false,
scaleFontSize: 10, scaleFontSize: 10,
responsive: false, responsive: false,
scaleLabel: " <%= '" + currencySymbol + " ' + Number(value).toFixed(0).replace('.', ',') %>", scaleLabel: " <%= accounting.formatMoney(value) %>",
tooltipFillColor: "rgba(0,0,0,0.5)", tooltipFillColor: "rgba(0,0,0,0.5)",
multiTooltipTemplate: "<%=datasetLabel%>: <%= '" + currencySymbol + " ' + Number(value).toFixed(2).replace('.', ',') %>" multiTooltipTemplate: "<%=datasetLabel%>: <%= accounting.formatMoney(value) %>"
}; };
@ -61,7 +78,7 @@ var defaultPieOptions = {
scaleFontSize: 10, scaleFontSize: 10,
responsive: false, responsive: false,
tooltipFillColor: "rgba(0,0,0,0.5)", tooltipFillColor: "rgba(0,0,0,0.5)",
tooltipTemplate: "<%if (label){%><%=label%>: <%}%>" + currencySymbol + " <%= value %>", tooltipTemplate: "<%if (label){%><%=label%>: <%}%> <%= accounting.formatMoney(value) %>",
}; };
@ -90,10 +107,10 @@ var defaultColumnOptions = {
scaleFontSize: 10, scaleFontSize: 10,
responsive: false, responsive: false,
animation: false, animation: false,
scaleLabel: "<%= '" + currencySymbol + " ' + Number(value).toFixed(0).replace('.', ',') %>", scaleLabel: "<%= accounting.formatMoney(value) %>",
tooltipFillColor: "rgba(0,0,0,0.5)", tooltipFillColor: "rgba(0,0,0,0.5)",
tooltipTemplate: "<%if (label){%><%=label%>: <%}%>" + currencySymbol + " <%= value %>", tooltipTemplate: "<%if (label){%><%=label%>: <%}%> <%= accounting.formatMoney(value) %>",
multiTooltipTemplate: "<%=datasetLabel%>: " + currencySymbol + " <%= Number(value).toFixed(2).replace('.', ',') %>" multiTooltipTemplate: "<%=datasetLabel%>: <%= accounting.formatMoney(value) %>"
}; };
var defaultStackedColumnOptions = { var defaultStackedColumnOptions = {
@ -105,9 +122,9 @@ var defaultStackedColumnOptions = {
animation: false, animation: false,
scaleFontSize: 10, scaleFontSize: 10,
responsive: false, responsive: false,
scaleLabel: "<%= '" + currencySymbol + " ' + Number(value).toFixed(0).replace('.', ',') %>", scaleLabel: "<%= accounting.formatMoney(value) %>",
tooltipFillColor: "rgba(0,0,0,0.5)", tooltipFillColor: "rgba(0,0,0,0.5)",
multiTooltipTemplate: "<%=datasetLabel%>: " + currencySymbol + " <%= Number(value).toFixed(2).replace('.', ',') %>" multiTooltipTemplate: "<%=datasetLabel%>: <%= accounting.formatMoney(value) %>"
}; };

View File

@ -1,93 +0,0 @@
/* globals google, expenseRestShow:true, incomeRestShow:true, year, shared, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
drawChart();
});
function drawChart() {
"use strict";
if (typeof columnChart !== 'undefined' && typeof year !== 'undefined' && typeof month === 'undefined') {
columnChart('chart/report/in-out/' + year + shared, 'income-expenses-chart');
columnChart('chart/report/in-out-sum/' + year + shared, 'income-expenses-sum-chart');
}
if (typeof stackedColumnChart !== 'undefined' && typeof year !== 'undefined' && typeof month === 'undefined') {
stackedColumnChart('chart/budget/year/' + year + shared, 'budgets');
stackedColumnChart('chart/category/spent-in-year/' + year + shared, 'categories-spent-in-year');
stackedColumnChart('chart/category/earned-in-year/' + year + shared, 'categories-earned-in-year');
}
if (typeof lineChart !== 'undefined' && typeof month !== 'undefined') {
lineChart('/chart/account/month/' + year + '/' + month + shared, 'account-balances-chart');
}
}
function openModal(e) {
"use strict";
var target = $(e.target).parent();
var URL = target.attr('href');
$.get(URL).success(function (data) {
$('#defaultModal').empty().html(data).modal('show');
}).fail(function () {
alert('Could not load data.');
});
return false;
}
function showIncomes() {
"use strict";
if (incomeRestShow) {
// hide everything, make button say "show"
$('#showIncomes').text(showTheRest);
$('.incomesCollapsed').removeClass('in').addClass('out');
// toggle:
incomeRestShow = false;
} else {
// show everything, make button say "hide".
$('#showIncomes').text(hideTheRest);
$('.incomesCollapsed').removeClass('out').addClass('in');
// toggle:
incomeRestShow = true;
}
return false;
}
function showExpenses() {
"use strict";
if (expenseRestShow) {
// hide everything, make button say "show"
$('#showExpenses').text(showTheRestExpense);
$('.expenseCollapsed').removeClass('in').addClass('out');
// toggle:
expenseRestShow = false;
} else {
// show everything, make button say "hide".
$('#showExpenses').text(hideTheRestExpense);
$('.expenseCollapsed').removeClass('out').addClass('in');
// toggle:
expenseRestShow = true;
}
return false;
}
$(function () {
"use strict";
$('.openModal').on('click', openModal);
// click open the top X income list:
$('#showIncomes').click(showIncomes);
// click open the top X expense list:
$('#showExpenses').click(showExpenses);
});

View File

@ -0,0 +1,64 @@
/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, expenseRestShow:true, incomeRestShow:true, year, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
drawChart();
// click open the top X income list:
$('#showIncomes').click(showIncomes);
// click open the top X expense list:
$('#showExpenses').click(showExpenses);
});
function drawChart() {
"use strict";
// month view:
// draw account chart
lineChart('/chart/account/report/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'account-balances-chart');
}
function showIncomes() {
"use strict";
if (incomeRestShow) {
// hide everything, make button say "show"
$('#showIncomes').text(showTheRest);
$('.incomesCollapsed').removeClass('in').addClass('out');
// toggle:
incomeRestShow = false;
} else {
// show everything, make button say "hide".
$('#showIncomes').text(hideTheRest);
$('.incomesCollapsed').removeClass('out').addClass('in');
// toggle:
incomeRestShow = true;
}
return false;
}
function showExpenses() {
"use strict";
if (expenseRestShow) {
// hide everything, make button say "show"
$('#showExpenses').text(showTheRestExpense);
$('.expenseCollapsed').removeClass('in').addClass('out');
// toggle:
expenseRestShow = false;
} else {
// show everything, make button say "hide".
$('#showExpenses').text(hideTheRestExpense);
$('.expenseCollapsed').removeClass('out').addClass('in');
// toggle:
expenseRestShow = true;
}
return false;
}

View File

@ -0,0 +1,155 @@
/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, expenseRestShow:true, incomeRestShow:true, year, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
drawChart();
});
function drawChart() {
"use strict";
// income and expense over multi year:
columnChart('chart/report/in-out/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-chart');
columnChart('chart/report/in-out-sum/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-sum-chart');
$.each($('.account-chart'), function (i, v) {
var holder = $(v);
console.log('Will draw chart for account #' + holder.data('id'));
});
// draw budget chart based on selected budgets:
$('.budget-checkbox').on('change', updateBudgetChart);
selectBudgetsByCookie();
updateBudgetChart();
// draw category chart based on selected budgets:
$('.category-checkbox').on('change', updateCategoryChart);
selectCategoriesByCookie();
updateCategoryChart();
}
function selectBudgetsByCookie() {
"use strict";
var cookie = readCookie('multi-year-budgets');
if (cookie !== null) {
var cookieArray = cookie.split(',');
for (var x in cookieArray) {
var budgetId = cookieArray[x];
$('.budget-checkbox[value="' + budgetId + '"').prop('checked', true);
}
}
}
function selectCategoriesByCookie() {
"use strict";
var cookie = readCookie('multi-year-categories');
if (cookie !== null) {
var cookieArray = cookie.split(',');
for (var x in cookieArray) {
var categoryId = cookieArray[x];
$('.category-checkbox[value="' + categoryId + '"').prop('checked', true);
}
}
}
function updateBudgetChart() {
"use strict";
console.log('will update budget chart.');
// get all budget ids:
var budgets = [];
$.each($('.budget-checkbox'), function (i, v) {
var current = $(v);
if (current.prop('checked')) {
budgets.push(current.val());
}
});
if (budgets.length > 0) {
var budgetIds = budgets.join(',');
// remove old chart:
$('#budgets-chart').replaceWith('<canvas id="budgets-chart" class="budgets-chart" style="width:100%;height:400px;"></canvas>');
// hide message:
$('#budgets-chart-message').hide();
// draw chart. Redraw when exists? Not sure if we support that.
columnChart('chart/budget/multi-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds + '/' + budgetIds, 'budgets-chart');
createCookie('multi-year-budgets', budgets, 365);
} else {
// hide canvas, show message:
$('#budgets-chart-message').show();
$('#budgets-chart').hide();
}
}
function updateCategoryChart() {
"use strict";
console.log('will update category chart.');
// get all category ids:
var categories = [];
$.each($('.category-checkbox'), function (i, v) {
var current = $(v);
if (current.prop('checked')) {
categories.push(current.val());
}
});
if (categories.length > 0) {
var categoryIds = categories.join(',');
// remove old chart:
$('#categories-chart').replaceWith('<canvas id="categories-chart" class="budgets-chart" style="width:100%;height:400px;"></canvas>');
// hide message:
$('#categories-chart-message').hide();
// draw chart. Redraw when exists? Not sure if we support that.
columnChart('chart/category/multi-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds + '/' + categoryIds, 'categories-chart');
createCookie('multi-year-categories', categories, 365);
} else {
// hide canvas, show message:
$('#categories-chart-message').show();
$('#categories-chart').hide();
}
}
function createCookie(name, value, days) {
"use strict";
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
} else {
expires = "";
}
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
}
function readCookie(name) {
"use strict";
var nameEQ = encodeURIComponent(name) + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}
function eraseCookie(name) {
createCookie(name, "", -1);
}

View File

@ -0,0 +1,212 @@
/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, expenseRestShow:true, incomeRestShow:true, year, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
drawChart();
if ($('#inputDateRange').length > 0) {
picker = $('#inputDateRange').daterangepicker(
{
locale: {
format: 'YYYY-MM-DD',
firstDay: 1,
},
minDate: minDate,
drops: 'up',
}
);
// set values from cookies, if any:
if (readCookie('report-type') !== null) {
$('select[name="report_type"]').val(readCookie('report-type'));
}
if ((readCookie('report-accounts') !== null)) {
var arr = readCookie('report-accounts').split(',');
arr.forEach(function (val) {
$('input[type="checkbox"][value="' + val + '"]').prop('checked', true);
});
}
// set date:
var startStr = readCookie('report-start');
var endStr = readCookie('report-end');
if (startStr !== null && endStr !== null && startStr.length == 8 && endStr.length == 8) {
var startDate = moment(startStr, "YYYYMMDD");
var endDate = moment(endStr, "YYYYMMDD");
var datePicker = $('#inputDateRange').data('daterangepicker');
datePicker.setStartDate(startDate);
datePicker.setEndDate(endDate);
}
}
$('.openModal').on('click', openModal);
$('.date-select').on('click', preSelectDate);
$('#report-form').on('submit', catchSubmit);
// click open the top X income list:
$('#showIncomes').click(showIncomes);
// click open the top X expense list:
$('#showExpenses').click(showExpenses);
});
function catchSubmit() {
"use strict";
// default;20141201;20141231;4;5
// report name:
var url = '' + $('select[name="report_type"]').val() + '/';
// date, processed:
var picker = $('#inputDateRange').data('daterangepicker');
url += moment(picker.startDate).format("YYYYMMDD") + '/';
url += moment(picker.endDate).format("YYYYMMDD") + '/';
// all account ids:
var count = 0;
var accounts = [];
$.each($('.account-checkbox'), function (i, v) {
var c = $(v);
if (c.prop('checked')) {
url += c.val() + ';';
accounts.push(c.val());
count++;
}
});
if (count > 0) {
// set cookie to remember choices.
createCookie('report-type', $('select[name="report_type"]').val(), 365);
createCookie('report-accounts', accounts, 365);
createCookie('report-start', moment(picker.startDate).format("YYYYMMDD"), 365);
createCookie('report-end', moment(picker.endDate).format("YYYYMMDD"), 365);
window.location.href = reportURL + "/" + url;
}
//console.log(url);
return false;
}
function preSelectDate(e) {
"use strict";
var link = $(e.target);
var picker = $('#inputDateRange').data('daterangepicker');
picker.setStartDate(link.data('start'));
picker.setEndDate(link.data('end'));
return false;
}
function drawChart() {
"use strict";
if (typeof columnChart !== 'undefined' && typeof year !== 'undefined' && typeof month === 'undefined') {
columnChart('chart/report/in-out/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-chart');
columnChart('chart/report/in-out-sum/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-sum-chart');
}
if (typeof stackedColumnChart !== 'undefined' && typeof year !== 'undefined' && typeof month === 'undefined') {
stackedColumnChart('chart/budget/year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'budgets');
stackedColumnChart('chart/category/spent-in-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'categories-spent-in-year');
stackedColumnChart('chart/category/earned-in-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'categories-earned-in-year');
}
//if (typeof lineChart !== 'undefined' && typeof month !== 'undefined' && typeof reportURL === 'undefined') {
// lineChart('/chart/account/month/' + year + '/' + month + shared, 'account-balances-chart');
//}
if (typeof lineChart !== 'undefined' && typeof accountIds !== 'undefined') {
lineChart('/chart/account/report/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'account-balances-chart');
}
}
function openModal(e) {
"use strict";
var target = $(e.target).parent();
var URL = target.attr('href');
$.get(URL).success(function (data) {
$('#defaultModal').empty().html(data).modal('show');
}).fail(function () {
alert('Could not load data.');
});
return false;
}
function showIncomes() {
"use strict";
if (incomeRestShow) {
// hide everything, make button say "show"
$('#showIncomes').text(showTheRest);
$('.incomesCollapsed').removeClass('in').addClass('out');
// toggle:
incomeRestShow = false;
} else {
// show everything, make button say "hide".
$('#showIncomes').text(hideTheRest);
$('.incomesCollapsed').removeClass('out').addClass('in');
// toggle:
incomeRestShow = true;
}
return false;
}
function showExpenses() {
"use strict";
if (expenseRestShow) {
// hide everything, make button say "show"
$('#showExpenses').text(showTheRestExpense);
$('.expenseCollapsed').removeClass('in').addClass('out');
// toggle:
expenseRestShow = false;
} else {
// show everything, make button say "hide".
$('#showExpenses').text(hideTheRestExpense);
$('.expenseCollapsed').removeClass('out').addClass('in');
// toggle:
expenseRestShow = true;
}
return false;
}
function createCookie(name, value, days) {
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
} else {
expires = "";
}
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
}
function readCookie(name) {
var nameEQ = encodeURIComponent(name) + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}
function eraseCookie(name) {
createCookie(name, "", -1);
}

View File

@ -0,0 +1,67 @@
/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, expenseRestShow:true, incomeRestShow:true, year, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
drawChart();
// click open the top X income list:
$('#showIncomes').click(showIncomes);
// click open the top X expense list:
$('#showExpenses').click(showExpenses);
});
function drawChart() {
"use strict";
columnChart('chart/report/in-out/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-chart');
columnChart('chart/report/in-out-sum/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'income-expenses-sum-chart');
stackedColumnChart('chart/budget/year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'budgets');
stackedColumnChart('chart/category/spent-in-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'categories-spent-in-year');
stackedColumnChart('chart/category/earned-in-year/' + reportType + '/' + startDate + '/' + endDate + '/' + accountIds, 'categories-earned-in-year');
}
function showIncomes() {
"use strict";
if (incomeRestShow) {
// hide everything, make button say "show"
$('#showIncomes').text(showTheRest);
$('.incomesCollapsed').removeClass('in').addClass('out');
// toggle:
incomeRestShow = false;
} else {
// show everything, make button say "hide".
$('#showIncomes').text(hideTheRest);
$('.incomesCollapsed').removeClass('out').addClass('in');
// toggle:
incomeRestShow = true;
}
return false;
}
function showExpenses() {
"use strict";
if (expenseRestShow) {
// hide everything, make button say "show"
$('#showExpenses').text(showTheRestExpense);
$('.expenseCollapsed').removeClass('in').addClass('out');
// toggle:
expenseRestShow = false;
} else {
// show everything, make button say "hide".
$('#showExpenses').text(hideTheRestExpense);
$('.expenseCollapsed').removeClass('out').addClass('in');
// toggle:
expenseRestShow = true;
}
return false;
}

122
public/js/reports/index.js Normal file
View File

@ -0,0 +1,122 @@
/* globals google, startDate ,reportURL, endDate , reportType ,accountIds , picker:true, minDate, expenseRestShow:true, incomeRestShow:true, year, month, hideTheRest, showTheRest, showTheRestExpense, hideTheRestExpense, columnChart, lineChart, stackedColumnChart */
$(function () {
"use strict";
if ($('#inputDateRange').length > 0) {
picker = $('#inputDateRange').daterangepicker(
{
locale: {
format: 'YYYY-MM-DD',
firstDay: 1,
},
minDate: minDate,
drops: 'up',
}
);
// set values from cookies, if any:
if (readCookie('report-type') !== null) {
$('select[name="report_type"]').val(readCookie('report-type'));
}
if ((readCookie('report-accounts') !== null)) {
var arr = readCookie('report-accounts').split(',');
arr.forEach(function (val) {
$('input[type="checkbox"][value="' + val + '"]').prop('checked', true);
});
}
// set date:
var startStr = readCookie('report-start');
var endStr = readCookie('report-end');
if (startStr !== null && endStr !== null && startStr.length == 8 && endStr.length == 8) {
var startDate = moment(startStr, "YYYYMMDD");
var endDate = moment(endStr, "YYYYMMDD");
var datePicker = $('#inputDateRange').data('daterangepicker');
datePicker.setStartDate(startDate);
datePicker.setEndDate(endDate);
}
}
$('.date-select').on('click', preSelectDate);
$('#report-form').on('submit', catchSubmit);
});
function catchSubmit() {
"use strict";
// default;20141201;20141231;4;5
// report name:
var url = '' + $('select[name="report_type"]').val() + '/';
// date, processed:
var picker = $('#inputDateRange').data('daterangepicker');
url += moment(picker.startDate).format("YYYYMMDD") + '/';
url += moment(picker.endDate).format("YYYYMMDD") + '/';
// all account ids:
var count = 0;
var accounts = [];
$.each($('.account-checkbox'), function (i, v) {
var c = $(v);
if (c.prop('checked')) {
url += c.val() + ',';
accounts.push(c.val());
count++;
}
});
if (count > 0) {
// set cookie to remember choices.
createCookie('report-type', $('select[name="report_type"]').val(), 365);
createCookie('report-accounts', accounts, 365);
createCookie('report-start', moment(picker.startDate).format("YYYYMMDD"), 365);
createCookie('report-end', moment(picker.endDate).format("YYYYMMDD"), 365);
window.location.href = reportURL + "/" + url;
}
//console.log(url);
return false;
}
function preSelectDate(e) {
"use strict";
var link = $(e.target);
var picker = $('#inputDateRange').data('daterangepicker');
picker.setStartDate(link.data('start'));
picker.setEndDate(link.data('end'));
return false;
}
function createCookie(name, value, days) {
"use strict";
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
} else {
expires = "";
}
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
}
function readCookie(name) {
"use strict";
var nameEQ = encodeURIComponent(name) + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}

View File

@ -250,35 +250,27 @@ return [
'details_for_expense' => 'Details for expense account ":name"', 'details_for_expense' => 'Details for expense account ":name"',
'details_for_revenue' => 'Details for revenue account ":name"', 'details_for_revenue' => 'Details for revenue account ":name"',
'details_for_cash' => 'Details for cash account ":name"', 'details_for_cash' => 'Details for cash account ":name"',
'store_new_asset_account' => 'Store new asset account', 'store_new_asset_account' => 'Store new asset account',
'store_new_expense_account' => 'Store new expense account', 'store_new_expense_account' => 'Store new expense account',
'store_new_revenue_account' => 'Store new revenue account', 'store_new_revenue_account' => 'Store new revenue account',
'edit_asset_account' => 'Edit asset account ":name"', 'edit_asset_account' => 'Edit asset account ":name"',
'edit_expense_account' => 'Edit expense account ":name"', 'edit_expense_account' => 'Edit expense account ":name"',
'edit_revenue_account' => 'Edit revenue account ":name"', 'edit_revenue_account' => 'Edit revenue account ":name"',
'delete_asset_account' => 'Delete asset account ":name"', 'delete_asset_account' => 'Delete asset account ":name"',
'delete_expense_account' => 'Delete expense account ":name"', 'delete_expense_account' => 'Delete expense account ":name"',
'delete_revenue_account' => 'Delete revenue account ":name"', 'delete_revenue_account' => 'Delete revenue account ":name"',
'asset_deleted' => 'Successfully deleted asset account ":name"', 'asset_deleted' => 'Successfully deleted asset account ":name"',
'expense_deleted' => 'Successfully deleted expense account ":name"', 'expense_deleted' => 'Successfully deleted expense account ":name"',
'revenue_deleted' => 'Successfully deleted revenue account ":name"', 'revenue_deleted' => 'Successfully deleted revenue account ":name"',
'update_asset_account' => 'Update asset account', 'update_asset_account' => 'Update asset account',
'update_expense_account' => 'Update expense account', 'update_expense_account' => 'Update expense account',
'update_revenue_account' => 'Update revenue account', 'update_revenue_account' => 'Update revenue account',
'make_new_asset_account' => 'Create a new asset account', 'make_new_asset_account' => 'Create a new asset account',
'make_new_expense_account' => 'Create a new expense account', 'make_new_expense_account' => 'Create a new expense account',
'make_new_revenue_account' => 'Create a new revenue account', 'make_new_revenue_account' => 'Create a new revenue account',
'asset_accounts' => 'Asset accounts', 'asset_accounts' => 'Asset accounts',
'expense_accounts' => 'Expense accounts', 'expense_accounts' => 'Expense accounts',
'revenue_accounts' => 'Revenue accounts', 'revenue_accounts' => 'Revenue accounts',
'accountExtraHelp_asset' => '', 'accountExtraHelp_asset' => '',
'accountExtraHelp_expense' => '', 'accountExtraHelp_expense' => '',
'accountExtraHelp_revenue' => '', 'accountExtraHelp_revenue' => '',
@ -341,6 +333,7 @@ return [
'Default account' => 'Asset account', 'Default account' => 'Asset account',
'Expense account' => 'Expense account', 'Expense account' => 'Expense account',
'Revenue account' => 'Revenue account', 'Revenue account' => 'Revenue account',
'Initial balance account' => 'Initial balance account',
'budgets' => 'Budgets', 'budgets' => 'Budgets',
'tags' => 'Tags', 'tags' => 'Tags',
'reports' => 'Reports', 'reports' => 'Reports',
@ -376,10 +369,13 @@ return [
'profile' => 'Profile', 'profile' => 'Profile',
// reports: // reports:
'reportForYear' => 'Yearly report for :year', 'report_default' => 'Default financial report for :start until :end',
'reportForYearShared' => 'Yearly report for :year (including shared accounts)', 'quick_link_reports' => 'Quick links',
'reportForMonth' => 'Montly report for :month', 'quick_link_default_report' => 'Default financial report',
'reportForMonthShared' => 'Montly report for :month (including shared accounts)', 'report_this_month_quick' => 'Current month, all accounts',
'report_this_year_quick' => 'Current year, all accounts',
'report_all_time_quick' => 'All-time, all accounts',
'reports_can_bookmark' => 'Remember that reports can be bookmarked.',
'incomeVsExpenses' => 'Income vs. expenses', 'incomeVsExpenses' => 'Income vs. expenses',
'accountBalances' => 'Account balances', 'accountBalances' => 'Account balances',
'balanceStartOfYear' => 'Balance at start of year', 'balanceStartOfYear' => 'Balance at start of year',
@ -398,6 +394,7 @@ return [
'outsideOfBudgets' => 'Outside of budgets', 'outsideOfBudgets' => 'Outside of budgets',
'leftInBudget' => 'Left in budget', 'leftInBudget' => 'Left in budget',
'sumOfSums' => 'Sum of sums', 'sumOfSums' => 'Sum of sums',
'noCategory' => '(no category)',
'notCharged' => 'Not charged (yet)', 'notCharged' => 'Not charged (yet)',
'inactive' => 'Inactive', 'inactive' => 'Inactive',
'difference' => 'Difference', 'difference' => 'Difference',
@ -407,9 +404,18 @@ return [
'showTheRest' => 'Show everything', 'showTheRest' => 'Show everything',
'hideTheRest' => 'Show only the top :number', 'hideTheRest' => 'Show only the top :number',
'sum_of_year' => 'Sum of year', 'sum_of_year' => 'Sum of year',
'sum_of_years' => 'Sum of years',
'average_of_year' => 'Average of year', 'average_of_year' => 'Average of year',
'average_of_years' => 'Average of years',
'categories_earned_in_year' => 'Categories (by earnings)', 'categories_earned_in_year' => 'Categories (by earnings)',
'categories_spent_in_year' => 'Categories (by spendings)', 'categories_spent_in_year' => 'Categories (by spendings)',
'report_type' => 'Report type',
'report_type_default' => 'Default financial report',
'report_included_accounts' => 'Included accounts',
'report_date_range' => 'Date range',
'report_include_help' => 'In all cases, transfers to shared accounts count as expenses, and transfers from shared accounts count as income.',
'report_preset_ranges' => 'Pre-set ranges',
'shared' => 'Shared',
// charts: // charts:
'dayOfMonth' => 'Day of the month', 'dayOfMonth' => 'Day of the month',

View File

@ -17,5 +17,6 @@ return [
"token" => "This password reset token is invalid.", "token" => "This password reset token is invalid.",
"sent" => "We have e-mailed your password reset link!", "sent" => "We have e-mailed your password reset link!",
"reset" => "Your password has been reset!", "reset" => "Your password has been reset!",
'blocked' => 'Nice try though.'
]; ];

View File

@ -13,6 +13,7 @@ return [
| |
*/ */
'invalid_domain' => 'Due to security constraints, you cannot register from this domain.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.', 'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Succesfully uploaded file ":name".', 'file_attached' => 'Succesfully uploaded file ":name".',
'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.', 'file_invalid_mime' => 'File ":name" is of type ":mime" which is not accepted as a new upload.',
@ -28,12 +29,10 @@ return [
"before" => "The :attribute must be a date before :date.", "before" => "The :attribute must be a date before :date.",
'unique_object_for_user' => 'This name is already in use', 'unique_object_for_user' => 'This name is already in use',
'unique_account_for_user' => 'This account name is already in use', 'unique_account_for_user' => 'This account name is already in use',
"between" => [ "between.numeric" => "The :attribute must be between :min and :max.",
"numeric" => "The :attribute must be between :min and :max.", "between.file" => "The :attribute must be between :min and :max kilobytes.",
"file" => "The :attribute must be between :min and :max kilobytes.", "between.string" => "The :attribute must be between :min and :max characters.",
"string" => "The :attribute must be between :min and :max characters.", "between.array" => "The :attribute must have between :min and :max items.",
"array" => "The :attribute must have between :min and :max items.",
],
"boolean" => "The :attribute field must be true or false.", "boolean" => "The :attribute field must be true or false.",
"confirmed" => "The :attribute confirmation does not match.", "confirmed" => "The :attribute confirmation does not match.",
"date" => "The :attribute is not a valid date.", "date" => "The :attribute is not a valid date.",
@ -48,19 +47,15 @@ return [
"in" => "The selected :attribute is invalid.", "in" => "The selected :attribute is invalid.",
"integer" => "The :attribute must be an integer.", "integer" => "The :attribute must be an integer.",
"ip" => "The :attribute must be a valid IP address.", "ip" => "The :attribute must be a valid IP address.",
"max" => [ "max.numeric" => "The :attribute may not be greater than :max.",
"numeric" => "The :attribute may not be greater than :max.", "max.file" => "The :attribute may not be greater than :max kilobytes.",
"file" => "The :attribute may not be greater than :max kilobytes.", "max.string" => "The :attribute may not be greater than :max characters.",
"string" => "The :attribute may not be greater than :max characters.", "max.array" => "The :attribute may not have more than :max items.",
"array" => "The :attribute may not have more than :max items.",
],
"mimes" => "The :attribute must be a file of type: :values.", "mimes" => "The :attribute must be a file of type: :values.",
"min" => [ "min.numeric" => "The :attribute must be at least :min.",
"numeric" => "The :attribute must be at least :min.", "min.file" => "The :attribute must be at least :min kilobytes.",
"file" => "The :attribute must be at least :min kilobytes.", "min.string" => "The :attribute must be at least :min characters.",
"string" => "The :attribute must be at least :min characters.", "min.array" => "The :attribute must have at least :min items.",
"array" => "The :attribute must have at least :min items.",
],
"not_in" => "The selected :attribute is invalid.", "not_in" => "The selected :attribute is invalid.",
"numeric" => "The :attribute must be a number.", "numeric" => "The :attribute must be a number.",
"regex" => "The :attribute format is invalid.", "regex" => "The :attribute format is invalid.",
@ -71,44 +66,13 @@ return [
"required_without" => "The :attribute field is required when :values is not present.", "required_without" => "The :attribute field is required when :values is not present.",
"required_without_all" => "The :attribute field is required when none of :values are present.", "required_without_all" => "The :attribute field is required when none of :values are present.",
"same" => "The :attribute and :other must match.", "same" => "The :attribute and :other must match.",
"size" => [ "size.numeric" => "The :attribute must be :size.",
"numeric" => "The :attribute must be :size.", "size.file" => "The :attribute must be :size kilobytes.",
"file" => "The :attribute must be :size kilobytes.", "size.string" => "The :attribute must be :size characters.",
"string" => "The :attribute must be :size characters.", "size.array" => "The :attribute must contain :size items.",
"array" => "The :attribute must contain :size items.",
],
"unique" => "The :attribute has already been taken.", "unique" => "The :attribute has already been taken.",
"url" => "The :attribute format is invalid.", "url" => "The :attribute format is invalid.",
"timezone" => "The :attribute must be a valid zone.", "timezone" => "The :attribute must be a valid zone.",
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [], 'attributes' => [],
]; ];

View File

@ -352,6 +352,7 @@ return [
'Default account' => 'Betaalrekening', 'Default account' => 'Betaalrekening',
'Expense account' => 'Crediteur', 'Expense account' => 'Crediteur',
'Revenue account' => 'Debiteur', 'Revenue account' => 'Debiteur',
'Initial balance account' => 'Startbalansrekening',
'budgets' => 'Budgetten', 'budgets' => 'Budgetten',
'tags' => 'Tags', 'tags' => 'Tags',
'reports' => 'Overzichten', 'reports' => 'Overzichten',
@ -387,12 +388,17 @@ return [
'yearly' => 'Jaarlijks', 'yearly' => 'Jaarlijks',
'sum_of_year' => 'Som van jaar', 'sum_of_year' => 'Som van jaar',
'average_of_year' => 'Gemiddelde in jaar', 'average_of_year' => 'Gemiddelde in jaar',
'sum_of_years' => 'Som van jaren',
'average_of_years' => 'Gemiddelde in jaren',
// reports: // reports:
'reportForYear' => 'Jaaroverzicht :year', 'report_default' => 'Standaard financieel rapport (:start tot :end)',
'reportForYearShared' => 'Jaaroverzicht :year (inclusief gedeelde rekeningen)', 'quick_link_reports' => 'Snelle links',
'reportForMonth' => 'Maandoverzicht voor :month', 'quick_link_default_report' => 'Standaard financieel rapport',
'reportForMonthShared' => 'Maandoverzicht voor :month (inclusief gedeelde rekeningen)', 'report_this_month_quick' => 'Deze maand, alle rekeningen',
'report_this_year_quick' => 'Dit jaar, alle rekeningen',
'report_all_time_quick' => 'Gehele periode, alle rekeningen',
'reports_can_bookmark' => 'Je kan rapporten aan je favorieten toevoegen.',
'incomeVsExpenses' => 'Inkomsten tegenover uitgaven', 'incomeVsExpenses' => 'Inkomsten tegenover uitgaven',
'accountBalances' => 'Rekeningsaldi', 'accountBalances' => 'Rekeningsaldi',
'balanceStartOfYear' => 'Saldo aan het begin van het jaar', 'balanceStartOfYear' => 'Saldo aan het begin van het jaar',
@ -411,6 +417,7 @@ return [
'outsideOfBudgets' => 'Buiten budgetten', 'outsideOfBudgets' => 'Buiten budgetten',
'leftInBudget' => 'Over van budget', 'leftInBudget' => 'Over van budget',
'sumOfSums' => 'Alles bij elkaar', 'sumOfSums' => 'Alles bij elkaar',
'noCategory' => '(zonder categorie)',
'notCharged' => '(Nog) niet betaald', 'notCharged' => '(Nog) niet betaald',
'inactive' => 'Niet actief', 'inactive' => 'Niet actief',
'difference' => 'Verschil', 'difference' => 'Verschil',
@ -421,6 +428,14 @@ return [
'hideTheRest' => 'Laat alleen de top :number zien', 'hideTheRest' => 'Laat alleen de top :number zien',
'categories_earned_in_year' => 'Categorieën (inkomsten)', 'categories_earned_in_year' => 'Categorieën (inkomsten)',
'categories_spent_in_year' => 'Categorieën (uitgaven)', 'categories_spent_in_year' => 'Categorieën (uitgaven)',
'report_type' => 'Rapporttype',
'report_type_default' => 'Standard financieel rapport',
'report_included_accounts' => 'Accounts in rapport',
'report_date_range' => 'Datumbereik',
'report_include_help' => 'Overboekingen naar gedeelde rekeningen tellen als uitgave. Overboekingen van gedeelde rekeningen tellen als inkomsten.',
'report_preset_ranges' => 'Standaardbereik',
'shared' => 'Gedeeld',
// charts: // charts:
'dayOfMonth' => 'Dag vd maand', 'dayOfMonth' => 'Dag vd maand',

View File

@ -17,5 +17,6 @@ return [
"token" => "Ongeldig token! Sorry", "token" => "Ongeldig token! Sorry",
"sent" => "Je krijgt een mailtje met een linkje om je wachtwoord te herstellen!", "sent" => "Je krijgt een mailtje met een linkje om je wachtwoord te herstellen!",
"reset" => "Je wachtwoord is hersteld!", "reset" => "Je wachtwoord is hersteld!",
'blocked' => 'Leuk geprobeerd wel.'
]; ];

View File

@ -13,6 +13,7 @@ return [
| |
*/ */
'invalid_domain' => 'Kan niet registereren vanaf dit domein.',
'file_already_attached' => 'Het geuploade bestand ":name" is al gelinkt aan deze transactie.', 'file_already_attached' => 'Het geuploade bestand ":name" is al gelinkt aan deze transactie.',
'file_attached' => 'Bestand met naam ":name" is met succes geuploaded.', 'file_attached' => 'Bestand met naam ":name" is met succes geuploaded.',
'file_invalid_mime' => 'Bestand ":name" is van het type ":mime", en die kan je niet uploaden.', 'file_invalid_mime' => 'Bestand ":name" is van het type ":mime", en die kan je niet uploaden.',
@ -28,12 +29,10 @@ return [
"before" => "The :attribute must be a date before :date.", "before" => "The :attribute must be a date before :date.",
'unique_object_for_user' => 'Deze naam is al in gebruik', 'unique_object_for_user' => 'Deze naam is al in gebruik',
'unique_account_for_user' => 'This rekeningnaam is already in use', 'unique_account_for_user' => 'This rekeningnaam is already in use',
"between" => [ "between.numeric" => "The :attribute must be between :min and :max.",
"numeric" => "The :attribute must be between :min and :max.", "between.file" => "The :attribute must be between :min and :max kilobytes.",
"file" => "The :attribute must be between :min and :max kilobytes.", "between.string" => "The :attribute must be between :min and :max characters.",
"string" => "The :attribute must be between :min and :max characters.", "between.array" => "The :attribute must have between :min and :max items.",
"array" => "The :attribute must have between :min and :max items.",
],
"boolean" => "The :attribute field must be true or false.", "boolean" => "The :attribute field must be true or false.",
"confirmed" => "The :attribute confirmation does not match.", "confirmed" => "The :attribute confirmation does not match.",
"date" => "The :attribute is not a valid date.", "date" => "The :attribute is not a valid date.",
@ -48,19 +47,15 @@ return [
"in" => "The selected :attribute is invalid.", "in" => "The selected :attribute is invalid.",
"integer" => "The :attribute must be an integer.", "integer" => "The :attribute must be an integer.",
"ip" => "The :attribute must be a valid IP address.", "ip" => "The :attribute must be a valid IP address.",
"max" => [ "max.numeric" => "The :attribute may not be greater than :max.",
"numeric" => "The :attribute may not be greater than :max.", "max.file" => "The :attribute may not be greater than :max kilobytes.",
"file" => "The :attribute may not be greater than :max kilobytes.", "max.string" => "The :attribute may not be greater than :max characters.",
"string" => "The :attribute may not be greater than :max characters.", "max.array" => "The :attribute may not have more than :max items.",
"array" => "The :attribute may not have more than :max items.",
],
"mimes" => "The :attribute must be a file of type: :values.", "mimes" => "The :attribute must be a file of type: :values.",
"min" => [ "min.numeric" => "The :attribute must be at least :min.",
"numeric" => "The :attribute must be at least :min.", "min.file" => "The :attribute must be at least :min kilobytes.",
"file" => "The :attribute must be at least :min kilobytes.", "min.string" => "The :attribute must be at least :min characters.",
"string" => "The :attribute must be at least :min characters.", "min.array" => "The :attribute must have at least :min items.",
"array" => "The :attribute must have at least :min items.",
],
"not_in" => "The selected :attribute is invalid.", "not_in" => "The selected :attribute is invalid.",
"numeric" => "The :attribute must be a number.", "numeric" => "The :attribute must be a number.",
"regex" => "The :attribute format is invalid.", "regex" => "The :attribute format is invalid.",
@ -71,44 +66,13 @@ return [
"required_without" => "The :attribute field is required when :values is not present.", "required_without" => "The :attribute field is required when :values is not present.",
"required_without_all" => "The :attribute field is required when none of :values are present.", "required_without_all" => "The :attribute field is required when none of :values are present.",
"same" => "The :attribute and :other must match.", "same" => "The :attribute and :other must match.",
"size" => [ "size.numeric" => "The :attribute must be :size.",
"numeric" => "The :attribute must be :size.", "size.file" => "The :attribute must be :size kilobytes.",
"file" => "The :attribute must be :size kilobytes.", "size.string" => "The :attribute must be :size characters.",
"string" => "The :attribute must be :size characters.", "size.array" => "The :attribute must contain :size items.",
"array" => "The :attribute must contain :size items.",
],
"unique" => "The :attribute has already been taken.", "unique" => "The :attribute has already been taken.",
"url" => "The :attribute format is invalid.", "url" => "The :attribute format is invalid.",
"timezone" => "The :attribute must be a valid zone.", "timezone" => "The :attribute must be a valid zone.",
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [], 'attributes' => [],
]; ];

View File

@ -17,16 +17,20 @@
<div class="register-box-body"> <div class="register-box-body">
<p class="login-box-msg">Register a new account</p> <p class="login-box-msg">Register a new account</p>
{% if host == 'geld.nder.be' %} {% if host == 'geld.nder.be' or host == 'firefly.app' %}
<p class="text-info login-box-msg">Please note that an account on this site will only <p class="text-info login-box-msg">Please note that an account on this site will only
work for one (1) week.</p> work for one (1) month.</p>
{% endif %} {% endif %}
<form role="form" id="register" method="POST" action="{{ URL.to('/auth/register') }}"> <form role="form" id="register" method="POST" action="{{ URL.to('/auth/register') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group has-feedback"> <div class="form-group has-feedback">
<input type="email" name="email" class="form-control" placeholder="Email"/> <input type="email" name="email" class="form-control" placeholder="Email" />
{% if host == 'geld.nder.be' or host == 'firefly.app' %}
<p class="help-block">You will receive an email from Firefly III. If your email address
is incorrect, your account may not work.</p>
{% endif %}
</div> </div>
<div class="form-group has-feedback"> <div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Password" name="password"/> <input type="password" class="form-control" placeholder="Password" name="password"/>

View File

@ -43,7 +43,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-4 col-sm-3"> <div class="col-lg-6 col-md-4 col-sm-3">
<small>{{ 'spent'|_ }}: <span class="text-danger">{{ (spent*-1)|formatAmountPlain }}</span></small> <small>{{ 'spent'|_ }}: {{ spent|formatAmount }}</small>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -144,7 +144,7 @@
</tr> </tr>
<tr> <tr>
<td style="width:50%;">{{ 'spent'|_ }}</td> <td style="width:50%;">{{ 'spent'|_ }}</td>
<td><span class="text-danger">{{ (budget.spent*-1)|formatAmountPlain }}</a></td> <td>{{ budget.spent|formatAmount }}</a></td>
</tr> </tr>
</table> </table>
</div> </div>
@ -179,7 +179,7 @@
{% block scripts %} {% block scripts %}
<script type="text/javascript"> <script type="text/javascript">
// actually spent bar data: // actually spent bar data:
var spent = {{ spent * -1 }}; var spent = {{ spent * -1 }}; // must be positive for the calculation to work.
var currencySymbol = "{{ getCurrencySymbol()|raw }}"; var currencySymbol = "{{ getCurrencySymbol()|raw }}";
// budgeted data: // budgeted data:

View File

@ -50,7 +50,7 @@
{% for limit in limits %} {% for limit in limits %}
{% for rep in limit.limitRepetitions %} {% for rep in limit.limitRepetitions %}
{% set spentInRep = (spentInRepetitionCorrected(rep)*-1) %} {% set spentInRep = spentInRepetition(rep) %}
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title"><a href="{{ route('budgets.show',[budget.id,rep.id]) }}">{{ rep.startdate.formatLocalized(monthFormat) }}</a> <h3 class="box-title"><a href="{{ route('budgets.show',[budget.id,rep.id]) }}">{{ rep.startdate.formatLocalized(monthFormat) }}</a>
@ -62,15 +62,15 @@
{{ 'amount'|_ }}: {{ rep.amount|formatAmount }} {{ 'amount'|_ }}: {{ rep.amount|formatAmount }}
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-6"> <div class="col-lg-6 col-md-6 col-sm-6">
{{ 'spent'|_ }}: <span class="text-danger">{{ spentInRep|formatAmountPlain }}</span> {{ 'spent'|_ }}: {{ spentInRep|formatAmount }}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="col-lg-12 col-md-12 col-sm-12">
{% set overspent = spentInRep > rep.amount %} {% set overspent = rep.amount + spentInRep < 0 %}
{% if overspent %} {% if overspent %}
{% set pct = (spentInRep != 0 ? (rep.amount / spentInRep)*100 : 0) %} {% set pct = (spentInRep != 0 ? (rep.amount / (spentInRep*-1))*100 : 0) %} <!-- must have -1 here -->
<div class="progress progress-striped"> <div class="progress progress-striped">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{ pct|round }}" aria-valuemin="0" <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{ pct|round }}" aria-valuemin="0"
aria-valuemax="100" style="width: {{ pct|round }}%;"></div> aria-valuemax="100" style="width: {{ pct|round }}%;"></div>
@ -79,7 +79,7 @@
</div> </div>
{% else %} {% else %}
{% set amount = rep.amount %} {% set amount = rep.amount %}
{% set pct = (amount != 0 ? ((spentInRep / amount)*100) : 0) %} {% set pct = (amount != 0 ? (((spentInRep*-1) / amount)*100) : 0) %} <!-- must have -1 here -->
<div class="progress progress-striped"> <div class="progress progress-striped">
<div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="{{ pct|round }}" aria-valuemin="0" <div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="{{ pct|round }}" aria-valuemin="0"
aria-valuemax="100" style="width: {{ pct|round }}%;"></div> aria-valuemax="100" style="width: {{ pct|round }}%;"></div>

View File

@ -36,6 +36,10 @@
Enjoy! Enjoy!
</p> </p>
<p style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:11px;color:#aaa;">
The registration has been created from IP {{ ip }}
</p>
<script type="application/ld+json"> <script type="application/ld+json">
{ {

View File

@ -17,3 +17,5 @@ Password reset:
Documentation: Documentation:
https://github.com/JC5/firefly-iii/wiki/First-use https://github.com/JC5/firefly-iii/wiki/First-use
https://github.com/JC5/firefly-iii/wiki/full-description https://github.com/JC5/firefly-iii/wiki/full-description
The registration has been created from IP {{ ip }}

View File

@ -6,7 +6,7 @@
<div class="popover-navigation"> <div class="popover-navigation">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-default" data-role="prev">&laquo; {{ 'prev'|_ }}</button> <button class="btn btn-sm btn-default" data-role="prev">&laquo; {{ 'prev'|_ }}</button>
<button class="btn btn-sm btn-default" data-role="next">{{ 'prev'|_ }} &raquo;</button> <button class="btn btn-sm btn-default" data-role="next">{{ 'next'|_ }} &raquo;</button>
<button class="btn btn-sm btn-default" data-role="pause-resume" data-pause-text="{{ 'pause'|_ }}" <button class="btn btn-sm btn-default" data-role="pause-resume" data-pause-text="{{ 'pause'|_ }}"
data-resume-text="Resume"> {{ 'pause'|_ }}</button> data-resume-text="Resume"> {{ 'pause'|_ }}</button>
</div> </div>

View File

@ -157,7 +157,7 @@
<script src="js/moment.min.js" type="text/javascript"></script> <script src="js/moment.min.js" type="text/javascript"></script>
<script src="js/daterangepicker.js" type="text/javascript"></script> <script src="js/daterangepicker.js" type="text/javascript"></script>
<script src="dist/js/app.min.js" type="text/javascript"></script> <script src="dist/js/app.min.js" type="text/javascript"></script>
<script type="text/javascript" src="js/accounting.min.js"></script>
<script src="js/bootstrap-tour.min.js" type="text/javascript"></script> <script src="js/bootstrap-tour.min.js" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">

View File

@ -24,10 +24,8 @@
<td> <td>
{% if event.amount < 0 %} {% if event.amount < 0 %}
<span class="text-danger">{{ trans('firefly.removed_amount', {amount: (event.amount)|formatAmountPlain})|raw }}</span>
<span class="text-danger">{{ trans('firefly.removed_amount', {amount: (event.amount*-1)|formatAmountPlain})|raw }}</span>
{% else %} {% else %}
<span class="text-success">{{ trans('firefly.added_amount', {amount: (event.amount)|formatAmountPlain})|raw }}</span> <span class="text-success">{{ trans('firefly.added_amount', {amount: (event.amount)|formatAmountPlain})|raw }}</span>
{% endif %} {% endif %}
</td> </td>

View File

@ -1,7 +1,7 @@
{% extends "./layout/default.twig" %} {% extends "./layout/default.twig" %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, start, shared) }} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, start, end, report_type, accountIds) }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -26,26 +26,26 @@
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-6 col-sm-6"> <div class="col-lg-6 col-md-6 col-sm-6">
{% include 'partials/reports/accounts.twig' %} {% include 'reports/partials/accounts.twig' %}
{% include 'partials/reports/income-vs-expenses.twig' %} {% include 'reports/partials/income-vs-expenses.twig' %}
</div> </div>
<div class="col-lg-3 col-md-3 col-sm-3"> <div class="col-lg-3 col-md-3 col-sm-3">
<!-- income --> <!-- income -->
{% include 'partials/reports/income.twig' %} {% include 'reports/partials/income.twig' %}
</div> </div>
<div class="col-lg-3 col-md-3 col-sm-3"> <div class="col-lg-3 col-md-3 col-sm-3">
<!-- expenses --> <!-- expenses -->
{% include 'partials/reports/expenses.twig' %} {% include 'reports/partials/expenses.twig' %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-8 col-md-8 col-sm-12"> <div class="col-lg-8 col-md-8 col-sm-12">
<!-- budgets --> <!-- budgets -->
{% include 'partials/reports/budgets.twig' %} {% include 'reports/partials/budgets.twig' %}
</div> </div>
<div class="col-lg-4 col-md-4 col-sm-12"> <div class="col-lg-4 col-md-4 col-sm-12">
<!-- categories --> <!-- categories -->
{% include 'partials/reports/categories.twig' %} {% include 'reports/partials/categories.twig' %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -55,12 +55,12 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="col-lg-12 col-md-12 col-sm-12">
{% include 'partials/reports/balance.twig' %} {% include 'reports/partials/balance.twig' %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="col-lg-12 col-md-12 col-sm-12">
{% include 'partials/reports/bills.twig' %} {% include 'reports/partials/bills.twig' %}
</div> </div>
</div> </div>
@ -85,8 +85,13 @@
<script type="text/javascript"> <script type="text/javascript">
var year = {{ start.year }}; var year = {{ start.year }};
var month = {{ start.month }}; var month = {{ start.month }};
var shared = {% if shared %}'/shared'
{% else %}''{% endif %}; // to report another URL:
var startDate = '{{ start.format('Ymd') }}';
var endDate = '{{ end.format('Ymd') }}';
var reportType = '{{ report_type }}';
var accountIds = '{{ accountIds }}';
var incomeTopLength = {{ incomeTopLength }}; var incomeTopLength = {{ incomeTopLength }};
var expenseTopLength = {{ expenseTopLength }}; var expenseTopLength = {{ expenseTopLength }};
var incomeRestShow = false; // starts hidden. var incomeRestShow = false; // starts hidden.
@ -96,5 +101,5 @@
var showTheRestExpense = '{{ trans('firefly.showTheRest',{number:expenseTopLength}) }}'; var showTheRestExpense = '{{ trans('firefly.showTheRest',{number:expenseTopLength}) }}';
var hideTheRestExpense = '{{ trans('firefly.hideTheRest',{number:expenseTopLength}) }}'; var hideTheRestExpense = '{{ trans('firefly.hideTheRest',{number:expenseTopLength}) }}';
</script> </script>
<script type="text/javascript" src="js/reports.js"></script> <script type="text/javascript" src="js/reports/default/month.js"></script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,141 @@
{% extends "./layout/default.twig" %}
{% block breadcrumbs %}
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, start, end, report_type, accountIds) }}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8 col-md-8 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="income-expenses-chart" style="width:100%;height:400px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-4 col-md-4 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="income-expenses-sum-chart" style="width:100%;height:400px;"></canvas>
</div>
</div>
</div>
</div>
{% for account in accounts %}
<div class="row" style="display:none;">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'Asset account'|_ }}: {{ account.name }}</h3>
</div>
<div class="box-body">
<canvas id="account-chart-{{ account.id }}" class="account-chart" data-id="{{ account.id }}" style="width:100%;height:400px;"></canvas>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Selected budgets</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-lg-12">
<p class="well" id="budgets-chart-message" style="display:none;">
Select one or more budgets to generate this chart. Insofar possible, your selection will be saved.
</p>
<canvas id="budgets-chart" class="budgets-chart" style="width:100%;height:400px;"></canvas>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<label class="checkbox-inline">
<input type="checkbox" class="budget-checkbox" name="budgets[]" value="0"> {{ 'noBudget'|_ }}
</label>
{% for budget in budgets %}
<label class="checkbox-inline">
<input type="checkbox" class="budget-checkbox" name="budgets[]" value="{{ budget.id }}"> {{ budget.name }}
</label>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Selected categories</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-lg-12">
<p class="well" id="categories-chart-message" style="display:none;">
Select one or more categories to generate this chart. Insofar possible, your selection will be saved.
</p>
<canvas id="categories-chart" class="categories-chart" style="width:100%;height:400px;"></canvas>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<label class="checkbox-inline">
<input type="checkbox" class="category-checkbox" name="categories[]" value="0"> {{ 'noCategory'|_ }}
</label>
{% for category in categories %}
<label class="checkbox-inline">
<input type="checkbox" name="categories[]" class="category-checkbox" value="{{ category.id }}"> {{ category.name }}
</label>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="css/bootstrap-sortable.css" type="text/css" media="all"/>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="js/bootstrap-sortable.js"></script>
<!-- load the libraries and scripts necessary for Google Charts: -->
{% if Config.get('firefly.chart') == 'google' %}
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript" src="js/gcharts.js"></script>
{% endif %}
{% if Config.get('firefly.chart') == 'chartjs' %}
<script type="text/javascript" src="js/Chart.min.js"></script>
<script type="text/javascript" src="js/charts.js"></script>
{% endif %}
<script type="text/javascript">
var year = {{ start.year }};
var month = {{ start.month }};
// to report another URL:
var startDate = '{{ start.format('Ymd') }}';
var endDate = '{{ end.format('Ymd') }}';
var reportType = '{{ report_type }}';
var accountIds = '{{ accountIds }}';
</script>
<script type="text/javascript" src="js/reports/default/multi-year.js"></script>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "./layout/default.twig" %} {% extends "./layout/default.twig" %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, start, shared) }} {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, start, end, report_type, accountIds) }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -13,12 +13,7 @@
<h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3> <h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
{% if Config.get('firefly.chart') == 'google' %}
<div id="income-expenses-chart"></div>
{% endif %}
{% if Config.get('firefly.chart') == 'chartjs' %}
<canvas id="income-expenses-chart" style="width:100%;height:400px;"></canvas> <canvas id="income-expenses-chart" style="width:100%;height:400px;"></canvas>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -28,12 +23,7 @@
<h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3> <h3 class="box-title">{{ 'incomeVsExpenses'|_ }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
{% if Config.get('firefly.chart') == 'google' %}
<div id="income-expenses-sum-chart"></div>
{% endif %}
{% if Config.get('firefly.chart') == 'chartjs' %}
<canvas id="income-expenses-sum-chart" style="width:100%;height:400px;"></canvas> <canvas id="income-expenses-sum-chart" style="width:100%;height:400px;"></canvas>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -41,15 +31,15 @@
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-6 col-sm-6"> <div class="col-lg-6 col-md-6 col-sm-6">
{% include 'partials/reports/accounts.twig' %} {% include 'reports/partials/accounts.twig' %}
{% include 'partials/reports/income-vs-expenses.twig' %} {% include 'reports/partials/income-vs-expenses.twig' %}
</div> </div>
<div class="col-lg-3 col-md-3 col-sm-3"> <div class="col-lg-3 col-md-3 col-sm-3">
{% include 'partials/reports/income.twig' %} {% include 'reports/partials/income.twig' %}
</div> </div>
<div class="col-lg-3 col-md-3 col-sm-3"> <div class="col-lg-3 col-md-3 col-sm-3">
{% include 'partials/reports/expenses.twig' %} {% include 'reports/partials/expenses.twig' %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -122,8 +112,14 @@
<script type="text/javascript"> <script type="text/javascript">
var year = '{{start.year}}'; var year = '{{start.year}}';
var shared = {% if shared %}'/shared'
{% else %}''{% endif %}; // to report another URL:
var startDate = '{{ start.format('Ymd') }}';
var endDate = '{{ end.format('Ymd') }}';
var reportType = '{{ report_type }}';
var accountIds = '{{ accountIds }}';
var incomeTopLength = {{ incomeTopLength }}; var incomeTopLength = {{ incomeTopLength }};
var expenseTopLength = {{ expenseTopLength }}; var expenseTopLength = {{ expenseTopLength }};
var incomeRestShow = false; // starts hidden. var incomeRestShow = false; // starts hidden.
@ -134,6 +130,6 @@
var hideTheRestExpense = '{{ trans('firefly.hideTheRest',{number:expenseTopLength}) }}'; var hideTheRestExpense = '{{ trans('firefly.hideTheRest',{number:expenseTopLength}) }}';
</script> </script>
<script type="text/javascript" src="js/reports.js"></script> <script type="text/javascript" src="js/reports/default/year.js"></script>
{% endblock %} {% endblock %}

View File

@ -6,45 +6,134 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12"> <div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">{{ 'reportsOwnAccounts'|_ }}</h3> <h3 class="box-title">{{ 'reports'|_ }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
{% for year, entries in months %} <form class="form-horizontal" id="report-form" action="{{ route('reports.index') }}" method="post">
<h4><a href="{{ route('reports.year',year) }}">{{ year }}</a></h4> <input type="hidden" name="_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="inputReportType" class="col-sm-3 control-label">{{ 'report_type'|_ }}</label>
<div class="col-sm-9">
<select name="report_type" class="form-control" id="inputReportType">
<option label="{{ 'report_type_default'|_ }}" value="default">{{ 'report_type_default'|_ }}</option>
</select>
</div>
</div>
<div class="form-group">
<label for="inputAccounts" class="col-sm-3 control-label">{{ 'report_included_accounts'|_ }}</label>
<div class="col-sm-9">
{% for account in accounts %}
<div class="checkbox">
<label>
<input type="checkbox" class="account-checkbox" name="accounts[]" value="{{ account.id }}">
{{ account.name }}
{% if account.getMeta('accountRole') == 'sharedAsset' %}
({{ 'shared'|_|lower }})
{% endif %}
</label>
</div>
{% endfor %}
<p class="help-block">
{{ 'report_include_help'|_ }}
</p>
</div>
</div>
<div class="form-group">
<label for="inputDateRange" class="col-sm-3 control-label">{{ 'report_date_range'|_ }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="inputDateRange" name="daterange"
value="{{ Session.get('start').format('Y-m-d') }} - {{ Session.get('end').format('Y-m-d') }}">
</div>
</div>
<div class="form-group">
<label for="__none__" class="col-sm-3 control-label">{{ 'report_preset_ranges'|_ }}</label>
<div class="col-sm-9">
{% for year, data in months %}
<a href="#" class="date-select" data-start="{{ data.start }}" data-end="{{ data.end }}">{{ year }}</a>
<ul class="list-inline"> <ul class="list-inline">
{% for month in entries %} {% for month in data.months %}
<li><a href="{{ route('reports.month',[month.year, month.month]) }}">{{ month.formatted }}</a></li> <li>
<a data-start="{{ month.start }}" data-end="{{ month.end }}" class="date-select" href="#">{{ month.formatted }}</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-default">{{ 'submit'|_ }}</button>
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12"> </div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">{{ 'reportsOwnAccountsAndShared'|_ }}</h3> <h3 class="box-title">{{ 'quick_link_reports'|_ }}</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<h4>{{ 'quick_link_default_report'|_ }}</h4>
{% for year, entries in months %} <ul>
<h4><a href="{{ route('reports.year',[year, 'shared']) }}">{{ year }}</a></h4> <li>
<ul class="list-inline"> <a href="{{ route('reports.report',
{% for month in entries %} ['default',
<li><a href="{{ route('reports.month',[month.year, month.month,'shared']) }}">{{ month.formatted }}</a></li> Session.get('start').startOfMonth.format('Ymd'),
{% endfor %} Session.get('start').endOfMonth.format('Ymd'),
accountList
]) }}">{{ 'report_this_month_quick'|_ }}</a>
</li>
<li>
<a href="{{ route('reports.report',
['default',
Session.get('start').startOfYear.format('Ymd'),
Session.get('start').endOfYear.format('Ymd'),
accountList
]) }}">{{ 'report_this_year_quick'|_ }}</a>
</li>
<li>
<a href="{{ route('reports.report',
['default',
start.format('Ymd'),
Session.get('end').endOfMonth.format('Ymd'),
accountList
]) }}">{{ 'report_all_time_quick'|_ }}</a>
</li>
</ul> </ul>
{% endfor %} <p>
<em>{{ 'reports_can_bookmark'|_ }}</em>
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script type="text/javascript" src="js/reports.js"></script> <script type="text/javascript">
var reportURL = "{{ route('reports.report', ['']) }}";
var minDate = "{{ start.format('m/d/Y') }}";
var picker;
</script>
<script type="text/javascript" src="js/reports/index.js"></script>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for account in accounts.getAccounts %} {% for account in accountReport.getAccounts %}
<tr> <tr>
<td> <td>
<a href="{{ route('accounts.show',account.id) }}" title="{{ account.name }}">{{ account.name }}</a> <a href="{{ route('accounts.show',account.id) }}" title="{{ account.name }}">{{ account.name }}</a>
@ -27,9 +27,9 @@
<tfoot> <tfoot>
<tr> <tr>
<td><em>{{ 'sumOfSums'|_ }}</em></td> <td><em>{{ 'sumOfSums'|_ }}</em></td>
<td>{{ accounts.getStart|formatAmount }}</td> <td>{{ accountReport.getStart|formatAmount }}</td>
<td>{{ accounts.getEnd|formatAmount }}</td> <td>{{ accountReport.getEnd|formatAmount }}</td>
<td>{{ accounts.getDifference|formatAmount }}</td> <td>{{ accountReport.getDifference|formatAmount }}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -14,7 +14,6 @@
<th> <th>
{{ 'leftInBudget'|_ }} {{ 'leftInBudget'|_ }}
</th> </th>
<th>{{ 'sum'|_ }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -35,7 +34,7 @@
{% for balanceEntry in balanceLine.getBalanceEntries %} {% for balanceEntry in balanceLine.getBalanceEntries %}
<td> <td>
{% if balanceEntry.getSpent != 0 %} {% if balanceEntry.getSpent != 0 %}
<span class="text-danger">{{ (balanceEntry.getSpent*-1)|formatAmountPlain }}</span> <span class="text-danger">{{ (balanceEntry.getSpent)|formatAmountPlain }}</span>
{% endif %} {% endif %}
{% if balanceEntry.getLeft != 0 %} {% if balanceEntry.getLeft != 0 %}
<span class="text-success">{{ (balanceEntry.getLeft)|formatAmountPlain }}</span> <span class="text-success">{{ (balanceEntry.getLeft)|formatAmountPlain }}</span>
@ -43,14 +42,7 @@
</td> </td>
{% endfor %} {% endfor %}
<td> <td>
{% if balanceLine.leftOfRepetition != 0 %}
{{ balanceLine.leftOfRepetition|formatAmount }} {{ balanceLine.leftOfRepetition|formatAmount }}
{% endif %}
</td>
<td>
{% if balanceLine.sumOfLeft != 0 %}
{{ balanceLine.sumOfLeft|formatAmount }}
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -39,7 +39,7 @@
{% endif %} {% endif %}
<td data-value="{{ (line.getMax - line.getAmount) }}"> <td data-value="{{ (line.getMax - line.getAmount) }}">
{% if line.isActive %} {% if line.isActive %}
{{ (line.getMax - line.getAmount)|formatAmount }} {{ (line.getMax + line.getAmount)|formatAmount }}
{% endif %} {% endif %}
</td> </td>

View File

@ -41,15 +41,17 @@
{% if budgetLine.getSpent != 0 %} {% if budgetLine.getSpent != 0 %}
{{ budgetLine.getSpent|formatAmount }} {{ budgetLine.getSpent|formatAmount }}
{% endif %} {% endif %}
</td>
<td> {% if budgetLine.getSpent == 0 %}
{% if budgetLine.getLeft != 0 %} {{ budgetLine.getSpent|formatAmount }}
{{ budgetLine.getLeft|formatAmount }}
{% endif %} {% endif %}
</td> </td>
<td>
{{ budgetLine.getLeft|formatAmount }}
</td>
<td> <td>
{% if budgetLine.getOverspent != 0 %} {% if budgetLine.getOverspent != 0 %}
<span class="text-danger">{{ budgetLine.getOverspent|formatAmountPlain }}</span> {{ budgetLine.getOverspent|formatAmount }}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -59,7 +61,14 @@
<tr> <tr>
<td colspan="2"><em>{{ 'sum'|_ }}</em></td> <td colspan="2"><em>{{ 'sum'|_ }}</em></td>
<td>{{ budgets.getBudgeted|formatAmount }}</td> <td>{{ budgets.getBudgeted|formatAmount }}</td>
<td>{{ budgets.getSpent|formatAmount }}</td> <td>
{% if budgets.getSpent != 0 %}
<span class="text-danger">{{ budgets.getSpent|formatAmountPlain }}</span>
{% endif %}
{% if budgets.getSpent == 0 %}
{{ budgets.getSpent|formatAmount }}
{% endif %}
</td>
<td>{{ budgets.getLeft|formatAmount }}</td> <td>{{ budgets.getLeft|formatAmount }}</td>
<td><span class="text-danger">{{ budgets.getOverspent|formatAmountPlain }}</span></td> <td><span class="text-danger">{{ budgets.getOverspent|formatAmountPlain }}</span></td>
</tr> </tr>

View File

@ -16,14 +16,14 @@
<td> <td>
<a href="{{ route('categories.show',cat.id) }}">{{ cat.name }}</a> <a href="{{ route('categories.show',cat.id) }}">{{ cat.name }}</a>
</td> </td>
<td><span class="text-danger">{{ (cat.spent * -1)|formatAmountPlain }}</span></td> <td>{{ cat.spent|formatAmount }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td><em>{{ 'sum'|_ }}</em></td> <td><em>{{ 'sum'|_ }}</em></td>
<td class="text-danger">{{ (categories.getTotal * -1)|formatAmountPlain }}</td> <td>{{ categories.getTotal|formatAmount }}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -18,7 +18,7 @@
<small>{{ expense.count }} {{ 'transactions'|_|lower }}</small> <small>{{ expense.count }} {{ 'transactions'|_|lower }}</small>
{% endif %} {% endif %}
</td> </td>
<td><span class="text-danger">{{ (expense.amount)|formatAmountPlain }}</span></td> <td>{{ (expense.amount)|formatAmount }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -32,7 +32,7 @@
{% endif %} {% endif %}
<tr> <tr>
<td><em>{{ 'sum'|_ }}</em></td> <td><em>{{ 'sum'|_ }}</em></td>
<td><span class="text-danger">{{ (expenses.getTotal)|formatAmountPlain }}</span></td> <td>{{ (expenses.getTotal)|formatAmount }}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>

View File

@ -10,11 +10,11 @@
</tr> </tr>
<tr> <tr>
<td>{{ 'out'|_ }}</td> <td>{{ 'out'|_ }}</td>
<td><span class="text-danger">{{ (expenses.getTotal)|formatAmountPlain }}</span></td> <td>{{ (expenses.getTotal)|formatAmount }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ 'difference'|_ }}</td> <td>{{ 'difference'|_ }}</td>
<td>{{ (incomes.getTotal + (expenses.getTotal * -1))|formatAmount }}</td> <td>{{ (incomes.getTotal + expenses.getTotal)|formatAmount }}</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -43,6 +43,12 @@
<td><a href="{{ route('categories.show',category.id) }}">{{ category.name }}</a></td> <td><a href="{{ route('categories.show',category.id) }}">{{ category.name }}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if journal.bill %}
<tr>
<td>{{ 'bill'|_ }}</td>
<td><a href="{{ route('bills.show', journal.bill_id) }}">{{ journal.bill.name }}</a></td>
</tr>
{% endif %}
{% if journal.tags|length > 0 %} {% if journal.tags|length > 0 %}
<tr> <tr>
<td>{{ 'tags'|_ }}</td> <td>{{ 'tags'|_ }}</td>

View File

@ -1,9 +1,5 @@
<?php <?php
use Carbon\Carbon;
use FireflyIII\Models\TransactionCurrency;
use League\FactoryMuffin\Facade as FactoryMuffin;
/** /**
* Class TestCase * Class TestCase
*/ */
@ -52,20 +48,4 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
{ {
parent::tearDown(); parent::tearDown();
} }
/**
* @param string $class
*
* @return Mockery\MockInterface
*/
public function mock($class)
{
$mock = Mockery::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
} }

View File

@ -1,20 +0,0 @@
<?php
/**
* Class ExampleTest
*/
class ExampleTest extends \PHPUnit_Framework_TestCase
{
protected function setUp()
{
}
protected function tearDown()
{
}
// tests
public function testMe()
{
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace FireflyIII\Models;
use TestCase;
class TransactionTypeTest extends TestCase
{
public function testIsWithdrawal()
{
$transactionType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first();
$this->assertTrue($transactionType->isWithdrawal());
}
public function testIsDeposit()
{
$transactionType = TransactionType::whereType(TransactionType::DEPOSIT)->first();
$this->assertTrue($transactionType->isDeposit());
}
public function testIsTransfer()
{
$transactionType = TransactionType::whereType(TransactionType::TRANSFER)->first();
$this->assertTrue($transactionType->isTransfer());
}
public function testIsOpeningBalance()
{
$transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
$this->assertTrue($transactionType->isOpeningBalance());
}
}