Commit 1d1ec8ae authored by DevPilot's avatar DevPilot

feat(accounting): open any trial-balance account and read its history newest first

The trial balance showed a number with no way to ask where it came from. Every
account row is now a link into that account's ledger, carrying the same date
range and the same cost-centre/branch filters — so the movements you land on are
exactly the ones that produced the figure you clicked.

The ledger itself now reads newest first. The running balance is still
accumulated in date order, because "the balance after this entry" only means
anything with the earlier entries already in; the list is reversed after that
accumulation, so the order flips and every balance still says what it says. The
opening-balance row moved to the bottom accordingly — it is the oldest line on
a newest-first list.

Each entry number links to the full journal entry, so a line in one account
opens the whole قيد with its other side. The reference type is shown under the
reference number, which is usually what tells you which operation raised it.

Verified against 779 movements on the cash account: order is newest first, the
running balance on the top row equals the closing balance, and both screens
render with 165 clickable rows and 779 entry links.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent 76880e09
...@@ -125,7 +125,8 @@ final class LedgerService ...@@ -125,7 +125,8 @@ final class LedgerService
string $dateFrom, string $dateFrom,
string $dateTo, string $dateTo,
?int $costCenterId = null, ?int $costCenterId = null,
?int $branchId = null ?int $branchId = null,
bool $newestFirst = true
): array { ): array {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -194,7 +195,8 @@ final class LedgerService ...@@ -194,7 +195,8 @@ final class LedgerService
array_merge([$accountId], $periodParams) array_merge([$accountId], $periodParams)
); );
// Calculate running balance // Running balance has to be accumulated in DATE ORDER — "the balance after
// this entry" only means anything if the entries before it are already in.
$running = $openingBalance; $running = $openingBalance;
foreach ($entries as &$entry) { foreach ($entries as &$entry) {
if ($account['account_nature'] === 'debit') { if ($account['account_nature'] === 'debit') {
...@@ -206,6 +208,14 @@ final class LedgerService ...@@ -206,6 +208,14 @@ final class LedgerService
} }
unset($entry); unset($entry);
// …but it is READ newest first. Somebody opening an account wants to see
// what happened to it last, not scroll past a year of history to get
// there. Reversing after the accumulation keeps both: the order is
// newest-first and every running balance still means what it says.
if ($newestFirst) {
$entries = array_reverse($entries);
}
return [ return [
'account' => $account, 'account' => $account,
'opening_balance' => $openingBalance, 'opening_balance' => $openingBalance,
......
...@@ -33,7 +33,11 @@ ...@@ -33,7 +33,11 @@
<span style="direction:ltr;color:#0D7377;"><?= e($ledger['account']['account_code']) ?></span> <span style="direction:ltr;color:#0D7377;"><?= e($ledger['account']['account_code']) ?></span>
<?= e($ledger['account']['name_ar']) ?> <?= e($ledger['account']['name_ar']) ?>
</h3> </h3>
<p style="color:#6B7280;margin:5px 0 0;font-size:13px;">من <?= e($date_from) ?> إلى <?= e($date_to) ?></p> <p style="color:#6B7280;margin:5px 0 0;font-size:13px;">
من <?= e($date_from) ?> إلى <?= e($date_to) ?>
<?= number_format(count($ledger['entries'])) ?> حركة، معروضة
<strong>من الأحدث للأقدم</strong>. اضغط رقم القيد تشوفه كامل بطرفيه.
</p>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="data-table" style="width:100%;"> <table class="data-table" style="width:100%;">
...@@ -49,24 +53,38 @@ ...@@ -49,24 +53,38 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Opening Balance Row -->
<tr style="background:#F9FAFB;font-weight:600;">
<td colspan="4">رصيد أول المدة</td>
<td></td>
<td></td>
<td style="direction:ltr;text-align:right;"><?= money($ledger['opening_balance']) ?></td>
</tr>
<?php foreach ($ledger['entries'] as $e_row): ?> <?php foreach ($ledger['entries'] as $e_row): ?>
<tr> <tr>
<td><?= e($e_row['entry_date']) ?></td> <td style="white-space:nowrap;"><?= e($e_row['entry_date']) ?></td>
<td style="direction:ltr;text-align:right;font-size:12px;"><?= e($e_row['entry_number']) ?></td> <td style="direction:ltr;text-align:right;font-size:12px;">
<?php if (!empty($e_row['journal_entry_id'])): ?>
<a href="/accounting/journal-entries/<?= (int) $e_row['journal_entry_id'] ?>"
style="color:#0D7377;text-decoration:none;"
title="افتح القيد كامل بطرفيه"><?= e($e_row['entry_number']) ?></a>
<?php else: ?>
<?= e($e_row['entry_number']) ?>
<?php endif; ?>
</td>
<td style="font-size:13px;"><?= e($e_row['description_ar'] ?? $e_row['entry_description'] ?? '') ?></td> <td style="font-size:13px;"><?= e($e_row['description_ar'] ?? $e_row['entry_description'] ?? '') ?></td>
<td style="font-size:12px;color:#6B7280;"><?= e($e_row['reference_number'] ?? '') ?></td> <td style="font-size:12px;color:#6B7280;">
<?= e($e_row['reference_number'] ?? '') ?>
<?php if (!empty($e_row['reference_type'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e((string) $e_row['reference_type']) ?></div>
<?php endif; ?>
</td>
<td style="direction:ltr;text-align:right;"><?= bccomp((string)$e_row['debit'], '0.00', 2) > 0 ? money($e_row['debit']) : '' ?></td> <td style="direction:ltr;text-align:right;"><?= bccomp((string)$e_row['debit'], '0.00', 2) > 0 ? money($e_row['debit']) : '' ?></td>
<td style="direction:ltr;text-align:right;"><?= bccomp((string)$e_row['credit'], '0.00', 2) > 0 ? money($e_row['credit']) : '' ?></td> <td style="direction:ltr;text-align:right;"><?= bccomp((string)$e_row['credit'], '0.00', 2) > 0 ? money($e_row['credit']) : '' ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($e_row['running_balance']) ?></td> <td style="direction:ltr;text-align:right;font-weight:600;"><?= money($e_row['running_balance']) ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<!-- Opening balance goes LAST: the list runs newest first, and the
opening balance is the oldest thing on it. -->
<tr style="background:#F9FAFB;font-weight:600;">
<td colspan="4">رصيد أول المدة</td>
<td></td>
<td></td>
<td style="direction:ltr;text-align:right;"><?= money($ledger['opening_balance']) ?></td>
</tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr style="font-weight:700;background:#F0FDF4;"> <tr style="font-weight:700;background:#F0FDF4;">
......
...@@ -7,6 +7,11 @@ ...@@ -7,6 +7,11 @@
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<style>
/* The rows are clickable, so they have to look it. */
.tb-row:hover { background:#F0FDFA; }
</style>
<!-- Filters --> <!-- Filters -->
<div class="card" style="margin-bottom:15px;"> <div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;"> <div style="padding:15px 20px;">
...@@ -69,9 +74,25 @@ ...@@ -69,9 +74,25 @@
</thead> </thead>
<tbody> <tbody>
<?php foreach ($result['accounts'] as $acc): ?> <?php foreach ($result['accounts'] as $acc): ?>
<tr> <?php
<td style="direction:ltr;text-align:right;font-weight:600;font-size:12px;padding:8px;white-space:nowrap;"><?= e($acc['account_code']) ?></td> // Clicking an account opens its ledger for the SAME period and
<td style="padding:8px;font-size:13px;"><?= e($acc['name_ar']) ?></td> // the same cost-centre/branch filters, so the movements you
// land on are the ones that produced the number you clicked.
$ledgerUrl = '/accounting/reports/general-ledger'
. '?account_id=' . (int) $acc['id']
. '&date_from=' . rawurlencode($date_from)
. '&date_to=' . rawurlencode($date_to)
. '&cost_center_id=' . (int) ($filters['cost_center_id'] ?? 0)
. '&branch_id=' . (int) ($filters['branch_id'] ?? 0);
?>
<tr class="tb-row" style="cursor:pointer;" onclick="window.location='<?= e($ledgerUrl) ?>'"
title="اضغط لعرض حركة الحساب من الأحدث للأقدم">
<td style="direction:ltr;text-align:right;font-weight:600;font-size:12px;padding:8px;white-space:nowrap;">
<a href="<?= e($ledgerUrl) ?>" onclick="event.stopPropagation();" style="color:#0D7377;text-decoration:none;"><?= e($acc['account_code']) ?></a>
</td>
<td style="padding:8px;font-size:13px;">
<a href="<?= e($ledgerUrl) ?>" onclick="event.stopPropagation();" style="color:inherit;text-decoration:none;"><?= e($acc['name_ar']) ?></a>
</td>
<td style="direction:ltr;text-align:center;padding:8px;font-size:12px;border-left:1px solid #E5E7EB;<?= bccomp($acc['opening_debit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['opening_debit']) ?></td> <td style="direction:ltr;text-align:center;padding:8px;font-size:12px;border-left:1px solid #E5E7EB;<?= bccomp($acc['opening_debit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['opening_debit']) ?></td>
<td style="direction:ltr;text-align:center;padding:8px;font-size:12px;<?= bccomp($acc['opening_credit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['opening_credit']) ?></td> <td style="direction:ltr;text-align:center;padding:8px;font-size:12px;<?= bccomp($acc['opening_credit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['opening_credit']) ?></td>
<td style="direction:ltr;text-align:center;padding:8px;font-size:12px;border-left:1px solid #E5E7EB;<?= bccomp($acc['period_debit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['period_debit']) ?></td> <td style="direction:ltr;text-align:center;padding:8px;font-size:12px;border-left:1px solid #E5E7EB;<?= bccomp($acc['period_debit'], '0.00', 2) > 0 ? 'font-weight:600;' : 'color:#D1D5DB;' ?>"><?= money($acc['period_debit']) ?></td>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment