User can submit new journal through API.

This commit is contained in:
James Cole
2019-03-31 13:36:49 +02:00
parent c07ef3658b
commit b692cccdfb
30 changed files with 1461 additions and 711 deletions

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Api\V1\Requests;
use FireflyIII\Rules\BelongsUser;
use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsDateOrTime;
use FireflyIII\Support\NullArrayObject;
use FireflyIII\Validation\TransactionValidation;
use Illuminate\Validation\Validator;
@@ -59,34 +60,8 @@ class TransactionRequest extends Request
public function getAll(): array
{
$data = [
'type' => $this->string('type'),
'date' => $this->dateTime('date'),
'description' => $this->string('description'),
'piggy_bank_id' => $this->integer('piggy_bank_id'),
'piggy_bank_name' => $this->string('piggy_bank_name'),
'bill_id' => $this->integer('bill_id'),
'bill_name' => $this->string('bill_name'),
'tags' => explode(',', $this->string('tags')),
'notes' => $this->string('notes'),
'sepa-cc' => $this->string('sepa_cc'),
'sepa-ct-op' => $this->string('sepa_ct_op'),
'sepa-ct-id' => $this->string('sepa_ct_id'),
'sepa-db' => $this->string('sepa_db'),
'sepa-country' => $this->string('sepa_country'),
'sepa-ep' => $this->string('sepa_ep'),
'sepa-ci' => $this->string('sepa_ci'),
'sepa-batch-id' => $this->string('sepa_batch_id'),
'interest_date' => $this->date('interest_date'),
'book_date' => $this->date('book_date'),
'process_date' => $this->date('process_date'),
'due_date' => $this->date('due_date'),
'payment_date' => $this->date('payment_date'),
'invoice_date' => $this->date('invoice_date'),
'internal_reference' => $this->string('internal_reference'),
'bunq_payment_id' => $this->string('bunq_payment_id'),
'external_id' => $this->string('external_id'),
'original-source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
'transactions' => $this->getTransactionData(),
'group_title' => $this->string('group_title'),
'transactions' => $this->getTransactionData(),
];
return $data;
@@ -101,61 +76,76 @@ class TransactionRequest extends Request
public function rules(): array
{
$rules = [
// basic fields for journal:
'type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'description' => 'between:1,255',
'date' => ['required', new IsDateOrTime],
'piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser],
'piggy_bank_name' => ['between:1,255', 'nullable', new BelongsUser],
'bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser],
'bill_name' => ['between:1,255', 'nullable', new BelongsUser],
'tags' => 'between:1,255',
// then, custom fields for journal
'notes' => 'min:1,max:50000|nullable',
// SEPA fields:
'sepa_cc' => 'min:1,max:255|nullable',
'sepa_ct_op' => 'min:1,max:255|nullable',
'sepa_ct_id' => 'min:1,max:255|nullable',
'sepa_db' => 'min:1,max:255|nullable',
'sepa_country' => 'min:1,max:255|nullable',
'sepa_ep' => 'min:1,max:255|nullable',
'sepa_ci' => 'min:1,max:255|nullable',
'sepa_batch_id' => 'min:1,max:255|nullable',
// dates
'interest_date' => 'date|nullable',
'book_date' => 'date|nullable',
'process_date' => 'date|nullable',
'due_date' => 'date|nullable',
'payment_date' => 'date|nullable',
'invoice_date' => 'date|nullable',
'internal_reference' => 'min:1,max:255|nullable',
'bunq_payment_id' => 'min:1,max:255|nullable',
'external_id' => 'min:1,max:255|nullable',
// basic fields for group:
'group_title' => 'between:1,255',
// transaction rules (in array for splits):
'transactions.*.amount' => 'required|numeric|more:0',
'transactions.*.description' => 'nullable|between:1,255',
'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation',
'transactions.*.date' => ['required', new IsDateOrTime],
// currency info
'transactions.*.currency_id' => 'numeric|exists:transaction_currencies,id',
'transactions.*.currency_code' => 'min:3|max:3|exists:transaction_currencies,code',
'transactions.*.foreign_amount' => 'numeric|more:0',
'transactions.*.foreign_currency_id' => 'numeric|exists:transaction_currencies,id',
'transactions.*.foreign_currency_code' => 'min:3|max:3|exists:transaction_currencies,code',
// amount
'transactions.*.amount' => 'required|numeric|more:0',
'transactions.*.foreign_amount' => 'numeric|more:0',
// description
'transactions.*.description' => 'nullable|between:1,255',
// source of transaction
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser],
'transactions.*.source_name' => 'between:1,255|nullable',
// destination of transaction
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser],
'transactions.*.destination_name' => 'between:1,255|nullable',
// budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser],
'transactions.*.budget_name' => ['between:1,255', 'nullable', new BelongsUser],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser],
'transactions.*.category_name' => 'between:1,255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser],
'transactions.*.bill_name' => ['between:1,255', 'nullable', new BelongsUser],
'transactions.*.piggy_bank_id' => ['numeric', 'nullable', 'mustExist:piggy_banks,id', new BelongsUser],
'transactions.*.piggy_bank_name' => ['between:1,255', 'nullable', new BelongsUser],
// other interesting fields
'transactions.*.reconciled' => [new IsBoolean],
'transactions.*.source_id' => ['numeric', 'nullable', new BelongsUser],
'transactions.*.source_name' => 'between:1,255|nullable',
'transactions.*.destination_id' => ['numeric', 'nullable', new BelongsUser],
'transactions.*.destination_name' => 'between:1,255|nullable',
'transactions.*.notes' => 'min:1,max:50000|nullable',
'transactions.*.tags' => 'between:1,255',
// meta info fields
'transactions.*.internal_reference' => 'min:1,max:255|nullable',
'transactions.*.external_id' => 'min:1,max:255|nullable',
'transactions.*.recurrence_id' => 'min:1,max:255|nullable',
'transactions.*.bunq_payment_id' => 'min:1,max:255|nullable',
// SEPA fields:
'transactions.*.sepa_cc' => 'min:1,max:255|nullable',
'transactions.*.sepa_ct_op' => 'min:1,max:255|nullable',
'transactions.*.sepa_ct_id' => 'min:1,max:255|nullable',
'transactions.*.sepa_db' => 'min:1,max:255|nullable',
'transactions.*.sepa_country' => 'min:1,max:255|nullable',
'transactions.*.sepa_ep' => 'min:1,max:255|nullable',
'transactions.*.sepa_ci' => 'min:1,max:255|nullable',
'transactions.*.sepa_batch_id' => 'min:1,max:255|nullable',
// dates
'transactions.*.interest_date' => 'date|nullable',
'transactions.*.book_date' => 'date|nullable',
'transactions.*.process_date' => 'date|nullable',
'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable',
];
if ('PUT' === $this->method()) {
unset($rules['type'], $rules['piggy_bank_id'], $rules['piggy_bank_name']);
unset($rules['transactions.*.type'], $rules['transactions.*.piggy_bank_id'], $rules['transactions.*.piggy_bank_name']);
}
return $rules;
@@ -174,13 +164,28 @@ class TransactionRequest extends Request
{
$validator->after(
function (Validator $validator) {
// must submit at least one transaction.
$this->validateOneTransaction($validator);
// all journals must have a description
$this->validateDescriptions($validator);
$this->validateJournalDescription($validator);
$this->validateSplitDescriptions($validator);
// all transaction types must be equal:
$this->validateTransactionTypes($validator);
// validate foreign currency info
$this->validateForeignCurrencyInformation($validator);
// validate all account info
$this->validateAccountInformation($validator);
// make sure all splits have valid source + dest info
$this->validateSplitAccounts($validator);
// the group must have a description if > 1 journal.
$this->validateGroupDescription($validator);
}
);
}
@@ -195,28 +200,88 @@ class TransactionRequest extends Request
private function getTransactionData(): array
{
$return = [];
/**
* @var int $index
* @var array $transaction
*/
foreach ($this->get('transactions') as $index => $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'amount' => $transaction['amount'],
'description' => $transaction['description'] ?? null,
'currency_id' => isset($transaction['currency_id']) ? (int)$transaction['currency_id'] : null,
'currency_code' => $transaction['currency_code'] ?? null,
'foreign_amount' => $transaction['foreign_amount'] ?? null,
'foreign_currency_id' => isset($transaction['foreign_currency_id']) ? (int)$transaction['foreign_currency_id'] : null,
'foreign_currency_code' => $transaction['foreign_currency_code'] ?? null,
'budget_id' => isset($transaction['budget_id']) ? (int)$transaction['budget_id'] : null,
'budget_name' => $transaction['budget_name'] ?? null,
'category_id' => isset($transaction['category_id']) ? (int)$transaction['category_id'] : null,
'category_name' => $transaction['category_name'] ?? null,
'source_id' => isset($transaction['source_id']) ? (int)$transaction['source_id'] : null,
'source_name' => isset($transaction['source_name']) ? (string)$transaction['source_name'] : null,
'destination_id' => isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null,
'destination_name' => isset($transaction['destination_name']) ? (string)$transaction['destination_name'] : null,
'reconciled' => $this->convertBoolean((string)($transaction['reconciled'] ?? 'false')),
'identifier' => $index,
// $this->dateFromValue($object[''])
'type' => $this->stringFromValue($object['type']),
'date' => $this->dateFromValue($object['date']),
'currency_id' => $this->integerFromValue($object['currency_id']),
'currency_code' => $this->stringFromValue($object['currency_code']),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']),
'foreign_currency_code' => $this->stringFromValue($object['foreign_currency_code']),
// amount and foreign amount. Cannot be 0.
'amount' => $this->stringFromValue($object['amount']),
'foreign_amount' => $this->stringFromValue($object['foreign_amount']),
// description.
'description' => $this->stringFromValue($object['description']),
// source of transaction. If everything is null, assume cash account.
'source_id' => $this->integerFromValue((string)$object['source_id']),
'source_name' => $this->stringFromValue($object['source_name']),
// destination of transaction. If everything is null, assume cash account.
'destination_id' => $this->integerFromValue((string)$object['destination_id']),
'destination_name' => $this->stringFromValue($object['destination_name']),
// budget info
'budget_id' => $this->integerFromValue((string)$object['budget_id']),
'budget_name' => $this->stringFromValue($object['budget_name']),
// category info
'category_id' => $this->integerFromValue((string)$object['category_id']),
'category_name' => $this->stringFromValue($object['category_name']),
// journal bill reference. Optional. Will only work for withdrawals
'bill_id' => $this->integerFromValue((string)$object['bill_id']),
'bill_name' => $this->stringFromValue($object['bill_name']),
// piggy bank reference. Optional. Will only work for transfers
'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']),
'piggy_bank_name' => $this->stringFromValue($object['piggy_bank_name']),
// some other interesting properties
'reconciled' => $this->convertBoolean((string)$object['reconciled']),
'notes' => $this->stringFromValue($object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->stringFromValue($object['internal_reference']),
'external_id' => $this->stringFromValue($object['external_id']),
'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->stringFromValue($object['bunq_payment_id']),
'sepa_cc' => $this->stringFromValue($object['sepa_cc']),
'sepa_ct_op' => $this->stringFromValue($object['sepa_ct_op']),
'sepa_ct_id' => $this->stringFromValue($object['sepa_ct_id']),
'sepa_db' => $this->stringFromValue($object['sepa_db']),
'sepa_country' => $this->stringFromValue($object['sepa_country']),
'sepa_ep' => $this->stringFromValue($object['sepa_ep']),
'sepa_ci' => $this->stringFromValue($object['sepa_ci']),
'sepa_batch_id' => $this->stringFromValue($object['sepa_batch_id']),
// custom date fields. Must be Carbon objects. Presence is optional.
'interest_date' => $this->dateFromValue($object['interest_date']),
'book_date' => $this->dateFromValue($object['book_date']),
'process_date' => $this->dateFromValue($object['process_date']),
'due_date' => $this->dateFromValue($object['due_date']),
'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']),
];
}
return $return;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* RenameMetaFields.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Console\Commands\Correction;
use DB;
use Illuminate\Console\Command;
/**
* Class RenameMetaFields
*/
class RenameMetaFields extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rename changed meta fields.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:rename-meta-fields';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$start = microtime(true);
$changes = [
'original-source' => 'original_source',
'importHash' => 'import_hash',
'importHashV2' => 'import_hash_v2',
'sepa-cc' => 'sepa_cc',
'sepa-ct-op' => 'sepa_ct_op',
'sepa-ct-id' => 'sepa_ct_id',
'sepa-db' => 'sepa_db',
'sepa-country' => 'sepa_country',
'sepa-ep' => 'sepa_ep',
'sepa-ci' => 'sepa_ci',
'sepa-batch-id' => 'sepa_batch_id',
];
foreach ($changes as $original => $update) {
$this->rename($original, $update);
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Renamed meta fields in %s seconds', $end));
return 0;
}
/**
* @param string $original
* @param string $update
*/
private function rename(string $original, string $update): void
{
DB::table('journal_meta')
->where('name', '=', $original)
->update(['name' => $update]);
}
}

