Improve bill overview.

This commit is contained in:
James Cole
2025-07-20 14:02:53 +02:00
parent 3bafcb6ad2
commit 93284682c8
4 changed files with 145 additions and 119 deletions

View File

@@ -312,11 +312,23 @@ class BillRepository implements BillRepositoryInterface, UserGroupInterface
Log::debug(sprintf('Search for linked journals between %s and %s', $start->toW3cString(), $end->toW3cString())); Log::debug(sprintf('Search for linked journals between %s and %s', $start->toW3cString(), $end->toW3cString()));
return $bill->transactionJournals() return $bill->transactionJournals()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_currencies AS currency', 'currency.id', '=', 'transactions.transaction_currency_id')
->leftJoin('transaction_currencies AS foreign_currency', 'foreign_currency.id', '=', 'transactions.foreign_currency_id')
->where('transactions.amount', '>', 0)
->before($end)->after($start)->get( ->before($end)->after($start)->get(
[ [
'transaction_journals.id', 'transaction_journals.id',
'transaction_journals.date', 'transaction_journals.date',
'transaction_journals.transaction_group_id', 'transaction_journals.transaction_group_id',
'transactions.transaction_currency_id',
'currency.code AS transaction_currency_code',
'currency.decimal_places AS transaction_currency_decimal_places',
'transactions.foreign_currency_id',
'foreign_currency.code AS foreign_currency_code',
'foreign_currency.decimal_places AS foreign_currency_decimal_places',
'transactions.amount',
'transactions.foreign_amount',
] ]
) )
; ;

View File

@@ -32,8 +32,10 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Support\Facades\Amount; use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Models\BillDateCalculator; use FireflyIII\Support\Models\BillDateCalculator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/** /**
* Class BillTransformer * Class BillTransformer
@@ -194,9 +196,9 @@ class BillTransformer extends AbstractTransformer
*/ */
protected function paidData(Bill $bill): array protected function paidData(Bill $bill): array
{ {
app('log')->debug(sprintf('Now in paidData for bill #%d', $bill->id)); Log::debug(sprintf('Now in paidData for bill #%d', $bill->id));
if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) { if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) {
app('log')->debug('parameters are NULL, return empty array'); Log::debug('parameters are NULL, return empty array');
return []; return [];
} }
@@ -217,27 +219,40 @@ class BillTransformer extends AbstractTransformer
$searchStart->startOfDay(); $searchStart->startOfDay();
$searchEnd->endOfDay(); $searchEnd->endOfDay();
app('log')->debug(sprintf('Parameters are start: %s end: %s', $start->format('Y-m-d'), $end->format('Y-m-d'))); Log::debug(sprintf('Parameters are start: %s end: %s', $start->format('Y-m-d'), $end->format('Y-m-d')));
app('log')->debug(sprintf('Search parameters are: start: %s', $searchStart->format('Y-m-d'))); Log::debug(sprintf('Search parameters are: start: %s', $searchStart->format('Y-m-d')));
// Get from database when bill was paid. // Get from database when bill was paid.
$set = $this->repository->getPaidDatesInRange($bill, $searchStart, $searchEnd); $set = $this->repository->getPaidDatesInRange($bill, $searchStart, $searchEnd);
app('log')->debug(sprintf('Count %d entries in getPaidDatesInRange()', $set->count())); Log::debug(sprintf('Count %d entries in getPaidDatesInRange()', $set->count()));
// Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date. // Grab from array the most recent payment. If none exist, fall back to the start date and pretend *that* was the last paid date.
app('log')->debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d'))); Log::debug(sprintf('Grab last paid date from function, return %s if it comes up with nothing.', $start->format('Y-m-d')));
$lastPaidDate = $this->lastPaidDate($set, $start); $lastPaidDate = $this->lastPaidDate($set, $start);
app('log')->debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d'))); Log::debug(sprintf('Result of lastPaidDate is %s', $lastPaidDate->format('Y-m-d')));
// At this point the "next match" is exactly after the last time the bill was paid. // At this point the "next match" is exactly after the last time the bill was paid.
$result = []; $result = [];
foreach ($set as $entry) { foreach ($set as $entry) {
$result[] = [ $array = [
'transaction_group_id' => (string)$entry->transaction_group_id, 'transaction_group_id' => (string)$entry->transaction_group_id,
'transaction_journal_id' => (string)$entry->id, 'transaction_journal_id' => (string)$entry->id,
'date' => $entry->date->format('Y-m-d'), 'date' => $entry->date->format('Y-m-d'),
'date_object' => $entry->date, 'date_object' => $entry->date,
'currency_id' => $entry->transaction_currency_id,
'currency_code' => $entry->transaction_currency_code,
'currency_decimal_places' => $entry->transaction_currency_decimal_places,
'amount' => Steam::bcround($entry->amount,$entry->transaction_currency_decimal_places),
]; ];
if (null !== $entry->foreign_amount && null !== $entry->foreign_currency_code) {
$array['foreign_currency_id'] = $entry->foreign_currency_id;
$array['foreign_currency_code'] = $entry->foreign_currency_code;
$array['foreign_currency_decimal_places'] = $entry->foreign_currency_decimal_places;
$array['foreign_amount'] = Steam::bcround($entry->foreign_amount,$entry->foreign_currency_decimal_places);
}
$result[] = $array;
} }
return $result; return $result;
@@ -265,7 +280,7 @@ class BillTransformer extends AbstractTransformer
private function getLastPaidDate(array $paidData): ?Carbon private function getLastPaidDate(array $paidData): ?Carbon
{ {
app('log')->debug('getLastPaidDate()'); Log::debug('getLastPaidDate()');
$return = null; $return = null;
foreach ($paidData as $entry) { foreach ($paidData as $entry) {
if (null !== $return) { if (null !== $return) {
@@ -274,15 +289,15 @@ class BillTransformer extends AbstractTransformer
if ($current->gt($return)) { if ($current->gt($return)) {
$return = clone $current; $return = clone $current;
} }
app('log')->debug(sprintf('Last paid date is: %s', $return->format('Y-m-d'))); Log::debug(sprintf('Last paid date is: %s', $return->format('Y-m-d')));
} }
if (null === $return) { if (null === $return) {
/** @var Carbon $return */ /** @var Carbon $return */
$return = $entry['date_object']; $return = $entry['date_object'];
app('log')->debug(sprintf('Last paid date is: %s', $return->format('Y-m-d'))); Log::debug(sprintf('Last paid date is: %s', $return->format('Y-m-d')));
} }
} }
app('log')->debug(sprintf('Last paid date is: "%s"', $return?->format('Y-m-d'))); Log::debug(sprintf('Last paid date is: "%s"', $return?->format('Y-m-d')));
return $return; return $return;
} }

