Merge branch 'release/5.6.0'

This commit is contained in:
James Cole 2021-09-17 15:19:52 +02:00
commit 1d02d86f9e
203 changed files with 4304 additions and 2747 deletions

View File

@ -0,0 +1,155 @@
<?php
/*
* CreateGroupMemberships.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Console\Commands\Upgrade;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole;
use FireflyIII\User;
use Illuminate\Console\Command;
use Log;
/**
* Class CreateGroupMemberships
*/
class CreateGroupMemberships extends Command
{
public const CONFIG_NAME = '560_create_group_memberships';
/**
* The console command description.
*
* @var string
*/
protected $description = 'SOME DESCRIPTION';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'firefly-iii:create-group-memberships {--F|force : Force the execution of this command.}';
/**
* Execute the console command.
*
* @return int
* @throws FireflyException
*/
public function handle(): int
{
$start = microtime(true);
if ($this->isExecuted() && true !== $this->option('force')) {
$this->warn('This command has already been executed.');
return 0;
}
$this->createGroupMemberships();
$this->markAsExecuted();
$end = round(microtime(true) - $start, 2);
$this->info(sprintf('in %s seconds.', $end));
return 0;
}
/**
* @return bool
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
*
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
Log::debug(sprintf('Manage group memberships for user #%d', $user->id));
if (!$this->hasGroupMembership($user)) {
Log::debug(sprintf('User #%d has no main group.', $user->id));
$this->createGroupMembership($user);
}
Log::debug(sprintf('Done with user #%d', $user->id));
}
}
/**
* @param User $user
*
* @return bool
*/
private function hasGroupMembership(User $user): bool
{
return $user->groupMemberships()->count() > 0;
}
/**
* @param User $user
*
* @throws FireflyException
*/
private function createGroupMembership(User $user): void
{
$userGroup = UserGroup::create(['title' => $user->email]);
$userRole = UserRole::where('title', UserRole::OWNER)->first();
if (null === $userRole) {
throw new FireflyException('Firefly III could not find a user role. Please make sure all validations have run.');
}
$membership = GroupMembership::create(
[
'user_id' => $user->id,
'user_role_id' => $userRole->id,
'user_group_id' => $userGroup->id,
]
);
if (null === $membership) {
throw new FireflyException('Firefly III could not create user group management object. Please make sure all validations have run.');
}
$user->user_group_id = $userGroup->id;
$user->save();
Log::debug(sprintf('User #%d now has main group.', $user->id));
}
/**
*
*/
private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
}
}

View File

@ -75,6 +75,7 @@ class UpgradeDatabase extends Command
'firefly-iii:migrate-tag-locations',
'firefly-iii:migrate-recurrence-type',
'firefly-iii:upgrade-liabilities',
'firefly-iii:create-group-memberships',
// there are 16 verify commands.
'firefly-iii:fix-piggies',

View File

@ -63,7 +63,7 @@ class APIEventHandler
// see if user has alternative email address:
$pref = app('preferences')->getForUser($user, 'remote_guard_alt_email');
if (null !== $pref) {
$email = $pref->data;
$email = (string)(is_array($pref->data) ? $email : $pref->data);
}
Log::debug(sprintf('Now in APIEventHandler::accessTokenCreated. Email is %s, IP is %s', $email, $ipAddress));

View File

@ -35,6 +35,9 @@ use FireflyIII\Mail\NewIPAddressWarningMail;
use FireflyIII\Mail\RegisteredUser as RegisteredUserMail;
use FireflyIII\Mail\RequestedNewPassword as RequestedNewPasswordMail;
use FireflyIII\Mail\UndoEmailChangeMail;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Illuminate\Auth\Events\Login;
@ -248,6 +251,34 @@ class UserEventHandler
return true;
}
/**
* @param RegisteredUser $event
*
* @return bool
* @throws FireflyException
*/
public function createGroupMembership(RegisteredUser $event): bool
{
$user = $event->user;
// create a new group.
$group = UserGroup::create(['title' => $user->email]);
$role = UserRole::where('title', UserRole::OWNER)->first();
if (null === $role) {
throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?');
}
GroupMembership::create(
[
'user_id' => $user->id,
'user_group_id' => $group->id,
'user_role_id' => $role->id,
]
);
$user->user_group_id = $group->id;
$user->save();
return true;
}
/**
* This method will send the user a registration mail, welcoming him or her to Firefly III.
* This message is only sent when the configuration of Firefly III says so.

View File

@ -767,4 +767,14 @@ class GroupCollector implements GroupCollectorInterface
return $groups;
}
/**
* @inheritDoc
*/
public function findNothing(): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', -1);
return $this;
}
}

View File

@ -57,6 +57,13 @@ interface GroupCollectorInterface
*/
public function amountLess(string $amount): GroupCollectorInterface;
/**
* Ensure the search will find nothing at all, zero results.
*
* @return GroupCollectorInterface
*/
public function findNothing(): GroupCollectorInterface;
/**
* Get transactions where the amount is more than.
*

View File

@ -137,10 +137,16 @@ class AvailableBudgetController extends Controller
*
* @return RedirectResponse|Redirector
*/
public function delete(AvailableBudget $availableBudget)
public function delete(Request $request)
{
$this->abRepository->destroyAvailableBudget($availableBudget);
session()->flash('success', trans('firefly.deleted_ab'));
$id = (int)$request->get('id');
if (0 !== $id) {
$availableBudget = $this->abRepository->findById($id);
if (null !== $availableBudget) {
$this->abRepository->destroyAvailableBudget($availableBudget);
session()->flash('success', trans('firefly.deleted_ab'));
}
}
return redirect(route('budgets.index'));
}

View File