View File

@@ -240,20 +240,20 @@ class MigrateToGroups extends Command
$notes = $this->journalRepository->getNoteText($journal);
$tags = $this->journalRepository->getTags($journal);
$internalRef = $this->journalRepository->getMetaField($journal, 'internal-reference');
$sepaCC = $this->journalRepository->getMetaField($journal, 'sepa-cc');
$sepaCtOp = $this->journalRepository->getMetaField($journal, 'sepa-ct-op');
$sepaCtId = $this->journalRepository->getMetaField($journal, 'sepa-ct-id');
$sepaDb = $this->journalRepository->getMetaField($journal, 'sepa-db');
$sepaCountry = $this->journalRepository->getMetaField($journal, 'sepa-country');
$sepaEp = $this->journalRepository->getMetaField($journal, 'sepa-ep');
$sepaCi = $this->journalRepository->getMetaField($journal, 'sepa-ci');
$sepaBatchId = $this->journalRepository->getMetaField($journal, 'sepa-batch-id');
$sepaCC = $this->journalRepository->getMetaField($journal, 'sepa_cc');
$sepaCtOp = $this->journalRepository->getMetaField($journal, 'sepa_ct_op');
$sepaCtId = $this->journalRepository->getMetaField($journal, 'sepa_ct_id');
$sepaDb = $this->journalRepository->getMetaField($journal, 'sepa_db');
$sepaCountry = $this->journalRepository->getMetaField($journal, 'sepa_country');
$sepaEp = $this->journalRepository->getMetaField($journal, 'sepa_ep');
$sepaCi = $this->journalRepository->getMetaField($journal, 'sepa_ci');
$sepaBatchId = $this->journalRepository->getMetaField($journal, 'sepa_batch_id');
$externalId = $this->journalRepository->getMetaField($journal, 'external-id');
$originalSource = $this->journalRepository->getMetaField($journal, 'original-source');
$recurrenceId = $this->journalRepository->getMetaField($journal, 'recurrence_id');
$bunq = $this->journalRepository->getMetaField($journal, 'bunq_payment_id');
$hash = $this->journalRepository->getMetaField($journal, 'importHash');
$hashTwo = $this->journalRepository->getMetaField($journal, 'importHashV2');
$hash = $this->journalRepository->getMetaField($journal, 'import_hash');
$hashTwo = $this->journalRepository->getMetaField($journal, 'import_hash_v2');
$interestDate = $this->journalRepository->getMetaDate($journal, 'interest_date');
$bookDate = $this->journalRepository->getMetaDate($journal, 'book_date');
$processDate = $this->journalRepository->getMetaDate($journal, 'process_date');
@@ -293,20 +293,20 @@ class MigrateToGroups extends Command
'notes' => $notes,
'tags' => $tags,
'internal_reference' => $internalRef,
'sepa-cc' => $sepaCC,
'sepa-ct-op' => $sepaCtOp,
'sepa-ct-id' => $sepaCtId,
'sepa-db' => $sepaDb,
'sepa-country' => $sepaCountry,
'sepa-ep' => $sepaEp,
'sepa-ci' => $sepaCi,
'sepa-batch-id' => $sepaBatchId,
'sepa_cc' => $sepaCC,
'sepa_ct_op' => $sepaCtOp,
'sepa_ct_id' => $sepaCtId,
'sepa_db' => $sepaDb,
'sepa_country' => $sepaCountry,
'sepa_ep' => $sepaEp,
'sepa_ci' => $sepaCi,
'sepa_batch_id' => $sepaBatchId,
'external_id' => $externalId,
'original-source' => $originalSource,
'recurrence_id' => $recurrenceId,
'bunq_payment_id' => $bunq,
'importHash' => $hash,
'importHashV2' => $hashTwo,
'import_hash' => $hash,
'import_hash_v2' => $hashTwo,
'interest_date' => $interestDate,
'book_date' => $bookDate,
'process_date' => $processDate,

View File

