Implement custom trigger for recurring transactions

This commit is contained in:
James Cole
2022-12-29 15:42:02 +01:00
parent 85a2a87806
commit 8c320fd199
16 changed files with 604 additions and 234 deletions

View File

@@ -57,7 +57,7 @@ class TransactionJournalMetaFactory
} }
if ($data['data'] instanceof Carbon) { if ($data['data'] instanceof Carbon) {
//Log::debug('Is a carbon object.'); Log::debug('Is a carbon object.');
$value = $data['data']->toW3cString(); $value = $data['data']->toW3cString();
} }
if ('' === (string) $value) { if ('' === (string) $value) {

View File

@@ -166,7 +166,6 @@ class IndexController extends Controller
$accounts->each( $accounts->each(
function (Account $account) use ($activities, $startBalances, $endBalances) { function (Account $account) use ($activities, $startBalances, $endBalances) {
$interest = (string)$this->repository->getMetaValue($account, 'interest'); $interest = (string)$this->repository->getMetaValue($account, 'interest');
$interest = '' === $interest ? '0' : $interest; $interest = '' === $interest ? '0' : $interest;

View File

@@ -87,10 +87,15 @@ class ShowController extends Controller
$today = today(config('app.timezone')); $today = today(config('app.timezone'));
$array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null; $array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null;
// transform dates back to Carbon objects: // transform dates back to Carbon objects and expand information
foreach ($array['repetitions'] as $index => $repetition) { foreach ($array['repetitions'] as $index => $repetition) {
foreach ($repetition['occurrences'] as $item => $occurrence) { foreach ($repetition['occurrences'] as $item => $occurrence) {
$array['repetitions'][$index]['occurrences'][$item] = new Carbon($occurrence); $date = (new Carbon($occurrence))->startOfDay();
$set = [
'date' => $date,
'fired' => $this->recurring->createdPreviously($recurrence, $date) || $this->recurring->getJournalCount($recurrence, $date) > 0,
];
$array['repetitions'][$index]['occurrences'][$item] = $set;
} }
} }

View File

@@ -0,0 +1,89 @@
<?php
/*
* TriggerController.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Http\Controllers\Recurring;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\TriggerRecurrenceRequest;
use FireflyIII\Jobs\CreateRecurringTransactions;
use FireflyIII\Models\Recurrence;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class TriggerController
*/
class TriggerController extends Controller
{
/**
* @param Recurrence $recurrence
* @param TriggerRecurrenceRequest $request
* @return RedirectResponse
*/
public function trigger(Recurrence $recurrence, TriggerRecurrenceRequest $request): RedirectResponse
{
$all = $request->getAll();
$date = $all['date'];
// grab the date from the last time the recurrence fired:
$backupDate = $recurrence->latest_date;
// fire the recurring cron job on the given date, then post-date the created transaction.
Log::info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s')));
/** @var CreateRecurringTransactions $job */
$job = app(CreateRecurringTransactions::class);
$job->setRecurrences(new Collection([$recurrence]));
$job->setDate($date);
$job->setForce(false);
$job->handle();
Log::debug('Done with recurrence.');
$groups = $job->getGroups();
/** @var TransactionGroup $group */
foreach ($groups as $group) {
/** @var TransactionJournal $journal */
foreach ($group->transactionJournals as $journal) {
Log::debug(sprintf('Set date of journal #%d to today!', $journal->id, $date));
$journal->date = Carbon::today();
$journal->save();
}
}
$recurrence->latest_date = $backupDate;
$recurrence->save();
app('preferences')->mark();
if (0 === $groups->count()) {
$request->session()->flash('info', (string)trans('firefly.no_new_transaction_in_recurrence'));
}
if (1 === $groups->count()) {
$first = $groups->first();
$request->session()->flash('success', (string)trans('firefly.stored_journal_no_descr'));
$request->session()->flash('success_url', route('transactions.show', [$first->id]));
}
return redirect(route('recurring.show', [$recurrence->id]));
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* TriggerRecurrenceRequest.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Http\Requests;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
class TriggerRecurrenceRequest extends FormRequest
{
use ConvertsDataTypes;
use ChecksLogin;
/**
* Returns the data required by the controller.
*
* @return array
*/
public function getAll(): array
{
return [
'date' => $this->getCarbonDate('date'),
];
}
/**
* Rules for this request.
*
* @return array
*/
public function rules(): array
{
return [
'date' => 'required|date',
];
}
}

View File

@@ -63,6 +63,8 @@ class CreateRecurringTransactions implements ShouldQueue
private TransactionGroupRepositoryInterface $groupRepository; private TransactionGroupRepositoryInterface $groupRepository;
private JournalRepositoryInterface $journalRepository; private JournalRepositoryInterface $journalRepository;
private RecurringRepositoryInterface $repository; private RecurringRepositoryInterface $repository;
private Collection $recurrences;
private Collection $groups;
/** /**
* Create a new job instance. * Create a new job instance.
@@ -73,13 +75,12 @@ class CreateRecurringTransactions implements ShouldQueue
*/ */
public function __construct(?Carbon $date) public function __construct(?Carbon $date)
{ {
if (null !== $date) { $newDate = new Carbon();
$newDate = clone $date;
$newDate->startOfDay(); $newDate->startOfDay();
$this->date = $newDate; $this->date = $newDate;
}
if (null === $date) { if (null !== $date) {
$newDate = new Carbon(); $newDate = clone $date;
$newDate->startOfDay(); $newDate->startOfDay();
$this->date = $newDate; $this->date = $newDate;
} }
@@ -90,6 +91,8 @@ class CreateRecurringTransactions implements ShouldQueue
$this->submitted = 0; $this->submitted = 0;
$this->executed = 0; $this->executed = 0;
$this->created = 0; $this->created = 0;
$this->recurrences = new Collection();
$this->groups = new Collection();
Log::debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d'))); Log::debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d')));
} }
@@ -100,14 +103,23 @@ class CreateRecurringTransactions implements ShouldQueue
public function handle(): void public function handle(): void
{ {
Log::debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y'))); Log::debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y')));
$recurrences = $this->repository->getAll();
// only use recurrences from database if there is no collection submitted.
if (0 !== count($this->recurrences)) {
Log::debug('Using predetermined set of recurrences.');
}
if (0 === count($this->recurrences)) {
Log::debug('Grab all recurrences from the database.');
$this->recurrences = $this->repository->getAll();
}
$result = []; $result = [];
$count = $recurrences->count(); $count = $this->recurrences->count();
$this->submitted = $count; $this->submitted = $count;
Log::debug(sprintf('Count of collection is %d', $count)); Log::debug(sprintf('Count of collection is %d', $count));
// filter recurrences: // filter recurrences:
$filtered = $this->filterRecurrences($recurrences); $filtered = $this->filterRecurrences($this->recurrences);
Log::debug(sprintf('Left after filtering is %d', $filtered->count())); Log::debug(sprintf('Left after filtering is %d', $filtered->count()));
/** @var Recurrence $recurrence */ /** @var Recurrence $recurrence */
foreach ($filtered as $recurrence) { foreach ($filtered as $recurrence) {
@@ -179,6 +191,7 @@ class CreateRecurringTransactions implements ShouldQueue
return false; return false;
} }
// is no longer running // is no longer running
if ($this->repeatUntilHasPassed($recurrence)) { if ($this->repeatUntilHasPassed($recurrence)) {
Log::info( Log::info(
@@ -312,7 +325,7 @@ class CreateRecurringTransactions implements ShouldQueue
); );
// start looping from $startDate to today perhaps we have a hit? // start looping from $startDate to today perhaps we have a hit?
// add two days to $this->date so we always include the weekend. // add two days to $this->date, so we always include the weekend.
$includeWeekend = clone $this->date; $includeWeekend = clone $this->date;
$includeWeekend->addDays(2); $includeWeekend->addDays(2);
$occurrences = $this->repository->getOccurrencesInRange($repetition, $recurrence->first_date, $includeWeekend); $occurrences = $this->repository->getOccurrencesInRange($repetition, $recurrence->first_date, $includeWeekend);
@@ -376,6 +389,13 @@ class CreateRecurringTransactions implements ShouldQueue
return null; return null;
} }
if ($this->repository->createdPreviously($recurrence, $date) && false === $this->force) {
Log::info('There is a transaction already made for this date, so will not be created now');
return null;
}
if ($journalCount > 0 && true === $this->force) { if ($journalCount > 0 && true === $this->force) {
app('log')->warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d'))); app('log')->warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d')));
} }
@@ -408,6 +428,7 @@ class CreateRecurringTransactions implements ShouldQueue
// trigger event: // trigger event:
event(new StoredTransactionGroup($group, $recurrence->apply_rules, true)); event(new StoredTransactionGroup($group, $recurrence->apply_rules, true));
$this->groups->push($group);
// update recurring thing: // update recurring thing:
$recurrence->latest_date = $date; $recurrence->latest_date = $date;
@@ -466,6 +487,7 @@ class CreateRecurringTransactions implements ShouldQueue
'bill_name' => null, 'bill_name' => null,
'recurrence_total' => $total, 'recurrence_total' => $total,
'recurrence_count' => $count, 'recurrence_count' => $count,
'recurrence_date' => $date,
]; ];
$return[] = $single; $return[] = $single;
} }
@@ -490,4 +512,20 @@ class CreateRecurringTransactions implements ShouldQueue
{ {
$this->force = $force; $this->force = $force;
} }
/**
* @param Collection $recurrences
*/
public function setRecurrences(Collection $recurrences): void
{
$this->recurrences = $recurrences;
}
/**
* @return Collection
*/
public function getGroups(): Collection
{
return $this->groups;
}
} }

