Merge branch 'release/5.6.3'

This commit is contained in:
James Cole 2021-11-12 20:27:11 +01:00
commit 7afe9fac0a
150 changed files with 2976 additions and 2558 deletions

View File

@ -177,6 +177,12 @@ MAP_DEFAULT_ZOOM=6
# https://docs.firefly-iii.org/advanced-installation/authentication # https://docs.firefly-iii.org/advanced-installation/authentication
AUTHENTICATION_GUARD=web AUTHENTICATION_GUARD=web
#
# Your LDAP server may speak a dialect. You can choose between 'OpenLDAP' and 'ActiveDirectory'
# Anything else defaults to 'ActiveDirectory'
#
LDAP_DIALECT=OpenLDAP
# #
# LDAP connection settings: # LDAP connection settings:
# #
@ -193,14 +199,13 @@ LDAP_PASSWORD=super_secret
LDAP_AUTH_FIELD=uid LDAP_AUTH_FIELD=uid
# #
# If you wish to only authenticate users from a specific group, use the # If you wish to only authenticate users from a specific group, use the base DN above.
# group filter. Leave empty or remove if not in use.
# #
# Example: cn=Administrators,dc=local,dc=com # If you require extra/special filters please use the LDAP_EXTRA_FILTER with a valid DN.
# #
# The group filter will only be applied after the user is authenticated. # The extra filter will only be applied after the user is authenticated.
# #
LDAP_GROUP_FILTER= LDAP_EXTRA_FILTER=
# #
# Remote user guard settings # Remote user guard settings
@ -297,6 +302,8 @@ FIREFLY_III_LAYOUT=v1
# It won't work. It doesn't do ANYTHING. Don't believe the lies you read online. I'm not joking. # It won't work. It doesn't do ANYTHING. Don't believe the lies you read online. I'm not joking.
# This configuration value WILL NOT HELP. # This configuration value WILL NOT HELP.
# #
# Notable exception to this rule is Synology, which, according to some users, will use APP_URL to rewrite stuff.
#
# This variable is ONLY used in some of the emails Firefly III sends around. Nowhere else. # This variable is ONLY used in some of the emails Firefly III sends around. Nowhere else.
# So when configuring anything WEB related this variable doesn't do anything. Nothing # So when configuring anything WEB related this variable doesn't do anything. Nothing
# #

View File

@ -1,7 +0,0 @@
pull_request_rules:
- name: PR on main is never approved.
conditions:
- base=main
actions:
close:
message: Please reopen this PR on the `develop` branch. Thank you.

4
.github/stale.yml vendored
View File

@ -1,11 +1,11 @@
# Configuration for probot-stale - https://github.com/probot/stale # Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale # Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 7 daysUntilStale: 14
# Number of days of inactivity before a stale Issue or Pull Request is closed. # Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7 daysUntilClose: 14
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
# - "[Status] Maybe Later" # - "[Status] Maybe Later"

View File

@ -1,58 +0,0 @@
---
build:
nodes:
analysis:
project_setup:
override: true
tests:
override:
- php-scrutinizer-run
checks:
javascript: true
php:
align_assignments: true
avoid_fixme_comments: true
avoid_multiple_statements_on_same_line: true
avoid_perl_style_comments: true
avoid_todo_comments: true
duplication: false
encourage_single_quotes: true
newline_at_end_of_file: true
no_goto: true
no_long_variable_names:
maximum: "20"
no_short_method_names:
minimum: "3"
no_short_variable_names:
minimum: "3"
optional_parameters_at_the_end: true
parameter_doc_comments: true
remove_extra_empty_lines: true
return_doc_comment_if_not_inferrable: true
return_doc_comments: true
uppercase_constants: true
use_self_instead_of_fqcn: true
coding_style:
php:
spaces:
around_operators:
concatenation: true
other:
after_type_cast: false
filter:
excluded_paths:
- database/migrations/*
- bootstrap/*
- config/*
- docker/*
- public/js/lib/*
- public/lib/adminlte/js/*
- public/lib/bootstrap/js/*
- resources/*
- routes/*
- storage/*
paths:
- app/*
- public/js/ff/*
tools:
external_code_coverage: false

View File

@ -1,20 +0,0 @@
language: php
php:
- '7.4'
dist: xenial
os: linux
cache:
directories:
- "/home/travis/.config"
- "/home/travis/build/firefly-iii/firefly-iii/vendor"
branches:
only:
- develop
before_script:
- phpenv config-rm xdebug.ini || return 0
script:
- "./.ci/phpstan.sh"
- "./.ci/phpunit.sh"

View File

@ -211,7 +211,7 @@ class StoreRequest extends FormRequest
// budget, category, bill and piggy // budget, category, bill and piggy
'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser], 'transactions.*.budget_id' => ['mustExist:budgets,id', new BelongsUser],
'transactions.*.budget_name' => ['between:1,255', 'nullable', new BelongsUser], 'transactions.*.budget_name' => ['between:1,255', 'nullable', new BelongsUser],
'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser], 'transactions.*.category_id' => ['mustExist:categories,id', new BelongsUser,'nullable'],
'transactions.*.category_name' => 'between:1,255|nullable', 'transactions.*.category_name' => 'between:1,255|nullable',
'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser], 'transactions.*.bill_id' => ['numeric', 'nullable', 'mustExist:bills,id', new BelongsUser],
'transactions.*.bill_name' => ['between:1,255', 'nullable', new BelongsUser], 'transactions.*.bill_name' => ['between:1,255', 'nullable', new BelongsUser],

View File

@ -154,6 +154,7 @@ class ExportData extends Command
/** /**
* @return array * @return array
* @throws FireflyException * @throws FireflyException
* @throws Exception
*/ */
private function parseOptions(): array private function parseOptions(): array
{ {
@ -201,12 +202,17 @@ class ExportData extends Command
$error = true; $error = true;
} }
} }
if(null === $this->option($field)) {
Log::info(sprintf('No date given in field "%s"', $field));
$error = true;
}
if (true === $error && 'start' === $field) { if (true === $error && 'start' === $field) {
$journal = $this->journalRepository->firstNull(); $journal = $this->journalRepository->firstNull();
$date = null === $journal ? Carbon::now()->subYear() : $journal->date; $date = null === $journal ? Carbon::now()->subYear() : $journal->date;
$date->startOfDay(); $date->startOfDay();
} }
if (true === $error && 'end' === $field) { if (true === $error && 'end' === $field) {
$date = today(config('app.timezone')); $date = today(config('app.timezone'));
$date->endOfDay(); $date->endOfDay();

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* ActuallyLoggedIn.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\Events;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
/**
* Class ActuallyLoggedIn
*/
class ActuallyLoggedIn extends Event
{
use SerializesModels;
public User $user;
/**
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Handlers\Events;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\DetectedNewIPAddress;
use FireflyIII\Events\RegisteredUser; use FireflyIII\Events\RegisteredUser;
use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedNewPassword;
@ -116,9 +117,24 @@ class UserEventHandler
*/ */
public function createGroupMembership(RegisteredUser $event): bool public function createGroupMembership(RegisteredUser $event): bool
{ {
$user = $event->user; $user = $event->user;
$groupExists = true;
$groupTitle = $user->email;
$index = 1;
// create a new group. // create a new group.
$group = UserGroup::create(['title' => $user->email]); while (true === $groupExists) {
$groupExists = UserGroup::where('title', $groupTitle)->count() > 0;
if(false === $groupExists) {
$group = UserGroup::create(['title' => $groupTitle]);
break;
}
$groupTitle = sprintf('%s-%d', $user->email, $index);
$index++;
if($index > 99) {
throw new FireflyException('Email address can no longer be used for registrations.');
}
}
$role = UserRole::where('title', UserRole::OWNER)->first(); $role = UserRole::where('title', UserRole::OWNER)->first();
if (null === $role) { if (null === $role) {
throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?'); throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?');
@ -317,12 +333,11 @@ class UserEventHandler
} }
/** /**
* @param Login $event * @param ActuallyLoggedIn $event
*
* @throws FireflyException
*/ */
public function storeUserIPAddress(Login $event): void public function storeUserIPAddress(ActuallyLoggedIn $event): void
{ {
Log::debug('Now in storeUserIPAddress');
/** @var User $user */ /** @var User $user */
$user = $event->user; $user = $event->user;
/** @var array $preference */ /** @var array $preference */

View File

@ -79,7 +79,7 @@ class NetWorth implements NetWorthInterface
$netWorth = []; $netWorth = [];
$result = []; $result = [];
Log::debug(sprintf('Now in getNetWorthByCurrency(%s)', $date->format('Y-m-d'))); //Log::debug(sprintf('Now in getNetWorthByCurrency(%s)', $date->format('Y-m-d')));
// get default currency // get default currency
$default = app('amount')->getDefaultCurrencyByUser($this->user); $default = app('amount')->getDefaultCurrencyByUser($this->user);
@ -90,16 +90,16 @@ class NetWorth implements NetWorthInterface
// get the preferred currency for this account // get the preferred currency for this account
/** @var Account $account */ /** @var Account $account */
foreach ($accounts as $account) { foreach ($accounts as $account) {
Log::debug(sprintf('Now at account #%d: "%s"', $account->id, $account->name)); //Log::debug(sprintf('Now at account #%d: "%s"', $account->id, $account->name));
$currencyId = (int)$this->accountRepository->getMetaValue($account, 'currency_id'); $currencyId = (int)$this->accountRepository->getMetaValue($account, 'currency_id');
$currencyId = 0 === $currencyId ? $default->id : $currencyId; $currencyId = 0 === $currencyId ? $default->id : $currencyId;
Log::debug(sprintf('Currency ID is #%d', $currencyId)); //Log::debug(sprintf('Currency ID is #%d', $currencyId));
// balance in array: // balance in array:
$balance = $balances[$account->id] ?? '0'; $balance = $balances[$account->id] ?? '0';
Log::debug(sprintf('Balance is %s', $balance)); //Log::debug(sprintf('Balance is %s', $balance));
// always subtract virtual balance. // always subtract virtual balance.
$virtualBalance = (string)$account->virtual_balance; $virtualBalance = (string)$account->virtual_balance;
@ -107,14 +107,14 @@ class NetWorth implements NetWorthInterface
$balance = bcsub($balance, $virtualBalance); $balance = bcsub($balance, $virtualBalance);
} }
Log::debug(sprintf('Balance corrected to %s because of virtual balance (%s)', $balance, $virtualBalance)); //Log::debug(sprintf('Balance corrected to %s because of virtual balance (%s)', $balance, $virtualBalance));
if (!array_key_exists($currencyId, $netWorth)) { if (!array_key_exists($currencyId, $netWorth)) {
$netWorth[$currencyId] = '0'; $netWorth[$currencyId] = '0';
} }
$netWorth[$currencyId] = bcadd($balance, $netWorth[$currencyId]); $netWorth[$currencyId] = bcadd($balance, $netWorth[$currencyId]);
Log::debug(sprintf('Total net worth for currency #%d is %s', $currencyId, $netWorth[$currencyId])); //Log::debug(sprintf('Total net worth for currency #%d is %s', $currencyId, $netWorth[$currencyId]));
} }
ksort($netWorth); ksort($netWorth);

View File

@ -209,10 +209,10 @@ class PopupReport implements PopupReportInterface
$collector = app(GroupCollectorInterface::class); $collector = app(GroupCollectorInterface::class);
// set report accounts + the request accounts: // set report accounts + the request accounts:
$set = $attributes['accounts'] ?? new Collection; //$set = $attributes['accounts'] ?? new Collection;
$set->push($account); //$set->push($account);
$collector->setBothAccounts($set) $collector->setDestinationAccounts(new Collection([$account]))
->setRange($attributes['startDate'], $attributes['endDate']) ->setRange($attributes['startDate'], $attributes['endDate'])
->withAccountInformation() ->withAccountInformation()
->withBudgetInformation() ->withBudgetInformation()
@ -222,7 +222,6 @@ class PopupReport implements PopupReportInterface
if (null !== $currency) { if (null !== $currency) {
$collector->setCurrency($currency); $collector->setCurrency($currency);
} }
return $collector->getExtractedJournals(); return $collector->getExtractedJournals();
} }

View File

@ -101,7 +101,7 @@ class IndexController extends Controller
$accounts->each( $accounts->each(
function (Account $account) use ($activities, $startBalances, $endBalances) { function (Account $account) use ($activities, $startBalances, $endBalances) {
$account->lastActivityDate = $this->isInArray($activities, $account->id); $account->lastActivityDate = $this->isInArrayDate($activities, $account->id);
$account->startBalance = $this->isInArray($startBalances, $account->id); $account->startBalance = $this->isInArray($startBalances, $account->id);
$account->endBalance = $this->isInArray($endBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id);
$account->difference = bcsub($account->endBalance, $account->startBalance); $account->difference = bcsub($account->endBalance, $account->startBalance);
@ -163,7 +163,7 @@ class IndexController extends Controller
$accounts->each( $accounts->each(
function (Account $account) use ($activities, $startBalances, $endBalances) { function (Account $account) use ($activities, $startBalances, $endBalances) {
// See reference nr. 68 // See reference nr. 68
$account->lastActivityDate = $this->isInArray($activities, $account->id); $account->lastActivityDate = $this->isInArrayDate($activities, $account->id);
$account->startBalance = $this->isInArray($startBalances, $account->id); $account->startBalance = $this->isInArray($startBalances, $account->id);
$account->endBalance = $this->isInArray($endBalances, $account->id); $account->endBalance = $this->isInArray($endBalances, $account->id);
$account->difference = bcsub($account->endBalance, $account->startBalance); $account->difference = bcsub($account->endBalance, $account->startBalance);

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Http\Controllers\Auth;
use Adldap; use Adldap;
use Cookie; use Cookie;
use DB; use DB;
use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Providers\RouteServiceProvider; use FireflyIII\Providers\RouteServiceProvider;
@ -119,6 +120,10 @@ class LoginController extends Controller
// if you just logged in, it can't be that you have a valid 2FA cookie. // if you just logged in, it can't be that you have a valid 2FA cookie.
// send a custom login event because laravel will also fire a login event if a "remember me"-cookie
// restores the event.
event(new ActuallyLoggedIn($this->guard()->user()));
return $this->sendLoginResponse($request); return $this->sendLoginResponse($request);
} }
Log::warning('Login attempt failed.'); Log::warning('Login attempt failed.');

View File

@ -59,7 +59,6 @@ class ReportController extends Controller
'category-entry' => $this->categoryEntry($attributes), 'category-entry' => $this->categoryEntry($attributes),
'budget-entry' => $this->budgetEntry($attributes), 'budget-entry' => $this->budgetEntry($attributes),
}; };
return response()->json(['html' => $html]); return response()->json(['html' => $html]);
} }
} }

View File

@ -648,10 +648,9 @@ class CategoryController extends Controller
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
* *
* @return mixed|string * @return string
* @throws JsonException
*/ */
public function operations(Collection $accounts, Carbon $start, Carbon $end) public function operations(Collection $accounts, Carbon $start, Carbon $end): string
{ {
// chart properties for cache: // chart properties for cache:
$cache = new CacheProperties; $cache = new CacheProperties;
@ -673,7 +672,7 @@ class CategoryController extends Controller
try { try {
$result = prefixView('reports.partials.categories', compact('report'))->render(); $result = (string)prefixView('reports.partials.categories', compact('report'))->render();
$cache->store($result); $cache->store($result);
} catch (Throwable $e) { // @phpstan-ignore-line } catch (Throwable $e) { // @phpstan-ignore-line
Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage())); Log::error(sprintf('Could not render category::expenses: %s', $e->getMessage()));

View File

@ -34,6 +34,7 @@ use FireflyIII\Support\Http\Controllers\ModelInformation;
use FireflyIII\Support\Http\Controllers\RuleManagement; use FireflyIII\Support\Http\Controllers\RuleManagement;
use FireflyIII\Support\Search\SearchInterface; use FireflyIII\Support\Search\SearchInterface;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
@ -237,15 +238,17 @@ class CreateController extends Controller
/** /**
* @param Rule $rule * @param Rule $rule
* *
* @return RedirectResponse * @return JsonResponse
*/ */
public function duplicate(Rule $rule): RedirectResponse public function duplicate(Request $request): JsonResponse
{ {
$newRule = $this->ruleRepos->duplicate($rule); $ruleId = (int)$request->get('id');
$rule = $this->ruleRepos->find($ruleId);
if (null !== $rule) {
$this->ruleRepos->duplicate($rule);
}
session()->flash('success', trans('firefly.duplicated_rule', ['title' => $rule->title, 'newTitle' => $newRule->title])); return new JsonResponse(['OK']);
return redirect(route('rules.index'));
} }
/** /**

View File

@ -28,6 +28,7 @@ use FireflyIII\Http\Requests\RuleGroupFormRequest;
use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleGroup;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Redirector; use Illuminate\Routing\Redirector;
@ -62,24 +63,38 @@ class EditController extends Controller
} }
/** /**
* Move a rule group down. * Move a rule group in either direction.
* *
* @param RuleGroup $ruleGroup * @param Request $request
* *
* @return RedirectResponse|Redirector * @return JsonResponse
*/ */
public function down(RuleGroup $ruleGroup) public function moveGroup(Request $request): JsonResponse
{ {
$maxOrder = $this->repository->maxOrder(); $groupId = (int)$request->get('id');
$order = (int)$ruleGroup->order; $ruleGroup= $this->repository->find($groupId);
if ($order < $maxOrder) { if(null !== $ruleGroup) {
$newOrder = $order + 1; $direction = $request->get('direction');
$this->repository->setOrder($ruleGroup, $newOrder); if('down' === $direction) {
$maxOrder = $this->repository->maxOrder();
$order = (int)$ruleGroup->order;
if ($order < $maxOrder) {
$newOrder = $order + 1;
$this->repository->setOrder($ruleGroup, $newOrder);
}
}
if('up' === $direction) {
$order = (int)$ruleGroup->order;
if ($order > 1) {
$newOrder = $order - 1;
$this->repository->setOrder($ruleGroup, $newOrder);
}
}
} }
return new JsonResponse(['OK']);
return redirect(route('rules.index'));
} }
/** /**
* Edit a rule group. * Edit a rule group.
* *
@ -106,25 +121,6 @@ class EditController extends Controller
return prefixView('rules.rule-group.edit', compact('ruleGroup', 'subTitle')); return prefixView('rules.rule-group.edit', compact('ruleGroup', 'subTitle'));
} }
/**
* Move the rule group up.
*
* @param RuleGroup $ruleGroup
*
* @return RedirectResponse|Redirector
*
*/
public function up(RuleGroup $ruleGroup)
{
$order = (int)$ruleGroup->order;
if ($order > 1) {
$newOrder = $order - 1;
$this->repository->setOrder($ruleGroup, $newOrder);
}
return redirect(route('rules.index'));
}
/** /**
* Update the rule group. * Update the rule group.
* *

View File

@ -32,9 +32,6 @@ use Illuminate\Contracts\Config\Repository;
*/ */
class TrustProxies extends Middleware class TrustProxies extends Middleware
{ {
/** @var int The headers to check. */
//protected $headers = Request::HEADER_X_FORWARDED_ALL;
/** /**
* TrustProxies constructor. * TrustProxies constructor.
* *
@ -42,11 +39,7 @@ class TrustProxies extends Middleware
*/ */
public function __construct(Repository $config) public function __construct(Repository $config)
{ {
$trustedProxies = (string)config('firefly.trusted_proxies'); $this->proxies = (string)config('firefly.trusted_proxies');
$this->proxies = explode(',', $trustedProxies);
if ('**' === $trustedProxies) {
$this->proxies = '**';
}
parent::__construct($config); parent::__construct($config);
} }
} }

View File

@ -69,7 +69,7 @@ class BillStoreRequest extends FormRequest
'transaction_currency_id' => 'required|exists:transaction_currencies,id', 'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date', 'date' => 'required|date',
'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly', 'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly',
'skip' => 'required|between:0,31', 'skip' => 'required|integer|gte:0|lte:31',
'active' => 'boolean', 'active' => 'boolean',
]; ];
} }