@@ -31,10 +31,10 @@ use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\NullArrayObject;
use FireflyIII\User;
use FireflyIII\Validation\AccountValidator;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Log;
@@ -46,6 +46,8 @@ class TransactionFactory
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var AccountValidator */
private $accountValidator;
/** @var TransactionJournal */
private $journal;
/** @var User */
@@ -60,6 +62,7 @@ class TransactionFactory
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
}
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->accountValidator = app(AccountValidator::class);
}
/**
@@ -110,16 +113,17 @@ class TransactionFactory
*/
public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection
{
$sourceAccount = $this->getAccount('source', $data['source'], (int)$data['source_id'], $data['source_name']);
$destinationAccount = $this->getAccount('destination', $data['destination'], (int)$data['destination_id'], $data['destination_name']);
$amount = $this->getAmount($data['amount']);
$foreignAmount = $this->getForeignAmount($data['foreign_amount']);
// validate source and destination using a new Validator.
$this->validateAccounts($data);
$this->makeDramaOverAccountTypes($sourceAccount, $destinationAccount);
// create or get source and destination accounts:
$sourceAccount = $this->getAccount('source', (int)$data['source_id'], $data['source_name']);
$destinationAccount = $this->getAccount('destination', (int)$data['destination_id'], $data['destination_name']);
$one = $this->create($sourceAccount, $currency, app('steam')->negative($amount));
$two = $this->create($destinationAccount, $currency, app('steam')->positive($amount));
$amount = $this->getAmount($data['amount']);
$foreignAmount = $this->getForeignAmount($data['foreign_amount']);
$one = $this->create($sourceAccount, $currency, app('steam')->negative($amount));
$two = $this->create($destinationAccount, $currency, app('steam')->positive($amount));
$one->reconciled = $data['reconciled'] ?? false;
$two->reconciled = $data['reconciled'] ?? false;
@@ -128,8 +132,8 @@ class TransactionFactory
if (null !== $foreignCurrency) {
$one->foreign_currency_id = $foreignCurrency->id;
$two->foreign_currency_id = $foreignCurrency->id;
$one->foreign_amount = $foreignAmount;
$two->foreign_amount = $foreignAmount;
$one->foreign_amount = app('steam')->negative($foreignAmount);
$two->foreign_amount = app('steam')->positive($foreignAmount);
}
@@ -141,18 +145,21 @@ class TransactionFactory
}
/**
* @param string $direction
* @param Account|null $source
* @param int|null $sourceId
* @param string|null $sourceName
* @param string $direction
* @param int|null $accountId
* @param string|null $accountName
*
* @return Account
* @throws FireflyException
*/
public function getAccount(string $direction, ?Account $source, ?int $sourceId, ?string $sourceName): Account
public function getAccount(string $direction, ?int $accountId, ?string $accountName): Account
{
Log::debug(sprintf('Now in getAccount(%s)', $direction));
Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true)));
// some debug logging:
Log::debug(sprintf('Now in getAccount(%s, %d, %s)', $direction, $accountId, $accountName));
// final result:
$result = null;
// expected type of source account, in order of preference
/** @var array $array */
$array = config('firefly.expected_source_types');
@@ -161,64 +168,62 @@ class TransactionFactory
// and now try to find it, based on the type of transaction.
$transactionType = $this->journal->transactionType->type;
Log::debug(
sprintf(
'Based on the fact that the transaction is a %s, the %s account should be in: %s', $transactionType, $direction,
implode(', ', $expectedTypes[$transactionType])
)
);
$message = 'Based on the fact that the transaction is a %s, the %s account should be in: %s';
Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType])));
// first attempt, check the "source" object.
if (null !== $source && $source->user_id === $this->user->id && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) {
Log::debug(sprintf('Found "account" object for %s: #%d, %s', $direction, $source->id, $source->name));
return $source;
}
// second attempt, find by ID.
if (null !== $sourceId) {
$source = $this->accountRepository->findNull($sourceId);
if (null !== $source && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) {
// first attempt, find by ID.
if (null !== $accountId) {
$search = $this->accountRepository->findNull($accountId);
if (null !== $search && in_array($search->accountType->type, $expectedTypes[$transactionType], true)) {
Log::debug(
sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $source->id, $source->name, $source->accountType->type)
sprintf('Found "account_id" object for %s: #%d, "%s" of type %s', $direction, $search->id, $search->name, $search->accountType->type)
);
return $source;
$result = $search;
}
}
// third attempt, find by name.
if (null !== $sourceName) {
// second attempt, find by name.
if (null === $result && null !== $accountName) {
Log::debug('Found nothing by account ID.');
// find by preferred type.
$source = $this->accountRepository->findByName($sourceName, [$expectedTypes[$transactionType][0]]);
// or any type.
$source = $source ?? $this->accountRepository->findByName($sourceName, $expectedTypes[$transactionType]);
$source = $this->accountRepository->findByName($accountName, [$expectedTypes[$transactionType][0]]);
// or any expected type.
$source = $source ?? $this->accountRepository->findByName($accountName, $expectedTypes[$transactionType]);
if (null !== $source) {
Log::debug(sprintf('Found "account_name" object for %s: #%d, %s', $direction, $source->id, $source->name));
return $source;
$result = $source;
}
}
if (null === $sourceName && \in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) {
return $this->accountRepository->getCashAccount();
}
$sourceName = $sourceName ?? '(no name)';
// final attempt, create it.
$preferredType = $expectedTypes[$transactionType][0];
if (AccountType::ASSET === $preferredType) {
throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with ID #%d or name "%s".', $sourceId, $sourceName));
// return cash account.
if (null === $result && null === $accountName
&& in_array(AccountType::CASH, $expectedTypes[$transactionType], true)) {
$result = $this->accountRepository->getCashAccount();
}
return $this->accountRepository->store(
[
'account_type_id' => null,
'accountType' => $preferredType,
'name' => $sourceName,
'active' => true,
'iban' => null,
]
);
// return new account.
if (null === $result) {
$accountName = $accountName ?? '(no name)';
// final attempt, create it.
$preferredType = $expectedTypes[$transactionType][0];
if (AccountType::ASSET === $preferredType) {
throw new FireflyException(sprintf('TransactionFactory: Cannot create asset account with ID #%d or name "%s".', $accountId, $accountName));
}
$result = $this->accountRepository->store(
[
'account_type_id' => null,
'accountType' => $preferredType,
'name' => $accountName,
'active' => true,
'iban' => null,
]
);
}
return $result;
}
/**
@@ -246,6 +251,7 @@ class TransactionFactory
*/
public function getForeignAmount(?string $amount): ?string
{
$result = null;
if (null === $amount) {
Log::debug('No foreign amount info in array. Return NULL');
@@ -266,31 +272,6 @@ class TransactionFactory
return $amount;
}
/**
* This method will throw a Firefly III Exception of the source and destination account types are not OK.
*
* @throws FireflyException
*
* @param Account $source
* @param Account $destination
*/
public function makeDramaOverAccountTypes(Account $source, Account $destination): void
{
// if the source is X, then Y is allowed as destination.
$combinations = config('firefly.source_dests');
$sourceType = $source->accountType->type;
$destType = $destination->accountType->type;
$journalType = $this->journal->transactionType->type;
$allowed = $combinations[$journalType][$sourceType] ?? [];
if (!\in_array($destType, $allowed, true)) {
throw new FireflyException(
sprintf(
'Journal of type "%s" has a source account of type "%s" and cannot accept a "%s"-account as destination, but only accounts of: %s', $journalType, $sourceType,
$destType, implode(', ', $combinations[$journalType][$sourceType])
)
);
}
}
/**
* @param TransactionJournal $journal
@@ -308,4 +289,33 @@ class TransactionFactory
$this->user = $user;
$this->accountRepository->setUser($user);
}
/**
* @param NullArrayObject $data
*
* @throws FireflyException
*/
private function validateAccounts(NullArrayObject $data): void
{
$transactionType = $data['type'] ?? 'invalid';
$this->accountValidator->setTransactionType($transactionType);
// validate source account.
$sourceId = isset($data['source_id']) ? (int)$data['source_id'] : null;
$sourceName = $data['source_name'] ?? null;
$validSource = $this->accountValidator->validateSource($sourceId, $sourceName);
// do something with result:
if (false === $validSource) {
throw new FireflyException($this->accountValidator->sourceError);
}
// validate destination account
$destinationId = isset($data['destination_id']) ? (int)$data['destination_id'] : null;
$destinationName = $data['destination_name'] ?? null;
$validDestination = $this->accountValidator->validateDestination($destinationId, $destinationName);
// do something with result:
if (false === $validDestination) {
throw new FireflyException($this->accountValidator->destError);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* TransactionGroupFactory.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Factory;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\User;
/**
* Class TransactionGroupFactory
*/
class TransactionGroupFactory
{
/** @var TransactionJournalFactory */
private $journalFactory;
/** @var User The user */
private $user;
/**
* TransactionGroupFactory constructor.
*/
public function __construct()
{
$this->journalFactory = app(TransactionJournalFactory::class);
}
/**
* Store a new transaction journal.
*
* @param array $data
*
* @return TransactionGroup
* @throws FireflyException
*/
public function create(array $data): TransactionGroup
{
$this->journalFactory->setUser($this->user);
$collection = $this->journalFactory->create($data);
$title = $data['group_title'] ?? null;
/** @var TransactionJournal $first */
$first = $collection->first();
$group = new TransactionGroup;
$group->user()->associate($first->user);
$group->title = $title ?? $first->description;
$group->save();
$group->transactionJournals()->saveMany($collection);
return $group;
}
/**
* Set the user.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
}

View File

@@ -26,9 +26,9 @@ namespace FireflyIII\Factory;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Note;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
@@ -77,14 +77,25 @@ class TransactionJournalFactory
*/
public function __construct()
{
$this->fields = ['sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', 'sepa-ci', 'interest_date', 'book_date', 'process_date',
'due_date', 'recurrence_id', 'payment_date', 'invoice_date', 'internal_reference', 'bunq_payment_id', 'importHash', 'importHashV2',
'external_id', 'sepa-batch-id', 'original-source'];
$this->fields = [
// sepa
'sepa_cc', 'sepa_ct_op', 'sepa_ct_id',
'sepa_db', 'sepa_country', 'sepa_ep',
'sepa_ci', 'sepa_batch_id',
// dates
'interest_date', 'book_date', 'process_date',
'due_date', 'payment_date', 'invoice_date',
// others
'recurrence_id', 'internal_reference', 'bunq_payment_id',
'import_hash', 'import_hash_v2', 'external_id', 'original_source'];
if ('testing' === config('app.env')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
}
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
$this->typeRepository = app(TransactionTypeRepositoryInterface::class);
$this->transactionFactory = app(TransactionFactory::class);
@@ -97,26 +108,22 @@ class TransactionJournalFactory
}
/**
* Store a new transaction journal.
* Store a new (set of) transaction journals.
*
* @param array $data
*
* @return TransactionGroup
* @throws Exception
* @return Collection
* @throws FireflyException
*/
public function create(array $data): TransactionGroup
public function create(array $data): Collection
{
// convert to special object.
$data = new NullArrayObject($data);
Log::debug('Start of TransactionJournalFactory::create()');
$collection = new Collection;
$transactions = $data['transactions'] ?? [];
$type = $this->typeRepository->findTransactionType(null, $data['type']);
$carbon = $data['date'] ?? new Carbon;
$carbon->setTimezone(config('app.timezone'));
Log::debug(sprintf('Going to store a %s.', $type->type));
if (0 === \count($transactions)) {
if (0 === count($transactions)) {
Log::error('There are no transactions in the array, the TransactionJournalFactory cannot continue.');
return new Collection;
@@ -124,79 +131,15 @@ class TransactionJournalFactory
/** @var array $row */
foreach ($transactions as $index => $row) {
$transaction = new NullArrayObject($row);
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions)));
/** Get basic fields */
Log::debug(sprintf('Now creating journal %d/%d', $index + 1, count($transactions)));
$currency = $this->currencyRepository->findCurrency(
$transaction['currency'], (int)$transaction['currency_id'], $transaction['currency_code']
);
$foreignCurrency = $this->findForeignCurrency($transaction);
$bill = $this->billRepository->findBill($transaction['bill'], (int)$transaction['bill_id'], $transaction['bill_name']);
$billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null;
$description = app('steam')->cleanString((string)$transaction['description']);
/** Create a basic journal. */
$journal = TransactionJournal::create(
[
'user_id' => $this->user->id,
'transaction_type_id' => $type->id,
'bill_id' => $billId,
'transaction_currency_id' => $currency->id,
'description' => '' === $description ? '(empty description)' : $description,
'date' => $carbon->format('Y-m-d H:i:s'),
'order' => 0,
'tag_count' => 0,
'completed' => 0,
]
);
Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description));
/** Create two transactions. */
$this->transactionFactory->setJournal($journal);
$this->transactionFactory->createPair($transaction, $currency, $foreignCurrency);
// verify that journal has two transactions. Otherwise, delete and cancel.
$count = $journal->transactions()->count();
if (2 !== $count) {
// @codeCoverageIgnoreStart
Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count));
$journal->delete();
return new Collection;
// @codeCoverageIgnoreEnd
$journal = $this->createJournal(new NullArrayObject($row));
if (null !== $journal) {
$collection->push($journal);
}
$journal->completed =true;
$journal->save();
/** Link all other data to the journal. */
/** Link budget */
$this->storeBudget($journal, $transaction);
/** Link category */
$this->storeCategory($journal, $transaction);
/** Set notes */
$this->storeNote($journal, $transaction['notes']);
/** Set piggy bank */
$this->storePiggyEvent($journal, $transaction);
/** Set tags */
$this->storeTags($journal, $transaction['tags']);
/** set all meta fields */
$this->storeMetaFields($journal, $transaction);
$collection->push($journal);
}
$group = $this->storeGroup($collection, $data['group_title']);
return $group;
return $collection;
}
/**
@@ -215,31 +158,6 @@ class TransactionJournalFactory
$this->piggyRepository->setUser($this->user);
}
/**
* Join multiple journals in a group.
*
* @param Collection $collection
* @param string|null $title
*
* @return TransactionGroup|null
*/
public function storeGroup(Collection $collection, ?string $title): ?TransactionGroup
{
if ($collection->count() < 2) {
return null; // @codeCoverageIgnore
}
/** @var TransactionJournal $first */
$first = $collection->first();
$group = new TransactionGroup;
$group->user()->associate($first->user);
$group->title = $title ?? $first->description;
$group->save();
$group->transactionJournals()->saveMany($collection);
return $group;
}
/**
* Link a piggy bank to this journal.
*
@@ -332,6 +250,88 @@ class TransactionJournalFactory
}
}
/**
* @param NullArrayObject $row
*
* @return TransactionJournal|null
* @throws FireflyException
*/
private function createJournal(NullArrayObject $row): ?TransactionJournal
{
$row['import_hash_v2'] = $this->hashArray($row);
/** Get basic fields */
$type = $this->typeRepository->findTransactionType(null, $row['type']);
$carbon = $row['date'] ?? new Carbon;
$currency = $this->currencyRepository->findCurrency((int)$row['currency_id'], $row['currency_code']);
$foreignCurrency = $this->findForeignCurrency($row);
$bill = $this->billRepository->findBill((int)$row['bill_id'], $row['bill_name']);
$billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null;
$description = app('steam')->cleanString((string)$row['description']);
/** Manipulate basic fields */
$carbon->setTimezone(config('app.timezone'));
/** Create a basic journal. */
$journal = TransactionJournal::create(
[
'user_id' => $this->user->id,
'transaction_type_id' => $type->id,
'bill_id' => $billId,
'transaction_currency_id' => $currency->id,
'description' => '' === $description ? '(empty description)' : $description,
'date' => $carbon->format('Y-m-d H:i:s'),
'order' => 0,
'tag_count' => 0,
'completed' => 0,
]
);
Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description));
/** Create two transactions. */
$this->transactionFactory->setJournal($journal);
$this->transactionFactory->createPair($row, $currency, $foreignCurrency);
// verify that journal has two transactions. Otherwise, delete and cancel.
$count = $journal->transactions()->count();
if (2 !== $count) {
// @codeCoverageIgnoreStart
Log::error(sprintf('The journal unexpectedly has %d transaction(s). This is not OK. Cancel operation.', $count));
try {
$journal->delete();
} catch (Exception $e) {
Log::debug(sprintf('Dont care: %s.', $e->getMessage()));
}
return null;
// @codeCoverageIgnoreEnd
}
$journal->completed = true;
$journal->save();
/** Link all other data to the journal. */
/** Link budget */
$this->storeBudget($journal, $row);
/** Link category */
$this->storeCategory($journal, $row);
/** Set notes */
$this->storeNote($journal, $row['notes']);
/** Set piggy bank */
$this->storePiggyEvent($journal, $row);
/** Set tags */
$this->storeTags($journal, $row['tags']);
/** set all meta fields */
$this->storeMetaFields($journal, $row);
return $journal;
}
/**
* This is a separate function because "findCurrency" will default to EUR and that may not be what we want.
*
@@ -345,17 +345,38 @@ class TransactionJournalFactory
return null;
}
return $this->currencyRepository->findCurrency(
$transaction['foreign_currency'], (int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code']
);
return $this->currencyRepository->findCurrency((int)$transaction['foreign_currency_id'], $transaction['foreign_currency_code']);
}
/**
* @param NullArrayObject $row
*
* @return string
*/
private function hashArray(NullArrayObject $row): string
{
$row['import_hash_v2'] = null;
$row['original_source'] = null;
$json = json_encode($row);
if (false === $json) {
$json = json_encode((string)microtime());
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash));
return $hash;
}
/**
* @param TransactionJournal $journal
* @param NullArrayObject $data
*/
private function storeBudget(TransactionJournal $journal, NullArrayObject $data): void
{
if (TransactionType::WITHDRAWAL !== $journal->transactionType->type) {
return;
}
$budget = $this->budgetRepository->findBudget($data['budget'], $data['budget_id'], $data['budget_name']);
if (null !== $budget) {
Log::debug(sprintf('Link budget #%d to journal #%d', $budget->id, $journal->id));

View File

@@ -86,18 +86,24 @@ class GroupCollector implements GroupCollectorInterface
$this->limit = 50;
$this->page = 0;
$this->fields = [
# group
'transaction_groups.id as transaction_group_id',
'transaction_groups.user_id as user_id',
'transaction_groups.created_at as created_at',
'transaction_groups.updated_at as updated_at',
'transaction_groups.title as transaction_group_title',
# journal
'transaction_journals.id as transaction_journal_id',
'transaction_journals.transaction_type_id',
'transaction_types.type as transaction_type_type',
'transaction_journals.description',
'transaction_journals.date',
# source info (always present)
'source.id as source_transaction_id',
'source.account_id as source_account_id',
'source.reconciled',
# currency info:
'source.amount as amount',
@@ -113,7 +119,8 @@ class GroupCollector implements GroupCollectorInterface
'foreign_currency.symbol as foreign_currency_symbol',
'foreign_currency.decimal_places as foreign_currency_decimal_places',
# destination account info:
# destination account info (always present)
#'destination.id as destination_transaction_id', // not interesting.
'destination.account_id as destination_account_id',
];
}
@@ -368,6 +375,8 @@ class GroupCollector implements GroupCollectorInterface
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', $transactionGroup->id);
return $this;
}
/**
@@ -534,46 +543,48 @@ class GroupCollector implements GroupCollectorInterface
private function parseArray(Collection $collection): Collection
{
$groups = [];
/** @var TransactionGroup $augumentedGroup */
foreach ($collection as $augumentedGroup) {
$groupId = $augumentedGroup->transaction_group_id;
/** @var TransactionGroup $augmentedGroup */
foreach ($collection as $augmentedGroup) {
$groupId = $augmentedGroup->transaction_group_id;
if (!isset($groups[$groupId])) {
// make new array
$groupArray = [
'id' => $augumentedGroup->id,
'title' => $augumentedGroup->title,
'id' => $augmentedGroup->transaction_group_id,
'user_id' => $augmentedGroup->user_id,
'title' => $augmentedGroup->title,
'count' => 1,
'sum' => $augumentedGroup->amount,
'foreign_sum' => $augumentedGroup->foreign_amount ?? '0',
'sum' => $augmentedGroup->amount,
'foreign_sum' => $augmentedGroup->foreign_amount ?? '0',
'transactions' => [],
];
$journalId = (int)$augumentedGroup->transaction_journal_id;
$groupArray['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup);
$journalId = (int)$augmentedGroup->transaction_journal_id;
$groupArray['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup);
$groups[$groupId] = $groupArray;
continue;
}
$groups[$groupId]['count']++;
$groups[$groupId]['sum'] = bcadd($augumentedGroup->amount, $groups[$groupId]['sum']);
$groups[$groupId]['foreign_sum'] = bcadd($augumentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']);
$journalId = (int)$augumentedGroup->transaction_journal_id;
$groups[$groupId]['transactions'][$journalId] = $this->parseAugumentedGroup($augumentedGroup);
$groups[$groupId]['sum'] = bcadd($augmentedGroup->amount, $groups[$groupId]['sum']);
$groups[$groupId]['foreign_sum'] = bcadd($augmentedGroup->foreign_amount ?? '0', $groups[$groupId]['foreign_sum']);
$journalId = (int)$augmentedGroup->transaction_journal_id;
$groups[$groupId]['transactions'][$journalId] = $this->parseAugmentedGroup($augmentedGroup);
}
return new Collection($groups);
}
/**
* @param TransactionGroup $augumentedGroup
* @param TransactionGroup $augmentedGroup
*
* @throws Exception
* @return array
* @throws Exception
*/
private function parseAugumentedGroup(TransactionGroup $augumentedGroup): array
private function parseAugmentedGroup(TransactionGroup $augmentedGroup): array
{
$result = $augumentedGroup->toArray();
$result = $augmentedGroup->toArray();
$result['date'] = new Carbon($result['date']);
$result['created_at'] = new Carbon($result['created_at']);
$result['updated_at'] = new Carbon($result['updated_at']);
$result['reconciled'] = 1 === (int)$result['reconciled'];
return $result;
}

View File

@@ -24,6 +24,7 @@ namespace FireflyIII\Http\Requests;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException;
use Exception;
use Illuminate\Foundation\Http\FormRequest;
use Log;
@@ -36,6 +37,26 @@ use Log;
*/
class Request extends FormRequest
{
/**
* @param $array
*
* @return array|null
*/
public function arrayFromValue($array): ?array
{
if (is_array($array)) {
return $array;
}
if (null === $array) {
return null;
}
if (is_string($array)) {
return explode(',', $array);
}
return null;
}
/**
* Return a boolean value.
*
@@ -60,11 +81,17 @@ class Request extends FormRequest
*
* @return bool
*/
public function convertBoolean(string $value): bool
public function convertBoolean(?string $value): bool
{
if (null === $value) {
return false;
}
if ('true' === $value) {
return true;
}
if ('yes' === $value) {
return true;
}
if (1 === $value) {
return true;
}
@@ -78,6 +105,30 @@ class Request extends FormRequest
return false;
}
/**
* @param string|null $string
*
* @return Carbon|null
*/
public function dateFromValue(?string $string): ?Carbon
{
if (null === $string) {
return null;
}
if ('' === $string) {
return null;
}
try {
$carbon = new Carbon($string);
} catch (Exception $e) {
Log::debug(sprintf('Invalid date: %s: %s', $string, $e->getMessage()));
return null;
}
return $carbon;
}
/**
* Return floating value.
*
@@ -107,6 +158,22 @@ class Request extends FormRequest
return (int)$this->get($field);
}
/**
* Parse to integer
*
* @param string|null $string
*
* @return int|null
*/
public function integerFromValue(?string $string): ?int
{
if (null === $string) {
return null;
}
return (int)$string;
}
/**
* Return string value.
*
@@ -121,12 +188,31 @@ class Request extends FormRequest
return app('steam')->cleanString((string)($this->get($field) ?? ''));
}
/**
* Parse and clean a string.
*
* @param string|null $string
*
* @return string|null
*/
public function stringFromValue(?string $string): ?string
{
if (null === $string) {
return null;
}
$result = app('steam')->cleanString($string);
return '' === $result ? null : $result;
}
/**
* Return date or NULL.
*
* @param string $field
*
* @return Carbon|null
* @throws Exception
*/
protected function date(string $field): ?Carbon
{

View File

@@ -220,12 +220,12 @@ class ImportArrayStorage
*/
private function getHash(array $transaction): string
{
unset($transaction['importHashV2'], $transaction['original-source']);
unset($transaction['import_hash_v2'], $transaction['original_source']);
$json = json_encode($transaction);
if (false === $json) {
// @codeCoverageIgnoreStart
/** @noinspection ForgottenDebugOutputInspection */
Log::error('Could not encode import array.', print_r($transaction, true));
Log::error('Could not encode import array.', $transaction);
throw new FireflyException('Could not encode import array. Please see the logs.');
// @codeCoverageIgnoreEnd
}
@@ -438,7 +438,7 @@ class ImportArrayStorage
Log::warning(sprintf('Row #%d seems to be a duplicate entry and will be ignored.', $index));
continue;
}
$transaction['importHashV2'] = $this->getHash($transaction);
$transaction['import_hash_v2'] = $this->getHash($transaction);
$toStore[] = $transaction;
}
$count = \count($toStore);

View File

@@ -75,6 +75,8 @@ use FireflyIII\Validation\FireflyValidator;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Twig;
use Twig_Extension_Debug;
use TwigBridge\Extension\Loader\Functions;
use Validator;
/**
@@ -98,19 +100,18 @@ class FireflyServiceProvider extends ServiceProvider
}
);
$config = app('config');
//Twig::addExtension(new Functions($config));
//Twig::addRuntimeLoader(new TransactionLoader);
//Twig::addRuntimeLoader(new AccountLoader);
//Twig::addRuntimeLoader(new TransactionJournalLoader);
//Twig::addRuntimeLoader(new TransactionGroupLoader);
Twig::addExtension(new Functions($config));
Twig::addRuntimeLoader(new TransactionLoader);
Twig::addRuntimeLoader(new AccountLoader);
Twig::addRuntimeLoader(new TransactionJournalLoader);
Twig::addExtension(new General);
Twig::addExtension(new TransactionGroupTwig);
//Twig::addExtension(new Journal);
Twig::addExtension(new Journal);
Twig::addExtension(new Translation);
//Twig::addExtension(new Transaction);
//Twig::addExtension(new Rule);
Twig::addExtension(new Transaction);
Twig::addExtension(new Rule);
Twig::addExtension(new AmountFormat);
//Twig::addExtension(new Twig_Extension_Debug);
Twig::addExtension(new Twig_Extension_Debug);
}
/**

View File

@@ -89,21 +89,13 @@ class BillRepository implements BillRepositoryInterface
/**
* Find bill by parameters.
*
* @param Bill|null $bill
* @param int|null $billId
* @param string|null $billName
*
* @return Bill|null
*/
public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill
public function findBill( ?int $billId, ?string $billName): ?Bill
{
Log::debug('Searching for bill information.');
if ($bill instanceof Bill && $bill->user_id === $this->user->id) {
Log::debug(sprintf('Bill object in parameters, will return Bill #%d', $bill->id));
return $bill;
}
if (null !== $billId) {
$searchResult = $this->find((int)$billId);
if (null !== $searchResult) {

View File

@@ -52,13 +52,12 @@ interface BillRepositoryInterface
/**
* Find bill by parameters.
*
* @param Bill|null $bill
* @param int|null $billId
* @param string|null $billName
*
* @return Bill|null
*/
public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill;
public function findBill(?int $billId, ?string $billName): ?Bill;
/**
* Find a bill by name.

View File

@@ -195,8 +195,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface
*
* @param string $currencyCode
*
* @deprecated
* @return TransactionCurrency|null
* @deprecated
*/
public function findByCodeNull(string $currencyCode): ?TransactionCurrency
{
@@ -221,8 +221,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface
*
* @param string $currencyName
*
* @deprecated
* @return TransactionCurrency
* @deprecated
*/
public function findByNameNull(string $currencyName): ?TransactionCurrency
{
@@ -247,8 +247,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface
*
* @param string $currencySymbol
*
* @deprecated
* @return TransactionCurrency
* @deprecated
*/
public function findBySymbolNull(string $currencySymbol): ?TransactionCurrency
{
@@ -258,25 +258,15 @@ class CurrencyRepository implements CurrencyRepositoryInterface
/**
* Find by object, ID or code. Returns user default or system default.
*
* @param TransactionCurrency|null $currency
* @param int|null $currencyId
* @param string|null $currencyCode
* @param int|null $currencyId
* @param string|null $currencyCode
*
* @return TransactionCurrency|null
*/
public function findCurrency(?TransactionCurrency $currency, ?int $currencyId, ?string $currencyCode): TransactionCurrency
public function findCurrency(?int $currencyId, ?string $currencyCode): TransactionCurrency
{
Log::debug('Now in findCurrency()');
$result = null;
if (null !== $currency) {
Log::debug(sprintf('Parameters contain %s, will return this.', $currency->code));
$result = $currency;
}
if (null === $result) {
Log::debug(sprintf('Searching for currency with ID #%d...', $currencyId));
$result = $this->find((int)$currencyId);
}
$result = $this->find((int)$currencyId);
if (null === $result) {
Log::debug(sprintf('Searching for currency with code %s...', $currencyCode));
$result = $this->findByCode((string)$currencyCode);
@@ -304,8 +294,8 @@ class CurrencyRepository implements CurrencyRepositoryInterface
*
* @param int $currencyId
*
* @deprecated
* @return TransactionCurrency|null
* @deprecated
*/
public function findNull(int $currencyId): ?TransactionCurrency
{

View File

@@ -135,13 +135,12 @@ interface CurrencyRepositoryInterface
/**
* Find by object, ID or code. Returns user default or system default.
*
* @param TransactionCurrency|null $currency
* @param int|null $currencyId
* @param string|null $currencyCode
*
* @return TransactionCurrency|null
*/
public function findCurrency(?TransactionCurrency $currency, ?int $currencyId, ?string $currencyCode): TransactionCurrency;
public function findCurrency(?int $currencyId, ?string $currencyCode): TransactionCurrency;
/**
* Find by ID, return NULL if not found.

View File

@@ -26,6 +26,7 @@ use Carbon\Carbon;
use DB;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionGroupFactory;
use FireflyIII\Factory\TransactionJournalFactory;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
@@ -175,7 +176,7 @@ class JournalRepository implements JournalRepositoryInterface
$result = TransactionJournalMeta::withTrashed()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('hash', $hashOfHash)
->where('name', 'importHashV2')
->where('name', 'import_hash_v2')
->first(['journal_meta.*']);
if (null === $result) {
Log::debug('Result is null');
@@ -782,8 +783,8 @@ class JournalRepository implements JournalRepositoryInterface
*/
public function store(array $data): TransactionGroup
{
/** @var TransactionJournalFactory $factory */
$factory = app(TransactionJournalFactory::class);
/** @var TransactionGroupFactory $factory */
$factory = app(TransactionGroupFactory::class);
$factory->setUser($this->user);
return $factory->create($data);

View File

@@ -185,8 +185,8 @@ class ImportTransaction
return;
}
$meta = ['sepa-ct-id', 'sepa-ct-op', 'sepa-db', 'sepa-cc', 'sepa-country', 'sepa-batch-id', 'sepa-ep', 'sepa-ci', 'internal-reference', 'date-interest',
'date-invoice', 'date-book', 'date-payment', 'date-process', 'date-due', 'original-source'];
$meta = ['sepa_ct_id', 'sepa_ct_op', 'sepa_db', 'sepa_cc', 'sepa_country', 'sepa_batch_id', 'sepa_ep', 'sepa_ci', 'internal_reference', 'date_interest',
'date_invoice', 'date_book', 'date_payment', 'date_process', 'date_due', 'original_source'];
Log::debug(sprintf('Now going to check role "%s".', $role));
if (\in_array($role, $meta, true)) {
Log::debug(sprintf('Role "%s" is in allowed meta roles, so store its value "%s".', $role, $columnValue->getValue()));

View File

@@ -226,14 +226,14 @@ class ImportableConverter
// all custom fields:
'internal_reference' => $importable->meta['internal-reference'] ?? null,
'sepa-cc' => $importable->meta['sepa-cc'] ?? null,
'sepa-ct-op' => $importable->meta['sepa-ct-op'] ?? null,
'sepa-ct-id' => $importable->meta['sepa-ct-id'] ?? null,
'sepa-db' => $importable->meta['sepa-db'] ?? null,
'sepa-country' => $importable->meta['sepa-country'] ?? null,
'sepa-ep' => $importable->meta['sepa-ep'] ?? null,
'sepa-ci' => $importable->meta['sepa-ci'] ?? null,
'sepa-batch-id' => $importable->meta['sepa-batch-id'] ?? null,
'sepa_cc' => $importable->meta['sepa_cc'] ?? null,
'sepa_ct_op' => $importable->meta['sepa_ct_op'] ?? null,
'sepa_ct_id' => $importable->meta['sepa_ct_id'] ?? null,
'sepa_db' => $importable->meta['sepa_db'] ?? null,
'sepa_country' => $importable->meta['sepa_country'] ?? null,
'sepa_ep' => $importable->meta['sepa_ep'] ?? null,
'sepa_ci' => $importable->meta['sepa_ci'] ?? null,
'sepa_batch_id' => $importable->meta['sepa_batch_id'] ?? null,
'interest_date' => $this->convertDateValue($importable->meta['date-interest'] ?? null),
'book_date' => $this->convertDateValue($importable->meta['date-book'] ?? null),
'process_date' => $this->convertDateValue($importable->meta['date-process'] ?? null),

View File

@@ -48,9 +48,9 @@ class TransactionGroupTransformer extends AbstractTransformer
{
$this->groupRepos = app(TransactionGroupRepositoryInterface::class);
$this->metaFields = [
'sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep',
'sepa-ci', 'sepa-batch-id', 'internal_reference', 'bunq_payment_id', 'importHashV2',
'recurrence_id', 'external_id', 'original-source',
'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep',
'sepa_ci', 'sepa_batch_id', 'internal_reference', 'bunq_payment_id', 'import_hash_v2',
'recurrence_id', 'external_id', 'original_source',
];
$this->metaDateFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
@@ -72,6 +72,7 @@ class TransactionGroupTransformer extends AbstractTransformer
'id' => (int)$first['transaction_group_id'],
'created_at' => $first['created_at']->toAtomString(),
'updated_at' => $first['updated_at']->toAtomString(),
'user' => (int)$data['user_id'],
'group_title' => $data['title'],
'transactions' => $this->transformTransactions($data),
'links' => [
@@ -114,57 +115,71 @@ class TransactionGroupTransformer extends AbstractTransformer
$metaDateData = $this->groupRepos->getMetaDateFields((int)$row['transaction_journal_id'], $this->metaDateFields);
$result[] = [
'transaction_journal_id' => $row['transaction_journal_id'],
'description' => $row['description'],
'date' => $row['date']->toAtomString(),
'type' => $type,
'reconciled' => $row['reconciled'],
'source_id' => $row['source_account_id'],
'source_name' => $row['source_account_name'],
'source_iban' => $row['source_account_iban'],
'source_type' => $row['source_account_type'],
'destination_id' => $row['destination_account_id'],
'destination_name' => $row['destination_account_name'],
'destination_iban' => $row['destination_account_iban'],
'destination_type' => $row['destination_account_type'],
'amount' => $amount,
'currency_id' => $row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
'foreign_amount' => $foreignAmount,
'user' => (int)$row['user_id'],
'transaction_journal_id' => $row['transaction_journal_id'],
'type' => strtolower($type),
'date' => $row['date']->toAtomString(),
'currency_id' => $row['currency_id'],
'currency_code' => $row['currency_code'],
'currency_symbol' => $row['currency_symbol'],
'currency_decimal_places' => $row['currency_decimal_places'],
'foreign_currency_id' => $row['foreign_currency_id'],
'foreign_currency_code' => $row['foreign_currency_code'],
'foreign_currency_symbol' => $row['foreign_currency_symbol'],
'foreign_currency_decimal_places' => $row['foreign_currency_decimal_places'],
'bill_id' => $row['bill_id'],
'bill_name' => $row['bill_name'],
'category_id' => $row['category_id'],
'category_name' => $row['category_name'],
'budget_id' => $row['budget_id'],
'budget_name' => $row['budget_name'],
'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']),
'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']),
'sepa_cc' => $metaFieldData['sepa-cc'],
'sepa_ct_op' => $metaFieldData['sepa-ct-op'],
'sepa_ct_id' => $metaFieldData['sepa-ct-id'],
'sepa_db' => $metaFieldData['sepa-ddb'],
'sepa_country' => $metaFieldData['sepa-country'],
'sepa_ep' => $metaFieldData['sepa-ep'],
'sepa_ci' => $metaFieldData['sepa-ci'],
'sepa_batch_id' => $metaFieldData['sepa-batch-id'],
'internal_reference' => $metaFieldData['internal_reference'],
'bunq_payment_id' => $metaFieldData['bunq_payment_id'],
'importHashV2' => $metaFieldData['importHashV2'],
'recurrence_id' => $metaFieldData['recurrence_id'],
'external_id' => $metaFieldData['external_id'],
'original_source' => $metaFieldData['original-source'],
'interest_date' => $metaDateData['interest_date'] ? $metaDateData['interest_date']->toAtomString() : null,
'book_date' => $metaDateData['book_date'] ? $metaDateData['book_date']->toAtomString() : null,
'process_date' => $metaDateData['process_date'] ? $metaDateData['process_date']->toAtomString() : null,
'due_date' => $metaDateData['due_date'] ? $metaDateData['due_date']->toAtomString() : null,
'payment_date' => $metaDateData['payment_date'] ? $metaDateData['payment_date']->toAtomString() : null,
'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null,
'amount' => $amount,
'foreign_amount' => $foreignAmount,
'description' => $row['description'],
'source_id' => $row['source_account_id'],
'source_name' => $row['source_account_name'],
'source_iban' => $row['source_account_iban'],
'source_type' => $row['source_account_type'],
'destination_id' => $row['destination_account_id'],
'destination_name' => $row['destination_account_name'],
'destination_iban' => $row['destination_account_iban'],
'destination_type' => $row['destination_account_type'],
'budget_id' => $row['budget_id'],
'budget_name' => $row['budget_name'],
'category_id' => $row['category_id'],
'category_name' => $row['category_name'],
'bill_id' => $row['bill_id'],
'bill_name' => $row['bill_name'],
'reconciled' => $row['reconciled'],
'notes' => $this->groupRepos->getNoteText((int)$row['transaction_journal_id']),
'tags' => $this->groupRepos->getTags((int)$row['transaction_journal_id']),
'internal_reference' => $metaFieldData['internal_reference'],
'external_id' => $metaFieldData['external_id'],
'original_source' => $metaFieldData['original_source'],
'recurrence_id' => $metaFieldData['recurrence_id'],
'bunq_payment_id' => $metaFieldData['bunq_payment_id'],
'import_hash_v2' => $metaFieldData['import_hash_v2'],
'sepa_cc' => $metaFieldData['sepa_cc'],
'sepa_ct_op' => $metaFieldData['sepa_ct_op'],
'sepa_ct_id' => $metaFieldData['sepa_ct_id'],
'sepa_db' => $metaFieldData['sepa_ddb'],
'sepa_country' => $metaFieldData['sepa_country'],
'sepa_ep' => $metaFieldData['sepa_ep'],
'sepa_ci' => $metaFieldData['sepa_ci'],
'sepa_batch_id' => $metaFieldData['sepa_batch_id'],
'interest_date' => $metaDateData['interest_date'] ? $metaDateData['interest_date']->toAtomString() : null,
'book_date' => $metaDateData['book_date'] ? $metaDateData['book_date']->toAtomString() : null,
'process_date' => $metaDateData['process_date'] ? $metaDateData['process_date']->toAtomString() : null,
'due_date' => $metaDateData['due_date'] ? $metaDateData['due_date']->toAtomString() : null,
'payment_date' => $metaDateData['payment_date'] ? $metaDateData['payment_date']->toAtomString() : null,
'invoice_date' => $metaDateData['invoice_date'] ? $metaDateData['invoice_date']->toAtomString() : null,
];
}

View File

@@ -100,14 +100,14 @@ class TransactionTransformer extends AbstractTransformer
'budget_id' => $budget['budget_id'],
'budget_name' => $budget['budget_name'],
'notes' => $notes,
'sepa_cc' => $this->repository->getMetaField($journal, 'sepa-cc'),
'sepa_ct_op' => $this->repository->getMetaField($journal, 'sepa-ct-op'),
'sepa_ct_id' => $this->repository->getMetaField($journal, 'sepa-ct-ud'),
'sepa_db' => $this->repository->getMetaField($journal, 'sepa-db'),
'sepa_country' => $this->repository->getMetaField($journal, 'sepa-country'),
'sepa_ep' => $this->repository->getMetaField($journal, 'sepa-ep'),
'sepa_ci' => $this->repository->getMetaField($journal, 'sepa-ci'),
'sepa_batch_id' => $this->repository->getMetaField($journal, 'sepa-batch-id'),
'sepa_cc' => $this->repository->getMetaField($journal, 'sepa_cc'),
'sepa_ct_op' => $this->repository->getMetaField($journal, 'sepa_ct_op'),
'sepa_ct_id' => $this->repository->getMetaField($journal, 'sepa_ct_ud'),
'sepa_db' => $this->repository->getMetaField($journal, 'sepa_db'),
'sepa_country' => $this->repository->getMetaField($journal, 'sepa_country'),
'sepa_ep' => $this->repository->getMetaField($journal, 'sepa_ep'),
'sepa_ci' => $this->repository->getMetaField($journal, 'sepa_ci'),
'sepa_batch_id' => $this->repository->getMetaField($journal, 'sepa_batch_id'),
'interest_date' => $this->repository->getMetaDateString($journal, 'interest_date'),
'book_date' => $this->repository->getMetaDateString($journal, 'book_date'),
'process_date' => $this->repository->getMetaDateString($journal, 'process_date'),
@@ -116,7 +116,7 @@ class TransactionTransformer extends AbstractTransformer
'invoice_date' => $this->repository->getMetaDateString($journal, 'invoice_date'),
'internal_reference' => $this->repository->getMetaField($journal, 'internal_reference'),
'bunq_payment_id' => $this->repository->getMetaField($journal, 'bunq_payment_id'),
'importHashV2' => $this->repository->getMetaField($journal, 'importHashV2'),
'import_hash_v2' => $this->repository->getMetaField($journal, 'import_hash_v2'),
'recurrence_id' => (int)$this->repository->getMetaField($journal, 'recurrence_id'),
'external_id' => $this->repository->getMetaField($journal, 'external_id'),
'original_source' => $this->repository->getMetaField($journal, 'original-source'),

View File

@@ -0,0 +1,415 @@
<?php
/**
* AccountValidator.php
* Copyright (c) 2019 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Validation;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use Log;
/**
* Class AccountValidator
*/
class AccountValidator
{
/** @var bool */
public $createMode;
/** @var string */
public $destError;
/** @var Account */
public $destination;
/** @var Account */
public $source;
/** @var string */
public $sourceError;
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var array */
private $combinations;
/** @var string */
private $transactionType;
/**
* AccountValidator constructor.
*/
public function __construct()
{
$this->createMode = false;
$this->destError = 'No error yet.';
$this->sourceError = 'No error yet.';
$this->combinations = config('firefly.source_dests');
/** @var AccountRepositoryInterface accountRepository */
$this->accountRepository = app(AccountRepositoryInterface::class);
}
/**
* @param string $transactionType
*/
public function setTransactionType(string $transactionType): void
{
$this->transactionType = ucfirst($transactionType);
}
/**
* @param int|null $destinationId
* @param $destinationName
*
* @return bool
*/
public function validateDestination(?int $destinationId, $destinationName): bool
{
Log::debug(sprintf('Now in AccountValidator::validateDestination(%d, "%s")', $destinationId, $destinationName));
if (null === $this->source) {
Log::error('Source is NULL');
$this->destError = 'No source account validation has taken place yet. Please do this first or overrule the object.';
return false;
}
switch ($this->transactionType) {
default:
$this->destError = sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType);
Log::error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType));
$result = false;
break;
case TransactionType::WITHDRAWAL:
$result = $this->validateWithdrawalDestination($destinationId, $destinationName);
break;
case TransactionType::DEPOSIT:
$result = $this->validateDepositDestination($destinationId, $destinationName);
break;
case TransactionType::TRANSFER:
$result = $this->validateTransferDestination($destinationId, $destinationName);
break;
//case TransactionType::OPENING_BALANCE:
//case TransactionType::RECONCILIATION:
// die(sprintf('Cannot handle type "%s"', $this->transactionType));
}
return $result;
}
/**
* @param int|null $accountId
* @param string|null $accountName
*
* @return bool
*/
public function validateSource(?int $accountId, ?string $accountName): bool
{
switch ($this->transactionType) {
default:
$result = false;
$this->sourceError = sprintf('Cannot handle type "%s"', $this->transactionType);
Log::error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will always return false.', $this->transactionType));
break;
case TransactionType::WITHDRAWAL:
$result = $this->validateWithdrawalSource($accountId, $accountName);
break;
case TransactionType::DEPOSIT:
$result = $this->validateDepositSource($accountId, $accountName);
break;
case TransactionType::TRANSFER:
$result = $this->validateTransferSource($accountId, $accountName);
break;
//case TransactionType::OPENING_BALANCE:
//case TransactionType::RECONCILIATION:
// die(sprintf('Cannot handle type "%s"', $this->transactionType));
}
return $result;
}
/**
* @param string $accountType
*
* @return bool
*/
private function canCreateType(string $accountType): bool
{
$result = false;
switch ($accountType) {
default:
Log::error(sprintf('AccountValidator::validateSource cannot handle "%s".', $this->transactionType));
break;
case AccountType::ASSET:
case AccountType::LOAN:
case AccountType::MORTGAGE:
case AccountType::DEBT:
$result = false;
break;
case AccountType::EXPENSE:
case AccountType::REVENUE:
$result = true;
break;
}
return $result;
}
/**
* @param array $accountTypes
*
* @return bool
*/
private function canCreateTypes(array $accountTypes): bool
{
/** @var string $accountType */
foreach ($accountTypes as $accountType) {
if ($this->canCreateType($accountType)) {
return true;
}
}
return false;
}
/**
* @param array $validTypes
* @param int|null $accountId
* @param string|null $accountName
*
* @return Account|null
*/
private function findExistingAccount(array $validTypes, int $accountId, string $accountName): ?Account
{
$result = null;
// find by ID
if ($accountId > 0) {
$first = $this->accountRepository->findNull($accountId);
if ((null !== $first) && in_array($first->accountType->type, $validTypes, true)) {
$result = $first;
}
}
// find by name:
if (null === $result && '' !== $accountName) {
$second = $this->accountRepository->findByName($accountName, $validTypes);
if (null !== $second) {
$result = $second;
}
}
return $result;
}
/**
* @param int|null $accountId
* @param $accountName
*
* @return bool
*/
private function validateDepositDestination(?int $accountId, $accountName): bool
{
$result = null;
Log::debug(sprintf('Now in validateDepositDestination(%d, "%s")', $accountId, $accountName));
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? [];
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return false,
// because the destination of a deposit can't be created.
$this->destError = (string)trans('validation.deposit_dest_need_data');
Log::error('Both values are NULL, cant create deposit destination.');
$result = false;
}
// if the account can be created anyway we don't need to search.
if (null === $result && true === $this->canCreateTypes($validTypes)) {
Log::debug('Can create some of these types, so return true.');
$this->createDestinationAccount($accountName);
$result = true;
}
if (null === $result) {
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName);
if (null === $search) {
$this->destError = (string)trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]);
$result = false;
}
if (null !== $search) {
$this->destination = $search;
$result = true;
}
}
$result = $result ?? false;
return $result;
}
/**
* @param int|null $accountId
* @param string|null $accountName
*
* @return bool
*/
private function validateDepositSource(?int $accountId, ?string $accountName): bool
{
$result = null;
// source can be any of the following types.
$validTypes = array_keys($this->combinations[$this->transactionType]);
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL return false,
// because the source of a deposit can't be created.
// (this never happens).
$this->sourceError = (string)trans('validation.deposit_source_need_data');
$result = false;
}
// if the account can be created anyway we don't need to search.
if (null === $result && true === $this->canCreateTypes($validTypes)) {
// set the source to be a (dummy) revenue account.
$result = true;
}
$result = $result ?? false;
// don't expect to end up here:
return $result;
}
/**
* @param int|null $accountId
* @param $accountName
*
* @return bool
*/
private function validateTransferDestination(?int $accountId, $accountName): bool
{
Log::debug(sprintf('Now in validateTransferDestination(%d, "%s")', $accountId, $accountName));
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? [];
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return false,
// because the destination of a transfer can't be created.
$this->destError = (string)trans('validation.transfer_dest_need_data');
Log::error('Both values are NULL, cant create transfer destination.');
return false;
}
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName);
if (null === $search) {
$this->destError = (string)trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]);
return false;
}
$this->destination = $search;
return true;
}
/**
* @param int|null $accountId
* @param string|null $accountName
*
* @return bool
*/
private function validateTransferSource(?int $accountId, ?string $accountName): bool
{
// source can be any of the following types.
$validTypes = array_keys($this->combinations[$this->transactionType]);
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return false,
// because the source of a withdrawal can't be created.
$this->sourceError = (string)trans('validation.transfer_source_need_data');
return false;
}
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName);
if (null === $search) {
$this->sourceError = (string)trans('validation.transfer_source_bad_data', ['id' => $accountId, 'name' => $accountName]);
return false;
}
$this->source = $search;
return true;
}
/**
* @param int|null $accountId
* @param string|null $accountName
*
* @return bool
*/
private function validateWithdrawalDestination(?int $accountId, ?string $accountName): bool
{
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? [];
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL return false,
// because the destination of a withdrawal can never be created automatically.
$this->destError = (string)trans('validation.withdrawal_dest_need_data');
return false;
}
// if the account can be created anyway don't need to search.
if (true === $this->canCreateTypes($validTypes)) {
return true;
}
// don't expect to end up here:
return false;
}
/**
* @param int|null $accountId
* @param string|null $accountName
*
* @return bool
*/
private function validateWithdrawalSource(?int $accountId, ?string $accountName): bool
{
// source can be any of the following types.
$validTypes = array_keys($this->combinations[$this->transactionType]);
if (null === $accountId && null === $accountName && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return false,
// because the source of a withdrawal can't be created.
$this->sourceError = (string)trans('validation.withdrawal_source_need_data');
return false;
}
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName);
if (null === $search) {
$this->sourceError = (string)trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]);
return false;
}
$this->source = $search;
return true;
}
}

