Commit 927edd62 authored by DevPilot's avatar DevPilot

feat(accounting): unified cheque register with full lifecycle history

The cheque data existed but the screen was a per-direction list and the
status was overwritten in place — there was no record of who did what,
when, or what the previous state was. Adds:

- صادر ووارد شيكات بنكية: one screen for both directions, with the
  full filter set (date range on either the cheque date or the movement
  date, direction, number, party, bank, branch, status, amount range),
  a reset, and per-direction summary cards whose totals are clickable
  and drive the filters.
- instrument_movements: every action is appended as an immutable row
  (action, from/to status, date, user, bank, reference, notes). Nothing
  is ever deleted, so each cheque carries a complete audit trail.
  Existing cheques get an opening "register" movement on migrate so the
  history starts from a known point.
- The complete status sets for both directions — registered, ready,
  delivered, deposited, under collection, pending, collected, paid,
  bounced, endorsed, returned, replaced, cancelled, closed — with a
  direction-specific transition map that refuses illogical moves such
  as collecting a cancelled cheque.
- Cheques with movements cannot be deleted; corrections are new
  actions, not edits.

The older screens now funnel through the same service, and the bounce
and resolve paths log movements too, so the trail stays complete no
matter which screen the action came from.
parent ec7ca512
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Accounting\Services\InstrumentLifecycleService;
use App\Modules\Accounting\Services\InstrumentRegisterService;
/**
* صادر ووارد شيكات بنكية — شاشة واحدة لمتابعة دورة حياة كل شيك.
*/
class InstrumentRegisterController extends Controller
{
private function filters(Request $request): array
{
return [
'direction' => (string) $request->get('direction', ''),
'date_from' => (string) $request->get('date_from', ''),
'date_to' => (string) $request->get('date_to', ''),
'date_basis' => (string) $request->get('date_basis', 'instrument'),
'number' => trim((string) $request->get('number', '')),
'party' => trim((string) $request->get('party', '')),
'bank' => trim((string) $request->get('bank', '')),
'branch_id' => (string) $request->get('branch_id', ''),
'status' => (string) $request->get('status', ''),
'amount_from' => (string) $request->get('amount_from', ''),
'amount_to' => (string) $request->get('amount_to', ''),
];
}
public function index(Request $request): Response
{
$this->authorize('accounting.instruments.view');
$db = App::getInstance()->db();
$f = $this->filters($request);
return $this->view('Accounting.Views.instruments.register', [
'filters' => $f,
'rows' => InstrumentRegisterService::search($f),
'summary' => InstrumentRegisterService::summary($f),
'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"),
'statusesIn' => InstrumentLifecycleService::statusesFor('receivable'),
'statusesOut' => InstrumentLifecycleService::statusesFor('payable'),
]);
}
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.view');
$db = App::getInstance()->db();
$ins = $db->selectOne(
"SELECT ni.*, b.account_name_ar AS bank_name, br.name_ar AS branch_name,
rep.instrument_number AS replaced_by_number
FROM negotiable_instruments ni
LEFT JOIN bank_accounts b ON b.id = ni.bank_account_id
LEFT JOIN branches br ON br.id = ni.branch_id
LEFT JOIN negotiable_instruments rep ON rep.id = ni.replaced_by_id
WHERE ni.id = ?",
[(int) $id]
);
if (!$ins) {
return $this->redirect('/accounting/instruments/register')->withError('الشيك غير موجود');
}
$allowed = InstrumentLifecycleService::allowedNext((string) $ins['direction'], (string) $ins['status']);
return $this->view('Accounting.Views.instruments.register_show', [
'ins' => $ins,
'history' => InstrumentLifecycleService::history((int) $id),
'allowed' => $allowed,
'bankAccounts' => $db->select("SELECT id, account_name_ar FROM bank_accounts WHERE is_active = 1 ORDER BY account_name_ar"),
'canDelete' => InstrumentLifecycleService::canDelete((int) $id),
]);
}
public function act(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$result = InstrumentLifecycleService::act((int) $id, (string) $request->post('to_status', ''), [
'action_date' => $request->postDate('action_date'),
'bank_account_id' => $request->post('bank_account_id'),
'endorsee_name' => $request->post('endorsee_name'),
'reference' => $request->post('reference'),
'notes' => $request->post('notes'),
]);
if (!$result['success']) {
return $this->redirect('/accounting/instruments/register/' . $id)->withError($result['error']);
}
return $this->redirect('/accounting/instruments/register/' . $id)->withSuccess('تم تسجيل الإجراء في سجل حركة الشيك');
}
public function note(Request $request, string $id): Response
{
$this->authorize('accounting.instruments.manage');
$result = InstrumentLifecycleService::addNote(
(int) $id,
(string) $request->post('notes', ''),
$request->postDate('action_date')
);
if (!$result['success']) {
return $this->redirect('/accounting/instruments/register/' . $id)->withError($result['error']);
}
return $this->redirect('/accounting/instruments/register/' . $id)->withSuccess('تمت إضافة الملاحظة للسجل');
}
}
...@@ -8,6 +8,7 @@ use App\Core\Request; ...@@ -8,6 +8,7 @@ use App\Core\Request;
use App\Core\App; use App\Core\App;
use App\Core\Response; use App\Core\Response;
use App\Modules\Accounting\Models\NegotiableInstrument; use App\Modules\Accounting\Models\NegotiableInstrument;
use App\Modules\Accounting\Services\InstrumentLifecycleService;
class NegotiableInstrumentController extends Controller class NegotiableInstrumentController extends Controller
{ {
...@@ -53,7 +54,7 @@ class NegotiableInstrumentController extends Controller ...@@ -53,7 +54,7 @@ class NegotiableInstrumentController extends Controller
$session = App::getInstance()->session(); $session = App::getInstance()->session();
NegotiableInstrument::create([ $created = NegotiableInstrument::create([
'instrument_type' => $data['instrument_type'], 'instrument_type' => $data['instrument_type'],
'direction' => $data['direction'], 'direction' => $data['direction'],
'instrument_number' => $data['instrument_number'], 'instrument_number' => $data['instrument_number'],
...@@ -73,8 +74,23 @@ class NegotiableInstrumentController extends Controller ...@@ -73,8 +74,23 @@ class NegotiableInstrumentController extends Controller
'created_by' => (int)($session->get('employee_id') ?? 0) ?: null, 'created_by' => (int)($session->get('employee_id') ?? 0) ?: null,
]); ]);
return $this->redirect('/accounting/instruments?direction=' . $data['direction']) // أول حركة في سجل الشيك — من غيرها السجل يبدأ ناقص
->withSuccess('تم تسجيل الورقة التجارية بنجاح'); if ($created && !empty($created->id)) {
App::getInstance()->db()->insert('instrument_movements', [
'instrument_id' => (int) $created->id,
'action' => 'register',
'from_status' => null,
'to_status' => 'in_hand',
'action_date' => $data['issue_date'],
'amount' => (float) $data['amount'],
'notes' => $request->post('notes'),
'performed_by' => (int) ($session->get('employee_id') ?? 0) ?: null,
'created_at' => date('Y-m-d H:i:s'),
]);
}
return $this->redirect('/accounting/instruments/register/' . ($created->id ?? ''))
->withSuccess('تم تسجيل الشيك وبدأ سجل حركته');
} }
public function show(Request $request, string $id): Response public function show(Request $request, string $id): Response
...@@ -92,26 +108,20 @@ class NegotiableInstrumentController extends Controller ...@@ -92,26 +108,20 @@ class NegotiableInstrumentController extends Controller
{ {
$this->authorize('accounting.instruments.manage'); $this->authorize('accounting.instruments.manage');
$instrument = NegotiableInstrument::findOrFail((int)$id); // كل تغيير حالة بيمر من نفس الخدمة عشان يتسجّل في سجل الحركة —
$newStatus = $request->post('new_status'); // مفيش تغيير حالة من غير حركة مقابلة.
$result = InstrumentLifecycleService::act((int) $id, (string) $request->post('new_status', ''), [
if (!$instrument->canTransitionTo($newStatus)) { 'bank_account_id' => $request->post('bank_account_id'),
return $this->redirect('/accounting/instruments/' . $id) 'endorsee_name' => $request->post('endorsee_name'),
->withError('لا يمكن تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus)); 'notes' => $request->post('notes'),
} ]);
$extra = []; if (!$result['success']) {
if ($request->post('bank_account_id')) { return $this->redirect('/accounting/instruments/' . $id)->withError($result['error']);
$extra['bank_account_id'] = (int)$request->post('bank_account_id');
} }
if ($request->post('endorsee_name')) {
$extra['endorsee_name'] = $request->post('endorsee_name');
}
$instrument->transitionTo($newStatus, $extra ?: null);
return $this->redirect('/accounting/instruments/' . $id) return $this->redirect('/accounting/instruments/register/' . $id)
->withSuccess('تم تغيير الحالة إلى: ' . (NegotiableInstrument::$statusLabels[$newStatus] ?? $newStatus)); ->withSuccess('تم تنفيذ الإجراء وتسجيله في سجل حركة الشيك');
} }
public function dueSoon(Request $request): Response public function dueSoon(Request $request): Response
......
...@@ -73,6 +73,10 @@ return [ ...@@ -73,6 +73,10 @@ return [
// ── Negotiable Instruments ────────────────────────────── // ── Negotiable Instruments ──────────────────────────────
['GET', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@index', ['auth'], 'accounting.instruments.view'], ['GET', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@index', ['auth'], 'accounting.instruments.view'],
['POST', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@store', ['auth', 'csrf'], 'accounting.instruments.manage'], ['POST', '/accounting/instruments', 'Accounting\Controllers\NegotiableInstrumentController@store', ['auth', 'csrf'], 'accounting.instruments.manage'],
['GET', '/accounting/instruments/register', 'Accounting\\Controllers\\InstrumentRegisterController@index', ['auth'], 'accounting.instruments.view'],
['GET', '/accounting/instruments/register/{id:\\d+}', 'Accounting\\Controllers\\InstrumentRegisterController@show', ['auth'], 'accounting.instruments.view'],
['POST', '/accounting/instruments/register/{id:\\d+}/act', 'Accounting\\Controllers\\InstrumentRegisterController@act', ['auth', 'csrf'], 'accounting.instruments.manage'],
['POST', '/accounting/instruments/register/{id:\\d+}/note', 'Accounting\\Controllers\\InstrumentRegisterController@note', ['auth', 'csrf'], 'accounting.instruments.manage'],
['GET', '/accounting/instruments/due-soon', 'Accounting\Controllers\NegotiableInstrumentController@dueSoon', ['auth'], 'accounting.instruments.view'], ['GET', '/accounting/instruments/due-soon', 'Accounting\Controllers\NegotiableInstrumentController@dueSoon', ['auth'], 'accounting.instruments.view'],
// الشيكات المرتدة والمتأخرة — declared before {id} so the word is not read as an id // الشيكات المرتدة والمتأخرة — declared before {id} so the word is not read as an id
['GET', '/accounting/instruments/bounced', 'Accounting\Controllers\BouncedChequeController@index', ['auth'], 'accounting.instruments.view'], ['GET', '/accounting/instruments/bounced', 'Accounting\Controllers\BouncedChequeController@index', ['auth'], 'accounting.instruments.view'],
......
...@@ -123,6 +123,13 @@ final class BouncedChequeService ...@@ -123,6 +123,13 @@ final class BouncedChequeService
return $result; return $result;
} }
self::logMovement($instrumentId, 'bounce', (string) $instrument['status'], 'bounced', [
'date' => $date ?? date('Y-m-d'),
'amount' => $instrument['amount'] ?? null,
'reference' => $opts['protest_number'] ?? null,
'notes' => 'ارتداد الشيك' . (!empty($code) ? ' — كود ' . $code : ''),
]);
$db->update('negotiable_instruments', [ $db->update('negotiable_instruments', [
'bounce_code' => $code, 'bounce_code' => $code,
'bounce_count' => (int) $instrument['bounce_count'] + 1, 'bounce_count' => (int) $instrument['bounce_count'] + 1,
...@@ -239,6 +246,11 @@ final class BouncedChequeService ...@@ -239,6 +246,11 @@ final class BouncedChequeService
} }
} }
self::logMovement($instrumentId, 'note', (string) ($instrument['status'] ?? ''), (string) ($instrument['status'] ?? ''), [
'date' => $date,
'notes' => 'معالجة ارتداد: ' . $resolution . (trim($notes) !== '' ? ' — ' . mb_substr($notes, 0, 200) : ''),
]);
$db->update('negotiable_instruments', [ $db->update('negotiable_instruments', [
'resolution' => $resolution, 'resolution' => $resolution,
'resolved_date' => $date, 'resolved_date' => $date,
...@@ -462,4 +474,29 @@ final class BouncedChequeService ...@@ -462,4 +474,29 @@ final class BouncedChequeService
{ {
return number_format((float) $v, self::SCALE, '.', ''); return number_format((float) $v, self::SCALE, '.', '');
} }
/**
* كل إجراء ارتداد أو معالجة بيتسجّل في سجل حركة الشيك كمان — عشان السجل
* يفضل كامل مهما كانت الشاشة اللي اتعمل منها الإجراء.
*/
private static function logMovement(int $instrumentId, string $action, ?string $from, ?string $to, array $opts = []): void
{
try {
$employee = App::getInstance()->currentEmployee();
App::getInstance()->db()->insert('instrument_movements', [
'instrument_id' => $instrumentId,
'action' => $action,
'from_status' => $from,
'to_status' => $to,
'action_date' => $opts['date'] ?? date('Y-m-d'),
'amount' => $opts['amount'] ?? null,
'reference' => $opts['reference'] ?? null,
'notes' => $opts['notes'] ?? null,
'performed_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
} catch (\Throwable $e) {
Logger::error('Instrument movement log failed: ' . $e->getMessage());
}
}
} }
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
/**
* سجل الشيكات الصادرة والواردة — البحث والفلترة والملخص المالي.
*/
final class InstrumentRegisterService
{
/**
* @param array $f من تاريخ/إلى تاريخ، الاتجاه، رقم الشيك، الطرف، البنك،
* الفرع، الحالة، المبلغ من/إلى، وأساس التاريخ (شيك/حركة)
*/
public static function search(array $f): array
{
$db = App::getInstance()->db();
[$where, $params] = self::buildWhere($f);
$rows = $db->select(
"SELECT ni.*,
b.account_name_ar AS bank_name,
br.name_ar AS branch_name
FROM negotiable_instruments ni
LEFT JOIN bank_accounts b ON b.id = ni.bank_account_id
LEFT JOIN branches br ON br.id = ni.branch_id
WHERE {$where}
ORDER BY ni.due_date ASC, ni.id DESC
LIMIT 500",
$params
);
$ids = array_map(fn($r) => (int) $r['id'], $rows);
$last = InstrumentLifecycleService::lastActionMap($ids);
foreach ($rows as &$r) {
$r['last_action'] = $last[(int) $r['id']]['action'] ?? null;
$r['last_action_date'] = $last[(int) $r['id']]['action_date'] ?? null;
}
return $rows;
}
/** ملخص مالي لكل اتجاه — عدد وقيمة لكل مجموعة حالات. */
public static function summary(array $f): array
{
$db = App::getInstance()->db();
$out = [];
foreach (['receivable', 'payable'] as $dir) {
$scoped = $f;
$scoped['direction'] = $dir;
[$where, $params] = self::buildWhere($scoped);
$row = $db->selectOne(
"SELECT COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total
FROM negotiable_instruments ni
WHERE {$where}",
$params
);
$byStatus = $db->select(
"SELECT ni.status, COUNT(*) AS n, COALESCE(SUM(ni.amount),0) AS total
FROM negotiable_instruments ni
WHERE {$where}
GROUP BY ni.status",
$params
);
$buckets = [];
foreach ($byStatus as $b) {
$buckets[$b['status']] = ['n' => (int) $b['n'], 'total' => (string) $b['total']];
}
$pick = function (array $statuses) use ($buckets) {
$n = 0; $t = '0.00';
foreach ($statuses as $s) {
if (isset($buckets[$s])) {
$n += $buckets[$s]['n'];
$t = bcadd($t, $buckets[$s]['total'], 2);
}
}
return ['n' => $n, 'total' => $t];
};
$out[$dir] = [
'all' => ['n' => (int) ($row['n'] ?? 0), 'total' => (string) ($row['total'] ?? '0.00')],
'settled' => $pick($dir === 'receivable' ? ['collected'] : ['paid']),
'bounced' => $pick(['bounced']),
'cancelled' => $pick(['cancelled']),
'open' => $pick($dir === 'receivable'
? ['in_hand', 'deposited', 'under_collection', 'endorsed', 'returned']
: ['in_hand', 'ready', 'delivered', 'pending_clearance', 'returned']),
'by_status' => $buckets,
];
}
return $out;
}
/** @return array{0:string,1:array} */
private static function buildWhere(array $f): array
{
$where = ['ni.is_archived = 0'];
$params = [];
if (!empty($f['direction']) && in_array($f['direction'], ['receivable', 'payable'], true)) {
$where[] = 'ni.direction = ?';
$params[] = $f['direction'];
}
// أساس التاريخ: تاريخ الشيك نفسه أم تاريخ آخر حركة عليه
$basis = ($f['date_basis'] ?? 'instrument') === 'movement' ? 'movement' : 'instrument';
if (!empty($f['date_from'])) {
if ($basis === 'movement') {
$where[] = 'EXISTS (SELECT 1 FROM instrument_movements m WHERE m.instrument_id = ni.id AND m.action_date >= ?)';
} else {
$where[] = 'ni.due_date >= ?';
}
$params[] = $f['date_from'];
}
if (!empty($f['date_to'])) {
if ($basis === 'movement') {
$where[] = 'EXISTS (SELECT 1 FROM instrument_movements m WHERE m.instrument_id = ni.id AND m.action_date <= ?)';
} else {
$where[] = 'ni.due_date <= ?';
}
$params[] = $f['date_to'];
}
if (!empty($f['number'])) {
$where[] = 'ni.instrument_number LIKE ?';
$params[] = '%' . $f['number'] . '%';
}
if (!empty($f['party'])) {
$where[] = '(ni.drawer_name LIKE ? OR ni.beneficiary_name LIKE ? OR ni.endorsee_name LIKE ?)';
$like = '%' . $f['party'] . '%';
$params[] = $like; $params[] = $like; $params[] = $like;
}
if (!empty($f['bank'])) {
$where[] = '(ni.drawer_bank LIKE ? OR EXISTS (SELECT 1 FROM bank_accounts b WHERE b.id = ni.bank_account_id AND b.account_name_ar LIKE ?))';
$like = '%' . $f['bank'] . '%';
$params[] = $like; $params[] = $like;
}
if (!empty($f['branch_id'])) {
$where[] = 'ni.branch_id = ?';
$params[] = (int) $f['branch_id'];
}
if (!empty($f['status'])) {
$where[] = 'ni.status = ?';
$params[] = $f['status'];
}
if (($f['amount_from'] ?? '') !== '') {
$where[] = 'ni.amount >= ?';
$params[] = $f['amount_from'];
}
if (($f['amount_to'] ?? '') !== '') {
$where[] = 'ni.amount <= ?';
$params[] = $f['amount_to'];
}
return [implode(' AND ', $where), $params];
}
}
This diff is collapsed.
This diff is collapsed.
...@@ -175,6 +175,7 @@ MenuRegistry::register('accounting', [ ...@@ -175,6 +175,7 @@ MenuRegistry::register('accounting', [
['label_ar' => 'الموازنات التقديرية', 'label_en' => 'Budgets', 'route' => '/accounting/budgets', 'permission' => 'accounting.budget.view', 'order' => 6], ['label_ar' => 'الموازنات التقديرية', 'label_en' => 'Budgets', 'route' => '/accounting/budgets', 'permission' => 'accounting.budget.view', 'order' => 6],
['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 7], ['label_ar' => 'الحسابات البنكية', 'label_en' => 'Bank Accounts', 'route' => '/accounting/bank-accounts', 'permission' => 'accounting.bank_account.view', 'order' => 7],
['label_ar' => 'الشيكات المرتدة', 'label_en' => 'Bounced Cheques', 'route' => '/accounting/instruments/bounced', 'permission' => 'accounting.instruments.view', 'order' => 33], ['label_ar' => 'الشيكات المرتدة', 'label_en' => 'Bounced Cheques', 'route' => '/accounting/instruments/bounced', 'permission' => 'accounting.instruments.view', 'order' => 33],
['label_ar' => 'صادر ووارد شيكات بنكية', 'label_en' => 'Cheque Register', 'route' => '/accounting/instruments/register', 'permission' => 'accounting.instruments.view', 'order' => 8],
['label_ar' => 'الأوراق التجارية', 'label_en' => 'Instruments', 'route' => '/accounting/instruments', 'permission' => 'accounting.instruments.view', 'order' => 8], ['label_ar' => 'الأوراق التجارية', 'label_en' => 'Instruments', 'route' => '/accounting/instruments', 'permission' => 'accounting.instruments.view', 'order' => 8],
['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9], ['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10], ['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10],
......
<?php
declare(strict_types=1);
/**
* دورة حياة الشيكات — الحالات الكاملة وسجل الحركة.
*
* قبل كده الحالة كانت بتتكتب فوق القديمة، فمفيش أي أثر لمين عمل إيه وإمتى.
* هنا بنضيف:
* - حالات ناقصة للصادر والوارد (إيداع، تسليم، جاهز للصرف، استبدال، إغلاق…)
* - عمود الفرع، والشيك البديل، وتاريخ الإغلاق
* - جدول instrument_movements: كل إجراء بيتسجّل كحركة جديدة ولا تتحذف أبدًا
* - حركة «تسجيل» أولى لكل شيك موجود، عشان السجل يبدأ من نقطة معروفة
*/
return [
'up' => "
ALTER TABLE `negotiable_instruments`
MODIFY COLUMN `status` ENUM(
'in_hand','under_collection','collected','bounced','endorsed',
'cancelled','paid','returned',
'ready','delivered','deposited','pending_clearance','replaced','closed'
) NOT NULL DEFAULT 'in_hand';
ALTER TABLE `negotiable_instruments`
ADD COLUMN `branch_id` BIGINT UNSIGNED NULL AFTER `bank_account_id`,
ADD COLUMN `replaced_by_id` BIGINT UNSIGNED NULL COMMENT 'الشيك البديل' AFTER `branch_id`,
ADD COLUMN `closed_at` DATETIME NULL AFTER `replaced_by_id`,
ADD INDEX `idx_ni_branch` (`branch_id`);
CREATE TABLE IF NOT EXISTS `instrument_movements` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`instrument_id` BIGINT UNSIGNED NOT NULL,
`action` VARCHAR(40) NOT NULL COMMENT 'register, deposit, collect, bounce, deliver, pay, cancel, replace, close, note',
`from_status` VARCHAR(30) NULL,
`to_status` VARCHAR(30) NULL,
`action_date` DATE NOT NULL,
`amount` DECIMAL(18,2) NULL COMMENT 'المبلغ المرتبط بالحركة إن وجد',
`bank_account_id` BIGINT UNSIGNED NULL,
`reference` VARCHAR(100) NULL,
`notes` TEXT NULL,
`performed_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_im_instrument` (`instrument_id`, `id`),
INDEX `idx_im_action_date` (`action_date`),
CONSTRAINT `fk_im_instrument` FOREIGN KEY (`instrument_id`)
REFERENCES `negotiable_instruments`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `instrument_movements`
(`instrument_id`, `action`, `from_status`, `to_status`, `action_date`, `amount`, `notes`, `performed_by`, `created_at`)
SELECT ni.`id`, 'register', NULL, ni.`status`, ni.`issue_date`, ni.`amount`,
'حركة افتتاحية — الشيك كان مسجّلًا قبل تفعيل سجل الحركة',
ni.`created_by`, ni.`created_at`
FROM `negotiable_instruments` ni
WHERE NOT EXISTS (
SELECT 1 FROM `instrument_movements` m WHERE m.`instrument_id` = ni.`id`
);
",
'down' => "
DROP TABLE IF EXISTS `instrument_movements`;
ALTER TABLE `negotiable_instruments`
DROP INDEX `idx_ni_branch`,
DROP COLUMN `branch_id`,
DROP COLUMN `replaced_by_id`,
DROP COLUMN `closed_at`;
",
];
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