@ -21,6 +21,7 @@
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Requests\CurrencyFormRequest;
use FireflyIII\Models\TransactionCurrency;
@ -41,7 +42,7 @@ use Log;
class CurrencyController extends Controller
{
protected CurrencyRepositoryInterface $repository;
protected UserRepositoryInterface $userRepository;
protected UserRepositoryInterface $userRepository;
/**
* CurrencyController constructor.
@ -54,7 +55,7 @@ class CurrencyController extends Controller
$this->middleware(
function ($request, $next) {
app('view')->share('title', (string) trans('firefly.currencies'));
app('view')->share('title', (string)trans('firefly.currencies'));
app('view')->share('mainTitleIcon', 'fa-usd');
$this->repository = app(CurrencyRepositoryInterface::class);
$this->userRepository = app(UserRepositoryInterface::class);
@ -63,6 +64,7 @@ class CurrencyController extends Controller
}
);
}
/**
* Create a currency.
*
@ -75,13 +77,13 @@ class CurrencyController extends Controller
/** @var User $user */
$user = auth()->user();
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
return redirect(route('currencies.index'));
}
$subTitleIcon = 'fa-plus';
$subTitle = (string) trans('firefly.create_currency');
$subTitle = (string)trans('firefly.create_currency');
// put previous url in session if not redirect from store (not "create another").
if (true !== session('currencies.create.fromStore')) {
@ -102,15 +104,23 @@ class CurrencyController extends Controller
*
* @return RedirectResponse|Redirector
*/
public function defaultCurrency(Request $request, TransactionCurrency $currency)
public function defaultCurrency(Request $request)
{
app('preferences')->set('currencyPreference', $currency->code);
app('preferences')->mark();
$currencyId = (int)$request->get('id');
if ($currencyId > 0) {
// valid currency?
$currency = $this->repository->find($currencyId);
if (null !== $currency) {
app('preferences')->set('currencyPreference', $currency->code);
app('preferences')->mark();
Log::channel('audit')->info(sprintf('Make %s the default currency.', $currency->code));
Log::channel('audit')->info(sprintf('Make %s the default currency.', $currency->code));
$this->repository->enable($currency);
$request->session()->flash('success', (string)trans('firefly.new_default_currency', ['name' => $currency->name]));
$this->repository->enable($currency);
$request->session()->flash('success', (string) trans('firefly.new_default_currency', ['name' => $currency->name]));
return redirect(route('currencies.index'));
}
}
return redirect(route('currencies.index'));
}
@ -129,7 +139,7 @@ class CurrencyController extends Controller
$user = auth()->user();
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
Log::channel('audit')->info(sprintf('Tried to visit page to delete currency %s but is not site owner.', $currency->code));
return redirect(route('currencies.index'));
@ -138,7 +148,7 @@ class CurrencyController extends Controller
if ($this->repository->currencyInUse($currency)) {
$location = $this->repository->currencyInUseAt($currency);
$message = (string) trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
$message = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
$request->session()->flash('error', $message);
Log::channel('audit')->info(sprintf('Tried to visit page to delete currency %s but currency is in use.', $currency->code));
@ -147,7 +157,7 @@ class CurrencyController extends Controller
// put previous url in session
$this->rememberPreviousUri('currencies.delete.uri');
$subTitle = (string) trans('form.delete_currency', ['name' => $currency->name]);
$subTitle = (string)trans('form.delete_currency', ['name' => $currency->name]);
Log::channel('audit')->info(sprintf('Visit page to delete currency %s.', $currency->code));
return prefixView('currencies.delete', compact('currency', 'subTitle'));
@ -167,7 +177,7 @@ class CurrencyController extends Controller
$user = auth()->user();
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
Log::channel('audit')->info(sprintf('Tried to delete currency %s but is not site owner.', $currency->code));
return redirect(route('currencies.index'));
@ -175,14 +185,14 @@ class CurrencyController extends Controller
}
if ($this->repository->currencyInUse($currency)) {
$request->session()->flash('error', (string) trans('firefly.cannot_delete_currency', ['name' => e($currency->name)]));
$request->session()->flash('error', (string)trans('firefly.cannot_delete_currency', ['name' => e($currency->name)]));
Log::channel('audit')->info(sprintf('Tried to delete currency %s but is in use.', $currency->code));
return redirect(route('currencies.index'));
}
if ($this->repository->isFallbackCurrency($currency)) {
$request->session()->flash('error', (string) trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)]));
$request->session()->flash('error', (string)trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)]));
Log::channel('audit')->info(sprintf('Tried to delete currency %s but is FALLBACK.', $currency->code));
return redirect(route('currencies.index'));
@ -191,7 +201,7 @@ class CurrencyController extends Controller
Log::channel('audit')->info(sprintf('Deleted currency %s.', $currency->code));
$this->repository->destroy($currency);
$request->session()->flash('success', (string) trans('firefly.deleted_currency', ['name' => $currency->name]));
$request->session()->flash('success', (string)trans('firefly.deleted_currency', ['name' => $currency->name]));
return redirect($this->getPreviousUri('currencies.delete.uri'));
}
@ -200,8 +210,8 @@ class CurrencyController extends Controller
* @param Request $request
* @param TransactionCurrency $currency
*
* @throws FireflyException
* @return RedirectResponse|Redirector
* @throws FireflyException
*/
public function disableCurrency(Request $request, TransactionCurrency $currency)
{
@ -211,7 +221,7 @@ class CurrencyController extends Controller
$user = auth()->user();
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
Log::channel('audit')->info(sprintf('Tried to disable currency %s but is not site owner.', $currency->code));
return redirect(route('currencies.index'));
@ -221,7 +231,7 @@ class CurrencyController extends Controller
if ($this->repository->currencyInUse($currency)) {
$location = $this->repository->currencyInUseAt($currency);
$message = (string) trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
$message = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
$request->session()->flash('error', $message);
Log::channel('audit')->info(sprintf('Tried to disable currency %s but is in use.', $currency->code));
@ -245,10 +255,10 @@ class CurrencyController extends Controller
}
if ('EUR' === $currency->code) {
session()->flash('warning', (string) trans('firefly.disable_EUR_side_effects'));
session()->flash('warning', (string)trans('firefly.disable_EUR_side_effects'));
}
session()->flash('success', (string) trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
session()->flash('success', (string)trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
return redirect(route('currencies.index'));
}
@ -267,7 +277,7 @@ class CurrencyController extends Controller
$user = auth()->user();
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
Log::channel('audit')->info(sprintf('Tried to edit currency %s but is not owner.', $currency->code));
return redirect(route('currencies.index'));
@ -275,13 +285,13 @@ class CurrencyController extends Controller
}
$subTitleIcon = 'fa-pencil';
$subTitle = (string) trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
$subTitle = (string)trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
$currency->symbol = htmlentities($currency->symbol);
// code to handle active-checkboxes
$hasOldInput = null !== $request->old('_token');
$preFilled = [
'enabled' => $hasOldInput ? (bool) $request->old('enabled') : $currency->enabled,
'enabled' => $hasOldInput ? (bool)$request->old('enabled') : $currency->enabled,
];
$request->session()->flash('preFilled', $preFilled);
@ -306,7 +316,7 @@ class CurrencyController extends Controller
app('preferences')->mark();
$this->repository->enable($currency);
session()->flash('success', (string) trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
session()->flash('success', (string)trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
Log::channel('audit')->info(sprintf('Enabled currency %s.', $currency->code));
return redirect(route('currencies.index'));
@ -323,8 +333,8 @@ class CurrencyController extends Controller
{
/** @var User $user */
$user = auth()->user();
$page = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
$page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$collection = $this->repository->getAll();
$total = $collection->count();
$collection = $collection->slice(($page - 1) * $pageSize, $pageSize);
@ -334,12 +344,13 @@ class CurrencyController extends Controller
$defaultCurrency = $this->repository->getCurrencyByPreference(app('preferences')->get('currencyPreference', config('firefly.default_currency', 'EUR')));
$isOwner = true;
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('info', (string) trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')]));
$request->session()->flash('info', (string)trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')]));
$isOwner = false;
}
return prefixView('currencies.index', compact('currencies', 'defaultCurrency', 'isOwner'));
}
/**
* Store new currency.
*
@ -367,15 +378,15 @@ class CurrencyController extends Controller
} catch (FireflyException $e) {
Log::error($e->getMessage());
Log::channel('audit')->info('Could not store (POST) currency without admin rights.', $data);
$request->session()->flash('error', (string) trans('firefly.could_not_store_currency'));
$request->session()->flash('error', (string)trans('firefly.could_not_store_currency'));
$currency = null;
}
$redirect = redirect($this->getPreviousUri('currencies.create.uri'));
if (null !== $currency) {
$request->session()->flash('success', (string) trans('firefly.created_currency', ['name' => $currency->name]));
$request->session()->flash('success', (string)trans('firefly.created_currency', ['name' => $currency->name]));
Log::channel('audit')->info('Created (POST) currency.', $data);
if (1 === (int) $request->get('create_another')) {
if (1 === (int)$request->get('create_another')) {
$request->session()->put('currencies.create.fromStore', true);
@ -386,6 +397,7 @@ class CurrencyController extends Controller
return $redirect;
}
/**
* Updates a currency.
*
@ -405,7 +417,7 @@ class CurrencyController extends Controller
}
if (!$this->userRepository->hasRole($user, 'owner')) {
$request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
$request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
Log::channel('audit')->info('Tried to update (POST) currency without admin rights.', $data);
return redirect(route('currencies.index'));
@ -413,10 +425,10 @@ class CurrencyController extends Controller
}
$currency = $this->repository->update($currency, $data);
Log::channel('audit')->info('Updated (POST) currency.', $data);
$request->session()->flash('success', (string) trans('firefly.updated_currency', ['name' => $currency->name]));
$request->session()->flash('success', (string)trans('firefly.updated_currency', ['name' => $currency->name]));
app('preferences')->mark();
if (1 === (int) $request->get('return_to_edit')) {
if (1 === (int)$request->get('return_to_edit')) {
$request->session()->put('currencies.edit.fromUpdate', true);

View File

@ -28,6 +28,7 @@ use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\User;

View File

@ -86,6 +86,7 @@ class InstallController extends Controller
'firefly-iii:migrate-tag-locations' => [],
'firefly-iii:migrate-recurrence-type' => [],
'firefly-iii:upgrade-liabilities' => [],
'firefly-iii:create-group-memberships' => [],
// verify commands
'firefly-iii:fix-piggies' => [],
@ -111,7 +112,7 @@ class InstallController extends Controller
// final command to set latest version in DB
'firefly-iii:set-latest-version' => ['--james-is-cool' => true],
'firefly-iii:verify-security-alerts' => [],
'firefly-iii:verify-security-alerts' => [],
];
$this->lastError = '';

View File

@ -39,8 +39,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class Account
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int $user_id
@ -89,16 +89,16 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|Account withTrashed()
* @method static Builder|Account withoutTrashed()
* @mixin Eloquent
* @property Carbon $lastActivityDate
* @property string $startBalance
* @property string $endBalance
* @property string $difference
* @property string $interest
* @property string $interestPeriod
* @property string $accountTypeString
* @property string $location
* @property string $liability_direction
* @property string $current_debt
* @property Carbon $lastActivityDate
* @property string $startBalance
* @property string $endBalance
* @property string $difference
* @property string $interest
* @property string $interestPeriod
* @property string $accountTypeString
* @property string $location
* @property string $liability_direction
* @property string $current_debt
*/
class Account extends Model
{
@ -149,11 +149,12 @@ class Account extends Model
}
/**
* Get all of the tags for the post.
* @return BelongsTo
* @codeCoverageIgnore
*/
public function objectGroups()
public function accountType(): BelongsTo
{
return $this->morphToMany(ObjectGroup::class, 'object_groupable');
return $this->belongsTo(AccountType::class);
}
/**
@ -165,24 +166,6 @@ class Account extends Model
return $this->morphMany(Attachment::class, 'attachable');
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function accountMeta(): HasMany
{
return $this->hasMany(AccountMeta::class);
}
/**
* @return BelongsTo
* @codeCoverageIgnore
*/
public function accountType(): BelongsTo
{
return $this->belongsTo(AccountType::class);
}
/**
* Get the account number.
*
@ -198,6 +181,15 @@ class Account extends Model
return $metaValue ? $metaValue->data : '';
}
/**
* @return HasMany
* @codeCoverageIgnore
*/
public function accountMeta(): HasMany
{
return $this->hasMany(AccountMeta::class);
}
/**
* @return string
* @codeCoverageIgnore
@ -231,6 +223,14 @@ class Account extends Model
return $this->morphMany(Note::class, 'noteable');
}
/**
* Get all of the tags for the post.
*/
public function objectGroups()
{
return $this->morphToMany(ObjectGroup::class, 'object_groupable');
}
/**
* @return HasMany
* @codeCoverageIgnore

View File

@ -27,6 +27,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use JsonException;
/**
* Class AccountMeta
@ -79,7 +80,7 @@ class AccountMeta extends Model
* @param mixed $value
*
* @return mixed
* @throws \JsonException
* @throws JsonException
* @codeCoverageIgnore
*/
public function getDataAttribute($value)

View File

@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\AccountType
*
@ -74,7 +75,7 @@ class AccountType extends Model
public const MORTGAGE = 'Mortgage';
/** @var string */
public const CREDITCARD = 'Credit card';
/** @var string */
/** @var string */
public const LIABILITY_CREDIT = 'Liability credit account';
/**
* The attributes that should be casted to native types.

View File

@ -37,12 +37,12 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Attachment
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $attachable_id
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $attachable_id
* @property string $attachable_type
* @property bool $file_exists
* @property string $md5

View File

@ -22,27 +22,27 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\AvailableBudget
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $transaction_currency_id
* @property string $amount
* @property \Illuminate\Support\Carbon $start_date
* @property \Illuminate\Support\Carbon $end_date
* @property Carbon $start_date
* @property Carbon $end_date
* @property-read TransactionCurrency $transactionCurrency
* @property-read User $user
* @method static \Illuminate\Database\Eloquent\Builder|AvailableBudget newModelQuery()
@ -65,6 +65,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class AvailableBudget extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*
@ -87,13 +88,13 @@ class AvailableBudget extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return AvailableBudget
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): AvailableBudget
{
if (auth()->check()) {
$availableBudgetId = (int) $value;
$availableBudgetId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var AvailableBudget $availableBudget */

View File

@ -39,9 +39,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* FireflyIII\Models\Budget
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property string $name
* @property bool $active
@ -74,7 +74,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|Budget withTrashed()
* @method static Builder|Budget withoutTrashed()
* @mixin Eloquent
* @property string $email
* @property string $email
*/
class Budget extends Model
{

View File

@ -22,23 +22,23 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\BudgetLimit
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $budget_id
* @property int|null $transaction_currency_id
* @property \Illuminate\Support\Carbon $start_date
* @property \Illuminate\Support\Carbon|null $end_date
* @property Carbon $start_date
* @property Carbon|null $end_date
* @property string $amount
* @property string $spent
* @property string|null $period

View File

@ -96,13 +96,13 @@ class Category extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return Category
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): Category
{
if (auth()->check()) {
$categoryId = (int) $value;
$categoryId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var Category $category */
@ -114,14 +114,6 @@ class Category extends Model
throw new NotFoundHttpException;
}
/**
* @codeCoverageIgnore
* @return BelongsToMany
*/
public function transactionJournals(): BelongsToMany
{
return $this->belongsToMany(TransactionJournal::class, 'category_transaction_journal', 'category_id');
}
/**
* @codeCoverageIgnore
* @return MorphMany
@ -140,6 +132,15 @@ class Category extends Model
return $this->morphMany(Note::class, 'noteable');
}
/**
* @codeCoverageIgnore
* @return BelongsToMany
*/
public function transactionJournals(): BelongsToMany
{
return $this->belongsToMany(TransactionJournal::class, 'category_transaction_journal', 'category_id');
}
/**
* @codeCoverageIgnore
* @return BelongsToMany

View File

@ -31,12 +31,12 @@ use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\Configuration
*
* @property int $id
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property string $name
* @property mixed $data
* @property string $name
* @property mixed $data
* @method static \Illuminate\Database\Eloquent\Builder|Configuration newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Configuration newQuery()
* @method static Builder|Configuration onlyTrashed()
@ -70,7 +70,8 @@ class Configuration extends Model
protected $table = 'configuration';
/**
* See reference nr. 17
* See reference nr. 17
*
* @codeCoverageIgnore
*
* @param mixed $value

View File

@ -22,24 +22,24 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\CurrencyExchangeRate
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $deleted_at
* @property int $user_id
* @property int $from_currency_id
* @property int $to_currency_id
* @property \Illuminate\Support\Carbon $date
* @property Carbon $date
* @property string $rate
* @property string|null $user_rate
* @property-read TransactionCurrency $fromCurrency

View File

@ -0,0 +1,61 @@
<?php
/*
* GroupMembership.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class GroupMembership
*/
class GroupMembership extends Model
{
protected $fillable = ['user_id', 'user_group_id', 'user_role_id'];
/**
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* @return BelongsTo
*/
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class);
}
/**
* @return BelongsTo
*/
public function userRole(): BelongsTo
{
return $this->belongsTo(UserRole::class);
}
}

View File

@ -22,21 +22,22 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\LinkType
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property string $name
* @property string $outward
* @property string $inward
@ -63,6 +64,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class LinkType extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*
@ -84,14 +86,14 @@ class LinkType extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return LinkType
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): LinkType
{
if (auth()->check()) {
$linkTypeId = (int) $value;
$linkTypeId = (int)$value;
$linkType = self::find($linkTypeId);
if (null !== $linkType) {
return $linkType;

View File

@ -23,6 +23,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\Location
*

View File

@ -23,6 +23,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
@ -32,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\ObjectGroup
*
@ -63,8 +65,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
*/
class ObjectGroup extends Model
{
protected $fillable = ['title', 'order', 'user_id'];
/**
* The attributes that should be casted to native types.
*
@ -77,13 +77,36 @@ class ObjectGroup extends Model
'user_id' => 'integer',
'deleted_at' => 'datetime',
];
protected $fillable = ['title', 'order', 'user_id'];
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*
* @param string $value
*
* @return ObjectGroup
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): ObjectGroup
{
if (auth()->check()) {
$objectGroupId = (int)$value;
/** @var ObjectGroup $objectGroup */
$objectGroup = self::where('object_groups.id', $objectGroupId)
->where('object_groups.user_id', auth()->user()->id)->first();
if (null !== $objectGroup) {
return $objectGroup;
}
}
throw new NotFoundHttpException;
}
/**
* @return MorphToMany
*/
public function piggyBanks()
public function accounts()
{
return $this->morphedByMany(PiggyBank::class, 'object_groupable');
return $this->morphedByMany(Account::class, 'object_groupable');
}
/**
@ -97,31 +120,9 @@ class ObjectGroup extends Model
/**
* @return MorphToMany
*/
public function accounts()
public function piggyBanks()
{
return $this->morphedByMany(Account::class, 'object_groupable');
}
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*
* @param string $value
*
* @throws NotFoundHttpException
* @return ObjectGroup
*/
public static function routeBinder(string $value): ObjectGroup
{
if (auth()->check()) {
$objectGroupId = (int) $value;
/** @var ObjectGroup $objectGroup */
$objectGroup = self::where('object_groups.id', $objectGroupId)
->where('object_groups.user_id', auth()->user()->id)->first();
if (null !== $objectGroup) {
return $objectGroup;
}
}
throw new NotFoundHttpException;
return $this->morphedByMany(PiggyBank::class, 'object_groupable');
}
/**

View File

@ -22,7 +22,6 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
@ -31,19 +30,21 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\PiggyBank
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $account_id
* @property string $name
* @property string $targetamount
* @property \Illuminate\Support\Carbon|null $startdate
* @property \Illuminate\Support\Carbon|null $targetdate
* @property Carbon|null $startdate
* @property Carbon|null $targetdate
* @property int $order
* @property bool $active
* @property bool $encrypted
@ -108,13 +109,13 @@ class PiggyBank extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return PiggyBank
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): PiggyBank
{
if (auth()->check()) {
$piggyBankId = (int) $value;
$piggyBankId = (int)$value;
$piggyBank = self::where('piggy_banks.id', $piggyBankId)
->leftJoin('accounts', 'accounts.id', '=', 'piggy_banks.account_id')
->where('accounts.user_id', auth()->user()->id)->first(['piggy_banks.*']);
@ -125,23 +126,6 @@ class PiggyBank extends Model
throw new NotFoundHttpException;
}
/**
* Get all of the tags for the post.
*/
public function objectGroups()
{
return $this->morphToMany(ObjectGroup::class, 'object_groupable');
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
/**
* @codeCoverageIgnore
* @return BelongsTo
@ -151,6 +135,15 @@ class PiggyBank extends Model
return $this->belongsTo(Account::class);
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
/**
* @codeCoverageIgnore
* Get all of the piggy bank's notes.
@ -160,6 +153,14 @@ class PiggyBank extends Model
return $this->morphMany(Note::class, 'noteable');
}
/**
* Get all of the tags for the post.
*/
public function objectGroups()
{
return $this->morphToMany(ObjectGroup::class, 'object_groupable');
}
/**
* @codeCoverageIgnore
* @return HasMany
@ -185,6 +186,6 @@ class PiggyBank extends Model
*/
public function setTargetamountAttribute($value): void
{
$this->attributes['targetamount'] = (string) $value;
$this->attributes['targetamount'] = (string)$value;
}
}

View File

@ -22,20 +22,21 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\PiggyBankEvent
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $piggy_bank_id
* @property int|null $transaction_journal_id
* @property \Illuminate\Support\Carbon $date
* @property Carbon $date
* @property string $amount
* @property PiggyBank $piggyBank
* @property-read TransactionJournal|null $transactionJournal
@ -85,7 +86,7 @@ class PiggyBankEvent extends Model
*/
public function setAmountAttribute($value): void
{
$this->attributes['amount'] = (string) $value;
$this->attributes['amount'] = (string)$value;
}
/**

View File

@ -31,14 +31,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* FireflyIII\Models\PiggyBankRepetition
*
* @property int $id
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property int $piggy_bank_id
* @property int $piggy_bank_id
* @property \Illuminate\Support\Carbon|null $startdate
* @property \Illuminate\Support\Carbon|null $targetdate
* @property string $currentamount
* @property-read PiggyBank $piggyBank
* @property string $currentamount
* @property-read PiggyBank $piggyBank
* @method static EloquentBuilder|PiggyBankRepetition newModelQuery()
* @method static EloquentBuilder|PiggyBankRepetition newQuery()
* @method static EloquentBuilder|PiggyBankRepetition onDates(Carbon $start, Carbon $target)
@ -124,6 +124,6 @@ class PiggyBankRepetition extends Model
*/
public function setCurrentamountAttribute($value): void
{
$this->attributes['currentamount'] = (string) $value;
$this->attributes['currentamount'] = (string)$value;
}
}

View File

@ -33,13 +33,13 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Preference
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $user_id
* @property string $name
* @property int|string|array|null $data
* @property-read User $user
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $user_id
* @property string $name
* @property int|string|array|null $data
* @property-read User $user
* @method static Builder|Preference newModelQuery()
* @method static Builder|Preference newQuery()
* @method static Builder|Preference query()

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Collection;
@ -37,10 +38,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Recurrence
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $transaction_type_id
* @property string $title
@ -89,6 +90,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Recurrence extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*
@ -120,13 +122,13 @@ class Recurrence extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return Recurrence
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): Recurrence
{
if (auth()->check()) {
$recurrenceId = (int) $value;
$recurrenceId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var Recurrence $recurrence */
@ -138,6 +140,15 @@ class Recurrence extends Model
throw new NotFoundHttpException;
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
/**
* @codeCoverageIgnore
* Get all of the notes.
@ -183,15 +194,6 @@ class Recurrence extends Model
return $this->belongsTo(TransactionCurrency::class);
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
/**
* @codeCoverageIgnore
* @return BelongsTo

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -58,6 +59,7 @@ use Illuminate\Support\Carbon;
class RecurrenceMeta extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*

View File

@ -22,20 +22,21 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\RecurrenceRepetition
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $recurrence_id
* @property string $repetition_type
* @property string $repetition_moment
@ -70,6 +71,7 @@ class RecurrenceRepetition extends Model
/** @var int */
public const WEEKEND_TO_MONDAY = 4;
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*

View File

@ -22,20 +22,21 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
/**
* FireflyIII\Models\RecurrenceTransaction
*
* @property int $id
* @property Carbon|null $created_at
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $recurrence_id
@ -49,7 +50,7 @@ use Illuminate\Support\Collection;
* @property-read Account $destinationAccount
* @property-read TransactionCurrency|null $foreignCurrency
* @property-read Recurrence $recurrence
* @property-read \Illuminate\Database\Eloquent\Collection|RecurrenceTransactionMeta[] $recurrenceTransactionMeta
* @property-read Collection|RecurrenceTransactionMeta[] $recurrenceTransactionMeta
* @property-read int|null $recurrence_transaction_meta_count
* @property-read Account $sourceAccount
* @property-read TransactionCurrency $transactionCurrency
@ -79,6 +80,7 @@ use Illuminate\Support\Collection;
class RecurrenceTransaction extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*
@ -153,6 +155,7 @@ class RecurrenceTransaction extends Model
{
return $this->belongsTo(TransactionCurrency::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -58,6 +59,7 @@ use Illuminate\Support\Carbon;
class RecurrenceTransactionMeta extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*

View File

@ -36,25 +36,25 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Rule
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $rule_group_id
* @property string $title
* @property string|null $description
* @property int $order
* @property bool $active
* @property bool $stop_processing
* @property bool $strict
* @property-read string $action_value
* @property-read Collection|RuleAction[] $ruleActions
* @property-read int|null $rule_actions_count
* @property-read RuleGroup $ruleGroup
* @property Collection|RuleTrigger[] $ruleTriggers
* @property-read int|null $rule_triggers_count
* @property-read User $user
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $rule_group_id
* @property string $title
* @property string|null $description
* @property int $order
* @property bool $active
* @property bool $stop_processing
* @property bool $strict
* @property-read string $action_value
* @property-read Collection|RuleAction[] $ruleActions
* @property-read int|null $rule_actions_count
* @property-read RuleGroup $ruleGroup
* @property Collection|RuleTrigger[] $ruleTriggers
* @property-read int|null $rule_triggers_count
* @property-read User $user
* @method static \Illuminate\Database\Eloquent\Builder|Rule newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Rule newQuery()
* @method static Builder|Rule onlyTrashed()

View File

@ -22,18 +22,18 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\RuleAction
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $rule_id
* @property string $action_type
* @property string $action_value

View File

@ -22,30 +22,31 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\RuleGroup
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property string $title
* @property string|null $description
* @property int $order
* @property bool $active
* @property bool $stop_processing
* @property \Illuminate\Database\Eloquent\Collection|Rule[] $rules
* @property Collection|Rule[] $rules
* @property-read int|null $rules_count
* @property-read User $user
* @method static \Illuminate\Database\Eloquent\Builder|RuleGroup newModelQuery()
@ -69,6 +70,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RuleGroup extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*
@ -92,13 +94,13 @@ class RuleGroup extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return RuleGroup
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): RuleGroup
{
if (auth()->check()) {
$ruleGroupId = (int) $value;
$ruleGroupId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var RuleGroup $ruleGroup */

View File

@ -22,18 +22,18 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* FireflyIII\Models\RuleTrigger
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $rule_id
* @property string $trigger_type
* @property string $trigger_value

View File

@ -35,33 +35,33 @@ use Illuminate\Database\Eloquent\SoftDeletes;
/**
* FireflyIII\Models\Transaction
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property bool $reconciled
* @property int $account_id
* @property int $transaction_journal_id
* @property string|null $description
* @property int|null $transaction_currency_id
* @property string $modified
* @property string $modified_foreign
* @property string $date
* @property string $max_date
* @property string $amount
* @property string|null $foreign_amount
* @property int|null $foreign_currency_id
* @property int $identifier
* @property-read \FireflyIII\Models\Account $account
* @property-read Collection|\FireflyIII\Models\Budget[] $budgets
* @property-read int|null $budgets_count
* @property-read Collection|\FireflyIII\Models\Category[] $categories
* @property-read int|null $categories_count
* @property-read \FireflyIII\Models\TransactionCurrency|null $foreignCurrency
* @property-read \FireflyIII\Models\TransactionCurrency|null $transactionCurrency
* @property-read \FireflyIII\Models\TransactionJournal $transactionJournal
* @method static Builder|Transaction after(\Carbon\Carbon $date)
* @method static Builder|Transaction before(\Carbon\Carbon $date)
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property bool $reconciled
* @property int $account_id
* @property int $transaction_journal_id
* @property string|null $description
* @property int|null $transaction_currency_id
* @property string $modified
* @property string $modified_foreign
* @property string $date
* @property string $max_date
* @property string $amount
* @property string|null $foreign_amount
* @property int|null $foreign_currency_id
* @property int $identifier
* @property-read Account $account
* @property-read Collection|Budget[] $budgets
* @property-read int|null $budgets_count
* @property-read Collection|Category[] $categories
* @property-read int|null $categories_count
* @property-read TransactionCurrency|null $foreignCurrency
* @property-read TransactionCurrency|null $transactionCurrency
* @property-read TransactionJournal $transactionJournal
* @method static Builder|Transaction after(Carbon $date)
* @method static Builder|Transaction before(Carbon $date)
* @method static Builder|Transaction newModelQuery()
* @method static Builder|Transaction newQuery()
* @method static \Illuminate\Database\Query\Builder|Transaction onlyTrashed()
@ -83,11 +83,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Query\Builder|Transaction withTrashed()
* @method static \Illuminate\Database\Query\Builder|Transaction withoutTrashed()
* @mixin Eloquent
* @property int $the_count
* @property int $the_count
*/
class Transaction extends Model
{
use SoftDeletes, HasFactory;
/**
* The attributes that should be casted to native types.
*
@ -110,30 +111,6 @@ class Transaction extends Model
/** @var array Hidden from view */
protected $hidden = ['encrypted'];
/**
* Check if a table is joined.
*
* @param Builder $query
* @param string $table
*
* @return bool
* @codeCoverageIgnore
*/
public static function isJoined(Builder $query, string $table): bool
{
$joins = $query->getQuery()->joins;
if (null === $joins) {
return false;
}
foreach ($joins as $join) {
if ($join->table === $table) {
return true;
}
}
return false;
}
/**
* Get the account this object belongs to.
*
@ -194,6 +171,30 @@ class Transaction extends Model
$query->where('transaction_journals.date', '>=', $date->format('Y-m-d 00:00:00'));
}
/**
* Check if a table is joined.
*
* @param Builder $query
* @param string $table
*
* @return bool
* @codeCoverageIgnore
*/
public static function isJoined(Builder $query, string $table): bool
{
$joins = $query->getQuery()->joins;
if (null === $joins) {
return false;
}
foreach ($joins as $join) {
if ($join->table === $table) {
return true;
}
}
return false;
}
/**
* Check for transactions BEFORE the specified date.
*
@ -235,7 +236,7 @@ class Transaction extends Model
*/
public function setAmountAttribute($value): void
{
$this->attributes['amount'] = (string) $value;
$this->attributes['amount'] = (string)$value;
}
/**

View File

@ -22,21 +22,22 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\TransactionCurrency
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property bool $enabled
* @property string $code
* @property string $name
@ -90,13 +91,13 @@ class TransactionCurrency extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return TransactionCurrency
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): TransactionCurrency
{
if (auth()->check()) {
$currencyId = (int) $value;
$currencyId = (int)$value;
$currency = self::find($currencyId);
if (null !== $currency) {
return $currency;

View File

@ -32,6 +32,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\TransactionGroup
*
@ -85,13 +86,13 @@ class TransactionGroup extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return TransactionGroup
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): TransactionGroup
{
if (auth()->check()) {
$groupId = (int) $value;
$groupId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var TransactionGroup $group */

View File

@ -28,6 +28,7 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -40,52 +41,52 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\TransactionJournal
*
* @property int $id
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property \Carbon\Carbon|null $deleted_at
* @property int $user_id
* @property int $transaction_type_id
* @property int|null $transaction_group_id
* @property int|null $bill_id
* @property int|null $transaction_currency_id
* @property string $description
* @property \Carbon\Carbon $date
* @property \Carbon\Carbon|null $interest_date
* @property \Carbon\Carbon|null $book_date
* @property \Carbon\Carbon|null $process_date
* @property int $order
* @property int $tag_count
* @property string $transaction_type_type
* @property bool $encrypted
* @property bool $completed
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Attachment[] $attachments
* @property-read int|null $attachments_count
* @property-read \FireflyIII\Models\Bill|null $bill
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Budget[] $budgets
* @property-read int|null $budgets_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Category[] $categories
* @property-read int|null $categories_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalLink[] $destJournalLinks
* @property-read int|null $dest_journal_links_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Note[] $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\PiggyBankEvent[] $piggyBankEvents
* @property-read int|null $piggy_bank_events_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalLink[] $sourceJournalLinks
* @property-read int|null $source_journal_links_count
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Tag[] $tags
* @property-read int|null $tags_count
* @property-read \FireflyIII\Models\TransactionCurrency|null $transactionCurrency
* @property-read \FireflyIII\Models\TransactionGroup|null $transactionGroup
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\TransactionJournalMeta[] $transactionJournalMeta
* @property-read int|null $transaction_journal_meta_count
* @property-read \FireflyIII\Models\TransactionType $transactionType
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Transaction[] $transactions
* @property-read int|null $transactions_count
* @property-read User $user
* @method static EloquentBuilder|TransactionJournal after(\Carbon\Carbon $date)
* @method static EloquentBuilder|TransactionJournal before(\Carbon\Carbon $date)
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property int $user_id
* @property int $transaction_type_id
* @property int|null $transaction_group_id
* @property int|null $bill_id
* @property int|null $transaction_currency_id
* @property string $description
* @property Carbon $date
* @property Carbon|null $interest_date
* @property Carbon|null $book_date
* @property Carbon|null $process_date
* @property int $order
* @property int $tag_count
* @property string $transaction_type_type
* @property bool $encrypted
* @property bool $completed
* @property-read Collection|Attachment[] $attachments
* @property-read int|null $attachments_count
* @property-read Bill|null $bill
* @property-read Collection|Budget[] $budgets
* @property-read int|null $budgets_count
* @property-read Collection|Category[] $categories
* @property-read int|null $categories_count
* @property-read Collection|TransactionJournalLink[] $destJournalLinks
* @property-read int|null $dest_journal_links_count
* @property-read Collection|Note[] $notes
* @property-read int|null $notes_count
* @property-read Collection|PiggyBankEvent[] $piggyBankEvents
* @property-read int|null $piggy_bank_events_count
* @property-read Collection|TransactionJournalLink[] $sourceJournalLinks
* @property-read int|null $source_journal_links_count
* @property-read Collection|Tag[] $tags
* @property-read int|null $tags_count
* @property-read TransactionCurrency|null $transactionCurrency
* @property-read TransactionGroup|null $transactionGroup
* @property-read Collection|TransactionJournalMeta[] $transactionJournalMeta
* @property-read int|null $transaction_journal_meta_count
* @property-read TransactionType $transactionType
* @property-read Collection|Transaction[] $transactions
* @property-read int|null $transactions_count
* @property-read User $user
* @method static EloquentBuilder|TransactionJournal after(Carbon $date)
* @method static EloquentBuilder|TransactionJournal before(Carbon $date)
* @method static EloquentBuilder|TransactionJournal newModelQuery()
* @method static EloquentBuilder|TransactionJournal newQuery()
* @method static \Illuminate\Database\Query\Builder|TransactionJournal onlyTrashed()
@ -112,9 +113,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static \Illuminate\Database\Query\Builder|TransactionJournal withTrashed()
* @method static \Illuminate\Database\Query\Builder|TransactionJournal withoutTrashed()
* @mixin Eloquent
* @property-read \Illuminate\Database\Eloquent\Collection|\FireflyIII\Models\Location[] $locations
* @property-read int|null $locations_count
* @property int $the_count
* @property-read Collection|Location[] $locations
* @property-read int|null $locations_count
* @property int $the_count
*/
class TransactionJournal extends Model
{
@ -147,31 +148,6 @@ class TransactionJournal extends Model
/** @var array Hidden from view */
protected $hidden = ['encrypted'];
/**
* Checks if tables are joined.
*
* @codeCoverageIgnore
*
* @param Builder $query
* @param string $table
*
* @return bool
*/
public static function isJoined(Builder $query, string $table): bool
{
$joins = $query->getQuery()->joins;
if (null === $joins) {
return false;
}
foreach ($joins as $join) {
if ($join->table === $table) {
return true;
}
}
return false;
}
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*
@ -215,15 +191,6 @@ class TransactionJournal extends Model
return $this->belongsTo(Bill::class);
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function locations(): MorphMany
{
return $this->morphMany(Location::class, 'locatable');
}
/**
* @codeCoverageIgnore
* @return BelongsToMany
@ -264,6 +231,15 @@ class TransactionJournal extends Model
return $this->transactionType->isTransfer();
}
/**
* @codeCoverageIgnore
* @return MorphMany
*/
public function locations(): MorphMany
{
return $this->morphMany(Location::class, 'locatable');
}
/**
* @codeCoverageIgnore
* Get all of the notes.
@ -324,6 +300,31 @@ class TransactionJournal extends Model
}
}
/**
* Checks if tables are joined.
*
* @codeCoverageIgnore
*
* @param Builder $query
* @param string $table
*
* @return bool
*/
public static function isJoined(Builder $query, string $table): bool
{
$joins = $query->getQuery()->joins;
if (null === $joins) {
return false;
}
foreach ($joins as $join) {
if ($join->table === $table) {
return true;
}
}
return false;
}
/**
* @codeCoverageIgnore
* @return HasMany

View File

@ -22,21 +22,21 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\TransactionJournalLink
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property int $link_type_id
* @property int $source_id
* @property int $destination_id
@ -80,14 +80,14 @@ class TransactionJournalLink extends Model
*
* @param string $value
*
* @throws NotFoundHttpException
* @return TransactionJournalLink
*
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): TransactionJournalLink
{
if (auth()->check()) {
$linkId = (int) $value;
$linkId = (int)$value;
$link = self::where('journal_links.id', $linkId)
->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id')
->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id')

View File

@ -60,6 +60,7 @@ use Illuminate\Support\Carbon;
class TransactionJournalMeta extends Model
{
use SoftDeletes;
/**
* The attributes that should be casted to native types.
*

46
app/Models/UserGroup.php Normal file
View File

@ -0,0 +1,46 @@
<?php
/*
* UserGroup.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class UserGroup
*/
class UserGroup extends Model
{
protected $fillable = ['title'];
/**
* @codeCoverageIgnore
*
* @return HasMany
*/
public function groupMemberships(): HasMany
{
return $this->hasMany(GroupMembership::class);
}
}

54
app/Models/UserRole.php Normal file
View File

@ -0,0 +1,54 @@
<?php
/*
* UserRole.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class UserRole
*/
class UserRole extends Model
{
public const READ_ONLY = 'ro';
public const CHANGE_TRANSACTIONS = 'change_tx';
public const CHANGE_RULES = 'change_rules';
public const CHANGE_PIGGY_BANKS = 'change_piggies';
public const CHANGE_REPETITIONS = 'change_reps';
public const VIEW_REPORTS = 'view_reports';
public const FULL = 'full';
public const OWNER = 'owner';
protected $fillable = ['title'];
/**
* @codeCoverageIgnore
*
* @return HasMany
*/
public function groupMemberships(): HasMany
{
return $this->hasMany(GroupMembership::class);
}
}

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
@ -36,7 +37,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\Webhook
*
* @property int $id
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
@ -66,8 +67,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static \Illuminate\Database\Query\Builder|Webhook withTrashed()
* @method static \Illuminate\Database\Query\Builder|Webhook withoutTrashed()
* @mixin Eloquent
* @property string $title
* @property string $secret
* @property string $title
* @property string $secret
* @method static Builder|Webhook whereSecret($value)
* @method static Builder|Webhook whereTitle($value)
*/
@ -88,9 +89,6 @@ class Webhook extends Model
// delivery
public const DELIVERY_JSON = 300;
protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'url', 'title', 'secret'];
protected $casts
= [
'active' => 'boolean',
@ -98,6 +96,7 @@ class Webhook extends Model
'response' => 'integer',
'delivery' => 'integer',
];
protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'url', 'title', 'secret'];
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).

View File

@ -64,15 +64,6 @@ class WebhookAttempt extends Model
{
use SoftDeletes;
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function webhookMessage(): BelongsTo
{
return $this->belongsTo(WebhookMessage::class);
}
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*
@ -95,4 +86,13 @@ class WebhookAttempt extends Model
}
throw new NotFoundHttpException;
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function webhookMessage(): BelongsTo
{
return $this->belongsTo(WebhookMessage::class);
}
}

View File

@ -36,7 +36,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* FireflyIII\Models\WebhookMessage
*
* @property int $id
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $deleted_at

View File

@ -68,6 +68,7 @@ class EventServiceProvider extends ServiceProvider
RegisteredUser::class => [
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail',
'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole',
'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership',
],
// is a User related event.
Login::class => [

View File

@ -79,6 +79,14 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
}
/**
* @inheritDoc
*/
public function findById(int $id): ?AvailableBudget
{
return $this->user->availableBudgets->find($id);
}
/**
* Return a list of all available budgets (in all currencies) (for the selected period).
*

View File

@ -56,6 +56,13 @@ interface AvailableBudgetRepositoryInterface
*/
public function find(TransactionCurrency $currency, Carbon $start, Carbon $end): ?AvailableBudget;
/**
* @param int $id
*
* @return AvailableBudget|null
*/
public function findById(int $id): ?AvailableBudget;
/**
* Return a list of all available budgets (in all currencies) (for the selected period).
*

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Repositories\Budget;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
@ -88,6 +89,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
)
->where('budget_limits.transaction_currency_id', $currency->id)
->whereNull('budgets.deleted_at')
->where('budgets.active', true)
->where('budgets.user_id', $this->user->id);
if (null !== $budgets && $budgets->count() > 0) {
$query->whereIn('budget_limits.budget_id', $budgets->pluck('id')->toArray());
@ -424,6 +426,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface
} catch (Exception $e) { // @phpstan-ignore-line
// @ignoreException
}
return null;
}
// update if exists:

View File

@ -117,6 +117,18 @@ class BudgetRepository implements BudgetRepositoryInterface
}
}
/**
* Find a budget or return NULL
*
* @param int|null $budgetId |null
*
* @return Budget|null
*/
public function find(int $budgetId = null): ?Budget
{
return $this->user->budgets()->find($budgetId);
}
/**
* @param int|null $budgetId
* @param string|null $budgetName
@ -157,18 +169,6 @@ class BudgetRepository implements BudgetRepositoryInterface
return $this->user->budgets()->where('name', 'LIKE', $query)->first();
}
/**
* Find a budget or return NULL
*
* @param int|null $budgetId |null
*
* @return Budget|null
*/
public function find(int $budgetId = null): ?Budget
{
return $this->user->budgets()->find($budgetId);
}
/**
* This method returns the oldest journal or transaction date known to this budget.
* Will cache result.

View File

@ -56,6 +56,14 @@ interface BudgetRepositoryInterface
*/
public function destroyAutoBudget(Budget $budget): void;
/**
*
* @param int|null $budgetId
*
* @return Budget|null
*/
public function find(int $budgetId = null): ?Budget;
/**
* @param int|null $budgetId
* @param string|null $budgetName
@ -73,14 +81,6 @@ interface BudgetRepositoryInterface
*/
public function findByName(?string $name): ?Budget;
/**
*
* @param int|null $budgetId
*
* @return Budget|null
*/
public function find(int $budgetId = null): ?Budget;
/**
* This method returns the oldest journal or transaction date known to this budget.
* Will cache result.

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Repositories\Budget;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionCurrency;
@ -146,8 +147,8 @@ class NoBudgetRepository implements NoBudgetRepositoryInterface
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* See reference nr. 15
* See reference nr. 16
* See reference nr. 15
* See reference nr. 16
*
* @param Carbon $start
* @param Carbon $end

View File

@ -22,6 +22,7 @@
declare(strict_types=1);
namespace FireflyIII\Repositories\Budget;
use Carbon\Carbon;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;

View File

@ -180,7 +180,7 @@ class OperationsRepository implements OperationsRepositoryInterface
// add journal to array:
// only a subset of the fields.
$journalId = (int)$journal['transaction_journal_id'];
$journalId = (int)$journal['transaction_journal_id'];
$array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = [
'amount' => app('steam')->negative($journal['amount']),
'destination_account_id' => $journal['destination_account_id'],
@ -227,7 +227,7 @@ class OperationsRepository implements OperationsRepositoryInterface
if ($accounts->count() > 0) {
$collector->setAccounts($accounts);
}
// See reference nr. 13
// See reference nr. 13
$set = $collector->getGroups();
$return = [];
$total = [];
@ -343,23 +343,13 @@ class OperationsRepository implements OperationsRepositoryInterface
$array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->negative($journal['foreign_amount']));
}
}
return $array;
}
/**
* @return Collection
*/
private function getBudgets(): Collection
{
/** @var BudgetRepositoryInterface $repos */
$repos = app(BudgetRepositoryInterface::class);
return $repos->getActiveBudgets();
}
/**
* For now, simply refer to whichever repository holds this function.
* See reference nr. 14
* See reference nr. 14
*
* @param Budget $budget
* @param Carbon|null $start
@ -374,4 +364,15 @@ class OperationsRepository implements OperationsRepositoryInterface
return $blRepository->getBudgetLimits($budget, $start, $end);
}
/**
* @return Collection
*/
private function getBudgets(): Collection
{
/** @var BudgetRepositoryInterface $repos */
$repos = app(BudgetRepositoryInterface::class);
return $repos->getActiveBudgets();
}
}

View File

@ -43,6 +43,7 @@ interface OperationsRepositoryInterface
* @return string
*/
public function budgetedPerDay(Budget $budget): string;
/**
* @param Collection $budgets
* @param Collection $accounts

View File

@ -88,14 +88,15 @@ trait ModifiesPiggyBanks
*/
public function canAddAmount(PiggyBank $piggyBank, string $amount): bool
{
$leftOnAccount = $this->leftOnAccount($piggyBank, today(config('app.timezone')));
$today = today(config('app.timezone'));
$leftOnAccount = $this->leftOnAccount($piggyBank, $today);
$savedSoFar = (string)$this->getRepetition($piggyBank)->currentamount;
$leftToSave = bcsub($piggyBank->targetamount, $savedSoFar);
$maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
$compare = bccomp($amount, $maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Left on account: %s', $leftOnAccount));
Log::debug(sprintf('Left on account: %s on %s', $leftOnAccount, $today->format('Y-m-d')));
Log::debug(sprintf('Saved so far: %s', $savedSoFar));
Log::debug(sprintf('Left to save: %s', $leftToSave));
Log::debug(sprintf('Maximum amount: %s', $maxAmount));

View File

@ -345,6 +345,10 @@ class OperatorQuerySearch implements SearchInterface
if (null !== $account) {
$this->collector->setSourceAccounts(new Collection([$account]));
}
if (null === $account) {
// since the source does not exist, cannot return results:
$this->collector->findNothing();
}
break;
case 'journal_id':
$parts = explode(',', $value);
@ -383,6 +387,9 @@ class OperatorQuerySearch implements SearchInterface
if (null !== $account) {
$this->collector->setDestinationAccounts(new Collection([$account]));
}
if (null === $account) {
$this->collector->findNothing();
}
break;
case 'account_id':
$parts = explode(',', $value);
@ -396,6 +403,9 @@ class OperatorQuerySearch implements SearchInterface
if ($collection->count() > 0) {
$this->collector->setAccounts($collection);
}
if (0 === $collection->count()) {
$this->collector->findNothing();
}
break;
//
// cash account
@ -436,12 +446,18 @@ class OperatorQuerySearch implements SearchInterface
if (null !== $currency) {
$this->collector->setCurrency($currency);
}
if (null === $currency) {
$this->collector->findNothing();
}
break;
case 'foreign_currency_is':
$currency = $this->findCurrency($value);
if (null !== $currency) {
$this->collector->setForeignCurrency($currency);
}
if (null === $currency) {
$this->collector->findNothing();
}
break;
//
// attachments
@ -463,6 +479,9 @@ class OperatorQuerySearch implements SearchInterface
if ($result->count() > 0) {
$this->collector->setCategories($result);
}
if (0 === $result->count()) {
$this->collector->findNothing();
}
break;
//
// budgets
@ -478,6 +497,9 @@ class OperatorQuerySearch implements SearchInterface
if ($result->count() > 0) {
$this->collector->setBudgets($result);
}
if (0 === $result->count()) {
$this->collector->findNothing();
}
break;
//
// bill
@ -493,6 +515,9 @@ class OperatorQuerySearch implements SearchInterface
if ($result->count() > 0) {
$this->collector->setBills($result);
}
if (0 === $result->count()) {
$this->collector->findNothing();
}
break;
//
// tags
@ -508,6 +533,11 @@ class OperatorQuerySearch implements SearchInterface
if ($result->count() > 0) {
$this->collector->setTags($result);
}
// no tags found means search must result in nothing.
if (0 === $result->count()) {
Log::info(sprintf('No valid tags in "%s"-operator, so search will not return ANY results.', $operator));
$this->collector->findNothing();
}
break;
//
// notes
@ -698,10 +728,8 @@ class OperatorQuerySearch implements SearchInterface
// get accounts:
$accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero accounts, search for invalid account.');
$account = new Account;
$account->id = 0;
$this->collector->$collectorMethod(new Collection([$account]));
Log::debug('Found zero accounts, search for non existing account, NO results will be returned.');
$this->collector->findNothing();
return;
}
@ -713,10 +741,8 @@ class OperatorQuerySearch implements SearchInterface
);
if (0 === $filtered->count()) {
Log::debug('Left with zero accounts, search for invalid account.');
$account = new Account;
$account->id = 0;
$this->collector->$collectorMethod(new Collection([$account]));
Log::debug('Left with zero accounts, so cannot find anything, NO results will be returned.');
$this->collector->findNothing();
return;
}
@ -764,9 +790,7 @@ class OperatorQuerySearch implements SearchInterface
$accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 25);
if (0 === $accounts->count()) {
Log::debug('Found zero accounts, search for invalid account.');
$account = new Account;
$account->id = 0;
$this->collector->$collectorMethod(new Collection([$account]));
$this->collector->findNothing();
return;
}
@ -791,9 +815,7 @@ class OperatorQuerySearch implements SearchInterface
if (0 === $filtered->count()) {
Log::debug('Left with zero, search for invalid account');
$account = new Account;
$account->id = 0;
$this->collector->$collectorMethod(new Collection([$account]));
$this->collector->findNothing();
return;
}

View File

@ -57,12 +57,18 @@ class UpdatePiggybank implements ActionInterface
public function actOnArray(array $journal): bool
{
Log::debug(sprintf('Triggered rule action UpdatePiggybank on journal #%d', $journal['transaction_journal_id']));
// refresh the transaction type.
$user = User::find($journal['user_id']);
$journalObj = $user->transactionJournals()->find($journal['transaction_journal_id']);
$type = TransactionType::find((int)$journalObj->transaction_type_id);
$journal['transaction_type_type'] = $type->type;
if (TransactionType::TRANSFER !== $journal['transaction_type_type']) {
Log::info(sprintf('Journal #%d is a "%s" so skip this action.', $journal['transaction_journal_id'], $journal['transaction_type_type']));
return false;
}
$user = User::find($journal['user_id']);
$piggyBank = $this->findPiggybank($user);
if (null === $piggyBank) {
@ -92,7 +98,7 @@ class UpdatePiggybank implements ActionInterface
return true;
}
Log::info('Piggy bank is not linked to source or destination, so no action will be taken.');
Log::info(sprintf('Piggy bank is not linked to source ("#%d") or destination ("#%d"), so no action will be taken.', $source->account_id, $destination->account_id));
return true;
}

View File

@ -34,6 +34,7 @@ use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\ObjectGroup;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Preference;
@ -45,8 +46,10 @@ use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\Webhook;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
@ -136,14 +139,14 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method static Builder|User whereObjectguid($value)
* @property string|null $provider
* @method static Builder|User whereProvider($value)
* @property-read \Illuminate\Database\Eloquent\Collection|ObjectGroup[] $objectGroups
* @property-read int|null $object_groups_count
* @property-read \Illuminate\Database\Eloquent\Collection|Webhook[] $webhooks
* @property-read int|null $webhooks_count
* @property string|null $two_factor_secret
* @property string|null $two_factor_recovery_codes
* @property string|null $guid
* @property string|null $domain
* @property-read \Illuminate\Database\Eloquent\Collection|ObjectGroup[] $objectGroups
* @property-read int|null $object_groups_count
* @property-read \Illuminate\Database\Eloquent\Collection|Webhook[] $webhooks
* @property-read int|null $webhooks_count
* @property string|null $two_factor_secret
* @property string|null $two_factor_recovery_codes
* @property string|null $guid
* @property string|null $domain
* @method static Builder|User whereDomain($value)
* @method static Builder|User whereGuid($value)
* @method static Builder|User whereTwoFactorRecoveryCodes($value)
@ -212,6 +215,16 @@ class User extends Authenticatable
return $this->hasMany(Account::class);
}
/**
* @codeCoverageIgnore
*
* @return HasMany
*/
public function groupMemberships(): HasMany
{
return $this->hasMany(GroupMembership::class)->with(['userGroup','userRole']);
}
/**
* @codeCoverageIgnore
* Link to attachments
@ -300,6 +313,14 @@ class User extends Authenticatable
return $this->hasMany(Category::class);
}
/**
* @codeCoverageIgnore
* @return BelongsTo
*/
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class,);
}
/**
* @codeCoverageIgnore
* Link to currency exchange rates
@ -449,6 +470,7 @@ class User extends Authenticatable
}
// start LDAP related code
/**
* Get the database column name of the domain.
*

View File

@ -28,7 +28,6 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionType;
use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@ -43,6 +42,7 @@ use Google2FA;
use Illuminate\Support\Collection;
use Illuminate\Validation\Validator;
use Log;
use ValueError;
use function is_string;
/**
@ -114,6 +114,7 @@ class FireflyValidator extends Validator
* @param mixed $value
*
* @return bool
* @throws FireflyException
*/
public function validateIban($attribute, $value): bool
{
@ -168,11 +169,15 @@ class FireflyValidator extends Validator
"\u{202F}", // narrow no-break space
"\u{3000}", // ideographic space
"\u{FEFF}", // zero width no -break space
'-',
'?'
];
$replace = '';
$value = str_replace($search, $replace, $value);
$value = strtoupper($value);
// replace characters outside of ASCI range.
$value = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
$search = [' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
$replace = ['', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31',
'32', '33', '34', '35',];
@ -185,7 +190,14 @@ class FireflyValidator extends Validator
if (0 === strlen($iban)) {
return false;
}
$checksum = bcmod($iban, '97');
try {
$checksum = bcmod($iban, '97');
} catch (ValueError $e) {
$message = sprintf('Could not validate IBAN check value "%s" (IBAN "%s")', $iban, $value);
Log::error($message);
Log::error($e->getTraceAsString());
return false;
}
return 1 === (int)$checksum;
}
@ -471,12 +483,13 @@ class FireflyValidator extends Validator
$ignore = (int)($parameters[0] ?? 0.0);
$accountTypeIds = $accountTypes->pluck('id')->toArray();
/** @var Collection $set */
$set = auth()->user()->accounts()->whereIn('account_type_id', $accountTypeIds)->where('id', '!=', $ignore)->get();
$set = auth()->user()->accounts()->whereIn('account_type_id', $accountTypeIds)->where('id', '!=', $ignore)->get();
$result = $set->first(
function (Account $account) use ($value) {
return $account->name === $value;
}
);
return null === $result;
}
@ -500,6 +513,7 @@ class FireflyValidator extends Validator
return $account->name === $value;
}
);
return null === $result;
}
@ -717,7 +731,8 @@ class FireflyValidator extends Validator
if (null !== $exclude) {
$query->where('piggy_banks.id', '!=', (int)$exclude);
}
$query->where('piggy_banks.name',$value);
$query->where('piggy_banks.name', $value);
return null === $query->first(['piggy_banks.*']);
}

View File

@ -2,38 +2,55 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 5.6.0 - 2021-xx-xx
## 5.6.0 - 2021-09-17
⚠️ This release features new LDAP libraries. Your mileage may vary. Make sure you back up everything. Firefly III may accidentally create a new account for you instead of reusing the old one. There is no option for LDAP filters yet.
Please refer to the [documentation](https://docs.firefly-iii.org/firefly-iii/) and support channels if you run into problems:
- [Gitter.im](https://gitter.im/firefly-iii/firefly-iii)
- [Twitter](https://twitter.com/Firefly_III/)
- [GitHub Issues](https://github.com/firefly-iii/firefly-iii/issues)
- [GitHub Discussions](https://github.com/firefly-iii/firefly-iii/discussions)
### Added
- A few new pages for the new v2 layout. Thanks @alex6480!
- Added a new currency yay!
- You can now manage loans and debts a little better.
- Added a new currency, thanks @kasperkls02!
- You can now manage loans and debts a little better, see also the documentation for help.
- Some screenshots are now in the GitHub repository for better management, thanks @Flightkick!
- @LBreda has added a service worker and updated icons, thanks!
### Changed
- @hoshsadiq has added all PHP requirements to the composer file, thanks!
- A better cache routine for layout v2 pages.
- All LDAP libraries have been upgrade.
### Deprecated
- Initial release.
- All LDAP libraries have been upgraded.
- New issue templates and help text for easier GitHub support.
- First preparations for multi-administration and group membership options.
- The search will return nothing if you submit invalid values, instead of everything.
### Removed
- All telemetry options have been removed.
### Fixed
- [Issue 4894](https://github.com/firefly-iii/firefly-iii/issues/4894) Bad number comparison
- [Issue 4987](https://github.com/firefly-iii/firefly-iii/issues/4987) Budgeted amount includes inactive budgets
- [Issue 4988](https://github.com/firefly-iii/firefly-iii/issues/4988) Can't select liability account from imported transactions
- #5042 HTTP 500 when creating Personal Access Token or OAuth Client
- Various Sonarqube issues, thanks @hazma-fadil!
- Correct menu display, thanks @vonsogt!
- The IBAN validator will filter special characters.
- In some cases, piggy banks would report being full or empty while this was not actually the case.
- Various other bugs and minor issues.
### Security
- Feature to be able to rebuild Docker images and show security warnings in new builds.
- [CVE-2021-3663](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3663) A missing rate limiter makes brute-forcing the login easy.
- It also fixes [CVE-2021-3728](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3728), [CVE-2021-3729](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3729) and [CVE-2021-3730](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3730), all variations of the same security vulnerability: some actions in Firefly III were vulnerable to CSRF.
### API
- You can disable webhooks with an extra field in API submissions.
- There is a static cron token (see `.env.example`) which is useful for Docker.
- A better endpoint to move transactions around, see [api-docs.firefly-iii.org](https://api-docs.firefly-iii.org).
## 5.5.13 - 2021-07-25

View File

@ -74,14 +74,18 @@
"ext-curl": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-pdo": "*",
"ext-session": "*",
"ext-simplexml": "*",
"ext-sodium": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"bacon/bacon-qr-code": "2.*",
"diglactic/laravel-breadcrumbs": "^7.0",
"doctrine/dbal": "3.*",
@ -173,6 +177,7 @@
"@php artisan firefly-iii:migrate-tag-locations",
"@php artisan firefly-iii:migrate-recurrence-type",
"@php artisan firefly-iii:upgrade-liabilities",
"@php artisan firefly-iii:create-group-memberships",
"@php artisan firefly-iii:fix-piggies",
"@php artisan firefly-iii:create-link-types",

602
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -101,9 +101,9 @@ return [
'webhooks' => true,
'handle_debts' => true,
],
'version' => '5.6.0-alpha.2',
'version' => '5.6.0',
'api_version' => '1.5.3',
'db_version' => 17,
'db_version' => 18,
// generic settings
'maxUploadSize' => 1073741824, // 1 GB

View File

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

40
config/user_roles.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/*
* user_roles.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
use FireflyIII\Models\UserRole;
return [
'roles' => [
UserRole::READ_ONLY => [],
UserRole::CHANGE_TRANSACTIONS => [],
UserRole::CHANGE_RULES => [],
UserRole::CHANGE_PIGGY_BANKS => [],
UserRole::CHANGE_REPETITIONS => [],
UserRole::VIEW_REPORTS => [],
UserRole::FULL => [],
UserRole::OWNER => [],
],
];

View File

@ -0,0 +1,148 @@
<?php
/*
* 2021_08_28_073733_user_groups.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Class UserGroups
*/
class UserGroups extends Migration
{
private array $tables
= ['accounts', 'attachments', 'available_budgets', 'bills', 'budgets', 'categories', 'recurrences', 'rule_groups', 'rules', 'tags',
'transaction_groups', 'transaction_journals', 'webhooks'];
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// remove columns from tables
/** @var string $tableName */
foreach ($this->tables as $tableName) {
Schema::table(
$tableName, function (Blueprint $table) use ($tableName) {
$table->dropForeign(sprintf('%s_to_ugi', $tableName));
if (Schema::hasColumn($tableName, 'user_group_id')) {
$table->dropColumn('user_group_id');
}
}
);
}
Schema::table(
'users', function (Blueprint $table) {
$table->dropForeign('type_user_group_id');
if (Schema::hasColumn('users', 'user_group_id')) {
$table->dropColumn('user_group_id');
}
}
);
Schema::dropIfExists('group_memberships');
Schema::dropIfExists('user_roles');
Schema::dropIfExists('user_groups');
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
/*
* user is a member of a user_group through a user_group_role
* may have multiple roles in a group
*/
Schema::create(
'user_groups', static function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->softDeletes();
$table->string('title', 255);
$table->unique('title');
}
);
Schema::create(
'user_roles', static function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->softDeletes();
$table->string('title', 255);
$table->unique('title');
}
);
Schema::create(
'group_memberships',
static function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->softDeletes();
$table->integer('user_id', false, true);
$table->bigInteger('user_group_id', false, true);
$table->bigInteger('user_role_id', false, true);
$table->foreign('user_id')->references('id')->on('users')->onUpdate('cascade')->onDelete('cascade');
$table->foreign('user_group_id')->references('id')->on('user_groups')->onUpdate('cascade')->onDelete('cascade');
$table->foreign('user_role_id')->references('id')->on('user_roles')->onUpdate('cascade')->onDelete('cascade');
$table->unique(['user_id', 'user_group_id', 'user_role_id']);
}
);
Schema::table(
'users', function (Blueprint $table) {
if (!Schema::hasColumn('users', 'user_group_id')) {
$table->bigInteger('user_group_id', false, true)->nullable();
$table->foreign('user_group_id', 'type_user_group_id')->references('id')->on('user_groups')->onDelete('set null')->onUpdate('cascade');
}
}
);
// ADD columns from tables
/** @var string $tableName */
foreach ($this->tables as $tableName) {
Schema::table(
$tableName, function (Blueprint $table) use ($tableName) {
if (!Schema::hasColumn($tableName, 'user_group_id')) {
$table->bigInteger('user_group_id', false, true)->nullable()->after('user_id');
$table->foreign('user_group_id', sprintf('%s_to_ugi', $tableName))->references('id')->on('user_groups')->onDelete('set null')->onUpdate('cascade');
}
}
);
}
}
}

View File

@ -40,5 +40,6 @@ class DatabaseSeeder extends Seeder
$this->call(PermissionSeeder::class);
$this->call(LinkTypeSeeder::class);
$this->call(ConfigSeeder::class);
$this->call(UserRoleSeeder::class);
}
}

View File

@ -0,0 +1,63 @@
<?php
/*
* UserRoleSeeder.php
* Copyright (c) 2021 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace Database\Seeders;
use FireflyIII\Models\UserRole;
use Illuminate\Database\Seeder;
use PDOEXception;
/**
* Class UserRoleSeeder
*/
class UserRoleSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$roles = [
UserRole::READ_ONLY,
UserRole::CHANGE_TRANSACTIONS,
UserRole::CHANGE_RULES,
UserRole::CHANGE_PIGGY_BANKS,
UserRole::CHANGE_REPETITIONS,
UserRole::VIEW_REPORTS,
UserRole::FULL,
UserRole::OWNER,
];
/** @var string $role */
foreach ($roles as $role) {
try {
UserRole::create(['title' => $role]);
} catch (PDOException $e) {
// @ignoreException
}
}
}
}

View File

@ -17,13 +17,13 @@
"lodash.clonedeep": "^4.5.0",
"postcss": "^8.1.14",
"resolve-url-loader": "^4.0.0",
"sass": "^1.37.0",
"sass": "^1.39.2",
"sass-loader": "^12.0.0",
"vue-i18n": "^8.24.2",
"vue-loader": "^15",
"vue-template-compiler": "^2.6.12",
"vuex": "^3.6.2",
"webpack": "^5.40.0"
"webpack": "^5.52.1"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",

View File

@ -279,8 +279,8 @@ export default {
// console.log('end of finaliseSubmission');
},
handleSubmissionError: function (errors) {
console.log('Bad');
console.log(errors);
console.error('Bad');
console.error(errors);
this.inError = true;
this.submitting = false;
this.errors = lodashClonedeep(this.defaultErrors);
@ -320,12 +320,12 @@ export default {
this.parseAccount(response.data);
}
).catch(error => {
console.log('I failed :(');
console.log(error);
console.error('I failed :(');
console.error(error);
});
},
storeField: function (payload) {
console.log(payload);
//console.log(payload);
if ('location' === payload.field) {
if (true === payload.value.hasMarker) {
this.account.location = payload.value;

View File

@ -20,13 +20,145 @@
<template>
<div>
I am a show
<div class="row">
<div class="col-lg-12 col-md-6 col-sm-12 col-xs-12">
<!-- Custom Tabs -->
<div class="card">
<div class="card-header d-flex p-0">
<h3 class="card-title p-3">Tabs</h3>
<ul class="nav nav-pills ml-auto p-2">
<li class="nav-item"><a class="nav-link active" href="#main_chart" data-toggle="tab">Chart</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>
</ul>
</div><!-- /.card-header -->
<div class="card-body">
<div class="tab-content">
<div class="tab-pane active" id="main_chart">
1: main chart
</div>
<!-- /.tab-pane -->
<div class="tab-pane" id="budgets">
2: tree map from/to budget
</div>
<!-- /.tab-pane -->
<div class="tab-pane" id="categories">
2: tree map from/to cat
</div>
<!-- /.tab-pane -->
</div>
<!-- /.tab-content -->
</div><!-- /.card-body -->
</div>
<!-- ./card -->
</div>
</div>
<TransactionListLarge
:entries="rawTransactions"
:page="currentPage"
:total="total"
:per-page="perPage"
:sort-desc="sortDesc"
v-on:jump-page="jumpToPage($event)"
/>
<div class="row">
<div class="col-lg-12 col-md-6 col-sm-12 col-xs-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
Blocks
</h3>
</div>
<div class="card-body">
Blocks
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import TransactionListLarge from "../transactions/TransactionListLarge";
import format from "date-fns/format";
import {mapGetters} from "vuex";
import {configureAxios} from "../../shared/forageStore";
export default {
name: "Show"
name: "Show",
computed: {
...mapGetters('root', ['listPageSize', 'cacheKey']),
...mapGetters('dashboard/index', ['start', 'end',]),
'showReady': function () {
return null !== this.start && null !== this.end && null !== this.listPageSize && this.ready;
},
},
data() {
return {
accountId: 0,
rawTransactions: [],
ready: false,
loading: false,
total: 0,
sortDesc: false,
currentPage: 1,
perPage: 51,
locale: 'en-US',
api: null,
}
},
created() {
this.ready = true;
let parts = window.location.pathname.split('/');
this.accountId = parseInt(parts[parts.length - 1]);
this.perPage = this.listPageSize ?? 51;
let params = new URLSearchParams(window.location.search);
this.currentPage = params.get('page') ? parseInt(params.get('page')) : 1;
this.getTransactions();
},
components: {TransactionListLarge},
methods: {
getTransactions: function () {
if (this.showReady && !this.loading) {
this.loading = true;
configureAxios().then(async (api) => {
// console.log('Now getTransactions() x Start');
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
this.rawTransactions = [];
let url = './api/v1/accounts/' + this.accountId + '/transactions?page=1&limit=' + this.perPage + '&start=' + startStr + '&end=' + endStr;
api.get(url)
.then(response => {
// console.log('Now getTransactions() DONE!');
this.total = parseInt(response.data.meta.pagination.total);
this.rawTransactions = response.data.data;
this.loading = false;
}
);
});
}
},
jumpToPage: function (event) {
// console.log('noticed a change!');
this.currentPage = event.page;
this.downloadTransactionList(event.page);
},
},
watch: {
start: function () {
this.getTransactions();
},
end: function () {
this.getTransactions();
},
}
}
</script>

View File

@ -0,0 +1,108 @@
<!--
- DashboardListLarge.vue
- Copyright (c) 2020 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/>.
-->
<template>
<table class="table table-striped table-sm">
<caption style="display:none;">{{ $t('firefly.transaction_table_description') }}</caption>
<thead>
<tr>
<th class="text-left" scope="col">{{ $t('firefly.description') }}</th>
<th scope="col">{{ $t('firefly.opposing_account') }}</th>
<th class="text-right" scope="col">{{ $t('firefly.amount') }}</th>
<th scope="col">{{ $t('firefly.category') }}</th>
<th scope="col">{{ $t('firefly.budget') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="transaction in this.transactions">
<td>
<a :href="'transactions/show/' + transaction.id " :title="transaction.date">
<span v-if="transaction.attributes.transactions.length > 1">{{ transaction.attributes.group_title }}</span>
<span v-if="1===transaction.attributes.transactions.length">{{ transaction.attributes.transactions[0].description }}</span>
</a>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="'withdrawal' === tr.type" :href="'accounts/show/' + tr.destination_id">{{ tr.destination_name }}</a>
<a v-if="'deposit' === tr.type" :href="'accounts/show/' + tr.source_id">{{ tr.source_name }}</a>
<a v-if="'transfer' === tr.type && parseInt(tr.source_id) === account_id" :href="'accounts/show/' + tr.destination_id">{{ tr.destination_name }}</a>
<a v-if="'transfer' === tr.type && parseInt(tr.destination_id) === account_id" :href="'accounts/show/' + tr.source_id">{{ tr.source_name }}</a>
<br/>
</span>
</td>
<td style="text-align:right;">
<span v-for="tr in transaction.attributes.transactions">
<span v-if="'withdrawal' === tr.type" class="text-danger">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount * -1) }}<br>
</span>
<span v-if="'deposit' === tr.type" class="text-success">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount) }}<br>
</span>
<span v-if="'transfer' === tr.type && parseInt(tr.source_id) === account_id" class="text-info">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount * -1) }}<br>
</span>
<span v-if="'transfer' === tr.type && parseInt(tr.destination_id) === account_id" class="text-info">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount) }}<br>
</span>
</span>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="0!==tr.category_id" :href="'categories/show/' + tr.category_id">{{ tr.category_name }}</a><br/>
</span>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="0!==tr.budget_id" :href="'budgets/show/' + tr.budget_id">{{ tr.budget_name }}</a><br/>
</span>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: "DashboardListLarge",
data() {
return {
locale: 'en-US'
}
},
created() {
this.locale = localStorage.locale ?? 'en-US';
},
props: {
transactions: {
type: Array,
default: function () {
return [];
}
},
account_id: {
type: Number,
default: function () {
return 0;
}
},
}
}
</script>