View File

@@ -23,82 +23,59 @@ declare(strict_types=1);
namespace FireflyIII\Validation;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\User;
use Illuminate\Validation\Validator;
use Log;
/**
* Trait TransactionValidation
*/
trait TransactionValidation
{
/**
* Validates the given account information. Switches on given transaction type.
*
* @param Validator $validator
*
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function validateAccountInformation(Validator $validator): void
{
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$idField = 'description';
$transactionType = $data['type'] ?? 'invalid';
// get transaction type:
if (!isset($data['type'])) {
// the journal may exist in the request:
/** @var Transaction $transaction */
$transaction = $this->route()->parameter('transaction');
if (null !== $transaction) {
$transactionType = strtolower($transaction->transactionJournal->transactionType->type);
}
}
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
/** @var AccountValidator $accountValidator */
$accountValidator = app(AccountValidator::class);
foreach ($transactions as $index => $transaction) {
$sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : null;
$sourceName = $transaction['source_name'] ?? null;
$destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null;
$destinationName = $transaction['destination_name'] ?? null;
$sourceAccount = null;
$destinationAccount = null;
switch ($transactionType) {
case 'withdrawal':
$idField = 'transactions.' . $index . '.source_id';
$nameField = 'transactions.' . $index . '.source_name';
$sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField);
$idField = 'transactions.' . $index . '.destination_id';
$destinationAccount = $this->opposingAccountExists($validator, AccountType::EXPENSE, $destinationId, $destinationName, $idField);
break;
case 'deposit':
$idField = 'transactions.' . $index . '.source_id';
$sourceAccount = $this->opposingAccountExists($validator, AccountType::REVENUE, $sourceId, $sourceName, $idField);
$transactionType = $transaction['type'] ?? 'invalid';
$accountValidator->setTransactionType($transactionType);
$idField = 'transactions.' . $index . '.destination_id';
$nameField = 'transactions.' . $index . '.destination_name';
$destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField);
break;
case 'transfer':
$idField = 'transactions.' . $index . '.source_id';
$nameField = 'transactions.' . $index . '.source_name';
$sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField);
// validate source account.
$sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : null;
$sourceName = $transaction['source_name'] ?? null;
$validSource = $accountValidator->validateSource($sourceId, $sourceName);
$idField = 'transactions.' . $index . '.destination_id';
$nameField = 'transactions.' . $index . '.destination_name';
$destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField);
break;
default:
$validator->errors()->add($idField, (string)trans('validation.invalid_account_info'));
return;
// do something with result:
if (false === $validSource) {
$validator->errors()->add(sprintf('transactions.%d.source_id', $index), $accountValidator->sourceError);
$validator->errors()->add(sprintf('transactions.%d.source_name', $index), $accountValidator->sourceError);
return;
}
// add some errors in case of same account submitted:
if (null !== $sourceAccount && null !== $destinationAccount && $sourceAccount->id === $destinationAccount->id) {
$validator->errors()->add($idField, (string)trans('validation.source_equals_destination'));
// validate destination account
$destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null;
$destinationName = $transaction['destination_name'] ?? null;
$validDestination = $accountValidator->validateDestination($destinationId, $destinationName);
// do something with result:
if (false === $validDestination) {
$validator->errors()->add(sprintf('transactions.%d.destination_id', $index), $accountValidator->destError);
$validator->errors()->add(sprintf('transactions.%d.destination_name', $index), $accountValidator->destError);
return;
}
}
}
@@ -110,18 +87,17 @@ trait TransactionValidation
*/
public function validateDescriptions(Validator $validator): void
{
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$journalDescription = (string)($data['description'] ?? null);
$validDescriptions = 0;
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$validDescriptions = 0;
foreach ($transactions as $index => $transaction) {
if ('' !== (string)($transaction['description'] ?? null)) {
$validDescriptions++;
}
}
// no valid descriptions and empty journal description? error.
if (0 === $validDescriptions && '' === $journalDescription) {
// no valid descriptions?
if (0 === $validDescriptions) {
$validator->errors()->add('description', (string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.description')]));
}
}
@@ -149,21 +125,15 @@ trait TransactionValidation
}
/**
* Adds an error to the validator when any transaction descriptions are equal to the journal description.
*
* @param Validator $validator
*/
public function validateJournalDescription(Validator $validator): void
public function validateGroupDescription(Validator $validator): void
{
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$journalDescription = (string)($data['description'] ?? null);
foreach ($transactions as $index => $transaction) {
$description = (string)($transaction['description'] ?? null);
// description cannot be equal to journal description.
if ($description === $journalDescription) {
$validator->errors()->add('transactions.' . $index . '.description', (string)trans('validation.equal_description'));
}
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$groupTitle = $data['group_title'] ?? '';
if ('' === $groupTitle && \count($transactions) > 1) {
$validator->errors()->add('group_title', (string)trans('validation.group_title_mandatory'));
}
}
@@ -239,126 +209,129 @@ trait TransactionValidation
}
/**
* Adds an error to the validator when the user submits a split transaction (more than 1 transactions)
* but does not give them a description.
* All types of splits must be equal.
*
* @param Validator $validator
*/
public function validateSplitDescriptions(Validator $validator): void
public function validateTransactionTypes(Validator $validator): void
{
$data = $validator->getData();
$transactions = $data['transactions'] ?? [];
$types = [];
foreach ($transactions as $index => $transaction) {
$description = (string)($transaction['description'] ?? null);
// filled description is mandatory for split transactions.
if ('' === $description && \count($transactions) > 1) {
$validator->errors()->add(
'transactions.' . $index . '.description',
(string)trans('validation.filled', ['attribute' => (string)trans('validation.attributes.transaction_description')])
);
}
$types[] = $transaction['type'] ?? 'invalid';
}
$unique = array_unique($types);
if (count($unique) > 1) {
$validator->errors()->add('transactions.0.type', (string)trans('validation.transaction_types_equal'));
return;
}
$first = $unique[0] ?? 'invalid';
if ('invalid' === $first) {
$validator->errors()->add('transactions.0.type', (string)trans('validation.invalid_transaction_type'));
}
}
/**
* Throws an error when this asset account is invalid.
*
* @noinspection MoreThanThreeArgumentsInspection
*
* @param Validator $validator
* @param int|null $accountId
* @param null|string $accountName
* @param string $idField
* @param string $nameField
*
* @return null|Account
*/
protected function assetAccountExists(Validator $validator, ?int $accountId, ?string $accountName, string $idField, string $nameField): ?Account
{
/** @var User $admin */
$admin = auth()->user();
$accountId = (int)$accountId;
$accountName = (string)$accountName;
// both empty? hard exit.
if ($accountId < 1 && '' === $accountName) {
$validator->errors()->add($idField, (string)trans('validation.filled', ['attribute' => $idField]));
return null;
}
// ID belongs to user and is asset account:
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($admin);
$set = $repository->getAccountsById([$accountId]);
Log::debug(sprintf('Count of accounts found by ID %d is: %d', $accountId, $set->count()));
if (1 === $set->count()) {
/** @var Account $first */
$first = $set->first();
if ($first->accountType->type !== AccountType::ASSET) {
$validator->errors()->add($idField, (string)trans('validation.belongs_user'));
return null;
}
// we ignore the account name at this point.
return $first;
}
$account = $repository->findByName($accountName, [AccountType::ASSET]);
if (null === $account) {
$validator->errors()->add($nameField, (string)trans('validation.belongs_user'));
return null;
}
return $account;
}
/**
* Throws an error when the given opposing account (of type $type) is invalid.
* Empty data is allowed, system will default to cash.
*
* @noinspection MoreThanThreeArgumentsInspection
*
* @param Validator $validator
* @param string $type
* @param int|null $accountId
* @param null|string $accountName
* @param string $idField
*
* @return null|Account
*/
protected function opposingAccountExists(Validator $validator, string $type, ?int $accountId, ?string $accountName, string $idField): ?Account
{
/** @var User $admin */
$admin = auth()->user();
$accountId = (int)$accountId;
$accountName = (string)$accountName;
// both empty? done!
if ($accountId < 1 && '' === $accountName) {
return null;
}
if (0 !== $accountId) {
// ID belongs to user and is $type account:
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($admin);
$set = $repository->getAccountsById([$accountId]);
if (1 === $set->count()) {
/** @var Account $first */
$first = $set->first();
if ($first->accountType->type !== $type) {
$validator->errors()->add($idField, (string)trans('validation.belongs_user'));
return null;
}
// we ignore the account name at this point.
return $first;
}
}
// not having an opposing account by this name is NOT a problem.
return null;
}
// /**
// * Throws an error when this asset account is invalid.
// *
// * @noinspection MoreThanThreeArgumentsInspection
// *
// * @param Validator $validator
// * @param int|null $accountId
// * @param null|string $accountName
// * @param string $idField
// * @param string $nameField
// *
// * @return null|Account
// */
// protected function assetAccountExists(Validator $validator, ?int $accountId, ?string $accountName, string $idField, string $nameField): ?Account
// {
// /** @var User $admin */
// $admin = auth()->user();
// $accountId = (int)$accountId;
// $accountName = (string)$accountName;
// // both empty? hard exit.
// if ($accountId < 1 && '' === $accountName) {
// $validator->errors()->add($idField, (string)trans('validation.filled', ['attribute' => $idField]));
//
// return null;
// }
// // ID belongs to user and is asset account:
// /** @var AccountRepositoryInterface $repository */
// $repository = app(AccountRepositoryInterface::class);
// $repository->setUser($admin);
// $set = $repository->getAccountsById([$accountId]);
// Log::debug(sprintf('Count of accounts found by ID %d is: %d', $accountId, $set->count()));
// if (1 === $set->count()) {
// /** @var Account $first */
// $first = $set->first();
// if ($first->accountType->type !== AccountType::ASSET) {
// $validator->errors()->add($idField, (string)trans('validation.belongs_user'));
//
// return null;
// }
//
// // we ignore the account name at this point.
// return $first;
// }
//
// $account = $repository->findByName($accountName, [AccountType::ASSET]);
// if (null === $account) {
// $validator->errors()->add($nameField, (string)trans('validation.belongs_user'));
//
// return null;
// }
//
// return $account;
// }
//
// /**
// * Throws an error when the given opposing account (of type $type) is invalid.
// * Empty data is allowed, system will default to cash.
// *
// * @noinspection MoreThanThreeArgumentsInspection
// *
// * @param Validator $validator
// * @param string $type
// * @param int|null $accountId
// * @param null|string $accountName
// * @param string $idField
// *
// * @return null|Account
// */
// protected function opposingAccountExists(Validator $validator, string $type, ?int $accountId, ?string $accountName, string $idField): ?Account
// {
// /** @var User $admin */
// $admin = auth()->user();
// $accountId = (int)$accountId;
// $accountName = (string)$accountName;
// // both empty? done!
// if ($accountId < 1 && '' === $accountName) {
// return null;
// }
// if (0 !== $accountId) {
// // ID belongs to user and is $type account:
// /** @var AccountRepositoryInterface $repository */
// $repository = app(AccountRepositoryInterface::class);
// $repository->setUser($admin);
// $set = $repository->getAccountsById([$accountId]);
// if (1 === $set->count()) {
// /** @var Account $first */
// $first = $set->first();
// if ($first->accountType->type !== $type) {
// $validator->errors()->add($idField, (string)trans('validation.belongs_user'));
//
// return null;
// }
//
// // we ignore the account name at this point.
// return $first;
// }
// }
//
// // not having an opposing account by this name is NOT a problem.
// return null;
// }
}

View File

@@ -359,56 +359,56 @@ return [
],
// SEPA end to end ID
'sepa-ct-id' => [
'sepa_ct_id' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_ct_id',
],
// SEPA opposing account identifier
'sepa-ct-op' => [
'sepa_ct_op' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_ct_op',
],
// SEPA Direct Debit Mandate Identifier
'sepa-db' => [
'sepa_db' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_db',
],
// SEPA clearing code
'sepa-cc' => [
'sepa_cc' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_cc',
],
// SEPA country
'sepa-country' => [
'sepa_country' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_country',
],
// SEPA external purpose
'sepa-ep' => [
'sepa_ep' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_ep',
],
// SEPA creditor identifier
'sepa-ci' => [
'sepa_ci' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',
'field' => 'sepa_ci',
],
// SEPA Batch ID
'sepa-batch-id' => [
'sepa_batch_id' => [
'mappable' => false,
'pre-process-map' => false,
'converter' => 'Description',

View File

@@ -291,14 +291,14 @@ return [
'column_rabo-debit-credit' => 'Rabobank specific debit/credit indicator',
'column_ing-debit-credit' => 'ING specific debit/credit indicator',
'column_generic-debit-credit' => 'Generic bank debit/credit indicator',
'column_sepa-ct-id' => 'SEPA end-to-end Identifier',
'column_sepa-ct-op' => 'SEPA Opposing Account Identifier',
'column_sepa-db' => 'SEPA Mandate Identifier',
'column_sepa-cc' => 'SEPA Clearing Code',
'column_sepa-ci' => 'SEPA Creditor Identifier',
'column_sepa-ep' => 'SEPA External Purpose',
'column_sepa-country' => 'SEPA Country Code',
'column_sepa-batch-id' => 'SEPA Batch ID',
'column_sepa_ct_id' => 'SEPA end-to-end Identifier',
'column_sepa_ct_op' => 'SEPA Opposing Account Identifier',
'column_sepa_db' => 'SEPA Mandate Identifier',
'column_sepa_cc' => 'SEPA Clearing Code',
'column_sepa_ci' => 'SEPA Creditor Identifier',
'column_sepa_ep' => 'SEPA External Purpose',
'column_sepa_country' => 'SEPA Country Code',
'column_sepa_batch_id' => 'SEPA Batch ID',
'column_tags-comma' => 'Tags (comma separated)',
'column_tags-space' => 'Tags (space separated)',
'column_account-number' => 'Asset account (account number)',

View File

@@ -106,14 +106,14 @@ return [
'account_on_spectre' => 'Account (Spectre)',
'account_on_ynab' => 'Account (YNAB)',
'do_import' => 'Import from this account',
'sepa-ct-id' => 'SEPA End to End Identifier',
'sepa-ct-op' => 'SEPA Opposing Account Identifier',
'sepa-db' => 'SEPA Mandate Identifier',
'sepa-country' => 'SEPA Country',
'sepa-cc' => 'SEPA Clearing Code',
'sepa-ep' => 'SEPA External Purpose',
'sepa-ci' => 'SEPA Creditor Identifier',
'sepa-batch-id' => 'SEPA Batch ID',
'sepa_ct_id' => 'SEPA End to End Identifier',
'sepa_ct_op' => 'SEPA Opposing Account Identifier',
'sepa_db' => 'SEPA Mandate Identifier',
'sepa_country' => 'SEPA Country',
'sepa_cc' => 'SEPA Clearing Code',
'sepa_ep' => 'SEPA External Purpose',
'sepa_ci' => 'SEPA Creditor Identifier',
'sepa_batch_id' => 'SEPA Batch ID',
'external_id' => 'External ID',
'account_at_bunq' => 'Account with bunq',
'file_name' => 'File name',

View File

@@ -33,9 +33,12 @@ return [
'rule_trigger_value' => 'This value is invalid for the selected trigger.',
'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Succesfully uploaded file ":name".',
'file_attached' => 'Successfully uploaded file ":name".',
'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.',
'group_title_mandatory' => 'A group title is mandatory when there is more than one transaction.',
'transaction_types_equal' => 'All splits must be of the same type.',
'invalid_transaction_type' => 'Invalid transaction type.',
'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is invalid for this field.',
'at_least_one_transaction' => 'Need at least one transaction.',
@@ -164,4 +167,19 @@ return [
'rule-trigger.4' => 'rule trigger #4',
'rule-trigger.5' => 'rule trigger #5',
],
// validation of accounts:
'withdrawal_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'withdrawal_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".',
'withdrawal_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.',
'deposit_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'deposit_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".',
'deposit_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.',
'deposit_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
'transfer_source_need_data' => 'Need to get a valid source account ID and/or valid source account name to continue.',
'transfer_source_bad_data' => 'Could not find a valid source account when searching for ID ":id" or name ":name".',
'transfer_dest_need_data' => 'Need to get a valid destination account ID and/or valid destination account name to continue.',
'transfer_dest_bad_data' => 'Could not find a valid destination account when searching for ID ":id" or name ":name".',
];

View File

@@ -1,6 +1,6 @@
<div class="list-group">
{% for group in groups %}
<a class="list-group-item">
<a class="list-group-item" href="{{ route('transactions.show', [group.id]) }}">
{% for transaction in group.transactions %}
{{ transaction.description }}
<span class="pull-right small">

View File

@@ -1,19 +1 @@
<h1 style="color:red;">DO NOT USE ME</h1>
{#<div class="list-group">
{% for transaction in transactions %}
<a class="list-group-item" title="{{ transaction.date.formatLocalized(trans('config.month_and_day')) }}"
{% if transaction.transaction_type_type == 'Opening balance' %}
href="#"
{% else %}
href="{{ route('transactions.show',transaction.journal_id) }}"
{% endif %}
>
{{ transaction|transactionIcon }}
{{ transaction|transactionDescription }}
<span class="pull-right small">
{{ transaction|transactionAmount }}
</span>
</a>
{% endfor %}
</div>
#}
<h1 style="color:red;">DO NOT USE ME</h1>

View File

@@ -1,3 +1,7 @@
<h1 style="color:red;">DO NOT USE</h1>
{#
{{ transactions.render|raw }}
<table class="table table-hover table-condensed {% if sorting %}sortable-table{% endif %}">
@@ -10,17 +14,14 @@
<th class="hidden-xs hidden-sm">{{ trans('list.date') }}</th>
<th class="hidden-xs hidden-sm hidden-md">{{ trans('list.from') }}</th>
<th class="hidden-xs hidden-sm hidden-md">{{ trans('list.to') }}</th>
{# Hide budgets? #}
{% if not hideBudgets %}
<th class="hidden-xs"><i class="fa fa-tasks fa-fw" title="{{ trans('list.budget') }}"></i></th>
{% endif %}
{# Hide categories? #}
{% if not hideCategories %}
<th class="hidden-xs"><i class="fa fa-bar-chart fa-fw" title="{{ trans('list.category') }}"></i></th>
{% endif %}
{# Hide bills? #}
{% if not hideBills %}
<th class="hidden-xs"><i class="fa fa-fw fa-calendar-o" title="{{ trans('list.bill') }}"></i></th>
{% endif %}
@@ -75,3 +76,4 @@
var edit_bulk_selected_txt = "{{ trans('firefly.bulk_edit')|escape('js') }}";
var delete_selected_txt = "{{ trans('firefly.delete')|escape('js') }}";
</script>
#}

View File

@@ -214,7 +214,7 @@
{% endif %}
{% endfor %}
{# all other meta values #}
{% for metaField in ['external_id','bunq_payment_id','internal_reference','sepa-batch-id','sepa-ct-id','sepa-ct-op','sepa-db','sepa-country','sepa-cc','sepa-ep','sepa-ci'] %}
{% for metaField in ['external_id','bunq_payment_id','internal_reference','sepa_batch_id','sepa_ct_id','sepa_ct_op','sepa_db','sepa_country','sepa_cc','sepa_ep','sepa_ci'] %}
{% if journalHasMeta(journal, metaField) %}
<tr>
<td>{{ trans('list.'~metaField) }}</td>

View File

@@ -21,23 +21,17 @@
declare(strict_types=1);
Route::group(
['namespace' => 'FireflyIII\Http\Controllers\System',
'as' => 'installer.', 'prefix' => 'install'], function () {
Route::get('', ['uses' => 'InstallController@index', 'as' => 'index']);
Route::post('runCommand', ['uses' => 'InstallController@runCommand', 'as' => 'runCommand']);
// Route::post('migrate', ['uses' => 'InstallController@migrate', 'as' => 'migrate']);
// Route::post('keys', ['uses' => 'InstallController@keys', 'as' => 'keys']);
// Route::post('upgrade', ['uses' => 'InstallController@upgrade', 'as' => 'upgrade']);
// Route::post('verify', ['uses' => 'InstallController@verify', 'as' => 'verify']);
// Route::post('decrypt', ['uses' => 'InstallController@decrypt', 'as' => 'decrypt']);
}
);
Route::group(
['middleware' => 'binders-only','namespace' => 'FireflyIII\Http\Controllers\System', 'as' => 'cron.', 'prefix' => 'cron'], function () {
Route::get('run/{cliToken}', ['uses' => 'CronController@cron', 'as' => 'cron']);
['middleware' => 'binders-only', 'namespace' => 'FireflyIII\Http\Controllers\System', 'as' => 'cron.', 'prefix' => 'cron'], static function () {
Route::get('run/{cliToken}', ['uses' => 'CronController@cron', 'as' => 'cron']);
}
);
@@ -45,7 +39,7 @@ Route::group(
* These routes only work when the user is NOT logged in.
*/
Route::group(
['middleware' => 'user-not-logged-in', 'namespace' => 'FireflyIII\Http\Controllers'], function () {
['middleware' => 'user-not-logged-in', 'namespace' => 'FireflyIII\Http\Controllers'], static function () {
// Authentication Routes...
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
@@ -58,7 +52,7 @@ Route::group(
// Password Reset Routes...
Route::get('password/reset/{token}', ['uses' => 'Auth\ResetPasswordController@showResetForm', 'as' => 'password.reset']);
Route::post('password/email', ['uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail', 'as' => 'password.email']);
Route::post('password/reset',['uses' => 'Auth\ResetPasswordController@reset']);
Route::post('password/reset', ['uses' => 'Auth\ResetPasswordController@reset']);
Route::get('password/reset', ['uses' => 'Auth\ForgotPasswordController@showLinkRequestForm', 'as' => 'password.reset.request']);
// Change email routes:
@@ -148,9 +142,10 @@ Route::group(
);
// show reconciliation
Route::get('reconcile/show/{tj}', ['uses' => 'Account\ReconcileController@show', 'as' => 'reconcile.show']);
Route::get('reconcile/edit/{tj}', ['uses' => 'Account\ReconcileController@edit', 'as' => 'reconcile.edit']);
Route::post('reconcile/update/{tj}', ['uses' => 'Account\ReconcileController@update', 'as' => 'reconcile.update']);
// TODO improve me
//Route::get('reconcile/show/{transactionGroup}', ['uses' => 'Account\ReconcileController@show', 'as' => 'reconcile.show']);
//Route::get('reconcile/edit/{transactionGroup}', ['uses' => 'Account\ReconcileController@edit', 'as' => 'reconcile.edit']);
//Route::post('reconcile/update/{transactionGroup}', ['uses' => 'Account\ReconcileController@update', 'as' => 'reconcile.update']);
}
@@ -553,10 +548,11 @@ Route::group(
// for auto complete
Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']);
Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']);
Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']);
// Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']);
// TODO improve me.
//Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']);
//Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']);
//Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']);
// Route::get('transaction-types', ['uses' => 'Json\AutoCompleteController@transactionTypes', 'as' => 'transaction-types']);
// boxes
Route::get('box/balance', ['uses' => 'Json\BoxController@balance', 'as' => 'box.balance']);
@@ -875,15 +871,19 @@ Route::group(
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'transactions', 'as' => 'transactions.'], function () {
Route::get('{what}/all', ['uses' => 'TransactionController@indexAll', 'as' => 'index.all'])->where(['what' => 'withdrawal|deposit|transfers|transfer']);
Route::get('{what}/{start_date?}/{end_date?}', ['uses' => 'TransactionController@index', 'as' => 'index'])->where(
['what' => 'withdrawal|deposit|transfers|transfer']
);
// Route::get('{what}/all', ['uses' => 'TransactionController@indexAll', 'as' => 'index.all'])->where(['what' => 'withdrawal|deposit|transfers|transfer']);
// Route::get('{what}/{start_date?}/{end_date?}', ['uses' => 'TransactionController@index', 'as' => 'index'])->where(
// ['what' => 'withdrawal|deposit|transfers|transfer']
// );
Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']);
Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']);
Route::post('reorder', ['uses' => 'TransactionController@reorder', 'as' => 'reorder']);
Route::post('reconcile', ['uses' => 'TransactionController@reconcile', 'as' => 'reconcile']);
//Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']);
//Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']);
//Route::get('show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'show']);
//Route::get('debug/{tj}', ['uses' => 'Transaction\SingleController@debugShow', 'as' => 'debug']);
//Route::post('reorder', ['uses' => 'TransactionController@reorder', 'as' => 'reorder']);
//Route::post('reconcile', ['uses' => 'TransactionController@reconcile', 'as' => 'reconcile']);
}
);
@@ -893,13 +893,13 @@ Route::group(
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions', 'as' => 'transactions.'],
function () {
Route::get('create/{what}', ['uses' => 'SingleController@create', 'as' => 'create'])->where(['what' => 'withdrawal|deposit|transfer']);
Route::get('edit/{tj}', ['uses' => 'SingleController@edit', 'as' => 'edit']);
Route::get('delete/{tj}', ['uses' => 'SingleController@delete', 'as' => 'delete']);
Route::post('store/{what}', ['uses' => 'SingleController@store', 'as' => 'store'])->where(['what' => 'withdrawal|deposit|transfer']);
Route::post('update/{tj}', ['uses' => 'SingleController@update', 'as' => 'update']);
Route::post('destroy/{tj}', ['uses' => 'SingleController@destroy', 'as' => 'destroy']);
Route::get('clone/{tj}', ['uses' => 'SingleController@cloneTransaction', 'as' => 'clone']);
// Route::get('create/{what}', ['uses' => 'SingleController@create', 'as' => 'create'])->where(['what' => 'withdrawal|deposit|transfer']);
// Route::get('edit/{tj}', ['uses' => 'SingleController@edit', 'as' => 'edit']);
// Route::get('delete/{tj}', ['uses' => 'SingleController@delete', 'as' => 'delete']);
// Route::post('store/{what}', ['uses' => 'SingleController@store', 'as' => 'store'])->where(['what' => 'withdrawal|deposit|transfer']);
// Route::post('update/{tj}', ['uses' => 'SingleController@update', 'as' => 'update']);
// Route::post('destroy/{tj}', ['uses' => 'SingleController@destroy', 'as' => 'destroy']);
// Route::get('clone/{tj}', ['uses' => 'SingleController@cloneTransaction', 'as' => 'clone']);
}
);
@@ -933,8 +933,8 @@ Route::group(
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/split',
'as' => 'transactions.split.'], function () {
Route::get('edit/{tj}', ['uses' => 'SplitController@edit', 'as' => 'edit']);
Route::post('update/{tj}', ['uses' => 'SplitController@update', 'as' => 'update']);
// Route::get('edit/{tj}', ['uses' => 'SplitController@edit', 'as' => 'edit']);
// Route::post('update/{tj}', ['uses' => 'SplitController@update', 'as' => 'update']);
}
);
@@ -945,8 +945,8 @@ Route::group(
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/convert',
'as' => 'transactions.convert.'], function () {
Route::get('{transactionType}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']);
Route::post('{transactionType}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']);
// Route::get('{transactionType}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']);
// Route::post('{transactionType}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']);
}
);
@@ -956,8 +956,7 @@ Route::group(
Route::group(
['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers\Transaction', 'prefix' => 'transactions/link', 'as' => 'transactions.link.'],
function () {
Route::post('store/{tj}', ['uses' => 'LinkController@store', 'as' => 'store']);
//Route::post('store/{tj}', ['uses' => 'LinkController@store', 'as' => 'store']);
Route::get('delete/{journalLink}', ['uses' => 'LinkController@delete', 'as' => 'delete']);
Route::get('switch/{journalLink}', ['uses' => 'LinkController@switchLink', 'as' => 'switch']);