View File

@ -73,7 +73,7 @@ class BillUpdateRequest extends FormRequest
'transaction_currency_id' => 'required|exists:transaction_currencies,id', 'transaction_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date', 'date' => 'required|date',
'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly', 'repeat_freq' => 'required|in:weekly,monthly,quarterly,half-year,yearly',
'skip' => 'required|between:0,31', 'skip' => 'required|integer|gte:0|lte:31',
'active' => 'boolean', 'active' => 'boolean',
]; ];
} }

View File

@ -63,7 +63,7 @@ class BudgetFormStoreRequest extends FormRequest
return [ return [
'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name', 'name' => 'required|between:1,100|uniqueObjectForUser:budgets,name',
'active' => 'numeric|between:0,1', 'active' => 'numeric|between:0,1',
'auto_budget_type' => 'numeric|between:0,2', 'auto_budget_type' => 'numeric|integer|gte:0|lte:2',
'auto_budget_currency_id' => 'exists:transaction_currencies,id', 'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2', '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', 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',

View File

@ -73,7 +73,7 @@ class BudgetFormUpdateRequest extends FormRequest
return [ return [
'name' => $nameRule, 'name' => $nameRule,
'active' => 'numeric|between:0,1', 'active' => 'numeric|between:0,1',
'auto_budget_type' => 'numeric|between:0,2', 'auto_budget_type' => 'numeric|integer|gte:0|lte:31',
'auto_budget_currency_id' => 'exists:transaction_currencies,id', 'auto_budget_currency_id' => 'exists:transaction_currencies,id',
'auto_budget_amount' => 'min:0|max:1000000000|required_if:auto_budget_type,1|required_if:auto_budget_type,2', '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', 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',

View File

@ -192,7 +192,7 @@ class RecurrenceFormRequest extends FormRequest
'title' => 'required|between:1,255|uniqueObjectForUser:recurrences,title', 'title' => 'required|between:1,255|uniqueObjectForUser:recurrences,title',
'first_date' => 'required|date|after:' . $today->format('Y-m-d'), 'first_date' => 'required|date|after:' . $today->format('Y-m-d'),
'repetition_type' => ['required', new ValidRecurrenceRepetitionValue, new ValidRecurrenceRepetitionType, 'between:1,20'], 'repetition_type' => ['required', new ValidRecurrenceRepetitionValue, new ValidRecurrenceRepetitionType, 'between:1,20'],
'skip' => 'required|numeric|between:0,31', 'skip' => 'required|numeric|integer|gte:0|lte:31',
// optional for recurrence: // optional for recurrence:
'recurring_description' => 'between:0,65000', 'recurring_description' => 'between:0,65000',

View File

@ -431,7 +431,7 @@ class CreateRecurringTransactions implements ShouldQueue
'user' => $recurrence->user_id, 'user' => $recurrence->user_id,
'currency_id' => (int)$transaction->transaction_currency_id, 'currency_id' => (int)$transaction->transaction_currency_id,
'currency_code' => null, 'currency_code' => null,
'description' => $recurrence->recurrenceTransactions()->first()->description, 'description' => $transactions->first()->description,
'amount' => $transaction->amount, 'amount' => $transaction->amount,
'budget_id' => $this->repository->getBudget($transaction), 'budget_id' => $this->repository->getBudget($transaction),
'budget_name' => null, 'budget_name' => null,
@ -452,7 +452,7 @@ class CreateRecurringTransactions implements ShouldQueue
'tags' => $this->repository->getTags($transaction), 'tags' => $this->repository->getTags($transaction),
'piggy_bank_id' => $this->repository->getPiggyBank($transaction), 'piggy_bank_id' => $this->repository->getPiggyBank($transaction),
'piggy_bank_name' => null, 'piggy_bank_name' => null,
'bill_id' => null, 'bill_id' => $this->repository->getBillId($transaction),
'bill_name' => null, 'bill_name' => null,
'recurrence_total' => $total, 'recurrence_total' => $total,
'recurrence_count' => $count, 'recurrence_count' => $count,

View File

@ -4,7 +4,8 @@ declare(strict_types=1);
namespace FireflyIII\Ldap\Rules; namespace FireflyIII\Ldap\Rules;
use LdapRecord\Laravel\Auth\Rule; use LdapRecord\Laravel\Auth\Rule;
use LdapRecord\Models\ActiveDirectory\Group; use LdapRecord\Models\Attributes\DistinguishedName;
use LdapRecord\Query\ObjectNotFoundException;
use Log; use Log;
/** /**
@ -16,22 +17,51 @@ class UserDefinedRule extends Rule
* Check if the rule passes validation. * Check if the rule passes validation.
* *
* @return bool * @return bool
* @throws ObjectNotFoundException
*/ */
public function isValid() public function isValid()
{ {
// LDAP_GROUP_FILTER $extraFilter = config('ldap.extra_filter');
$groupFilter = config('ldap.group_filter'); Log::debug(sprintf('UserDefinedRule with extra filter "%s"', $extraFilter));
Log::debug(sprintf('UserDefinedRule with group filter "%s"', $groupFilter));
if (null !== $groupFilter && '' !== (string)$groupFilter) {
Log::debug('Group filter is not empty, will now apply it.');
$administrators = Group::find($groupFilter);
$result = $this->user->groups()->recursive()->exists($administrators);
Log::debug(sprintf('Search result is %s.', var_export($result, true)));
return $result; if (empty($extraFilter)) {
Log::debug('Extra filter is empty, return true.');
return true;
} }
Log::debug('Group filter is empty or NULL, so will return true.'); Log::debug('Extra filter is not empty, continue.');
return true; // group class:
// use ;
$openLDAP = class_exists(\LdapRecord\Models\OpenLDAP\Group::class) ? \LdapRecord\Models\OpenLDAP\Group::class : '';
$activeDirectory = class_exists(\LdapRecord\Models\ActiveDirectory\Group::class) ? \LdapRecord\Models\ActiveDirectory\Group::class : '';
$groupClass = config('ldap.dialect') === 'OpenLDAP' ? $openLDAP : $activeDirectory;
Log::debug(sprintf('Will use dialect group class "%s"', $groupClass));
// We've been given an invalid group filter. We will assume the
// developer is using some group ANR attribute, and attempt
// to check the user's membership with the resulting group.
if (!DistinguishedName::isValid($extraFilter)) {
Log::debug('UserDefinedRule: Is not valid DN');
return $this->user->groups()->recursive()->exists($groupClass::findByAnrOrFail($extraFilter));
}
$head = strtolower(DistinguishedName::make($extraFilter)->head());
Log::debug(sprintf('UserDefinedRule: Head is "%s"', $head));
// If the head of the DN we've been given is an OU, we will assume
// the developer is looking to filter users based on hierarchy.
// Otherwise, we'll attempt locating a group by the given
// group filter and checking the users group membership.
if ('ou' === $head) {
Log::debug('UserDefinedRule: Will return if user is a descendant of.');
return $this->user->isDescendantOf($extraFilter);
}
Log::debug('UserDefinedRule: Will return if user exists in group.');
return $this->user->groups()->recursive()->exists($groupClass::findOrFail($extraFilter));
} }
} }

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Ldap\Scopes;
use LdapRecord\Models\Model;
use LdapRecord\Models\Scope;
use LdapRecord\Query\Model\Builder;
use Log;
/**
* Class UserDefinedScope
*/
class UserDefinedScope implements Scope
{
/**
* Apply the scope to the given query.
*
* @param Builder $query
* @param Model $model
*
* @return void
*/
public function apply(Builder $query, Model $model)
{
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Providers; namespace FireflyIII\Providers;
use Exception; use Exception;
use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\AdminRequestedTestMessage; use FireflyIII\Events\AdminRequestedTestMessage;
use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Events\DetectedNewIPAddress; use FireflyIII\Events\DetectedNewIPAddress;
@ -74,6 +75,8 @@ class EventServiceProvider extends ServiceProvider
Login::class => [ Login::class => [
'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin', 'FireflyIII\Handlers\Events\UserEventHandler@checkSingleUserIsAdmin',
'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish', 'FireflyIII\Handlers\Events\UserEventHandler@demoUserBackToEnglish',
],
ActuallyLoggedIn::class => [
'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress', 'FireflyIII\Handlers\Events\UserEventHandler@storeUserIPAddress',
], ],
DetectedNewIPAddress::class => [ DetectedNewIPAddress::class => [

View File

@ -251,7 +251,7 @@ class BillRepository implements BillRepositoryInterface
$journalIds = $set->pluck('id')->toArray(); $journalIds = $set->pluck('id')->toArray();
$amount = (string)Transaction::whereIn('transaction_journal_id', $journalIds)->where('amount', '<', 0)->sum('amount'); $amount = (string)Transaction::whereIn('transaction_journal_id', $journalIds)->where('amount', '<', 0)->sum('amount');
$sum = bcadd($sum, $amount); $sum = bcadd($sum, $amount);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $amount, $sum)); //Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $amount, $sum));
} }
} }
@ -281,7 +281,7 @@ class BillRepository implements BillRepositoryInterface
$amount = (string)Transaction::whereIn('transaction_journal_id', $journalIds)->where('amount', '<', 0)->sum('amount'); $amount = (string)Transaction::whereIn('transaction_journal_id', $journalIds)->where('amount', '<', 0)->sum('amount');
$return[$currencyId] = $return[$currencyId] ?? '0'; $return[$currencyId] = $return[$currencyId] ?? '0';
$return[$currencyId] = bcadd($amount, $return[$currencyId]); $return[$currencyId] = bcadd($amount, $return[$currencyId]);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (currency %d)', $amount, $return[$currencyId], $currencyId)); //Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (currency %d)', $amount, $return[$currencyId], $currencyId));
} }
} }
@ -302,18 +302,18 @@ class BillRepository implements BillRepositoryInterface
$sum = '0'; $sum = '0';
/** @var Bill $bill */ /** @var Bill $bill */
foreach ($bills as $bill) { foreach ($bills as $bill) {
Log::debug(sprintf('Now at bill #%d (%s)', $bill->id, $bill->name)); //Log::debug(sprintf('Now at bill #%d (%s)', $bill->id, $bill->name));
$dates = $this->getPayDatesInRange($bill, $start, $end); $dates = $this->getPayDatesInRange($bill, $start, $end);
$count = $bill->transactionJournals()->after($start)->before($end)->count(); $count = $bill->transactionJournals()->after($start)->before($end)->count();
$total = $dates->count() - $count; $total = $dates->count() - $count;
Log::debug(sprintf('Dates = %d, journalCount = %d, total = %d', $dates->count(), $count, $total)); //Log::debug(sprintf('Dates = %d, journalCount = %d, total = %d', $dates->count(), $count, $total));
if ($total > 0) { if ($total > 0) {
$average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2');
$multi = bcmul($average, (string)$total); $multi = bcmul($average, (string)$total);
$sum = bcadd($sum, $multi); $sum = bcadd($sum, $multi);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $multi, $sum)); //Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $multi, $sum));
} }
} }
@ -334,20 +334,20 @@ class BillRepository implements BillRepositoryInterface
$return = []; $return = [];
/** @var Bill $bill */ /** @var Bill $bill */
foreach ($bills as $bill) { foreach ($bills as $bill) {
Log::debug(sprintf('Now at bill #%d (%s)', $bill->id, $bill->name)); //Log::debug(sprintf('Now at bill #%d (%s)', $bill->id, $bill->name));
$dates = $this->getPayDatesInRange($bill, $start, $end); $dates = $this->getPayDatesInRange($bill, $start, $end);
$count = $bill->transactionJournals()->after($start)->before($end)->count(); $count = $bill->transactionJournals()->after($start)->before($end)->count();
$total = $dates->count() - $count; $total = $dates->count() - $count;
$currencyId = (int)$bill->transaction_currency_id; $currencyId = (int)$bill->transaction_currency_id;
Log::debug(sprintf('Dates = %d, journalCount = %d, total = %d', $dates->count(), $count, $total)); //Log::debug(sprintf('Dates = %d, journalCount = %d, total = %d', $dates->count(), $count, $total));
if ($total > 0) { if ($total > 0) {
$average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2');
$multi = bcmul($average, (string)$total); $multi = bcmul($average, (string)$total);
$return[$currencyId] = $return[$currencyId] ?? '0'; $return[$currencyId] = $return[$currencyId] ?? '0';
$return[$currencyId] = bcadd($return[$currencyId], $multi); $return[$currencyId] = bcadd($return[$currencyId], $multi);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (for currency %d)', $multi, $return[$currencyId], $currencyId)); //Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (for currency %d)', $multi, $return[$currencyId], $currencyId));
} }
} }
@ -481,7 +481,7 @@ class BillRepository implements BillRepositoryInterface
//Log::debug(sprintf('First currentstart is %s', $currentStart->format('Y-m-d'))); //Log::debug(sprintf('First currentstart is %s', $currentStart->format('Y-m-d')));
while ($currentStart <= $end) { while ($currentStart <= $end) {
Log::debug(sprintf('Currentstart is now %s.', $currentStart->format('Y-m-d'))); //Log::debug(sprintf('Currentstart is now %s.', $currentStart->format('Y-m-d')));
$nextExpectedMatch = $this->nextDateMatch($bill, $currentStart); $nextExpectedMatch = $this->nextDateMatch($bill, $currentStart);
//Log::debug(sprintf('Next Date match after %s is %s', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d'))); //Log::debug(sprintf('Next Date match after %s is %s', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d')));
if ($nextExpectedMatch > $end) {// If nextExpectedMatch is after end, we continue if ($nextExpectedMatch > $end) {// If nextExpectedMatch is after end, we continue

View File

@ -35,8 +35,7 @@ use Illuminate\Support\Collection;
*/ */
class OperationsRepository implements OperationsRepositoryInterface class OperationsRepository implements OperationsRepositoryInterface
{ {
/** @var User */ private User $user;
private $user;
/** /**
* This method returns a list of all the withdrawal transaction journals (as arrays) set in that period * This method returns a list of all the withdrawal transaction journals (as arrays) set in that period
@ -340,4 +339,136 @@ class OperationsRepository implements OperationsRepositoryInterface
{ {
return $this->user->categories()->get(); return $this->user->categories()->get();
} }
/**
* @inheritDoc
*/
public function listTransferredIn(Carbon $start, Carbon $end, Collection $accounts, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::TRANSFER])
->setDestinationAccounts($accounts)->excludeSourceAccounts($accounts);
if (null !== $categories && $categories->count() > 0) {
$collector->setCategories($categories);
}
if (null === $categories || (null !== $categories && 0 === $categories->count())) {
$collector->setCategories($this->getCategories());
}
$collector->withCategoryInformation()->withAccountInformation()->withBudgetInformation();
$journals = $collector->getExtractedJournals();
$array = [];
foreach ($journals as $journal) {
$currencyId = (int)$journal['currency_id'];
$categoryId = (int)$journal['category_id'];
$categoryName = (string)$journal['category_name'];
// catch "no category" entries.
if (0 === $categoryId) {
continue;
}
// info about the currency:
$array[$currencyId] = $array[$currencyId] ?? [
'categories' => [],
'currency_id' => $currencyId,
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_decimal_places' => $journal['currency_decimal_places'],
];
// info about the categories:
$array[$currencyId]['categories'][$categoryId] = $array[$currencyId]['categories'][$categoryId] ?? [
'id' => $categoryId,
'name' => $categoryName,
'transaction_journals' => [],
];
// add journal to array:
// only a subset of the fields.
$journalId = (int)$journal['transaction_journal_id'];
$array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [
'amount' => app('steam')->positive($journal['amount']),
'date' => $journal['date'],
'source_account_id' => $journal['source_account_id'],
'category_name' => $journal['category_name'],
'source_account_name' => $journal['source_account_name'],
'destination_account_id' => $journal['destination_account_id'],
'destination_account_name' => $journal['destination_account_name'],
'description' => $journal['description'],
'transaction_group_id' => $journal['transaction_group_id'],
];
}
return $array;
}
/**
* @inheritDoc
*/
public function listTransferredOut(Carbon $start, Carbon $end, Collection $accounts, ?Collection $categories = null): array
{
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::TRANSFER])
->setSourceAccounts($accounts)->excludeDestinationAccounts($accounts);
if (null !== $categories && $categories->count() > 0) {
$collector->setCategories($categories);
}
if (null === $categories || (null !== $categories && 0 === $categories->count())) {
$collector->setCategories($this->getCategories());
}
$collector->withCategoryInformation()->withAccountInformation()->withBudgetInformation();
$journals = $collector->getExtractedJournals();
$array = [];
foreach ($journals as $journal) {
$currencyId = (int)$journal['currency_id'];
$categoryId = (int)$journal['category_id'];
$categoryName = (string)$journal['category_name'];
// catch "no category" entries.
if (0 === $categoryId) {
continue;
}
// info about the currency:
$array[$currencyId] = $array[$currencyId] ?? [
'categories' => [],
'currency_id' => $currencyId,
'currency_name' => $journal['currency_name'],
'currency_symbol' => $journal['currency_symbol'],
'currency_code' => $journal['currency_code'],
'currency_decimal_places' => $journal['currency_decimal_places'],
];
// info about the categories:
$array[$currencyId]['categories'][$categoryId] = $array[$currencyId]['categories'][$categoryId] ?? [
'id' => $categoryId,
'name' => $categoryName,
'transaction_journals' => [],
];
// add journal to array:
// only a subset of the fields.
$journalId = (int)$journal['transaction_journal_id'];
$array[$currencyId]['categories'][$categoryId]['transaction_journals'][$journalId] = [
'amount' => app('steam')->negative($journal['amount']),
'date' => $journal['date'],
'source_account_id' => $journal['source_account_id'],
'category_name' => $journal['category_name'],
'source_account_name' => $journal['source_account_name'],
'destination_account_id' => $journal['destination_account_id'],
'destination_account_name' => $journal['destination_account_name'],
'description' => $journal['description'],
'transaction_group_id' => $journal['transaction_group_id'],
];
}
return $array;
}
} }

