Merge branch 'develop' into feature/credit_calc

# Conflicts:
#	app/Factory/TransactionJournalMetaFactory.php
#	app/Repositories/Account/AccountRepository.php
#	app/Transformers/AccountTransformer.php
#	config/firefly.php
#	frontend/src/components/accounts/Create.vue
This commit is contained in:
James Cole 2021-04-27 06:54:38 +02:00
commit 65c49c7a71
No known key found for this signature in database
GPG Key ID: B5669F9493CDE38D
259 changed files with 3163 additions and 2520 deletions

View File

@ -30,4 +30,4 @@ parameters:
- ../bootstrap/app.php
# The level 8 is the highest level. original was 5
level: 2
level: 3

View File

@ -175,6 +175,10 @@ MAP_DEFAULT_ZOOM=6
# For full instructions on these settings please visit:
# https://docs.firefly-iii.org/advanced-installation/authentication
# If you use Docker or similar, you can set this variable from a file by appending it with _FILE
#
# If you enable 'ldap' AND you run Docker, the Docker image will contact packagist.org
# This is necessary to download the required packages.
#
LOGIN_PROVIDER=eloquent
# It's also possible to change the way users are authenticated. You could use Authelia for example.

View File

@ -1,24 +1,5 @@
<?php
declare(strict_types=1);
/*
* AccountController.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Controllers\Data\Bulk;

View File

@ -1,24 +1,5 @@
<?php
declare(strict_types=1);
/*
* MoveTransactionsRequest.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V1\Requests\Data\Bulk;

View File

@ -76,8 +76,8 @@ class StoreRequest extends FormRequest
'currency_code' => 'exists:transaction_currencies,code',
// auto budget info
'auto_budget_type' => 'in:reset,rollover,none',
'auto_budget_amount' => 'min:0|max:1000000000',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',
'auto_budget_amount' => 'numeric|min:0|max:1000000000|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover',
];
}

View File

@ -76,6 +76,7 @@ class CorrectDatabase extends Command
'firefly-iii:fix-recurring-transactions',
'firefly-iii:restore-oauth-keys',
'firefly-iii:fix-transaction-types',
'firefly-iii:fix-frontpage-accounts'
];
foreach ($commands as $command) {
$this->line(sprintf('Now executing %s', $command));

View File

@ -71,6 +71,7 @@ class DeleteEmptyJournals extends Command
->groupBy('transactions.transaction_journal_id')
->get([DB::raw('COUNT(transactions.transaction_journal_id) as the_count'), 'transaction_journal_id']);
$total = 0;
/** @var Transaction $row */
foreach ($set as $row) {
$count = (int)$row->the_count;
if (1 === $count % 2) {

View File

@ -0,0 +1,106 @@
<?php
/*
* FixFrontpageAccounts.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Preference;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\User;
use Illuminate\Console\Command;
/**
* Class FixFrontpageAccounts
*/
class FixFrontpageAccounts extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes a preference that may include deleted accounts or accounts of another type.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:fix-frontpage-accounts';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$start = microtime(true);
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$preference = Preferences::getForUser($user, 'frontPageAccounts', null);
if (null !== $preference) {
$this->fixPreference($preference);
}
}
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('Verifying account preferences took %s seconds', $end));
return 0;
}
/**
* @param Preference $preference
*/
private function fixPreference(Preference $preference): void
{
$fixed = [];
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
if (null === $preference->user) {
return;
}
$repository->setUser($preference->user);
$data = $preference->data;
if (is_array($data)) {
/** @var string $accountId */
foreach ($data as $accountId) {
$accountId = (int)$accountId;
$account = $repository->findNull($accountId);
if (null !== $account) {
if (
in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE], true)
&& true === $account->active
) {
$fixed[] = $account->id;
continue;
}
}
}
}
Preferences::setForUser($preference->user, 'frontPageAccounts', $fixed);
}
}

View File

