mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-06 04:34:00 +00:00
Merge branch 'develop' into adminlte4
# Conflicts: # yarn.lock
This commit is contained in:
@@ -29,8 +29,9 @@ use FireflyIII\Api\V2\Controllers\Controller;
|
|||||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||||
use FireflyIII\Models\Account;
|
use FireflyIII\Models\Account;
|
||||||
use FireflyIII\Models\AccountType;
|
use FireflyIII\Models\AccountType;
|
||||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||||
|
use FireflyIII\User;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
@@ -63,6 +64,8 @@ class AccountController extends Controller
|
|||||||
* This endpoint is documented at
|
* This endpoint is documented at
|
||||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/charts/getChartAccountOverview
|
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/charts/getChartAccountOverview
|
||||||
*
|
*
|
||||||
|
* The native currency is the preferred currency on the page /currencies.
|
||||||
|
*
|
||||||
* @param DateRequest $request
|
* @param DateRequest $request
|
||||||
*
|
*
|
||||||
* @return JsonResponse
|
* @return JsonResponse
|
||||||
@@ -77,6 +80,12 @@ class AccountController extends Controller
|
|||||||
$start = $dates['start'];
|
$start = $dates['start'];
|
||||||
/** @var Carbon $end */
|
/** @var Carbon $end */
|
||||||
$end = $dates['end'];
|
$end = $dates['end'];
|
||||||
|
/** @var User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// group ID
|
||||||
|
$administrationId = $user->getAdministrationId();
|
||||||
|
$this->repository->setAdministrationId($administrationId);
|
||||||
|
|
||||||
// user's preferences
|
// user's preferences
|
||||||
$defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray();
|
$defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray();
|
||||||
@@ -98,33 +107,39 @@ class AccountController extends Controller
|
|||||||
}
|
}
|
||||||
$currentSet = [
|
$currentSet = [
|
||||||
'label' => $account->name,
|
'label' => $account->name,
|
||||||
|
// the currency that belongs to the account.
|
||||||
'currency_id' => (string)$currency->id,
|
'currency_id' => (string)$currency->id,
|
||||||
'currency_code' => $currency->code,
|
'currency_code' => $currency->code,
|
||||||
'currency_symbol' => $currency->symbol,
|
'currency_symbol' => $currency->symbol,
|
||||||
'currency_decimal_places' => $currency->decimal_places,
|
'currency_decimal_places' => $currency->decimal_places,
|
||||||
'native_id' => null,
|
|
||||||
'native_code' => null,
|
// the default currency of the user (may be the same!)
|
||||||
'native_symbol' => null,
|
'native_id' => $default->id,
|
||||||
'native_decimal_places' => null,
|
'native_code' => $default->code,
|
||||||
|
'native_symbol' => $default->symbol,
|
||||||
|
'native_decimal_places' => $default->decimal_places,
|
||||||
'start_date' => $start->toAtomString(),
|
'start_date' => $start->toAtomString(),
|
||||||
'end_date' => $end->toAtomString(),
|
'end_date' => $end->toAtomString(),
|
||||||
'type' => 'line', // line, area or bar
|
|
||||||
'yAxisID' => 0, // 0, 1, 2
|
|
||||||
'entries' => [],
|
'entries' => [],
|
||||||
|
'converted_entries' => [],
|
||||||
];
|
];
|
||||||
$currentStart = clone $start;
|
$currentStart = clone $start;
|
||||||
$range = app('steam')->balanceInRange($account, $start, clone $end);
|
$range = app('steam')->balanceInRange($account, $start, clone $end, $currency);
|
||||||
|
$rangeConverted = app('steam')->balanceInRangeConverted($account, $start, clone $end, $default);
|
||||||
// 2022-10-11: this method no longer converts to floats
|
|
||||||
|
|
||||||
$previous = array_values($range)[0];
|
$previous = array_values($range)[0];
|
||||||
|
$previousConverted = array_values($rangeConverted)[0];
|
||||||
while ($currentStart <= $end) {
|
while ($currentStart <= $end) {
|
||||||
$format = $currentStart->format('Y-m-d');
|
$format = $currentStart->format('Y-m-d');
|
||||||
$label = $currentStart->toAtomString();
|
$label = $currentStart->toAtomString();
|
||||||
$balance = array_key_exists($format, $range) ? $range[$format] : $previous;
|
$balance = array_key_exists($format, $range) ? $range[$format] : $previous;
|
||||||
|
$balanceConverted = array_key_exists($format, $rangeConverted) ? $rangeConverted[$format] : $previousConverted;
|
||||||
$previous = $balance;
|
$previous = $balance;
|
||||||
|
$previousConverted = $balanceConverted;
|
||||||
|
|
||||||
$currentStart->addDay();
|
$currentStart->addDay();
|
||||||
$currentSet['entries'][$label] = $balance;
|
$currentSet['entries'][$label] = $balance;
|
||||||
|
$currentSet['converted_entries'][$label] = $balanceConverted;
|
||||||
}
|
}
|
||||||
$currentSet = $this->cerChartSet($currentSet);
|
$currentSet = $this->cerChartSet($currentSet);
|
||||||
$chartData[] = $currentSet;
|
$chartData[] = $currentSet;
|
||||||
|
@@ -78,9 +78,9 @@ class Cron extends Command
|
|||||||
$force = (bool)$this->option('force');
|
$force = (bool)$this->option('force');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fire recurring transaction cron job.
|
* Fire exchange rates cron job.
|
||||||
*/
|
*/
|
||||||
if (true === config('cer.enabled')) {
|
if (true === config('cer.download_enabled')) {
|
||||||
try {
|
try {
|
||||||
$this->exchangeRatesCronJob($force, $date);
|
$this->exchangeRatesCronJob($force, $date);
|
||||||
} catch (FireflyException $e) {
|
} catch (FireflyException $e) {
|
||||||
|
@@ -121,7 +121,7 @@ class DownloadExchangeRates implements ShouldQueue
|
|||||||
app('log')->warning(sprintf('Trying to grab "%s" resulted in bad JSON.', $url));
|
app('log')->warning(sprintf('Trying to grab "%s" resulted in bad JSON.', $url));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$date = Carbon::createFromFormat('Y-m-d', $json['date']);
|
$date = Carbon::createFromFormat('Y-m-d', $json['date'], config('app.timezone'));
|
||||||
$this->saveRates($currency, $date, $json['rates']);
|
$this->saveRates($currency, $date, $json['rates']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,6 +25,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Repositories\Administration\Account;
|
namespace FireflyIII\Repositories\Administration\Account;
|
||||||
|
|
||||||
|
use FireflyIII\Models\Account;
|
||||||
|
use FireflyIII\Models\AccountMeta;
|
||||||
|
use FireflyIII\Models\AccountType;
|
||||||
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -35,6 +39,7 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
{
|
{
|
||||||
use AdministrationTrait;
|
use AdministrationTrait;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
@@ -62,4 +67,98 @@ class AccountRepository implements AccountRepositoryInterface
|
|||||||
|
|
||||||
return $dbQuery->take($limit)->get(['accounts.*']);
|
return $dbQuery->take($limit)->get(['accounts.*']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getAccountsByType(array $types, ?array $sort = []): Collection
|
||||||
|
{
|
||||||
|
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
||||||
|
$query = $this->userGroup->accounts();
|
||||||
|
if (0 !== count($types)) {
|
||||||
|
$query->accountTypeIn($types);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add sort parameters. At this point they're filtered to allowed fields to sort by:
|
||||||
|
if (0 !== count($sort)) {
|
||||||
|
foreach ($sort as $param) {
|
||||||
|
$query->orderBy($param[0], $param[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === count($sort)) {
|
||||||
|
if (0 !== count($res)) {
|
||||||
|
$query->orderBy('accounts.order', 'ASC');
|
||||||
|
}
|
||||||
|
$query->orderBy('accounts.active', 'DESC');
|
||||||
|
$query->orderBy('accounts.name', 'ASC');
|
||||||
|
}
|
||||||
|
return $query->get(['accounts.*']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $accountIds
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function getAccountsById(array $accountIds): Collection
|
||||||
|
{
|
||||||
|
$query = $this->userGroup->accounts();
|
||||||
|
|
||||||
|
if (0 !== count($accountIds)) {
|
||||||
|
$query->whereIn('accounts.id', $accountIds);
|
||||||
|
}
|
||||||
|
$query->orderBy('accounts.order', 'ASC');
|
||||||
|
$query->orderBy('accounts.active', 'DESC');
|
||||||
|
$query->orderBy('accounts.name', 'ASC');
|
||||||
|
|
||||||
|
return $query->get(['accounts.*']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Account $account
|
||||||
|
*
|
||||||
|
* @return TransactionCurrency|null
|
||||||
|
*/
|
||||||
|
public function getAccountCurrency(Account $account): ?TransactionCurrency
|
||||||
|
{
|
||||||
|
$type = $account->accountType->type;
|
||||||
|
$list = config('firefly.valid_currency_account_types');
|
||||||
|
|
||||||
|
// return null if not in this list.
|
||||||
|
if (!in_array($type, $list, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$currencyId = (int)$this->getMetaValue($account, 'currency_id');
|
||||||
|
if ($currencyId > 0) {
|
||||||
|
return TransactionCurrency::find($currencyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return meta value for account. Null if not found.
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @param string $field
|
||||||
|
*
|
||||||
|
* @return null|string
|
||||||
|
*/
|
||||||
|
public function getMetaValue(Account $account, string $field): ?string
|
||||||
|
{
|
||||||
|
$result = $account->accountMeta->filter(
|
||||||
|
function (AccountMeta $meta) use ($field) {
|
||||||
|
return strtolower($meta->name) === strtolower($field);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (0 === $result->count()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (1 === $result->count()) {
|
||||||
|
return (string)$result->first()->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Repositories\Administration\Account;
|
namespace FireflyIII\Repositories\Administration\Account;
|
||||||
|
|
||||||
|
use FireflyIII\Models\Account;
|
||||||
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,4 +42,36 @@ interface AccountRepositoryInterface
|
|||||||
* @return Collection
|
* @return Collection
|
||||||
*/
|
*/
|
||||||
public function searchAccount(string $query, array $types, int $limit): Collection;
|
public function searchAccount(string $query, array $types, int $limit): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $types
|
||||||
|
* @param array|null $sort
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function getAccountsByType(array $types, ?array $sort = []): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $accountIds
|
||||||
|
*
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function getAccountsById(array $accountIds): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Account $account
|
||||||
|
*
|
||||||
|
* @return TransactionCurrency|null
|
||||||
|
*/
|
||||||
|
public function getAccountCurrency(Account $account): ?TransactionCurrency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return meta value for account. Null if not found.
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @param string $field
|
||||||
|
*
|
||||||
|
* @return null|string
|
||||||
|
*/
|
||||||
|
public function getMetaValue(Account $account, string $field): ?string;
|
||||||
}
|
}
|
||||||
|
@@ -26,9 +26,10 @@ namespace FireflyIII\Support\Http\Api;
|
|||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
use FireflyIII\Models\CurrencyExchangeRate;
|
use FireflyIII\Models\CurrencyExchangeRate;
|
||||||
use FireflyIII\Models\TransactionCurrency;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use Illuminate\Support\Facades\Log;
|
use FireflyIII\Support\CacheProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trait ConvertsExchangeRates
|
* Trait ConvertsExchangeRates
|
||||||
@@ -49,6 +50,8 @@ trait ConvertsExchangeRates
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if not enabled, return the same array but without conversion:
|
// if not enabled, return the same array but without conversion:
|
||||||
|
return $set;
|
||||||
|
$this->enabled = false;
|
||||||
if (false === $this->enabled) {
|
if (false === $this->enabled) {
|
||||||
$set['converted'] = false;
|
$set['converted'] = false;
|
||||||
return $set;
|
return $set;
|
||||||
@@ -69,7 +72,7 @@ trait ConvertsExchangeRates
|
|||||||
$carbon = Carbon::createFromFormat(DateTimeInterface::ATOM, $date);
|
$carbon = Carbon::createFromFormat(DateTimeInterface::ATOM, $date);
|
||||||
$rate = $this->getRate($currency, $native, $carbon);
|
$rate = $this->getRate($currency, $native, $carbon);
|
||||||
$rate = '0' === $rate ? '1' : $rate;
|
$rate = '0' === $rate ? '1' : $rate;
|
||||||
Log::debug(sprintf('bcmul("%s", "%s")', (string)$entry, $rate));
|
app('log')->debug(sprintf('bcmul("%s", "%s")', (string)$entry, $rate));
|
||||||
$set['entries'][$date] = (float)bcmul((string)$entry, $rate);
|
$set['entries'][$date] = (float)bcmul((string)$entry, $rate);
|
||||||
}
|
}
|
||||||
return $set;
|
return $set;
|
||||||
@@ -80,7 +83,7 @@ trait ConvertsExchangeRates
|
|||||||
*/
|
*/
|
||||||
private function getPreference(): void
|
private function getPreference(): void
|
||||||
{
|
{
|
||||||
$this->enabled = true;
|
$this->enabled = config('cer.currency_conversion');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,55 +106,65 @@ trait ConvertsExchangeRates
|
|||||||
* @param Carbon $date
|
* @param Carbon $date
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
|
* @throws FireflyException
|
||||||
*/
|
*/
|
||||||
private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('getRate(%s, %s, "%s")', $from->code, $to->code, $date->format('Y-m-d')));
|
// first attempt:
|
||||||
|
$rate = $this->getFromDB((int)$from->id, (int)$to->id, $date->format('Y-m-d'));
|
||||||
|
if (null !== $rate) {
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
// no result. perhaps the other way around?
|
||||||
|
$rate = $this->getFromDB((int)$to->id, (int)$from->id, $date->format('Y-m-d'));
|
||||||
|
if (null !== $rate) {
|
||||||
|
return bcdiv('1', $rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if nothing in place, fall back on the rate for $from to EUR
|
||||||
|
$first = $this->getEuroRate($from, $date);
|
||||||
|
$second = $this->getEuroRate($to, $date);
|
||||||
|
|
||||||
|
// combined (if present), they can be used to calculate the necessary conversion rate.
|
||||||
|
if ('0' === $first || '0' === $second) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
$second = bcdiv('1', $second);
|
||||||
|
return bcmul($first, $second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $from
|
||||||
|
* @param int $to
|
||||||
|
* @param string $date
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function getFromDB(int $from, int $to, string $date): ?string
|
||||||
|
{
|
||||||
|
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
|
||||||
|
|
||||||
|
$cache = new CacheProperties();
|
||||||
|
$cache->addProperty($key);
|
||||||
|
if ($cache->has()) {
|
||||||
|
return $cache->get();
|
||||||
|
}
|
||||||
|
|
||||||
/** @var CurrencyExchangeRate $result */
|
/** @var CurrencyExchangeRate $result */
|
||||||
$result = auth()->user()
|
$result = auth()->user()
|
||||||
->currencyExchangeRates()
|
->currencyExchangeRates()
|
||||||
->where('from_currency_id', $from->id)
|
->where('from_currency_id', $from)
|
||||||
->where('to_currency_id', $to->id)
|
->where('to_currency_id', $to)
|
||||||
->where('date', '<=', $date->format('Y-m-d'))
|
->where('date', '<=', $date)
|
||||||
->orderBy('date', 'DESC')
|
->orderBy('date', 'DESC')
|
||||||
->first();
|
->first();
|
||||||
if (null !== $result) {
|
if (null !== $result) {
|
||||||
$rate = (string)$result->rate;
|
$rate = (string)$result->rate;
|
||||||
Log::debug(sprintf('Rate is %s', $rate));
|
$cache->store($rate);
|
||||||
return $rate;
|
return $rate;
|
||||||
}
|
}
|
||||||
// no result. perhaps the other way around?
|
return null;
|
||||||
/** @var CurrencyExchangeRate $result */
|
|
||||||
$result = auth()->user()
|
|
||||||
->currencyExchangeRates()
|
|
||||||
->where('from_currency_id', $to->id)
|
|
||||||
->where('to_currency_id', $from->id)
|
|
||||||
->where('date', '<=', $date->format('Y-m-d'))
|
|
||||||
->orderBy('date', 'DESC')
|
|
||||||
->first();
|
|
||||||
if (null !== $result) {
|
|
||||||
$rate = bcdiv('1', (string)$result->rate);
|
|
||||||
Log::debug(sprintf('Reversed rate is %s', $rate));
|
|
||||||
return $rate;
|
|
||||||
}
|
|
||||||
// try euro rates
|
|
||||||
$result1 = $this->getEuroRate($from, $date);
|
|
||||||
if ('0' === $result1) {
|
|
||||||
Log::debug(sprintf('No exchange rate between EUR and %s', $from->code));
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
$result2 = $this->getEuroRate($to, $date);
|
|
||||||
if ('0' === $result2) {
|
|
||||||
Log::debug(sprintf('No exchange rate between EUR and %s', $to->code));
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
// still need to inverse rate 2:
|
|
||||||
$result2 = bcdiv('1', $result2);
|
|
||||||
$rate = bcmul($result1, $result2);
|
|
||||||
Log::debug(sprintf('Rate %s to EUR is %s', $from->code, $result1));
|
|
||||||
Log::debug(sprintf('Rate EUR to %s is %s', $to->code, $result2));
|
|
||||||
Log::debug(sprintf('Rate for %s to %s is %s', $from->code, $to->code, $rate));
|
|
||||||
return $rate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,47 +172,55 @@ trait ConvertsExchangeRates
|
|||||||
* @param Carbon $date
|
* @param Carbon $date
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
|
* @throws FireflyException
|
||||||
*/
|
*/
|
||||||
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
|
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('Find rate for %s to Euro', $currency->code));
|
$euroId = $this->getEuroId();
|
||||||
|
if ($euroId === (int)$currency->id) {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
$rate = $this->getFromDB((int)$currency->id, $euroId, $date->format('Y-m-d'));
|
||||||
|
|
||||||
|
if (null !== $rate) {
|
||||||
|
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
$rate = $this->getFromDB($euroId, (int)$currency->id, $date->format('Y-m-d'));
|
||||||
|
if (null !== $rate) {
|
||||||
|
$rate = bcdiv('1', $rate);
|
||||||
|
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
|
||||||
|
return $rate;
|
||||||
|
}
|
||||||
|
// grab backup values from config file:
|
||||||
|
$backup = config(sprintf('cer.rates.%s', $currency->code));
|
||||||
|
if (null !== $backup) {
|
||||||
|
$backup = bcdiv('1', (string)$backup);
|
||||||
|
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
|
||||||
|
return $backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
* @throws FireflyException
|
||||||
|
*/
|
||||||
|
private function getEuroId(): int
|
||||||
|
{
|
||||||
|
$cache = new CacheProperties();
|
||||||
|
$cache->addProperty('cer-euro-id');
|
||||||
|
if ($cache->has()) {
|
||||||
|
return $cache->get();
|
||||||
|
}
|
||||||
$euro = TransactionCurrency::whereCode('EUR')->first();
|
$euro = TransactionCurrency::whereCode('EUR')->first();
|
||||||
if (null === $euro) {
|
if (null === $euro) {
|
||||||
app('log')->warning('Cannot do indirect conversion without EUR.');
|
throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.');
|
||||||
return '0';
|
|
||||||
}
|
}
|
||||||
|
$cache->store((int)$euro->id);
|
||||||
// try one way:
|
return (int)$euro->id;
|
||||||
/** @var CurrencyExchangeRate $result */
|
|
||||||
$result = auth()->user()
|
|
||||||
->currencyExchangeRates()
|
|
||||||
->where('from_currency_id', $currency->id)
|
|
||||||
->where('to_currency_id', $euro->id)
|
|
||||||
->where('date', '<=', $date->format('Y-m-d'))
|
|
||||||
->orderBy('date', 'DESC')
|
|
||||||
->first();
|
|
||||||
if (null !== $result) {
|
|
||||||
$rate = (string)$result->rate;
|
|
||||||
Log::debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
|
|
||||||
return $rate;
|
|
||||||
}
|
|
||||||
// try the other way around and inverse it.
|
|
||||||
/** @var CurrencyExchangeRate $result */
|
|
||||||
$result = auth()->user()
|
|
||||||
->currencyExchangeRates()
|
|
||||||
->where('from_currency_id', $euro->id)
|
|
||||||
->where('to_currency_id', $currency->id)
|
|
||||||
->where('date', '<=', $date->format('Y-m-d'))
|
|
||||||
->orderBy('date', 'DESC')
|
|
||||||
->first();
|
|
||||||
if (null !== $result) {
|
|
||||||
$rate = bcdiv('1', (string)$result->rate);
|
|
||||||
Log::debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
|
|
||||||
return $rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::debug(sprintf('No rate for %s to EUR.', $currency->code));
|
|
||||||
return '0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,7 +289,7 @@ trait ConvertsExchangeRates
|
|||||||
*/
|
*/
|
||||||
private function convertAmount(string $amount, TransactionCurrency $from, TransactionCurrency $to, ?Carbon $date = null): string
|
private function convertAmount(string $amount, TransactionCurrency $from, TransactionCurrency $to, ?Carbon $date = null): string
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('Converting %s from %s to %s', $amount, $from->code, $to->code));
|
app('log')->debug(sprintf('Converting %s from %s to %s', $amount, $from->code, $to->code));
|
||||||
$date = $date ?? today(config('app.timezone'));
|
$date = $date ?? today(config('app.timezone'));
|
||||||
$rate = $this->getRate($from, $to, $date);
|
$rate = $this->getRate($from, $to, $date);
|
||||||
|
|
||||||
|
59
app/Support/Http/Api/ExchangeRateConverter.php
Normal file
59
app/Support/Http/Api/ExchangeRateConverter.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ExchangeRateConverter.php
|
||||||
|
* Copyright (c) 2023 james@firefly-iii.org
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace FireflyIII\Support\Http\Api;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ExchangeRateConverter
|
||||||
|
*/
|
||||||
|
class ExchangeRateConverter
|
||||||
|
{
|
||||||
|
use ConvertsExchangeRates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TransactionCurrency $from
|
||||||
|
* @param TransactionCurrency $to
|
||||||
|
* @param Carbon $date
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws FireflyException
|
||||||
|
*/
|
||||||
|
public function getCurrencyRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||||
|
{
|
||||||
|
if (null === $this->enabled) {
|
||||||
|
$this->getPreference();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not enabled, return "1"
|
||||||
|
if (false === $this->enabled) {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = $this->getRate($from, $to, $date);
|
||||||
|
return '0' === $rate ? '1' : $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -73,6 +73,9 @@ trait AdministrationTrait
|
|||||||
throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $this->administrationId));
|
throw new FireflyException(sprintf('User #%d has no access to administration #%d', $this->user->id, $this->administrationId));
|
||||||
}
|
}
|
||||||
$this->userGroup = UserGroup::find($this->administrationId);
|
$this->userGroup = UserGroup::find($this->administrationId);
|
||||||
|
if (null === $this->userGroup) {
|
||||||
|
throw new FireflyException(sprintf('Unfound administration for user #%d', $this->user->id));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new FireflyException(sprintf('Cannot validate administration for user #%d', $this->user->id));
|
throw new FireflyException(sprintf('Cannot validate administration for user #%d', $this->user->id));
|
||||||
|
@@ -57,7 +57,7 @@ trait FiltersWeekends
|
|||||||
$isWeekend = $date->isWeekend();
|
$isWeekend = $date->isWeekend();
|
||||||
if (!$isWeekend) {
|
if (!$isWeekend) {
|
||||||
$return[] = clone $date;
|
$return[] = clone $date;
|
||||||
Log::debug(sprintf('Date is %s, not a weekend date.', $date->format('D d M Y')));
|
//Log::debug(sprintf('Date is %s, not a weekend date.', $date->format('D d M Y')));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ trait FiltersWeekends
|
|||||||
$return[] = $clone;
|
$return[] = $clone;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Log::debug(sprintf('Date is %s, removed from final result', $date->format('D d M Y')));
|
//Log::debug(sprintf('Date is %s, removed from final result', $date->format('D d M Y')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter unique dates
|
// filter unique dates
|
||||||
|
@@ -31,6 +31,7 @@ use FireflyIII\Models\Account;
|
|||||||
use FireflyIII\Models\Transaction;
|
use FireflyIII\Models\Transaction;
|
||||||
use FireflyIII\Models\TransactionCurrency;
|
use FireflyIII\Models\TransactionCurrency;
|
||||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||||
|
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
@@ -114,7 +115,6 @@ class Steam
|
|||||||
*/
|
*/
|
||||||
public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array
|
public function balanceInRange(Account $account, Carbon $start, Carbon $end, ?TransactionCurrency $currency = null): array
|
||||||
{
|
{
|
||||||
// abuse chart properties:
|
|
||||||
$cache = new CacheProperties();
|
$cache = new CacheProperties();
|
||||||
$cache->addProperty($account->id);
|
$cache->addProperty($account->id);
|
||||||
$cache->addProperty('balance-in-range');
|
$cache->addProperty('balance-in-range');
|
||||||
@@ -238,6 +238,235 @@ class Steam
|
|||||||
return $balance;
|
return $balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Account $account
|
||||||
|
* @param Carbon $start
|
||||||
|
* @param Carbon $end
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* @throws FireflyException
|
||||||
|
*/
|
||||||
|
public function balanceInRangeConverted(Account $account, Carbon $start, Carbon $end, TransactionCurrency $native): array
|
||||||
|
{
|
||||||
|
|
||||||
|
$cache = new CacheProperties();
|
||||||
|
$cache->addProperty($account->id);
|
||||||
|
$cache->addProperty('balance-in-range-converted');
|
||||||
|
$cache->addProperty($native->id);
|
||||||
|
$cache->addProperty($start);
|
||||||
|
$cache->addProperty($end);
|
||||||
|
if ($cache->has()) {
|
||||||
|
return $cache->get();
|
||||||
|
}
|
||||||
|
app('log')->debug(sprintf('balanceInRangeConverted for account #%d to %s', $account->id, $native->code));
|
||||||
|
$start->subDay();
|
||||||
|
$end->addDay();
|
||||||
|
$balances = [];
|
||||||
|
$formatted = $start->format('Y-m-d');
|
||||||
|
$currencies = [];
|
||||||
|
$startBalance = $this->balanceConverted($account, $start, $native); // already converted to native amount
|
||||||
|
$balances[$formatted] = $startBalance;
|
||||||
|
|
||||||
|
app('log')->debug(sprintf('Start balance on %s is %s', $formatted, $startBalance));
|
||||||
|
|
||||||
|
$converter = new ExchangeRateConverter();
|
||||||
|
|
||||||
|
// not sure why this is happening:
|
||||||
|
$start->addDay();
|
||||||
|
|
||||||
|
// grab all transactions between start and end:
|
||||||
|
$set = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
|
||||||
|
->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00'))
|
||||||
|
->where('transaction_journals.date', '<=', $end->format('Y-m-d 23:59:59'))
|
||||||
|
->orderBy('transaction_journals.date', 'ASC')
|
||||||
|
->whereNull('transaction_journals.deleted_at')
|
||||||
|
->get(
|
||||||
|
[
|
||||||
|
'transaction_journals.date',
|
||||||
|
'transactions.transaction_currency_id',
|
||||||
|
'transactions.amount',
|
||||||
|
'transactions.foreign_currency_id',
|
||||||
|
'transactions.foreign_amount',
|
||||||
|
]
|
||||||
|
)->toArray();
|
||||||
|
|
||||||
|
// loop the set and convert if necessary:
|
||||||
|
$currentBalance = $startBalance;
|
||||||
|
/** @var Transaction $transaction */
|
||||||
|
foreach ($set as $transaction) {
|
||||||
|
$day = Carbon::createFromFormat('Y-m-d H:i:s', $transaction['date'], config('app.timezone'));
|
||||||
|
$format = $day->format('Y-m-d');
|
||||||
|
// if the transaction is in the expected currency, change nothing.
|
||||||
|
if ((int)$transaction['transaction_currency_id'] === (int)$native->id) {
|
||||||
|
// change the current balance, set it to today, continue the loop.
|
||||||
|
$currentBalance = bcadd($currentBalance, $transaction['amount']);
|
||||||
|
$balances[$format] = $currentBalance;
|
||||||
|
app('log')->debug(sprintf('%s: transaction in %s, new balance is %s.', $format, $native->code, $currentBalance));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// if foreign currency is in the expected currency, do nothing:
|
||||||
|
if ((int)$transaction['foreign_currency_id'] === (int)$native->id) {
|
||||||
|
$currentBalance = bcadd($currentBalance, $transaction['foreign_amount']);
|
||||||
|
$balances[$format] = $currentBalance;
|
||||||
|
app('log')->debug(sprintf('%s: transaction in %s (foreign), new balance is %s.', $format, $native->code, $currentBalance));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// otherwise, convert 'amount' to the necessary currency:
|
||||||
|
$currencyId = (int)$transaction['transaction_currency_id'];
|
||||||
|
$currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId);
|
||||||
|
|
||||||
|
|
||||||
|
$rate = $converter->getCurrencyRate($currency, $native, $day);
|
||||||
|
$convertedAmount = bcmul($transaction['amount'], $rate);
|
||||||
|
$currentBalance = bcadd($currentBalance, $convertedAmount);
|
||||||
|
$balances[$format] = $currentBalance;
|
||||||
|
|
||||||
|
app('log')->debug(sprintf('%s: transaction in %s(!). Conversion rate is %s. %s %s = %s %s', $format, $currency->code, $rate,
|
||||||
|
$currency->code, $transaction['amount'],
|
||||||
|
$native->code, $convertedAmount
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache->store($balances);
|
||||||
|
|
||||||
|
return $balances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets balance at the end of current month by default. Returns the balance converted
|
||||||
|
* to the indicated currency ($native).
|
||||||
|
*
|
||||||
|
* @param Account $account
|
||||||
|
* @param Carbon $date
|
||||||
|
* @param TransactionCurrency $native
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* @throws FireflyException
|
||||||
|
*/
|
||||||
|
public function balanceConverted(Account $account, Carbon $date, TransactionCurrency $native): string
|
||||||
|
{
|
||||||
|
app('log')->debug(sprintf('Now in balanceConverted (%s) for account #%d, converting to %s', $date->format('Y-m-d'), $account->id, $native->code));
|
||||||
|
// abuse chart properties:
|
||||||
|
$cache = new CacheProperties();
|
||||||
|
$cache->addProperty($account->id);
|
||||||
|
$cache->addProperty('balance');
|
||||||
|
$cache->addProperty($date);
|
||||||
|
$cache->addProperty($native ? $native->id : 0);
|
||||||
|
if ($cache->has()) {
|
||||||
|
return $cache->get();
|
||||||
|
}
|
||||||
|
/** @var AccountRepositoryInterface $repository */
|
||||||
|
$repository = app(AccountRepositoryInterface::class);
|
||||||
|
$currency = $repository->getAccountCurrency($account);
|
||||||
|
if (null === $currency) {
|
||||||
|
throw new FireflyException('Cannot get converted account balance: no currency found for account.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* selection of transactions
|
||||||
|
* 1: all normal transactions. No foreign currency info. In $currency. Need conversion.
|
||||||
|
* 2: all normal transactions. No foreign currency info. In $native. Need NO conversion.
|
||||||
|
* 3: all normal transactions. No foreign currency info. In neither currency. Need conversion.
|
||||||
|
* Then, select everything with foreign currency info:
|
||||||
|
* 4. All transactions with foreign currency info in $native. Normal currency value is ignored. Do not need conversion.
|
||||||
|
* 5. All transactions with foreign currency info NOT in $native, but currency info in $currency. Need conversion.
|
||||||
|
* 6. All transactions with foreign currency info NOT in $native, and currency info NOT in $currency. Need conversion.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
$new = [];
|
||||||
|
$existing = [];
|
||||||
|
// 1
|
||||||
|
$new[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.transaction_currency_id', $currency->id)
|
||||||
|
->whereNull('transactions.foreign_currency_id')
|
||||||
|
->get(['transaction_journals.date', 'transactions.amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #1', count($new[0])));
|
||||||
|
// 2
|
||||||
|
$existing[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.transaction_currency_id', $native->id)
|
||||||
|
->whereNull('transactions.foreign_currency_id')
|
||||||
|
->get(['transactions.amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #2', count($existing[0])));
|
||||||
|
// 3
|
||||||
|
$new[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.transaction_currency_id', '!=', $currency->id)
|
||||||
|
->where('transactions.transaction_currency_id', '!=', $native->id)
|
||||||
|
->whereNull('transactions.foreign_currency_id')
|
||||||
|
->get(['transaction_journals.date', 'transactions.amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #3', count($new[1])));
|
||||||
|
// 4
|
||||||
|
$existing[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.foreign_currency_id', $native->id)
|
||||||
|
->whereNotNull('transactions.foreign_amount')
|
||||||
|
->get(['transactions.foreign_amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #4', count($existing[1])));
|
||||||
|
// 5
|
||||||
|
$new[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.transaction_currency_id', $currency->id)
|
||||||
|
->where('transactions.foreign_currency_id', '!=', $native->id)
|
||||||
|
->whereNotNull('transactions.foreign_amount')
|
||||||
|
->get(['transaction_journals.date', 'transactions.amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #5', count($new[2])));
|
||||||
|
// 6
|
||||||
|
$new[] = $account->transactions()
|
||||||
|
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||||
|
->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'))
|
||||||
|
->where('transactions.transaction_currency_id', '!=', $currency->id)
|
||||||
|
->where('transactions.foreign_currency_id', '!=', $native->id)
|
||||||
|
->whereNotNull('transactions.foreign_amount')
|
||||||
|
->get(['transaction_journals.date', 'transactions.amount'])->toArray();
|
||||||
|
app('log')->debug(sprintf('%d transactions in set #6', count($new[3])));
|
||||||
|
|
||||||
|
// process both sets of transactions. Of course, no need to convert set "existing".
|
||||||
|
$balance = $this->sumTransactions($existing[0], 'amount');
|
||||||
|
$balance = bcadd($balance, $this->sumTransactions($existing[1], 'foreign_amount'));
|
||||||
|
//app('log')->debug(sprintf('Balance from set #2 and #4 is %f', $balance));
|
||||||
|
|
||||||
|
// need to convert the others. All sets use the "amount" value as their base (that's easy)
|
||||||
|
// but we need to convert each transaction separately because the date difference may
|
||||||
|
// incur huge currency changes.
|
||||||
|
$converter = new ExchangeRateConverter();
|
||||||
|
foreach ($new as $index => $set) {
|
||||||
|
foreach ($set as $transaction) {
|
||||||
|
$date = Carbon::createFromFormat('Y-m-d H:i:s', $transaction['date']);
|
||||||
|
$rate = $converter->getCurrencyRate($currency, $native, $date);
|
||||||
|
$convertedAmount = bcmul($transaction['amount'], $rate);
|
||||||
|
$balance = bcadd($balance, $convertedAmount);
|
||||||
|
// app('log')->debug(sprintf('Date: %s, rate: %s, amount: %s %s, new: %s %s',
|
||||||
|
// $date->format('Y-m-d'),
|
||||||
|
// $rate,
|
||||||
|
// $currency->code,
|
||||||
|
// $transaction['amount'],
|
||||||
|
// $native->code,
|
||||||
|
// $convertedAmount
|
||||||
|
// ));
|
||||||
|
}
|
||||||
|
//app('log')->debug(sprintf('Balance from new set #%d is %f', $index, $balance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add virtual balance
|
||||||
|
$virtual = null === $account->virtual_balance ? '0' : (string)$account->virtual_balance;
|
||||||
|
$balance = bcadd($balance, $virtual);
|
||||||
|
|
||||||
|
$cache->store($balance);
|
||||||
|
|
||||||
|
return $balance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method always ignores the virtual balance.
|
* This method always ignores the virtual balance.
|
||||||
*
|
*
|
||||||
|
79
composer.lock
generated
79
composer.lock
generated
@@ -473,16 +473,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/dbal",
|
"name": "doctrine/dbal",
|
||||||
"version": "3.6.4",
|
"version": "3.6.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/doctrine/dbal.git",
|
"url": "https://github.com/doctrine/dbal.git",
|
||||||
"reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f"
|
"reference": "96d5a70fd91efdcec81fc46316efc5bf3da17ddf"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
|
"url": "https://api.github.com/repos/doctrine/dbal/zipball/96d5a70fd91efdcec81fc46316efc5bf3da17ddf",
|
||||||
"reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
|
"reference": "96d5a70fd91efdcec81fc46316efc5bf3da17ddf",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -497,10 +497,10 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/coding-standard": "12.0.0",
|
"doctrine/coding-standard": "12.0.0",
|
||||||
"fig/log-test": "^1",
|
"fig/log-test": "^1",
|
||||||
"jetbrains/phpstorm-stubs": "2022.3",
|
"jetbrains/phpstorm-stubs": "2023.1",
|
||||||
"phpstan/phpstan": "1.10.14",
|
"phpstan/phpstan": "1.10.21",
|
||||||
"phpstan/phpstan-strict-rules": "^1.5",
|
"phpstan/phpstan-strict-rules": "^1.5",
|
||||||
"phpunit/phpunit": "9.6.7",
|
"phpunit/phpunit": "9.6.9",
|
||||||
"psalm/plugin-phpunit": "0.18.4",
|
"psalm/plugin-phpunit": "0.18.4",
|
||||||
"squizlabs/php_codesniffer": "3.7.2",
|
"squizlabs/php_codesniffer": "3.7.2",
|
||||||
"symfony/cache": "^5.4|^6.0",
|
"symfony/cache": "^5.4|^6.0",
|
||||||
@@ -565,7 +565,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/doctrine/dbal/issues",
|
"issues": "https://github.com/doctrine/dbal/issues",
|
||||||
"source": "https://github.com/doctrine/dbal/tree/3.6.4"
|
"source": "https://github.com/doctrine/dbal/tree/3.6.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -581,7 +581,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-06-15T07:40:12+00:00"
|
"time": "2023-07-17T09:15:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/deprecations",
|
"name": "doctrine/deprecations",
|
||||||
@@ -5661,16 +5661,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-html",
|
"name": "spatie/laravel-html",
|
||||||
"version": "3.2.1",
|
"version": "3.2.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/spatie/laravel-html.git",
|
"url": "https://github.com/spatie/laravel-html.git",
|
||||||
"reference": "bf7bdb55cc5ce15c4ec8134aa1df709c0397c397"
|
"reference": "f9dac9f250735dd01d80e023f5acf6f3d10d3b3f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/spatie/laravel-html/zipball/bf7bdb55cc5ce15c4ec8134aa1df709c0397c397",
|
"url": "https://api.github.com/repos/spatie/laravel-html/zipball/f9dac9f250735dd01d80e023f5acf6f3d10d3b3f",
|
||||||
"reference": "bf7bdb55cc5ce15c4ec8134aa1df709c0397c397",
|
"reference": "f9dac9f250735dd01d80e023f5acf6f3d10d3b3f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -5727,7 +5727,7 @@
|
|||||||
"spatie"
|
"spatie"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/spatie/laravel-html/tree/3.2.1"
|
"source": "https://github.com/spatie/laravel-html/tree/3.2.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5735,7 +5735,7 @@
|
|||||||
"type": "custom"
|
"type": "custom"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-01-24T23:47:16+00:00"
|
"time": "2023-07-20T18:59:53+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-ignition",
|
"name": "spatie/laravel-ignition",
|
||||||
@@ -9339,37 +9339,33 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mockery/mockery",
|
"name": "mockery/mockery",
|
||||||
"version": "1.6.2",
|
"version": "1.6.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/mockery/mockery.git",
|
"url": "https://github.com/mockery/mockery.git",
|
||||||
"reference": "13a7fa2642c76c58fa2806ef7f565344c817a191"
|
"reference": "d1413755e26fe56a63455f7753221c86cbb88f66"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/mockery/mockery/zipball/13a7fa2642c76c58fa2806ef7f565344c817a191",
|
"url": "https://api.github.com/repos/mockery/mockery/zipball/d1413755e26fe56a63455f7753221c86cbb88f66",
|
||||||
"reference": "13a7fa2642c76c58fa2806ef7f565344c817a191",
|
"reference": "d1413755e26fe56a63455f7753221c86cbb88f66",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"hamcrest/hamcrest-php": "^2.0.1",
|
"hamcrest/hamcrest-php": "^2.0.1",
|
||||||
"lib-pcre": ">=7.0",
|
"lib-pcre": ">=7.0",
|
||||||
"php": "^7.4 || ^8.0"
|
"php": ">=7.4,<8.3"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"phpunit/phpunit": "<8.0"
|
"phpunit/phpunit": "<8.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^8.5 || ^9.3",
|
"phpunit/phpunit": "^8.5 || ^9.3",
|
||||||
"psalm/plugin-phpunit": "^0.18",
|
"psalm/plugin-phpunit": "^0.18.4",
|
||||||
"vimeo/psalm": "^5.9"
|
"symplify/easy-coding-standard": "^11.5.0",
|
||||||
|
"vimeo/psalm": "^5.13.1"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "1.6.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"files": [
|
"files": [
|
||||||
"library/helpers.php",
|
"library/helpers.php",
|
||||||
@@ -9387,12 +9383,20 @@
|
|||||||
{
|
{
|
||||||
"name": "Pádraic Brady",
|
"name": "Pádraic Brady",
|
||||||
"email": "padraic.brady@gmail.com",
|
"email": "padraic.brady@gmail.com",
|
||||||
"homepage": "http://blog.astrumfutura.com"
|
"homepage": "https://github.com/padraic",
|
||||||
|
"role": "Author"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dave Marshall",
|
"name": "Dave Marshall",
|
||||||
"email": "dave.marshall@atstsolutions.co.uk",
|
"email": "dave.marshall@atstsolutions.co.uk",
|
||||||
"homepage": "http://davedevelopment.co.uk"
|
"homepage": "https://davedevelopment.co.uk",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nathanael Esayeas",
|
||||||
|
"email": "nathanael.esayeas@protonmail.com",
|
||||||
|
"homepage": "https://github.com/ghostwriter",
|
||||||
|
"role": "Lead Developer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Mockery is a simple yet flexible PHP mock object framework",
|
"description": "Mockery is a simple yet flexible PHP mock object framework",
|
||||||
@@ -9410,10 +9414,13 @@
|
|||||||
"testing"
|
"testing"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
|
"docs": "https://docs.mockery.io/",
|
||||||
"issues": "https://github.com/mockery/mockery/issues",
|
"issues": "https://github.com/mockery/mockery/issues",
|
||||||
"source": "https://github.com/mockery/mockery/tree/1.6.2"
|
"rss": "https://github.com/mockery/mockery/releases.atom",
|
||||||
|
"security": "https://github.com/mockery/mockery/security/advisories",
|
||||||
|
"source": "https://github.com/mockery/mockery"
|
||||||
},
|
},
|
||||||
"time": "2023-06-07T09:07:52+00:00"
|
"time": "2023-07-19T15:51:02+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
@@ -9984,16 +9991,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "1.10.25",
|
"version": "1.10.26",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpstan/phpstan.git",
|
"url": "https://github.com/phpstan/phpstan.git",
|
||||||
"reference": "578f4e70d117f9a90699324c555922800ac38d8c"
|
"reference": "5d660cbb7e1b89253a47147ae44044f49832351f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/578f4e70d117f9a90699324c555922800ac38d8c",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/5d660cbb7e1b89253a47147ae44044f49832351f",
|
||||||
"reference": "578f4e70d117f9a90699324c555922800ac38d8c",
|
"reference": "5d660cbb7e1b89253a47147ae44044f49832351f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -10042,7 +10049,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-07-06T12:11:37+00:00"
|
"time": "2023-07-19T12:44:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan-deprecation-rules",
|
"name": "phpstan/phpstan-deprecation-rules",
|
||||||
|
@@ -25,49 +25,52 @@ declare(strict_types=1);
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'url' => 'https://ff3exchangerates.z6.web.core.windows.net',
|
'url' => 'https://ff3exchangerates.z6.web.core.windows.net',
|
||||||
'enabled' => env('ENABLE_EXTERNAL_RATES', false),
|
'enabled' => true,
|
||||||
|
'download_enabled' => env('ENABLE_EXTERNAL_RATES', false),
|
||||||
|
|
||||||
// if currencies are added, default rates must be added as well!
|
// if currencies are added, default rates must be added as well!
|
||||||
// last exchange rate update: 6-6-2022
|
// last exchange rate update: 6-6-2022
|
||||||
// source: https://www.xe.com/currencyconverter/
|
// source: https://www.xe.com/currencyconverter/
|
||||||
'date' => '2022-06-06',
|
'date' => '2022-06-06',
|
||||||
|
|
||||||
|
// all rates are from EUR to $currency:
|
||||||
'rates' => [
|
'rates' => [
|
||||||
|
|
||||||
// europa
|
// europa
|
||||||
['EUR', 'HUF', 387.9629],
|
'EUR' => 1,
|
||||||
['EUR', 'GBP', 0.85420754],
|
'HUF' => 387.9629,
|
||||||
['EUR', 'UAH', 31.659752],
|
'GBP' => 0.85420754,
|
||||||
['EUR', 'PLN', 4.581788],
|
'UAH' => 31.659752,
|
||||||
['EUR', 'TRY', 17.801397],
|
'PLN' => 4.581788,
|
||||||
['EUR', 'DKK', 7.4389753],
|
'TRY' => 17.801397,
|
||||||
|
'DKK' => 7.4389753,
|
||||||
|
|
||||||
// Americas
|
// Americas
|
||||||
['EUR', 'USD', 1.0722281],
|
'USD' => 1.0722281,
|
||||||
['EUR', 'BRL', 5.0973173],
|
'BRL' => 5.0973173,
|
||||||
['EUR', 'CAD', 1.3459969],
|
'CAD' => 1.3459969,
|
||||||
['EUR', 'MXN', 20.899824],
|
'MXN' => 20.899824,
|
||||||
|
|
||||||
// Oceania currencies
|
// Oceania currencies
|
||||||
['EUR', 'IDR', 15466.299],
|
'IDR' => 15466.299,
|
||||||
['EUR', 'AUD', 1.4838549],
|
'AUD' => 1.4838549,
|
||||||
['EUR', 'NZD', 1.6425829],
|
'NZD' => 1.6425829,
|
||||||
|
|
||||||
// africa
|
// africa
|
||||||
['EUR', 'EGP', 19.99735],
|
'EGP' => 19.99735,
|
||||||
['EUR', 'MAD', 10.573307],
|
'MAD' => 10.573307,
|
||||||
['EUR', 'ZAR', 16.413167],
|
'ZAR' => 16.413167,
|
||||||
|
|
||||||
// asia
|
// asia
|
||||||
['EUR', 'JPY', 140.15257],
|
'JPY' => 140.15257,
|
||||||
['EUR', 'RMB', 7.1194265],
|
'RMB' => 7.1194265,
|
||||||
['EUR', 'RUB', 66.000895],
|
'CNY' => 1,
|
||||||
['EUR', 'INR', 83.220481],
|
'RUB' => 66.000895,
|
||||||
|
'INR' => 83.220481,
|
||||||
|
|
||||||
// int
|
// int
|
||||||
['EUR', 'XBT', 0, 00003417],
|
'ILS' => 3.5712508,
|
||||||
['EUR', 'BCH', 0.00573987],
|
'CHF' => 1.0323891,
|
||||||
['EUR', 'ETH', 0, 00056204],
|
'HRK' => 7.5220845,
|
||||||
|
|
||||||
['EUR', 'ILS', 3.5712508],
|
|
||||||
['EUR', 'CHF', 1.0323891],
|
|
||||||
['EUR', 'HRK', 7.5220845],
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@@ -106,6 +106,7 @@ return [
|
|||||||
'telemetry' => false,
|
'telemetry' => false,
|
||||||
'webhooks' => true,
|
'webhooks' => true,
|
||||||
'handle_debts' => true,
|
'handle_debts' => true,
|
||||||
|
// see cer.php for exchange rates feature flag.
|
||||||
],
|
],
|
||||||
'version' => '6.0.18',
|
'version' => '6.0.18',
|
||||||
'api_version' => '2.0.4',
|
'api_version' => '2.0.4',
|
||||||
@@ -127,7 +128,7 @@ return [
|
|||||||
'disable_csp_header' => env('DISABLE_CSP_HEADER', false),
|
'disable_csp_header' => env('DISABLE_CSP_HEADER', false),
|
||||||
'allow_webhooks' => env('ALLOW_WEBHOOKS', false),
|
'allow_webhooks' => env('ALLOW_WEBHOOKS', false),
|
||||||
|
|
||||||
// email flags
|
// flags
|
||||||
'send_report_journals' => envNonEmpty('SEND_REPORT_JOURNALS', true),
|
'send_report_journals' => envNonEmpty('SEND_REPORT_JOURNALS', true),
|
||||||
|
|
||||||
// info for demo site
|
// info for demo site
|
||||||
|
@@ -34,7 +34,12 @@ export default () => ({
|
|||||||
loading: false,
|
loading: false,
|
||||||
loadingAccounts: false,
|
loadingAccounts: false,
|
||||||
accountList: [],
|
accountList: [],
|
||||||
|
autoConvert: false,
|
||||||
chart: null,
|
chart: null,
|
||||||
|
switchConversion() {
|
||||||
|
this.autoConvert = !this.autoConvert;
|
||||||
|
this.loadChart();
|
||||||
|
},
|
||||||
loadChart() {
|
loadChart() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return;
|
return;
|
||||||
@@ -91,8 +96,18 @@ export default () => ({
|
|||||||
if (response.data.hasOwnProperty(i)) {
|
if (response.data.hasOwnProperty(i)) {
|
||||||
let current = response.data[i];
|
let current = response.data[i];
|
||||||
let entry = [];
|
let entry = [];
|
||||||
|
let collection = [];
|
||||||
|
// use the "native" currency code and use the "converted_entries" as array
|
||||||
|
if (this.autoConvert) {
|
||||||
|
window.currencies.push(current.native_code);
|
||||||
|
collection = current.converted_entries;
|
||||||
|
}
|
||||||
|
if (!this.autoConvert) {
|
||||||
window.currencies.push(current.currency_code);
|
window.currencies.push(current.currency_code);
|
||||||
for (const [ii, value] of Object.entries(current.entries)) {
|
collection = current.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [ii, value] of Object.entries(collection)) {
|
||||||
entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
|
entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
|
||||||
}
|
}
|
||||||
options.series.push({name: current.label, data: entry});
|
options.series.push({name: current.label, data: entry});
|
||||||
|
@@ -22,6 +22,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="account-chart"></div>
|
<div id="account-chart"></div>
|
||||||
|
<p x-data="accounts">
|
||||||
|
<template x-if="autoConvert">
|
||||||
|
<button type="button" @click="switchConversion" class="btn btn-info btm-sm">
|
||||||
|
Disable auto-convert
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="!autoConvert">
|
||||||
|
<button type="button" @click="switchConversion" class="btn btn-info btm-sm">
|
||||||
|
Enable auto-convert
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user