View File

@ -47,6 +47,36 @@ interface OperationsRepositoryInterface
*/ */
public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array; public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $categories = null): array;
/**
* This method returns a list of all the transfer transaction journals (as arrays) set in that period
* which have the specified category set to them, transferred INTO the listed accounts.
* It excludes any transfers between the listed accounts.
* It's grouped per currency, with as few details in the array as possible. Amounts are always negative.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* @param Collection|null $categories
*
* @return array
*/
public function listTransferredIn(Carbon $start, Carbon $end, Collection $accounts, ?Collection $categories = null): array;
/**
* This method returns a list of all the transfer transaction journals (as arrays) set in that period
* which have the specified category set to them, transferred FROM the listed accounts.
* It excludes any transfers between the listed accounts.
* It's grouped per currency, with as few details in the array as possible. Amounts are always negative.
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
* @param Collection|null $categories
*
* @return array
*/
public function listTransferredOut(Carbon $start, Carbon $end, Collection $accounts, ?Collection $categories = null): array;
/** /**
* This method returns a list of all the deposit transaction journals (as arrays) set in that period * This method returns a list of all the deposit transaction journals (as arrays) set in that period
* which have the specified category set to them. It's grouped per currency, with as few details in the array * which have the specified category set to them. It's grouped per currency, with as few details in the array

View File

@ -613,4 +613,20 @@ class RecurringRepository implements RecurringRepositoryInterface
return $filtered; return $filtered;
} }
/**
* @inheritDoc
*/
public function getBillId(RecurrenceTransaction $recTransaction): ?int
{
$return = null;
/** @var RecurrenceTransactionMeta $meta */
foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
if ('bill_id' === $meta->name) {
$return = (int)$meta->value;
}
}
return $return;
}
} }