View File

@@ -43,6 +43,7 @@ use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrences;
use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrencesSince; use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrencesSince;
use FireflyIII\Support\Repositories\Recurring\FiltersWeekends; use FireflyIII\Support\Repositories\Recurring\FiltersWeekends;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use JsonException; use JsonException;
@@ -207,7 +208,6 @@ class RecurringRepository implements RecurringRepositoryInterface
if (null !== $end) { if (null !== $end) {
$query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00')); $query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00'));
} }
return $query->count(['transaction_journals.id']); return $query->count(['transaction_journals.id']);
} }
@@ -629,4 +629,34 @@ class RecurringRepository implements RecurringRepositoryInterface
return $service->update($recurrence, $data); return $service->update($recurrence, $data);
} }
/**
* @inheritDoc
*/
public function createdPreviously(Recurrence $recurrence, Carbon $date): bool
{
// if not, loop set and try to read the recurrence_date. If it matches start or end, return it as well.
$set =
TransactionJournalMeta::where(function (Builder $q1) use ($recurrence) {
$q1->where('name', 'recurrence_id');
$q1->where('data', json_encode((string)$recurrence->id));
})->get(['journal_meta.transaction_journal_id']);
// there are X journals made for this recurrence. Any of them meant for today?
foreach ($set as $journalMeta) {
$count = TransactionJournalMeta::where(function (Builder $q2) use ($date) {
$string = (string)$date;
Log::debug(sprintf('Search for date: %s', json_encode($string)));
$q2->where('name', 'recurrence_date');
$q2->where('data', json_encode($string));
})
->where('transaction_journal_id', $journalMeta->transaction_journal_id)
->count();
if ($count > 0) {
Log::debug(sprintf('Looks like journal #%d was already created', $journalMeta->transaction_journal_id));
return true;
}
}
return false;
}
} }

