diff --git a/app/Controllers/MedicalProcurement.php b/app/Controllers/MedicalProcurement.php index 72f89e7..a0e39d3 100644 --- a/app/Controllers/MedicalProcurement.php +++ b/app/Controllers/MedicalProcurement.php @@ -569,6 +569,8 @@ class MedicalProcurement extends BaseController $grnDate = trim((string)$this->request->getPost('grn_date') ?: date('Y-m-d')); $remarks = trim((string)$this->request->getPost('remarks') ?: ''); $postToInventory = (int)($this->request->getPost('post_to_inventory') ?: 0); + $gstType = trim((string)$this->request->getPost('gst_type') ?: 'intra_state'); + $roundOff = (float)($this->request->getPost('round_off') ?: 0.0); $itemsRaw = $this->request->getPost('items'); $items = is_array($itemsRaw) ? $itemsRaw : []; @@ -586,9 +588,13 @@ class MedicalProcurement extends BaseController $grnNumber = $this->grnModel->generateGrnNumber(); $userId = $this->getCurrentUserId(); + $totalGross = 0.0; + $totalDisc = 0.0; $totalTaxable = 0.0; + $totalCgst = 0.0; + $totalSgst = 0.0; + $totalIgst = 0.0; $totalTax = 0.0; - $netTotal = 0.0; $validItems = []; foreach ($items as $row) { @@ -615,9 +621,12 @@ class MedicalProcurement extends BaseController } $discPercent = (float)($row['discount_percent'] ?? 0); - $cgstPer = (float)($row['cgst_percent'] ?? 6.0); - $sgstPer = (float)($row['sgst_percent'] ?? 6.0); - $igstPer = (float)($row['igst_percent'] ?? 0.0); + $schDiscPer = (float)($row['sch_disc_per'] ?? 0); + $schDiscAmt = (float)($row['sch_disc_amt'] ?? 0); + + // Determine GST total rate: check gst_rate, then fallback to cgst_percent * 2 + $gstRate = isset($row['gst_rate']) ? (float)$row['gst_rate'] : ((float)($row['cgst_percent'] ?? 2.5) * 2); + $hsnCode = trim((string)($row['hsn_code'] ?? '')); $rackNo = trim((string)($row['rack_no'] ?? '')); $shelfNo = trim((string)($row['shelf_no'] ?? '')); @@ -631,20 +640,46 @@ class MedicalProcurement extends BaseController continue; } - // Calculations + // Calculations strictly aligned with Indian Pharma Invoicing: $grossAmount = round($receivedPacks * $unitPurchaseRate, 2); - $discountAmt = round($grossAmount * ($discPercent / 100), 2); - $lineTaxable = $grossAmount - $discountAmt; - $cgstAmt = round($lineTaxable * ($cgstPer / 100), 2); - $sgstAmt = round($lineTaxable * ($sgstPer / 100), 2); - $igstAmt = round($lineTaxable * ($igstPer / 100), 2); - $lineTax = $cgstAmt + $sgstAmt + $igstAmt; - $lineTotal = $lineTaxable + $lineTax; + // 1. Scheme discount (percent or amount) + $schPerAmt = ($schDiscPer > 0) ? round($grossAmount * ($schDiscPer / 100), 2) : 0.0; + $lineSchDisc = $schPerAmt + $schDiscAmt; + $afterSch = max(0.0, $grossAmount - $lineSchDisc); + // 2. Trade discount + $tradeDiscAmt = ($discPercent > 0) ? round($afterSch * ($discPercent / 100), 2) : 0.0; + $lineTaxable = max(0.0, $afterSch - $tradeDiscAmt); + + // 3. GST Calculation (Equal CGST/SGST split or 100% IGST) + if ($gstType === 'inter_state') { + $cgstPer = 0.0; + $sgstPer = 0.0; + $igstPer = $gstRate; + $cgstAmt = 0.0; + $sgstAmt = 0.0; + $igstAmt = round($lineTaxable * ($igstPer / 100), 2); + $lineTax = $igstAmt; + } else { + $cgstPer = round($gstRate / 2, 2); + $sgstPer = round($gstRate / 2, 2); + $igstPer = 0.0; + $cgstAmt = round($lineTaxable * ($cgstPer / 100), 2); + $sgstAmt = round($lineTaxable * ($sgstPer / 100), 2); + $igstAmt = 0.0; + $lineTax = $cgstAmt + $sgstAmt; + } + + $lineTotal = round($lineTaxable + $lineTax, 2); + + $totalGross += $grossAmount; + $totalDisc += ($lineSchDisc + $tradeDiscAmt); $totalTaxable += $lineTaxable; + $totalCgst += $cgstAmt; + $totalSgst += $sgstAmt; + $totalIgst += $igstAmt; $totalTax += $lineTax; - $netTotal += $lineTotal; $validItems[] = [ 'po_item_id' => $poItemId > 0 ? $poItemId : null, @@ -659,9 +694,12 @@ class MedicalProcurement extends BaseController 'rejected_packs' => $rejectedPacks, 'rejection_reason' => $rejectionReason ?: null, 'unit_purchase_rate' => $unitPurchaseRate, + 'gross_amount' => $grossAmount, 'mrp' => $mrp, 'selling_unit_rate' => $sellingUnitRate, 'discount_percent' => $discPercent, + 'sch_disc_per' => $schDiscPer, + 'sch_disc_amt' => $schDiscAmt, 'taxable_amount' => $lineTaxable, 'cgst_percent' => $cgstPer, 'cgst_amount' => $cgstAmt, @@ -681,6 +719,8 @@ class MedicalProcurement extends BaseController return $this->response->setJSON(['status' => 0, 'message' => 'No valid item rows with batch and expiry were found.']); } + $netTotal = round($totalTaxable + $totalTax + $roundOff, 2); + $this->db->transStart(); $grnHeader = [ @@ -694,6 +734,11 @@ class MedicalProcurement extends BaseController 'grn_date' => $grnDate, 'total_received_amount' => $totalTaxable, 'total_tax_amount' => $totalTax, + 'cgst_amount' => $totalCgst, + 'sgst_amount' => $totalSgst, + 'igst_amount' => $totalIgst, + 'round_off' => $roundOff, + 'gst_type' => $gstType, 'net_amount' => $netTotal, 'status' => 'verified', 'received_by' => $userId, @@ -701,7 +746,6 @@ class MedicalProcurement extends BaseController 'verified_at' => date('Y-m-d H:i:s'), 'remarks' => $remarks, 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), ]; $this->grnModel->insert($grnHeader); @@ -812,8 +856,8 @@ class MedicalProcurement extends BaseController 'date_of_invoice' => $grn['invoice_date'], 'Invoice_no' => $grn['supplier_invoice_no'], 'Taxable_Amt' => $grn['total_received_amount'], - 'CGST_Amt' => round($grn['total_tax_amount'] / 2, 2), - 'SGST_Amt' => round($grn['total_tax_amount'] / 2, 2), + 'CGST_Amt' => (float)($grn['cgst_amount'] ?? round($grn['total_tax_amount'] / 2, 2)), + 'SGST_Amt' => (float)($grn['sgst_amount'] ?? round($grn['total_tax_amount'] / 2, 2)), 'T_Net_Amount' => $grn['net_amount'], 'inv_status' => 1, 'inv_status_update_by' => $userId, @@ -851,14 +895,16 @@ class MedicalProcurement extends BaseController 'qty' => $receivedPacks, 'qty_free' => $freePacks, 'tqty' => $receivedPacks + $freePacks, - 'amount' => round($receivedPacks * $purchasePricePerPack, 2), - 'discount' => $gItem['discount_percent'], - 'taxable_amount' => $gItem['taxable_amount'], - 'CGST_per' => $gItem['cgst_percent'], - 'CGST' => $gItem['cgst_amount'], - 'SGST_per' => $gItem['sgst_percent'], - 'SGST' => $gItem['sgst_amount'], - 'net_amount' => $gItem['total_amount'], + 'amount' => (float)($gItem['gross_amount'] ?: round($receivedPacks * $purchasePricePerPack, 2)), + 'discount' => (float)($gItem['discount_percent'] ?? 0), + 'sch_disc_amt' => (float)($gItem['sch_disc_amt'] ?? 0), + 'sch_disc_per' => (float)($gItem['sch_disc_per'] ?? 0), + 'taxable_amount' => (float)$gItem['taxable_amount'], + 'CGST_per' => (float)$gItem['cgst_percent'], + 'CGST' => (float)$gItem['cgst_amount'], + 'SGST_per' => (float)$gItem['sgst_percent'], + 'SGST' => (float)$gItem['sgst_amount'], + 'net_amount' => (float)$gItem['total_amount'], 'HSNCODE' => $gItem['hsn_code'] ?: '', 'selling_price' => $gItem['selling_unit_rate'] * $packing, 'total_unit' => $totalUnits, diff --git a/app/Models/PharmacyGrnItemModel.php b/app/Models/PharmacyGrnItemModel.php index 52a4d7c..3b0f573 100644 --- a/app/Models/PharmacyGrnItemModel.php +++ b/app/Models/PharmacyGrnItemModel.php @@ -24,9 +24,12 @@ class PharmacyGrnItemModel extends Model 'rejected_packs', 'rejection_reason', 'unit_purchase_rate', + 'gross_amount', 'mrp', 'selling_unit_rate', 'discount_percent', + 'sch_disc_per', + 'sch_disc_amt', 'taxable_amount', 'cgst_percent', 'cgst_amount', diff --git a/app/Models/PharmacyGrnModel.php b/app/Models/PharmacyGrnModel.php index d53dfe2..a03c7ca 100644 --- a/app/Models/PharmacyGrnModel.php +++ b/app/Models/PharmacyGrnModel.php @@ -21,6 +21,11 @@ class PharmacyGrnModel extends Model 'grn_date', 'total_received_amount', 'total_tax_amount', + 'cgst_amount', + 'sgst_amount', + 'igst_amount', + 'round_off', + 'gst_type', 'net_amount', 'status', 'purchase_id', diff --git a/app/Views/medical/grn_detail.php b/app/Views/medical/grn_detail.php index 2fb144c..cea61b5 100644 --- a/app/Views/medical/grn_detail.php +++ b/app/Views/medical/grn_detail.php @@ -62,45 +62,73 @@
- +
- - - - - - - - - - - - - + + + + + + + + + + + + + + + + + $it): ?> + 0 ? ($gross * $schPer / 100) : 0) + $schAmt; + $tradeDisc = (float)$it['discount_percent']; + $afterSch = max(0, $gross - $schTotal); + $tradeDiscAmt = ($tradeDisc > 0) ? ($afterSch * $tradeDisc / 100) : 0; + $rowDisc = $schTotal + $tradeDiscAmt; + + $totalGross += $gross; + $totalDiscount += $rowDisc; + + $isInter = (!empty($grn['gst_type']) && $grn['gst_type'] === 'inter_state') || ((float)$it['igst_percent'] > 0); + $gstLabel = $isInter + ? number_format((float)$it['igst_percent'], 1) . '% IGST' + : number_format((float)$it['cgst_percent'] + (float)$it['sgst_percent'], 0) . '% (' . number_format((float)$it['cgst_percent'], 1) . '+' . number_format((float)$it['sgst_percent'], 1) . '%)'; + ?> - + - - - + + + + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/app/Views/medical/grn_form.php b/app/Views/medical/grn_form.php index 9104763..97be93c 100644 --- a/app/Views/medical/grn_form.php +++ b/app/Views/medical/grn_form.php @@ -10,7 +10,7 @@ Create Goods Received Note (GRN) - Inward Stock Physical Inspection & Batch Entry + Inward Stock Physical Inspection & Vendor Invoice Reconciliation
@@ -21,14 +21,14 @@
- Inward Delivery Metadata + Inward Delivery & Invoice Metadata
-
+
- +
- +
- - + +
- +
- +
-
- - -
-
- - -
- -
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Physical Inspection & Batch Intake Table - Enter Batch, Expiry, Received Packs & Pricing + Match Vendor Invoice Columns (Gross, Discounts, Taxable, GST & Net Total)
-
-
- - +
+
+ +
- +
-
- - +
+ +
-
+
+ + +
+
+ + +
+
+ + +
+
-
#Product NameBatch NoExpiry DatePackRec. PacksFreeRejectedRate/PackMRPGST %Line TotalLive Stock ID#Medicine NameBatch NoExpiryPackRec. QtyFreeRate/PackMRPGross AmtDisc %Sch DiscTaxable AmtGSTLine TotalLive Stock ID
+ + 0): ?> + Damaged: () + + s - - - () - - ₹ ₹%₹₹%₹₹₹ Item # @@ -114,18 +142,53 @@
Taxable Amount:₹Sub Total (Gross Amount):₹
Total GST:₹Total Discount (Trade & Scheme):₹
Taxable Amount:₹
IGST Amount:₹
CGST Amount:₹
SGST Amount:₹
Total GST:₹
Round Off: 0 ? '+' : '') . number_format((float)$grn['round_off'], 2) ?>
Net Inward Bill Total:₹Net Inward Bill Total (Grand Total):₹
+
- - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + $it): ?> + - + - - + + + + + - + - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -220,7 +298,7 @@ - Calls stored procedure p_stock_update_purchase_id to make batches immediately sellable. + Creates verified purchase invoice records and updates stock ledgers.
@@ -264,6 +342,8 @@ function initGrnDrugAutocomplete() { document.getElementById('grn_sel_item_code').value = item.item_code || item.l_item_code || ''; document.getElementById('grn_sel_item_name').value = item.value || ''; document.getElementById('grn_sel_packing').value = item.l_packing || 10; + if (item.mrp) document.getElementById('grn_sel_mrp').value = parseFloat(item.mrp).toFixed(2); + if (item.purchase_price) document.getElementById('grn_sel_rate').value = parseFloat(item.purchase_price).toFixed(2); document.getElementById('btn-add-grn-row').disabled = false; } }); @@ -281,7 +361,10 @@ function addCustomItemToGrn() { var batch = document.getElementById('grn_sel_batch').value.trim(); var expiry = document.getElementById('grn_sel_expiry').value; var packs = parseFloat(document.getElementById('grn_sel_rec_packs').value || 1); + var freePacks = parseFloat(document.getElementById('grn_sel_free_packs').value || 0); var packing = parseInt(document.getElementById('grn_sel_packing').value || 10); + var rate = parseFloat(document.getElementById('grn_sel_rate').value || 100.00); + var mrp = parseFloat(document.getElementById('grn_sel_mrp').value || 130.00); if (!code || !name) { alert('Please search and select a medicine.'); @@ -305,13 +388,23 @@ function addCustomItemToGrn() { var rowIdx = tbody.querySelectorAll('tr').length; var tr = document.createElement('tr'); + var gross = packs * rate; + tr.innerHTML = ` -
+ - - + + + + + - + `; @@ -368,7 +468,7 @@ function removeGrnRow(btn) { var tbody = document.getElementById('grn-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'); @@ -380,28 +480,114 @@ function removeGrnRow(btn) { function recalculateGrnTotals() { var rows = document.querySelectorAll('#grn-items-body tr:not(#grn-no-items)'); - var taxable = 0.0; + var isInterState = (document.getElementById('grn_gst_type')?.value === 'inter_state'); + + var totalGross = 0.0; + var totalDisc = 0.0; + var totalTaxable = 0.0; + var totalCgst = 0.0; + var totalSgst = 0.0; + var totalIgst = 0.0; var totalTax = 0.0; rows.forEach(function(r) { var recPacks = parseFloat(r.querySelector('.line-rec')?.value || 0); var rate = parseFloat(r.querySelector('.line-rate')?.value || 0); - var gstVal = parseFloat(r.querySelector('.line-gst')?.value || 0) * 2; // CGST + SGST + var gross = recPacks * rate; - var lineSub = recPacks * rate; - var lineTax = lineSub * (gstVal / 100); - var lineTotal = lineSub + lineTax; + var discPer = parseFloat(r.querySelector('.line-disc')?.value || 0); + var schPer = parseFloat(r.querySelector('.line-sch-per')?.value || 0); + var schAmt = parseFloat(r.querySelector('.line-sch-amt')?.value || 0); - taxable += lineSub; - totalTax += lineTax; + // Scheme discount calculation + var schPerAmt = (schPer > 0) ? (gross * (schPer / 100)) : 0.0; + var lineSch = schPerAmt + schAmt; + var afterSch = Math.max(0.0, gross - lineSch); + + // Trade discount calculation + var tradeDiscAmt = (discPer > 0) ? (afterSch * (discPer / 100)) : 0.0; + var taxable = Math.max(0.0, afterSch - tradeDiscAmt); + + // GST calculation + var gstRate = parseFloat(r.querySelector('.line-gst')?.value || 0); + var cgstPer = 0.0; + var sgstPer = 0.0; + var igstPer = 0.0; + var cgstAmt = 0.0; + var sgstAmt = 0.0; + var igstAmt = 0.0; + + if (isInterState) { + igstPer = gstRate; + igstAmt = taxable * (igstPer / 100); + } else { + cgstPer = gstRate / 2; + sgstPer = gstRate / 2; + cgstAmt = taxable * (cgstPer / 100); + sgstAmt = taxable * (sgstPer / 100); + } + + var lineTax = cgstAmt + sgstAmt + igstAmt; + var lineTotal = taxable + lineTax; + + // Update displays + var grossDisp = r.querySelector('.line-gross-disp'); + if (grossDisp) grossDisp.innerText = '₹' + gross.toFixed(2); + + var taxableDisp = r.querySelector('.line-taxable-disp'); + if (taxableDisp) taxableDisp.innerText = '₹' + taxable.toFixed(2); var totalDisp = r.querySelector('.line-total-disp'); if (totalDisp) totalDisp.innerText = '₹' + lineTotal.toFixed(2); + + // Update hidden inputs + var cgstInp = r.querySelector('.line-cgst'); + if (cgstInp) cgstInp.value = cgstPer; + + var sgstInp = r.querySelector('.line-sgst'); + if (sgstInp) sgstInp.value = sgstPer; + + var igstInp = r.querySelector('.line-igst'); + if (igstInp) igstInp.value = igstPer; + + var grossInp = r.querySelector('.line-gross-hid'); + if (grossInp) grossInp.value = gross.toFixed(2); + + var taxableInp = r.querySelector('.line-taxable-hid'); + if (taxableInp) taxableInp.value = taxable.toFixed(2); + + // Accumulate + totalGross += gross; + totalDisc += (lineSch + tradeDiscAmt); + totalTaxable += taxable; + totalCgst += cgstAmt; + totalSgst += sgstAmt; + totalIgst += igstAmt; + totalTax += lineTax; }); - document.getElementById('grn-taxable-disp').innerText = '₹' + taxable.toFixed(2); + document.getElementById('grn-gross-disp').innerText = '₹' + totalGross.toFixed(2); + document.getElementById('grn-discount-disp').innerText = '₹' + totalDisc.toFixed(2); + document.getElementById('grn-taxable-disp').innerText = '₹' + totalTaxable.toFixed(2); + + if (isInterState) { + document.getElementById('row-cgst').style.display = 'none'; + document.getElementById('row-sgst').style.display = 'none'; + document.getElementById('row-igst').style.display = ''; + document.getElementById('grn-igst-disp').innerText = '₹' + totalIgst.toFixed(2); + } else { + document.getElementById('row-cgst').style.display = ''; + document.getElementById('row-sgst').style.display = ''; + document.getElementById('row-igst').style.display = 'none'; + document.getElementById('grn-cgst-disp').innerText = '₹' + totalCgst.toFixed(2); + document.getElementById('grn-sgst-disp').innerText = '₹' + totalSgst.toFixed(2); + } + document.getElementById('grn-tax-disp').innerText = '₹' + totalTax.toFixed(2); - document.getElementById('grn-net-disp').innerText = '₹' + (taxable + totalTax).toFixed(2); + + var roundOff = parseFloat(document.getElementById('grn_round_off')?.value || 0); + var netPayable = totalTaxable + totalTax + roundOff; + document.getElementById('grn-net-disp').innerText = '₹' + netPayable.toFixed(2); } function submitGrnForm() { diff --git a/app/Views/medical/grn_print_pdf.php b/app/Views/medical/grn_print_pdf.php index b3dcaac..a9261db 100644 --- a/app/Views/medical/grn_print_pdf.php +++ b/app/Views/medical/grn_print_pdf.php @@ -62,37 +62,61 @@
#Medicine NameBatch No *Expiry *PackOrderedRec. Packs *FreeDamagedRate/PackMRPGST %Line Total
#Medicine NameBatch No *Expiry *PackRec. Qty *FreeRate/PackMRPGross AmtDisc %Sch %Sch AmtTaxable AmtGST %Line Total
- + + + + + + + + + @@ -147,64 +189,100 @@ - - - - + - + - - - + - + - + + ₹ + - + + + ₹ + - ₹0.00₹ - +
- No items loaded. Select a PO above or use the Search box to add items. + + No items loaded. Select an approved PO above or use the Search box to add medicines.
Taxable Amount:₹0.00Sub Total (Gross Amount):₹0.00
GST Tax Amount:₹0.00Total Discount (Trade & Scheme):₹0.00
Taxable Amount:₹0.00
CGST Amount:₹0.00
SGST Amount:₹0.00
Total GST:₹0.00
Round Off (+ / -): + +
Net Inward Bill Total:₹0.00Net Inward Bill Total (Grand Total):₹0.00
${rowIdx + 1}${rowIdx + 1} - ${escapeHtml(name)} + ${escapeHtml(name)} + + + + + + + + @@ -320,36 +413,43 @@ function addCustomItemToGrn() { - - 0 - + - + - + - + - + + ₹${gross.toFixed(2)} + - + + + ₹${gross.toFixed(2)} + - ₹0.00₹0.00 - +
No items loaded.
No items loaded.
- + - - - - - - - - - - + + + + + + + + + + + + + $it): ?> + 0 ? ($gross * $schPer / 100) : 0) + $schAmt; + $tradeDisc = (float)$it['discount_percent']; + $afterSch = max(0, $gross - $schTotal); + $tradeDiscAmt = ($tradeDisc > 0) ? ($afterSch * $tradeDisc / 100) : 0; + $rowDisc = $schTotal + $tradeDiscAmt; + + $pdfGross += $gross; + $pdfDisc += $rowDisc; + + $isInter = (!empty($grn['gst_type']) && $grn['gst_type'] === 'inter_state') || ((float)($it['igst_percent'] ?? 0) > 0); + $gstStr = $isInter + ? number_format((float)($it['igst_percent'] ?? 0), 0) . '% IGST' + : number_format((float)$it['cgst_percent'] + (float)$it['sgst_percent'], 0) . '%'; + ?> - - + + + + @@ -101,16 +125,45 @@
## Item NameBatch NoExpiryPackOrderedReceivedFreeDamagedRate/PackMRPLine TotalBatch NoExpiryPackRec. QtyFreeRateMRPGrossDisc%TaxableGSTLine Total
s "> - - ₹ ₹₹%₹ ₹
+ + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + - +
Subtotal (Gross Amount):₹
Total Discount:₹
Taxable Amount: ₹
IGST Amount:₹
CGST Amount:₹
SGST Amount:₹
GST Tax:Total GST: ₹
Round Off: 0 ? '+' : '') . number_format((float)$grn['round_off'], 2) ?>
Net Payable Total:Net Inward Bill Total: ₹
diff --git a/app/Views/medical/purchase_invoice_edit.php b/app/Views/medical/purchase_invoice_edit.php index ddd2fb4..589e67d 100644 --- a/app/Views/medical/purchase_invoice_edit.php +++ b/app/Views/medical/purchase_invoice_edit.php @@ -102,25 +102,20 @@
- trim((string)($r->gst_per ?? '0')), $gstRates) : $stdRates; - foreach ($stdRates as $sr) { - if (!in_array($sr, $pRates, true)) $pRates[] = $sr; - } + $stdRates = ['0', '2.5', '6', '9', '14']; ?> - + @@ -128,7 +123,7 @@
@@ -179,6 +174,12 @@