View File

@ -1,5 +1,5 @@
<!--
- TransactionListMedium.vue
- DashboardListMedium.vue
- Copyright (c) 2020 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
@ -68,7 +68,7 @@
<script>
export default {
name: "TransactionListMedium",
name: "DashboardListMedium",
data() {
return {
locale: 'en-US'

View File

@ -1,5 +1,5 @@
<!--
- TransactionListSmall.vue
- DashboardListSmall.vue
- Copyright (c) 2020 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
@ -59,7 +59,7 @@
<script>
export default {
name: "TransactionListSmall",
name: "DashboardListSmall",
data() {
return {
locale: 'en-US'

View File

@ -107,8 +107,8 @@ export default {
this.drawChart();
})
.catch(error => {
console.log('Has error!');
console.log(error);
console.error('Has error!');
console.error(error);
this.error = true;
});
},

View File

@ -62,9 +62,9 @@
</div>
<div class="card-body table-responsive p-0">
<div>
<transaction-list-large v-if="1===accounts.length" :account_id="account.id" :transactions="account.transactions"/>
<transaction-list-medium v-if="2===accounts.length" :account_id="account.id" :transactions="account.transactions"/>
<transaction-list-small v-if="accounts.length > 2" :account_id="account.id" :transactions="account.transactions"/>
<dashboard-list-large v-if="1===accounts.length" :account_id="account.id" :transactions="account.transactions"/>
<dashboard-list-medium v-if="2===accounts.length" :account_id="account.id" :transactions="account.transactions"/>
<dashboard-list-small v-if="accounts.length > 2" :account_id="account.id" :transactions="account.transactions"/>
</div>
</div>
</div>

View File

@ -830,6 +830,10 @@ export default {
//console.log('getExpectedSourceTypes.');
this.sourceAllowedTypes = response.data.data.value.source[this.transactionType];
this.destinationAllowedTypes = response.data.data.value.destination[this.transactionType];
// console.log('sourceAllowedTypes');
// console.log(this.sourceAllowedTypes);
// console.log('Source allowed types for ' + this.transactionType + ' is: ');
// console.log(this.sourceAllowedTypes);

View File

@ -28,7 +28,6 @@
<SplitPills
:transactions="transactions"
:count="transactions.length"
/>
<div class="tab-content">
@ -132,6 +131,7 @@ export default {
this.groupId = parseInt(parts[parts.length - 1]);
this.transactions = [];
this.getTransactionGroup();
//this.getExpectedSourceTypes();
this.getAllowedOpposingTypes();
this.getCustomFields();
},
@ -238,6 +238,9 @@ export default {
watch: {
submittedAttachments: function () {
this.finaliseSubmission();
},
transactionType: function() {
this.getExpectedSourceTypes();
}
},
@ -299,8 +302,12 @@ export default {
this.transactionType = array.type.charAt(0).toUpperCase() + array.type.slice(1);
// See reference nr. 5
this.sourceAllowedTypes = [array.source_type];
this.destinationAllowedTypes = [array.destination_type];
// console.log('sourceAllowedTypes (parseTransaction)');
// console.log(this.sourceAllowedTypes);
this.date = array.date.substring(0, 16);
this.originalDate = array.date.substring(0, 16);
}
@ -427,6 +434,18 @@ export default {
this.originalTransactions[index].links.push(object);
});
},
getExpectedSourceTypes: function () {
axios.get('./api/v1/configuration/firefly.expected_source_types')
.then(response => {
this.sourceAllowedTypes = response.data.data.value.source[this.transactionType];
this.destinationAllowedTypes = response.data.data.value.destination[this.transactionType];
// console.log('sourceAllowedTypes (getExpectedSourceTypes)');
// console.log(JSON.stringify(response.data.data.value.source[this.transactionType]));
// console.log(JSON.stringify(response.data.data.value.source));
// console.log(this.transactionType);
//console.log(this.transactionType);
});
},
/**
* Get API value.
*/

View File

@ -36,123 +36,15 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-md-6 col-sm-12 col-xs-12">
<b-pagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<button @click="newCacheKey" class="btn btn-sm float-right btn-info"><span class="fas fa-sync"></span></button>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body p-0">
<b-table id="my-table" small striped hover responsive="md" primary-key="key" :no-local-sorting="false"
:items="transactionRows"
:fields="fields"
:per-page="perPage"
sort-icon-left
ref="table"
:current-page="currentPage"
:busy.sync="loading"
:sort-desc.sync="sortDesc"
:sort-compare="tableSortCompare"
>
<template #table-busy>
<span class="fa fa-spinner"></span>
</template>
<template #cell(type)="data">
<span v-if="! data.item.split || data.item.split_parent === null">
<span class="fas fa-long-arrow-alt-right" v-if="'deposit' === data.item.type"></span>
<span class="fas fa-long-arrow-alt-left" v-else-if="'withdrawal' === data.item.type"></span>
<span class="fas fa-long-arrows-alt-h" v-else-if="'transfer' === data.item.type"></span>
</span>
</template>
<template #cell(description)="data">
<span class="fas fa-angle-right" v-if="data.item.split && data.item.split_parent !== null"></span>
<a :class="false === data.item.active ? 'text-muted' : ''" :href="'./transactions/show/' + data.item.id" :title="data.value">{{
data.value
}}</a>
</template>
<template #cell(amount)="data">
<span class="text-success" v-if="'deposit' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }}
</span>
<span class="text-danger" v-else-if="'withdrawal' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(-data.item.amount) }}
</span>
<span class="text-muted" v-else-if="'transfer' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }}
</span>
</template>
<template #cell(date)="data">
{{ data.item.date_formatted }}
</template>
<template #cell(source_account)="data">
<a :class="false === data.item.active ? 'text-muted' : ''" :href="'./accounts/show/' + data.item.source_id"
:title="data.item.source_name">{{ data.item.source_name }}</a>
</template>
<template #cell(destination_account)="data">
<a :class="false === data.item.active ? 'text-muted' : ''" :href="'./accounts/show/' + data.item.destination_id"
:title="data.item.destination_name">{{ data.item.destination_name }}</a>
</template>
<template #cell(menu)="data">
<div class="btn-group btn-group-sm" v-if="! data.item.split || data.item.split_parent === null">
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" :id="'dropdownMenuButton' + data.item.id" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }}
</button>
<div class="dropdown-menu" :aria-labelledby="'dropdownMenuButton' + data.item.id">
<a class="dropdown-item" :href="'./transactions/edit/' + data.item.id"><span class="fa fas fa-pencil-alt"></span> {{ $t('firefly.edit') }}</a>
<a class="dropdown-item" :href="'./transactions/delete/' + data.item.id"><span class="fa far fa-trash"></span> {{ $t('firefly.delete') }}</a>
</div>
</div>
</div>
<div class="btn btn-light btn-sm" v-if="data.item.split && data.item.split_parent === null && data.item.collapsed === true"
v-on:click="toggleCollapse(data.item)">
<span class="fa fa-caret-down"></span>
{{ $t('firefly.transaction_expand_split') }}
</div>
<div class="btn btn-light btn-sm" v-else-if="data.item.split && data.item.split_parent === null && data.item.collapsed === false"
v-on:click="toggleCollapse(data.item)">
<span class="fa fa-caret-up"></span>
{{ $t('firefly.transaction_collapse_split') }}
</div>
</template>
<template #cell(category)="data">
{{ data.item.category_name }}
</template>
</b-table>
</div>
<div class="card-footer">
<a :href="'./transactions/create/' + type" class="btn btn-success"
:title="$t('firefly.create_new_transaction')">{{ $t('firefly.create_new_transaction') }}</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-md-6 col-sm-12 col-xs-12">
<b-pagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<button @click="newCacheKey" class="btn btn-sm float-right btn-info"><span class="fas fa-sync"></span></button>
</div>
</div>
<!-- page is ignored for the time being -->
<TransactionListLarge
:entries="rawTransactions"
:page="currentPage"
:total="total"
:per-page="perPage"
:sort-desc="sortDesc"
v-on:jump-page="jumpToPage($event)"
/>
<div class="row">
<div class="col-xl-2 col-lg-4 col-sm-6 col-xs-12" v-for="range in ranges">
<div class="card">
@ -176,18 +68,18 @@ import sub from "date-fns/sub";
import startOfMonth from "date-fns/startOfMonth";
import endOfMonth from "date-fns/endOfMonth";
import {configureAxios} from "../../shared/forageStore";
import TransactionListLarge from "./TransactionListLarge";
export default {
name: "Index",
components: {TransactionListLarge},
data() {
return {
transactions: [],
transactionRows: [],
rawTransactions: [],
type: 'all',
downloaded: false,
loading: false,
ready: false,
fields: [],
currentPage: 1,
perPage: 5,
total: 1,
@ -236,6 +128,7 @@ export default {
let parts = pathName.split('/');
this.type = parts[parts.length - 1];
this.perPage = this.listPageSize ?? 51;
if (5 === parts.length) {
this.urlStart = new Date(parts[3]);
this.urlEnd = new Date(parts[4]);
@ -244,57 +137,27 @@ export default {
let params = new URLSearchParams(window.location.search);
this.currentPage = params.get('page') ? parseInt(params.get('page')) : 1;
this.updateFieldList();
this.ready = true;
// make object thing:
// let token = document.head.querySelector('meta[name="csrf-token"]');
// this.api = setup(
// {
// // `axios` options
// //baseURL: './',
// headers: {'X-CSRF-TOKEN': token.content, 'X-James': 'yes'},
//
// // `axios-cache-adapter` options
// cache: {
// maxAge: 15 * 60 * 1000,
// readHeaders: false,
// exclude: {
// query: false,
// },
// debug: true
// }
// });
},
methods: {
...mapMutations('root', ['refreshCacheKey',]),
updateFieldList: function () {
this.fields = [
{key: 'type', label: ' ', sortable: false},
{key: 'description', label: this.$t('list.description'), sortable: true},
{key: 'amount', label: this.$t('list.amount'), sortable: true},
{key: 'date', label: this.$t('list.date'), sortable: true},
{key: 'source_account', label: this.$t('list.source_account'), sortable: true},
{key: 'destination_account', label: this.$t('list.destination_account'), sortable: true},
{key: 'category_name', label: this.$t('list.category'), sortable: true},
{key: 'menu', label: ' ', sortable: false},
];
},
newCacheKey: function () {
this.refreshCacheKey();
this.downloaded = false;
this.accounts = [];
this.getTransactionList();
},
jumpToPage: function(event) {
// console.log('noticed a change!');
this.currentPage = event.page;
this.downloadTransactionList(event.page);
},
getTransactionList: function () {
// console.log('getTransactionList()');
if (this.indexReady && !this.loading && !this.downloaded) {
// console.log('Index ready, not loading and not already downloaded. Reset.');
this.loading = true;
this.perPage = this.listPageSize ?? 51;
this.transactions = [];
this.transactionRows = [];
this.downloadTransactionList(1);
this.rawTransactions = [];
this.downloadTransactionList(this.currentPage);
this.calculateDateRanges();
}
},
@ -303,165 +166,39 @@ export default {
let currentDate = this.start;
while (currentDate > yearAgo) {
// start + end of month:
let st = startOfMonth(currentDate);
let en = endOfMonth(currentDate);
this.ranges.push({start: st, end: en});
currentDate = sub(currentDate, {months: 1});
//console.log(currentDate);
}
},
formatDate: function (date, frm) {
return format(date, frm);
},
downloadTransactionList: function (page) {
// console.log('downloadTransactionList(' + page + ')');
configureAxios().then(async (api) => {
let startStr = format(this.start, 'y-MM-dd');
let endStr = format(this.end, 'y-MM-dd');
// console.log(this.urlEnd);
// console.log(this.urlStart);
if(null !== this.urlEnd && null !== this.urlStart) {
if (null !== this.urlEnd && null !== this.urlStart) {
startStr = format(this.urlStart, 'y-MM-dd');
endStr = format(this.urlEnd, 'y-MM-dd');
}
api.get('./api/v1/transactions?type=' + this.type + '&page=' + page + "&start=" + startStr + "&end=" + endStr + '&cache=' + this.cacheKey)
let url = './api/v1/transactions?type=' + this.type + '&page=' + page + "&start=" + startStr + "&end=" + endStr + '&cache=' + this.cacheKey;
api.get(url)
.then(response => {
//let currentPage = parseInt(response.data.meta.pagination.current_page);
//let totalPages = parseInt(response.data.meta.pagination.total_pages);
this.total = parseInt(response.data.meta.pagination.total);
//console.log('total is ' + this.total);
this.transactions.push(...response.data.data);
// if (currentPage < totalPage) {
// let nextPage = currentPage + 1;
// this.downloadTransactionList(nextPage);
// }
// if (currentPage >= totalPage) {
// console.log('Looks like all downloaded.');
this.downloaded = true;
this.createTransactionRows();
// }
this.rawTransactions = response.data.data;
this.loading = false;
}
);
});
},
createTransactionRows: function () {
this.transactionRows = [];
for (let i in this.transactions) {
let transaction = this.transactions[i];
let transactionRow = this.getTransactionRow(transaction, 0);
this.transactionRows.push(transactionRow);
if (transaction.attributes.transactions.length > 1) {
transactionRow.description = transaction.attributes.group_title;
transactionRow.split = true;
transactionRow.collapsed = transaction.collapsed === true || transaction.collapsed === undefined;
transactionRow.amount = transaction.attributes.transactions
.map(transaction => Number(transaction.amount))
.reduce((sum, n) => sum + n);
transactionRow.source_name = '';
transactionRow.source_id = '';
transactionRow.destination_name = '';
transactionRow.destination_id = '';
if (!transactionRow.collapsed) {
for (let i = 0; i < transaction.attributes.transactions.length; i++) {
let splitTransactionRow = this.getTransactionRow(transaction, i);
splitTransactionRow.key = splitTransactionRow.id + "." + i
splitTransactionRow.split = true;
splitTransactionRow.split_index = i + 1;
splitTransactionRow.split_parent = transactionRow;
this.transactionRows.push(splitTransactionRow);
}
}
}
}
this.loading = false;
},
getTransactionRow(transaction, index) {
let transactionRow = {};
let currentTransaction = transaction.attributes.transactions[index];
transactionRow.key = transaction.id;
transactionRow.id = transaction.id;
transactionRow.type = currentTransaction.type;
transactionRow.description = currentTransaction.description;
transactionRow.amount = currentTransaction.amount;
transactionRow.currency_code = currentTransaction.currency_code;
transactionRow.date = new Date(currentTransaction.date);
transactionRow.date_formatted = format(transactionRow.date, this.$t('config.month_and_day_fns'));
transactionRow.source_name = currentTransaction.source_name;
transactionRow.source_id = currentTransaction.source_id;
transactionRow.destination_name = currentTransaction.destination_name;
transactionRow.destination_id = currentTransaction.destination_id;
transactionRow.category_id = currentTransaction.category_id;
transactionRow.category_name = currentTransaction.category_name;
transactionRow.split = false;
transactionRow.split_index = 0;
transactionRow.split_parent = null;
return transactionRow;
},
toggleCollapse: function (row) {
let transaction = this.transactions.filter(transaction => transaction.id === row.id)[0];
if (transaction.collapsed === undefined) {
transaction.collapsed = false;
} else {
transaction.collapsed = !transaction.collapsed;
}
this.createTransactionRows();
},
tableSortCompare: function (aRow, bRow, key, sortDesc, formatter, compareOptions, compareLocale) {
let a = aRow[key]
let b = bRow[key]
if (aRow.id === bRow.id) {
// Order split transactions normally when compared to each other, except always put the header first
if (aRow.split_parent === null) {
return sortDesc ? 1 : -1;
} else if (bRow.split_parent === null) {
return sortDesc ? -1 : 1;
}
} else {
// Sort split transactions based on their parent when compared to other transactions
if (aRow.split && aRow.split_parent !== null) {
a = aRow.split_parent[key]
}
if (bRow.split && bRow.split_parent !== null) {
b = bRow.split_parent[key]
}
}
if (
(typeof a === 'number' && typeof b === 'number') ||
(a instanceof Date && b instanceof Date)
) {
// If both compared fields are native numbers or both are native dates
return a < b ? -1 : a > b ? 1 : 0
} else {
// Otherwise stringify the field data and use String.prototype.localeCompare
return toString(a).localeCompare(toString(b), compareLocale, compareOptions)
}
function toString(value) {
if (value === null || typeof value === 'undefined') {
return ''
} else if (value instanceof Object) {
return Object.keys(value)
.sort()
.map(key => toString(value[key]))
.join(' ')
} else {
return String(value)
}
}
},
},
}
</script>

View File

@ -19,84 +19,390 @@
-->
<template>
<table class="table table-striped table-sm">
<caption style="display:none;">{{ $t('firefly.transaction_table_description') }}</caption>
<thead>
<tr>
<th class="text-left" scope="col">{{ $t('firefly.description') }}</th>
<th scope="col">{{ $t('firefly.opposing_account') }}</th>
<th class="text-right" scope="col">{{ $t('firefly.amount') }}</th>
<th scope="col">{{ $t('firefly.category') }}</th>
<th scope="col">{{ $t('firefly.budget') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="transaction in this.transactions">
<td>
<a :href="'transactions/show/' + transaction.id " :title="transaction.date">
<span v-if="transaction.attributes.transactions.length > 1">{{ transaction.attributes.group_title }}</span>
<span v-if="1===transaction.attributes.transactions.length">{{ transaction.attributes.transactions[0].description }}</span>
</a>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="'withdrawal' === tr.type" :href="'accounts/show/' + tr.destination_id">{{ tr.destination_name }}</a>
<a v-if="'deposit' === tr.type" :href="'accounts/show/' + tr.source_id">{{ tr.source_name }}</a>
<a v-if="'transfer' === tr.type && parseInt(tr.source_id) === account_id" :href="'accounts/show/' + tr.destination_id">{{ tr.destination_name }}</a>
<a v-if="'transfer' === tr.type && parseInt(tr.destination_id) === account_id" :href="'accounts/show/' + tr.source_id">{{ tr.source_name }}</a>
<br/>
<div>
<div class="row">
<div class="col-lg-8 col-md-6 col-sm-12 col-xs-12">
<BPagination v-if="!loading"
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></BPagination>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<button @click="newCacheKey" class="btn btn-sm float-right btn-info"><span class="fas fa-sync"></span></button>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body p-0">
<BTable id="my-table" small striped hover responsive="md" primary-key="key" :no-local-sorting="false"
:items="transactions"
:fields="fields"
:per-page="perPage"
sort-icon-left
ref="table"
:current-page="currentPage"
:busy.sync="loading"
:sort-desc.sync="sortDesc"
:sort-compare="tableSortCompare"
>
<template #table-busy>
<span class="fa fa-spinner fa-spin"></span>
</template>
<template #cell(type)="data">
<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-left" v-else-if="'withdrawal' === data.item.type"></span>
<span class="fas fa-long-arrows-alt-h" v-else-if="'transfer' === data.item.type"></span>
</span>
</td>
<td style="text-align:right;">
<span v-for="tr in transaction.attributes.transactions">
<span v-if="'withdrawal' === tr.type" class="text-danger">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount * -1) }}<br>
</span>
<span v-if="'deposit' === tr.type" class="text-success">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount) }}<br>
</span>
<span v-if="'transfer' === tr.type && parseInt(tr.source_id) === account_id" class="text-info">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount * -1) }}<br>
</span>
<span v-if="'transfer' === tr.type && parseInt(tr.destination_id) === account_id" class="text-info">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: tr.currency_code}).format(tr.amount) }}<br>
</template>
<template #cell(description)="data">
<span class="fa fa-spinner fa-spin" v-if="data.item.dummy"></span>
<span v-if="!data.item.split">
<a :href="'./transactions/show/' + data.item.id" :title="data.value">
{{ data.item.description }}
</a>
</span>
<span v-if="data.item.split">
<!-- title first -->
<span class="fas fa-angle-right" @click="toggleCollapse(data.item.id)" style="cursor: pointer;"></span>
<a :href="'./transactions/show/' + data.item.id" :title="data.value">
{{ data.item.description }}
</a><br />
<span v-if="!data.item.collapsed">
<span v-for="(split, index) in data.item.splits" v-bind:key="index">
&nbsp; &nbsp; {{ split.description }}<br />
</span>
</span>
</span>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="0!==tr.category_id" :href="'categories/show/' + tr.category_id">{{ tr.category_name }}</a><br/>
</template>
<template #cell(amount)="data">
<!-- row amount first (3x) -->
<span :class="'text-success ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'deposit' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }}
</span>
</td>
<td>
<span v-for="tr in transaction.attributes.transactions">
<a v-if="0!==tr.budget_id" :href="'budgets/show/' + tr.budget_id">{{ tr.budget_name }}</a><br/>
<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) }}
</span>
</td>
</tr>
</tbody>
</table>
<span :class="'text-muted ' + (!data.item.collapsed ? 'font-weight-bold' : '')" v-if="'transfer' === data.item.type">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: data.item.currency_code}).format(data.item.amount) }}
</span>
<br />
<!-- splits -->
<span v-if="!data.item.collapsed">
<span v-for="(split, index) in data.item.splits" v-bind:key="index">
{{ Intl.NumberFormat(locale, {style: 'currency', currency: split.currency_code}).format(split.amount) }}<br />
</span>
</span>
</template>
<template #cell(date)="data">
{{ data.item.date_formatted }}
</template>
<template #cell(source_account)="data">
<!-- extra break for splits -->
<span v-if="true===data.item.split && !data.item.collapsed">
<br />
</span>
<em v-if="true===data.item.split && data.item.collapsed">
...
</em>
<!-- loop all accounts, hidden if split -->
<span v-for="(split, index) in data.item.splits" v-bind:key="index" v-if="false===data.item.split || (true===data.item.split && !data.item.collapsed)">
<a :href="'./accounts/show/' + split.source_id" :title="split.source_name">{{ split.source_name }}</a><br />
</span>
</template>
<template #cell(destination_account)="data">
<!-- extra break for splits -->
<span v-if="true===data.item.split && !data.item.collapsed">
<br />
</span>
<em v-if="true===data.item.split && data.item.collapsed">
...
</em>
<!-- loop all accounts, hidden if split -->
<span v-for="(split, index) in data.item.splits" v-bind:key="index" v-if="false===data.item.split || (true===data.item.split && !data.item.collapsed)">
<a :href="'./accounts/show/' + split.destination_id" :title="split.destination_name">{{ split.destination_name }}</a><br />
</span>
</template>
<template #cell(menu)="data">
<div class="btn-group btn-group-sm">
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" :id="'dropdownMenuButton' + data.item.id" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }}
</button>
<div class="dropdown-menu" :aria-labelledby="'dropdownMenuButton' + data.item.id">
<a class="dropdown-item" :href="'./transactions/edit/' + data.item.id"><span class="fa fas fa-pencil-alt"></span> {{
$t('firefly.edit')
}}</a>
<a class="dropdown-item" :href="'./transactions/delete/' + data.item.id"><span class="fa far fa-trash"></span> {{
$t('firefly.delete')
}}</a>
</div>
</div>
</div>
</template>
<template #cell(category_name)="data">
<!-- extra break for splits -->
<span v-if="true===data.item.split && !data.item.collapsed">
<br />
</span>
<em v-if="true===data.item.split && data.item.collapsed">
...
</em>
<!-- loop all categories, hidden if split -->
<span v-for="(split, index) in data.item.splits" v-bind:key="index" v-if="false===data.item.split || (true===data.item.split && !data.item.collapsed)">
<a :href="'./categories/show/' + split.category_id" :title="split.category_name">{{ split.category_name }}</a><br />
</span>
</template>
</BTable>
</div>
<div class="card-footer"> (button)
<!--
<a :href="'./transactions/create/' + type" class="btn btn-success"
:title="$t('firefly.create_new_transaction')">{{ $t('firefly.create_new_transaction') }}</a>
-->
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 col-md-6 col-sm-12 col-xs-12">
<BPagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="my-table"
></BPagination>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<button @click="newCacheKey" class="btn btn-sm float-right btn-info"><span class="fas fa-sync"></span></button>
</div>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
import {BPagination, BTable} from 'bootstrap-vue';
import format from "date-fns/format";
export default {
name: "TransactionListLarge",
components: {BPagination, BTable},
data() {
return {
locale: 'en-US'
locale: 'en-US',
fields: [],
currentPage: 1,
transactions: [],
loading: true
}
},
computed: {
...mapGetters('root', ['listPageSize', 'cacheKey']),
},
created() {
this.locale = localStorage.locale ?? 'en-US';
this.updateFieldList();
//this.currentPage = this.page;
this.parseTransactions();
},
watch: {
currentPage: function (value) {
// console.log('Watch currentPage go to ' + value);
this.$emit('jump-page', {page: value});
},
entries: function (value) {
this.parseTransactions();
},
},
methods: {
...mapMutations('root', ['refreshCacheKey',]),
parseTransactions: function () {
// console.log('Start of parseTransactions. Count of entries is ' + this.entries.length + ' and page is ' + this.page);
// console.log('Reported total is ' + this.total);
if (0 === this.entries.length) {
// console.log('Will not render now');
return;
}
// console.log('Now have ' + this.transactions.length + ' transactions');
for (let i = 0; i < this.total; i++) {
this.transactions.push({dummy: true});
// console.log('Push dummy to index ' + i);
// console.log('Now have ' + this.transactions.length + ' transactions');
}
// console.log('Generated ' + this.total + ' dummies');
// console.log('Now have ' + this.transactions.length + ' transactions');
let index = (this.page - 1) * this.perPage;
// console.log('Start index is ' + index);
for (let i in this.entries) {
let transaction = this.entries[i];
// build split
this.transactions[index] = this.parseTransaction(transaction);
// console.log('Push transaction to index ' + index);
// console.log('Now have ' + this.transactions.length + ' transactions');
index++;
}
// console.log('Added ' + this.entries.length + ' entries');
// console.log('Now have ' + this.transactions.length + ' transactions');
// console.log(this.transactions);
this.loading = false;
},
newCacheKey: function () {
alert('TODO');
this.refreshCacheKey();
},
updateFieldList: function () {
this.fields = [
{key: 'type', label: ' ', sortable: false},
{key: 'description', label: this.$t('list.description'), sortable: true},
{key: 'amount', label: this.$t('list.amount'), sortable: true},
{key: 'date', label: this.$t('list.date'), sortable: true},
{key: 'source_account', label: this.$t('list.source_account'), sortable: true},
{key: 'destination_account', label: this.$t('list.destination_account'), sortable: true},
{key: 'category_name', label: this.$t('list.category'), sortable: true},
{key: 'menu', label: ' ', sortable: false},
];
},
/**
* Parse a single transaction.
* @param transaction
*/
parseTransaction: function (transaction) {
let row = {};
// default values:
row.splits = [];
row.key = transaction.id;
row.id = transaction.id
row.dummy = false;
// pick this up from the first transaction
let first = transaction.attributes.transactions[0];
row.type = first.type;
row.date = new Date(first.date);
row.date_formatted = format(row.date, this.$t('config.month_and_day_fns'));
row.description = first.description;
row.collapsed = true;
row.split = false;
row.amount = 0;
row.currency_code = first.currency_code;
if (transaction.attributes.transactions.length > 1) {
row.split = true;
row.description = transaction.attributes.group_title;
}
// collapsed?
if (typeof transaction.collapsed !== 'undefined') {
row.collapsed = transaction.collapsed;
}
//console.log('is collapsed? ' + row.collapsed);
// then loop each split
for (let i in transaction.attributes.transactions) {
if (transaction.attributes.transactions.hasOwnProperty(i)) {
let info = transaction.attributes.transactions[i];
let split = {};
row.amount = row.amount + parseFloat(info.amount);
split.description = info.description;
split.amount = info.amount;
split.currency_code = info.currency_code;
split.source_name = info.source_name;
split.source_id = info.source_id;
split.destination_name = info.destination_name;
split.destination_id = info.destination_id;
split.category_id = info.category_id;
split.category_name = info.category_name;
split.split_index = i;
row.splits.push(split);
}
}
return row;
},
toggleCollapse: function (id) {
let transaction = this.transactions.filter(transaction => transaction.id === id)[0];
transaction.collapsed = !transaction.collapsed;
},
tableSortCompare: function (aRow, bRow, key, sortDesc, formatter, compareOptions, compareLocale) {
let a = aRow[key]
let b = bRow[key]
if (aRow.id === bRow.id) {
// Order split transactions normally when compared to each other, except always put the header first
if (aRow.split_parent === null) {
return sortDesc ? 1 : -1;
} else if (bRow.split_parent === null) {
return sortDesc ? -1 : 1;
}
} else {
// Sort split transactions based on their parent when compared to other transactions
if (aRow.split && aRow.split_parent !== null) {
a = aRow.split_parent[key]
}
if (bRow.split && bRow.split_parent !== null) {
b = bRow.split_parent[key]
}
}
if (
(typeof a === 'number' && typeof b === 'number') ||
(a instanceof Date && b instanceof Date)
) {
// If both compared fields are native numbers or both are native dates
return a < b ? -1 : a > b ? 1 : 0
} else {
// Otherwise stringify the field data and use String.prototype.localeCompare
return toString(a).localeCompare(toString(b), compareLocale, compareOptions)
}
function toString(value) {
if (value === null || typeof value === 'undefined') {
return ''
} else if (value instanceof Object) {
return Object.keys(value)
.sort()
.map(key => toString(value[key]))
.join(' ')
} else {
return String(value)
}
}
},
},
props: {
transactions: {
page: {
type: Number
},
perPage: {
type: Number,
default: 1
},
sortDesc: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 1
},
entries: {
type: Array,
default: function () {
return [];
}
},
account_id: {
accountId: {
type: Number,
default: function () {
return 0;

View File

@ -169,13 +169,13 @@
"repeat_freq_quarterly": "\u03c4\u03c1\u03b9\u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03c9\u03c2",
"repeat_freq_monthly": "\u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03c9\u03c2",
"repeat_freq_weekly": "\u03b5\u03b2\u03b4\u03bf\u03bc\u03b1\u03b4\u03b9\u03b1\u03af\u03c9\u03c2",
"credit_card_type_monthlyFull": "Full payment every month",
"credit_card_type_monthlyFull": "\u0395\u03be\u03cc\u03c6\u03bb\u03b7\u03c3\u03b7 \u03ba\u03ac\u03b8\u03b5 \u03bc\u03ae\u03bd\u03b1",
"update_liabilities_account": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c5\u03c0\u03bf\u03c7\u03c1\u03ad\u03c9\u03c3\u03b7\u03c2",
"update_expense_account": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03b4\u03b1\u03c0\u03b1\u03bd\u03ce\u03bd",
"update_revenue_account": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03b5\u03c3\u03cc\u03b4\u03c9\u03bd",
"update_undefined_account": "Update account",
"update_undefined_account": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd",
"update_asset_account": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03ba\u03b5\u03c6\u03b1\u03bb\u03b1\u03af\u03bf\u03c5",
"updated_account_js": "Updated account \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
"updated_account_js": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
},
"list": {
"piggy_bank": "\u039a\u03bf\u03c5\u03bc\u03c0\u03b1\u03c1\u03ac\u03c2",

View File

@ -169,13 +169,13 @@
"repeat_freq_quarterly": "trimestriel",
"repeat_freq_monthly": "mensuel",
"repeat_freq_weekly": "hebdomadaire",
"credit_card_type_monthlyFull": "Full payment every month",
"credit_card_type_monthlyFull": "Paiement complet tous les mois",
"update_liabilities_account": "Mettre \u00e0 jour le passif",
"update_expense_account": "Mettre \u00e0 jour le compte de d\u00e9penses",
"update_revenue_account": "Mettre \u00e0 jour le compte de recettes",
"update_undefined_account": "Update account",
"update_undefined_account": "Mettre \u00e0 jour le compte",
"update_asset_account": "Mettre \u00e0 jour le compte d\u2019actif",
"updated_account_js": "Updated account \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
"updated_account_js": "Compte \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\" mis \u00e0 jour."
},
"list": {
"piggy_bank": "Tirelire",

View File

@ -173,9 +173,9 @@
"update_liabilities_account": "\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u66f4\u65b0",
"update_expense_account": "\u652f\u51fa\u5148\u30a2\u30ab\u30a6\u30f3\u30c8\uff08\u652f\u51fa\u5143\u30a2\u30ab\u30a6\u30f3\u30c8\n\uff09",
"update_revenue_account": "\u652f\u51fa\u30a2\u30ab\u30a6\u30f3\u30c8\uff08\u53ce\u5165\u30a2\u30ab\u30a6\u30f3\u30c8\uff09",
"update_undefined_account": "Update account",
"update_undefined_account": "\u53e3\u5ea7\u60c5\u5831\u306e\u66f4\u65b0",
"update_asset_account": "\u652f\u51fa\u30a2\u30ab\u30a6\u30f3\u30c8\uff08\u8cc7\u7523\u52d8\u5b9a\uff09",
"updated_account_js": "Updated account \"<a href=\"accounts\/show\/{ID}\">{title}<\/a>\"."
"updated_account_js": "\u53e3\u5ea7\u300c<a href=\"accounts\/show\/{ID}\">{title}<\/a>\u300d\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002"
},
"list": {
"piggy_bank": "\u8caf\u91d1\u7bb1",

View File

@ -149,7 +149,7 @@
"bill_repeats_weekly_other": "Repeats every other week",
"bill_repeats_monthly_other": "Repeats every other month",
"bill_repeats_quarterly_other": "Repeats every other quarter",
"bill_repeats_half-year_other": "Repeats yearly",
"bill_repeats_half-year_other": "\u041f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f \u0435\u0436\u0435\u0433\u043e\u0434\u043d\u043e",
"bill_repeats_yearly_other": "Repeats every other year",
"bill_repeats_weekly_skip": "Repeats every {skip} weeks",
"bill_repeats_monthly_skip": "Repeats every {skip} months",

View File

@ -1,3 +1,23 @@
/*
* edit.js
* 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/>.
*/
require('../../bootstrap');
import Edit from "../../components/accounts/Edit";

View File

@ -21,6 +21,7 @@
require('../../bootstrap');
import store from '../../components/store';
import Show from "../../components/accounts/Show";
// i18n
@ -29,12 +30,22 @@ let i18n = require('../../i18n');
// get page name?
let props = {
};
let props = {};
const app = new Vue({
i18n,
render(createElement) {
return createElement(Show, {props: props});
}
}).$mount('#accounts_show');
i18n,
store,
render(createElement) {
return createElement(Show, {props: props});
},
beforeCreate() {
// See reference nr. 10
this.$store.commit('initialiseStore');
this.$store.dispatch('updateCurrencyPreference');
// init the new root store (dont care about results)
this.$store.dispatch('root/initialiseStore');
// also init the dashboard store.
this.$store.dispatch('dashboard/index/initialiseStore');
},
}).$mount('#accounts_show');

View File

@ -28,9 +28,9 @@ import MainBudgetList from '../components/dashboard/MainBudgetList';
import MainCreditList from '../components/dashboard/MainCreditList';
import MainDebitList from '../components/dashboard/MainDebitList';
import MainPiggyList from '../components/dashboard/MainPiggyList';
import TransactionListLarge from '../components/transactions/TransactionListLarge';
import TransactionListMedium from '../components/transactions/TransactionListMedium';
import TransactionListSmall from '../components/transactions/TransactionListSmall';
import DashboardListLarge from '../components/dashboard/DashboardListLarge';
import DashboardListMedium from '../components/dashboard/DashboardListMedium';
import DashboardListSmall from '../components/dashboard/DashboardListSmall';
import Calendar from '../components/dashboard/Calendar';
import MainCategoryList from '../components/dashboard/MainCategoryList';
import Vue from 'vue';
@ -48,9 +48,9 @@ import store from '../components/store';
require('../bootstrap');
require('chart.js');
Vue.component('transaction-list-large', TransactionListLarge);
Vue.component('transaction-list-medium', TransactionListMedium);
Vue.component('transaction-list-small', TransactionListSmall);
Vue.component('dashboard-list-large', DashboardListLarge);
Vue.component('dashboard-list-medium', DashboardListMedium);
Vue.component('dashboard-list-small', DashboardListSmall);
// components as an example

View File

@ -23,16 +23,12 @@ require('../../bootstrap');
import Vue from "vue";
import store from "../../components/store";
import Index from "../../components/transactions/Index";
import {BPagination, BTable} from 'bootstrap-vue';
import Calendar from "../../components/dashboard/Calendar";
// i18n
let i18n = require('../../i18n');
let props = {};
Vue.component('b-table', BTable);
Vue.component('b-pagination', BPagination);
const app = new Vue({
i18n,
store,

58
frontend/src/serviceworker.js vendored Normal file
View File

@ -0,0 +1,58 @@
/*
* serviceworker.js
* Copyright (c) 2021 Lorenzo Breda (https://github.com/lbreda)
*
* 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/>.
*/
let staticCachePrefix = "firefly-III-"
let staticCacheName = staticCachePrefix + new Date().getTime();
let cachedFiles = [
'/offline',
'/v2/plugins/local-fonts/gf-source.css',
'/v2/css/app.css',
];
// Create cache on install
self.addEventListener("install", event => {
this.skipWaiting();
event.waitUntil(
caches.open(staticCacheName).then(cache => cache.addAll(cachedFiles))
)
});
// Clear cache on activate
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(cacheName => (cacheName.startsWith(staticCachePrefix)))
.filter(cacheName => (cacheName !== staticCacheName))
.map(cacheName => caches.delete(cacheName))
);
})
);
});
// Serve from Cache or return the offline page
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request)
.then(response => (response || fetch(event.request)))
.catch(() => caches.match('offline'))
)
});

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
},
"devDependencies": {
"@johmun/vue-tags-input": "^2",
"@vue/compiler-sfc": "^3.1.5",
"@vue/compiler-sfc": "^3.2.11",
"axios": "^0.21",
"bootstrap-sass": "^3",
"cross-env": "^7.0",
@ -21,7 +21,7 @@
"uiv": "^1.3",
"vue": "^2.6",
"vue-i18n": "^8.25",
"vue-loader": "^16.3.1",
"vue-loader": "^15",
"vue-template-compiler": "^2.6"
}
}

