From 5a13ce2b5600da732fe9a72fb4291e8c6a24cd39 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Mon, 26 Oct 2026 22:31:00 +0530 Subject: [PATCH] feat(pharmacy): enhance PO creation with medicine master search, last PTR auto-fill, exact GST selection, and scheme/discount support --- app/Config/Routes.php | 1 + app/Controllers/MedicalProcurement.php | 401 ++++++++++++++++-- app/Models/PharmacyPurchaseOrderItemModel.php | 3 + app/Views/medical/po_form.php | 237 ++++++++--- app/Views/medical/po_print_pdf.php | 55 ++- 5 files changed, 604 insertions(+), 93 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 4bd2a60..6029a02 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -133,6 +133,7 @@ $routes->get('Medical/po_edit/(:num)', 'MedicalProcurement::po_edit/$1'); $routes->post('Medical/po_status/(:num)', 'MedicalProcurement::po_status/$1'); $routes->get('Medical/po_print/(:num)', 'MedicalProcurement::po_print/$1'); $routes->get('Medical/po_reorder_items/(:num)', 'MedicalProcurement::po_reorder_items/$1'); +$routes->get('Medical/po_search_drug', 'MedicalProcurement::po_search_drug'); $routes->get('Medical/grn_list', 'MedicalProcurement::grn_list'); $routes->get('Medical/grn_new', 'MedicalProcurement::grn_new'); diff --git a/app/Controllers/MedicalProcurement.php b/app/Controllers/MedicalProcurement.php index 17e41a3..940e77e 100644 --- a/app/Controllers/MedicalProcurement.php +++ b/app/Controllers/MedicalProcurement.php @@ -26,6 +26,28 @@ class MedicalProcurement extends BaseController $this->grnModel = new PharmacyGrnModel(); $this->grnItemModel = new PharmacyGrnItemModel(); $this->ensureGrnSchema(); + $this->ensurePoSchema(); + } + + private function ensurePoSchema(): void + { + try { + if (!$this->db->tableExists('mst_purchase_order_items')) { + return; + } + + if (!$this->db->fieldExists('discount_percent', 'mst_purchase_order_items')) { + $this->db->query("ALTER TABLE `mst_purchase_order_items` ADD COLUMN `discount_percent` DECIMAL(5,2) DEFAULT 0.00 AFTER `estimated_ptr`"); + } + if (!$this->db->fieldExists('sch_disc_per', 'mst_purchase_order_items')) { + $this->db->query("ALTER TABLE `mst_purchase_order_items` ADD COLUMN `sch_disc_per` DECIMAL(5,2) DEFAULT 0.00 AFTER `discount_percent`"); + } + if (!$this->db->fieldExists('scheme_info', 'mst_purchase_order_items')) { + $this->db->query("ALTER TABLE `mst_purchase_order_items` ADD COLUMN `scheme_info` VARCHAR(100) DEFAULT '' AFTER `sch_disc_per`"); + } + } catch (\Throwable $e) { + log_message('error', 'ensurePoSchema: ' . $e->getMessage()); + } } private function ensureGrnSchema(): void @@ -215,6 +237,7 @@ class MedicalProcurement extends BaseController return $deny; } + $poId = (int)($this->request->getPost('po_id') ?? 0); $supplierId = (int)$this->request->getPost('supplier_id'); $storeId = (int)($this->request->getPost('store_id') ?: 1); $poDate = trim((string)$this->request->getPost('po_date') ?: date('Y-m-d')); @@ -233,7 +256,6 @@ class MedicalProcurement extends BaseController return $this->response->setJSON(['status' => 0, 'message' => 'Please add at least one item to the purchase order.']); } - $poNumber = $this->poModel->generatePoNumber(); $userId = $this->getCurrentUserId(); $totalEstimatedAmount = 0.0; @@ -249,16 +271,23 @@ class MedicalProcurement extends BaseController $unitsPerPack = (int)($row['units_per_pack'] ?? 10); $estimatedPtr = (float)($row['estimated_ptr'] ?? 0); $gstRate = (float)($row['gst_rate'] ?? 12.0); + $discountPercent = (float)($row['discount_percent'] ?? 0.0); + $schDiscPer = (float)($row['sch_disc_per'] ?? 0.0); + $schemeInfo = trim((string)($row['scheme_info'] ?? '')); if ($itemId <= 0 || $itemName === '' || $orderPacks <= 0) { continue; } - $lineSubtotal = round($orderPacks * $estimatedPtr, 2); - $lineTax = round($lineSubtotal * ($gstRate / 100), 2); - $lineTotal = $lineSubtotal + $lineTax; + $lineGross = round($orderPacks * $estimatedPtr, 2); + $schAmt = ($schDiscPer > 0) ? round($lineGross * ($schDiscPer / 100), 2) : 0.0; + $afterSch = max(0.0, $lineGross - $schAmt); + $discAmt = ($discountPercent > 0) ? round($afterSch * ($discountPercent / 100), 2) : 0.0; + $taxable = max(0.0, $afterSch - $discAmt); + $lineTax = round($taxable * ($gstRate / 100), 2); + $lineTotal = $taxable + $lineTax; - $totalEstimatedAmount += $lineSubtotal; + $totalEstimatedAmount += $taxable; $totalTaxAmount += $lineTax; $validItems[] = [ @@ -271,6 +300,9 @@ class MedicalProcurement extends BaseController 'received_packs' => 0, 'remaining_packs' => $orderPacks, 'estimated_ptr' => $estimatedPtr, + 'discount_percent' => $discountPercent, + 'sch_disc_per' => $schDiscPer, + 'scheme_info' => $schemeInfo, 'gst_rate' => $gstRate, 'estimated_amount' => $lineTotal, 'status' => 'pending', @@ -283,35 +315,75 @@ class MedicalProcurement extends BaseController $this->db->transStart(); - $poHeader = [ - 'store_id' => $storeId, - 'supplier_id' => $supplierId, - 'po_number' => $poNumber, - 'po_date' => $poDate, - 'expected_delivery_date' => $expectedDelivery ?: null, - 'payment_terms' => $paymentTerms, - 'total_items' => count($validItems), - 'total_estimated_amount' => $totalEstimatedAmount, - 'tax_amount' => $totalTaxAmount, - 'grand_total' => $totalEstimatedAmount + $totalTaxAmount, - 'status' => $status, - 'remarks' => $remarks, - 'created_by' => $userId, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]; + if ($poId > 0) { + $existingPo = $this->poModel->find($poId); + if (!$existingPo) { + $this->db->transRollback(); + return $this->response->setJSON(['status' => 0, 'message' => 'Existing purchase order not found.']); + } - if ($status === 'approved' || $status === 'ordered') { - $poHeader['approved_by'] = $userId; - $poHeader['approved_at'] = date('Y-m-d H:i:s'); - } + $poNumber = $existingPo['po_number']; - $this->poModel->insert($poHeader); - $poId = (int)$this->db->insertID(); + $poHeader = [ + 'store_id' => $storeId, + 'supplier_id' => $supplierId, + 'po_date' => $poDate, + 'expected_delivery_date' => $expectedDelivery ?: null, + 'payment_terms' => $paymentTerms, + 'total_items' => count($validItems), + 'total_estimated_amount' => $totalEstimatedAmount, + 'tax_amount' => $totalTaxAmount, + 'grand_total' => $totalEstimatedAmount + $totalTaxAmount, + 'status' => $status, + 'remarks' => $remarks, + 'updated_at' => date('Y-m-d H:i:s'), + ]; - foreach ($validItems as &$item) { - $item['po_id'] = $poId; - $this->poItemModel->insert($item); + if (($status === 'approved' || $status === 'ordered') && empty($existingPo['approved_at'])) { + $poHeader['approved_by'] = $userId; + $poHeader['approved_at'] = date('Y-m-d H:i:s'); + } + + $this->poModel->update($poId, $poHeader); + $this->poItemModel->where('po_id', $poId)->delete(); + + foreach ($validItems as &$item) { + $item['po_id'] = $poId; + $this->poItemModel->insert($item); + } + } else { + $poNumber = $this->poModel->generatePoNumber(); + + $poHeader = [ + 'store_id' => $storeId, + 'supplier_id' => $supplierId, + 'po_number' => $poNumber, + 'po_date' => $poDate, + 'expected_delivery_date' => $expectedDelivery ?: null, + 'payment_terms' => $paymentTerms, + 'total_items' => count($validItems), + 'total_estimated_amount' => $totalEstimatedAmount, + 'tax_amount' => $totalTaxAmount, + 'grand_total' => $totalEstimatedAmount + $totalTaxAmount, + 'status' => $status, + 'remarks' => $remarks, + 'created_by' => $userId, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + if ($status === 'approved' || $status === 'ordered') { + $poHeader['approved_by'] = $userId; + $poHeader['approved_at'] = date('Y-m-d H:i:s'); + } + + $this->poModel->insert($poHeader); + $poId = (int)$this->db->insertID(); + + foreach ($validItems as &$item) { + $item['po_id'] = $poId; + $this->poItemModel->insert($item); + } } $this->db->transComplete(); @@ -322,7 +394,7 @@ class MedicalProcurement extends BaseController return $this->response->setJSON([ 'status' => 1, - 'message' => "Purchase Order {$poNumber} created successfully!", + 'message' => "Purchase Order {$poNumber} saved successfully!", 'po_id' => $poId, 'po_number' => $poNumber, ]); @@ -460,15 +532,19 @@ class MedicalProcurement extends BaseController // Fetch drugs supplied by this vendor or general low stock items $sql = "SELECT p.id AS item_id, p.item_name, p.genericname AS generic_name, - p.formulation AS category, 10 AS units_per_pack, + p.formulation AS category, + IFNULL(t.packing, IFNULL(p.packing, 10)) AS units_per_pack, IFNULL(t.purchase_price, 0) AS estimated_ptr, - IFNULL(t.CGST_per + t.SGST_per, 12) AS gst_rate, + IFNULL(t.CGST_per + t.SGST_per, IFNULL(p.CGST_per + p.SGST_per, 12)) AS gst_rate, + IFNULL(t.discount, 0) AS discount_percent, + IFNULL(t.sch_disc_per, 0) AS sch_disc_per, + IF(t.qty_free > 0, CONCAT('Free: ', CAST(t.qty_free AS UNSIGNED)), '') AS scheme_info, (IFNULL(t.total_unit, 0) - IFNULL(t.total_sale_unit, 0) - IFNULL(t.total_return_unit, 0) - IFNULL(t.total_lost_unit, 0)) AS current_stock FROM med_product_master p LEFT JOIN purchase_invoice_item t ON p.id = t.item_code LEFT JOIN purchase_invoice m ON t.purchase_id = m.id WHERE (m.sid = ? OR m.sid IS NULL) - AND p.is_continue = 1 + AND (p.is_continue = 1 OR p.is_continue IS NULL) GROUP BY p.id HAVING current_stock < 50 ORDER BY current_stock ASC @@ -483,6 +559,261 @@ class MedicalProcurement extends BaseController ]); } + public function po_search_drug() + { + if ($deny = $this->ensurePharmacyAccess()) { + return $deny; + } + + $term = trim((string)$this->request->getGet('term')); + $supplierId = (int)($this->request->getGet('supplier_id') ?? 0); + + if ($term === '') { + return $this->response->setJSON([]); + } + + if (!$this->db->tableExists('med_product_master')) { + return $this->response->setJSON([]); + } + + $pFields = $this->db->getFieldNames('med_product_master') ?? []; + $hasGeneric = in_array('genericname', $pFields, true); + $hasCompany = in_array('company_name', $pFields, true); + $hasContinue = in_array('is_continue', $pFields, true); + + $builder = $this->db->table('med_product_master p') + ->select('p.id, p.item_name, p.formulation, ' + . ($hasGeneric ? 'p.genericname' : "'' AS genericname") . ', ' + . (in_array('packing', $pFields, true) ? 'p.packing' : "'10' AS packing") . ', ' + . (in_array('CGST_per', $pFields, true) ? 'p.CGST_per' : '0 AS CGST_per') . ', ' + . (in_array('SGST_per', $pFields, true) ? 'p.SGST_per' : '0 AS SGST_per') . ', ' + . ($hasCompany ? 'p.company_name' : "'' AS company_name")); + + if ($hasContinue) { + $builder->groupStart() + ->where('p.is_continue', 1) + ->orWhere('p.is_continue IS NULL') + ->groupEnd(); + } + + $builder->groupStart() + ->like('p.item_name', $term); + if ($hasGeneric) { + $builder->orLike('p.genericname', $term); + } + if ($hasCompany) { + $builder->orLike('p.company_name', $term); + } + $builder->groupEnd(); + + // Order: matches starting with term first, then alphabetical + $builder->orderBy("CASE WHEN p.item_name LIKE " . $this->db->escape($term . '%') . " THEN 1 ELSE 2 END", 'ASC', false) + ->orderBy('p.item_name', 'ASC') + ->limit(40); + + $products = $builder->get()->getResultArray(); + + $hasPiItem = $this->db->tableExists('purchase_invoice_item'); + $hasPi = $this->db->tableExists('purchase_invoice'); + $hasGrnItem = $this->db->tableExists('mst_grn_items'); + $hasGrn = $this->db->tableExists('mst_grns'); + + $result = []; + + foreach ($products as $p) { + $itemId = (int)$p['id']; + $lastPur = null; + $isVendorMatch = false; + + // 1. Check last purchase from selected supplier in purchase_invoice_item + if ($supplierId > 0 && $hasPiItem && $hasPi) { + $lastPur = $this->db->table('purchase_invoice_item pii') + ->select('pii.purchase_price, pii.purchase_unit_rate, pii.packing, pii.CGST_per, pii.SGST_per, pii.discount, pii.sch_disc_per, pii.sch_disc_amt, pii.qty_free, pii.mrp, pi.sid, pi.supp_name, pi.date_of_invoice') + ->join('purchase_invoice pi', 'pi.id = pii.purchase_id', 'left') + ->where('pii.item_code', $itemId) + ->where('pi.sid', $supplierId) + ->orderBy('pii.id', 'DESC') + ->get(1) + ->getRowArray(); + if ($lastPur) { + $isVendorMatch = true; + } + } + + // 2. Check in mst_grn_items from selected supplier if still not found + if (!$lastPur && $supplierId > 0 && $hasGrnItem && $hasGrn) { + $lastGrn = $this->db->table('mst_grn_items gi') + ->select('gi.unit_purchase_rate, gi.packing, gi.gross_amount, gi.cgst_percent, gi.sgst_percent, gi.igst_percent, gi.discount_percent, gi.sch_disc_per, gi.sch_disc_amt, gi.free_packs, gi.mrp, g.supplier_id AS sid, s.name_supplier AS supp_name, g.grn_date AS date_of_invoice') + ->join('mst_grns g', 'g.grn_id = gi.grn_id', 'left') + ->join('med_supplier s', 's.sid = g.supplier_id', 'left') + ->where('gi.item_code', $itemId) + ->where('g.supplier_id', $supplierId) + ->orderBy('gi.grn_item_id', 'DESC') + ->get(1) + ->getRowArray(); + + if ($lastGrn) { + $isVendorMatch = true; + $pack = (int)($lastGrn['packing'] ?? 10); + $ratePerPack = (float)($lastGrn['unit_purchase_rate'] ?? 0) * ($pack > 0 ? $pack : 1); + $lastPur = [ + 'purchase_price' => $ratePerPack, + 'purchase_unit_rate' => $lastGrn['unit_purchase_rate'], + 'packing' => $pack, + 'CGST_per' => $lastGrn['cgst_percent'], + 'SGST_per' => $lastGrn['sgst_percent'], + 'discount' => $lastGrn['discount_percent'], + 'sch_disc_per' => $lastGrn['sch_disc_per'], + 'sch_disc_amt' => $lastGrn['sch_disc_amt'], + 'qty_free' => $lastGrn['free_packs'], + 'mrp' => $lastGrn['mrp'], + 'sid' => $lastGrn['sid'], + 'supp_name' => $lastGrn['supp_name'], + 'date_of_invoice' => $lastGrn['date_of_invoice'], + ]; + } + } + + // 3. Fallback: check last purchase from ANY supplier in purchase_invoice_item + if (!$lastPur && $hasPiItem && $hasPi) { + $lastPur = $this->db->table('purchase_invoice_item pii') + ->select('pii.purchase_price, pii.purchase_unit_rate, pii.packing, pii.CGST_per, pii.SGST_per, pii.discount, pii.sch_disc_per, pii.sch_disc_amt, pii.qty_free, pii.mrp, pi.sid, pi.supp_name, pi.date_of_invoice') + ->join('purchase_invoice pi', 'pi.id = pii.purchase_id', 'left') + ->where('pii.item_code', $itemId) + ->orderBy('pii.id', 'DESC') + ->get(1) + ->getRowArray(); + } + + // 4. Fallback: check in mst_grn_items from ANY supplier + if (!$lastPur && $hasGrnItem && $hasGrn) { + $lastGrn = $this->db->table('mst_grn_items gi') + ->select('gi.unit_purchase_rate, gi.packing, gi.gross_amount, gi.cgst_percent, gi.sgst_percent, gi.igst_percent, gi.discount_percent, gi.sch_disc_per, gi.sch_disc_amt, gi.free_packs, gi.mrp, g.supplier_id AS sid, s.name_supplier AS supp_name, g.grn_date AS date_of_invoice') + ->join('mst_grns g', 'g.grn_id = gi.grn_id', 'left') + ->join('med_supplier s', 's.sid = g.supplier_id', 'left') + ->where('gi.item_code', $itemId) + ->orderBy('gi.grn_item_id', 'DESC') + ->get(1) + ->getRowArray(); + + if ($lastGrn) { + $pack = (int)($lastGrn['packing'] ?? 10); + $ratePerPack = (float)($lastGrn['unit_purchase_rate'] ?? 0) * ($pack > 0 ? $pack : 1); + $lastPur = [ + 'purchase_price' => $ratePerPack, + 'purchase_unit_rate' => $lastGrn['unit_purchase_rate'], + 'packing' => $pack, + 'CGST_per' => $lastGrn['cgst_percent'], + 'SGST_per' => $lastGrn['sgst_percent'], + 'discount' => $lastGrn['discount_percent'], + 'sch_disc_per' => $lastGrn['sch_disc_per'], + 'sch_disc_amt' => $lastGrn['sch_disc_amt'], + 'qty_free' => $lastGrn['free_packs'], + 'mrp' => $lastGrn['mrp'], + 'sid' => $lastGrn['sid'], + 'supp_name' => $lastGrn['supp_name'], + 'date_of_invoice' => $lastGrn['date_of_invoice'], + ]; + } + } + + // Determine packing + $packing = (int)($lastPur['packing'] ?? 0); + if ($packing <= 0) { + $packing = (int)preg_replace('/[^0-9]/', '', (string)($p['packing'] ?? '')); + } + if ($packing <= 0) { + $packing = 10; + } + + // Determine Rate (estimated PTR per pack) + $rate = 0.0; + if ($lastPur && (float)($lastPur['purchase_price'] ?? 0) > 0) { + $rate = (float)$lastPur['purchase_price']; + } elseif ($lastPur && (float)($lastPur['purchase_unit_rate'] ?? 0) > 0) { + $rate = (float)$lastPur['purchase_unit_rate'] * $packing; + } + + // Determine GST % + $gstRate = null; + if ($lastPur) { + $purGst = (float)($lastPur['CGST_per'] ?? 0) + (float)($lastPur['SGST_per'] ?? 0); + if ($purGst > 0) { + $gstRate = $purGst; + } + } + if ($gstRate === null) { + $masterGst = (float)($p['CGST_per'] ?? 0) + (float)($p['SGST_per'] ?? 0); + if ($masterGst > 0) { + $gstRate = $masterGst; + } + } + if ($gstRate === null) { + $gstRate = 12.0; + } + + // Scheme & Discount + $tradeDisc = (float)($lastPur['discount'] ?? 0); + $schDiscPer = (float)($lastPur['sch_disc_per'] ?? 0); + $schDiscAmt = (float)($lastPur['sch_disc_amt'] ?? 0); + $freeQty = (float)($lastPur['qty_free'] ?? 0); + + $schemeParts = []; + if ($tradeDisc > 0) { + $schemeParts[] = "Disc: " . rtrim(rtrim(number_format($tradeDisc, 2), '0'), '.') . "%"; + } + if ($schDiscPer > 0) { + $schemeParts[] = "Sch Disc: " . rtrim(rtrim(number_format($schDiscPer, 2), '0'), '.') . "%"; + } + if ($schDiscAmt > 0) { + $schemeParts[] = "Sch Amt: ₹" . number_format($schDiscAmt, 2); + } + if ($freeQty > 0) { + $schemeParts[] = "Free: " . (int)$freeQty; + } + $schemeSummary = !empty($schemeParts) ? implode(', ', $schemeParts) : ''; + + $schemeInfo = ''; + if ($freeQty > 0) { + $schemeInfo = "Free: " . (int)$freeQty; + } elseif ($schDiscPer > 0) { + $schemeInfo = "Sch: " . rtrim(rtrim(number_format($schDiscPer, 2), '0'), '.') . "%"; + } + + // Clean Label for Autocomplete: Only Medicine Name, Formulation, and Generic Salt + $cleanLabel = trim($p['item_name']); + if (!empty($p['formulation'])) { + $cleanLabel .= ' [' . trim($p['formulation']) . ']'; + } + if (!empty($p['genericname'])) { + $cleanLabel .= ' - ' . trim($p['genericname']); + } + + $result[] = [ + 'label' => $cleanLabel, + 'value' => $p['item_name'], + 'item_code' => (int)$p['id'], + 'generic_name' => (string)($p['genericname'] ?? ''), + 'formulation' => (string)($p['formulation'] ?? 'Tablet'), + 'packing' => $packing, + 'estimated_ptr' => number_format($rate, 2, '.', ''), + 'gst_rate' => (float)$gstRate, + 'discount_percent' => number_format($tradeDisc, 2, '.', ''), + 'sch_disc_per' => number_format($schDiscPer, 2, '.', ''), + 'sch_disc_amt' => number_format($schDiscAmt, 2, '.', ''), + 'qty_free' => (float)$freeQty, + 'scheme_info' => $schemeInfo, + 'scheme_summary' => $schemeSummary, + 'has_last_purchase' => $lastPur ? 1 : 0, + 'is_vendor_match' => $isVendorMatch ? 1 : 0, + 'last_supplier_name' => (string)($lastPur['supp_name'] ?? ''), + 'last_purchase_date' => !empty($lastPur['date_of_invoice']) ? date('d-M-Y', strtotime($lastPur['date_of_invoice'])) : '', + ]; + } + + return $this->response->setJSON($result); + } + // ========================================================================= // 2. GOODS RECEIVED NOTE (GRN) // ========================================================================= diff --git a/app/Models/PharmacyPurchaseOrderItemModel.php b/app/Models/PharmacyPurchaseOrderItemModel.php index 9617b45..2755dbc 100644 --- a/app/Models/PharmacyPurchaseOrderItemModel.php +++ b/app/Models/PharmacyPurchaseOrderItemModel.php @@ -21,6 +21,9 @@ class PharmacyPurchaseOrderItemModel extends Model 'received_packs', 'remaining_packs', 'estimated_ptr', + 'discount_percent', + 'sch_disc_per', + 'scheme_info', 'gst_rate', 'estimated_amount', 'status', diff --git a/app/Views/medical/po_form.php b/app/Views/medical/po_form.php index 317e099..37be651 100644 --- a/app/Views/medical/po_form.php +++ b/app/Views/medical/po_form.php @@ -33,6 +33,9 @@
+ + +
@@ -87,7 +90,7 @@
-
+
@@ -95,18 +98,22 @@ + + + +
- +
- +
- +
+ + +
- + - - - - - - - + + + + + + + + $it): ?> + + - @@ -184,18 +218,23 @@ - - + + + + + + + - - + + - - + + @@ -235,20 +274,55 @@ function initPoDrugAutocomplete() { $("#quick_drug_search").autocomplete({ source: function(request, response) { - $.getJSON("", request, function(data) { + var suppId = $('#po_supplier_id').val() || 0; + $.getJSON("", { + term: request.term, + supplier_id: suppId + }, function(data) { response(data || []); }); }, minLength: 1, select: function(event, ui) { var item = ui.item || {}; - document.getElementById('selected_item_id').value = item.item_code || item.l_item_code || ''; + document.getElementById('selected_item_id').value = item.item_code || ''; document.getElementById('selected_item_name').value = item.value || ''; document.getElementById('selected_generic_name').value = item.generic_name || ''; document.getElementById('selected_category').value = item.formulation || 'Tablet'; - document.getElementById('quick_units_per_pack').value = item.l_packing || 10; - document.getElementById('quick_ptr').value = item.l_unit_rate ? (parseFloat(item.l_unit_rate) * (item.l_packing || 10)).toFixed(2) : '100.00'; + document.getElementById('quick_units_per_pack').value = item.packing || 10; + document.getElementById('quick_ptr').value = item.estimated_ptr || '0.00'; + document.getElementById('selected_gst_rate').value = item.gst_rate !== undefined ? item.gst_rate : 12; + document.getElementById('selected_discount_percent').value = item.discount_percent || 0; + document.getElementById('selected_sch_disc_per').value = item.sch_disc_per || 0; + document.getElementById('selected_scheme_info').value = item.scheme_info || ''; document.getElementById('btn-add-line').disabled = false; + + // Render Drug History / Scheme info pill + var pill = document.getElementById('selected_drug_info_pill'); + if (pill) { + var h = '
'; + h += '
'; + h += ' ' + escapeHtml(item.value) + ''; + if (item.has_last_purchase) { + h += 'Last Rate: ₹' + item.estimated_ptr + ''; + h += 'GST: ' + item.gst_rate + '%'; + if (item.scheme_summary) { + h += ' ' + escapeHtml(item.scheme_summary) + ''; + } + if (item.is_vendor_match) { + h += ' From selected vendor'; + } else if (item.last_supplier_name) { + h += '(Last: ' + escapeHtml(item.last_supplier_name) + (item.last_purchase_date ? ' ' + item.last_purchase_date : '') + ')'; + } + } else { + h += 'New Item • Master GST: ' + item.gst_rate + '%'; + } + h += '
'; + h += 'Auto-filled rate & GST • Click + Add'; + h += '
'; + pill.innerHTML = h; + pill.style.display = 'block'; + } } }); } @@ -271,7 +345,10 @@ function addItemToPoTable(prefill) { units_per_pack: document.getElementById('quick_units_per_pack').value || 10, order_packs: document.getElementById('quick_order_packs').value || 10, estimated_ptr: document.getElementById('quick_ptr').value || 0, - gst_rate: 12 + gst_rate: document.getElementById('selected_gst_rate').value || 12, + discount_percent: document.getElementById('selected_discount_percent').value || 0, + sch_disc_per: document.getElementById('selected_sch_disc_per').value || 0, + scheme_info: document.getElementById('selected_scheme_info').value || '' }; if (!item.item_id || !item.item_name) { @@ -296,6 +373,21 @@ function addItemToPoTable(prefill) { var tr = document.createElement('tr'); tr.setAttribute('data-item-id', item.item_id); + var itemGst = parseFloat(item.gst_rate !== undefined ? item.gst_rate : 12); + var discVal = parseFloat(item.discount_percent || 0); + var schVal = parseFloat(item.sch_disc_per || 0); + var schInfo = item.scheme_info || ''; + + // Standard GST options + var standardGsts = [0, 5, 12, 18, 28]; + var gstOptionsHtml = ''; + standardGsts.forEach(function(g) { + gstOptionsHtml += ``; + }); + if (!standardGsts.some(g => Math.abs(itemGst - g) < 0.1)) { + gstOptionsHtml += ``; + } + tr.innerHTML = ` + @@ -344,7 +446,19 @@ function resetQuickAddInputs() { document.getElementById('selected_item_id').value = ''; document.getElementById('selected_item_name').value = ''; document.getElementById('selected_generic_name').value = ''; + document.getElementById('selected_gst_rate').value = '12'; + document.getElementById('selected_discount_percent').value = '0'; + document.getElementById('selected_sch_disc_per').value = '0'; + document.getElementById('selected_scheme_info').value = ''; + document.getElementById('quick_ptr').value = '0.00'; document.getElementById('btn-add-line').disabled = true; + + var pill = document.getElementById('selected_drug_info_pill'); + if (pill) { + pill.innerHTML = ''; + pill.style.display = 'none'; + } + document.getElementById('quick_drug_search').focus(); } @@ -353,7 +467,7 @@ function removePoRow(btn) { var tbody = document.getElementById('po-items-body'); var rows = tbody.querySelectorAll('tr'); if (rows.length === 0) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; } else { rows.forEach(function(r, idx) { var numTd = r.querySelector('.row-num'); @@ -365,28 +479,48 @@ function removePoRow(btn) { function recalculatePoTotals() { var rows = document.querySelectorAll('#po-items-body tr:not(#no-items-row)'); - var subtotal = 0.0; + var totalGross = 0.0; + var totalDiscount = 0.0; + var totalTaxable = 0.0; var totalTax = 0.0; rows.forEach(function(r) { var packs = parseFloat(r.querySelector('.line-packs')?.value || 0); var rate = parseFloat(r.querySelector('.line-rate')?.value || 0); + var discPer = parseFloat(r.querySelector('.line-disc')?.value || 0); + var schPer = parseFloat(r.querySelector('.line-sch-per')?.value || 0); var gst = parseFloat(r.querySelector('.line-gst')?.value || 0); - var lineSub = packs * rate; - var lineTax = lineSub * (gst / 100); - var lineTotal = lineSub + lineTax; + var lineGross = packs * rate; + var schAmt = (schPer > 0) ? (lineGross * (schPer / 100)) : 0.0; + var afterSch = Math.max(0.0, lineGross - schAmt); + var tradeDiscAmt = (discPer > 0) ? (afterSch * (discPer / 100)) : 0.0; + var lineTaxable = Math.max(0.0, afterSch - tradeDiscAmt); + var lineTax = lineTaxable * (gst / 100); + var lineTotal = lineTaxable + lineTax; - subtotal += lineSub; + totalGross += lineGross; + totalDiscount += (schAmt + tradeDiscAmt); + totalTaxable += lineTaxable; totalTax += lineTax; var totalDisp = r.querySelector('.line-total-display'); if (totalDisp) totalDisp.innerText = '₹' + lineTotal.toFixed(2); }); - document.getElementById('po-subtotal-disp').innerText = '₹' + subtotal.toFixed(2); + document.getElementById('po-subtotal-disp').innerText = '₹' + totalTaxable.toFixed(2); document.getElementById('po-tax-disp').innerText = '₹' + totalTax.toFixed(2); - document.getElementById('po-grand-total-disp').innerText = '₹' + (subtotal + totalTax).toFixed(2); + document.getElementById('po-grand-total-disp').innerText = '₹' + (totalTaxable + totalTax).toFixed(2); + + var discRow = document.getElementById('po-disc-row'); + if (discRow) { + if (totalDiscount > 0) { + discRow.style.display = ''; + document.getElementById('po-discount-disp').innerText = '-₹' + totalDiscount.toFixed(2); + } else { + discRow.style.display = 'none'; + } + } } function fetchSupplierReorderItems() { @@ -406,8 +540,11 @@ function fetchSupplierReorderItems() { category: it.category || 'Tablet', units_per_pack: it.units_per_pack || 10, order_packs: 10, - estimated_ptr: it.estimated_ptr || 50, - gst_rate: it.gst_rate || 12 + estimated_ptr: it.estimated_ptr || 0, + gst_rate: it.gst_rate !== undefined ? it.gst_rate : 12, + discount_percent: it.discount_percent || 0, + sch_disc_per: it.sch_disc_per || 0, + scheme_info: it.scheme_info || '' }); added++; }); diff --git a/app/Views/medical/po_print_pdf.php b/app/Views/medical/po_print_pdf.php index 63137f9..c4c840a 100644 --- a/app/Views/medical/po_print_pdf.php +++ b/app/Views/medical/po_print_pdf.php @@ -102,19 +102,45 @@ if (!isset($branding) || !is_array($branding)) {
## Product Name Generic / SaltFormulationUnits/PackOrder PacksEst. Rate (₹)GST %Line Total (₹)FormulationUnits/PackOrder PacksEst. Rate (₹)Scheme / DiscGST %Line Total (₹)
@@ -150,20 +167,37 @@ - + - + - + - + +
+ Sch + +
+ + +
+ @@ -176,7 +210,7 @@
+ No items added yet. Search above to add items or auto-load low stock items.
Estimated Subtotal:₹0.00Estimated Subtotal (Taxable):₹0.00
Estimated GST Tax:₹0.00Estimated GST Tax:₹0.00
Grand Total Commitment:₹0.00Grand Total Commitment:₹0.00
${rowIdx + 1} @@ -312,20 +404,30 @@ function addItemToPoTable(prefill) { - + - + - + - + +
+ Sch + +
+ + +
+ ₹0.00
No items added yet.
No items added yet. Search above to add items or auto-load low stock items.
- + - - - - - + + + + + + - + $it): ?> + 0) ? round($lineGross * ($schPer / 100), 2) : 0.0; + $afterSch = max(0.0, $lineGross - $schAmt); + $discPer = (float)($it['discount_percent'] ?? 0); + $discAmt = ($discPer > 0) ? round($afterSch * ($discPer / 100), 2) : 0.0; + $calcGross += $lineGross; + $calcDiscount += ($schAmt + $discAmt); + + $discTags = []; + if ($discPer > 0) { + $discTags[] = $discPer . '% Disc'; + } + if ($schPer > 0) { + $discTags[] = $schPer . '% Sch'; + } + if (!empty($it['scheme_info'])) { + $discTags[] = esc($it['scheme_info']); + } + ?> @@ -122,6 +148,9 @@ if (!isset($branding) || !is_array($branding)) { + @@ -131,8 +160,18 @@ if (!isset($branding) || !is_array($branding)) {
## Item / Brand Name Salt CompositionPack SizeOrder QtyRate/PackGST %Total (₹)Pack SizeOrder QtyRate/PackScheme / DiscGST %Total (₹)
s ₹ + ', $discTags) : '-' ?> + % ₹
+ 0.009): ?> - + + + + + + + + + +
Estimated Subtotal:Gross Amount:₹
Total Schemes / Discounts:-₹
Taxable Subtotal: ₹