View File

@@ -31,76 +31,54 @@ let afterPromises = false;
let apiData = []; let apiData = [];
let subscriptionData = {}; let subscriptionData = {};
function downloadSubscriptions(params) { function addObjectGroupInfo(data) {
const getter = new Get(); let objectGroupId = parseInt(data.object_group_id);
return getter.list(params)
// first promise: parse the data:
.then((response) => {
let data = response.data.data;
//console.log(data);
for (let i in data) {
if (data.hasOwnProperty(i)) {
let current = data[i];
//console.log(current);
if (current.attributes.active && current.attributes.pay_dates.length > 0) {
let objectGroupId = null === current.attributes.object_group_id ? 0 : current.attributes.object_group_id;
let objectGroupTitle = null === current.attributes.object_group_title ? i18next.t('firefly.default_group_title_name_plain') : current.attributes.object_group_title;
let objectGroupOrder = null === current.attributes.object_group_order ? 0 : current.attributes.object_group_order;
if (!subscriptionData.hasOwnProperty(objectGroupId)) { if (!subscriptionData.hasOwnProperty(objectGroupId)) {
subscriptionData[objectGroupId] = { subscriptionData[objectGroupId] = {
id: objectGroupId, id: objectGroupId,
title: objectGroupTitle, title: null === data.object_group_title ? i18next.t('firefly.default_group_title_name_plain') : data.object_group_title,
order: objectGroupOrder, order: parseInt(data.object_group_order),
payment_info: {}, payment_info: {},
bills: [], bills: [],
}; };
} }
// TODO this conversion needs to be inside some kind of a parsing class. }
let bill = {
id: current.id,
name: current.attributes.name,
// amount
amount_min: current.attributes.amount_min,
amount_max: current.attributes.amount_max,
amount: (parseFloat(current.attributes.amount_max) + parseFloat(current.attributes.amount_min)) / 2,
currency_code: current.attributes.currency_code,
// native amount
// native_amount_min: current.attributes.native_amount_min,
// native_amount_max: current.attributes.native_amount_max,
// native_amount: (parseFloat(current.attributes.native_amount_max) + parseFloat(current.attributes.native_amount_min)) / 2,
// native_currency_code: current.attributes.native_currency_code,
function parseBillInfo(data) {
let result = {
id: data.id,
name: data.attributes.name,
amount_min: data.attributes.amount_min,
amount_max: data.attributes.amount_max,
amount: (parseFloat(data.attributes.amount_max) + parseFloat(data.attributes.amount_min)) / 2,
currency_code: data.attributes.currency_code,
// paid transactions: // paid transactions:
transactions: [], transactions: [],
// unpaid moments // unpaid moments
pay_dates: current.attributes.pay_dates, pay_dates: data.attributes.pay_dates,
paid: current.attributes.paid_dates.length > 0, paid: data.attributes.paid_dates.length > 0,
}; };
// set variables // set variables
bill.expected_amount = formatMoney(bill.amount, bill.currency_code); result.expected_amount = formatMoney(result.amount, result.currency_code);
bill.expected_times = i18next.t('firefly.subscr_expected_x_times', { result.expected_times = i18next.t('firefly.subscr_expected_x_times', {
times: current.attributes.pay_dates.length, times: data.attributes.pay_dates.length,
amount: bill.expected_amount amount: result.expected_amount
}); });
return result;
}
function parsePaidTransactions(paid_dates, bill) {
if( !paid_dates || paid_dates.length < 1) {
return [];
}
let result = [];
// add transactions (simpler version) // add transactions (simpler version)
for (let iii in current.attributes.paid_dates) { for (let i in paid_dates) {
if (current.attributes.paid_dates.hasOwnProperty(iii)) { if (paid_dates.hasOwnProperty(i)) {
const currentPayment = current.attributes.paid_dates[iii]; const currentPayment = paid_dates[i];
let percentage = 100;
// math: -100+(paid/expected)*100
if (params.convertToNative) {
percentage = Math.round(-100 + ((parseFloat(currentPayment.native_amount) * -1) / parseFloat(bill.native_amount)) * 100);
}
if (!params.convertToNative) {
percentage = Math.round(-100 + ((parseFloat(currentPayment.amount) * -1) / parseFloat(bill.amount)) * 100);
}
// TODO fix me
currentPayment.currency_code = 'EUR';
console.log('Currency code: "'+currentPayment+'"');
console.log(currentPayment); console.log(currentPayment);
// math: -100+(paid/expected)*100
let percentage = Math.round(-100 + ((parseFloat(currentPayment.amount) ) / parseFloat(bill.amount)) * 100);
let currentTransaction = { let currentTransaction = {
amount: formatMoney(currentPayment.amount, currentPayment.currency_code), amount: formatMoney(currentPayment.amount, currentPayment.currency_code),
percentage: percentage, percentage: percentage,
@@ -112,9 +90,30 @@ function downloadSubscriptions(params) {
currentTransaction.foreign_currency_code = currentPayment.foreign_currency_code; currentTransaction.foreign_currency_code = currentPayment.foreign_currency_code;
} }
bill.transactions.push(currentTransaction); result.push(currentTransaction);
} }
} }
return result;
}
function downloadSubscriptions(params) {
const getter = new Get();
return getter.list(params)
// first promise: parse the data:
.then((response) => {
let data = response.data.data;
//console.log(data);
for (let i in data) {
if (data.hasOwnProperty(i)) {
let current = data[i];
if (current.attributes.active && current.attributes.pay_dates.length > 0) {
// create or update object group
let objectGroupId = parseInt(current.attributes.object_group_id);
addObjectGroupInfo(current.attributes);
// create and update the bill.
let bill = parseBillInfo(current);
bill.transactions = parsePaidTransactions(current.attributes.paid_dates, bill);
subscriptionData[objectGroupId].bills.push(bill); subscriptionData[objectGroupId].bills.push(bill);
if (0 === current.attributes.paid_dates.length) { if (0 === current.attributes.paid_dates.length) {

View File

@@ -1866,8 +1866,8 @@ return [
'skip_help_text' => 'Use the skip field to create bi-monthly (skip = 1) or other custom intervals.', 'skip_help_text' => 'Use the skip field to create bi-monthly (skip = 1) or other custom intervals.',
'subscription' => 'Subscription', 'subscription' => 'Subscription',
'not_expected_period' => 'Not expected this period', 'not_expected_period' => 'Not expected this period',
'subscriptions_in_group' => 'Subscriptions in group "%{title}"', 'subscriptions_in_group' => 'Subscriptions in group "{{title}}"',
'subscr_expected_x_times' => 'Expect to pay %{amount} %{times} times this period', 'subscr_expected_x_times' => 'Expect to pay {{amount}} {{times}} times this period',
'not_or_not_yet' => 'Not (yet)', 'not_or_not_yet' => 'Not (yet)',
'visit_bill' => 'Visit subscription ":name" at Firefly III', 'visit_bill' => 'Visit subscription ":name" at Firefly III',
'match_between_amounts' => 'Subscription matches transactions between :low and :high.', 'match_between_amounts' => 'Subscription matches transactions between :low and :high.',