View File

@ -1,7 +1,7 @@
# Please report security issues related to Firefly III
# to me ASAP via the email address below.
# For this (sub)domain, report Firefly III security issues ONLY
# Other reports will be IGNORED
Contact: mailto:thegrumpydictator@gmail.com
Encryption: https://keybase.io/jc5/pgp_keys.asc?fingerprint=90f546f13b81b67a1baa5dddc16961e655e74b5e
Contact: mailto:james@firefly-iii.org
Encryption: https://keybase.io/jc5/pgp_keys.asc?fingerprint=5d22ce912243bea5185ab2289d53bf7bdccbf49e
Acknowledgements: https://github.com/firefly-iii/firefly-iii
Signature: https://firefly-iii.org/.well-known/security.txt.sig

View File

@ -3,15 +3,83 @@
"short_name": "Firefly III",
"start_url": "/",
"icons": [
{
"src": "/maskable72.png",
"sizes": "72x72",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable76.png",
"sizes": "76x76",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable96.png",
"sizes": "96x96",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable120.png",
"sizes": "120x120",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable128.png",
"sizes": "128x128",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable144.png",
"sizes": "144x144",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable152.png",
"sizes": "152x152",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable180.png",
"sizes": "180x180",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
"type": "image/png",
"scope": "any"
},
{
"src": "/maskable192.png",
"sizes": "192x192",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/maskable384.png",
"sizes": "384x384",
"type": "image/png",
"scope": "maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
"type": "image/png",
"scope": "any"
},
{
"src": "/maskable512.png",
"sizes": "512x512",
"type": "image/png",
"scope": "maskable"
}
],
"theme_color": "#1e6581",

1
public/maskable-icon.svg Normal file
View File

@ -0,0 +1 @@
<svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

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