feat(medical-store): add edit purchase invoice option with stock adjustment and ledger reconciliation
This commit is contained in:
parent
ca6926015a
commit
48bb726b6d
|
|
@ -1684,7 +1684,10 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
|||
$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->get('medical-store/purchases', 'MedicalStoreApi::getPurchases');
|
||||
$routes->get('medical-store/purchase/(:num)', 'MedicalStoreApi::getPurchaseDetail/$1');
|
||||
$routes->post('medical-store/purchase/save', 'MedicalStoreApi::savePurchase');
|
||||
$routes->post('medical-store/purchase/update', 'MedicalStoreApi::updatePurchase');
|
||||
$routes->post('medical-store/purchase/payment', 'MedicalStoreApi::saveSupplierPayment');
|
||||
|
||||
// Short Items / Deficiency & Reorder Book
|
||||
|
|
|
|||
|
|
@ -2311,6 +2311,394 @@ class MedicalStoreApi extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function getPurchases()
|
||||
{
|
||||
$storeId = (int)($this->request->getGet('store_id') ?: 0);
|
||||
$supplierId = (int)($this->request->getGet('supplier_id') ?: 0);
|
||||
$search = trim($this->request->getGet('search') ?: '');
|
||||
|
||||
$builder = $this->db->table('mst_purchases p')
|
||||
->select('p.*, s.supplier_name, s.phone, s.contact_person, s.gstin, COUNT(pi.purchase_item_id) as items_count')
|
||||
->join('mst_suppliers s', 's.supplier_id = p.supplier_id', 'left')
|
||||
->join('mst_purchase_items pi', 'pi.purchase_id = p.purchase_id', 'left')
|
||||
->groupBy('p.purchase_id')
|
||||
->orderBy('p.invoice_date', 'DESC')
|
||||
->orderBy('p.purchase_id', 'DESC')
|
||||
->limit(100);
|
||||
|
||||
if ($storeId > 0) {
|
||||
$builder->where('p.store_id', $storeId);
|
||||
}
|
||||
if ($supplierId > 0) {
|
||||
$builder->where('p.supplier_id', $supplierId);
|
||||
}
|
||||
if (!empty($search)) {
|
||||
$builder->groupStart()
|
||||
->like('p.supplier_invoice_no', $search)
|
||||
->orLike('s.supplier_name', $search)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$purchases = $builder->get()->getResultArray();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'purchases' => $purchases
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPurchaseDetail(int $purchaseId)
|
||||
{
|
||||
$purchase = $this->db->table('mst_purchases p')
|
||||
->select('p.*, 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 = p.supplier_id', 'left')
|
||||
->where('p.purchase_id', $purchaseId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$purchase) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Purchase invoice not found.']);
|
||||
}
|
||||
|
||||
$items = $this->db->table('mst_purchase_items pi')
|
||||
->select('pi.*, i.item_name, i.generic_name, i.category')
|
||||
->join('mst_items i', 'i.item_id = pi.item_id', 'left')
|
||||
->where('pi.purchase_id', $purchaseId)
|
||||
->orderBy('pi.purchase_item_id', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'purchase' => $purchase,
|
||||
'items' => $items
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatePurchase()
|
||||
{
|
||||
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
$purchaseId = (int)($json['purchase_id'] ?? 0);
|
||||
$storeId = (int)($json['store_id'] ?? 1);
|
||||
$supplierId = (int)($json['supplier_id'] ?? 0);
|
||||
$items = $json['items'] ?? [];
|
||||
|
||||
if ($purchaseId <= 0) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Valid Purchase ID is required.']);
|
||||
}
|
||||
if ($supplierId <= 0 || empty($items)) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Supplier and items are required.']);
|
||||
}
|
||||
|
||||
$existingPurchase = $this->db->table('mst_purchases')->where('purchase_id', $purchaseId)->get()->getRowArray();
|
||||
if (!$existingPurchase) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Purchase invoice not found.']);
|
||||
}
|
||||
|
||||
$paidAmount = (float)($existingPurchase['paid_amount'] ?? 0);
|
||||
if ($paidAmount > 0 && $supplierId !== (int)$existingPurchase['supplier_id']) {
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => "Cannot change the supplier on an invoice that already has payments recorded against it. Please reverse the payment first."
|
||||
]);
|
||||
}
|
||||
|
||||
// Fetch current purchase items to compute stock rollback
|
||||
$oldItems = $this->db->table('mst_purchase_items')->where('purchase_id', $purchaseId)->get()->getResultArray();
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
// Step 1: Revert previous stock quantities added by old purchase items
|
||||
foreach ($oldItems as $oldIt) {
|
||||
$oldBatch = $this->db->table('mst_batches')
|
||||
->where('store_id', $existingPurchase['store_id'])
|
||||
->where('item_id', $oldIt['item_id'])
|
||||
->where('batch_no', $oldIt['batch_no'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($oldBatch) {
|
||||
$batchId = (int)$oldBatch['batch_id'];
|
||||
$stockRow = $this->db->table('mst_stock')
|
||||
->where('store_id', $existingPurchase['store_id'])
|
||||
->where('item_id', $oldIt['item_id'])
|
||||
->where('batch_id', $batchId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($stockRow) {
|
||||
$newQty = max(0, (int)$stockRow['current_qty'] - (int)$oldIt['total_units']);
|
||||
$this->db->table('mst_stock')
|
||||
->where('stock_id', $stockRow['stock_id'])
|
||||
->update(['current_qty' => $newQty]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old purchase items
|
||||
$this->db->table('mst_purchase_items')->where('purchase_id', $purchaseId)->delete();
|
||||
|
||||
// Step 2: Process new items, create/update items & batches, and add new stock
|
||||
$taxableTotal = 0;
|
||||
$cgstTotal = 0;
|
||||
$sgstTotal = 0;
|
||||
$igstTotal = 0;
|
||||
$discountTotal = (float)($json['discount_amount'] ?? 0);
|
||||
$purchaseItems = [];
|
||||
|
||||
foreach ($items as $it) {
|
||||
$itemId = (int)($it['item_id'] ?? 0);
|
||||
$itemName = trim($it['item_name'] ?? '');
|
||||
|
||||
if ($itemId <= 0 && $itemName !== '') {
|
||||
$mst = $this->db->table('mst_items')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($mst) {
|
||||
$itemId = (int)$mst['item_id'];
|
||||
} else {
|
||||
$uPack = (int)($it['units_per_pack'] ?? 10);
|
||||
$gRate = (float)($it['gst_rate'] ?? 12.00);
|
||||
$hCode = trim($it['hsn_code'] ?? '3004');
|
||||
|
||||
$pmId = 0;
|
||||
if ($this->db->tableExists('med_product_master')) {
|
||||
$pmRow = $this->db->table('med_product_master')
|
||||
->where('LOWER(TRIM(item_name))', strtolower($itemName))
|
||||
->get()
|
||||
->getRowArray();
|
||||
if ($pmRow) {
|
||||
$pmId = (int)$pmRow['id'];
|
||||
} else {
|
||||
$this->db->table('med_product_master')->insert([
|
||||
'item_name' => $itemName,
|
||||
'formulation' => 'Tablet',
|
||||
'genericname' => '',
|
||||
'packing' => (string)$uPack,
|
||||
'HSNCODE' => $hCode,
|
||||
'CGST_per' => round($gRate / 2, 2),
|
||||
'SGST_per' => round($gRate / 2, 2),
|
||||
'is_continue' => 1,
|
||||
'insert_by' => 'Purchase Update Auto'
|
||||
]);
|
||||
$pmId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->table('mst_items')->insert([
|
||||
'product_master_id' => $pmId > 0 ? $pmId : null,
|
||||
'item_name' => $itemName,
|
||||
'category' => 'Tablet',
|
||||
'hsn_code' => $hCode,
|
||||
'gst_rate' => $gRate,
|
||||
'units_per_pack' => $uPack,
|
||||
'unit_pack' => $uPack . ' Units',
|
||||
'drug_schedule' => 'OTC',
|
||||
'is_active' => 1,
|
||||
'min_reorder_level' => 10
|
||||
]);
|
||||
$itemId = (int)$this->db->insertID();
|
||||
}
|
||||
}
|
||||
|
||||
$batchNo = trim($it['batch_no']);
|
||||
$expiryDate = trim($it['expiry_date']);
|
||||
$qtyPacks = (int)$it['qty_packs'];
|
||||
$freePacks = (int)($it['free_qty_packs'] ?? 0);
|
||||
$unitsPerPack = (int)($it['units_per_pack'] ?? 10);
|
||||
$totalUnits = ($qtyPacks + $freePacks) * $unitsPerPack;
|
||||
|
||||
$mrp = (float)$it['mrp'];
|
||||
$ptr = (float)$it['ptr'];
|
||||
$discPct = (float)($it['discount_pct'] ?? 0);
|
||||
$gstRate = (float)($it['gst_rate'] ?? 12.00);
|
||||
|
||||
$lineNetPtr = ($qtyPacks * $ptr) * (1 - ($discPct / 100));
|
||||
$taxAmount = round($lineNetPtr * ($gstRate / 100), 2);
|
||||
|
||||
$cgst = round($taxAmount / 2, 2);
|
||||
$sgst = round($taxAmount - $cgst, 2);
|
||||
$igst = 0;
|
||||
$lineTotal = round($lineNetPtr + $taxAmount, 2);
|
||||
$netUnitLanding = $totalUnits > 0 ? round($lineTotal / $totalUnits, 2) : 0;
|
||||
|
||||
$taxableTotal += $lineNetPtr;
|
||||
$cgstTotal += $cgst;
|
||||
$sgstTotal += $sgst;
|
||||
|
||||
$purchaseItems[] = [
|
||||
'purchase_id' => $purchaseId,
|
||||
'item_id' => $itemId,
|
||||
'batch_no' => $batchNo,
|
||||
'expiry_date' => $expiryDate,
|
||||
'qty_packs' => $qtyPacks,
|
||||
'free_qty_packs' => $freePacks,
|
||||
'units_per_pack' => $unitsPerPack,
|
||||
'total_units' => $totalUnits,
|
||||
'mrp' => $mrp,
|
||||
'ptr' => $ptr,
|
||||
'discount_pct' => $discPct,
|
||||
'hsn_code' => $it['hsn_code'] ?? '3004',
|
||||
'gst_rate' => $gstRate,
|
||||
'taxable_value' => $lineNetPtr,
|
||||
'cgst_amount' => $cgst,
|
||||
'sgst_amount' => $sgst,
|
||||
'igst_amount' => $igst,
|
||||
'total_amount' => $lineTotal,
|
||||
'net_unit_landing_cost' => $netUnitLanding
|
||||
];
|
||||
|
||||
// Update/create batch in mst_batches
|
||||
$batchRow = $this->db->table('mst_batches')
|
||||
->where('store_id', $storeId)
|
||||
->where('item_id', $itemId)
|
||||
->where('batch_no', $batchNo)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($batchRow) {
|
||||
$batchId = (int)$batchRow['batch_id'];
|
||||
$this->db->table('mst_batches')->where('batch_id', $batchId)->update([
|
||||
'expiry_date' => $expiryDate,
|
||||
'mrp' => $mrp,
|
||||
'ptr' => $ptr,
|
||||
'purchase_rate_net' => $netUnitLanding,
|
||||
'gst_rate' => $gstRate
|
||||
]);
|
||||
} else {
|
||||
$this->db->table('mst_batches')->insert([
|
||||
'store_id' => $storeId,
|
||||
'item_id' => $itemId,
|
||||
'batch_no' => $batchNo,
|
||||
'expiry_date' => $expiryDate,
|
||||
'mrp' => $mrp,
|
||||
'ptr' => $ptr,
|
||||
'purchase_rate_net' => $netUnitLanding,
|
||||
'gst_rate' => $gstRate
|
||||
]);
|
||||
$batchId = $this->db->insertID();
|
||||
}
|
||||
|
||||
// Increase stock in mst_stock
|
||||
$stockRow = $this->db->table('mst_stock')
|
||||
->where('store_id', $storeId)
|
||||
->where('item_id', $itemId)
|
||||
->where('batch_id', $batchId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($stockRow) {
|
||||
$this->db->table('mst_stock')
|
||||
->where('stock_id', $stockRow['stock_id'])
|
||||
->update(['current_qty' => $stockRow['current_qty'] + $totalUnits]);
|
||||
} else {
|
||||
$this->db->table('mst_stock')->insert([
|
||||
'store_id' => $storeId,
|
||||
'item_id' => $itemId,
|
||||
'batch_id' => $batchId,
|
||||
'current_qty' => $totalUnits
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$grandTotal = round($taxableTotal + $cgstTotal + $sgstTotal + $igstTotal - $discountTotal);
|
||||
|
||||
// Validation against paid_amount
|
||||
if ($paidAmount > 0 && $grandTotal < $paidAmount) {
|
||||
$this->db->transRollback();
|
||||
return $this->response->setStatusCode(400)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => "Cannot reduce invoice net amount to ₹$grandTotal because ₹$paidAmount has already been paid to the supplier. Adjust or reverse the payment first."
|
||||
]);
|
||||
}
|
||||
|
||||
// Determine updated payment_status
|
||||
$paymentStatus = 'unpaid';
|
||||
if ($paidAmount >= $grandTotal && $grandTotal > 0) {
|
||||
$paymentStatus = 'paid';
|
||||
} elseif ($paidAmount > 0) {
|
||||
$paymentStatus = 'partially_paid';
|
||||
}
|
||||
|
||||
// Update purchase record
|
||||
$updateData = [
|
||||
'store_id' => $storeId,
|
||||
'supplier_id' => $supplierId,
|
||||
'supplier_invoice_no' => trim($json['supplier_invoice_no'] ?: $existingPurchase['supplier_invoice_no']),
|
||||
'invoice_date' => $json['invoice_date'] ?? $existingPurchase['invoice_date'],
|
||||
'taxable_amount' => $taxableTotal,
|
||||
'cgst_amount' => $cgstTotal,
|
||||
'sgst_amount' => $sgstTotal,
|
||||
'igst_amount' => $igstTotal,
|
||||
'discount_amount' => $discountTotal,
|
||||
'net_amount' => $grandTotal,
|
||||
'payment_status' => $paymentStatus,
|
||||
'remarks' => trim($json['remarks'] ?? $existingPurchase['remarks'])
|
||||
];
|
||||
$this->db->table('mst_purchases')->where('purchase_id', $purchaseId)->update($updateData);
|
||||
|
||||
// Insert new purchase items
|
||||
foreach ($purchaseItems as $pi) {
|
||||
$this->db->table('mst_purchase_items')->insert($pi);
|
||||
}
|
||||
|
||||
// Step 3: Refresh double-entry ledger postings
|
||||
$this->db->table('mst_ledger_entries')
|
||||
->where('voucher_type', 'PURCHASE')
|
||||
->where('reference_type', 'purchase_invoice')
|
||||
->where('reference_id', $purchaseId)
|
||||
->delete();
|
||||
|
||||
$purchaseHead = $this->db->table('mst_account_heads')->where('head_code', '4001')->get()->getRowArray();
|
||||
$inputCgstHead = $this->db->table('mst_account_heads')->where('head_code', '1006')->get()->getRowArray();
|
||||
$inputSgstHead = $this->db->table('mst_account_heads')->where('head_code', '1007')->get()->getRowArray();
|
||||
$creditorHead = $this->db->table('mst_account_heads')->where('head_code', '2001')->get()->getRowArray();
|
||||
|
||||
$vNo = $updateData['supplier_invoice_no'];
|
||||
if ($purchaseHead) {
|
||||
$this->db->table('mst_ledger_entries')->insert([
|
||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PURCHASE', 'voucher_date' => $updateData['invoice_date'],
|
||||
'account_head_id' => $purchaseHead['head_id'], 'debit_amount' => $taxableTotal, 'credit_amount' => 0,
|
||||
'reference_type' => 'purchase_invoice', 'reference_id' => $purchaseId, 'narration' => "Updated Purchase from Supplier #$supplierId Bill $vNo"
|
||||
]);
|
||||
}
|
||||
if ($cgstTotal > 0 && $inputCgstHead) {
|
||||
$this->db->table('mst_ledger_entries')->insert([
|
||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PURCHASE', 'voucher_date' => $updateData['invoice_date'],
|
||||
'account_head_id' => $inputCgstHead['head_id'], 'debit_amount' => $cgstTotal, 'credit_amount' => 0,
|
||||
'reference_type' => 'purchase_invoice', 'reference_id' => $purchaseId, 'narration' => "Updated Input CGST credit on $vNo"
|
||||
]);
|
||||
}
|
||||
if ($sgstTotal > 0 && $inputSgstHead) {
|
||||
$this->db->table('mst_ledger_entries')->insert([
|
||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PURCHASE', 'voucher_date' => $updateData['invoice_date'],
|
||||
'account_head_id' => $inputSgstHead['head_id'], 'debit_amount' => $sgstTotal, 'credit_amount' => 0,
|
||||
'reference_type' => 'purchase_invoice', 'reference_id' => $purchaseId, 'narration' => "Updated Input SGST credit on $vNo"
|
||||
]);
|
||||
}
|
||||
if ($creditorHead) {
|
||||
$this->db->table('mst_ledger_entries')->insert([
|
||||
'store_id' => $storeId, 'voucher_no' => $vNo, 'voucher_type' => 'PURCHASE', 'voucher_date' => $updateData['invoice_date'],
|
||||
'account_head_id' => $creditorHead['head_id'], 'debit_amount' => 0, 'credit_amount' => $grandTotal,
|
||||
'reference_type' => 'purchase_invoice', 'reference_id' => $purchaseId, 'narration' => "Updated Credit payable to Supplier #$supplierId"
|
||||
]);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to update purchase invoice.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => 'Purchase invoice #' . $vNo . ' updated successfully! Stock and ledgers adjusted.',
|
||||
'purchase_id' => $purchaseId,
|
||||
'net_amount' => $grandTotal
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveSupplierPayment()
|
||||
{
|
||||
$json = $this->request->getJSON(true) ?: $this->request->getPost();
|
||||
|
|
|
|||
|
|
@ -4113,6 +4113,13 @@ function PurchaseView({ store, notify }) {
|
|||
const [loadedPoId, setLoadedPoId] = useState(null);
|
||||
const [loadedPoNumber, setLoadedPoNumber] = useState('');
|
||||
|
||||
// --- Purchase Invoice History & Edit States ---
|
||||
const [editingPurchaseId, setEditingPurchaseId] = useState(null);
|
||||
const [recentPurchases, setRecentPurchases] = useState([]);
|
||||
const [isLoadingPurchases, setIsLoadingPurchases] = useState(false);
|
||||
const [showPurchaseHistoryModal, setShowPurchaseHistoryModal] = useState(false);
|
||||
const [purchaseHistorySearch, setPurchaseHistorySearch] = useState('');
|
||||
|
||||
// --- Data Fetching & Action Handlers ---
|
||||
const fetchLedgerSummary = async () => {
|
||||
setIsLoadingLedger(true);
|
||||
|
|
@ -4130,6 +4137,71 @@ function PurchaseView({ store, notify }) {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchRecentPurchases = async () => {
|
||||
setIsLoadingPurchases(true);
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'purchases?store_id=' + store.store_id);
|
||||
const data = await res.json();
|
||||
if (data.status && data.purchases) {
|
||||
setRecentPurchases(data.purchases);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('fetchRecentPurchases error:', e);
|
||||
} finally {
|
||||
setIsLoadingPurchases(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPurchaseForEdit = async (purchaseId) => {
|
||||
if (!purchaseId) return;
|
||||
try {
|
||||
const res = await fetch(API_BASE + 'purchase/' + purchaseId);
|
||||
const data = await res.json();
|
||||
if (data.status && data.purchase) {
|
||||
setEditingPurchaseId(Number(data.purchase.purchase_id));
|
||||
setSelectedSupplierId(String(data.purchase.supplier_id));
|
||||
setInvoiceNo(data.purchase.supplier_invoice_no || '');
|
||||
setInvoiceDate(data.purchase.invoice_date || new Date().toISOString().split('T')[0]);
|
||||
setLoadedPoId(null);
|
||||
setLoadedPoNumber('');
|
||||
if (data.items && data.items.length > 0) {
|
||||
setRows(data.items.map(it => ({
|
||||
item_id: Number(it.item_id || 0),
|
||||
item_name: it.item_name || '',
|
||||
generic_name: it.generic_name || '',
|
||||
category: it.category || 'Tablet',
|
||||
batch_no: it.batch_no || '',
|
||||
expiry_date: it.expiry_date || '',
|
||||
qty_packs: Number(it.qty_packs || 1),
|
||||
free_qty_packs: Number(it.free_qty_packs || 0),
|
||||
units_per_pack: Number(it.units_per_pack || 10),
|
||||
mrp: Number(it.mrp || 0),
|
||||
ptr: Number(it.ptr || 0),
|
||||
discount_pct: Number(it.discount_pct || 0),
|
||||
gst_rate: Number(it.gst_rate || 12),
|
||||
hsn_code: it.hsn_code || '3004'
|
||||
})));
|
||||
}
|
||||
setSubTab('inward');
|
||||
setShowPurchaseHistoryModal(false);
|
||||
notify('Purchase Invoice #' + data.purchase.supplier_invoice_no + ' loaded for editing.', 'info');
|
||||
} else {
|
||||
notify(data.message || 'Could not load purchase details.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
notify('Error loading purchase invoice: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEditPurchase = () => {
|
||||
setEditingPurchaseId(null);
|
||||
setInvoiceNo('');
|
||||
setLoadedPoId(null);
|
||||
setLoadedPoNumber('');
|
||||
setRows([{ item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }]);
|
||||
notify('Exited purchase edit mode. Ready for new inward entry.', 'info');
|
||||
};
|
||||
|
||||
const fetchSupplierLedger = async (supplierId) => {
|
||||
if (!supplierId) {
|
||||
setActiveLedgerDetail(null);
|
||||
|
|
@ -4416,6 +4488,7 @@ function PurchaseView({ store, notify }) {
|
|||
useEffect(() => {
|
||||
fetchSuppliers();
|
||||
fetchMasterSupportData();
|
||||
fetchRecentPurchases();
|
||||
}, []);
|
||||
|
||||
const fetchMasterSupportData = async () => {
|
||||
|
|
@ -4713,30 +4786,45 @@ function PurchaseView({ store, notify }) {
|
|||
hsn_code: r.hsn_code || '3004'
|
||||
}));
|
||||
|
||||
const res = await fetch(API_BASE + 'purchase/save', {
|
||||
const endpoint = editingPurchaseId ? (API_BASE + 'purchase/update') : (API_BASE + 'purchase/save');
|
||||
const payload = {
|
||||
store_id: store.store_id,
|
||||
supplier_id: Number(selectedSupplierId),
|
||||
supplier_invoice_no: invoiceNo,
|
||||
invoice_date: invoiceDate,
|
||||
items: itemsPayload
|
||||
};
|
||||
if (editingPurchaseId) {
|
||||
payload.purchase_id = editingPurchaseId;
|
||||
} else if (loadedPoId) {
|
||||
payload.po_id = loadedPoId;
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
store_id: store.store_id,
|
||||
supplier_id: Number(selectedSupplierId),
|
||||
supplier_invoice_no: invoiceNo,
|
||||
invoice_date: invoiceDate,
|
||||
po_id: loadedPoId || undefined,
|
||||
items: itemsPayload
|
||||
})
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status) {
|
||||
notify('Purchase inward recorded! Stock & Supplier Ledger updated.' + (loadedPoNumber ? (' (PO ' + loadedPoNumber + ' marked as received)') : ''), 'success');
|
||||
if (editingPurchaseId) {
|
||||
notify('Purchase invoice #' + invoiceNo + ' updated successfully! Stock & supplier ledger adjusted.', 'success');
|
||||
} else {
|
||||
notify('Purchase inward recorded! Stock & Supplier Ledger updated.' + (loadedPoNumber ? (' (PO ' + loadedPoNumber + ' marked as received)') : ''), 'success');
|
||||
}
|
||||
setEditingPurchaseId(null);
|
||||
setInvoiceNo('');
|
||||
setLoadedPoId(null);
|
||||
setLoadedPoNumber('');
|
||||
setRows([{ item_id: 0, item_name: '', generic_name: '', category: 'Tablet', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12, hsn_code: '3004' }]);
|
||||
fetchRecentPurchases();
|
||||
if (activeLedgerSupplierId) fetchSupplierLedger(activeLedgerSupplierId);
|
||||
fetchLedgerSummary();
|
||||
} else {
|
||||
notify(data.message || 'Failed to save purchase bill.', 'error');
|
||||
notify(data.message || 'Failed to save purchase invoice.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
notify('Purchase bill error: ' + e.message, 'error');
|
||||
notify('Purchase invoice error: ' + e.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -4767,8 +4855,27 @@ function PurchaseView({ store, notify }) {
|
|||
{subTab === 'inward' && (
|
||||
<div className="counter-card">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
|
||||
<h5 className="fw-bold mb-0"><i className="bi bi-receipt text-primary me-1"></i> Distributor Purchase Inward (Stock Entry)</h5>
|
||||
<div>
|
||||
<h5 className="fw-bold mb-0">
|
||||
<i className="bi bi-receipt text-primary me-1"></i>
|
||||
{editingPurchaseId ? 'Edit Distributor Purchase Inward' : 'Distributor Purchase Inward (Stock Entry)'}
|
||||
</h5>
|
||||
<small className="text-muted">
|
||||
{editingPurchaseId ? `Modifying existing Bill #${invoiceNo} (Ref #${editingPurchaseId})` : 'Enter vendor delivery challan / tax invoice to replenish store stock'}
|
||||
</small>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => {
|
||||
fetchRecentPurchases();
|
||||
setShowPurchaseHistoryModal(true);
|
||||
}}
|
||||
title="View recent inward bills and select one to edit"
|
||||
>
|
||||
<i className="bi bi-clock-history me-1"></i> Inward Bills History / Edit ({recentPurchases.length})
|
||||
</button>
|
||||
{loadedPoNumber ? (
|
||||
<div className="badge bg-info-subtle text-info border border-info px-2 py-1.5 d-flex align-items-center gap-1.5" style={{ fontSize: '12px' }}>
|
||||
<i className="bi bi-link-45deg fs-6"></i>
|
||||
|
|
@ -4789,6 +4896,25 @@ function PurchaseView({ store, notify }) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingPurchaseId && (
|
||||
<div className="alert alert-warning border border-warning-subtle shadow-sm py-2 px-3 mb-3 d-flex justify-content-between align-items-center">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="badge bg-warning text-dark fs-6 px-2.5 py-1.5"><i className="bi bi-pencil-square me-1"></i>EDIT MODE</span>
|
||||
<div>
|
||||
<strong className="text-dark d-block">Editing Purchase Invoice #{invoiceNo} (Purchase ID: #{editingPurchaseId})</strong>
|
||||
<small className="text-muted">Updating this invoice will recalculate batch stock in this store and adjust supplier ledger balances.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-dark btn-sm fw-semibold"
|
||||
onClick={cancelEditPurchase}
|
||||
>
|
||||
<i className="bi bi-x-circle me-1"></i> Cancel Edit & New Inward
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="row g-2 mb-3">
|
||||
<div className="col-md-4">
|
||||
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||
|
|
@ -5024,13 +5150,24 @@ function PurchaseView({ store, notify }) {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between mt-2">
|
||||
<div className="d-flex justify-content-between align-items-center mt-3">
|
||||
<button className="btn btn-sm btn-outline-primary" onClick={addRow}>
|
||||
<i className="bi bi-plus-circle me-1"></i> Add Another Item
|
||||
</button>
|
||||
<button className="btn btn-success-custom" onClick={savePurchaseBill}>
|
||||
<i className="bi bi-check-circle me-1"></i> Record Inward Bill & Update Inventory
|
||||
</button>
|
||||
<div className="d-flex gap-2">
|
||||
{editingPurchaseId && (
|
||||
<button type="button" className="btn btn-outline-secondary btn-sm px-3" onClick={cancelEditPurchase}>
|
||||
<i className="bi bi-x-circle me-1"></i> Cancel Edit
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={editingPurchaseId ? "btn btn-warning shadow-sm fw-bold px-3 py-2 text-dark" : "btn btn-success-custom shadow-sm"}
|
||||
onClick={savePurchaseBill}
|
||||
>
|
||||
<i className={editingPurchaseId ? "bi bi-check2-all me-1" : "bi bi-check-circle me-1"}></i>
|
||||
{editingPurchaseId ? `Update Purchase Invoice #${invoiceNo}` : 'Record Inward Bill & Update Inventory'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -5434,12 +5571,21 @@ function PurchaseView({ store, notify }) {
|
|||
</span>
|
||||
</td>
|
||||
<td className="text-end pe-3">
|
||||
<button
|
||||
className="btn btn-sm btn-success py-1 px-2.5 shadow-sm fw-semibold"
|
||||
onClick={() => openPaymentModal(activeLedgerDetail.supplier.supplier_id, inv.purchase_id, inv.pending_amount)}
|
||||
>
|
||||
<i className="bi bi-cash me-1"></i> Pay Bill
|
||||
</button>
|
||||
<div className="btn-group btn-group-sm">
|
||||
<button
|
||||
className="btn btn-outline-primary py-1 px-2 shadow-sm"
|
||||
onClick={() => loadPurchaseForEdit(inv.purchase_id)}
|
||||
title="Edit this Purchase Invoice"
|
||||
>
|
||||
<i className="bi bi-pencil-square me-1"></i> Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success py-1 px-2.5 shadow-sm fw-semibold"
|
||||
onClick={() => openPaymentModal(activeLedgerDetail.supplier.supplier_id, inv.purchase_id, inv.pending_amount)}
|
||||
>
|
||||
<i className="bi bi-cash me-1"></i> Pay Bill
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
|
@ -5479,7 +5625,20 @@ function PurchaseView({ store, notify }) {
|
|||
activeLedgerDetail.statement.map((row, idx) => (
|
||||
<tr key={idx} className={row.type === 'PAYMENT' ? 'table-success-subtle' : ''}>
|
||||
<td className="text-nowrap">{row.date}</td>
|
||||
<td className="fw-semibold text-primary">{row.voucher_no}</td>
|
||||
<td className="fw-semibold text-primary">
|
||||
{row.type === 'INVOICE' && row.invoice_id ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link p-0 text-decoration-none fw-bold text-primary"
|
||||
onClick={() => loadPurchaseForEdit(row.invoice_id)}
|
||||
title="Click to edit purchase invoice"
|
||||
>
|
||||
{row.voucher_no} <i className="bi bi-pencil-square text-muted ms-1" style={{ fontSize: '11px' }}></i>
|
||||
</button>
|
||||
) : (
|
||||
row.voucher_no
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${row.type === 'INVOICE' ? 'bg-primary-subtle text-primary border border-primary-subtle' : 'bg-success text-white'}`}>
|
||||
{row.type === 'INVOICE' ? 'Purchase Bill' : 'Payment'}
|
||||
|
|
@ -6770,6 +6929,115 @@ function PurchaseView({ store, notify }) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* MODAL: INWARD PURCHASE BILLS HISTORY & EDIT SELECTOR */}
|
||||
{/* ================================================================= */}
|
||||
{showPurchaseHistoryModal && (
|
||||
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
|
||||
<div className="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div className="modal-content border-0 shadow-lg">
|
||||
<div className="modal-header bg-dark text-white py-2.5 px-3 d-flex justify-content-between align-items-center">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<div className="bg-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
|
||||
<i className="bi bi-clock-history fs-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="modal-title mb-0 fw-bold">Inward Purchase Bills History & Edit</h6>
|
||||
<small className="text-light opacity-75">Browse past invoices and click Edit to modify lines, rates, or batches</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn-close btn-close-white" onClick={() => setShowPurchaseHistoryModal(false)}></button>
|
||||
</div>
|
||||
<div className="modal-body p-3 bg-light">
|
||||
<div className="d-flex justify-content-between align-items-center gap-2 mb-3">
|
||||
<div className="input-group input-group-sm" style={{ maxWidth: 350 }}>
|
||||
<span className="input-group-text bg-white"><i className="bi bi-search text-muted"></i></span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search by Bill No or Supplier Name..."
|
||||
value={purchaseHistorySearch}
|
||||
onChange={(e) => setPurchaseHistorySearch(e.target.value)}
|
||||
/>
|
||||
{purchaseHistorySearch && (
|
||||
<button className="btn btn-outline-secondary" onClick={() => setPurchaseHistorySearch('')}>
|
||||
<i className="bi bi-x"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-outline-primary btn-sm" onClick={fetchRecentPurchases} title="Refresh Invoices">
|
||||
<i className="bi bi-arrow-clockwise me-1"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive bg-white rounded border shadow-sm">
|
||||
<table className="table table-hover align-middle mb-0" style={{ fontSize: '13px' }}>
|
||||
<thead className="table-light">
|
||||
<tr>
|
||||
<th style={{ width: 110 }}>Date</th>
|
||||
<th>Bill / Invoice No</th>
|
||||
<th>Supplier / Distributor</th>
|
||||
<th className="text-center">Items</th>
|
||||
<th className="text-end">Net Bill Amount</th>
|
||||
<th className="text-end">Paid Amount</th>
|
||||
<th className="text-center">Payment Status</th>
|
||||
<th className="text-end pe-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoadingPurchases ? (
|
||||
<tr><td colSpan="8" className="text-center py-4"><span className="spinner-border spinner-border-sm me-2"></span>Loading invoices...</td></tr>
|
||||
) : recentPurchases.length === 0 ? (
|
||||
<tr><td colSpan="8" className="text-center py-4 text-muted">No inward purchase bills recorded yet.</td></tr>
|
||||
) : (
|
||||
recentPurchases
|
||||
.filter(p => {
|
||||
if (!purchaseHistorySearch) return true;
|
||||
const q = purchaseHistorySearch.toLowerCase();
|
||||
return (p.supplier_invoice_no || '').toLowerCase().includes(q) || (p.supplier_name || '').toLowerCase().includes(q);
|
||||
})
|
||||
.map(p => (
|
||||
<tr key={p.purchase_id}>
|
||||
<td>{p.invoice_date}</td>
|
||||
<td className="fw-bold text-primary">{p.supplier_invoice_no}</td>
|
||||
<td>
|
||||
<div className="fw-semibold text-dark">{p.supplier_name || 'Unknown Supplier'}</div>
|
||||
{p.gstin && <small className="text-muted">GST: {p.gstin}</small>}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<span className="badge bg-light text-dark border px-2 py-1">{p.items_count || 1} lines</span>
|
||||
</td>
|
||||
<td className="text-end fw-bold text-dark">₹{Number(p.net_amount || 0).toFixed(2)}</td>
|
||||
<td className="text-end text-success">₹{Number(p.paid_amount || 0).toFixed(2)}</td>
|
||||
<td className="text-center">
|
||||
<span className={`badge ${p.payment_status === 'paid' ? 'bg-success' : p.payment_status === 'partially_paid' ? 'bg-warning text-dark' : 'bg-danger'} px-2 py-1`}>
|
||||
{p.payment_status === 'paid' ? 'Paid' : p.payment_status === 'partially_paid' ? 'Partially Paid' : 'Unpaid'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-end pe-3">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary py-1 px-2.5 shadow-sm fw-semibold"
|
||||
onClick={() => loadPurchaseForEdit(p.purchase_id)}
|
||||
title="Load this bill into Inward form to edit items and rates"
|
||||
>
|
||||
<i className="bi bi-pencil-square me-1"></i> Edit Invoice
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer py-2 bg-light">
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => setShowPurchaseHistoryModal(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user