@ -60,6 +60,7 @@ class FixGroupAccounts extends Command
$res = TransactionJournal
::groupBy('transaction_group_id')
->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
/** @var TransactionJournal $journal */
foreach ($res as $journal) {
if ((int)$journal->the_count > 1) {
$groups[] = (int)$journal->transaction_group_id;

View File

@ -177,7 +177,7 @@ class DecryptDatabase extends Command
/**
* Tries to decrypt data. Will only throw an exception when the MAC is invalid.
*
* @param $value
* @param mixed $value
*
* @return string
* @throws FireflyException

View File

@ -28,6 +28,7 @@ use Carbon\Carbon;
use Exception;
use FireflyIII\Console\Commands\VerifiesAccessToken;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
@ -124,11 +125,11 @@ class ExportData extends Command
$exporter->setExportBills($options['export']['bills']);
$exporter->setExportPiggies($options['export']['piggies']);
$data = $exporter->export();
if (0===count($data)) {
if (0 === count($data)) {
$this->error('You must export *something*. Use --export-transactions or another option. See docs.firefly-iii.org');
}
$returnCode = 0;
if (0!== count($data)) {
if (0 !== count($data)) {
try {
$this->exportData($options, $data);
app('telemetry')->feature('system.command.executed', $this->signature);
@ -199,24 +200,27 @@ class ExportData extends Command
$error = false;
if (null !== $this->option($field)) {
try {
$date = Carbon::createFromFormat('Y-m-d', $this->option($field));
$date = Carbon::createFromFormat('!Y-m-d', $this->option($field));
} catch (InvalidArgumentException $e) {
Log::error($e->getMessage());
$this->error(sprintf('%s date "%s" must be formatted YYYY-MM-DD. Field will be ignored.', $field, $this->option('start')));
$error = true;
}
}
if (false === $error && 'start' === $field) {
if (true === $error && 'start' === $field) {
$journal = $this->journalRepository->firstNull();
$date = null === $journal ? Carbon::now()->subYear() : $journal->date;
$date->startOfDay();
}
if (false === $error && 'end' === $field) {
if (true === $error && 'end' === $field) {
$date = today(config('app.timezone'));
$date->endOfDay();
}
if ('end' === $field) {
$date->endOfDay();
}
// fallback
return $date;
}
@ -238,7 +242,7 @@ class ExportData extends Command
$accounts = $this->accountRepository->getAccountsByType($types);
}
// filter accounts,
/** @var AccountType $account */
/** @var Account $account */
foreach ($accounts as $account) {
if (in_array($account->accountType->type, $types, true)) {
$final->push($account);

View File

@ -144,8 +144,7 @@ class BackToJournals extends Command
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')->whereIn('transactions.id', $chunk)
->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
$set = DB::table('transactions')->whereIn('transactions.id', $chunk)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
$array = array_merge($array, $set);
}

View File

@ -414,7 +414,7 @@ class MigrateToGroups extends Command
if ($total > 0) {
Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $total));
$this->line(sprintf('Going to convert %d transaction journals. Please hold..', $total));
/** @var array $journal */
/** @var array $array */
foreach ($orphanedJournals as $array) {
$this->giveGroup($array);
}

View File

@ -95,6 +95,7 @@ class UpgradeDatabase extends Command
'firefly-iii:fix-recurring-transactions',
'firefly-iii:unify-group-accounts',
'firefly-iii:fix-transaction-types',
'firefly-iii:fix-frontpage-accounts',
// two report commands
'firefly-iii:report-empty-objects',

View File

@ -48,38 +48,38 @@ class GracefulNotFoundHandler extends ExceptionHandler
* Render an exception into an HTTP response.
*
* @param Request $request
* @param Exception $exception
* @param Throwable $e
*
* @return mixed
* @throws Exception
* @throws Throwable
*/
public function render($request, Throwable $exception)
public function render($request, Throwable $e)
{
$route = $request->route();
if (null === $route) {
return parent::render($request, $exception);
return parent::render($request, $e);
}
$name = $route->getName();
if (!auth()->check()) {
return parent::render($request, $exception);
return parent::render($request, $e);
}
switch ($name) {
default:
Log::warning(sprintf('GracefulNotFoundHandler cannot handle route with name "%s"', $name));
return parent::render($request, $exception);
return parent::render($request, $e);
case 'accounts.show':
case 'accounts.show.all':
return $this->handleAccount($request, $exception);
return $this->handleAccount($request, $e);
case 'transactions.show':
return $this->handleGroup($request, $exception);
return $this->handleGroup($request, $e);
case 'attachments.show':
case 'attachments.edit':
case 'attachments.download':
case 'attachments.view':
// redirect to original attachment holder.
return $this->handleAttachment($request, $exception);
return $this->handleAttachment($request, $e);
break;
case 'bills.show':
$request->session()->reflash();
@ -131,7 +131,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('index'));
}
return parent::render($request, $exception);
return parent::render($request, $e);
}
}
@ -141,7 +141,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
* @param Throwable $exception
*
* @return Redirector|Response
* @throws Exception
* @throws Throwable
*/
private function handleAccount(Request $request, Throwable $exception)
{
@ -165,11 +165,11 @@ class GracefulNotFoundHandler extends ExceptionHandler
}
/**
* @param Throwable $request
* @param Exception $exception
* @param Request $request
* @param Throwable $exception
*
* @return RedirectResponse|\Illuminate\Http\Response|Redirector|Response
* @throws Exception
* @throws Throwable
*/
private function handleGroup(Request $request, Throwable $exception)
{
@ -209,7 +209,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
* @param Throwable $exception
*
* @return RedirectResponse|Redirector|Response
* @throws Exception
* @throws Throwable
*/
private function handleAttachment(Request $request, Throwable $exception)
{

View File

@ -46,7 +46,7 @@ class TransactionCurrencyFactory
public function create(array $data): TransactionCurrency
{
try {
/** @var TransactionCurrency $currency */
/** @var TransactionCurrency $result */
$result = TransactionCurrency::create(
[
'name' => $data['name'],

View File

@ -543,7 +543,7 @@ class TransactionJournalFactory
'data' => (string)($data[$field] ?? ''),
];
Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
//Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);

View File

@ -61,7 +61,7 @@ class TransactionJournalMetaFactory
$value = $data['data']->toW3cString();
}
if ('' === (string)$value) {
//Log::debug('Is an empty string.');
// Log::debug('Is an empty string.');
// don't store blank strings.
if (null !== $entry) {
Log::debug('Will not store empty strings, delete meta value');

View File

@ -207,7 +207,7 @@ trait MetaCollection
$this->query->leftJoin('journal_meta', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id');
}
$this->query->where('journal_meta.name', '=', 'external_id');
$this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $externalId));
$this->query->where('journal_meta.data', '=', sprintf('%s', $externalId));
return $this;
}
@ -269,6 +269,19 @@ trait MetaCollection
return $this;
}
/**
* Limit results to transactions without a bill..
*
* @return GroupCollectorInterface
*/
public function withBill(): GroupCollectorInterface
{
$this->withBillInformation();
$this->query->whereNotNull('transaction_journals.bill_id');
return $this;
}
/**
* Will include bill name + ID, if any.
*

View File

@ -699,7 +699,7 @@ class GroupCollector implements GroupCollectorInterface
$result = $this->convertToInteger($result);
$result['reconciled'] = 1 === (int)$result['reconciled'];
if (array_key_exists('tag_id', $result)) { // assume the other fields are present as well.
if (array_key_exists('tag_id', $result) && null !== $result['tag_id']) { // assume the other fields are present as well.
$tagId = (int)$augumentedJournal['tag_id'];
$tagDate = null;
try {

View File

@ -520,6 +520,13 @@ interface GroupCollectorInterface
*/
public function withTagInformation(): GroupCollectorInterface;
/**
* Limit results to transactions without a bill..
*
* @return GroupCollectorInterface
*/
public function withBill(): GroupCollectorInterface;
/**
* Limit results to a transactions without a bill.
*
@ -542,8 +549,6 @@ interface GroupCollectorInterface
public function withoutCategory(): GroupCollectorInterface;
/**
* @param string $value
*
* @return GroupCollectorInterface
*/
public function withoutNotes(): GroupCollectorInterface;

View File

@ -91,7 +91,7 @@ class ReportHelper implements ReportHelperInterface
'paid_moments' => [],
];
/** @var Carbon $start */
/** @var Carbon $expectedStart */
foreach ($expectedDates as $expectedStart) {
$expectedEnd = app('navigation')->endOfX($expectedStart, $bill->repeat_freq, null);

View File

@ -39,9 +39,7 @@ use Log;
*/
class LinkController extends Controller
{
/** @var LinkTypeRepositoryInterface */
private $repository;
private LinkTypeRepositoryInterface $repository;
/**
* LinkController constructor.

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Admin;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
@ -62,8 +63,7 @@ class UpdateController extends Controller
* Show page with update options.
*
* @return Factory|View
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws FireflyException
*/
public function index()
{

View File

@ -59,7 +59,7 @@ class TwoFactorController extends Controller
{
/** @var array $mfaHistory */
$mfaHistory = Preferences::get('mfa_history', [])->data;
$mfaCode = $request->get('one_time_password');
$mfaCode = (string)$request->get('one_time_password');
// is in history? then refuse to use it.
if ($this->inMFAHistory($mfaCode, $mfaHistory)) {

View File

@ -159,7 +159,6 @@ class IndexController extends Controller
continue;
}
/** @var TransactionCurrency $currency */
$currencyId = $bill['currency_id'];
$sums[$groupOrder][$currencyId] = $sums[$groupOrder][$currencyId] ?? [
'currency_id' => $currencyId,

View File

@ -135,16 +135,21 @@ class BudgetLimitController extends Controller
if (null === $currency || null === $budget) {
throw new FireflyException('No valid currency or budget.');
}
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
$start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
$amount = (string)$request->get('amount');
$start->startOfDay();
$end->startOfDay();
if ('' === $amount) {
return response()->json([]);
}
Log::debug(sprintf('Start: %s, end: %s', $start->format('Y-m-d'), $end->format('Y-m-d')));
$limit = $this->blRepository->find($budget, $currency, $start, $end);
if (null !== $limit) {
$limit->amount = $request->get('amount');
$limit->amount = $amount;
$limit->save();
}
if (null === $limit) {
@ -154,7 +159,7 @@ class BudgetLimitController extends Controller
'currency_id' => (int)$request->get('transaction_currency_id'),
'start_date' => $start,
'end_date' => $end,
'amount' => $request->get('amount'),
'amount' => $amount,
]
);
}
@ -176,7 +181,7 @@ class BudgetLimitController extends Controller
return response()->json($array);
}
return redirect(route('budgets.index', [$start->format('Y-m-d'), $end->format('Y-m-d')]));
return response()->json([]);
}
/**
@ -187,7 +192,10 @@ class BudgetLimitController extends Controller
*/
public function update(Request $request, BudgetLimit $budgetLimit): JsonResponse
{
$amount = $request->get('amount');
$amount = (string)$request->get('amount');
if ('' === $amount) {
$amount = '0';
}
$limit = $this->blRepository->update($budgetLimit, ['amount' => $amount]);
$array = $limit->toArray();

View File

@ -144,7 +144,7 @@ class ShowController extends Controller
*/
public function show(Request $request, Budget $budget)
{
/** @var Carbon $start */
/** @var Carbon $allStart */
$allStart = session('first', Carbon::now()->startOfYear());
$allEnd = today();
$page = (int)$request->get('page');

View File

@ -108,7 +108,7 @@ class BudgetController extends Controller
$currencies = [];
$defaultEntries = [];
while ($end >= $loopStart) {
/** @var Carbon $currentEnd */
/** @var Carbon $loopEnd */
$loopEnd = app('navigation')->endOfPeriod($loopStart, $step);
$spent = $this->opsRepository->sumExpenses($loopStart, $loopEnd, null, $collection);
$label = trim(app('navigation')->periodShow($loopStart, $step));

View File

@ -283,8 +283,8 @@ class CategoryController extends Controller
* Chart for a specific period.
* TODO test method, for category refactor.
*
* @param Category $category
* @param $date
* @param Category $category
* @param Carbon $date
*
* @return JsonResponse
*/

View File

@ -140,10 +140,8 @@ class CategoryReportController extends Controller
* @param Collection $categories
* @param Carbon $start
* @param Carbon $end
* @param string $others
*
* @return JsonResponse
*
*/
public function categoryIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): JsonResponse
{

View File

@ -80,9 +80,9 @@ class PiggyBankController extends Controller
$locale = app('steam')->getLocale();
// get first event or start date of piggy bank or today
$startDate = $piggyBank->start_date ?? today(config('app.timezone'));
$startDate = $piggyBank->startdate ?? today(config('app.timezone'));
/** @var PiggyBankEvent $first */
/** @var PiggyBankEvent $firstEvent */
$firstEvent = $set->first();
$firstDate = null === $firstEvent ? new Carbon : $firstEvent->date;

View File

@ -52,12 +52,10 @@ class TransactionController extends Controller
}
/**
* @param string $objectType
* @param Carbon $start
* @param Carbon $end
*
* @return JsonResponse
* @throws FireflyException
*/
public function budgets(Carbon $start, Carbon $end)
{

View File

@ -32,7 +32,7 @@ class HelpController extends Controller
/**
* Show help for a route.
*
* @param $route
* @param string $route
*
* @return JsonResponse
*/

View File

@ -30,54 +30,9 @@ use Illuminate\Http\Request;
/**
* Class AutoCompleteController.
*
* TODO autocomplete for transaction types.
*
*/
class AutoCompleteController extends Controller
{
/**
* Searches in the titles of all transaction journals.
* The result is limited to the top 15 unique results.
*
* If the query is numeric, it will append the journal with that particular ID.
*
* @param Request $request
*
* @return JsonResponse
*/
public function allJournalsWithID(Request $request): JsonResponse
{
$search = (string)$request->get('search');
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
/** @var TransactionGroupRepositoryInterface $groupRepos */
$groupRepos = app(TransactionGroupRepositoryInterface::class);
$result = $repository->searchJournalDescriptions($search);
$array = [];
if (is_numeric($search)) {
// search for group, not journal.
$firstResult = $groupRepos->find((int)$search);
if (null !== $firstResult) {
// group may contain multiple journals, each a result:
foreach ($firstResult->transactionJournals as $journal) {
$array[] = $journal->toArray();
}
}
}
// if not numeric, search ahead!
// limit and unique
$limited = $result->slice(0, 15);
$array = array_merge($array, $limited->toArray());
foreach ($array as $index => $item) {
// give another key for consistency
$array[$index]['name'] = sprintf('#%d: %s', $item['transaction_group_id'], $item['description']);
}
return response()->json($array);
}
}

View File

@ -191,7 +191,7 @@ class PreferencesController extends Controller
// same for locale:
if (!auth()->user()->hasRole('demo')) {
/** @var Preference $currentLocale */
/** @var Preference $locale */
$locale = $request->get('locale');
app('preferences')->set('locale', $locale);
}

View File

@ -148,13 +148,13 @@ class CreateController extends Controller
RecurrenceRepetition::WEEKEND_TO_MONDAY => (string)trans('firefly.jump_to_monday'),
];
/** @var Transaction $source */
/** @var Transaction $dest */
// fill prefilled with journal info
$type = strtolower($journal->transactionType->type);
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
/** @var Transaction $dest */
$dest = $journal->transactions()->where('amount', '>', 0)->first();
$category = $journal->categories()->first() ? $journal->categories()->first()->name : '';
$budget = $journal->budgets()->first() ? $journal->budgets()->first()->id : 0;

View File

@ -122,7 +122,7 @@ class TagController extends Controller
foreach ($earned as $currency) {
$currencyId = $currency['currency_id'];
/** @var array $category */
/** @var array $tag */
foreach ($currency['tags'] as $tag) {
foreach ($tag['transaction_journals'] as $journal) {
$destinationId = $journal['destination_account_id'];

View File

@ -73,12 +73,10 @@ class IndexController extends Controller
*/
public function index()
{
/** @var User $user */
$user = auth()->user();
$this->createDefaultRuleGroup();
$this->createDefaultRule();
$this->ruleGroupRepos->resetOrder();
$ruleGroups = $this->ruleGroupRepos->getRuleGroupsWithRules(null);
$ruleGroups = $this->ruleGroupRepos->getAllRuleGroupsWithRules(null);
return prefixView('rules.index', compact('ruleGroups'));
}

View File

@ -105,6 +105,7 @@ class InstallController extends Controller
'firefly-iii:fix-recurring-transactions' => [],
'firefly-iii:unify-group-accounts' => [],
'firefly-iii:fix-transaction-types' => [],
'firefly-iii:fix-frontpage-accounts' => [],
// final command to set latest version in DB
'firefly-iii:set-latest-version' => ['--james-is-cool' => true],

View File

@ -100,7 +100,7 @@ class CreateController extends Controller
$repository = app(AccountRepositoryInterface::class);
$cash = $repository->getCashAccount();
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string)trans('breadcrumbs.create_new_transaction');
$subTitle = (string)trans(sprintf('breadcrumbs.create_%s', strtolower((string)$objectType)));
$subTitleIcon = 'fa-plus';
$optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');

View File

@ -34,7 +34,7 @@ use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/** @var int The headers to check. */
protected $headers = Request::HEADER_X_FORWARDED_ALL;
//protected $headers = Request::HEADER_X_FORWARDED_ALL;
/**
* TrustProxies constructor.

View File

@ -65,7 +65,7 @@ class BudgetFormStoreRequest extends FormRequest
'active' => 'numeric|between:0,1',
'auto_budget_type' => 'numeric|between:0,2',
'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_amount' => 'min:0|max:1000000000',
'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',
];
}

View File

@ -75,7 +75,7 @@ class BudgetFormUpdateRequest extends FormRequest
'active' => 'numeric|between:0,1',
'auto_budget_type' => 'numeric|between:0,2',
'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_amount' => 'min:0|max:1000000000',
'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2',
'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',
];
}

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
@ -87,6 +88,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|Account withTrashed()
* @method static Builder|Account withoutTrashed()
* @mixin Eloquent
* @property Carbon $lastActivityDate
*/
class Account extends Model
{

View File

@ -74,6 +74,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|Budget withTrashed()
* @method static Builder|Budget withoutTrashed()
* @mixin Eloquent
* @property string $email
*/
class Budget extends Model
{

View File

@ -44,6 +44,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int $user_id
* @property string $name
* @property Carbon $lastActivity
* @property bool $encrypted
* @property-read Collection|\FireflyIII\Models\Attachment[] $attachments
* @property-read int|null $attachments_count

View File

@ -40,6 +40,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property string $name
* @property string $outward
* @property string $inward
* @property int $journalCount
* @property bool $editable
* @property-read Collection|\FireflyIII\Models\TransactionJournalLink[] $transactionJournalLinks
* @property-read int|null $transaction_journal_links_count

View File

@ -22,7 +22,6 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
@ -33,13 +32,13 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Preference
*
* @property int $id
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property int $user_id
* @property string $name
* @property int|string|array|null $data
* @property-read User $user
* @property int $user_id
* @property string $name
* @property int|string|array|null $data
* @property-read User $user
* @method static Builder|Preference newModelQuery()
* @method static Builder|Preference newQuery()
* @method static Builder|Preference query()
@ -73,19 +72,29 @@ class Preference extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return Preference
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): Preference
{
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
/** @var Preference $preference */
/** @var Preference|null $preference */
$preference = $user->preferences()->where('name', $value)->first();
if (null !== $preference) {
return $preference;
}
$default = config('firefly.default_preferences');
if (array_key_exists($value, $default)) {
$preference = new Preference;
$preference->name = $value;
$preference->data = $default[$value];
$preference->user_id = $user->id;
$preference->save();
return $preference;
}
}
throw new NotFoundHttpException;
}

View File

@ -83,6 +83,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Query\Builder|Transaction withTrashed()
* @method static \Illuminate\Database\Query\Builder|Transaction withoutTrashed()
* @mixin Eloquent
* @property int $the_count
*/
class Transaction extends Model
{

View File

@ -114,6 +114,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @mixin Eloquent
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Location[] $locations
* @property-read int|null $locations_count
* @property int $the_count
*/
class TransactionJournal extends Model
{

View File

@ -46,6 +46,7 @@ interface AccountRepositoryInterface
*/
public function count(array $types): int;
/**
* Moved here from account CRUD.
*
@ -65,6 +66,14 @@ interface AccountRepositoryInterface
*/
public function expandWithDoubles(Collection $accounts): Collection;
/**
* @param string $number
* @param array $types
*
* @return Account|null
*/
public function findByAccountNumber(string $number, array $types): ?Account;
/**
* @param string $iban
* @param array $types

View File

@ -98,10 +98,10 @@ class BudgetRepository implements BudgetRepositoryInterface
$budgets = $this->getBudgets();
/** @var Budget $budget */
foreach ($budgets as $budget) {
DB::table('budget_transaction')->where('budget_id', $budget->id)->delete();
DB::table('budget_transaction_journal')->where('budget_id', $budget->id)->delete();
RecurrenceTransactionMeta::where('name', 'budget_id')->where('value', $budget->id)->delete();
RuleAction::where('action_type', 'set_budget')->where('action_value', $budget->id)->delete();
DB::table('budget_transaction')->where('budget_id', (int)$budget->id)->delete();
DB::table('budget_transaction_journal')->where('budget_id', (int)$budget->id)->delete();
RecurrenceTransactionMeta::where('name', 'budget_id')->where('value', (string)$budget->id)->delete();
RuleAction::where('action_type', 'set_budget')->where('action_value', (string)$budget->id)->delete();
$budget->delete();
}
}
@ -328,7 +328,7 @@ class BudgetRepository implements BudgetRepositoryInterface
Log::error($e->getTraceAsString());
throw new FireflyException('400002: Could not store budget.', 0, $e);
}
if (!array_key_exists('auto_budget_type', $data)) {
if (!array_key_exists('auto_budget_type', $data) || !array_key_exists('auto_budget_amount', $data) || !array_key_exists('auto_budget_period', $data)) {
return $newBudget;
}
$type = $data['auto_budget_type'];

View File

@ -258,6 +258,58 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
);
}
/**
* @param string|null $filter
*
* @return Collection
*/
public function getAllRuleGroupsWithRules(?string $filter): Collection
{
$groups = $this->user->ruleGroups()
->orderBy('order', 'ASC')
->with(
[
'rules' => static function (HasMany $query) {
$query->orderBy('order', 'ASC');
},
'rules.ruleTriggers' => static function (HasMany $query) {
$query->orderBy('order', 'ASC');
},
'rules.ruleActions' => static function (HasMany $query) {
$query->orderBy('order', 'ASC');
},
]
)->get();
if (null === $filter) {
return $groups;
}
Log::debug(sprintf('Will filter getRuleGroupsWithRules on "%s".', $filter));
return $groups->map(
function (RuleGroup $group) use ($filter) {
Log::debug(sprintf('Now filtering group #%d', $group->id));
// filter the rules in the rule group:
$group->rules = $group->rules->filter(
function (Rule $rule) use ($filter) {
Log::debug(sprintf('Now filtering rule #%d', $rule->id));
foreach ($rule->ruleTriggers as $trigger) {
if ('user_action' === $trigger->trigger_type && $filter === $trigger->trigger_value) {
Log::debug(sprintf('Rule #%d triggers on %s, include it.', $rule->id, $filter));
return true;
}
}
Log::debug(sprintf('Rule #%d does not trigger on %s, do not include it.', $rule->id, $filter));
return false;
}
);
return $group;
}
);
}
/**
* @param RuleGroup $group
*

View File

@ -114,6 +114,15 @@ interface RuleGroupRepositoryInterface
*/
public function getRuleGroupsWithRules(?string $filter): Collection;
/**
* Also inactive groups.
*
* @param string|null $filter
*
* @return Collection
*/
public function getAllRuleGroupsWithRules(?string $filter): Collection;
/**
* @param RuleGroup $group
*

View File

@ -79,6 +79,7 @@ trait JournalServiceTrait
$result = $this->findAccountById($data, $expectedTypes[$transactionType]);
$result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType]);
$result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType]);
$result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType]);
$result = $this->createAccount($result, $data, $expectedTypes[$transactionType][0]);
return $this->getCashAccount($result, $data, $expectedTypes[$transactionType]);
@ -160,6 +161,34 @@ trait JournalServiceTrait
return $account;
}
/**
* @param Account|null $account
* @param array $data
* @param array $types
*
* @return Account|null
*/
private function findAccountByNumber(?Account $account, array $data, array $types): ?Account
{
// third attempt, find by account number
if (null === $account && null !== $data['number']) {
Log::debug(sprintf('Searching for account number "%s".', $data['number']));
// find by preferred type.
$source = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]);
// or any expected type.
$source = $source ?? $this->accountRepository->findByAccountNumber((string) $data['number'], $types);
if (null !== $source) {
Log::debug(sprintf('Found account: #%d, %s', $source->id, $source->name));
$account = $source;
}
}
return $account;
}
/**
* @param Account|null $account
* @param array $data

View File

@ -292,7 +292,7 @@ class JournalUpdateService
$validator->setTransactionType($expectedType);
$validator->setUser($this->transactionJournal->user);
$validator->source = $this->getValidSourceAccount();
$result = $validator->validateDestination($destId, $destName, null);
$result = $validator->validateDestination($destId, $destName, null);
Log::debug(sprintf('hasValidDestinationAccount(%d, "%s") will return %s', $destId, $destName, var_export($result, true)));
// TODO typeOverrule: the account validator may have another opinion on the transaction type.
@ -527,6 +527,10 @@ class JournalUpdateService
Log::debug('Will update budget.');
$this->storeBudget($this->transactionJournal, new NullArrayObject($this->data));
}
// is transfer? remove budget
if (TransactionType::TRANSFER === $this->transactionJournal->transactionType->type) {
$this->transactionJournal->budgets()->sync([]);
}
}
/**

View File

@ -221,16 +221,15 @@ class ExpandedForm
/** @var Eloquent $entry */
foreach ($set as $entry) {
$entryId = (int)$entry->id; // @phpstan-ignore-line
$current = $entry->toArray();
$title = null;
foreach ($fields as $field) {
if (property_exists($entry, $field) && null === $title) {
$title = $entry->$field; // @phpstan-ignore-line
if (array_key_exists($field, $current) && null === $title) {
$title = $current[$field]; // @phpstan-ignore-line
}
}
$selectList[$entryId] = $title;
}
return $selectList;
}

View File

@ -153,7 +153,7 @@ class Preferences
*/
public function getForUser(User $user, string $name, $default = null): ?Preference
{
$preference = Preference::where('user_id', $user->id)->where('name', $name)->first(['id', 'name', 'data', 'updated_at', 'created_at']);
$preference = Preference::where('user_id', $user->id)->where('name', $name)->first(['id','user_id', 'name', 'data', 'updated_at', 'created_at']);
if (null !== $preference && null === $preference->data) {
try {
$preference->delete();

View File

@ -476,6 +476,12 @@ class OperatorQuerySearch implements SearchInterface
//
// bill
//
case 'has_no_bill':
$this->collector->withoutBill();
break;
case 'has_any_bill':
$this->collector->withBill();
break;
case 'bill_is':
$result = $this->billRepository->searchBill($value, 25);
if ($result->count() > 0) {

View File

@ -56,8 +56,8 @@ class SetDestinationAccount implements ActionInterface
*/
public function actOnArray(array $journal): bool
{
$user = User::find($journal['user_id']);
$type = $journal['transaction_type_type'];
$user = User::find($journal['user_id']);
$type = $journal['transaction_type_type'];
/** @var TransactionJournal|null $object */
$object = $user->transactionJournals()->find((int)$journal['transaction_journal_id']);
$this->repository = app(AccountRepositoryInterface::class);
@ -108,8 +108,9 @@ class SetDestinationAccount implements ActionInterface
}
// if this is a withdrawal, the new destination account must be a expense account and may be created:
// or it is a liability, in which case it must be returned.
if (TransactionType::WITHDRAWAL === $type) {
$newAccount = $this->findExpenseAccount();
$newAccount = $this->findWithdrawalDestinationAccount();
}
Log::debug(sprintf('New destination account is #%d ("%s").', $newAccount->id, $newAccount->name));
@ -145,9 +146,10 @@ class SetDestinationAccount implements ActionInterface
/**
* @return Account
*/
private function findExpenseAccount(): Account
private function findWithdrawalDestinationAccount(): Account
{
$account = $this->repository->findByName($this->action->action_value, [AccountType::EXPENSE]);
$allowed = config('firefly.expected_source_types.destination.Withdrawal');
$account = $this->repository->findByName($this->action->action_value, $allowed);
if (null === $account) {
$data = [
'name' => $this->action->action_value,

View File

@ -105,8 +105,9 @@ class SetSourceAccount implements ActionInterface
}
// if this is a deposit, the new source account must be a revenue account and may be created:
// or its a liability
if (TransactionType::DEPOSIT === $type) {
$newAccount = $this->findRevenueAccount();
$newAccount = $this->findDepositSourceAccount();
}
Log::debug(sprintf('New source account is #%d ("%s").', $newAccount->id, $newAccount->name));
@ -140,7 +141,7 @@ class SetSourceAccount implements ActionInterface
/**
* @return Account
*/
private function findRevenueAccount(): Account
private function findDepositSourceAccount(): Account
{
$allowed = config('firefly.expected_source_types.source.Deposit');
$account = $this->repository->findByName($this->action->action_value, $allowed);

View File

@ -174,6 +174,11 @@ class SearchRuleEngine implements RuleEngineInterface
private function fireRule(Rule $rule): bool
{
Log::debug(sprintf('Now going to fire rule #%d', $rule->id));
if (false === $rule->active) {
Log::debug(sprintf('Rule #%d is not active!', $rule->id));
return false;
}
if (true === $rule->strict) {
Log::debug(sprintf('Rule #%d is a strict rule.', $rule->id));
@ -223,7 +228,7 @@ class SearchRuleEngine implements RuleEngineInterface
Log::debug(sprintf('Now in findStrictRule(#%d)', $rule->id ?? 0));
$searchArray = [];
/** @var RuleTrigger $ruleTrigger */
foreach ($rule->ruleTriggers()->where('active',1)->get() as $ruleTrigger) {
foreach ($rule->ruleTriggers()->where('active', 1)->get() as $ruleTrigger) {
// if needs no context, value is different:
$needsContext = config(sprintf('firefly.search.operators.%s.needs_context', $ruleTrigger->trigger_type)) ?? true;
if (false === $needsContext) {
@ -368,7 +373,7 @@ class SearchRuleEngine implements RuleEngineInterface
{
Log::debug(sprintf('SearchRuleEngine:: Will now execute actions on transaction journal #%d', $transaction['transaction_journal_id']));
/** @var RuleAction $ruleAction */
foreach ($rule->ruleActions()->where('active',1)->get() as $ruleAction) {
foreach ($rule->ruleActions()->where('active', 1)->get() as $ruleAction) {
$break = $this->processRuleAction($ruleAction, $transaction);
if (true === $break) {
break;
@ -444,7 +449,7 @@ class SearchRuleEngine implements RuleEngineInterface
$total = new Collection;
$count = 0;
/** @var RuleTrigger $ruleTrigger */
foreach ($rule->ruleTriggers()->where('active',1)->get() as $ruleTrigger) {
foreach ($rule->ruleTriggers()->where('active', 1)->get() as $ruleTrigger) {
if ('user_action' === $ruleTrigger->trigger_type) {
Log::debug('Skip trigger type.');
continue;

View File

@ -117,7 +117,7 @@ class AccountTransformer extends AbstractTransformer
'opening_balance_date' => $openingBalanceDate,
'liability_type' => $liabilityType,
'liability_direction' => $liabilityDirection,
'interest' => (float)$interest,
'interest' => $interest,
'interest_period' => $interestPeriod,
'include_net_worth' => $includeNetWorth,
'longitude' => $longitude,

View File

@ -48,16 +48,21 @@ trait ValidatesAutoBudgetRequest
return;
}
// basic float check:
if (!is_numeric($amount)) {
$validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget'));
return;
}
if ('' === $amount) {
$validator->errors()->add('auto_budget_amount', (string)trans('validation.amount_required_for_auto_budget'));
}
if (null !== $amount && 1 !== bccomp((string)$amount, '0')) {
if (1 !== bccomp((string)$amount, '0')) {
$validator->errors()->add('auto_budget_amount', (string)trans('validation.auto_budget_amount_positive'));
}
if ('' === $period) {
$validator->errors()->add('auto_budget_period', (string)trans('validation.auto_budget_period_mandatory'));
}
if (null !== $amount && null !== $currencyId && null !== $currencyCode && '' === $currencyCode && 0 === $currencyId) {
if (null !== $currencyId && null !== $currencyCode && '' === $currencyCode && 0 === $currencyId) {
$validator->errors()->add('auto_budget_amount', (string)trans('validation.require_currency_info'));
}
}

View File

@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 5.5.9 (API 1.5.2) 2021-04-24
This update fixes some of the more annoying issues in the new experimental v2 layout (see also [GitHub](https://github.com/firefly-iii/firefly-iii/issues/4618)), but some minor other issues as well.
### Fixed
- Dashboard preferences would some times retain old or bad data.
### API
- [Issue 4697](https://github.com/firefly-iii/firefly-iii/issues/4697) Submitting an existing account with an account number only would store it as a new account.
- [Issue 4706](https://github.com/firefly-iii/firefly-iii/issues/4706) Account interest was a float and not a string.
- Store Budget API call would not properly handle auto budgets.
## 5.5.8 (API 1.5.2) 2021-04-17
This update fixes some of the more annoying issues in the new experimental v2 layout (see also [GitHub](https://github.com/firefly-iii/firefly-iii/issues/4618)), but some minor other issues as well.
### Fixed
- [Issue 4656](https://github.com/firefly-iii/firefly-iii/issues/4656) [issue 4660](https://github.com/firefly-iii/firefly-iii/issues/4660) Various fixes in the v2 layout.
- [Issue 4663](https://github.com/firefly-iii/firefly-iii/issues/4663) It was possible to assign a budget to a transfer.
- [Issue 4664](https://github.com/firefly-iii/firefly-iii/issues/4664) Null pointer in bulk editor
- [Issue 4668](https://github.com/firefly-iii/firefly-iii/issues/4668) Inactive rule groups would not be listed.
## 5.5.7 (API 1.5.2) 2021-04-11
### Added
- [Issue 4627](https://github.com/firefly-iii/firefly-iii/issues/4627) The search and rule engine can search for any transaction with any bill or with no bill at all. Thanks, @devfaz!
### Fixed
- [Issue 4625](https://github.com/firefly-iii/firefly-iii/issues/4625) Old MySQL servers would choke on the migrations.
- [Issue 4625](https://github.com/firefly-iii/firefly-iii/issues/4625) Some arrays are null when Firefly III starts for the first time.
- [Issue 4628](https://github.com/firefly-iii/firefly-iii/issues/4628) Every transaction appeared to have attachments.
- [Issue 4635](https://github.com/firefly-iii/firefly-iii/issues/4635) Export command ignores your dates. Thanks for the suggested fix, @urquilla!
- [Issue 4646](https://github.com/firefly-iii/firefly-iii/issues/4646) Empty select list
## 5.5.6 (API 1.5.2) 2021-04-09
Firefly III features a new *experimental* layout that I'm currently building. You can enable it by setting environment variable `FIREFLY_III_LAYOUT=v2`. Check out [GitHub](https://github.com/firefly-iii/firefly-iii/issues/4618) for the announcement and status updates. This release features an update API version. Check out [the difference](https://github.com/firefly-iii/api-docs-generator/compare/1.5.1...1.5.2).

View File

@ -115,8 +115,7 @@
"phpstan/phpstan-deprecation-rules": "^0.12.5",
"phpunit/phpunit": "^9.2",
"roave/security-advisories": "dev-master",
"thecodingmachine/phpstan-strict-rules": "^0.12.0",
"vimeo/psalm": "^4.1"
"thecodingmachine/phpstan-strict-rules": "^0.12.0"
},
"suggest": {
"adldap2/adldap2-laravel": "If you want to login using LDAP.",
@ -192,6 +191,7 @@
"@php artisan firefly-iii:fix-recurring-transactions",
"@php artisan firefly-iii:unify-group-accounts",
"@php artisan firefly-iii:fix-transaction-types",
"@php artisan firefly-iii:fix-frontpage-accounts",
"@php artisan firefly-iii:report-empty-objects",
"@php artisan firefly-iii:report-sum",

906
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -100,7 +100,7 @@ return [
'handle_debts' => true,
],
'version' => '5.5.6',
'version' => '5.5.9',
'api_version' => '1.5.2',
'db_version' => 16,
'maxUploadSize' => 1073741824, // 1 GB
@ -477,6 +477,8 @@ return [
'has_any_category' => ['alias' => false, 'needs_context' => false,],
'has_no_budget' => ['alias' => false, 'needs_context' => false,],
'has_any_budget' => ['alias' => false, 'needs_context' => false,],
'has_no_bill' => ['alias' => false, 'needs_context' => false,],
'has_any_bill' => ['alias' => false, 'needs_context' => false,],
'has_no_tag' => ['alias' => false, 'needs_context' => false,],
'has_any_tag' => ['alias' => false, 'needs_context' => false,],
'notes_contain' => ['alias' => false, 'needs_context' => true,],
@ -628,14 +630,12 @@ return [
// expected source types for each transaction type, in order of preference.
'expected_source_types' => [
'source' => [
TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE,
AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION,],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE,],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
TransactionTypeModel::LIABILITY_CREDIT => [AccountType::LIABILITY_CREDIT,],
TransactionTypeModel::WITHDRAWAL => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::DEPOSIT => [AccountType::REVENUE, AccountType::CASH, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::TRANSFER => [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE],
TransactionTypeModel::OPENING_BALANCE => [AccountType::INITIAL_BALANCE, AccountType::ASSET, AccountType::LOAN, AccountType::DEBT,
AccountType::MORTGAGE,],
TransactionTypeModel::RECONCILIATION => [AccountType::RECONCILIATION, AccountType::ASSET],
// in case no transaction type is known yet, it could be anything.
'none' => [
AccountType::ASSET,
@ -866,4 +866,11 @@ return [
'valid_asset_fields' => ['account_role', 'account_number', 'currency_id', 'BIC', 'include_net_worth'],
'valid_cc_fields' => ['account_role', 'cc_monthly_payment_date', 'cc_type', 'account_number', 'currency_id', 'BIC', 'include_net_worth'],
'valid_account_fields' => ['account_number', 'currency_id', 'BIC', 'interest', 'interest_period', 'include_net_worth', 'liability_direction'],
'default_preferences' => [
'frontPageAccounts' => [],
'listPageSize' => 50,
'currencyPreference' => 'EUR',
'language' => 'en_US',
'locale' => 'equal',
],
];

View File

@ -47,7 +47,7 @@ yarn prod
# mv public/css ../public/v2
# also copy fonts
cp -r fonts ../public
#cp -r fonts ../public/v2/css
# remove built stuff
rm -rf public
rm -rf public/

View File

@ -11,6 +11,7 @@
},
"devDependencies": {
"axios": "^0.21",
"date-fns": "^2.21.1",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.19",
"lodash.clonedeep": "^4.5.0",
@ -29,7 +30,7 @@
"admin-lte": "^3.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"chart.js": "^3.0.2",
"chart.js": "^3.2.0",
"icheck-bootstrap": "^3.0.1",
"jquery-ui": "^1.12.1",
"leaflet": "^1.7.1",

View File

@ -22,7 +22,7 @@
<div>
<Alert :message="errorMessage" type="danger"/>
<Alert :message="successMessage" type="success"/>
<form @submit="submitForm">
<form @submit="submitForm" autocomplete="off">
<div class="row">
<div class="col-xl-6 col-lg-6 col-md-12 col-sm-12 col-xs-12">
<div class="card card-primary">
@ -217,7 +217,7 @@ export default {
},
methods: {
storeField: function (payload) {
console.log(payload);
// console.log(payload);
if ('location' === payload.field) {
if (true === payload.value.hasMarker) {
this.location = payload.value;
@ -232,8 +232,8 @@ export default {
e.preventDefault();
this.submitting = true;
let submission = this.getSubmission();
console.log('Will submit:');
console.log(submission);
// console.log('Will submit:');
// console.log(submission);
let url = './api/v1/accounts';
axios.post(url, submission)
@ -250,7 +250,7 @@ export default {
}
this.submitting = false;
if (this.resetFormAfter) {
console.log('reset!');
// console.log('reset!');
this.name = '';
this.liability_type = 'Loan';
this.liability_direction = 'debit';
@ -277,7 +277,7 @@ export default {
},
parseErrors: function (errors) {
this.errors = lodashClonedeep(this.defaultErrors);
console.log(errors);
// console.log(errors);
for (let i in errors.errors) {
if (errors.errors.hasOwnProperty(i)) {
this.errors[i] = errors.errors[i];

View File

@ -91,7 +91,7 @@ export default {
},
created() {
let pathName = window.location.pathname;
console.log(pathName);
// console.log(pathName);
let parts = pathName.split('/');
this.accountId = parseInt(parts[parts.length - 1]);
this.getAccount();
@ -153,7 +153,7 @@ export default {
}
);
// get accounts of the same type.
console.log('Go for "' + type + '"');
// console.log('Go for "' + type + '"');
},
getPiggyBankCount: function (type, currencyCode) {
axios.get('./api/v1/accounts/' + this.accountId + '/piggy_banks')

View File

@ -133,6 +133,7 @@
import {mapGetters} from "vuex";
import Sortable from "sortablejs";
import format from "date-fns/format";
export default {
name: "Index",
@ -259,9 +260,9 @@ export default {
this.fields.push({key: 'menu', label: ' ', sortable: false});
},
getAccountList: function () {
console.log('getAccountList()');
// console.log('getAccountList()');
if (this.indexReady && !this.loading && !this.downloaded) {
console.log('Index ready, not loading and not already downloaded. Reset.');
// console.log('Index ready, not loading and not already downloaded. Reset.');
this.loading = true;
this.perPage = this.listPageSize ?? 51;
this.accounts = [];
@ -269,14 +270,14 @@ export default {
this.downloadAccountList(1);
}
if (this.indexReady && !this.loading && this.downloaded) {
console.log('Index ready, not loading and not downloaded.');
// console.log('Index ready, not loading and not downloaded.');
this.loading = true;
this.filterAccountList();
// TODO filter accounts.
}
},
downloadAccountList: function (page) {
console.log('downloadAccountList(' + page + ')');
// console.log('downloadAccountList(' + page + ')');
axios.get('./api/v1/accounts?type=' + this.type + '&page=' + page)
.then(response => {
let currentPage = parseInt(response.data.meta.pagination.current_page);
@ -288,7 +289,7 @@ export default {
this.downloadAccountList(nextPage);
}
if (currentPage >= totalPage) {
console.log('Looks like all downloaded.');
// console.log('Looks like all downloaded.');
this.downloaded = true;
this.filterAccountList();
}
@ -296,7 +297,7 @@ export default {
);
},
filterAccountList: function () {
console.log('filterAccountList()');
// console.log('filterAccountList()');
this.accounts = [];
for (let i in this.allAccounts) {
if (this.allAccounts.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
@ -304,14 +305,14 @@ export default {
// 2 = inactive only
// 3 = both
if (1 === this.activeFilter && false === this.allAccounts[i].active) {
console.log('Skip account #' + this.allAccounts[i].id + ' because not active.');
// console.log('Skip account #' + this.allAccounts[i].id + ' because not active.');
continue;
}
if (2 === this.activeFilter && true === this.allAccounts[i].active) {
console.log('Skip account #' + this.allAccounts[i].id + ' because active.');
// console.log('Skip account #' + this.allAccounts[i].id + ' because active.');
continue;
}
console.log('Include account #' + this.allAccounts[i].id + '.');
// console.log('Include account #' + this.allAccounts[i].id + '.');
this.accounts.push(this.allAccounts[i]);
}
@ -330,7 +331,7 @@ export default {
//console.log('Total is now ' + this.total);
},
parseAccounts: function (data) {
console.log('In parseAccounts()');
// console.log('In parseAccounts()');
for (let key in data) {
if (data.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
let current = data[key];
@ -357,7 +358,7 @@ export default {
}
},
getAccountBalanceDifference: function (index, acct) {
console.log('getAccountBalanceDifference(' + index + ')');
// console.log('getAccountBalanceDifference(' + index + ')');
// get account on day 0
let promises = [];
@ -370,9 +371,10 @@ export default {
}
);
}));
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + this.start.toISOString().split('T')[0]));
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + this.end.toISOString().split('T')[0]));
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + startStr));
promises.push(axios.get('./api/v1/accounts/' + acct.id + '?date=' + endStr));
Promise.all(promises).then(responses => {
let index = responses[0].index;

View File

@ -102,11 +102,9 @@ export default {
},
ticks: {
callback: function (value, index, values) {
//return this.getLabelForValue(value);
let dateObj = new Date(this.getLabelForValue(value));
let options = {year: 'numeric', month: 'long', day: 'numeric'};
let str = new Intl.DateTimeFormat(localStorage.locale, options).format(dateObj);
return str;
let dateObj = new Date(this.getLabelForValue(value).split('T')[0]);
return new Intl.DateTimeFormat(localStorage.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(dateObj);
//return str;
// // //console.log();
// // //return self.formatLabel(value, 20);
// // return self.formatLabel(str, 20);

View File

@ -46,7 +46,8 @@
class="btn btn-secondary"
@click="resetDate"
><i class="fas fa-history"></i></button>
<button id="dropdownMenuButton" :title="$t('firefly.select_period')" aria-expanded="false" aria-haspopup="true" class="btn btn-secondary dropdown-toggle"
<button id="dropdownMenuButton" :title="$t('firefly.select_period')" aria-expanded="false" aria-haspopup="true"
class="btn btn-secondary dropdown-toggle"
data-toggle="dropdown"
type="button">
<i class="fas fa-list"></i>
@ -78,8 +79,23 @@
import {createNamespacedHelpers} from "vuex";
import Vue from "vue";
import DatePicker from "v-calendar/lib/components/date-picker.umd";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
import subDays from 'date-fns/subDays';
import addDays from 'date-fns/addDays';
import addMonths from 'date-fns/addMonths';
import startOfDay from 'date-fns/startOfDay';
import endOfDay from 'date-fns/endOfDay';
import startOfWeek from 'date-fns/startOfWeek';
import endOfWeek from 'date-fns/endOfWeek';
import endOfMonth from 'date-fns/endOfMonth';
import format from 'date-fns/format';
import startOfQuarter from 'date-fns/startOfQuarter';
import subMonths from 'date-fns/subMonths';
import endOfQuarter from 'date-fns/endOfQuarter';
import subQuarters from 'date-fns/subQuarters';
import addQuarters from 'date-fns/addQuarters';
import startOfMonth from 'date-fns/startOfMonth';
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
Vue.component('date-picker', DatePicker)
@ -130,50 +146,400 @@ export default {
this.generatePeriods()
return false;
},
generatePeriods: function () {
this.periods = [];
// create periods.
let today;
let end;
today = new Date(this.range.start);
// previous month
firstDayOfMonth = new Date(today.getFullYear(), today.getMonth()-1, 1);
lastDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 0);
generateDaily: function () {
let today = new Date(this.range.start);
// yesterday
this.periods.push(
{
start: firstDayOfMonth.toDateString(),
end: lastDayOfMonth.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(firstDayOfMonth)
start: startOfDay(subDays(today, 1)).toDateString(),
end: endOfDay(subDays(today, 1)).toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(subDays(today, 1))
}
);
// today
this.periods.push(
{
start: startOfDay(today).toDateString(),
end: endOfDay(today).toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(today)
}
);
// tomorrow:
this.periods.push(
{
start: startOfDay(addDays(today, 1)).toDateString(),
end: endOfDay(addDays(today, 1)).toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(addDays(today, 1))
}
);
// The Day After Tomorrow dun-dun-dun!
this.periods.push(
{
start: startOfDay(addDays(today, 2)).toDateString(),
end: endOfDay(addDays(today, 2)).toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(addDays(today, 2))
}
);
},
generateWeekly: function () {
//console.log('weekly');
let today = new Date(this.range.start);
//console.log('Today is ' + today);
let start = startOfDay(startOfWeek(subDays(today, 7), {weekStartsOn: 1}));
let end = endOfDay(endOfWeek(subDays(today, 7), {weekStartsOn: 1}));
let dateFormat = this.$t('config.week_in_year_fns');
//console.log('Date format: "'+dateFormat+'"');
let title = format(start, dateFormat);
// last week
// console.log('Last week');
// console.log(start);
// console.log(end);
// console.log(title);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// this week
start = startOfDay(startOfWeek(today, {weekStartsOn: 1}));
end = endOfDay(endOfWeek(today, {weekStartsOn: 1}));
title = format(start, dateFormat);
// console.log('This week');
// console.log(start);
// console.log(end);
// console.log(title);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// next week
start = startOfDay(startOfWeek(addDays(today, 7), {weekStartsOn: 1}));
end = endOfDay(endOfWeek(addDays(today, 7), {weekStartsOn: 1}));
title = format(start, dateFormat);
// console.log('Next week');
// console.log(start);
// console.log(end);
// console.log(title);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
},
generateMonthly: function () {
let today = new Date(this.range.start);
// previous month
let start = startOfDay(startOfMonth(subMonths(today, 1)));
let end = endOfDay(endOfMonth(subMonths(today, 1)));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(start)
}
);
// this month
firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
lastDayOfMonth = new Date(today.getFullYear(), today.getMonth()+1, 0);
start = startOfDay(startOfMonth(today));
end = endOfDay(endOfMonth(today));
this.periods.push(
{
start: firstDayOfMonth.toDateString(),
end: lastDayOfMonth.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(firstDayOfMonth)
start: start.toDateString(),
end: end.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(start)
}
);
// next month
let firstDayOfMonth = new Date(today.getFullYear(), today.getMonth()+1, 1);
let lastDayOfMonth = new Date(today.getFullYear(), today.getMonth()+2, 0);
start = startOfDay(startOfMonth(addMonths(today, 1)));
end = endOfDay(endOfMonth(addMonths(today, 1)));
this.periods.push(
{
start: firstDayOfMonth.toDateString(),
end: lastDayOfMonth.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(firstDayOfMonth)
start: start.toDateString(),
end: end.toDateString(),
title: new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long'}).format(start)
}
);
},
generateQuarterly: function () {
let today = new Date(this.range.start);
// last quarter
let start = startOfDay(startOfQuarter(subQuarters(today, 1)));
let end = endOfDay(endOfQuarter(subQuarters(today, 1)));
let dateFormat = this.$t('config.quarter_fns');
let title = format(start, dateFormat);
// last week
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// this quarter
start = startOfDay(startOfQuarter(today));
end = endOfDay(endOfQuarter(today));
title = format(start, dateFormat);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// next quarter
start = startOfDay(startOfQuarter(addQuarters(today, 1)));
end = endOfDay(endOfQuarter(addQuarters(today, 1)));
title = format(start, dateFormat);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
},
generateHalfYearly: function () {
let today = new Date(this.range.start);
let start;
let end;
let title = 'todo';
let half = 1;
// its currently first half of year:
if (today.getMonth() <= 5) {
// previous year, last half:
start = today;
start.setFullYear(start.getFullYear() - 1);
start.setMonth(6);
start.setDate(1);
start = startOfDay(start);
end = start;
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
half = 2;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// this year, first half:
start = today;
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = today;
end.setMonth(5);
end.setDate(30);
end = endOfDay(start);
half = 1;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// this year, second half:
start = today;
start.setMonth(6);
start.setDate(1);
start = startOfDay(start);
end = start;
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
half = 2;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
return;
}
// this year, first half:
start = today;
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = start;
end.setMonth(5);
end.setDate(30);
end = endOfDay(end);
half = 1;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// this year, current (second) half:
start = today;
start.setMonth(6);
start.setDate(1);
start = startOfDay(start);
end = today;
end.setMonth(11);
end.setDate(31);
end = endOfDay(start);
half = 2;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
// next year, first half:
start = today;
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = start;
end.setMonth(5);
end.setDate(30);
end = endOfDay(end);
half = 1;
title = format(start, this.$t('config.half_year_fns', {half: half}));
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: title
}
);
},
generateYearly: function () {
let today = new Date(this.range.start);
let start;
let end;
let title;
// last year
start = new Date(today);
start.setFullYear(start.getFullYear() - 1);
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = new Date(today);
end.setFullYear(end.getFullYear() - 1);
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: start.getFullYear()
}
);
// this year
start = new Date(today);
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = new Date(today);
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: start.getFullYear()
}
);
// next year
start = new Date(today);
start.setFullYear(start.getFullYear() + 1);
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = new Date(today);
end.setFullYear(end.getFullYear() + 1);
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
this.periods.push(
{
start: start.toDateString(),
end: end.toDateString(),
title: start.getFullYear()
}
);
},
generatePeriods: function () {
this.periods = [];
//console.log('The view range is "' + this.viewRange + '".');
switch (this.viewRange) {
case '1D':
this.generateDaily();
break;
case '1W':
this.generateWeekly();
break;
case '1M':
this.generateMonthly();
break;
case '3M':
this.generateQuarterly();
break;
case '6M':
this.generateHalfYearly();
break;
case '1Y':
this.generateYearly();
break;
}
// last 7 days
today = new Date;
end = new Date;
let today = new Date;
let end = new Date;
end.setDate(end.getDate() - 7);
this.periods.push(
{

View File

@ -49,12 +49,11 @@ import DataConverter from "../charts/DataConverter";
import DefaultLineOptions from "../charts/DefaultLineOptions";
import {mapGetters} from "vuex";
import * as ChartJs from 'chart.js'
import format from "date-fns/format";
ChartJs.Chart.register.apply(null, Object.values(ChartJs).filter((chartClass) => (chartClass.id)));
export default {
name: "MainAccount",
components: {}, // MainAccountChart
@ -63,6 +62,7 @@ export default {
loading: true,
error: false,
ready: false,
initialised: false,
dataCollection: {},
chartOptions: {},
_chart: null,
@ -71,18 +71,18 @@ export default {
}
},
created() {
this.ready = true;
this.chartOptions = DefaultLineOptions.methods.getDefaultOptions();
this.localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
this.systemTimeZone = this.timezone;
this.ready = true;
},
computed: {
...mapGetters('dashboard/index',['start', 'end']),
...mapGetters('root',['timezone']),
...mapGetters('dashboard/index', ['start', 'end']),
...mapGetters('root', ['timezone']),
'datesReady': function () {
return null !== this.start && null !== this.end && this.ready;
},
timezoneDifference: function() {
timezoneDifference: function () {
return this.localTimeZone !== this.systemTimeZone;
}
},
@ -93,18 +93,20 @@ export default {
}
},
start: function () {
//this.initialiseChart();
this.updateChart();
},
end: function () {
//this.initialiseChart();
this.updateChart();
},
},
methods: {
initialiseChart: function () {
this.loading = true;
this.error = false;
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
//let startStr = this.start.toISOString().split('T')[0];
//let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
let url = './api/v1/chart/account/overview?start=' + startStr + '&end=' + endStr;
axios.get(url)
.then(response => {
@ -122,12 +124,33 @@ export default {
});
},
drawChart: function () {
this._chart = new ChartJs.Chart(this.$refs.canvas.getContext('2d'), {
type: 'line',
data: this.dataCollection,
options: this.chartOptions
}
);
//console.log('drawChart');
if ('undefined' !== typeof this._chart) {
// console.log('update!');
this._chart.data = this.dataCollection;
this._chart.update();
this.initialised = true;
}
if ('undefined' === typeof this._chart) {
// console.log('new!');
this._chart = new ChartJs.Chart(this.$refs.canvas.getContext('2d'), {
type: 'line',
data: this.dataCollection,
options: this.chartOptions
}
);
this.initialised = true;
}
},
updateChart: function () {
// console.log('updateChart');
if (this.initialised) {
// console.log('MUST Update chart!');
// reset some vars so it wont trigger again:
this.initialised = false;
this.initialiseChart();
}
}
},
}

View File

@ -75,6 +75,7 @@
<script>
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
@ -123,7 +124,7 @@ export default {
initialiseList: function () {
this.loading = true;
this.accounts = [];
axios.get('./api/v1/preferences/frontpageAccounts')
axios.get('./api/v1/preferences/frontPageAccounts')
.then(response => {
this.loadAccounts(response);
}
@ -162,8 +163,10 @@ export default {
);
},
loadTransactions(key, accountId) {
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/accounts/' + accountId + '/transactions?page=1&limit=10&start=' + startStr + '&end=' + endStr)
.then(response => {
this.accounts[key].transactions = response.data.data;

View File

@ -77,6 +77,7 @@
</template>
<script>
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
export default {
@ -125,8 +126,10 @@ export default {
initialiseBills: function () {
this.loading = true;
this.bills = [];
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/bills?start=' + startStr + '&end=' + endStr)
.then(response => {

View File

@ -68,6 +68,7 @@
<script>
import BudgetListGroup from "./BudgetListGroup";
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
@ -136,8 +137,10 @@ export default {
other: [],
};
this.loading = true;
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/budgets?start=' + startStr + '&end=' + endStr)
.then(response => {
this.parseBudgets(response.data);
@ -164,7 +167,7 @@ export default {
spent: spentData.sum
}
);
console.log('Added budget ' + current.attributes.name + ' (' + spentData.currency_code + ')');
//console.log('Added budget ' + current.attributes.name + ' (' + spentData.currency_code + ')');
}
}
}
@ -172,8 +175,10 @@ export default {
this.getBudgetLimits();
},
getBudgetLimits() {
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/budget-limits?start=' + startStr + '&end=' + endStr)
.then(response => {
this.parseBudgetLimits(response.data);
@ -192,7 +197,7 @@ export default {
id: currentId,
name: current.attributes.name,
};
console.log('Collected meta data: budget #' + currentId + ' is named ' + current.attributes.name);
//console.log('Collected meta data: budget #' + currentId + ' is named ' + current.attributes.name);
}
}
@ -250,7 +255,7 @@ export default {
for (let i in this.rawBudgets) {
if (this.rawBudgets.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (this.rawBudgets[i].currency_id === currencyId && this.rawBudgets[i].id === budgetId) {
console.log('Budget ' + this.rawBudgets[i].name + ' with currency ' + this.rawBudgets[i].currency_code + ' will be removed in favor of a budget limit.');
//console.log('Budget ' + this.rawBudgets[i].name + ' with currency ' + this.rawBudgets[i].currency_code + ' will be removed in favor of a budget limit.');
this.rawBudgets.splice(parseInt(i), 1);
}
}

View File

@ -90,6 +90,7 @@
<script>
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
@ -136,27 +137,42 @@ export default {
},
methods:
{
getCategories() {
getCategories: function () {
this.categories = [];
this.sortedList = [];
this.spent = 0;
this.earned = 0;
this.loading = true;
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
axios.get('./api/v1/categories?start=' + startStr + '&end=' + endStr)
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
this.getCategoryPage(startStr, endStr, 1);
},
getCategoryPage: function (start, end, page) {
axios.get('./api/v1/categories?start=' + start + '&end=' + end + '&page=' + page)
.then(response => {
this.parseCategories(response.data);
this.loading = false;
let categories = response.data.data;
let currentPage = parseInt(response.data.meta.pagination.current_page);
let totalPages = parseInt(response.data.meta.pagination.total_pages);
this.parseCategories(categories);
if (currentPage < totalPages) {
let nextPage = currentPage + 1;
this.getCategoryPage(start, end, nextPage);
}
if (currentPage === totalPages) {
this.loading = false;
this.sortCategories();
}
}
).catch(error => {
this.error = true;
});
},
parseCategories(data) {
for (let i in data.data) {
if (data.data.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let current = data.data[i];
for (let i in data) {
if (data.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let current = data[i];
let entryKey = null;
let categoryId = parseInt(current.id);
@ -207,7 +223,6 @@ export default {
}
}
}
this.sortCategories();
},
sortCategories() {
// no longer care about keys:

View File

@ -75,6 +75,7 @@
<script>
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
@ -126,8 +127,10 @@ export default {
this.loading = true;
this.income = [];
this.error = false;
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/insight/income/revenue?start=' + startStr + '&end=' + endStr)
.then(response => {
// do something with response.

View File

@ -75,6 +75,7 @@
<script>
import {createNamespacedHelpers} from "vuex";
import format from "date-fns/format";
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
@ -125,8 +126,10 @@ export default {
this.loading = true;
this.error = false;
this.expenses = [];
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
// let startStr = this.start.toISOString().split('T')[0];
// let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
axios.get('./api/v1/insight/expense/expense?start=' + startStr + '&end=' + endStr)
.then(response => {
// do something with response.

View File

@ -30,7 +30,7 @@
<span v-if="error" class="info-box-text"><i class="fas fa-exclamation-triangle text-danger"></i></span>
<!-- balance in preferred currency -->
<span v-for="balance in prefCurrencyBalances" :title="balance.sub_title" class="info-box-number">{{ balance.value_parsed }}</span>
<span v-if="0 === prefCurrencyBalances.length" class="info-box-number">&nbsp;</span>
<div class="progress bg-info">
<div class="progress-bar" style="width: 0"></div>
</div>
@ -55,6 +55,7 @@
<span v-if="error" class="info-box-text"><i class="fas fa-exclamation-triangle text-danger"></i></span>
<!-- bills unpaid, in preferred currency. -->
<span v-for="balance in prefBillsUnpaid" class="info-box-number">{{ balance.value_parsed }}</span>
<span v-if="0===prefBillsUnpaid.length" class="info-box-number">&nbsp;</span>
<div class="progress bg-teal">
<div class="progress-bar" style="width: 0"></div>
@ -80,6 +81,7 @@
<span v-if="error" class="info-box-text"><i class="fas fa-exclamation-triangle text-danger"></i></span>
<!-- left to spend in preferred currency -->
<span v-for="left in prefLeftToSpend" :title="left.sub_title" class="info-box-number">{{ left.value_parsed }}</span>
<span v-if="0 === prefLeftToSpend.length" class="info-box-number">&nbsp;</span>
<div class="progress bg-success">
<div class="progress-bar" style="width: 0"></div>
@ -105,7 +107,7 @@
<span v-if="loading && !error" class="info-box-text"><i class="fas fa-spinner fa-spin"></i></span>
<span v-if="error" class="info-box-text"><i class="fas fa-exclamation-triangle text-danger"></i></span>
<span v-for="nw in prefNetWorth" :title="nw.sub_title" class="info-box-number">{{ nw.value_parsed }}</span>
<span v-if="0===prefNetWorth.length">&nbsp;</span>
<div class="progress bg-success">
<div class="progress-bar" style="width: 0"></div>
</div>
@ -124,6 +126,7 @@
<script>
import {createNamespacedHelpers} from "vuex";
import format from 'date-fns/format';
const {mapState, mapGetters, mapActions, mapMutations} = createNamespacedHelpers('dashboard/index')
export default {
@ -249,8 +252,12 @@ export default {
this.billsUnpaid = [];
this.leftToSpend = [];
this.netWorth = [];
let startStr = this.start.toISOString().split('T')[0];
let endStr = this.end.toISOString().split('T')[0];
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
//let startStr = this.start.toISOString().split('T')[0];
//let endStr = this.end.toISOString().split('T')[0];
//console.log(startStr);
//console.log(endStr);
axios.get('./api/v1/summary/basic?start=' + startStr + '&end=' + endStr)
.then(response => {
this.summary = response.data;

View File

@ -19,12 +19,20 @@
*/
// initial state
import startOfDay from "date-fns/startOfDay";
import endOfDay from 'date-fns/endOfDay'
import startOfWeek from 'date-fns/startOfWeek'
import endOfWeek from 'date-fns/endOfWeek'
import startOfQuarter from 'date-fns/startOfQuarter';
import endOfQuarter from 'date-fns/endOfQuarter';
import endOfMonth from "date-fns/endOfMonth";
import startOfMonth from 'date-fns/startOfMonth';
const state = () => (
{
viewRange: 'default',
start: null,
end: null,
// default range:
defaultStart: null,
defaultEnd: null,
}
@ -53,24 +61,32 @@ const getters = {
// actions
const actions = {
initialiseStore(context) {
if ('default' === context.state.viewRange) {
axios.get('./api/v1/preferences/viewRange')
.then(response => {
let viewRange = response.data.data.attributes.data;
context.commit('setViewRange', viewRange);
// call another action:
// console.log('initialiseStore');
// restore from local storage:
context.dispatch('restoreViewRange');
axios.get('./api/v1/preferences/viewRange')
.then(response => {
let viewRange = response.data.data.attributes.data;
let oldViewRange = context.getters.viewRange;
context.commit('setViewRange', viewRange);
if (viewRange !== oldViewRange) {
// console.log('View range changed from "' + oldViewRange + '" to "' + viewRange + '"');
context.dispatch('setDatesFromViewRange');
}
).catch(error => {
// console.log(error);
context.commit('setViewRange', '1M');
// call another action:
context.dispatch('setDatesFromViewRange');
});
}
if (viewRange === oldViewRange) {
// console.log('Restore view range dates');
context.dispatch('restoreViewRangeDates');
}
}
).catch(() => {
context.commit('setViewRange', '1M');
context.dispatch('setDatesFromViewRange');
});
},
setDatesFromViewRange(context) {
// console.log('Must set dates from viewRange "' + context.state.viewRange + '"');
restoreViewRangeDates: function (context) {
// check local storage first?
if (localStorage.viewRangeStart) {
// console.log('view range start set from local storage.');
@ -91,89 +107,76 @@ const actions = {
// console.log(localStorage.viewRangeDefaultEnd);
context.commit('setDefaultEnd', new Date(localStorage.viewRangeDefaultEnd));
}
if (null !== context.getters.end && null !== context.getters.start) {
return;
},
restoreViewRange: function (context) {
// console.log('restoreViewRange');
let viewRange = localStorage.getItem('viewRange');
if (null !== viewRange) {
// console.log('restored restoreViewRange ' + viewRange );
context.commit('setViewRange', viewRange);
}
},
setDatesFromViewRange(context) {
let start;
let end;
let viewRange = context.getters.viewRange;
let today = new Date;
// console.log('Will recreate view range on ' + viewRange);
switch (viewRange) {
case '1D':
// one day:
start = new Date;
end = new Date(start.getTime());
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
// today:
start = startOfDay(today);
end = endOfDay(today);
break;
case '1W':
// this week:
start = new Date;
end = new Date(start.getTime());
// start of week
let diff = start.getDate() - start.getDay() + (start.getDay() === 0 ? -6 : 1);
start.setDate(diff);
start.setHours(0, 0, 0, 0);
// end of week
let lastday = end.getDate() - (end.getDay() - 1) + 6;
end.setDate(lastday);
end.setHours(23, 59, 59, 999);
start = startOfDay(startOfWeek(today, {weekStartsOn: 1}));
end = endOfDay(endOfWeek(today, {weekStartsOn: 1}));
break;
case '1M':
// this month:
start = new Date;
start = new Date(start.getFullYear(), start.getMonth(), 1);
start.setHours(0, 0, 0, 0);
end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999);
start = startOfDay(startOfMonth(today));
end = endOfDay(endOfMonth(today));
break;
case '3M':
// this quarter
start = new Date;
end = new Date;
let quarter = Math.floor((start.getMonth() + 3) / 3) - 1;
// start and end months? I'm sure this could be better:
let startMonths = [0, 3, 6, 9];
let endMonths = [2, 5, 8, 11];
// set start to the correct month, day one:
start = new Date(start.getFullYear(), startMonths[quarter], 1);
start.setHours(0, 0, 0, 0);
// set end to the correct month, day one
end = new Date(end.getFullYear(), endMonths[quarter], 1);
// then to the last day of the month:
end = new Date(end.getFullYear(), end.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999);
start = startOfDay(startOfQuarter(today));
end = endOfDay(endOfQuarter(today));
break;
case '6M':
// this half-year
start = new Date;
end = new Date;
let half = start.getMonth() <= 5 ? 0 : 1;
let startHalf = [0, 6];
let endHalf = [5, 11];
// set start to the correct month, day one:
start = new Date(start.getFullYear(), startHalf[half], 1);
start.setHours(0, 0, 0, 0);
// set end to the correct month, day one
end = new Date(end.getFullYear(), endHalf[half], 1);
// then to the last day of the month:
end = new Date(end.getFullYear(), end.getMonth() + 1, 0);
end.setHours(23, 59, 59, 999);
if (today.getMonth() <= 5) {
start = new Date(today);
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = new Date(today);
end.setMonth(5);
end.setDate(30);
end = endOfDay(start);
}
if (today.getMonth() > 5) {
start = new Date(today);
start.setMonth(6);
start.setDate(1);
start = startOfDay(start);
end = new Date(today);
end.setMonth(11);
end.setDate(31);
end = endOfDay(start);
}
break;
case '1Y':
// this year
start = new Date;
end = new Date;
start = new Date(start.getFullYear(), 0, 1);
start = new Date(today);
start.setMonth(0);
start.setDate(1);
start = startOfDay(start);
end = new Date(end.getFullYear(), 11, 31);
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
end = new Date(today);
end.setMonth(11);
end.setDate(31);
end = endOfDay(end);
break;
}
// console.log('Range is ' + viewRange);
@ -206,6 +209,7 @@ const mutations = {
},
setViewRange(state, range) {
state.viewRange = range;
window.localStorage.setItem('viewRange', range);
}
}

View File

@ -46,6 +46,9 @@ const getters = {
transactions: state => {
return state.transactions;
},
defaultErrors: state => {
return state.defaultErrors;
},
groupTitle: state => {
return state.groupTitle;
},

View File

@ -22,7 +22,7 @@
<div>
<alert :message="errorMessage" type="danger"/>
<alert :message="successMessage" type="success"/>
<form @submit="submitTransaction">
<form @submit="submitTransaction" autocomplete="off">
<SplitPills :transactions="transactions"/>
<div class="tab-content">
<!-- v-on:switch-accounts="switchAccounts($event)" -->
@ -40,6 +40,7 @@
:transaction="transaction"
:transaction-type="transactionType"
v-on:uploaded-attachments="uploadedAttachment($event)"
v-on:selected-attachments="selectedAttachment($event)"
v-on:set-marker-location="storeLocation($event)"
v-on:set-account="storeAccountValue($event)"
v-on:set-date="storeDate($event)"
@ -118,6 +119,7 @@ import SplitPills from "./SplitPills";
import TransactionGroupTitle from "./TransactionGroupTitle";
import SplitForm from "./SplitForm";
import {mapGetters, mapMutations} from "vuex";
import {getDefaultErrors} from "../../shared/transactions";
export default {
@ -138,8 +140,9 @@ export default {
let type = parts[parts.length - 1];
// set a basic date-time string:
let date = new Date;
this.date = [date.getFullYear(), ('0' + (date.getMonth() + 1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-') + 'T00:00';
this.date = (new Date).toISOString().split('T')[0] + 'T00:00';
//console.log('Date is set to "' + this.date + '"');
this.setTransactionType(type[0].toUpperCase() + type.substring(1));
@ -165,7 +168,7 @@ export default {
// things the process is done working on (3 phases):
submittedTransaction: false,
submittedLinks: false,
submittedAttachments: false,
submittedAttachments: -1, // -1 (no attachments), 0 = uploading, 1 = uploaded
// transaction was actually submitted?
inError: false,
@ -196,18 +199,12 @@ export default {
/**
* Grabbed from the store.
*/
...mapGetters('transactions/create', ['transactionType', 'transactions', 'groupTitle']),
...mapGetters('transactions/create', ['transactionType', 'transactions', 'groupTitle','defaultErrors']),
...mapGetters('root', ['listPageSize'])
},
watch: {
submittedTransaction: function () {
this.finalizeSubmit();
},
submittedLinks: function () {
this.finalizeSubmit();
},
submittedAttachments: function () {
this.finalizeSubmit();
this.finaliseSubmission();
}
},
methods: {
@ -237,58 +234,152 @@ export default {
// console.log('Triggered to remove transaction ' + payload.index);
this.$store.commit('transactions/create/deleteTransaction', payload);
},
/**
* Submitting a transaction consists of 3 steps: submitting the transaction, uploading attachments
* and creating links. Only once all three steps are executed may the message be shown or the user be
* forwarded.
*/
finalizeSubmit() {
// console.log('finalizeSubmit (' + this.submittedTransaction + ', ' + this.submittedAttachments + ', ' + this.submittedLinks + ')');
if (this.submittedTransaction && this.submittedAttachments && this.submittedLinks) {
// console.log('all true');
// console.log('createAnother = ' + this.createAnother);
// console.log('inError = ' + this.inError);
if (false === this.createAnother && false === this.inError) {
// console.log('redirect');
window.location.href = (window.previousURL ?? '/') + '?transaction_group_id=' + this.returnedGroupId + '&message=created';
return;
submitData: function (url, data) {
return axios.post(url, data);
},
handleSubmissionResponse: function (response) {
//console.log('In handleSubmissionResponse()');
// save some meta data:
this.returnedGroupId = parseInt(response.data.data.id);
this.returnedGroupTitle = null === response.data.data.attributes.group_title ? response.data.data.attributes.transactions[0].description : response.data.data.attributes.group_title;
let journals = [];
// save separate journal ID's (useful ahead in the process):
let result = response.data.data.attributes.transactions
for (let i in result) {
if (result.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
journals.push(parseInt(result[i].transaction_journal_id));
}
}
if (false === this.inError) {
// show message:
this.errorMessage = '';
this.successMessage = this.$t('firefly.transaction_stored_link', {ID: this.returnedGroupId, title: this.returnedGroupTitle});
}
// enable flags:
this.enableSubmit = true;
this.submittedTransaction = false;
this.submittedLinks = false;
this.submittedAttachments = false;
this.inError = false;
// reset attachments (always do this)
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (this.transactions.hasOwnProperty(i)) {
// console.log('Reset attachment #' + i);
this.updateField({index: i, field: 'transaction_journal_id', value: 0});
return new Promise((resolve) => {
resolve(
{
journals: journals,
}
);
});
},
submitLinks: function (response, submission) {
let promises = [];
// for
for (let i in response.journals) {
if (response.journals.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let journalId = response.journals[i];
let links = submission.transactions[i].links;
for (let ii in links) {
if (links.hasOwnProperty(ii) && /^0$|^[1-9]\d*$/.test(ii) && ii <= 4294967294) {
let currentLink = links[ii];
if (0 === currentLink.outward_id) {
currentLink.outward_id = journalId;
}
if (0 === currentLink.inward_id) {
currentLink.inward_id = journalId;
}
promises.push(axios.post('./api/v1/transaction_links', currentLink));
}
}
}
this.submittedAttCount = [];
// reset the form:
if (this.resetFormAfter) {
this.resetTransactions();
// do a short time out?
setTimeout(() => this.addTransaction(), 50);
}
// console.log('Done with finalizeSubmit!');
// return;
}
// console.log('Did nothing in finalizeSubmit');
if (0 === promises.length) {
return new Promise((resolve) => {
resolve(
{
response: 'from submitLinks'
}
);
});
}
return Promise.all(promises);
},
submitAttachments: function (response, submission) {
let anyAttachments = false;
for (let i in response.journals) {
if (response.journals.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let journalId = response.journals[i];
let hasAttachments = submission.transactions[i].attachments;
// console.log('Decided that ' + journalId);
// console.log(hasAttachments);
if (hasAttachments) {
// console.log('upload!');
this.updateField({index: i, field: 'transaction_journal_id', value: journalId});
anyAttachments = true;
}
}
}
if (true === anyAttachments) {
this.submittedAttachments = 0;
}
return new Promise((resolve) => {
resolve(
{
response: 'from submitAttachments'
}
);
});
},
selectedAttachment: function (payload) {
this.updateField({index: payload.index, field: 'attachments', value: true});
},
finaliseSubmission: function () {
if (0 === this.submittedAttachments) {
// console.log('submittedAttachments = ' + this.submittedAttachments);
return;
}
//console.log('In finaliseSubmission');
if (false === this.createAnother) {
window.location.href = (window.previousURL ?? '/') + '?transaction_group_id=' + this.returnedGroupId + '&message=created';
return;
}
//console.log('Is in error?');
//console.log(this.inError);
if (false === this.inError) {
// show message:
this.errorMessage = '';
this.successMessage = this.$t('firefly.transaction_stored_link', {ID: this.returnedGroupId, title: this.returnedGroupTitle});
}
// enable flags:
this.enableSubmit = true;
this.submittedTransaction = false;
this.submittedAttachments = -1;
// reset attachments + errors
if (!this.resetFormAfter) {
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (this.transactions.hasOwnProperty(i)) {
//this.
// console.log('Reset attachment #' + i);
this.updateField({index: i, field: 'transaction_journal_id', value: 0});
this.updateField({index: i, field: 'errors', value: this.defaultErrors})
}
}
}
}
// reset the form:
if (this.resetFormAfter) {
this.resetTransactions();
this.addTransaction();
}
return new Promise((resolve) => {
resolve(
{
response: 'from finaliseSubmission'
}
);
});
},
handleSubmissionError: function (error) {
//console.log('in handleSubmissionError');
// oh noes Firefly III has something to bitch about.
this.enableSubmit = true;
// but report an error because error:
this.inError = true;
this.parseErrors(error.response.data);
},
/**
* Actually submit the transaction to Firefly III. This is a fairly complex beast of a thing because multiple things
@ -300,64 +391,26 @@ export default {
// disable the submit button:
this.enableSubmit = false;
// assume nothing breaks
this.inError = false;
// remove old warnings etc.
this.successMessage = '';
this.errorMessage = '';
// convert the data so its ready to be submitted:
const url = './api/v1/transactions';
const data = this.convertData();
// console.log('Will submit:');
// console.log(data);
// POST the transaction.
axios.post(url, data)
this.submitData(url, data)
.then(this.handleSubmissionResponse)
.then(response => {
// console.log('Response is OK!');
// report the transaction is submitted.
this.submittedTransaction = true;
return Promise.all([this.submitLinks(response, data), this.submitAttachments(response, data)]);
}
)
.then(this.finaliseSubmission)
.catch(this.handleSubmissionError);
// submit links and attachments (can only be done when the transaction is created)
this.submitTransactionLinks(data, response);
this.submitAttachments(data, response);
// meanwhile, store the ID and the title in some easy to access variables.
this.returnedGroupId = parseInt(response.data.data.id);
this.returnedGroupTitle = null === response.data.data.attributes.group_title ? response.data.data.attributes.transactions[0].description : response.data.data.attributes.group_title;
// console.log('Group title is now "' + this.groupTitle + '"');
})
.catch(error => {
// oh noes Firefly III has something to bitch about.
this.enableSubmit = true;
// console.log('enable submit = true');
// report the transaction is submitted.
this.submittedTransaction = true;
// also report attachments and links are submitted:
this.submittedAttachments = true;
this.submittedLinks = true;
// but report an error because error:
this.inError = true;
this.parseErrors(error.response.data);
});
},
/**
* Submitting transactions means we will give each TransactionAttachment component
* the ID of the transaction journal (so it works for multiple splits). Each component
* will then start uploading their transactions (so its a separated concern) and report
* back to the "uploadedAttachment" function below via an event emitter.
*
* The ID is set via the store.
*/
submitAttachments: function (data, response) {
// console.log('submitAttachments()');
let result = response.data.data.attributes.transactions
for (let i in data.transactions) {
if (data.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (result.hasOwnProperty(i)) {
// console.log('updateField(' + i + ', transaction_journal_id, ' + result[i].transaction_journal_id + ')');
this.updateField({index: i, field: 'transaction_journal_id', value: result[i].transaction_journal_id});
}
}
}
},
/**
* When a attachment component is done uploading it ends up here. We create an object where we count how many
@ -367,6 +420,7 @@ export default {
* Once the number of components matches the number of splits we know all attachments have been uploaded.
*/
uploadedAttachment: function (journalId) {
this.submittedAttachments = 0;
// console.log('Triggered uploadedAttachment(' + journalId + ')');
let key = 'str' + journalId;
this.submittedAttCount[key] = 1;
@ -374,9 +428,9 @@ export default {
// console.log('Count is now ' + count);
// console.log('Length is ' + this.transactions.length);
if (count === this.transactions.length) {
// mark the attachments as stored:
this.submittedAttachments = true;
// console.log('Got them all!');
// mark the attachments as stored:
this.submittedAttachments = 1;
}
},
/**
@ -457,11 +511,13 @@ export default {
this.submittedLinks = true;
});
},
parseErrors: function (errors) {
for (let i in this.transactions) {
this.resetErrors({index: i});
if (this.transactions.hasOwnProperty(i)) {
this.resetErrors({index: i});
}
}
this.successMessage = '';
this.errorMessage = this.$t('firefly.errors_submission');
if (typeof errors.errors === 'undefined') {
@ -470,12 +526,6 @@ export default {
}
let payload;
//payload = {index: 0, field: 'description', errors: ['Test error index 0']};
//this.setTransactionError(payload);
//payload = {index: 1, field: 'description', errors: ['Test error index 1']};
//this.setTransactionError(payload);
let transactionIndex;
let fieldName;
@ -558,6 +608,7 @@ export default {
//console.log('Group title is: "' + this.groupTitle + '"');
if (this.groupTitle.length > 0) {
data.group_title = this.groupTitle;
//console.log('1) data.group_title is now "'+data.group_title+'"');
}
for (let i in this.transactions) {
@ -565,8 +616,9 @@ export default {
data.transactions.push(this.convertSplit(i, this.transactions[i]));
}
}
if (data.transactions.length > 1 && '' !== data.transactions[0].description) {
if (data.transactions.length > 1 && '' !== data.transactions[0].description && (null === data.group_title || '' === data.group_title)) {
data.group_title = data.transactions[0].description;
//console.log('2) data.group_title is now "'+data.group_title+'"');
}
// depending on the transaction type for this thing, we need to
@ -616,27 +668,6 @@ export default {
},
// switchAccounts: function (index) {
// // console.log('user wants to switch Accounts');
// let origSourceId = this.transactions[index].source_account_id;
// let origSourceName = this.transactions[index].source_account_name;
// let origSourceType = this.transactions[index].source_account_type;
//
// let origDestId = this.transactions[index].destination_account_id;
// let origDestName = this.transactions[index].destination_account_name;
// let origDestType = this.transactions[index].destination_account_type;
//
// this.updateField({index: 0, field: 'source_account_id', value: origDestId});
// this.updateField({index: 0, field: 'source_account_name', value: origDestName});
// this.updateField({index: 0, field: 'source_account_type', value: origDestType});
//
// this.updateField({index: 0, field: 'destination_account_id', value: origSourceId});
// this.updateField({index: 0, field: 'destination_account_name', value: origSourceName});
// this.updateField({index: 0, field: 'destination_account_type', value: origSourceType});
// this.calculateTransactionType(0);
// },
/**
*
* @param key
@ -700,6 +731,7 @@ export default {
// from thing:
order: 0,
reconciled: false,
attachments: array.attachments,
};
if (0 !== array.tags.length) {
@ -804,8 +836,8 @@ export default {
getAllowedOpposingTypes: function () {
axios.get('./api/v1/configuration/firefly.allowed_opposing_types')
.then(response => {
console.log('opposing types things.');
console.log(response.data.data.value);
// console.log('opposing types things.');
// console.log(response.data.data.value);
this.allowedOpposingTypes = response.data.data.value;
});
},

View File

@ -23,7 +23,8 @@
<Alert :message="errorMessage" type="danger"/>
<Alert :message="successMessage" type="success"/>
<Alert :message="warningMessage" type="warning"/>
<form @submit="submitTransaction">
<form @submit="submitTransaction" autocomplete="off">
<SplitPills :transactions="transactions"/>
<div class="tab-content">
@ -40,7 +41,6 @@
:destination-allowed-types="destinationAllowedTypes"
:source-allowed-types="sourceAllowedTypes"
:allow-switch="false"
:submitted-transaction="submittedTransaction"
v-on:uploaded-attachments="uploadedAttachment($event)"
v-on:set-marker-location="storeLocation($event)"
v-on:set-account="storeAccountValue($event)"
@ -74,7 +74,8 @@
<div class="text-xs d-none d-lg-block d-xl-block">
&nbsp;
</div>
<button type="button" class="btn btn-outline-primary btn-block" @click="addTransaction"><i class="far fa-clone"></i> {{ $t('firefly.add_another_split') }}
<button type="button" class="btn btn-outline-primary btn-block" @click="addTransaction"><i class="far fa-clone"></i>
{{ $t('firefly.add_another_split') }}
</button>
</div>
<div class="col">
@ -109,12 +110,14 @@
</template>
<script>
const lodashClonedeep = require('lodash.clonedeep');
import {mapMutations} from "vuex";
import Alert from '../partials/Alert';
import SplitPills from "./SplitPills";
import SplitForm from "./SplitForm";
import TransactionGroupTitle from "./TransactionGroupTitle";
import {getDefaultErrors, getDefaultTransaction, toW3CString} from '../../shared/transactions';
import {getDefaultErrors, getDefaultTransaction} from '../../shared/transactions';
const lodashClonedeep = require('lodash.clonedeep');
export default {
name: "Edit",
@ -156,10 +159,15 @@ export default {
// things the process is done working on (3 phases):
submittedTransaction: false,
submittedLinks: false,
submittedAttachments: false,
// submittedLinks: false,
submittedAttachments: -1, // -1 = no attachments, 0 = uploading, 1 = uploaded
inError: false,
// number of uploaded attachments
// its an object because we count per transaction journal (which can have multiple attachments)
// and array doesn't work right.
submittedAttCount: {},
// meta data for accounts
allowedOpposingTypes: {},
destinationAllowedTypes: [],
@ -179,21 +187,13 @@ export default {
},
watch: {
submittedTransaction: function () {
// see finalizeSubmit()
this.finalizeSubmit();
},
submittedLinks: function () {
// see finalizeSubmit()
this.finalizeSubmit();
},
submittedAttachments: function () {
// see finalizeSubmit()
this.finalizeSubmit();
this.finaliseSubmission();
}
},
methods: {
...mapMutations('transactions/create', ['updateField',]),
/**
* Grap transaction group from URL and submit GET.
*/
@ -203,8 +203,8 @@ export default {
this.parseTransactionGroup(response.data);
}
).catch(error => {
// console.log('I failed :(');
// console.log(error);
console.log('I failed :(');
console.log(error);
});
},
/**
@ -219,6 +219,10 @@ export default {
this.groupTitle = attributes.group_title;
this.originalGroupTitle = attributes.group_title;
//this.returnedGroupId = parseInt(response.data.id);
this.returnedGroupTitle = null === this.originalGroupTitle ? response.data.attributes.transactions[0].description : this.originalGroupTitle;
for (let i in transactions) {
if (transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let result = this.parseTransaction(parseInt(i), transactions[i]);
@ -239,6 +243,8 @@ export default {
//console.log('index: ' + index);
if (0 === index) {
this.transactionType = array.type.charAt(0).toUpperCase() + array.type.slice(1);
// TODO here you may need to catch stuff like loan/debt/mortgage
this.sourceAllowedTypes = [array.source_type];
this.destinationAllowedTypes = [array.destination_type];
this.date = array.date.substring(0, 16);
@ -292,7 +298,6 @@ export default {
result.zoom_level = array.zoom_level;
result.longitude = array.longitude;
result.latitude = array.latitude;
// error handling
result.errors = getDefaultErrors();
return result;
@ -370,14 +375,12 @@ export default {
});
},
/**
* TODO same method as Create
* Get API value.
*/
getAllowedOpposingTypes: function () {
axios.get('./api/v1/configuration/firefly.allowed_opposing_types')
.then(response => {
this.allowedOpposingTypes = response.data.data.value;
// console.log('Set allowedOpposingTypes');
});
},
/**
@ -389,8 +392,20 @@ export default {
});
},
uploadedAttachment: function (payload) {
console.log('event: uploadedAttachment');
console.log(payload);
//console.log('event: uploadedAttachment');
//console.log(payload);
this.submittedAttachments = 0;
// console.log('Triggered uploadedAttachment(' + journalId + ')');
let key = 'str' + payload;
this.submittedAttCount[key] = 1;
let count = Object.keys(this.submittedAttCount).length;
//console.log('Count is now ' + count);
//console.log('Length is ' + this.transactions.length);
if (count === this.transactions.length) {
//console.log('Got them all!');
// mark the attachments as stored:
this.submittedAttachments = 1;
}
},
storeLocation: function (payload) {
this.transactions[payload.index].zoom_level = payload.zoomLevel;
@ -405,26 +420,25 @@ export default {
this.transactions[index][direction + '_account_name'] = payload.name;
},
storeDate: function (payload) {
// console.log('event: storeDate');
// console.log(payload);
this.date = payload.date;
},
storeTime: function (payload) {
this.time = payload.time;
// console.log('event: storeTime');
// console.log(payload);
},
storeField: function (payload) {
let field = payload.field;
if ('category' === field) {
field = 'category_name';
}
// console.log('event: storeField(' + field + ')');
this.transactions[payload.index][field] = payload.value;
},
removeTransaction: function (payload) {
//console.log('removeTransaction()');
//console.log(payload);
//console.log('length: ' + this.transactions.length);
this.transactions.splice(payload.index, 1);
//console.log('length: ' + this.transactions.length);
//this.originalTransactions.splice(payload.index, 1);
// this kills the original transactions.
this.originalTransactions = [];
},
@ -432,9 +446,12 @@ export default {
this.groupTitle = payload;
},
selectedAttachments: function (payload) {
//console.log('Now in selectedAttachments()');
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (parseInt(this.transactions[i].transaction_journal_id) === parseInt(payload)) {
// console.log('Payload is');
// console.log(payload);
if (parseInt(this.transactions[i].transaction_journal_id) === parseInt(payload.id)) {
// console.log('selectedAttachments ' + payload);
this.transactions[i].selectedAttachments = true;
}
@ -450,56 +467,55 @@ export default {
submitTransaction: function (event) {
event.preventDefault();
let submission = {transactions: []};
// parse data to see if we should submit anything at all:
let shouldSubmit = false;
let shouldLinks = false;
let shouldUpload = false;
// if the group title has changed, should submit:
if (this.groupTitle !== this.originalGroupTitle) {
submission.group_title = this.groupTitle;
shouldSubmit = true;
}
let transactionCount = this.originalTransactions.length;
let newTransactionCount = this.transactions.length;
console.log('Found ' + this.transactions.length + ' split(s).');
// if something with the group title:
let newTransactionCount = this.transactions.length;
if (newTransactionCount > 1 && typeof submission.group_title === 'undefined' && (null === this.originalGroupTitle || '' === this.originalGroupTitle)) {
submission.group_title = this.transactions[0].description;
shouldSubmit = true;
}
// loop each transaction (edited by the user):
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// original transaction present?
// original transaction present:
let currentTransaction = this.transactions[i];
let originalTransaction = this.originalTransactions.hasOwnProperty(i) ? this.originalTransactions[i] : {};
// the difference:
let diff = {};
// compare basic fields:
let basicFields = [
'description',
'source_account_id', 'source_account_name',
'destination_account_id', 'destination_account_name',
'amount', 'foreign_amount', 'foreign_currency_id',
'category_name', 'budget_id', 'bill_id',
'interest_date', 'book_date', 'due_date', 'payment_date', 'invoice_date',
'external_url', 'internal_reference', 'external_id', 'notes',
'zoom_level', 'longitude', 'latitude'
];
let basicFields = ['description', 'source_account_id', 'source_account_name', 'destination_account_id', 'destination_account_name', 'amount', 'foreign_amount', 'foreign_currency_id', 'category_name', 'budget_id', 'bill_id', 'interest_date', 'book_date', 'due_date', 'payment_date', 'invoice_date', 'external_url', 'internal_reference', 'external_id', 'notes', 'zoom_level', 'longitude', 'latitude'];
// source and destination may be overruled:
// source and destination are overruled in some cases:
if (i > 0) {
diff.type = this.transactionType.toLowerCase();
if ('Deposit' === this.transactionType || 'Transfer' === this.transactionType) {
if ('deposit' === this.transactionType.toLowerCase() || 'transfer' === this.transactionType.toLowerCase()) {
// set destination to be whatever is in transaction zero:
currentTransaction.destination_account_name = this.originalTransactions[0].destination_account_name;
currentTransaction.destination_account_id = this.originalTransactions[0].destination_account_id;
}
if ('Withdrawal' === this.transactionType || 'Transfer' === this.transactionType) {
if ('withdrawal' === this.transactionType.toLowerCase() || 'transfer' === this.transactionType.toLowerCase()) {
// set source to be whatever is in transaction zero:
currentTransaction.source_account_name = this.originalTransactions[0].source_account_name;
currentTransaction.source_account_id = this.originalTransactions[0].source_account_id;
}
console.log('Will overrule accounts for split ' + i);
}
// loop the basic fields and verify
for (let ii in basicFields) {
if (basicFields.hasOwnProperty(ii) && /^0$|^[1-9]\d*$/.test(ii) && ii <= 4294967294) {
let fieldName = basicFields[ii];
@ -519,13 +535,7 @@ export default {
continue;
}
// console.log('Index ' + i + ': Field ' + fieldName + ' updated ("' + originalTransaction[fieldName] + '" > "' + currentTransaction[fieldName] + '")');
// console.log(originalTransaction[fieldName]);
// console.log(currentTransaction[fieldName]);
// some field names may need to be different. little basic but it works:
// console.log('pre: ' + submissionFieldName);
if ('source_account_id' === submissionFieldName) {
submissionFieldName = 'source_id';
}
@ -539,26 +549,19 @@ export default {
submissionFieldName = 'destination_name';
}
// otherwise save them and remember them for submission:
diff[submissionFieldName] = currentTransaction[fieldName];
shouldSubmit = true;
}
}
}
if (0 !== currentTransaction.piggy_bank_id) {
diff.piggy_bank_id = currentTransaction.piggy_bank_id;
shouldSubmit = true;
}
if (JSON.stringify(currentTransaction.tags) !== JSON.stringify(originalTransaction.tags)) {
// console.log('tags are different');
// console.log(currentTransaction.tags);
// console.log(originalTransaction.tags);
diff.tags = [];//currentTransaction.tags;
// tags different?
if (JSON.stringify(currentTransaction.tags) !== JSON.stringify(originalTransaction.tags)) {
diff.tags = [];
if (0 !== currentTransaction.tags.length) {
for (let ii in currentTransaction.tags) {
if (currentTransaction.tags.hasOwnProperty(ii) && /^0$|^[1-9]\d*$/.test(ii) && ii <= 4294967294) {
// array.tags
let currentTag = currentTransaction.tags[ii];
if (typeof currentTag === 'object' && null !== currentTag) {
diff.tags.push(currentTag.text);
@ -569,79 +572,258 @@ export default {
}
}
}
shouldSubmit = true;
}
// compare links:
let newLinks = this.compareLinks(currentTransaction.links);
let originalLinks = this.compareLinks(originalTransaction.links);
// console.log('links are?');
// console.log(newLinks);
// console.log(originalLinks);
if (newLinks !== originalLinks) {
// console.log('links are different!');
// console.log(newLinks);
// console.log(originalLinks);
shouldLinks = true;
}
// this.transactions[i].selectedAttachments
// console.log(typeof currentTransaction.selectedAttachments);
// console.log(currentTransaction.selectedAttachments);
if (typeof currentTransaction.selectedAttachments !== 'undefined' && true === currentTransaction.selectedAttachments) {
// must upload!
shouldUpload = true;
}
if (
this.date !== this.originalDate
) {
console.log('Date and/or time is changed');
// set date and time!
if (this.date !== this.originalDate) {
shouldSubmit = true;
diff.date = this.date;
}
console.log('Now at index ' + i);
console.log(Object.keys(diff).length);
if (Object.keys(diff).length === 0 && newTransactionCount > 1) {
console.log('Will submit just the ID!');
// Will submit just the ID!
diff.transaction_journal_id = originalTransaction.transaction_journal_id;
submission.transactions.push(lodashClonedeep(diff));
shouldSubmit = true;
} else if (Object.keys(diff).length !== 0) {
// will submit all:
diff.transaction_journal_id = originalTransaction.transaction_journal_id ?? 0;
submission.transactions.push(lodashClonedeep(diff));
shouldSubmit = true;
}
}
}
this.submitUpdate(submission, shouldSubmit, shouldLinks, shouldUpload);
},
console.log('submitTransaction');
console.log('shouldUpload : ' + shouldUpload);
console.log('shouldLinks : ' + shouldLinks);
console.log('shouldSubmit : ' + shouldSubmit);
if (shouldSubmit) {
this.submitUpdate(submission, shouldLinks, shouldUpload);
}
submitData: function (shouldSubmit, submission) {
//console.log('submitData');
if (!shouldSubmit) {
this.submittedTransaction = true;
//console.log('No need!');
return new Promise((resolve) => {
resolve({});
});
}
if (!shouldLinks) {
this.submittedLinks = true;
const url = './api/v1/transactions/' + this.groupId;
return axios.put(url, submission);
},
handleSubmissionResponse: function (response) {
//console.log('handleSubmissionResponse()');
// report the transaction is submitted.
this.submittedTransaction = true;
let journals = [];
// meanwhile, store the ID and the title in some easy to access variables.
if (typeof response.data !== 'undefined') {
this.returnedGroupId = parseInt(response.data.data.id) ?? null;
this.returnedGroupTitle = null === response.data.data.attributes.group_title ? response.data.data.attributes.transactions[0].description : response.data.data.attributes.group_title;
let result = response.data.data.attributes.transactions
for (let i in result) {
if (result.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
journals.push(parseInt(result[i].transaction_journal_id));
}
}
} else {
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
journals.push(this.transactions[i].transaction_journal_id);
}
}
}
if (!shouldUpload) {
this.submittedAttachments = true;
journals = journals.reverse();
return new Promise((resolve) => {
resolve(
{
journals: journals,
}
);
});
},
submitLinks: function (shouldSubmit) {
//console.log('submitLinks()');
if (!shouldSubmit) {
//console.log('no need!');
return new Promise((resolve) => {
resolve({});
});
}
if (!shouldSubmit && shouldLinks) {
this.submitTransactionLinks();
return this.deleteAllOriginalLinks().then(() => this.submitNewLinks());
},
submitAttachments: function (shouldSubmit, response) {
//console.log('submitAttachments');
if (!shouldSubmit) {
//console.log('no need!');
return new Promise((resolve) => {
resolve({});
});
}
//console.log('Do upload thing!');
//console.log(response);
let anyAttachments = false;
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let transaction = this.transactions[i];
let journalId = transaction.transaction_journal_id;
//console.log(journalId);
if (typeof response !== 'undefined') {
journalId = response.journals[i]
}
let hasAttachments = transaction.selectedAttachments;
this.transactions[i].transaction_journal_id = journalId;
this.transactions[i].uploadTrigger = true;
//console.log('Decided that ' + journalId);
//console.log('upload index ' + i);
//console.log(hasAttachments);
if (hasAttachments) {
anyAttachments = true;
}
}
}
if (true === anyAttachments) {
this.submittedAttachments = 0;
}
},
finaliseSubmission: function () {
//console.log('finaliseSubmission');
if (0 === this.submittedAttachments) {
return;
}
//console.log('continue (' + this.submittedAttachments + ')');
if (true === this.stayHere && false === this.inError) {
//console.log('no error + no changes + no redirect');
// show message:
this.errorMessage = '';
this.warningMessage = '';
this.successMessage = this.$t('firefly.transaction_updated_link', {ID: this.groupId, title: this.returnedGroupTitle});
}
// no error + changes + redirect
if (false === this.stayHere && false === this.inError) {
//console.log('no error + changes + redirect');
window.location.href = (window.previousURL ?? '/') + '?transaction_group_id=' + this.groupId + '&message=updated';
}
if (!shouldSubmit && shouldLinks) {
// TODO
//this.submittedAttachments();
this.enableSubmit = true;
this.submittedAttachments = -1;
this.inError = false;
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (this.transactions.hasOwnProperty(i)) {
this.transactions[i].clearTrigger = true;
}
}
}
console.log('Done with submit methd.');
//console.log(submission);
},
submitUpdate: function (submission, shouldSubmit, shouldLinks, shouldUpload) {
//console.log('submitUpdate()');
this.inError = false;
this.submitData(shouldSubmit, submission)
.then(this.handleSubmissionResponse) // error or OK
.then(response => {
return Promise.all([
this.submitLinks(shouldLinks, response),
this.submitAttachments(shouldUpload, response)]);
}
)
.then(this.finaliseSubmission)
.catch(this.handleSubmissionError);
// if (shouldLinks) {
// promises.push(this.submitTransactionLinks())
// }
// if (!shouldLinks) {
// promises.push(new Promise((resolve) => {
// resolve({});
// }));
// }
// if (shouldUpload) {
// console.log('Attachments = Respond to promise from shouldSubmit/!shouldSubmit');
// promises.push(submissionPromise.then(result => this.uploadAttachments(result)));
// }
// if (!shouldUpload) {
// promises.push(new Promise((resolve) => {
// resolve({});
// }));
// }
// all promises fulfilled:
// console.log('All promises done, process results?');
// Promise.all(promises).then(function (responses) {
// console.log('I believe all ' + promises.length + ' promises fulfilled!');
// }).catch(function (errors) {
// console.log('Somebody errored?');
// });
//
//
//
// if (shouldSubmit) {
// console.log('shouldSubmit');
// // do submission:
// // console.log(JSON.stringify(submission));
// // console.log(submission);
// axios.put(url, submission)
// .then(response => {
// // console.log('Response is OK!');
// // report the transaction is submitted.
// this.submittedTransaction = true;
//
// // submit links and attachments (can only be done when the transaction is created)
// if (shouldLinks) {
// console.log('Submit links using return from server:');
// this.submitTransactionLinks();
// }
// if (shouldUpload) {
// // console.log('Need to upload.');
// this.submitAttachments(response.data.data.attributes.transactions);
// }
// // meanwhile, store the ID and the title in some easy to access variables.
// this.returnedGroupId = parseInt(response.data.data.id);
// this.returnedGroupTitle = null === response.data.data.attributes.group_title ? response.data.data.attributes.transactions[0].description : response.data.data.attributes.group_title;
// }
// )
// .catch(error => {
// console.log('error :(');
// console.log(error.response.data);
// // oh noes Firefly III has something to bitch about.
// this.enableSubmit = true;
// // report the transaction is submitted.
// this.submittedTransaction = true;
// // // also report attachments and links are submitted:
// this.submittedAttachments = true;
// this.submittedLinks = true;
// //
// // but report an error because error:
// this.inError = true;
// this.parseErrors(error.response.data);
// }
// );
// }
// if (!shouldSubmit && shouldLinks) {
// // update links
// console.log('Submit links using whatever is here:');
// this.submitTransactionLinks();
// }
// if (!shouldSubmit && shouldUpload) {
// // upload
// // console.log('Need to upload.');
// this.submitAttachments(this.transactions);
// }
},
compareLinks: function (array) {
let compare = [];
@ -659,55 +841,26 @@ export default {
);
}
}
// console.log('compareLinks');
// console.log(compare);
return JSON.stringify(compare);
},
submitUpdate: function (submission, shouldLinks, shouldUpload) {
console.log('submitUpdate');
this.inError = false;
const url = './api/v1/transactions/' + this.groupId;
console.log(JSON.stringify(submission));
console.log(submission);
axios.put(url, submission)
.then(response => {
console.log('Response is OK!');
// report the transaction is submitted.
this.submittedTransaction = true;
uploadAttachments: function (result) {
//console.log('TODO, upload attachments.');
if (0 === Object.keys(result).length) {
// submit links and attachments (can only be done when the transaction is created)
if (shouldLinks) {
console.log('Need to update links.');
this.submitTransactionLinks();
}
if (!shouldLinks) {
console.log('No need to update links.');
}
// TODO attachments:
// this.submitAttachments(data, response);
//
// // meanwhile, store the ID and the title in some easy to access variables.
this.returnedGroupId = parseInt(response.data.data.id);
this.returnedGroupTitle = null === response.data.data.attributes.group_title ? response.data.data.attributes.transactions[0].description : response.data.data.attributes.group_title;
}
)
.catch(error => {
console.log('error :(');
console.log(error.response.data);
// oh noes Firefly III has something to bitch about.
this.enableSubmit = true;
// report the transaction is submitted.
this.submittedTransaction = true;
// // also report attachments and links are submitted:
this.submittedAttachments = true;
this.submittedLinks = true;
//
// but report an error because error:
this.inError = true;
this.parseErrors(error.response.data);
}
);
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
//console.log('updateField(' + i + ', transaction_journal_id, ' + result[i].transaction_journal_id + ')');
this.updateField({index: i, field: 'transaction_journal_id', value: result[i].transaction_journal_id});
}
}
//console.log('Transactions not changed, use original objects.');
} else {
//console.log('Transactions changed!');
}
this.submittedAttachments = true;
},
parseErrors: function (errors) {
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
@ -800,97 +953,92 @@ export default {
},
deleteOriginalLinks: function (transaction) {
console.log(transaction.links);
let promises = [];
for (let i in transaction.links) {
if (transaction.links.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let current = transaction.links[i];
let url = '/api/v1/transaction_links/' + current.id;
axios.delete(url).then(response => {
// TODO response
});
promises.push(axios.delete(url));
}
}
return Promise.all(promises);
},
/**
* Submit transaction links.
* TODO same method as CREATE
*/
submitTransactionLinks() {
let total = 0;
deleteAllOriginalLinks: function () {
//console.log('deleteAllOriginalLinks()');
// loop to delete old transaction links.
let promises = [];
console.log('submitTransactionLinks()');
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// original transaction present?
let currentTransaction = this.transactions[i];
let originalTransaction = this.originalTransactions.hasOwnProperty(i) ? this.originalTransactions[i] : {};
// compare links:
let newLinks = this.compareLinks(currentTransaction.links);
let originalLinks = this.compareLinks(originalTransaction.links);
if (newLinks !== originalLinks) {
if ('[]' !== originalLinks) {
this.deleteOriginalLinks(originalTransaction);
promises.push(this.deleteOriginalLinks(originalTransaction));
}
console.log('links are different!');
// console.log(newLinks);
// console.log(originalLinks);
for (let ii in currentTransaction.links) {
if (currentTransaction.links.hasOwnProperty(ii) && /^0$|^[1-9]\d*$/.test(ii) && ii <= 4294967294) {
let currentLink = currentTransaction.links[ii];
let linkObject = {
inward_id: currentTransaction.transaction_journal_id,
outward_id: currentTransaction.transaction_journal_id,
link_type_id: 'something'
};
let parts = currentLink.link_type_id.split('-');
linkObject.link_type_id = parts[0];
if ('inward' === parts[1]) {
linkObject.inward_id = currentLink.transaction_journal_id;
}
if ('outward' === parts[1]) {
linkObject.outward_id = currentLink.transaction_journal_id;
}
console.log(linkObject);
total++;
// submit transaction link:
promises.push(axios.post('./api/v1/transaction_links', linkObject).then(response => {
// TODO error handling.
}));
}
}
// shouldLinks = true;
} else {
promises.push(new Promise((resolve) => {
resolve({});
}));
}
}
}
if (0 === total) {
this.submittedLinks = true;
return;
}
Promise.all(promises).then(function () {
this.submittedLinks = true;
});
return Promise.all(promises);
},
finalizeSubmit: function () {
console.log('now in finalizeSubmit()');
console.log('submittedTransaction : ' + this.submittedTransaction);
console.log('submittedLinks : ' + this.submittedLinks);
console.log('submittedAttachments : ' + this.submittedAttachments);
submitNewLinks: function () {
//console.log('submitNewLinks()');
let promises = [];
for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let currentTransaction = this.transactions[i];
for (let ii in currentTransaction.links) {
if (currentTransaction.links.hasOwnProperty(ii) && /^0$|^[1-9]\d*$/.test(ii) && ii <= 4294967294) {
let currentLink = currentTransaction.links[ii];
let linkObject = {
inward_id: currentTransaction.transaction_journal_id,
outward_id: currentTransaction.transaction_journal_id,
link_type_id: 'something'
};
let parts = currentLink.link_type_id.split('-');
linkObject.link_type_id = parts[0];
if ('inward' === parts[1]) {
linkObject.inward_id = currentLink.transaction_journal_id;
}
if ('outward' === parts[1]) {
linkObject.outward_id = currentLink.transaction_journal_id;
}
promises.push(axios.post('./api/v1/transaction_links', linkObject));
}
}
}
}
return Promise.all(promises);
},
/**
* Submit transaction links.
*/
submitTransactionLinksX: function () {
//return this.deleteAllOriginalLinks().then(() => this.submitNewLinks());
},
finalizeSubmitX: function () {
// console.log('now in finalizeSubmit()');
// console.log('submittedTransaction : ' + this.submittedTransaction);
// console.log('submittedLinks : ' + this.submittedLinks);
// console.log('submittedAttachments : ' + this.submittedAttachments);
if (this.submittedTransaction && this.submittedAttachments && this.submittedLinks) {
console.log('all true');
console.log('inError = ' + this.inError);
console.log('stayHere = ' + this.stayHere);
console.log('returnedGroupId = ' + this.returnedGroupId);
// console.log('all true');
// console.log('inError = ' + this.inError);
// console.log('stayHere = ' + this.stayHere);
// console.log('returnedGroupId = ' + this.returnedGroupId);
// no error + no changes + no redirect
if (true === this.stayHere && false === this.inError && 0 === this.returnedGroupId) {
console.log('no error + no changes + no redirect');
// console.log('no error + no changes + no redirect');
// show message:
this.errorMessage = '';
this.successMessage = '';
@ -900,12 +1048,12 @@ export default {
// no error + no changes + redirect
if (false === this.stayHere && false === this.inError && 0 === this.returnedGroupId) {
console.log('no error + no changes + redirect');
// console.log('no error + no changes + redirect');
window.location.href = (window.previousURL ?? '/') + '?transaction_group_id=' + this.groupId + '&message=no_change';
}
// no error + changes + no redirect
if (true === this.stayHere && false === this.inError && 0 !== this.returnedGroupId) {
console.log('no error + changes + redirect');
// console.log('no error + changes + redirect');
// show message:
this.errorMessage = '';
this.warningMessage = '';
@ -915,10 +1063,10 @@ export default {
// no error + changes + redirect
if (false === this.stayHere && false === this.inError && 0 !== this.returnedGroupId) {
console.log('no error + changes + redirect');
// console.log('no error + changes + redirect');
window.location.href = (window.previousURL ?? '/') + '?transaction_group_id=' + this.groupId + '&message=updated';
}
console.log('end of the line');
// console.log('end of the line');
// enable flags:
this.enableSubmit = true;
this.submittedTransaction = false;

View File

@ -267,8 +267,9 @@
v-on="$listeners"
:custom-fields.sync="customFields"
:index="index"
:submitted_transaction="submittedTransaction"
:transaction_journal_id="transaction.transaction_journal_id"
:upload-trigger="transaction.uploadTrigger"
:clear-trigger="transaction.clearTrigger"
/>
<TransactionLocation
v-model="transaction.location"
@ -347,11 +348,6 @@ export default {
type: String,
required: true
},
submittedTransaction: {
type: Boolean,
required: false,
default: false
}, // need to know if transaction is submitted.
sourceAllowedTypes: {
type: Array,
required: false,
@ -381,21 +377,24 @@ export default {
return this.date;
},
sourceAccount: function () {
console.log('computed::sourceAccount');
//console.log('computed::sourceAccount(' + this.index + ')');
let value = {
id: this.transaction.source_account_id,
name: this.transaction.source_account_name,
type: this.transaction.source_account_type,
};
console.log(JSON.stringify(value));
//console.log(JSON.stringify(value));
return value;
},
destinationAccount: function () {
return {
//console.log('computed::destinationAccount(' + this.index + ')');
let value = {
id: this.transaction.destination_account_id,
name: this.transaction.destination_account_name,
type: this.transaction.destination_account_type,
};
//console.log(JSON.stringify(value));
return value;
},
hasMetaFields: function () {
let requiredFields = [

View File

@ -23,7 +23,7 @@
<div v-if="visible" class="text-xs d-none d-lg-block d-xl-block">
<span v-if="0 === this.index">{{ $t('firefly.' + this.direction + '_account') }}</span>
<span v-if="this.index > 0" class="text-warning">{{ $t('firefly.first_split_overrules_' + this.direction) }}</span>
<!-- <br><span>{{ selectedAccount }}</span> -->
<!--<br><span>{{ selectedAccount }}</span>-->
</div>
<div v-if="!visible" class="text-xs d-none d-lg-block d-xl-block">
&nbsp;
@ -127,8 +127,8 @@ export default {
this.selectedAccount = event;
},
systemReturnedAccount: function (event) {
// console.log('systemReturnedAccount!');
// console.log('To prevent invalid propogation, set selectedAccountTrigger = false');
//console.log('systemReturnedAccount!');
//console.log('To prevent invalid propogation, set selectedAccountTrigger = false');
this.selectedAccountTrigger = false;
this.selectedAccount = event;
},
@ -192,10 +192,10 @@ export default {
* @param value
*/
selectedAccount: function (value) {
console.log('TransactionAccount::watch selectedAccount()');
console.log(value);
//console.log('TransactionAccount::watch selectedAccount()');
// console.log(value);
if (true === this.selectedAccountTrigger) {
console.log('$emit alles!');
// console.log('$emit alles!');
this.$emit('set-account',
{
index: this.index,
@ -208,18 +208,27 @@ export default {
currency_symbol: value.currency_symbol,
}
);
console.log('watch::selectedAccount() will now set accountName because selectedAccountTrigger = true');
//console.log('watch::selectedAccount() will now set accountName because selectedAccountTrigger = true');
this.accountName = value.name;
}
if (false === this.selectedAccountTrigger) {
//console.log('watch::selectedAccount() will NOT set accountName because selectedAccountTrigger = false');
}
if (false === this.selectedAccountTrigger && this.accountName !== value.name && null !== value.name) {
//console.log('watch::selectedAccount() will set accountName. selectedAccountTrigger = false but name is different ("' + this.accountName + '" > "' + value.name + '")');
this.selectedAccountTrigger = true;
this.accountName = value.name;
}
},
accountName: function (value) {
console.log('now at watch accountName("' + value + '")');
console.log(this.selectedAccountTrigger);
// console.log('now at watch accountName("' + value + '")');
// console.log(this.selectedAccountTrigger);
if (true === this.selectedAccountTrigger) {
console.log('Do nothing because selectedAccountTrigger = true');
// console.log('Do nothing because selectedAccountTrigger = true');
}
if (false === this.selectedAccountTrigger) {
console.log('$emit name from watch::accountName() because selectedAccountTrigger = false');
// console.log('$emit name from watch::accountName() because selectedAccountTrigger = false');
this.$emit('set-account',
{
index: this.index,
@ -234,11 +243,11 @@ export default {
);
// this.account = {name: value, type: null, id: null, currency_id: null, currency_code: null, currency_symbol: null};
}
console.log('set selectedAccountTrigger to be FALSE');
// console.log('set selectedAccountTrigger to be FALSE');
this.selectedAccountTrigger = false;
},
value: function (value) {
console.log('TransactionAccount::watch value(' + JSON.stringify(value) + ')');
//console.log('TransactionAccount(' + this.index + ')::watch value(' + JSON.stringify(value) + ')');
this.systemReturnedAccount(value);
// // console.log('Index ' + this.index + ' nwAct: ', value);

View File

@ -39,27 +39,29 @@
<script>
export default {
name: "TransactionAttachments",
props: ['transaction_journal_id', 'customFields'],
props: ['transaction_journal_id', 'customFields', 'index', 'uploadTrigger', 'clearTrigger'],
data() {
return {
availableFields: this.customFields
availableFields: this.customFields,
uploads: 0,
created: 0,
uploaded: 0,
}
},
watch: {
customFields: function (value) {
this.availableFields = value;
},
uploadTrigger: function () {
//console.log('uploadTrigger(' + this.transaction_journal_id + ',' + this.index + ')');
this.doUpload();
},
clearTrigger: function () {
//console.log('clearTrigger(' + this.transaction_journal_id + ',' + this.index + ')');
this.$refs.att.value = null;
},
transaction_journal_id: function (value) {
if (!this.showField) {
// console.log('Field is hidden. Emit event!');
this.$emit('uploaded-attachments', value);
return;
}
// console.log('transaction_journal_id changed to ' + value);
// do upload!
if (0 !== value) {
this.doUpload();
}
//console.log('watch transaction_journal_id: ' + value + ' (index ' + this.index + ')');
}
},
computed: {
@ -71,47 +73,65 @@ export default {
}
},
methods: {
selectedFile: function() {
this.$emit('selected-attachments', this.transaction_journal_id);
selectedFile: function () {
this.$emit('selected-attachments', {index: this.index, id: this.transaction_journal_id});
},
createAttachment: function (name) {
// console.log('Now in createAttachment()');
const uri = './api/v1/attachments';
const data = {
filename: name,
attachable_type: 'TransactionJournal',
attachable_id: this.transaction_journal_id,
};
// create new attachment:
return axios.post(uri, data);
},
uploadAttachment: function (attachmentId, data) {
this.created++;
// console.log('Now in uploadAttachment()');
const uploadUri = './api/v1/attachments/' + attachmentId + '/upload';
return axios.post(uploadUri, data)
},
countAttachment: function () {
this.uploaded++;
//console.log('Uploaded ' + this.uploaded + ' / ' + this.uploads);
if (this.uploaded >= this.uploads) {
//console.log('All files uploaded. Emit event for ' + this.transaction_journal_id + '(' + this.index + ')');
this.$emit('uploaded-attachments', this.transaction_journal_id);
}
},
doUpload: function () {
// console.log('Now in doUpload() for ' + this.$refs.att.files.length + ' files.');
for (let i in this.$refs.att.files) {
if (this.$refs.att.files.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
let current = this.$refs.att.files[i];
let files = this.$refs.att.files;
this.uploads = files.length;
// loop all files and create attachments.
for (let i in files) {
if (files.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// console.log('Now at file ' + (parseInt(i) + 1) + ' / ' + files.length);
// read file into file reader:
let current = files[i];
let fileReader = new FileReader();
let theParent = this; // dont ask me why i need to do this.
fileReader.onloadend = function (evt) {
fileReader.onloadend = evt => {
if (evt.target.readyState === FileReader.DONE) {
// do upload here
const uri = './api/v1/attachments';
const data = {
filename: current.name,
attachable_type: 'TransactionJournal',
attachable_id: theParent.transaction_journal_id,
};
// create new attachment:
axios.post(uri, data).then(response => {
// upload actual file:
const uploadUri = './api/v1/attachments/' + response.data.data.id + '/upload';
axios
.post(uploadUri, new Blob([evt.target.result]))
.then(attachmentResponse => {
// TODO feedback etc.
// console.log('Uploaded a file. Emit event!');
// console.log(attachmentResponse);
theParent.$emit('uploaded-attachments', this.transaction_journal_id);
});
});
// console.log('I am done reading file ' + (parseInt(i) + 1));
this.createAttachment(current.name).then(response => {
// console.log('Created attachment. Now upload (1)');
return theParent.uploadAttachment(response.data.data.id, new Blob([evt.target.result]));
}).then(theParent.countAttachment);
}
}
fileReader.readAsArrayBuffer(current);
}
}
if (0 === this.$refs.att.files.length) {
// console.log('No files to upload. Emit event!');
if (0 === files.length) {
//console.log('No files to upload. Emit event!');
this.$emit('uploaded-attachments', this.transaction_journal_id);
}
// Promise.all(promises).then(response => {
// console.log('All files uploaded. Emit event!');
// this.$emit('uploaded-attachments', this.transaction_journal_id);
// });
}
}

View File

@ -61,13 +61,12 @@ export default {
return {
categories: [],
initialSet: [],
category: this.value,
emitEvent: true
category: this.value
}
},
created() {
//console.log('Created category(' + this.index + ') "' + this.value + '"');
// initial list of accounts:
axios.get(this.getACURL(''))
.then(response => {
@ -82,11 +81,13 @@ export default {
},
getACURL: function (query) {
// update autocomplete URL:
// console.log('getACURL("' + query + '")');
return document.getElementsByTagName('base')[0].href + 'api/v1/autocomplete/categories?query=' + query;
},
lookupCategory: debounce(function () {
// update autocomplete URL:
axios.get(this.getACURL(this.value))
//console.log('Do a search for "'+this.category+'"');
axios.get(this.getACURL(this.category))
.then(response => {
this.categories = response.data;
})
@ -94,7 +95,6 @@ export default {
},
watch: {
value: function (value) {
this.emitEvent = false;
this.category = value ?? '';
},
category: function (value) {

View File

@ -19,7 +19,7 @@
-->
<template>
<div class="form-group">
<div class="form-group" v-if="0===index">
<div class="text-xs d-none d-lg-block d-xl-block">
{{ $t('firefly.date_and_time') }}
</div>
@ -28,7 +28,6 @@
ref="date"
v-model="dateStr"
:class="errors.length > 0 ? 'form-control is-invalid' : 'form-control'"
:disabled="index > 0"
:placeholder="dateStr"
:title="$t('firefly.date')"
autocomplete="off"
@ -39,7 +38,6 @@
ref="time"
v-model="timeStr"
:class="errors.length > 0 ? 'form-control is-invalid' : 'form-control'"
:disabled="index > 0"
:placeholder="timeStr"
:title="$t('firefly.time')"
autocomplete="off"
@ -64,7 +62,7 @@ export default {
created() {
this.localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
this.systemTimeZone = this.timezone;
console.log('TransactionDate: ' + this.date);
// console.log('TransactionDate: ' + this.date);
// split date and time:
let parts = this.date.split('T');
this.dateStr = parts[0];

View File

@ -91,7 +91,7 @@
</div>
<div class="row">
<div class="col">
<form v-on:submit.prevent="search">
<form v-on:submit.prevent="search" autocomplete="off">
<div class="input-group">
<input id="query" v-model="query" autocomplete="off" class="form-control" maxlength="255" name="search"
placeholder="Search query" type="text">

View File

@ -78,7 +78,7 @@ export default {
this.tagList = value;
},
tagList: function (value) {
console.log('watch tagList');
// console.log('watch tagList');
if (true === this.emitEvent) {
this.$emit('set-field', {field: 'tags', index: this.index, value: value});
}

View File

@ -141,7 +141,10 @@
"next_expected_match": "\u0421\u043b\u0435\u0434\u0432\u0430\u0449o \u043e\u0447\u0430\u043a\u0432\u0430\u043do \u0441\u044a\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435"
},
"config": {
"html_language": "bg"
"html_language": "bg",
"week_in_year_fns": "'Week' w, yyyy",
"quarter_fns": "'Q'Q, yyyy",
"half_year_fns": "'H{half}', yyyy"
},
"form": {
"foreign_amount": "\u0421\u0443\u043c\u0430 \u0432\u044a\u0432 \u0432\u0430\u043b\u0443\u0442\u0430",

View File

@ -141,7 +141,10 @@
"next_expected_match": "Dal\u0161\u00ed o\u010dek\u00e1van\u00e1 shoda"
},
"config": {
"html_language": "cs"
"html_language": "cs",
"week_in_year_fns": "'Week' w, yyyy",
"quarter_fns": "'Q'Q, yyyy",
"half_year_fns": "'H{half}', yyyy"
},
"form": {
"foreign_amount": "\u010c\u00e1stka v ciz\u00ed m\u011bn\u011b",

View File

@ -109,7 +109,7 @@
"edit": "Bearbeiten",
"account_type_Loan": "Darlehen",
"account_type_Mortgage": "Hypothek",
"timezone_difference": "Your browser reports time zone \"{local}\". Firefly III is configured for time zone \"{system}\". This chart may drift.",
"timezone_difference": "Ihr Browser meldet die Zeitzone \u201e{local}\u201d. Firefly III ist aber f\u00fcr die Zeitzone \u201e{system}\u201d konfiguriert. Diese Karte kann deshalb abweichen.",
"stored_new_account_js": "Neues Konto \"<a href=\"accounts\/show\/{ID}\">\u201e{name}\u201d<\/a>\" gespeichert!",
"account_type_Debt": "Schuld",
"delete": "L\u00f6schen",
@ -141,7 +141,10 @@
"next_expected_match": "N\u00e4chste erwartete \u00dcbereinstimmung"
},
"config": {
"html_language": "de"
"html_language": "de",
"week_in_year_fns": "'Woche' ww\/yyyy",
"quarter_fns": "'Q'QQQ, yyyy",
"half_year_fns": "'H{half}', yyyy"
},
"form": {
"foreign_amount": "Ausl\u00e4ndischer Betrag",

View File

@ -141,7 +141,10 @@
"next_expected_match": "\u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7"
},
"config": {
"html_language": "el"
"html_language": "el",
"week_in_year_fns": "'Week' w, yyyy",
"quarter_fns": "'Q'Q, yyyy",
"half_year_fns": "'H{half}', yyyy"
},
"form": {
"foreign_amount": "\u03a0\u03bf\u03c3\u03cc \u03c3\u03b5 \u03be\u03ad\u03bd\u03bf \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1",

View File

@ -141,7 +141,10 @@
"next_expected_match": "Next expected match"
},
"config": {
"html_language": "en-gb"
"html_language": "en-gb",
"week_in_year_fns": "'Week' w, yyyy",
"quarter_fns": "'Q'Q, yyyy",
"half_year_fns": "'H{half}', yyyy"
},
"form": {
"foreign_amount": "Foreign amount",

Some files were not shown because too many files have changed in this diff Show More