feat(medical-store): add supplier ledger, payment tracking, short items book, and purchase orders
This commit is contained in:
parent
62397c5a98
commit
e635e823e5
|
|
@ -1675,8 +1675,21 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
||||||
$routes->get('medical-store/suppliers', 'MedicalStoreApi::suppliers');
|
$routes->get('medical-store/suppliers', 'MedicalStoreApi::suppliers');
|
||||||
$routes->post('medical-store/suppliers/save', 'MedicalStoreApi::saveSupplier');
|
$routes->post('medical-store/suppliers/save', 'MedicalStoreApi::saveSupplier');
|
||||||
$routes->post('medical-store/suppliers/delete', 'MedicalStoreApi::deleteSupplier');
|
$routes->post('medical-store/suppliers/delete', 'MedicalStoreApi::deleteSupplier');
|
||||||
|
$routes->get('medical-store/suppliers/ledger-summary', 'MedicalStoreApi::getSupplierLedgerSummary');
|
||||||
|
$routes->get('medical-store/suppliers/ledger/(:num)', 'MedicalStoreApi::getSupplierLedger/$1');
|
||||||
|
$routes->post('medical-store/suppliers/record-payment', 'MedicalStoreApi::saveSupplierPayment');
|
||||||
|
|
||||||
|
// Purchase Orders & Inward Bill
|
||||||
|
$routes->get('medical-store/purchase-orders', 'MedicalStoreApi::getPurchaseOrders');
|
||||||
|
$routes->get('medical-store/purchase-orders/(:num)', 'MedicalStoreApi::getPurchaseOrderDetail/$1');
|
||||||
|
$routes->post('medical-store/purchase-orders/save', 'MedicalStoreApi::savePurchaseOrder');
|
||||||
|
$routes->post('medical-store/purchase-orders/cancel', 'MedicalStoreApi::cancelPurchaseOrder');
|
||||||
$routes->post('medical-store/purchase/save', 'MedicalStoreApi::savePurchase');
|
$routes->post('medical-store/purchase/save', 'MedicalStoreApi::savePurchase');
|
||||||
$routes->post('medical-store/purchase/payment', 'MedicalStoreApi::saveSupplierPayment');
|
$routes->post('medical-store/purchase/payment', 'MedicalStoreApi::saveSupplierPayment');
|
||||||
|
|
||||||
|
// Short Items / Deficiency & Reorder Book
|
||||||
|
$routes->get('medical-store/stock/short-items', 'MedicalStoreApi::getShortItems');
|
||||||
|
$routes->post('medical-store/stock/flag-short-item', 'MedicalStoreApi::flagShortItem');
|
||||||
$routes->post('medical-store/transfer/request', 'MedicalStoreApi::requestTransfer');
|
$routes->post('medical-store/transfer/request', 'MedicalStoreApi::requestTransfer');
|
||||||
$routes->post('medical-store/transfer/dispatch', 'MedicalStoreApi::dispatchTransfer');
|
$routes->post('medical-store/transfer/dispatch', 'MedicalStoreApi::dispatchTransfer');
|
||||||
$routes->post('medical-store/transfer/receive', 'MedicalStoreApi::receiveTransfer');
|
$routes->post('medical-store/transfer/receive', 'MedicalStoreApi::receiveTransfer');
|
||||||
|
|
|
||||||
|
|
@ -2245,6 +2245,17 @@ class MedicalStoreApi extends BaseController
|
||||||
$this->db->table('mst_purchases')->insert($purchaseData);
|
$this->db->table('mst_purchases')->insert($purchaseData);
|
||||||
$purchaseId = $this->db->insertID();
|
$purchaseId = $this->db->insertID();
|
||||||
|
|
||||||
|
// If inward bill is created against a Purchase Order, mark PO as received
|
||||||
|
$poId = (int)($json['po_id'] ?? 0);
|
||||||
|
if ($poId > 0 && $this->db->tableExists('mst_purchase_orders')) {
|
||||||
|
$this->db->table('mst_purchase_orders')
|
||||||
|
->where('po_id', $poId)
|
||||||
|
->update([
|
||||||
|
'status' => 'received',
|
||||||
|
'updated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($purchaseItems as $pi) {
|
foreach ($purchaseItems as $pi) {
|
||||||
$pi['purchase_id'] = $purchaseId;
|
$pi['purchase_id'] = $purchaseId;
|
||||||
$this->db->table('mst_purchase_items')->insert($pi);
|
$this->db->table('mst_purchase_items')->insert($pi);
|
||||||
|
|
@ -2304,41 +2315,383 @@ class MedicalStoreApi extends BaseController
|
||||||
{
|
{
|
||||||
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||||
$supplierId = (int)($json['supplier_id'] ?? 0);
|
$supplierId = (int)($json['supplier_id'] ?? 0);
|
||||||
|
$purchaseId = (int)($json['purchase_id'] ?? 0);
|
||||||
$amount = (float)($json['amount'] ?? 0);
|
$amount = (float)($json['amount'] ?? 0);
|
||||||
$paymentMode = trim($json['payment_mode'] ?? 'Bank'); // Cash, Bank, Cheque, NEFT
|
$paymentMode = trim($json['payment_mode'] ?? 'Bank'); // Cash, Bank, Cheque, NEFT, UPI
|
||||||
$refNo = trim($json['reference_no'] ?? '');
|
$refNo = trim($json['reference_no'] ?? '');
|
||||||
|
$bankName = trim($json['bank_name'] ?? '');
|
||||||
|
$remarks = trim($json['remarks'] ?? '');
|
||||||
|
$paymentDate = !empty($json['payment_date']) ? trim($json['payment_date']) : date('Y-m-d');
|
||||||
$storeId = (int)($json['store_id'] ?? 1);
|
$storeId = (int)($json['store_id'] ?? 1);
|
||||||
|
|
||||||
if ($supplierId <= 0 || $amount <= 0) {
|
if ($supplierId <= 0 || $amount <= 0) {
|
||||||
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Supplier and valid amount required.']);
|
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Supplier and valid payment amount are required.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$vNo = 'PMT-' . time();
|
$vNo = 'PMT-' . date('Ymd') . '-' . rand(1000, 9999);
|
||||||
|
|
||||||
|
$this->db->transStart();
|
||||||
|
|
||||||
|
// 1. Insert into mst_supplier_payments
|
||||||
|
$this->db->table('mst_supplier_payments')->insert([
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'supplier_id' => $supplierId,
|
||||||
|
'purchase_id' => $purchaseId > 0 ? $purchaseId : null,
|
||||||
|
'payment_date' => $paymentDate,
|
||||||
|
'amount' => $amount,
|
||||||
|
'payment_mode' => $paymentMode,
|
||||||
|
'reference_no' => $refNo,
|
||||||
|
'bank_name' => $bankName,
|
||||||
|
'voucher_no' => $vNo,
|
||||||
|
'remarks' => $remarks,
|
||||||
|
'created_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$paymentId = $this->db->insertID();
|
||||||
|
|
||||||
|
// 2. Adjust invoice(s) paid_amount and payment_status
|
||||||
|
if ($purchaseId > 0) {
|
||||||
|
$inv = $this->db->table('mst_purchases')->where('purchase_id', $purchaseId)->get()->getRowArray();
|
||||||
|
if ($inv) {
|
||||||
|
$newPaid = (float)$inv['paid_amount'] + $amount;
|
||||||
|
$newStatus = ($newPaid >= (float)$inv['net_amount']) ? 'paid' : 'partially_paid';
|
||||||
|
$this->db->table('mst_purchases')->where('purchase_id', $purchaseId)->update([
|
||||||
|
'paid_amount' => $newPaid,
|
||||||
|
'payment_status' => $newStatus
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// On-account payment: FIFO allocation against unpaid / partially paid invoices
|
||||||
|
$remaining = $amount;
|
||||||
|
$unpaidInvoices = $this->db->table('mst_purchases')
|
||||||
|
->where('supplier_id', $supplierId)
|
||||||
|
->whereIn('payment_status', ['unpaid', 'partially_paid'])
|
||||||
|
->orderBy('invoice_date', 'ASC')
|
||||||
|
->orderBy('purchase_id', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
foreach ($unpaidInvoices as $uInv) {
|
||||||
|
if ($remaining <= 0) break;
|
||||||
|
$dueOnInv = max(0, (float)$uInv['net_amount'] - (float)$uInv['paid_amount']);
|
||||||
|
if ($dueOnInv <= 0) continue;
|
||||||
|
|
||||||
|
$alloc = min($remaining, $dueOnInv);
|
||||||
|
$newPaid = (float)$uInv['paid_amount'] + $alloc;
|
||||||
|
$newStatus = ($newPaid >= (float)$uInv['net_amount']) ? 'paid' : 'partially_paid';
|
||||||
|
|
||||||
|
$this->db->table('mst_purchases')->where('purchase_id', $uInv['purchase_id'])->update([
|
||||||
|
'paid_amount' => $newPaid,
|
||||||
|
'payment_status' => $newStatus
|
||||||
|
]);
|
||||||
|
|
||||||
|
$remaining -= $alloc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Double-entry ledger in mst_ledger_entries: Dr Sundry Creditor, Cr Cash/Bank
|
||||||
$creditorHead = $this->db->table('mst_account_heads')->where('head_code', '2001')->get()->getRowArray();
|
$creditorHead = $this->db->table('mst_account_heads')->where('head_code', '2001')->get()->getRowArray();
|
||||||
$sourceHeadCode = ($paymentMode === 'Cash') ? '1001' : '1002';
|
$sourceHeadCode = ($paymentMode === 'Cash') ? '1001' : '1002';
|
||||||
$sourceHead = $this->db->table('mst_account_heads')->where('head_code', $sourceHeadCode)->get()->getRowArray();
|
$sourceHead = $this->db->table('mst_account_heads')->where('head_code', $sourceHeadCode)->get()->getRowArray();
|
||||||
|
|
||||||
$this->db->transStart();
|
if ($creditorHead) {
|
||||||
|
$this->db->table('mst_ledger_entries')->insert([
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'voucher_no' => $vNo,
|
||||||
|
'voucher_type' => 'PAYMENT',
|
||||||
|
'voucher_date' => $paymentDate,
|
||||||
|
'account_head_id' => $creditorHead['head_id'],
|
||||||
|
'debit_amount' => $amount,
|
||||||
|
'credit_amount' => 0,
|
||||||
|
'reference_type' => 'supplier_payment',
|
||||||
|
'reference_id' => $supplierId,
|
||||||
|
'narration' => "Payment to Supplier #$supplierId via $paymentMode " . ($refNo ? "Ref: $refNo" : "")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Dr Creditor (reducing liability)
|
if ($sourceHead) {
|
||||||
$this->db->table('mst_ledger_entries')->insert([
|
$this->db->table('mst_ledger_entries')->insert([
|
||||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PAYMENT', 'voucher_date' => date('Y-m-d'),
|
'store_id' => $storeId,
|
||||||
'account_head_id' => $creditorHead['head_id'], 'debit_amount' => $amount, 'credit_amount' => 0,
|
'voucher_no' => $vNo,
|
||||||
'reference_type' => 'supplier_payment', 'reference_id' => $supplierId,
|
'voucher_type' => 'PAYMENT',
|
||||||
'narration' => "Payment made to Supplier #$supplierId via $paymentMode Ref: $refNo"
|
'voucher_date' => $paymentDate,
|
||||||
]);
|
'account_head_id' => $sourceHead['head_id'],
|
||||||
|
'debit_amount' => 0,
|
||||||
// Cr Cash / Bank
|
'credit_amount' => $amount,
|
||||||
$this->db->table('mst_ledger_entries')->insert([
|
'reference_type' => 'supplier_payment',
|
||||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PAYMENT', 'voucher_date' => date('Y-m-d'),
|
'reference_id' => $supplierId,
|
||||||
'account_head_id' => $sourceHead['head_id'], 'debit_amount' => 0, 'credit_amount' => $amount,
|
'narration' => "Payment outflow via $paymentMode to Supplier #$supplierId"
|
||||||
'reference_type' => 'supplier_payment', 'reference_id' => $supplierId,
|
]);
|
||||||
'narration' => "Payment outflow via $paymentMode to Supplier #$supplierId"
|
}
|
||||||
]);
|
|
||||||
|
|
||||||
$this->db->transComplete();
|
$this->db->transComplete();
|
||||||
|
|
||||||
return $this->response->setJSON(['status' => 1, 'message' => 'Supplier payment recorded successfully.', 'voucher_no' => $vNo]);
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to record supplier payment.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'message' => 'Supplier payment recorded successfully.',
|
||||||
|
'voucher_no' => $vNo,
|
||||||
|
'payment_id' => $paymentId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 5B. PURCHASE ORDERS (PO)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function getPurchaseOrders()
|
||||||
|
{
|
||||||
|
$storeId = (int)($this->request->getGet('store_id') ?: 0);
|
||||||
|
$status = trim($this->request->getGet('status') ?: '');
|
||||||
|
|
||||||
|
$builder = $this->db->table('mst_purchase_orders po')
|
||||||
|
->select('po.*, s.supplier_name, s.phone, s.contact_person, s.address, s.gstin')
|
||||||
|
->join('mst_suppliers s', 's.supplier_id = po.supplier_id')
|
||||||
|
->orderBy('po.po_date', 'DESC')
|
||||||
|
->orderBy('po.po_id', 'DESC');
|
||||||
|
|
||||||
|
if ($storeId > 0) {
|
||||||
|
$builder->where('po.store_id', $storeId);
|
||||||
|
}
|
||||||
|
if (!empty($status) && $status !== 'all') {
|
||||||
|
$builder->where('po.status', $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders = $builder->get()->getResultArray();
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'purchase_orders' => $orders
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPurchaseOrderDetail(int $poId)
|
||||||
|
{
|
||||||
|
$po = $this->db->table('mst_purchase_orders po')
|
||||||
|
->select('po.*, s.supplier_name, s.contact_person, s.phone, s.email, s.address, s.gstin, s.dl_no_20b, s.dl_no_21b')
|
||||||
|
->join('mst_suppliers s', 's.supplier_id = po.supplier_id')
|
||||||
|
->where('po.po_id', $poId)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (!$po) {
|
||||||
|
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Purchase order not found.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $this->db->table('mst_purchase_order_items')
|
||||||
|
->where('po_id', $poId)
|
||||||
|
->orderBy('po_item_id', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'purchase_order' => $po,
|
||||||
|
'items' => $items
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savePurchaseOrder()
|
||||||
|
{
|
||||||
|
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||||
|
$storeId = (int)($json['store_id'] ?? 1);
|
||||||
|
$supplierId = (int)($json['supplier_id'] ?? 0);
|
||||||
|
$items = $json['items'] ?? [];
|
||||||
|
$expectedDate = !empty($json['expected_delivery_date']) ? trim($json['expected_delivery_date']) : date('Y-m-d', strtotime('+3 days'));
|
||||||
|
$remarks = trim($json['remarks'] ?? '');
|
||||||
|
|
||||||
|
if ($supplierId <= 0 || empty($items)) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Supplier and order items are required.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$poNumber = 'PO-' . date('Ymd') . '-' . rand(1000, 9999);
|
||||||
|
|
||||||
|
$totalEstAmount = 0;
|
||||||
|
$orderItems = [];
|
||||||
|
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$itemId = (int)($it['item_id'] ?? 0);
|
||||||
|
$itemName = trim($it['item_name'] ?? '');
|
||||||
|
$orderPacks = max(1, (int)($it['order_packs'] ?? 1));
|
||||||
|
$unitsPerPack = max(1, (int)($it['units_per_pack'] ?? 10));
|
||||||
|
$estPtr = (float)($it['estimated_ptr'] ?? 0);
|
||||||
|
$gstRate = (float)($it['gst_rate'] ?? 12.00);
|
||||||
|
|
||||||
|
$lineNet = $orderPacks * $estPtr;
|
||||||
|
$lineTotal = round($lineNet * (1 + ($gstRate / 100)), 2);
|
||||||
|
$totalEstAmount += $lineTotal;
|
||||||
|
|
||||||
|
$orderItems[] = [
|
||||||
|
'item_id' => $itemId > 0 ? $itemId : null,
|
||||||
|
'item_name' => $itemName,
|
||||||
|
'generic_name' => trim($it['generic_name'] ?? ''),
|
||||||
|
'category' => trim($it['category'] ?? 'Tablet'),
|
||||||
|
'order_packs' => $orderPacks,
|
||||||
|
'units_per_pack' => $unitsPerPack,
|
||||||
|
'estimated_ptr' => $estPtr,
|
||||||
|
'gst_rate' => $gstRate,
|
||||||
|
'estimated_amount' => $lineTotal
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transStart();
|
||||||
|
|
||||||
|
$this->db->table('mst_purchase_orders')->insert([
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'supplier_id' => $supplierId,
|
||||||
|
'po_number' => $poNumber,
|
||||||
|
'po_date' => date('Y-m-d'),
|
||||||
|
'expected_delivery_date' => $expectedDate,
|
||||||
|
'total_items' => count($orderItems),
|
||||||
|
'total_estimated_amount' => round($totalEstAmount, 2),
|
||||||
|
'status' => 'ordered',
|
||||||
|
'remarks' => $remarks,
|
||||||
|
'created_at' => date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$poId = $this->db->insertID();
|
||||||
|
|
||||||
|
foreach ($orderItems as &$oi) {
|
||||||
|
$oi['po_id'] = $poId;
|
||||||
|
$this->db->table('mst_purchase_order_items')->insert($oi);
|
||||||
|
}
|
||||||
|
unset($oi);
|
||||||
|
|
||||||
|
$this->db->transComplete();
|
||||||
|
|
||||||
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to create purchase order.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'message' => 'Purchase order created successfully.',
|
||||||
|
'po_id' => $poId,
|
||||||
|
'po_number' => $poNumber,
|
||||||
|
'total_estimated_amount' => round($totalEstAmount, 2)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelPurchaseOrder()
|
||||||
|
{
|
||||||
|
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||||
|
$poId = (int)($json['po_id'] ?? 0);
|
||||||
|
if ($poId <= 0) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Valid PO ID required.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('mst_purchase_orders')
|
||||||
|
->where('po_id', $poId)
|
||||||
|
->update([
|
||||||
|
'status' => 'cancelled',
|
||||||
|
'updated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->response->setJSON(['status' => 1, 'message' => 'Purchase order marked as cancelled.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 5C. SHORT ITEMS & DEFICIENCY BOOK
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function getShortItems()
|
||||||
|
{
|
||||||
|
$storeId = (int)($this->request->getGet('store_id') ?: 1);
|
||||||
|
|
||||||
|
// Fetch items with current stock aggregated across batches for this store
|
||||||
|
$items = $this->db->table('mst_items i')
|
||||||
|
->select('i.item_id, i.item_name, i.category, i.unit_pack, i.units_per_pack, i.min_reorder_level, i.drug_schedule, i.hsn_code, i.gst_rate, COALESCE(SUM(s.current_qty), 0) as current_qty')
|
||||||
|
->join('mst_stock s', "s.item_id = i.item_id AND s.store_id = {$storeId}", 'left')
|
||||||
|
->where('i.is_active', 1)
|
||||||
|
->groupBy('i.item_id')
|
||||||
|
->having('current_qty <= i.min_reorder_level OR current_qty = 0')
|
||||||
|
->orderBy('current_qty', 'ASC')
|
||||||
|
->orderBy('i.item_name', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$shortList = [];
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$itemId = (int)$it['item_id'];
|
||||||
|
$currentQty = (int)$it['current_qty'];
|
||||||
|
$minReorder = max(1, (int)$it['min_reorder_level']);
|
||||||
|
$unitsPerPack = max(1, (int)$it['units_per_pack']);
|
||||||
|
|
||||||
|
// Find last purchase details for supplier and PTR
|
||||||
|
$lastPurchase = $this->db->table('mst_purchase_items pi')
|
||||||
|
->select('pi.ptr, pi.mrp, pi.gst_rate, p.supplier_id, p.received_date, s.supplier_name')
|
||||||
|
->join('mst_purchases p', 'p.purchase_id = pi.purchase_id')
|
||||||
|
->join('mst_suppliers s', 's.supplier_id = p.supplier_id')
|
||||||
|
->where('pi.item_id', $itemId)
|
||||||
|
->orderBy('p.received_date', 'DESC')
|
||||||
|
->orderBy('p.purchase_id', 'DESC')
|
||||||
|
->limit(1)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$lastPtr = (float)($lastPurchase['ptr'] ?? 0);
|
||||||
|
$lastMrp = (float)($lastPurchase['mrp'] ?? 0);
|
||||||
|
$lastSupplierId = (int)($lastPurchase['supplier_id'] ?? 0);
|
||||||
|
$lastSupplierName = $lastPurchase['supplier_name'] ?? 'Not Assigned';
|
||||||
|
$gstRate = (float)($lastPurchase['gst_rate'] ?? $it['gst_rate'] ?? 12.00);
|
||||||
|
|
||||||
|
// Suggested pack calculation: reorder up to (min_reorder_level * 2) units
|
||||||
|
$deficitUnits = max($unitsPerPack, ($minReorder * 2) - $currentQty);
|
||||||
|
$suggestedPacks = max(1, (int)ceil($deficitUnits / $unitsPerPack));
|
||||||
|
|
||||||
|
$status = ($currentQty <= 0) ? 'OUT_OF_STOCK' : 'LOW_STOCK';
|
||||||
|
|
||||||
|
$shortList[] = [
|
||||||
|
'item_id' => $itemId,
|
||||||
|
'item_name' => $it['item_name'],
|
||||||
|
'category' => $it['category'],
|
||||||
|
'unit_pack' => $it['unit_pack'],
|
||||||
|
'units_per_pack' => $unitsPerPack,
|
||||||
|
'min_reorder_level' => $minReorder,
|
||||||
|
'current_qty' => $currentQty,
|
||||||
|
'current_packs' => round($currentQty / $unitsPerPack, 1),
|
||||||
|
'suggested_packs' => $suggestedPacks,
|
||||||
|
'status' => $status,
|
||||||
|
'drug_schedule' => $it['drug_schedule'],
|
||||||
|
'last_ptr' => $lastPtr,
|
||||||
|
'last_mrp' => $lastMrp,
|
||||||
|
'gst_rate' => $gstRate,
|
||||||
|
'last_supplier_id' => $lastSupplierId,
|
||||||
|
'last_supplier_name' => $lastSupplierName,
|
||||||
|
'last_purchase_date' => $lastPurchase['received_date'] ?? null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'store_id' => $storeId,
|
||||||
|
'count' => count($shortList),
|
||||||
|
'short_items' => $shortList
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flagShortItem()
|
||||||
|
{
|
||||||
|
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||||
|
$itemId = (int)($json['item_id'] ?? 0);
|
||||||
|
$minReorder = (int)($json['min_reorder_level'] ?? 10);
|
||||||
|
|
||||||
|
if ($itemId <= 0) {
|
||||||
|
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Valid Item ID is required.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('mst_items')->where('item_id', $itemId)->update([
|
||||||
|
'min_reorder_level' => $minReorder
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'message' => 'Item reorder level updated and tracked in Short Book.',
|
||||||
|
'item_id' => $itemId,
|
||||||
|
'min_reorder_level' => $minReorder
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -2516,6 +2869,87 @@ class MedicalStoreApi extends BaseController
|
||||||
// 7. INDIAN ACCOUNTING, LEDGERS & GST REPORTS
|
// 7. INDIAN ACCOUNTING, LEDGERS & GST REPORTS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
public function getSupplierLedgerSummary()
|
||||||
|
{
|
||||||
|
$suppliers = $this->db->table('mst_suppliers')
|
||||||
|
->where('is_active', 1)
|
||||||
|
->orderBy('supplier_name', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
$totalAllPurchases = 0;
|
||||||
|
$totalAllPaid = 0;
|
||||||
|
$totalAllBalance = 0;
|
||||||
|
$totalPendingBills = 0;
|
||||||
|
|
||||||
|
foreach ($suppliers as $sup) {
|
||||||
|
$supId = (int)$sup['supplier_id'];
|
||||||
|
|
||||||
|
// Purchases total
|
||||||
|
$purchases = $this->db->table('mst_purchases')
|
||||||
|
->select('SUM(net_amount) as total_purchases, SUM(paid_amount) as total_paid_in_invoices, COUNT(*) as invoice_count, MAX(invoice_date) as last_invoice_date')
|
||||||
|
->where('supplier_id', $supId)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$totalPurchases = (float)($purchases['total_purchases'] ?? 0);
|
||||||
|
|
||||||
|
// Payments total from mst_supplier_payments
|
||||||
|
$pmts = $this->db->table('mst_supplier_payments')
|
||||||
|
->select('SUM(amount) as total_payments, COUNT(*) as payment_count, MAX(payment_date) as last_payment_date')
|
||||||
|
->where('supplier_id', $supId)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$totalPayments = (float)($pmts['total_payments'] ?? 0);
|
||||||
|
$effectivePaid = max($totalPayments, (float)($purchases['total_paid_in_invoices'] ?? 0));
|
||||||
|
$balance = max(0, $totalPurchases - $effectivePaid);
|
||||||
|
|
||||||
|
// Pending invoices count
|
||||||
|
$pendingCount = $this->db->table('mst_purchases')
|
||||||
|
->where('supplier_id', $supId)
|
||||||
|
->whereIn('payment_status', ['unpaid', 'partially_paid'])
|
||||||
|
->countAllResults();
|
||||||
|
|
||||||
|
$supData = [
|
||||||
|
'supplier_id' => $supId,
|
||||||
|
'supplier_name' => $sup['supplier_name'],
|
||||||
|
'contact_person' => $sup['contact_person'] ?? '',
|
||||||
|
'phone' => $sup['phone'] ?? '',
|
||||||
|
'email' => $sup['email'] ?? '',
|
||||||
|
'gstin' => $sup['gstin'] ?? '',
|
||||||
|
'city' => $sup['city'] ?? '',
|
||||||
|
'total_purchases' => round($totalPurchases, 2),
|
||||||
|
'total_paid' => round($effectivePaid, 2),
|
||||||
|
'outstanding_balance' => round($balance, 2),
|
||||||
|
'invoice_count' => (int)($purchases['invoice_count'] ?? 0),
|
||||||
|
'pending_invoices' => $pendingCount,
|
||||||
|
'last_purchase_date' => $purchases['last_invoice_date'] ?? null,
|
||||||
|
'last_payment_date' => $pmts['last_payment_date'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalAllPurchases += $totalPurchases;
|
||||||
|
$totalAllPaid += $effectivePaid;
|
||||||
|
$totalAllBalance += $balance;
|
||||||
|
$totalPendingBills += $pendingCount;
|
||||||
|
|
||||||
|
$result[] = $supData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'status' => 1,
|
||||||
|
'suppliers' => $result,
|
||||||
|
'summary' => [
|
||||||
|
'total_purchases' => round($totalAllPurchases, 2),
|
||||||
|
'total_paid' => round($totalAllPaid, 2),
|
||||||
|
'outstanding_balance' => round($totalAllBalance, 2),
|
||||||
|
'total_pending_bills' => $totalPendingBills,
|
||||||
|
'total_suppliers' => count($result)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getSupplierLedger(int $supplierId)
|
public function getSupplierLedger(int $supplierId)
|
||||||
{
|
{
|
||||||
$supplier = $this->db->table('mst_suppliers')->where('supplier_id', $supplierId)->get()->getRowArray();
|
$supplier = $this->db->table('mst_suppliers')->where('supplier_id', $supplierId)->get()->getRowArray();
|
||||||
|
|
@ -2523,35 +2957,110 @@ class MedicalStoreApi extends BaseController
|
||||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Supplier not found.']);
|
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Supplier not found.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ledger entries for this supplier
|
// 1. All invoices
|
||||||
$entries = $this->db->table('mst_ledger_entries le')
|
$invoices = $this->db->table('mst_purchases')
|
||||||
->select('le.*, ah.head_name')
|
->where('supplier_id', $supplierId)
|
||||||
->join('mst_account_heads ah', 'ah.head_id = le.account_head_id')
|
->orderBy('invoice_date', 'DESC')
|
||||||
->groupStart()
|
->orderBy('purchase_id', 'DESC')
|
||||||
->where('le.reference_type', 'purchase_invoice')
|
|
||||||
->orWhere('le.reference_type', 'supplier_payment')
|
|
||||||
->groupEnd()
|
|
||||||
->orderBy('le.voucher_date', 'ASC')
|
|
||||||
->orderBy('le.entry_id', 'ASC')
|
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|
||||||
$runningBalance = 0; // Credit positive (Payable)
|
$totalPurchases = 0;
|
||||||
$statement = [];
|
$totalInvoicePaid = 0;
|
||||||
foreach ($entries as $e) {
|
$pendingInvoices = [];
|
||||||
$debit = (float)$e['debit_amount'];
|
foreach ($invoices as &$inv) {
|
||||||
$credit = (float)$e['credit_amount'];
|
$net = (float)$inv['net_amount'];
|
||||||
$runningBalance += ($credit - $debit);
|
$paid = (float)$inv['paid_amount'];
|
||||||
|
$pending = max(0, $net - $paid);
|
||||||
|
$inv['pending_amount'] = round($pending, 2);
|
||||||
|
$totalPurchases += $net;
|
||||||
|
$totalInvoicePaid += $paid;
|
||||||
|
if ($pending > 0 || in_array($inv['payment_status'], ['unpaid', 'partially_paid'])) {
|
||||||
|
$pendingInvoices[] = $inv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($inv);
|
||||||
|
|
||||||
$e['balance'] = round($runningBalance, 2);
|
// 2. All payments
|
||||||
$statement[] = $e;
|
$payments = $this->db->table('mst_supplier_payments')
|
||||||
|
->where('supplier_id', $supplierId)
|
||||||
|
->orderBy('payment_date', 'DESC')
|
||||||
|
->orderBy('payment_id', 'DESC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$totalPaymentsMade = 0;
|
||||||
|
foreach ($payments as $pmt) {
|
||||||
|
$totalPaymentsMade += (float)$pmt['amount'];
|
||||||
|
}
|
||||||
|
$effectivePaid = max($totalPaymentsMade, $totalInvoicePaid);
|
||||||
|
$outstandingBalance = max(0, $totalPurchases - $effectivePaid);
|
||||||
|
|
||||||
|
// 3. Chronological Statement (Dr / Cr)
|
||||||
|
// Purchases are Credit (increases liability), Payments are Debit (reduces liability)
|
||||||
|
$rawEvents = [];
|
||||||
|
foreach ($invoices as $inv) {
|
||||||
|
$rawEvents[] = [
|
||||||
|
'date' => $inv['invoice_date'],
|
||||||
|
'sort_order' => 1,
|
||||||
|
'type' => 'INVOICE',
|
||||||
|
'voucher_no' => $inv['supplier_invoice_no'],
|
||||||
|
'particulars' => "Purchase Bill #" . $inv['supplier_invoice_no'] . " (" . $inv['payment_status'] . ")",
|
||||||
|
'debit' => 0,
|
||||||
|
'credit' => (float)$inv['net_amount'],
|
||||||
|
'invoice_id' => (int)$inv['purchase_id'],
|
||||||
|
'payment_id' => null,
|
||||||
|
'payment_mode'=> null,
|
||||||
|
'ref_no' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($payments as $pmt) {
|
||||||
|
$rawEvents[] = [
|
||||||
|
'date' => $pmt['payment_date'],
|
||||||
|
'sort_order' => 2,
|
||||||
|
'type' => 'PAYMENT',
|
||||||
|
'voucher_no' => $pmt['voucher_no'] ?: ('PMT-' . $pmt['payment_id']),
|
||||||
|
'particulars' => "Payment via " . $pmt['payment_mode'] . ($pmt['reference_no'] ? " (Ref: " . $pmt['reference_no'] . ")" : "") . ($pmt['bank_name'] ? " (" . $pmt['bank_name'] . ")" : ""),
|
||||||
|
'debit' => (float)$pmt['amount'],
|
||||||
|
'credit' => 0,
|
||||||
|
'invoice_id' => (int)($pmt['purchase_id'] ?? 0),
|
||||||
|
'payment_id' => (int)$pmt['payment_id'],
|
||||||
|
'payment_mode'=> $pmt['payment_mode'],
|
||||||
|
'ref_no' => $pmt['reference_no']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort chronological (date ASC, invoices before payments on same date)
|
||||||
|
usort($rawEvents, function ($a, $b) {
|
||||||
|
$c = strcmp($a['date'], $b['date']);
|
||||||
|
if ($c !== 0) return $c;
|
||||||
|
return $a['sort_order'] - $b['sort_order'];
|
||||||
|
});
|
||||||
|
|
||||||
|
$runningBalance = 0;
|
||||||
|
$statement = [];
|
||||||
|
foreach ($rawEvents as $ev) {
|
||||||
|
$runningBalance += ($ev['credit'] - $ev['debit']);
|
||||||
|
$ev['balance'] = round($runningBalance, 2);
|
||||||
|
$statement[] = $ev;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'status' => 1,
|
'status' => 1,
|
||||||
'supplier' => $supplier,
|
'supplier' => $supplier,
|
||||||
'current_balance' => round($runningBalance, 2),
|
'summary' => [
|
||||||
'statement' => $statement
|
'total_purchases' => round($totalPurchases, 2),
|
||||||
|
'total_paid' => round($effectivePaid, 2),
|
||||||
|
'outstanding_balance' => round($outstandingBalance, 2),
|
||||||
|
'total_invoices' => count($invoices),
|
||||||
|
'pending_invoices_count' => count($pendingInvoices),
|
||||||
|
'pending_amount' => round($outstandingBalance, 2)
|
||||||
|
],
|
||||||
|
'pending_invoices' => $pendingInvoices,
|
||||||
|
'invoices' => $invoices,
|
||||||
|
'payments' => $payments,
|
||||||
|
'statement' => $statement
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class CreateSupplierLedgersAndPurchaseOrders extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
|
// 1. Table: mst_supplier_payments
|
||||||
|
if (!$db->tableExists('mst_supplier_payments')) {
|
||||||
|
$this->forge->addField([
|
||||||
|
'payment_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'unsigned' => true,
|
||||||
|
'auto_increment' => true,
|
||||||
|
],
|
||||||
|
'store_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'default' => 1,
|
||||||
|
],
|
||||||
|
'supplier_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
],
|
||||||
|
'purchase_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
],
|
||||||
|
'payment_date' => [
|
||||||
|
'type' => 'DATE',
|
||||||
|
],
|
||||||
|
'amount' => [
|
||||||
|
'type' => 'DECIMAL',
|
||||||
|
'constraint' => '12,2',
|
||||||
|
'default' => 0.00,
|
||||||
|
],
|
||||||
|
'payment_mode' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 50,
|
||||||
|
'default' => 'Bank',
|
||||||
|
],
|
||||||
|
'reference_no' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 100,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
],
|
||||||
|
'bank_name' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 100,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
],
|
||||||
|
'voucher_no' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 100,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
],
|
||||||
|
'remarks' => [
|
||||||
|
'type' => 'TEXT',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'created_by' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'created_at' => [
|
||||||
|
'type' => 'DATETIME',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->forge->addKey('payment_id', true);
|
||||||
|
$this->forge->addKey('supplier_id');
|
||||||
|
$this->forge->addKey('purchase_id');
|
||||||
|
$this->forge->addKey('payment_date');
|
||||||
|
$this->forge->createTable('mst_supplier_payments', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Table: mst_purchase_orders
|
||||||
|
if (!$db->tableExists('mst_purchase_orders')) {
|
||||||
|
$this->forge->addField([
|
||||||
|
'po_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'unsigned' => true,
|
||||||
|
'auto_increment' => true,
|
||||||
|
],
|
||||||
|
'store_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'default' => 1,
|
||||||
|
],
|
||||||
|
'supplier_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
],
|
||||||
|
'po_number' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 100,
|
||||||
|
],
|
||||||
|
'po_date' => [
|
||||||
|
'type' => 'DATE',
|
||||||
|
],
|
||||||
|
'expected_delivery_date' => [
|
||||||
|
'type' => 'DATE',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'total_items' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'default' => 0,
|
||||||
|
],
|
||||||
|
'total_estimated_amount' => [
|
||||||
|
'type' => 'DECIMAL',
|
||||||
|
'constraint' => '12,2',
|
||||||
|
'default' => 0.00,
|
||||||
|
],
|
||||||
|
'status' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 30,
|
||||||
|
'default' => 'ordered', // draft, ordered, received, cancelled
|
||||||
|
],
|
||||||
|
'remarks' => [
|
||||||
|
'type' => 'TEXT',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'created_by' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'created_at' => [
|
||||||
|
'type' => 'DATETIME',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'updated_at' => [
|
||||||
|
'type' => 'DATETIME',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->forge->addKey('po_id', true);
|
||||||
|
$this->forge->addKey('po_number');
|
||||||
|
$this->forge->addKey('supplier_id');
|
||||||
|
$this->forge->addKey('status');
|
||||||
|
$this->forge->createTable('mst_purchase_orders', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Table: mst_purchase_order_items
|
||||||
|
if (!$db->tableExists('mst_purchase_order_items')) {
|
||||||
|
$this->forge->addField([
|
||||||
|
'po_item_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'unsigned' => true,
|
||||||
|
'auto_increment' => true,
|
||||||
|
],
|
||||||
|
'po_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'unsigned' => true,
|
||||||
|
],
|
||||||
|
'item_id' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
],
|
||||||
|
'item_name' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 200,
|
||||||
|
],
|
||||||
|
'generic_name' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 250,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'category' => [
|
||||||
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 100,
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'order_packs' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'default' => 1,
|
||||||
|
],
|
||||||
|
'units_per_pack' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 11,
|
||||||
|
'default' => 10,
|
||||||
|
],
|
||||||
|
'estimated_ptr' => [
|
||||||
|
'type' => 'DECIMAL',
|
||||||
|
'constraint' => '10,2',
|
||||||
|
'default' => 0.00,
|
||||||
|
],
|
||||||
|
'gst_rate' => [
|
||||||
|
'type' => 'DECIMAL',
|
||||||
|
'constraint' => '5,2',
|
||||||
|
'default' => 12.00,
|
||||||
|
],
|
||||||
|
'estimated_amount' => [
|
||||||
|
'type' => 'DECIMAL',
|
||||||
|
'constraint' => '12,2',
|
||||||
|
'default' => 0.00,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->forge->addKey('po_item_id', true);
|
||||||
|
$this->forge->addKey('po_id');
|
||||||
|
$this->forge->addKey('item_id');
|
||||||
|
$this->forge->createTable('mst_purchase_order_items', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Ensure index on mst_purchases for payment_status and supplier_id
|
||||||
|
if ($db->tableExists('mst_purchases')) {
|
||||||
|
try {
|
||||||
|
$db->query("ALTER TABLE `mst_purchases` ADD INDEX `idx_mst_purchases_supp_status` (`supplier_id`, `payment_status`)");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Index may already exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
$this->forge->dropTable('mst_purchase_order_items', true);
|
||||||
|
$this->forge->dropTable('mst_purchase_orders', true);
|
||||||
|
$this->forge->dropTable('mst_supplier_payments', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user