Commit 761ee72a authored by DevPilot's avatar DevPilot

fix(accounting): cheque register summary lost closed cheques and cards did not reconcile

A cheque that was collected and then closed fell out of every bucket, so the
five cards stopped summing to the total. 'closed' is archival and is reachable
from collected/paid/endorsed/returned/replaced/cancelled, so resolve it back to
the status it held before the close movement.

Buckets are now defined once in bucketStatuses() and read by both the summary
and the filter, and the cards link by bucket instead of a single status, so the
number on a card equals what you get when you click it. 'open' is the remainder
so the cards always reconcile. The summary ignores the selected status/bucket
so you can still navigate between cards after clicking one.
parent c1b0f334
...@@ -27,6 +27,7 @@ class InstrumentRegisterController extends Controller ...@@ -27,6 +27,7 @@ class InstrumentRegisterController extends Controller
'bank' => trim((string) $request->get('bank', '')), 'bank' => trim((string) $request->get('bank', '')),
'branch_id' => (string) $request->get('branch_id', ''), 'branch_id' => (string) $request->get('branch_id', ''),
'status' => (string) $request->get('status', ''), 'status' => (string) $request->get('status', ''),
'bucket' => (string) $request->get('bucket', ''),
'amount_from' => (string) $request->get('amount_from', ''), 'amount_from' => (string) $request->get('amount_from', ''),
'amount_to' => (string) $request->get('amount_to', ''), 'amount_to' => (string) $request->get('amount_to', ''),
]; ];
...@@ -42,7 +43,11 @@ class InstrumentRegisterController extends Controller ...@@ -42,7 +43,11 @@ class InstrumentRegisterController extends Controller
return $this->view('Accounting.Views.instruments.register', [ return $this->view('Accounting.Views.instruments.register', [
'filters' => $f, 'filters' => $f,
'rows' => InstrumentRegisterService::search($f), 'rows' => InstrumentRegisterService::search($f),
'summary' => InstrumentRegisterService::summary($f), // الملخص بيتأثر بالفلاتر العامة (تاريخ/فرع/طرف...) لكن مش بالحالة
// المختارة — وإلا أول ما تضغط كارت الباقي كله يبقى صفر وما تعرفش ترجع.
'summary' => InstrumentRegisterService::summary(
array_merge($f, ['status' => '', 'bucket' => ''])
),
'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"), 'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"),
'bankAccounts' => $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"), 'bankAccounts' => $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"),
'statusesIn' => InstrumentLifecycleService::statusesFor('receivable'), 'statusesIn' => InstrumentLifecycleService::statusesFor('receivable'),
......
...@@ -10,9 +10,41 @@ use App\Core\App; ...@@ -10,9 +10,41 @@ use App\Core\App;
*/ */
final class InstrumentRegisterService final class InstrumentRegisterService
{ {
/**
* الحالة الفعلية للشيك لأغراض الملخص: الشيك «المغلق» بيترجّع لآخر حالة
* كان عليها قبل الإغلاق، لأن الإغلاق أرشفة مش نتيجة.
*/
private const EFFECTIVE_STATUS = "
CASE WHEN ni.status = 'closed' THEN COALESCE((
SELECT m.from_status FROM instrument_movements m
WHERE m.instrument_id = ni.id AND m.action = 'close'
ORDER BY m.id DESC LIMIT 1), 'closed')
ELSE ni.status END";
/**
* الحالات اللي بتكوّن كل مجموعة في الملخص. الملخص والفلترة الاتنين
* بيقروا من هنا، عشان الرقم في الكارت يساوي اللي تشوفه لما تضغط عليه.
*
* @return array{settled:string[], bounced:string[], cancelled:string[]}
*/
public static function bucketStatuses(string $direction): array
{
// المظهَّر خرج من عندنا وسدّد التزام، والمستبدل قيمته اتنقلت لشيك تاني —
// فالاتنين مش «قائمين».
$settled = $direction === 'payable'
? ['paid']
: ($direction === 'receivable' ? ['collected', 'endorsed'] : ['collected', 'paid', 'endorsed']);
return [
'settled' => $settled,
'bounced' => ['bounced'],
'cancelled' => ['cancelled', 'replaced'],
];
}
/** /**
* @param array $f من تاريخ/إلى تاريخ، الاتجاه، رقم الشيك، الطرف، البنك، * @param array $f من تاريخ/إلى تاريخ، الاتجاه، رقم الشيك، الطرف، البنك،
* الفرع، الحالة، المبلغ من/إلى، وأساس التاريخ (شيك/حركة) * الفرع، الحالة/المجموعة، المبلغ من/إلى، وأساس التاريخ (شيك/حركة)
*/ */
public static function search(array $f): array public static function search(array $f): array
{ {
...@@ -61,11 +93,15 @@ final class InstrumentRegisterService ...@@ -61,11 +93,15 @@ final class InstrumentRegisterService
$params $params
); );
// «مغلق» حالة أرشفة بتيجي بعد نتيجة تانية (تحصيل/صرف/إلغاء/استبدال...)،
// فلو جمعناها لوحدها الشيك المقفول بيختفي من كل المجموعات والكروت
// ما تجمعش الإجمالي. فبنرجّع المقفول لآخر حالة قبل الإغلاق.
$byStatus = $db->select( $byStatus = $db->select(
"SELECT ni.status, COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total "SELECT " . self::EFFECTIVE_STATUS . " AS status,
COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total
FROM negotiable_instruments ni FROM negotiable_instruments ni
WHERE {$where} WHERE {$where}
GROUP BY ni.status", GROUP BY " . self::EFFECTIVE_STATUS,
$params $params
); );
...@@ -85,14 +121,26 @@ final class InstrumentRegisterService ...@@ -85,14 +121,26 @@ final class InstrumentRegisterService
return ['n' => $n, 'total' => $t]; return ['n' => $n, 'total' => $t];
}; };
$b = self::bucketStatuses($dir);
$all = ['n' => (int) ($row['n'] ?? 0), 'total' => (string) ($row['total'] ?? '0.00')];
$settled = $pick($b['settled']);
$bounced = $pick($b['bounced']);
$cancelled = $pick($b['cancelled']);
// «القائم» = الباقي، عشان الأربع كروت يجمعوا الإجمالي دايمًا
// مهما اتضافت حالات جديدة بعدين.
$open = [
'n' => $all['n'] - $settled['n'] - $bounced['n'] - $cancelled['n'],
'total' => bcsub(bcsub(bcsub($all['total'], $settled['total'], 2), $bounced['total'], 2), $cancelled['total'], 2),
];
$out[$dir] = [ $out[$dir] = [
'all' => ['n' => (int) ($row['n'] ?? 0), 'total' => (string) ($row['total'] ?? '0.00')], 'all' => $all,
'settled' => $pick($dir === 'receivable' ? ['collected'] : ['paid']), 'settled' => $settled,
'bounced' => $pick(['bounced']), 'bounced' => $bounced,
'cancelled' => $pick(['cancelled']), 'cancelled' => $cancelled,
'open' => $pick($dir === 'receivable' 'open' => $open,
? ['in_hand', 'deposited', 'under_collection', 'endorsed', 'returned']
: ['in_hand', 'ready', 'delivered', 'pending_clearance', 'returned']),
'by_status' => $buckets, 'by_status' => $buckets,
]; ];
} }
...@@ -149,9 +197,32 @@ final class InstrumentRegisterService ...@@ -149,9 +197,32 @@ final class InstrumentRegisterService
$params[] = (int) $f['branch_id']; $params[] = (int) $f['branch_id'];
} }
if (!empty($f['status'])) { if (!empty($f['status'])) {
$where[] = 'ni.status = ?'; // لازم يطابق نفس منطق الملخص، وإلا الضغط على كارت «المحصل» يوديك
// لليستة أقل من الرقم اللي في الكارت. و«مغلق» بتتطابق حرفيًا برضه.
$where[] = '(ni.status = ? OR ' . self::EFFECTIVE_STATUS . ' = ?)';
$params[] = $f['status'];
$params[] = $f['status']; $params[] = $f['status'];
} }
// الضغط على كارت في الملخص بيفلتر بمجموعة كاملة مش بحالة واحدة،
// و«القائم» هو الباقي بعد استبعاد المنتهي والمرتد والملغي.
if (!empty($f['bucket'])) {
$b = self::bucketStatuses((string) ($f['direction'] ?? ''));
$negate = false;
if ($f['bucket'] === 'open') {
$list = array_merge($b['settled'], $b['bounced'], $b['cancelled']);
$negate = true;
} else {
$list = $b[$f['bucket']] ?? [];
}
if ($list) {
$ph = implode(',', array_fill(0, count($list), '?'));
$where[] = self::EFFECTIVE_STATUS . ($negate ? ' NOT IN ' : ' IN ') . "({$ph})";
$params = array_merge($params, $list);
}
}
if (($f['amount_from'] ?? '') !== '') { if (($f['amount_from'] ?? '') !== '') {
$where[] = 'ni.amount >= ?'; $where[] = 'ni.amount >= ?';
$params[] = $f['amount_from']; $params[] = $f['amount_from'];
......
...@@ -28,20 +28,20 @@ $qs = function (array $over) use ($f) { ...@@ -28,20 +28,20 @@ $qs = function (array $over) use ($f) {
<div class="card" style="border-top:3px solid <?= $color ?>;"> <div class="card" style="border-top:3px solid <?= $color ?>;">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;"> <div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<strong style="color:<?= $color ?>;font-size:15px;"><?= e($label) ?></strong> <strong style="color:<?= $color ?>;font-size:15px;"><?= e($label) ?></strong>
<a href="<?= e($qs(['direction' => $dir, 'status' => ''])) ?>" style="font-size:12px;">عرض الكل</a> <a href="<?= e($qs(['direction' => $dir, 'status' => '', 'bucket' => ''])) ?>" style="font-size:12px;">عرض الكل</a>
</div> </div>
<div style="padding:12px 16px;display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:13px;"> <div style="padding:12px 16px;display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:13px;">
<?php <?php
$cells = [ $cells = [
['الإجمالي', $s['all'], ''], ['الإجمالي', $s['all'], ''],
[$dir === 'receivable' ? 'المحصّل' : 'المصروف', $s['settled'], $dir === 'receivable' ? 'collected' : 'paid'], [$dir === 'receivable' ? 'المحصّل' : 'المصروف', $s['settled'], 'settled'],
['القائم', $s['open'], ''], ['القائم', $s['open'], 'open'],
['المرتد', $s['bounced'], 'bounced'], ['المرتد', $s['bounced'], 'bounced'],
['الملغي', $s['cancelled'], 'cancelled'], ['الملغي / المستبدل', $s['cancelled'], 'cancelled'],
]; ];
foreach ($cells as [$t, $v, $st]): foreach ($cells as [$t, $v, $bk]):
?> ?>
<a href="<?= e($qs(['direction' => $dir, 'status' => $st])) ?>" <a href="<?= e($qs(['direction' => $dir, 'status' => '', 'bucket' => $bk])) ?>"
style="display:block;padding:8px 10px;border:1px solid #E5E7EB;border-radius:6px;text-decoration:none;color:inherit;"> style="display:block;padding:8px 10px;border:1px solid #E5E7EB;border-radius:6px;text-decoration:none;color:inherit;">
<div style="color:#6B7280;font-size:11px;"><?= e($t) ?> (<?= (int) $v['n'] ?>)</div> <div style="color:#6B7280;font-size:11px;"><?= e($t) ?> (<?= (int) $v['n'] ?>)</div>
<div style="font-weight:700;direction:ltr;text-align:left;"><?= money($v['total']) ?></div> <div style="font-weight:700;direction:ltr;text-align:left;"><?= money($v['total']) ?></div>
......
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