View File

@ -82,6 +82,15 @@ interface RecurringRepositoryInterface
*/ */
public function getCategoryId(RecurrenceTransaction $recTransaction): ?int; public function getCategoryId(RecurrenceTransaction $recTransaction): ?int;
/**
* Get the category from a recurring transaction transaction.
*
* @param RecurrenceTransaction $recTransaction
*
* @return null|int
*/
public function getBillId(RecurrenceTransaction $recTransaction): ?int;
/** /**
* Get the category from a recurring transaction transaction. * Get the category from a recurring transaction transaction.
* *

View File

@ -329,10 +329,8 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
*/ */
public function resetOrder(): bool public function resetOrder(): bool
{ {
$this->user->ruleGroups()->where('active', false)->update(['order' => 0]);
$set = $this->user $set = $this->user
->ruleGroups() ->ruleGroups()
->where('active', true)
->whereNull('deleted_at') ->whereNull('deleted_at')
->orderBy('order', 'ASC') ->orderBy('order', 'ASC')
->orderBy('title', 'DESC') ->orderBy('title', 'DESC')
@ -363,7 +361,6 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
{ {
$set = $ruleGroup->rules() $set = $ruleGroup->rules()
->orderBy('order', 'ASC') ->orderBy('order', 'ASC')
->where('active', true)
->orderBy('title', 'DESC') ->orderBy('title', 'DESC')
->orderBy('updated_at', 'DESC') ->orderBy('updated_at', 'DESC')
->get(['rules.*']); ->get(['rules.*']);

View File

@ -26,6 +26,7 @@ use Exception;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Role; use FireflyIII\Models\Role;
use FireflyIII\Models\UserGroup;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -164,7 +165,10 @@ class UserRepository implements UserRepositoryInterface
public function destroy(User $user): bool public function destroy(User $user): bool
{ {
Log::debug(sprintf('Calling delete() on user %d', $user->id)); Log::debug(sprintf('Calling delete() on user %d', $user->id));
$user->groupMemberships()->delete();
$user->delete(); $user->delete();
$this->deleteEmptyGroups();
return true; return true;
} }
@ -396,4 +400,20 @@ class UserRepository implements UserRepositoryInterface
return true; return true;
} }
/**
* @inheritDoc
*/
public function deleteEmptyGroups(): void
{
$groups = UserGroup::get();
/** @var UserGroup $group */
foreach ($groups as $group) {
$count = $group->groupMemberships()->count();
if (0 === $count) {
Log::info(sprintf('Deleted empty group #%d ("%s")', $group->id, $group->title));
$group->delete();
}
}
}
} }

View File

@ -39,6 +39,11 @@ interface UserRepositoryInterface
*/ */
public function all(): Collection; public function all(): Collection;
/**
*
*/
public function deleteEmptyGroups(): void;
/** /**
* Gives a user a role. * Gives a user a role.
* *

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Controllers; namespace FireflyIII\Support\Http\Controllers;
use Carbon\Carbon;
/** /**
* Trait BasicDataSupport * Trait BasicDataSupport
* *
@ -30,15 +32,28 @@ namespace FireflyIII\Support\Http\Controllers;
trait BasicDataSupport trait BasicDataSupport
{ {
/** /**
* Find the ID in a given array. Return '0' of not there (amount). * Find the ID in a given array. Return '0' if not there (amount).
* *
* @param array $array * @param array $array
* @param int $entryId * @param int $entryId
* *
* @return null|mixed * @return null|mixed
*/ */
protected function isInArray(array $array, int $entryId) // helper for data (math, calculations) protected function isInArray(array $array, int $entryId)
{ {
return $array[$entryId] ?? '0'; return $array[$entryId] ?? '0';
} }
/**
* Find the ID in a given array. Return null if not there (amount).
*
* @param array $array
* @param int $entryId
*
* @return null|Carbon
*/
protected function isInArrayDate(array $array, int $entryId): ?Carbon
{
return $array[$entryId] ?? null;
}
} }

View File

@ -249,7 +249,7 @@ class BudgetReportGenerator
'budget_limits' => [], 'budget_limits' => [],
]; ];
$noBudget = $this->nbRepository->sumExpenses($this->start, $this->end); $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts);
foreach ($noBudget as $noBudgetEntry) { foreach ($noBudget as $noBudgetEntry) {
// currency information: // currency information:

View File

@ -66,6 +66,11 @@ class CategoryReportGenerator
{ {
$earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts); $earnedWith = $this->opsRepository->listIncome($this->start, $this->end, $this->accounts);
$spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts); $spentWith = $this->opsRepository->listExpenses($this->start, $this->end, $this->accounts);
// also transferred out and transferred into these accounts in this category:
$transferredIn = $this->opsRepository->listTransferredIn($this->start, $this->end, $this->accounts);
$transferredOut = $this->opsRepository->listTransferredOut($this->start, $this->end, $this->accounts);
$earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts); $earnedWithout = $this->noCatRepository->listIncome($this->start, $this->end, $this->accounts);
$spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts); $spentWithout = $this->noCatRepository->listExpenses($this->start, $this->end, $this->accounts);
@ -75,7 +80,7 @@ class CategoryReportGenerator
]; ];
// needs four for-each loops. // needs four for-each loops.
foreach ([$earnedWith, $spentWith, $earnedWithout, $spentWithout] as $data) { foreach ([$earnedWith, $spentWith, $earnedWithout, $spentWithout, $transferredIn, $transferredOut] as $data) {
$this->processOpsArray($data); $this->processOpsArray($data);
} }
} }

View File

@ -156,7 +156,7 @@ class OperatorQuerySearch implements SearchInterface
$parser = new QueryParser(); $parser = new QueryParser();
try { try {
$query1 = $parser->parse($query); $query1 = $parser->parse($query);
} catch (TypeError|LogicException $e) { } catch (TypeError | LogicException $e) {
Log::error($e->getMessage()); Log::error($e->getMessage());
Log::error(sprintf('Could not parse search: "%s".', $query)); Log::error(sprintf('Could not parse search: "%s".', $query));
throw new FireflyException('Invalid search value. See the logs.', 0, $e); throw new FireflyException('Invalid search value. See the logs.', 0, $e);
@ -245,6 +245,7 @@ class OperatorQuerySearch implements SearchInterface
private function handleSearchNode(Node $searchNode): void private function handleSearchNode(Node $searchNode): void
{ {
$class = get_class($searchNode); $class = get_class($searchNode);
Log::debug(sprintf('Now in handleSearchNode(%s)', $class));
switch ($class) { switch ($class) {
default: default:
Log::error(sprintf('Cannot handle node %s', $class)); Log::error(sprintf('Cannot handle node %s', $class));
@ -252,7 +253,7 @@ class OperatorQuerySearch implements SearchInterface
case Subquery::class: case Subquery::class:
// loop all notes in subquery: // loop all notes in subquery:
foreach ($searchNode->getNodes() as $subNode) { // @phpstan-ignore-line foreach ($searchNode->getNodes() as $subNode) { // @phpstan-ignore-line
$this->handleSearchNode($subNode); // lets hope its not too recursive! $this->handleSearchNode($subNode); // let's hope it's not too recursive!
} }
break; break;
case Word::class: case Word::class:
@ -282,13 +283,15 @@ class OperatorQuerySearch implements SearchInterface
'value' => (string)$value, 'value' => (string)$value,
] ]
); );
} else { Log::debug(sprintf('Added operator type "%s"', $operator));
}
if (!in_array($operator, $this->validOperators, true)) {
Log::debug(sprintf('Added INVALID operator type "%s"', $operator));
$this->invalidOperators[] = [ $this->invalidOperators[] = [
'type' => $operator, 'type' => $operator,
'value' => (string)$value, 'value' => (string)$value,
]; ];
} }
break;
} }
} }
@ -302,7 +305,7 @@ class OperatorQuerySearch implements SearchInterface
*/ */
private function updateCollector(string $operator, string $value): bool private function updateCollector(string $operator, string $value): bool
{ {
Log::debug(sprintf('updateCollector("%s", "%s")', $operator, $value)); Log::debug(sprintf('Now in updateCollector("%s", "%s")', $operator, $value));
// check if alias, replace if necessary: // check if alias, replace if necessary:
$operator = self::getRootOperator($operator); $operator = self::getRootOperator($operator);

View File

@ -406,7 +406,7 @@ class Steam
->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]); // @phpstan-ignore-line ->get(['transactions.account_id', DB::raw('MAX(transaction_journals.date) AS max_date')]); // @phpstan-ignore-line
foreach ($set as $entry) { foreach ($set as $entry) {
$date = new Carbon($entry->max_date, 'UTC'); $date = new Carbon($entry->max_date, config('app.timezone'));
$date->setTimezone(config('app.timezone')); $date->setTimezone(config('app.timezone'));
$list[(int)$entry->account_id] = $date; $list[(int)$entry->account_id] = $date;
} }

View File

@ -49,6 +49,7 @@ class General extends AbstractExtension
$this->mimeIcon(), $this->mimeIcon(),
$this->markdown(), $this->markdown(),
$this->floatval(), $this->floatval(),
$this->phpHostName(),
]; ];
} }
@ -91,6 +92,24 @@ class General extends AbstractExtension
); );
} }
/**
* Show URL host name
*
* @return TwigFilter
*/
protected function phpHostName(): TwigFilter
{
return new TwigFilter(
'phphost',
static function (string $string): string {
$proto = (string)parse_url($string, PHP_URL_SCHEME);
$host = (string)parse_url($string, PHP_URL_HOST);
return e(sprintf('%s://%s', $proto, $host));
}
);
}
/** /**
* Used to convert 1024 to 1kb etc. * Used to convert 1024 to 1kb etc.
* *

View File

@ -60,9 +60,11 @@ class DeleteTransaction implements ActionInterface
// trigger delete factory: // trigger delete factory:
$journal = TransactionJournal::find($journal['transaction_group_id']); $journal = TransactionJournal::find($journal['transaction_group_id']);
/** @var JournalDestroyService $service */ if (null !== $journal) {
$service = app(JournalDestroyService::class); /** @var JournalDestroyService $service */
$service->destroy($journal); $service = app(JournalDestroyService::class);
$service->destroy($journal);
}
return true; return true;
} }

View File

@ -351,7 +351,7 @@ class TransactionGroupTransformer extends AbstractTransformer
$bill = $this->getBill($journal->bill); $bill = $this->getBill($journal->bill);
if (null !== $foreignAmount && null !== $foreignCurrency) { if (null !== $foreignAmount && null !== $foreignCurrency) {
$foreignAmount = number_format((float)$foreignAmount, $foreignCurrency->decimal_places ?? 0, '.', ''); $foreignAmount = number_format((float)$foreignAmount, $foreignCurrency['decimal_places'] ?? 0, '.', '');
} }
$longitude = null; $longitude = null;

View File

@ -67,7 +67,7 @@ trait TransferValidation
return false; return false;
} }
// otherwise try to find the account: // or try to find the account:
$search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName); $search = $this->findExistingAccount($validTypes, (int)$accountId, (string)$accountName);
if (null === $search) { if (null === $search) {
$this->destError = (string)trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]); $this->destError = (string)trans('validation.transfer_dest_bad_data', ['id' => $accountId, 'name' => $accountName]);

View File