View File

@@ -45,6 +45,13 @@ interface RecurringRepositoryInterface
*/ */
public function destroy(Recurrence $recurrence): void; public function destroy(Recurrence $recurrence): void;
/**
* @param Recurrence $recurrence
* @param Carbon $date
* @return bool
*/
public function createdPreviously(Recurrence $recurrence, Carbon $date): bool;
/** /**
* Destroy all recurring transactions. * Destroy all recurring transactions.
*/ */

View File

@@ -104,7 +104,6 @@ class Steam
*/ */
public function bcround(?string $number, int $precision = 0): string public function bcround(?string $number, int $precision = 0): string
{ {
if (null === $number) { if (null === $number) {
return '0'; return '0';
} }

View File

@@ -221,9 +221,14 @@ return [
// account types that may have or set a currency // account types that may have or set a currency
'valid_currency_account_types' => [ 'valid_currency_account_types' => [
AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE, AccountType::ASSET,
AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::LIABILITY_CREDIT, AccountType::LOAN,
AccountType::RECONCILIATION AccountType::DEBT,
AccountType::MORTGAGE,
AccountType::CASH,
AccountType::INITIAL_BALANCE,
AccountType::LIABILITY_CREDIT,
AccountType::RECONCILIATION,
], ],
// "value must be in this list" values // "value must be in this list" values
@@ -528,8 +533,13 @@ return [
TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeEnum::DEPOSIT->value => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeEnum::DEPOSIT->value => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, TransactionTypeModel::OPENING_BALANCE => [
AccountType::MORTGAGE,], AccountType::INITIAL_BALANCE,
AccountType::ASSET,
AccountType::LOAN,
AccountType::DEBT,
AccountType::MORTGAGE,
],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
// in case no transaction type is known yet, it could be anything. // in case no transaction type is known yet, it could be anything.
@@ -543,46 +553,111 @@ return [
], ],
], ],
'destination' => [ 'destination' => [
TransactionTypeModel::WITHDRAWAL => [AccountType::EXPENSE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, TransactionTypeModel::WITHDRAWAL => [
AccountType::MORTGAGE,], AccountType::EXPENSE,
AccountType::CASH,
AccountType::LOAN,
AccountType::DEBT,
AccountType::MORTGAGE,
],
TransactionTypeEnum::DEPOSIT->value => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeEnum::DEPOSIT->value => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, TransactionTypeModel::OPENING_BALANCE => [
AccountType::MORTGAGE,], AccountType::INITIAL_BALANCE,
AccountType::ASSET,
AccountType::LOAN,
AccountType::DEBT,
AccountType::MORTGAGE,
],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET], TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
], ],
], ],
'allowed_opposing_types' => [ 'allowed_opposing_types' => [
'source' => [ 'source' => [
AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::ASSET => [
AccountType::LOAN, AccountType::RECONCILIATION, AccountType::MORTGAGE], AccountType::ASSET,
AccountType::CASH,
AccountType::DEBT,
AccountType::EXPENSE,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::RECONCILIATION,
AccountType::MORTGAGE,
],
AccountType::CASH => [AccountType::ASSET], AccountType::CASH => [AccountType::ASSET],
AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::DEBT => [
AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], AccountType::ASSET,
AccountType::DEBT,
AccountType::EXPENSE,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::LIABILITY_CREDIT,
],
AccountType::EXPENSE => [], // is not allowed as a source. AccountType::EXPENSE => [], // is not allowed as a source.
AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE],
AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::LOAN => [
AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], AccountType::ASSET,
AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::EXPENSE, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE, AccountType::LIABILITY_CREDIT], AccountType::EXPENSE,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::LIABILITY_CREDIT,
],
AccountType::MORTGAGE => [
AccountType::ASSET,
AccountType::DEBT,
AccountType::EXPENSE,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::LIABILITY_CREDIT,
],
AccountType::RECONCILIATION => [AccountType::ASSET], AccountType::RECONCILIATION => [AccountType::ASSET],
AccountType::REVENUE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], AccountType::REVENUE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE],
AccountType::LIABILITY_CREDIT => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], AccountType::LIABILITY_CREDIT => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE],
], ],
'destination' => [ 'destination' => [
AccountType::ASSET => [AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::ASSET => [
AccountType::MORTGAGE, AccountType::RECONCILIATION, AccountType::REVENUE,], AccountType::ASSET,
AccountType::CASH,
AccountType::DEBT,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::RECONCILIATION,
AccountType::REVENUE,
],
AccountType::CASH => [AccountType::ASSET], AccountType::CASH => [AccountType::ASSET],
AccountType::DEBT => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT => [
AccountType::REVENUE,], AccountType::ASSET,
AccountType::DEBT,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::REVENUE,
],
AccountType::EXPENSE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], AccountType::EXPENSE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE],
AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], AccountType::INITIAL_BALANCE => [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE],
AccountType::LOAN => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, AccountType::LOAN => [
AccountType::REVENUE,], AccountType::ASSET,
AccountType::MORTGAGE => [AccountType::ASSET, AccountType::DEBT, AccountType::INITIAL_BALANCE, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT,
AccountType::REVENUE,], AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::REVENUE,
],
AccountType::MORTGAGE => [
AccountType::ASSET,
AccountType::DEBT,
AccountType::INITIAL_BALANCE,
AccountType::LOAN,
AccountType::MORTGAGE,
AccountType::REVENUE,
],
AccountType::RECONCILIATION => [AccountType::ASSET], AccountType::RECONCILIATION => [AccountType::ASSET],
AccountType::REVENUE => [], // is not allowed as a destination AccountType::REVENUE => [], // is not allowed as a destination
AccountType::LIABILITY_CREDIT => [],// is not allowed as a destination AccountType::LIABILITY_CREDIT => [],// is not allowed as a destination
@@ -591,31 +666,66 @@ return [
// depending on the account type, return the allowed transaction types: // depending on the account type, return the allowed transaction types:
'allowed_transaction_types' => [ 'allowed_transaction_types' => [
'source' => [ 'source' => [
AccountType::ASSET => [TransactionTypeModel::WITHDRAWAL, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, AccountType::ASSET => [
TransactionTypeModel::RECONCILIATION,], TransactionTypeModel::WITHDRAWAL,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::RECONCILIATION,
],
AccountType::EXPENSE => [], // is not allowed as a source. AccountType::EXPENSE => [], // is not allowed as a source.
AccountType::REVENUE => [TransactionTypeEnum::DEPOSIT->value], AccountType::REVENUE => [TransactionTypeEnum::DEPOSIT->value],
AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, AccountType::LOAN => [
TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], TransactionTypeModel::WITHDRAWAL,
AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], TransactionTypeModel::TRANSFER,
AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::OPENING_BALANCE, TransactionTypeModel::LIABILITY_CREDIT], TransactionTypeModel::LIABILITY_CREDIT,
],
AccountType::DEBT => [
TransactionTypeModel::WITHDRAWAL,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::LIABILITY_CREDIT,
],
AccountType::MORTGAGE => [
TransactionTypeModel::WITHDRAWAL,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::LIABILITY_CREDIT,
],
AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE],
AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION],
AccountType::LIABILITY_CREDIT => [TransactionTypeModel::LIABILITY_CREDIT], AccountType::LIABILITY_CREDIT => [TransactionTypeModel::LIABILITY_CREDIT],
], ],
'destination' => [ 'destination' => [
AccountType::ASSET => [TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE, AccountType::ASSET => [
TransactionTypeModel::RECONCILIATION,], TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::RECONCILIATION,
],
AccountType::EXPENSE => [TransactionTypeModel::WITHDRAWAL], AccountType::EXPENSE => [TransactionTypeModel::WITHDRAWAL],
AccountType::REVENUE => [], // is not allowed as destination. AccountType::REVENUE => [], // is not allowed as destination.
AccountType::LOAN => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, AccountType::LOAN => [
TransactionTypeModel::OPENING_BALANCE,], TransactionTypeModel::WITHDRAWAL,
AccountType::DEBT => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::OPENING_BALANCE,], TransactionTypeModel::TRANSFER,
AccountType::MORTGAGE => [TransactionTypeModel::WITHDRAWAL, TransactionTypeEnum::DEPOSIT->value, TransactionTypeModel::TRANSFER, TransactionTypeModel::OPENING_BALANCE,
TransactionTypeModel::OPENING_BALANCE,], ],
AccountType::DEBT => [
TransactionTypeModel::WITHDRAWAL,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
],
AccountType::MORTGAGE => [
TransactionTypeModel::WITHDRAWAL,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeModel::TRANSFER,
TransactionTypeModel::OPENING_BALANCE,
],
AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE], AccountType::INITIAL_BALANCE => [TransactionTypeModel::OPENING_BALANCE],
AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION], AccountType::RECONCILIATION => [TransactionTypeModel::RECONCILIATION],
AccountType::LIABILITY_CREDIT => [], // is not allowed as a destination AccountType::LIABILITY_CREDIT => [], // is not allowed as a destination
@@ -727,23 +837,40 @@ return [
AccountType::LIABILITY_CREDIT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE], AccountType::LIABILITY_CREDIT => [AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
], ],
], ],
// if you add fields to this array, dont forget to update the export routine (ExportDataGenerator). // if you add fields to this array, don't forget to update the export routine (ExportDataGenerator).
'journal_meta_fields' => [ 'journal_meta_fields' => [
// sepa // sepa
'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_cc',
'sepa_db', 'sepa_country', 'sepa_ep', 'sepa_ct_op',
'sepa_ci', 'sepa_batch_id', 'external_url', 'sepa_ct_id',
'sepa_db',
'sepa_country',
'sepa_ep',
'sepa_ci',
'sepa_batch_id',
'external_url',
// dates // dates
'interest_date', 'book_date', 'process_date', 'interest_date',
'due_date', 'payment_date', 'invoice_date', 'book_date',
'process_date',
'due_date',
'payment_date',
'invoice_date',
// others // others
'recurrence_id', 'internal_reference', 'bunq_payment_id', 'recurrence_id',
'import_hash', 'import_hash_v2', 'external_id', 'original_source', 'internal_reference',
'bunq_payment_id',
'import_hash',
'import_hash_v2',
'external_id',
'original_source',
// recurring transactions // recurring transactions
'recurrence_total', 'recurrence_count', 'recurrence_total',
'recurrence_count',
'recurrence_date',
], ],
'webhooks' => [ 'webhooks' => [
'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3), 'max_attempts' => env('WEBHOOK_MAX_ATTEMPTS', 3),

View File

@@ -2447,6 +2447,8 @@ return [
'no_bills_create_default' => 'Create a bill', 'no_bills_create_default' => 'Create a bill',
// recurring transactions // recurring transactions
'create_right_now' => 'Create right now',
'no_new_transaction_in_recurrence' => 'No new transaction was created. Perhaps it was already fired for this date?',
'recurrences' => 'Recurring transactions', 'recurrences' => 'Recurring transactions',
'repeat_until_in_past' => 'This recurring transaction stopped repeating on :date.', 'repeat_until_in_past' => 'This recurring transaction stopped repeating on :date.',
'recurring_calendar_view' => 'Calendar', 'recurring_calendar_view' => 'Calendar',

View File

@@ -37,8 +37,10 @@
</div> </div>
<div class="box-footer"> <div class="box-footer">
<div class="btn-group"> <div class="btn-group">
<a href="{{ route('recurring.edit', [array.id]) }}" class="btn btn-sm btn-default"><span class="fa fa-pencil"></span> {{ 'edit'|_ }}</a> <a href="{{ route('recurring.edit', [array.id]) }}" class="btn btn-sm btn-default"><span
<a href="{{ route('recurring.delete', [array.id]) }}" class="btn btn-sm btn-danger">{{ 'delete'|_ }} <span class="fa fa-trash"></span></a> class="fa fa-pencil"></span> {{ 'edit'|_ }}</a>
<a href="{{ route('recurring.delete', [array.id]) }}" class="btn btn-sm btn-danger">{{ 'delete'|_ }}
<span class="fa fa-trash"></span></a>
</div> </div>
</div> </div>
</div> </div>
@@ -57,24 +59,37 @@
{{ trans('firefly.repeat_until_in_past', {date: array.repeat_until.isoFormat(monthAndDayFormat) }) }} {{ trans('firefly.repeat_until_in_past', {date: array.repeat_until.isoFormat(monthAndDayFormat) }) }}
</span> </span>
{% endif %} {% endif %}
<ul>
{% for rep in array.repetitions %} {% for rep in array.repetitions %}
<li> <p>
{{ rep.description }} <strong>{{ rep.description }}
{% if rep.repetition_skip == 1 %} {% if rep.repetition_skip == 1 %}
({{ trans('firefly.recurring_skips_one')|lower }}) ({{ trans('firefly.recurring_skips_one')|lower }})
{% endif %} {% endif %}
{% if rep.repetition_skip > 1 %} {% if rep.repetition_skip > 1 %}
({{ trans('firefly.recurring_skips_more', {count: rep.repetition_skip})|lower }}) ({{ trans('firefly.recurring_skips_more', {count: rep.repetition_skip})|lower }})
{% endif %} {% endif %}
<ul> </strong>
</p>
<table class="table">
<tbody>
{% for occ in rep.occurrences %} {% for occ in rep.occurrences %}
<li>{{ occ.isoFormat(trans('config.month_and_date_day_js')) }}</li> <tr>
<td>{{ occ.date.isoFormat(trans('config.month_and_date_day_js')) }}</td>
<td>
{% if not occ.fired %}
<form action="{{ route('recurring.trigger', [recurrence.id]) }}" method="post" style="display: inline;">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="{{ occ.date.isoFormat('YYYY-MM-DD') }}">
<input type="submit" name="submit" value="{{ 'create_right_now'|_ }}"
class="btn btn-sm btn-default">
</form>
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</ul> </tbody>
</li> </table>
{% endfor %} {% endfor %}
</ul>
</div> </div>
<div class="box-footer"> <div class="box-footer">
<small> <small>
@@ -175,11 +190,13 @@
{% endblock %} {% endblock %}
{% block styles %} {% block styles %}
<link rel="stylesheet" href="v1/css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all" nonce="{{ JS_NONCE }}"> <link rel="stylesheet" href="v1/css/bootstrap-sortable.css?v={{ FF_VERSION }}" type="text/css" media="all"
nonce="{{ JS_NONCE }}">
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script type="text/javascript" src="v1/js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script> <script type="text/javascript" src="v1/js/lib/bootstrap-sortable.js?v={{ FF_VERSION }}"
nonce="{{ JS_NONCE }}"></script>
{# required for groups.twig #} {# required for groups.twig #}
<script type="text/javascript" src="v1/js/ff/list/groups.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script> <script type="text/javascript" src="v1/js/ff/list/groups.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
{% endblock %} {% endblock %}

View File

@@ -718,6 +718,7 @@ Route::group(
Route::post('store', ['uses' => 'Recurring\CreateController@store', 'as' => 'store']); Route::post('store', ['uses' => 'Recurring\CreateController@store', 'as' => 'store']);
Route::post('update/{recurrence}', ['uses' => 'Recurring\EditController@update', 'as' => 'update']); Route::post('update/{recurrence}', ['uses' => 'Recurring\EditController@update', 'as' => 'update']);
Route::post('destroy/{recurrence}', ['uses' => 'Recurring\DeleteController@destroy', 'as' => 'destroy']); Route::post('destroy/{recurrence}', ['uses' => 'Recurring\DeleteController@destroy', 'as' => 'destroy']);
Route::post('trigger/{recurrence}', ['uses' => 'Recurring\TriggerController@trigger', 'as' => 'trigger']);
// JSON routes: // JSON routes:
Route::get('events', ['uses' => 'Json\RecurrenceController@events', 'as' => 'events']); Route::get('events', ['uses' => 'Json\RecurrenceController@events', 'as' => 'events']);