@ -41,15 +41,13 @@ trait ValidatesBulkTransactionQuery
protected function validateTransactionQuery(Validator $validator): void protected function validateTransactionQuery(Validator $validator): void
{ {
$data = $validator->getData(); $data = $validator->getData();
// assumption is all validation has already taken place // assumption is all validation has already taken place and the query key exists.
// and the query key exists.
$json = json_decode($data['query'], true, 8, JSON_THROW_ON_ERROR); $json = json_decode($data['query'], true, 8, JSON_THROW_ON_ERROR);
if (array_key_exists('account_id', $json['where']) if (array_key_exists('account_id', $json['where'])
&& array_key_exists('account_id', $json['update']) && array_key_exists('account_id', $json['update'])
) { ) {
// find both accounts // find both accounts, must be same type.
// must be same type.
// already validated: belongs to this user. // already validated: belongs to this user.
$repository = app(AccountRepositoryInterface::class); $repository = app(AccountRepositoryInterface::class);
$source = $repository->find((int)$json['where']['account_id']); $source = $repository->find((int)$json['where']['account_id']);

View File

@ -2,7 +2,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## 5.6.2 - 2021-10-xx ## 5.6.3 - 2021-11-12
### Changed
- [Issue 5133](https://github.com/firefly-iii/firefly-iii/issues/5133) Several possible fixes for LDAP filter.
### Fixed
- [Issue 5116](https://github.com/firefly-iii/firefly-iii/issues/5116) Fix missing icon
- [Issue 5173](https://github.com/firefly-iii/firefly-iii/issues/5173) Firefly III would email too often about logins
- [Issue 5178](https://github.com/firefly-iii/firefly-iii/issues/5178) Fix export parameter
- [Issue 5179](https://github.com/firefly-iii/firefly-iii/issues/5179) Possible fix for issue with empty date strings
- [Issue 5196](https://github.com/firefly-iii/firefly-iii/issues/5196) Fix issue with foreign amount formatting
- [Issue 5200](https://github.com/firefly-iii/firefly-iii/issues/5200) Link bill to recurring transaction
- [Issue 5218](https://github.com/firefly-iii/firefly-iii/issues/5218) Error in search
- [Issue 5226](https://github.com/firefly-iii/firefly-iii/issues/5226) Could submit negative "skip" values
- [Issue 5229](https://github.com/firefly-iii/firefly-iii/issues/5229) Fix serviceworker registration
### Security
- Logout is now POST and other minor CSRF issues.
## 5.6.2 - 2021-10-09
### Added ### Added
- `/health` will return `200 OK` if Firefly III is up and running, thanks @ajgon! - `/health` will return `200 OK` if Firefly III is up and running, thanks @ajgon!

View File

@ -91,25 +91,25 @@
"doctrine/dbal": "3.*", "doctrine/dbal": "3.*",
"fideloper/proxy": "4.*", "fideloper/proxy": "4.*",
"gdbots/query-parser": "^2.0", "gdbots/query-parser": "^2.0",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.4",
"jc5/google2fa-laravel": "2.0.6", "jc5/google2fa-laravel": "2.0.6",
"jc5/recovery": "^2", "jc5/recovery": "^2",
"laravel/framework": "^8.51", "laravel/framework": "^8.69",
"laravel/passport": "10.*", "laravel/passport": "10.*",
"laravel/ui": "^3.0", "laravel/ui": "^3.3",
"laravelcollective/html": "6.*", "laravelcollective/html": "6.*",
"league/commonmark": "2.*", "league/commonmark": "2.*",
"league/csv": "^9.6", "league/csv": "^9.6",
"league/fractal": "0.*", "league/fractal": "0.*",
"pragmarx/google2fa": "^8.0", "pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"psr/log": "<3",
"ramsey/uuid": "^4.2", "ramsey/uuid": "^4.2",
"rcrowe/twigbridge": "^0.12.1", "rcrowe/twigbridge": "^0.12.1",
"spatie/data-transfer-object": "^3.1", "spatie/data-transfer-object": "^3.1"
"psr/log": "<2"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.3", "barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-ide-helper": "2.*", "barryvdh/laravel-ide-helper": "2.*",
"filp/whoops": "2.*", "filp/whoops": "2.*",
"fakerphp/faker": "1.*", "fakerphp/faker": "1.*",

545
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,9 @@ declare(strict_types=1);
use FireflyIII\Ldap\AttributeHandler; use FireflyIII\Ldap\AttributeHandler;
use FireflyIII\Ldap\Rules\UserDefinedRule; use FireflyIII\Ldap\Rules\UserDefinedRule;
$openLDAP = class_exists(LdapRecord\Models\OpenLDAP\User::class) ? LdapRecord\Models\OpenLDAP\User::class : '';
$activeDirectory = class_exists(LdapRecord\Models\ActiveDirectory\User::class) ? LdapRecord\Models\ActiveDirectory\User::class : '';
return [ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -108,8 +111,7 @@ return [
'ldap' => [ 'ldap' => [
'driver' => 'ldap', 'driver' => 'ldap',
//'model' => LdapRecord\Models\ActiveDirectory\User::class, 'model' => env('LDAP_DIALECT') === 'OpenLDAP' ? $openLDAP : $activeDirectory,
'model' => LdapRecord\Models\OpenLDAP\User::class,
'rules' => [ 'rules' => [
UserDefinedRule::class, UserDefinedRule::class,
], ],

View File

@ -101,7 +101,7 @@ return [
'webhooks' => true, 'webhooks' => true,
'handle_debts' => true, 'handle_debts' => true,
], ],
'version' => '5.6.2', 'version' => '5.6.3',
'api_version' => '1.5.4', 'api_version' => '1.5.4',
'db_version' => 18, 'db_version' => 18,

View File

@ -36,8 +36,8 @@ return [
*/ */
'default' => env('LDAP_CONNECTION', 'default'), 'default' => env('LDAP_CONNECTION', 'default'),
'extra_filter' => env('LDAP_EXTRA_FILTER'),
'group_filter' => env('LDAP_GROUP_FILTER'), 'dialect' => env('LDAP_DIALECT'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -15,15 +15,15 @@
"laravel-mix": "^6", "laravel-mix": "^6",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"postcss": "^8.1.14", "postcss": "^8.3.11",
"resolve-url-loader": "^4.0.0", "resolve-url-loader": "^4.0.0",
"sass": "^1.39.2", "sass": "^1.43.3",
"sass-loader": "^12.0.0", "sass-loader": "^12.2.0",
"vue-i18n": "^8.24.2", "vue-i18n": "^8.26.7",
"vue-loader": "^15", "vue-loader": "^15",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"webpack": "^5.52.1" "webpack": "^5.62.1"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3", "@fortawesome/fontawesome-free": "^5.15.3",
@ -32,7 +32,7 @@
"axios-cache-adapter": "^2.7.3", "axios-cache-adapter": "^2.7.3",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.21.2",
"chart.js": "^3.4.0", "chart.js": "^3.6.0",
"icheck-bootstrap": "^3.0.1", "icheck-bootstrap": "^3.0.1",
"jquery-ui": "^1.12.1", "jquery-ui": "^1.12.1",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
@ -40,7 +40,7 @@
"localforage-memoryStorageDriver": "^0.9.2", "localforage-memoryStorageDriver": "^0.9.2",
"overlayscrollbars": "^1.13.1", "overlayscrollbars": "^1.13.1",
"sortablejs": "^1.14.0", "sortablejs": "^1.14.0",
"uiv": "^1.3.1", "uiv": "^1.4.1",
"v-calendar": "^2.3.2", "v-calendar": "^2.3.2",
"vue-typeahead-bootstrap": "^2.8.0", "vue-typeahead-bootstrap": "^2.8.0",
"vue2-leaflet": "^2.7.1" "vue2-leaflet": "^2.7.1"

View File

@ -97,7 +97,7 @@
<GenericLocation :disabled="submitting" v-model="location" :title="$t('form.location')" :errors="errors.location" <GenericLocation :disabled="submitting" v-model="location" :title="$t('form.location')" :errors="errors.location"
v-on:set-field="storeField($event)"/> v-on:set-field="storeField($event)"/>
<!-- attachments -->
<GenericAttachments :disabled="submitting" :title="$t('form.attachments')" field-name="attachments" :errors="errors.attachments" <GenericAttachments :disabled="submitting" :title="$t('form.attachments')" field-name="attachments" :errors="errors.attachments"
v-on:selected-attachments="selectedAttachments($event)" v-on:selected-attachments="selectedAttachments($event)"
v-on:selected-no-attachments="selectedNoAttachments($event)" v-on:selected-no-attachments="selectedNoAttachments($event)"
@ -275,6 +275,8 @@ export default {
this.hasAttachments = false; this.hasAttachments = false;
}, },
uploadedAttachments: function (e) { uploadedAttachments: function (e) {
console.log('Response to event uploaded-attachments');
console.log(e);
this.finishSubmission(); this.finishSubmission();
}, },
submitForm: function (e) { submitForm: function (e) {

View File

@ -23,6 +23,7 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-6 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-6 col-sm-12 col-xs-12">
<!-- Custom Tabs --> <!-- Custom Tabs -->
<!--
<div class="card"> <div class="card">
<div class="card-header d-flex p-0"> <div class="card-header d-flex p-0">
<h3 class="card-title p-3">Tabs</h3> <h3 class="card-title p-3">Tabs</h3>
@ -31,36 +32,34 @@
<li class="nav-item"><a class="nav-link" href="#budgets" data-toggle="tab">Budgets</a></li> <li class="nav-item"><a class="nav-link" href="#budgets" data-toggle="tab">Budgets</a></li>
<li class="nav-item"><a class="nav-link" href="#categories" data-toggle="tab">Categories</a></li> <li class="nav-item"><a class="nav-link" href="#categories" data-toggle="tab">Categories</a></li>
</ul> </ul>
</div><!-- /.card-header --> </div>
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="main_chart"> <div class="tab-pane active" id="main_chart">
1: main chart 1: main chart
</div> </div>
<!-- /.tab-pane -->
<div class="tab-pane" id="budgets"> <div class="tab-pane" id="budgets">
2: tree map from/to budget 2: tree map from/to budget
</div> </div>
<!-- /.tab-pane -->
<div class="tab-pane" id="categories"> <div class="tab-pane" id="categories">
2: tree map from/to cat 2: tree map from/to cat
</div> </div>
<!-- /.tab-pane -->
</div> </div>
<!-- /.tab-content --> </div>
</div><!-- /.card-body -->
</div> </div>
<!-- ./card --> -->
</div> </div>
</div> </div>
<TransactionListLarge <TransactionListLarge
:entries="rawTransactions" :entries="rawTransactions"
:page="currentPage" :page="currentPage"
ref="list"
:total="total" :total="total"
:per-page="perPage" :per-page="perPage"
:sort-desc="sortDesc" :sort-desc="sortDesc"
v-on:jump-page="jumpToPage($event)" v-on:jump-page="jumpToPage($event)"
v-on:refreshed-cache-key="refreshedKey"
/> />
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-6 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-6 col-sm-12 col-xs-12">
@ -106,7 +105,7 @@ export default {
perPage: 51, perPage: 51,
locale: 'en-US', locale: 'en-US',
api: null, api: null,
nameLoading:false nameLoading: false
} }
}, },
created() { created() {
@ -132,16 +131,23 @@ export default {
.then(response => { .then(response => {
let start = new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(this.start); let start = new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(this.start);
let end = new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(this.end); let end = new Intl.DateTimeFormat(this.locale, {year: 'numeric', month: 'long', day: 'numeric'}).format(this.end);
document.getElementById('page-subTitle').innerText = this.$t('firefly.journals_in_period_for_account_js', {start: start, end: end, title: response.data.data.attributes.name}); document.getElementById('page-subTitle').innerText = this.$t('firefly.journals_in_period_for_account_js', {
start: start,
end: end,
title: response.data.data.attributes.name
});
}); });
}); });
} }
}, },
refreshedKey: function () {
this.loading = false;
this.getTransactions();
this.updatePageTitle();
},
getTransactions: function () { getTransactions: function () {
if (this.showReady && !this.loading) { if (this.showReady && !this.loading) {
this.loading = true; this.loading = true;
configureAxios().then(async (api) => { configureAxios().then(async (api) => {
// console.log('Now getTransactions() x Start'); // console.log('Now getTransactions() x Start');
@ -149,8 +155,7 @@ export default {
let endStr = format(this.end, 'y-MM-dd'); let endStr = format(this.end, 'y-MM-dd');
this.rawTransactions = []; this.rawTransactions = [];
let url = './api/v1/accounts/' + this.accountId + '/transactions?page=1&limit=' + this.perPage + '&start=' + startStr + '&end=' + endStr; let url = './api/v1/accounts/' + this.accountId + '/transactions?page=1&limit=' + this.perPage + '&start=' + startStr + '&end=' + endStr + '&cache=' + this.cacheKey;
api.get(url) api.get(url)
.then(response => { .then(response => {

View File

@ -30,7 +30,6 @@
aria-valuemax="100" aria-valuemin="0" class="progress-bar bg-success" role="progressbar"> aria-valuemax="100" aria-valuemin="0" class="progress-bar bg-success" role="progressbar">
<span v-if="budgetLimit.pctGreen > 35"> <span v-if="budgetLimit.pctGreen > 35">
{{ $t('firefly.spent_x_of_y', {amount: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.spent), total: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.amount)}) }} {{ $t('firefly.spent_x_of_y', {amount: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.spent), total: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.amount)}) }}
<!-- -->
</span> </span>
@ -45,7 +44,7 @@
<div :aria-valuenow="budgetLimit.pctRed" :style="'width: '+ budgetLimit.pctRed + '%;'" <div :aria-valuenow="budgetLimit.pctRed" :style="'width: '+ budgetLimit.pctRed + '%;'"
aria-valuemax="100" aria-valuemin="0" class="progress-bar bg-danger" role="progressbar"> aria-valuemax="100" aria-valuemin="0" class="progress-bar bg-danger" role="progressbar">
<span v-if="budgetLimit.pctOrange <= 50 && budgetLimit.pctRed > 35" class="text-muted"> <span v-if="budgetLimit.pctOrange <= 50 && budgetLimit.pctRed > 35" class="text-white">
{{ $t('firefly.spent_x_of_y', {amount: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.spent), total: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.amount)}) }} {{ $t('firefly.spent_x_of_y', {amount: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.spent), total: Intl.NumberFormat(locale, {style: 'currency', currency: budgetLimit.currency_code}).format(budgetLimit.amount)}) }}
</span> </span>
</div> </div>

View File

@ -24,6 +24,7 @@
<td style="width:25%;"> <td style="width:25%;">
<a :href="'./budgets/show/' + budget.id">{{ budget.name }}</a> <a :href="'./budgets/show/' + budget.id">{{ budget.name }}</a>
</td> </td>
<td>&nbsp;</td>
<td class="align-middle text-right"> <td class="align-middle text-right">
<span class="text-danger"> <span class="text-danger">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: budget.currency_code}).format(parseFloat(budget.spent)) }} {{ Intl.NumberFormat(locale, {style: 'currency', currency: budget.currency_code}).format(parseFloat(budget.spent)) }}

View File

@ -214,8 +214,8 @@ export default {
let pctGreen = 0; let pctGreen = 0;
let pctOrange = 0; let pctOrange = 0;
let pctRed = 0; let pctRed = 0;
//console.log('Collected "' + period + '" budget limit #' + currentId + ' (part of budget #' + budgetId + ')'); console.log('Collected "' + period + '" budget limit #' + currentId + ' (part of budget #' + budgetId + ')');
//console.log('Spent ' + spentFloat + ' of ' + amount); console.log('Spent ' + spentFloatPos + ' of ' + amount);
// remove budget info from rawBudgets if it's there: // remove budget info from rawBudgets if it's there:
this.filterBudgets(budgetId, currencyId); this.filterBudgets(budgetId, currencyId);
@ -230,6 +230,12 @@ export default {
pctOrange = (spentFloatPos / amount) * 100; pctOrange = (spentFloatPos / amount) * 100;
pctRed = 100 - pctOrange; pctRed = 100 - pctOrange;
} }
// spent exactly on budget
if (0.0 !== spentFloatPos && spentFloatPos === amount) {
pctOrange = 0;
pctRed = 100;
}
let obj = { let obj = {
id: currentId, id: currentId,
amount: current.attributes.amount, amount: current.attributes.amount,

View File

@ -97,22 +97,22 @@ export default {
// }); // });
// new code // new code
// console.log('start of new'); console.log('start of new');
let files = this.$refs.att.files; let files = this.$refs.att.files;
this.uploads = files.length; this.uploads = files.length;
// loop all files and create attachments. // loop all files and create attachments.
for (let i in files) { for (let i in files) {
if (files.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) { if (files.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
// console.log('Now at file ' + (parseInt(i) + 1) + ' / ' + files.length); console.log('Now at file ' + (parseInt(i) + 1) + ' / ' + files.length);
// read file into file reader: // read file into file reader:
let current = files[i]; let current = files[i];
let fileReader = new FileReader(); let fileReader = new FileReader();
let theParent = this; // dont ask me why i need to do this. let theParent = this; // dont ask me why i need to do this.
fileReader.onloadend = evt => { fileReader.onloadend = evt => {
if (evt.target.readyState === FileReader.DONE) { if (evt.target.readyState === FileReader.DONE) {
// console.log('I am done reading file ' + (parseInt(i) + 1)); console.log('I am done reading file ' + (parseInt(i) + 1));
this.createAttachment(current.name).then(response => { this.createAttachment(current.name).then(response => {
// console.log('Created attachment. Now upload (1)'); console.log('Created attachment. Now upload (1)');
return theParent.uploadAttachment(response.data.data.id, new Blob([evt.target.result])); return theParent.uploadAttachment(response.data.data.id, new Blob([evt.target.result]));
}).then(theParent.countAttachment); }).then(theParent.countAttachment);
} }
@ -121,7 +121,7 @@ export default {
} }
} }
if (0 === files.length) { if (0 === files.length) {
// console.log('No files to upload. Emit event!'); console.log('No files to upload. Emit event!');
this.$emit('uploaded-attachments', this.transaction_journal_id); this.$emit('uploaded-attachments', this.transaction_journal_id);
} }
// Promise.all(promises).then(response => { // Promise.all(promises).then(response => {
@ -138,15 +138,15 @@ export default {
methods: { methods: {
countAttachment: function () { countAttachment: function () {
this.uploaded++; this.uploaded++;
// console.log('Uploaded ' + this.uploaded + ' / ' + this.uploads); console.log('Uploaded ' + this.uploaded + ' / ' + this.uploads);
if (this.uploaded >= this.uploads) { if (this.uploaded >= this.uploads) {
// console.log('All files uploaded. Emit event for ' + this.transaction_journal_id + '(' + this.index + ')'); console.log('All files uploaded. Emit event for ' + this.uploadObjectId);
this.$emit('uploaded-attachments', this.transaction_journal_id); this.$emit('uploaded-attachments', this.uploadObjectId);
} }
}, },
uploadAttachment: function (attachmentId, data) { uploadAttachment: function (attachmentId, data) {
this.created++; this.created++;
// console.log('Now in uploadAttachment()'); console.log('Now in uploadAttachment()');
const uploadUri = './api/v1/attachments/' + attachmentId + '/upload'; const uploadUri = './api/v1/attachments/' + attachmentId + '/upload';
return axios.post(uploadUri, data) return axios.post(uploadUri, data)
}, },

View File

@ -31,6 +31,7 @@
:count="transactions.length" :count="transactions.length"
:custom-fields="customFields" :custom-fields="customFields"
:date="date" :date="date"
ref="splitForms"
:destination-allowed-types="destinationAllowedTypes" :destination-allowed-types="destinationAllowedTypes"
:index="index" :index="index"
:source-allowed-types="sourceAllowedTypes" :source-allowed-types="sourceAllowedTypes"
@ -70,7 +71,7 @@
&nbsp; &nbsp;
</div> </div>
<button type="button" class="btn btn-outline-primary btn-block" @click="addTransactionArray"><span class="far fa-clone"></span> {{ <button type="button" class="btn btn-outline-primary btn-block" @click="addTransactionArray"><span class="far fa-clone"></span> {{
$t('firefly.add_another_split') $t('firefly.add_another_split')
}} }}
</button> </button>
</div> </div>
@ -333,7 +334,6 @@ export default {
for (let i in this.transactions) { for (let i in this.transactions) {
if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) { if (this.transactions.hasOwnProperty(i) && /^0$|^[1-9]\d*$/.test(i) && i <= 4294967294) {
if (this.transactions.hasOwnProperty(i)) { if (this.transactions.hasOwnProperty(i)) {
//this.
// console.log('Reset attachment #' + i); // console.log('Reset attachment #' + i);
this.updateField({index: i, field: 'transaction_journal_id', value: 0}); this.updateField({index: i, field: 'transaction_journal_id', value: 0});
this.updateField({index: i, field: 'errors', value: this.defaultErrors}) this.updateField({index: i, field: 'errors', value: this.defaultErrors})
@ -344,7 +344,8 @@ export default {
// reset the form: // reset the form:
if (this.resetFormAfter) { if (this.resetFormAfter) {
this.resetTransactions(); this.resetTransactions();
this.addTransaction(); setTimeout(this.addTransaction, 50);
} }
return Promise.resolve({response: 'from finaliseSubmission'}); return Promise.resolve({response: 'from finaliseSubmission'});
}, },
@ -433,6 +434,12 @@ export default {
this.updateField({index: payload.index, field: payload.direction + '_account_currency_symbol', value: payload.currency_symbol}); this.updateField({index: payload.index, field: payload.direction + '_account_currency_symbol', value: payload.currency_symbol});
//this.calculateTransactionType(payload.index); //this.calculateTransactionType(payload.index);
if ('source' === payload.direction && true === payload.user_selected) {
this.$refs.splitForms[payload.index].$refs.destinationAccount.giveFocus();
}
if ('destination' === payload.direction && true === payload.user_selected) {
this.$refs.splitForms[payload.index].$refs.amount.giveFocus();
}
}, },
storeField: function (payload) { storeField: function (payload) {
this.updateField(payload); this.updateField(payload);

View File

@ -20,6 +20,7 @@
<template> <template>
<div> <div>
<!--
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
@ -36,6 +37,7 @@
</div> </div>
</div> </div>
</div> </div>
-->
<!-- page is ignored for the time being --> <!-- page is ignored for the time being -->
<TransactionListLarge <TransactionListLarge
:entries="rawTransactions" :entries="rawTransactions"
@ -44,7 +46,9 @@
:per-page="perPage" :per-page="perPage"
:sort-desc="sortDesc" :sort-desc="sortDesc"
v-on:jump-page="jumpToPage($event)" v-on:jump-page="jumpToPage($event)"
v-on:refreshed-cache-key="refreshedKey"
/> />
<!--
<div class="row"> <div class="row">
<div class="col-xl-2 col-lg-4 col-sm-6 col-xs-12" v-for="range in ranges"> <div class="col-xl-2 col-lg-4 col-sm-6 col-xs-12" v-for="range in ranges">
<div class="card"> <div class="card">
@ -56,7 +60,9 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
-->
</div> </div>
</template> </template>
@ -141,13 +147,12 @@ export default {
}, },
methods: { methods: {
...mapMutations('root', ['refreshCacheKey',]), ...mapMutations('root', ['refreshCacheKey',]),
newCacheKey: function () { refreshedKey: function () {
this.refreshCacheKey();
this.downloaded = false; this.downloaded = false;
this.accounts = []; this.rawTransactions = [];
this.getTransactionList(); this.getTransactionList();
}, },
jumpToPage: function(event) { jumpToPage: function (event) {
// console.log('noticed a change!'); // console.log('noticed a change!');
this.currentPage = event.page; this.currentPage = event.page;
this.downloadTransactionList(event.page); this.downloadTransactionList(event.page);

View File

@ -58,6 +58,8 @@
:source-allowed-types="sourceAllowedTypes" :source-allowed-types="sourceAllowedTypes"
:transaction-type="transactionType" :transaction-type="transactionType"
direction="source" direction="source"
ref="sourceAccount"
v-on:selected-account="triggerNextAccount($event)"
/> />
</div> </div>
<!-- switcharoo! --> <!-- switcharoo! -->
@ -79,6 +81,7 @@
:destination-allowed-types="destinationAllowedTypes" :destination-allowed-types="destinationAllowedTypes"
:errors="transaction.errors.destination" :errors="transaction.errors.destination"
:index="index" :index="index"
ref="destinationAccount"
:transaction-type="transactionType" :transaction-type="transactionType"
:source-allowed-types="sourceAllowedTypes" :source-allowed-types="sourceAllowedTypes"
direction="destination" direction="destination"
@ -93,6 +96,7 @@
<!-- AMOUNT --> <!-- AMOUNT -->
<TransactionAmount <TransactionAmount
v-on="$listeners" v-on="$listeners"
ref="amount"
:amount="transaction.amount" :amount="transaction.amount"
:destination-currency-symbol="this.transaction.destination_account_currency_symbol" :destination-currency-symbol="this.transaction.destination_account_currency_symbol"
:errors="transaction.errors.amount" :errors="transaction.errors.amount"
@ -379,6 +383,13 @@ export default {
// console.log('Will remove transaction ' + this.index); // console.log('Will remove transaction ' + this.index);
this.$emit('remove-transaction', {index: this.index}); this.$emit('remove-transaction', {index: this.index});
}, },
triggerNextAccount: function(e) {
//alert(e);
if('source' === e) {
console.log('Jump to destination!');
this.$refs.destinationAccount.giveFocus();
}
}
}, },
computed: { computed: {
splitDate: function () { splitDate: function () {

View File

@ -37,6 +37,7 @@
:placeholder="$t('firefly.' + direction + '_account')" :placeholder="$t('firefly.' + direction + '_account')"
:serializer="item => item.name_with_balance" :serializer="item => item.name_with_balance"
:showOnFocus=true :showOnFocus=true
ref="inputThing"
aria-autocomplete="none" aria-autocomplete="none"
autocomplete="off" autocomplete="off"
@hit="userSelectedAccount" @hit="userSelectedAccount"
@ -119,6 +120,12 @@ export default {
getACURL: function (types, query) { getACURL: function (types, query) {
return './api/v1/autocomplete/accounts?types=' + types.join(',') + '&query=' + query; return './api/v1/autocomplete/accounts?types=' + types.join(',') + '&query=' + query;
}, },
giveFocus: function() {
console.log('I want focus! now OK: ' + this.direction + ' l: ' + this.accounts.length);
//console.log(this.$refs.inputThing.$refs.input.value);
this.$refs.inputThing.$refs.input.focus();
console.log(this.$refs.inputThing.isFocused);
},
userSelectedAccount: function (event) { userSelectedAccount: function (event) {
// console.log('userSelectedAccount!'); // console.log('userSelectedAccount!');
// console.log('To prevent invalid propogation, set selectedAccountTrigger = true'); // console.log('To prevent invalid propogation, set selectedAccountTrigger = true');
@ -205,10 +212,14 @@ export default {
currency_id: value.currency_id, currency_id: value.currency_id,
currency_code: value.currency_code, currency_code: value.currency_code,
currency_symbol: value.currency_symbol, currency_symbol: value.currency_symbol,
user_selected: true,
} }
// jump to next field somehow.
); );
//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; this.accountName = value.name;
} }
if (false === this.selectedAccountTrigger) { if (false === this.selectedAccountTrigger) {
//console.log('watch::selectedAccount() will NOT set accountName because selectedAccountTrigger = false'); //console.log('watch::selectedAccount() will NOT set accountName because selectedAccountTrigger = false');
@ -238,6 +249,7 @@ export default {
currency_id: null, currency_id: null,
currency_code: null, currency_code: null,
currency_symbol: null, currency_symbol: null,
user_selected: false
} }
); );
// this.account = {name: value, type: null, id: null, currency_id: null, currency_code: null, currency_symbol: null}; // this.account = {name: value, type: null, id: null, currency_id: null, currency_code: null, currency_symbol: null};

View File

@ -33,6 +33,7 @@
autocomplete="off" autocomplete="off"
name="amount[]" name="amount[]"
type="number" type="number"
ref="input"
step="any" step="any"
> >
</div> </div>
@ -71,7 +72,10 @@ export default {
methods: { methods: {
formatNumber(str) { formatNumber(str) {
return parseFloat(str).toFixed(this.fractionDigits); return parseFloat(str).toFixed(this.fractionDigits);
} },
giveFocus: function() {
this.$refs.input.focus();
},
}, },
data() { data() {
return { return {

View File

@ -53,9 +53,9 @@
</template> </template>
<template #cell(type)="data"> <template #cell(type)="data">
<span v-if="!data.item.dummy"> <span v-if="!data.item.dummy">
<span class="fas fa-long-arrow-alt-right" v-if="'deposit' === data.item.type"></span> <span class="fas fa-long-arrow-alt-right" v-if="'deposit' === data.item.type.toLowerCase()"></span>
<span class="fas fa-long-arrow-alt-left" v-else-if="'withdrawal' === data.item.type"></span> <span class="fas fa-long-arrow-alt-left" v-if="'withdrawal' === data.item.type.toLowerCase()"></span>
<span class="fas fa-long-arrows-alt-h" v-else-if="'transfer' === data.item.type"></span> <span class="fas fa-arrows-alt-h" v-if="'transfer' === data.item.type.toLowerCase()"></span>
</span> </span>
</template> </template>
<template #cell(description)="data"> <template #cell(description)="data">
@ -86,7 +86,7 @@
<span :class="'text-danger ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'withdrawal' === data.item.type"> <span :class="'text-danger ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'withdrawal' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(-data.item.amount) }} {{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(-data.item.amount) }}
</span> </span>
<span :class="'text-muted ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'transfer' === data.item.type"> <span :class="'text-muted ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'transfer' === data.item.type.toLowerCase()">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }} {{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }}
</span> </span>
<br /> <br />
@ -220,21 +220,26 @@ export default {
this.$emit('jump-page', {page: value}); this.$emit('jump-page', {page: value});
}, },
entries: function (value) { entries: function (value) {
console.log('detected new transactions!');
this.parseTransactions(); this.parseTransactions();
}, },
value: function(value) {
console.log('Watch value!');
}
}, },
methods: { methods: {
...mapMutations('root', ['refreshCacheKey',]), ...mapMutations('root', ['refreshCacheKey',]),
parseTransactions: function () { parseTransactions: function () {
this.transactions = [];
// console.log('Start of parseTransactions. Count of entries is ' + this.entries.length + ' and page is ' + this.page); // console.log('Start of parseTransactions. Count of entries is ' + this.entries.length + ' and page is ' + this.page);
// console.log('Reported total is ' + this.total); // console.log('Reported total is ' + this.total);
if (0 === this.entries.length) { if (0 === this.entries.length) {
// console.log('Will not render now'); console.log('Will not render now');
return; return;
} }
// console.log('Now have ' + this.transactions.length + ' transactions'); console.log('Now have ' + this.transactions.length + ' transactions');
for (let i = 0; i < this.total; i++) { for (let i = 0; i < this.total; i++) {
this.transactions.push({dummy: true}); this.transactions.push({dummy: true,type: 'x'});
// console.log('Push dummy to index ' + i); // console.log('Push dummy to index ' + i);
// console.log('Now have ' + this.transactions.length + ' transactions'); // console.log('Now have ' + this.transactions.length + ' transactions');
} }
@ -259,8 +264,9 @@ export default {
this.loading = false; this.loading = false;
}, },
newCacheKey: function () { newCacheKey: function () {
alert('TODO');
this.refreshCacheKey(); this.refreshCacheKey();
console.log('Cache key is now ' + this.cacheKey);
this.$emit('refreshed-cache-key');
}, },
updateFieldList: function () { updateFieldList: function () {
this.fields = [ this.fields = [
@ -314,6 +320,7 @@ export default {
let info = transaction.attributes.transactions[i]; let info = transaction.attributes.transactions[i];
let split = {}; let split = {};
row.amount = row.amount + parseFloat(info.amount); row.amount = row.amount + parseFloat(info.amount);
split.type = info.type;
split.description = info.description; split.description = info.description;
split.amount = info.amount; split.amount = info.amount;
split.currency_code = info.currency_code; split.currency_code = info.currency_code;

View File

@ -70,7 +70,7 @@
"daily_budgets": "Denn\u00ed rozpo\u010dty", "daily_budgets": "Denn\u00ed rozpo\u010dty",
"weekly_budgets": "T\u00fddenn\u00ed rozpo\u010dty", "weekly_budgets": "T\u00fddenn\u00ed rozpo\u010dty",
"monthly_budgets": "M\u011bs\u00ed\u010dn\u00ed rozpo\u010dty", "monthly_budgets": "M\u011bs\u00ed\u010dn\u00ed rozpo\u010dty",
"journals_in_period_for_account_js": "All transactions for account {title} between {start} and {end}", "journals_in_period_for_account_js": "Ve\u0161ker\u00e9 transakce pro \u00fa\u010det {title} mezi {start} a {end}",
"quarterly_budgets": "\u010ctvrtletn\u00ed rozpo\u010dty", "quarterly_budgets": "\u010ctvrtletn\u00ed rozpo\u010dty",
"create_new_expense": "Vytvo\u0159it v\u00fddajov\u00fd \u00fa\u010det", "create_new_expense": "Vytvo\u0159it v\u00fddajov\u00fd \u00fa\u010det",
"create_new_revenue": "Vytvo\u0159it nov\u00fd p\u0159\u00edjmov\u00fd \u00fa\u010det", "create_new_revenue": "Vytvo\u0159it nov\u00fd p\u0159\u00edjmov\u00fd \u00fa\u010det",

View File

@ -70,7 +70,7 @@
"daily_budgets": "Presupuestos diarios", "daily_budgets": "Presupuestos diarios",
"weekly_budgets": "Presupuestos semanales", "weekly_budgets": "Presupuestos semanales",
"monthly_budgets": "Presupuestos mensuales", "monthly_budgets": "Presupuestos mensuales",
"journals_in_period_for_account_js": "All transactions for account {title} between {start} and {end}", "journals_in_period_for_account_js": "Todas las transacciones de la cuenta {title} entre {start} y {end}",
"quarterly_budgets": "Presupuestos trimestrales", "quarterly_budgets": "Presupuestos trimestrales",
"create_new_expense": "Crear nueva cuenta de gastos", "create_new_expense": "Crear nueva cuenta de gastos",
"create_new_revenue": "Crear nueva cuenta de ingresos", "create_new_revenue": "Crear nueva cuenta de ingresos",
@ -142,27 +142,27 @@
"transaction_expand_split": "Expandir divisi\u00f3n", "transaction_expand_split": "Expandir divisi\u00f3n",
"transaction_collapse_split": "Colapsar divisi\u00f3n", "transaction_collapse_split": "Colapsar divisi\u00f3n",
"default_group_title_name": "(sin agrupaci\u00f3n)", "default_group_title_name": "(sin agrupaci\u00f3n)",
"bill_repeats_weekly": "Repeats weekly", "bill_repeats_weekly": "Repetir semanalmente",
"bill_repeats_monthly": "Repeats monthly", "bill_repeats_monthly": "Repetir mensualmente",
"bill_repeats_quarterly": "Repeats quarterly", "bill_repeats_quarterly": "Repite trimestralmente",
"bill_repeats_half-year": "Repeats every half year", "bill_repeats_half-year": "Repetir cada 6 meses",
"bill_repeats_yearly": "Repeats yearly", "bill_repeats_yearly": "Repetir anualmente",
"bill_repeats_weekly_other": "Repeats every other week", "bill_repeats_weekly_other": "Repetir cada dos semanas",
"bill_repeats_monthly_other": "Repeats every other month", "bill_repeats_monthly_other": "Repetir cada dos meses",
"bill_repeats_quarterly_other": "Repeats every other quarter", "bill_repeats_quarterly_other": "Repetir cada dos trimestres",
"bill_repeats_half-year_other": "Repeats yearly", "bill_repeats_half-year_other": "Repetir anualmente",
"bill_repeats_yearly_other": "Repeats every other year", "bill_repeats_yearly_other": "Repetir cada dos a\u00f1os",
"bill_repeats_weekly_skip": "Repeats every {skip} weeks", "bill_repeats_weekly_skip": "Repetir cada {skip} semanas",
"bill_repeats_monthly_skip": "Repeats every {skip} months", "bill_repeats_monthly_skip": "Repetir cada {skip} meses",
"bill_repeats_quarterly_skip": "Repeats every {skip} quarters", "bill_repeats_quarterly_skip": "Repetir cada {skip} trimestres",
"bill_repeats_half-year_skip": "Repeats every {skip} half years", "bill_repeats_half-year_skip": "Repetir cada {skip} medios a\u00f1os",
"bill_repeats_yearly_skip": "Repeats every {skip} years", "bill_repeats_yearly_skip": "Repetir cada {skip} a\u00f1os",
"not_expected_period": "No se espera en este per\u00edodo", "not_expected_period": "No se espera en este per\u00edodo",
"subscriptions": "Subscriptions", "subscriptions": "Suscripciones",
"bill_expected_date_js": "Expected {date}", "bill_expected_date_js": "Expected {date}",
"inactive": "Inactivo", "inactive": "Inactivo",
"forever": "Forever", "forever": "Siempre",
"extension_date_is": "Extension date is {date}", "extension_date_is": "Fecha de extensi\u00f3n es {date}",
"create_new_bill": "Crear nueva factura", "create_new_bill": "Crear nueva factura",
"store_new_bill": "Crear factura", "store_new_bill": "Crear factura",
"repeat_freq_yearly": "anualmente", "repeat_freq_yearly": "anualmente",
@ -170,13 +170,13 @@
"repeat_freq_quarterly": "trimestralmente", "repeat_freq_quarterly": "trimestralmente",
"repeat_freq_monthly": "mensualmente", "repeat_freq_monthly": "mensualmente",
"repeat_freq_weekly": "semanalmente", "repeat_freq_weekly": "semanalmente",
"credit_card_type_monthlyFull": "Full payment every month", "credit_card_type_monthlyFull": "Pago completo cada mes",
"update_liabilities_account": "Actualizar pasivo", "update_liabilities_account": "Actualizar pasivo",
"update_expense_account": "Actualizar cuenta de gastos", "update_expense_account": "Actualizar cuenta de gastos",
"update_revenue_account": "Actualizar cuenta de ingresos", "update_revenue_account": "Actualizar cuenta de ingresos",
"update_undefined_account": "Update account", "update_undefined_account": "Actualizar cuenta",
"update_asset_account": "Actualizar cuenta de activos", "update_asset_account": "Actualizar cuenta de activos",
"updated_account_js": "Updated account \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"." "updated_account_js": "Cuenta actualizada \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
}, },
"list": { "list": {
"piggy_bank": "Alcancilla", "piggy_bank": "Alcancilla",
@ -197,10 +197,10 @@
"liability_direction": "Pasivo entrada\/salida", "liability_direction": "Pasivo entrada\/salida",
"currentBalance": "Balance actual", "currentBalance": "Balance actual",
"next_expected_match": "Pr\u00f3xima coincidencia esperada", "next_expected_match": "Pr\u00f3xima coincidencia esperada",
"expected_info": "Next expected transaction", "expected_info": "Siguiente transacci\u00f3n esperada",
"start_date": "Start date", "start_date": "Fecha de inicio",
"end_date": "End date", "end_date": "Fecha fin",
"payment_info": "Payment information" "payment_info": "Informaci\u00f3n del pago"
}, },
"config": { "config": {
"html_language": "es", "html_language": "es",
@ -221,7 +221,7 @@
"repeat_freq": "Repetici\u00f3n", "repeat_freq": "Repetici\u00f3n",
"skip": "Saltar", "skip": "Saltar",
"startdate": "Fecha de inicio", "startdate": "Fecha de inicio",
"enddate": "End date", "enddate": "Fecha fin",
"object_group": "Grupo", "object_group": "Grupo",
"attachments": "Adjuntos", "attachments": "Adjuntos",
"active": "Activo", "active": "Activo",
@ -252,6 +252,6 @@
"amount_max": "Importe m\u00e1ximo", "amount_max": "Importe m\u00e1ximo",
"start_date": "Inicio del rango", "start_date": "Inicio del rango",
"end_date": "Final del rango", "end_date": "Final del rango",
"extension_date": "Extension date" "extension_date": "Fecha de extensi\u00f3n"
} }
} }

View File

@ -70,7 +70,7 @@
"daily_budgets": "Dagliga budgetar", "daily_budgets": "Dagliga budgetar",
"weekly_budgets": "Veckovis budgetar", "weekly_budgets": "Veckovis budgetar",
"monthly_budgets": "M\u00e5natliga budgetar", "monthly_budgets": "M\u00e5natliga budgetar",
"journals_in_period_for_account_js": "All transactions for account {title} between {start} and {end}", "journals_in_period_for_account_js": "Alla transaktioner f\u00f6r konto {title} mellan {start} och {end}",
"quarterly_budgets": "Kvartalsbudgetar", "quarterly_budgets": "Kvartalsbudgetar",
"create_new_expense": "Skapa ett nytt utgiftskonto", "create_new_expense": "Skapa ett nytt utgiftskonto",
"create_new_revenue": "Skapa ett nytt int\u00e4ktskonto", "create_new_revenue": "Skapa ett nytt int\u00e4ktskonto",
@ -174,9 +174,9 @@
"update_liabilities_account": "Uppdatera skuld", "update_liabilities_account": "Uppdatera skuld",
"update_expense_account": "Uppdatera utgiftskonto", "update_expense_account": "Uppdatera utgiftskonto",
"update_revenue_account": "Uppdatera int\u00e4ktskonto", "update_revenue_account": "Uppdatera int\u00e4ktskonto",
"update_undefined_account": "Update account", "update_undefined_account": "Uppdatera konto",
"update_asset_account": "Uppdatera tillg\u00e5ngskonto", "update_asset_account": "Uppdatera tillg\u00e5ngskonto",
"updated_account_js": "Updated account \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"." "updated_account_js": "Uppdaterade kontot \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
}, },
"list": { "list": {
"piggy_bank": "Spargris", "piggy_bank": "Spargris",

File diff suppressed because it is too large Load Diff

View File

@ -10,17 +10,17 @@
}, },
"devDependencies": { "devDependencies": {
"@johmun/vue-tags-input": "^2", "@johmun/vue-tags-input": "^2",
"@vue/compiler-sfc": "^3.2.19", "@vue/compiler-sfc": "^3.2.21",
"axios": "^0.22", "axios": "^0.24",
"bootstrap-sass": "^3", "bootstrap-sass": "^3",
"cross-env": "^7.0", "cross-env": "^7.0",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"jquery": "^3", "jquery": "^3",
"laravel-mix": "^6.0", "laravel-mix": "^6.0",
"postcss": "^8.3", "postcss": "^8.3",
"uiv": "^1.3", "uiv": "^1.4",
"vue": "^2.6", "vue": "^2.6",
"vue-i18n": "^8.25", "vue-i18n": "^8.26",
"vue-loader": "^15", "vue-loader": "^15",
"vue-template-compiler": "^2.6" "vue-template-compiler": "^2.6"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,6 +59,32 @@ function readCookie(name) {
return null; return null;
} }
function moveRuleGroup(e) {
let box = $(e.currentTarget);
var direction = box.data('direction');
var groupId = box.data('id');
$.post(moveRuleGroupUrl, {_token: token, direction: direction, id: groupId}).then(function () {
location.reload();
}).fail(function() {
alert('I failed :(');
});
return false;
}
function duplicateRule(e) {
let box = $(e.currentTarget);
var ruleId = box.data('id');
$.post(duplicateRuleUrl, {_token: token, id: ruleId}).then(function () {
location.reload();
}).fail(function() {
alert('I failed :(');
});
return false;
}
$(function () { $(function () {
"use strict"; "use strict";
@ -71,6 +97,9 @@ $(function () {
} }
); );
$('.move-group').click(moveRuleGroup);
$('.duplicate-rule').click(duplicateRule);
$('.rules-box').each(function (i, v) { $('.rules-box').each(function (i, v) {
var box = $(v); var box = $(v);
var groupId = box.data('group'); var groupId = box.data('group');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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