feat(pharmacy): implement Purchase Orders (PO) and Inward Goods Received Notes (GRN) workflow
This commit is contained in:
parent
65a071928c
commit
8cbf55537f
|
|
@ -125,7 +125,23 @@ $routes->post('Product_master/Product_search', 'Medical::product_search');
|
|||
$routes->get('Product_master/drug_terminology_autocomplete', 'Medical::drug_terminology_autocomplete');
|
||||
$routes->get('Product_master/drug_terminology_detail', 'Medical::drug_terminology_detail');
|
||||
$routes->get('Product_master/Product_edit/(:num)', 'Medical::product_edit/$1');
|
||||
$routes->post('Product_master/product_master_update/(:num)', 'Medical::product_master_update/$1');
|
||||
// Pharmacy Procurement: Purchase Orders (PO) & GRN
|
||||
$routes->get('Medical/purchase_orders', 'MedicalProcurement::po_list');
|
||||
$routes->get('Medical/po_new', 'MedicalProcurement::po_new');
|
||||
$routes->post('Medical/po_create', 'MedicalProcurement::po_create');
|
||||
$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/grn_list', 'MedicalProcurement::grn_list');
|
||||
$routes->get('Medical/grn_new', 'MedicalProcurement::grn_new');
|
||||
$routes->get('Medical/grn_new/(:num)', 'MedicalProcurement::grn_new/$1');
|
||||
$routes->post('Medical/grn_create', 'MedicalProcurement::grn_create');
|
||||
$routes->get('Medical/grn_edit/(:num)', 'MedicalProcurement::grn_edit/$1');
|
||||
$routes->post('Medical/grn_post_inventory/(:num)', 'MedicalProcurement::grn_post_inventory/$1');
|
||||
$routes->get('Medical/grn_print/(:num)', 'MedicalProcurement::grn_print/$1');
|
||||
|
||||
$routes->get('Medical/Purchase', 'Medical::purchase');
|
||||
$routes->post('Medical/PurchaseInvoice', 'Medical::purchase_invoice');
|
||||
$routes->get('Medical/PurchaseNew', 'Medical::purchase_new');
|
||||
|
|
|
|||
973
app/Controllers/MedicalProcurement.php
Normal file
973
app/Controllers/MedicalProcurement.php
Normal file
|
|
@ -0,0 +1,973 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\MedicalModel;
|
||||
use App\Models\PharmacyPurchaseOrderModel;
|
||||
use App\Models\PharmacyPurchaseOrderItemModel;
|
||||
use App\Models\PharmacyGrnModel;
|
||||
use App\Models\PharmacyGrnItemModel;
|
||||
use Mpdf\Mpdf;
|
||||
|
||||
class MedicalProcurement extends BaseController
|
||||
{
|
||||
protected $db;
|
||||
protected $poModel;
|
||||
protected $poItemModel;
|
||||
protected $grnModel;
|
||||
protected $grnItemModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->poModel = new PharmacyPurchaseOrderModel();
|
||||
$this->poItemModel = new PharmacyPurchaseOrderItemModel();
|
||||
$this->grnModel = new PharmacyGrnModel();
|
||||
$this->grnItemModel = new PharmacyGrnItemModel();
|
||||
}
|
||||
|
||||
private function ensurePharmacyAccess()
|
||||
{
|
||||
$user = service('auth')->user();
|
||||
|
||||
$allowed = false;
|
||||
if ($user && method_exists($user, 'can')) {
|
||||
$allowed = $user->can('pharmacy.access')
|
||||
|| $user->can('billing.access')
|
||||
|| $user->can('pharmacy.po.create');
|
||||
}
|
||||
|
||||
if (! $allowed && $user && method_exists($user, 'inGroup')) {
|
||||
$allowed = $user->inGroup('superadmin', 'admin', 'developer');
|
||||
}
|
||||
|
||||
if (! $allowed) {
|
||||
$sess = session();
|
||||
if ($sess->get('logged_in') || $sess->get('user_id') || $sess->get('username') || $sess->get('agent_id')) {
|
||||
$allowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($allowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->response
|
||||
->setStatusCode(403)
|
||||
->setBody('<div class="alert alert-danger m-3">Access denied for Pharmacy Procurement module.</div>');
|
||||
}
|
||||
|
||||
private function getCurrentUserId(): int
|
||||
{
|
||||
$user = service('auth')->user();
|
||||
if ($user && isset($user->id)) {
|
||||
return (int) $user->id;
|
||||
}
|
||||
return (int) (session()->get('user_id') ?? 1);
|
||||
}
|
||||
|
||||
private function getCurrentUserLabel(): string
|
||||
{
|
||||
$user = service('auth')->user();
|
||||
if ($user) {
|
||||
$name = trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? ''));
|
||||
return ($name !== '' ? $name : ($user->username ?? 'User')) . '[' . ($user->id ?? 0) . ']';
|
||||
}
|
||||
$legacyName = session()->get('username') ?? 'Admin';
|
||||
return $legacyName . '[' . $this->getCurrentUserId() . ']';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. PURCHASE ORDERS (PO)
|
||||
// =========================================================================
|
||||
|
||||
public function po_list()
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$status = trim((string)($this->request->getGet('status') ?? ''));
|
||||
$supplierId = (int)($this->request->getGet('supplier_id') ?? 0);
|
||||
$fromDate = trim((string)($this->request->getGet('from') ?? ''));
|
||||
$toDate = trim((string)($this->request->getGet('to') ?? ''));
|
||||
$search = trim((string)($this->request->getGet('q') ?? ''));
|
||||
|
||||
$builder = $this->db->table('mst_purchase_orders po')
|
||||
->select('po.*, s.name_supplier, s.contact_no AS supplier_phone')
|
||||
->join('med_supplier s', 's.sid = po.supplier_id', 'left');
|
||||
|
||||
if (!empty($status) && $status !== 'all') {
|
||||
$builder->where('po.status', $status);
|
||||
}
|
||||
if ($supplierId > 0) {
|
||||
$builder->where('po.supplier_id', $supplierId);
|
||||
}
|
||||
if (!empty($fromDate)) {
|
||||
$builder->where('po.po_date >=', $fromDate);
|
||||
}
|
||||
if (!empty($toDate)) {
|
||||
$builder->where('po.po_date <=', $toDate);
|
||||
}
|
||||
if (!empty($search)) {
|
||||
$builder->groupStart()
|
||||
->like('po.po_number', $search)
|
||||
->orLike('s.name_supplier', $search)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$orders = $builder->orderBy('po.po_id', 'DESC')->get(150)->getResultArray();
|
||||
|
||||
// Get suppliers list for filter dropdown
|
||||
$suppliers = $this->db->table('med_supplier')
|
||||
->select('sid, name_supplier')
|
||||
->orderBy('name_supplier', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return view('medical/po_list', [
|
||||
'orders' => $orders,
|
||||
'suppliers' => $suppliers,
|
||||
'status' => $status,
|
||||
'supplierId' => $supplierId,
|
||||
'fromDate' => $fromDate,
|
||||
'toDate' => $toDate,
|
||||
'search' => $search,
|
||||
]);
|
||||
}
|
||||
|
||||
public function po_new()
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$suppliers = $this->db->table('med_supplier')
|
||||
->select('sid, name_supplier, contact_no, city, state, gst_no')
|
||||
->orderBy('name_supplier', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$stores = [];
|
||||
if ($this->db->tableExists('mst_stores')) {
|
||||
$stores = $this->db->table('mst_stores')
|
||||
->where('is_active', 1)
|
||||
->orderBy('is_main_store', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
$nextPoNumber = $this->poModel->generatePoNumber();
|
||||
|
||||
return view('medical/po_form', [
|
||||
'suppliers' => $suppliers,
|
||||
'stores' => $stores,
|
||||
'nextPoNumber' => $nextPoNumber,
|
||||
'po' => null,
|
||||
'poItems' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function po_create()
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$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'));
|
||||
$expectedDelivery = trim((string)$this->request->getPost('expected_delivery_date') ?: null);
|
||||
$paymentTerms = trim((string)$this->request->getPost('payment_terms') ?: 'Net 30 Days');
|
||||
$remarks = trim((string)$this->request->getPost('remarks') ?: '');
|
||||
$status = trim((string)$this->request->getPost('status') ?: 'ordered');
|
||||
|
||||
$itemsRaw = $this->request->getPost('items');
|
||||
$items = is_array($itemsRaw) ? $itemsRaw : [];
|
||||
|
||||
if ($supplierId <= 0) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Please select a valid supplier.']);
|
||||
}
|
||||
if (empty($items)) {
|
||||
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;
|
||||
$totalTaxAmount = 0.0;
|
||||
$validItems = [];
|
||||
|
||||
foreach ($items as $row) {
|
||||
$itemId = (int)($row['item_id'] ?? 0);
|
||||
$itemName = trim((string)($row['item_name'] ?? ''));
|
||||
$genericName = trim((string)($row['generic_name'] ?? ''));
|
||||
$category = trim((string)($row['category'] ?? 'Tablet'));
|
||||
$orderPacks = (float)($row['order_packs'] ?? 0);
|
||||
$unitsPerPack = (int)($row['units_per_pack'] ?? 10);
|
||||
$estimatedPtr = (float)($row['estimated_ptr'] ?? 0);
|
||||
$gstRate = (float)($row['gst_rate'] ?? 12.0);
|
||||
|
||||
if ($itemId <= 0 || $itemName === '' || $orderPacks <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lineSubtotal = round($orderPacks * $estimatedPtr, 2);
|
||||
$lineTax = round($lineSubtotal * ($gstRate / 100), 2);
|
||||
$lineTotal = $lineSubtotal + $lineTax;
|
||||
|
||||
$totalEstimatedAmount += $lineSubtotal;
|
||||
$totalTaxAmount += $lineTax;
|
||||
|
||||
$validItems[] = [
|
||||
'item_id' => $itemId,
|
||||
'item_name' => $itemName,
|
||||
'generic_name' => $genericName,
|
||||
'category' => $category,
|
||||
'order_packs' => $orderPacks,
|
||||
'units_per_pack' => $unitsPerPack > 0 ? $unitsPerPack : 1,
|
||||
'received_packs' => 0,
|
||||
'remaining_packs' => $orderPacks,
|
||||
'estimated_ptr' => $estimatedPtr,
|
||||
'gst_rate' => $gstRate,
|
||||
'estimated_amount' => $lineTotal,
|
||||
'status' => 'pending',
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($validItems)) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'No valid item rows were submitted.']);
|
||||
}
|
||||
|
||||
$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 ($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();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Failed to save purchase order. Database error.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => "Purchase Order {$poNumber} created successfully!",
|
||||
'po_id' => $poId,
|
||||
'po_number' => $poNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
public function po_edit(int $poId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$po = $this->db->table('mst_purchase_orders po')
|
||||
->select('po.*, s.name_supplier')
|
||||
->join('med_supplier s', 's.sid = po.supplier_id', 'left')
|
||||
->where('po.po_id', $poId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$po) {
|
||||
return view('medical/placeholder', ['title' => 'Purchase Order Not Found']);
|
||||
}
|
||||
|
||||
$items = $this->poItemModel->where('po_id', $poId)->findAll();
|
||||
|
||||
$suppliers = $this->db->table('med_supplier')
|
||||
->select('sid, name_supplier, contact_no, city, state, gst_no')
|
||||
->orderBy('name_supplier', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$stores = $this->db->tableExists('mst_stores')
|
||||
? $this->db->table('mst_stores')->where('is_active', 1)->get()->getResultArray()
|
||||
: [];
|
||||
|
||||
return view('medical/po_form', [
|
||||
'suppliers' => $suppliers,
|
||||
'stores' => $stores,
|
||||
'nextPoNumber' => $po['po_number'],
|
||||
'po' => $po,
|
||||
'poItems' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
public function po_status(int $poId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$newStatus = trim((string)$this->request->getPost('status'));
|
||||
$cancelReason = trim((string)$this->request->getPost('reason') ?: '');
|
||||
|
||||
if (!in_array($newStatus, ['approved', 'ordered', 'cancelled'], true)) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Invalid status transition.']);
|
||||
}
|
||||
|
||||
$po = $this->poModel->find($poId);
|
||||
if (!$po) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Purchase order not found.']);
|
||||
}
|
||||
|
||||
$userId = $this->getCurrentUserId();
|
||||
$updates = ['status' => $newStatus, 'updated_at' => date('Y-m-d H:i:s')];
|
||||
|
||||
if ($newStatus === 'approved' || $newStatus === 'ordered') {
|
||||
$updates['approved_by'] = $userId;
|
||||
$updates['approved_at'] = date('Y-m-d H:i:s');
|
||||
} elseif ($newStatus === 'cancelled') {
|
||||
$updates['cancelled_by'] = $userId;
|
||||
$updates['cancelled_at'] = date('Y-m-d H:i:s');
|
||||
$updates['cancelled_reason'] = $cancelReason;
|
||||
}
|
||||
|
||||
$this->poModel->update($poId, $updates);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => "Purchase Order status updated to {$newStatus}.",
|
||||
]);
|
||||
}
|
||||
|
||||
public function po_print(int $poId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$po = $this->db->table('mst_purchase_orders po')
|
||||
->select('po.*, s.name_supplier, s.contact_no, s.city, s.state, s.gst_no')
|
||||
->join('med_supplier s', 's.sid = po.supplier_id', 'left')
|
||||
->where('po.po_id', $poId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$po) {
|
||||
return view('medical/placeholder', ['title' => 'Purchase Order Not Found']);
|
||||
}
|
||||
|
||||
$items = $this->poItemModel->where('po_id', $poId)->findAll();
|
||||
|
||||
$html = view('medical/po_print_pdf', [
|
||||
'po' => $po,
|
||||
'items' => $items,
|
||||
]);
|
||||
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'orientation' => 'P',
|
||||
'margin_left' => 10,
|
||||
'margin_right' => 10,
|
||||
'margin_top' => 10,
|
||||
'margin_bottom' => 10,
|
||||
'tempDir' => WRITEPATH . 'cache',
|
||||
]);
|
||||
|
||||
$mpdf->SetTitle('Purchase Order - ' . $po['po_number']);
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setBody($mpdf->Output('Purchase_Order_' . $po['po_number'] . '.pdf', 'S'));
|
||||
}
|
||||
|
||||
public function po_reorder_items(int $supplierId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
if ($supplierId <= 0) {
|
||||
return $this->response->setJSON(['status' => 0, 'items' => []]);
|
||||
}
|
||||
|
||||
// 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,
|
||||
IFNULL(t.purchase_price, 0) AS estimated_ptr,
|
||||
IFNULL(t.CGST_per + t.SGST_per, 12) AS gst_rate,
|
||||
(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
|
||||
GROUP BY p.id
|
||||
HAVING current_stock < 50
|
||||
ORDER BY current_stock ASC
|
||||
LIMIT 50";
|
||||
|
||||
$rows = $this->db->query($sql, [$supplierId])->getResultArray();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'count' => count($rows),
|
||||
'items' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. GOODS RECEIVED NOTE (GRN)
|
||||
// =========================================================================
|
||||
|
||||
public function grn_list()
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$status = trim((string)($this->request->getGet('status') ?? ''));
|
||||
$supplierId = (int)($this->request->getGet('supplier_id') ?? 0);
|
||||
$fromDate = trim((string)($this->request->getGet('from') ?? ''));
|
||||
$toDate = trim((string)($this->request->getGet('to') ?? ''));
|
||||
$search = trim((string)($this->request->getGet('q') ?? ''));
|
||||
|
||||
$builder = $this->db->table('mst_grns grn')
|
||||
->select('grn.*, s.name_supplier, po.po_number')
|
||||
->join('med_supplier s', 's.sid = grn.supplier_id', 'left')
|
||||
->join('mst_purchase_orders po', 'po.po_id = grn.po_id', 'left');
|
||||
|
||||
if (!empty($status) && $status !== 'all') {
|
||||
$builder->where('grn.status', $status);
|
||||
}
|
||||
if ($supplierId > 0) {
|
||||
$builder->where('grn.supplier_id', $supplierId);
|
||||
}
|
||||
if (!empty($fromDate)) {
|
||||
$builder->where('grn.grn_date >=', $fromDate);
|
||||
}
|
||||
if (!empty($toDate)) {
|
||||
$builder->where('grn.grn_date <=', $toDate);
|
||||
}
|
||||
if (!empty($search)) {
|
||||
$builder->groupStart()
|
||||
->like('grn.grn_number', $search)
|
||||
->orLike('grn.supplier_invoice_no', $search)
|
||||
->orLike('po.po_number', $search)
|
||||
->orLike('s.name_supplier', $search)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$grnList = $builder->orderBy('grn.grn_id', 'DESC')->get(150)->getResultArray();
|
||||
|
||||
$suppliers = $this->db->table('med_supplier')
|
||||
->select('sid, name_supplier')
|
||||
->orderBy('name_supplier', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return view('medical/grn_list', [
|
||||
'grnList' => $grnList,
|
||||
'suppliers' => $suppliers,
|
||||
'status' => $status,
|
||||
'supplierId' => $supplierId,
|
||||
'fromDate' => $fromDate,
|
||||
'toDate' => $toDate,
|
||||
'search' => $search,
|
||||
]);
|
||||
}
|
||||
|
||||
public function grn_new($poId = 0)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$poId = (int)$poId;
|
||||
$po = null;
|
||||
$poItems = [];
|
||||
|
||||
if ($poId > 0) {
|
||||
$po = $this->db->table('mst_purchase_orders po')
|
||||
->select('po.*, s.name_supplier')
|
||||
->join('med_supplier s', 's.sid = po.supplier_id', 'left')
|
||||
->where('po.po_id', $poId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($po) {
|
||||
$poItems = $this->poItemModel
|
||||
->where('po_id', $poId)
|
||||
->where('remaining_packs >', 0)
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
|
||||
$suppliers = $this->db->table('med_supplier')
|
||||
->select('sid, name_supplier')
|
||||
->orderBy('name_supplier', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$approvedPos = $this->db->table('mst_purchase_orders')
|
||||
->select('po_id, po_number, po_date, supplier_id, total_estimated_amount')
|
||||
->whereIn('status', ['approved', 'ordered', 'partially_received'])
|
||||
->orderBy('po_id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$nextGrnNumber = $this->grnModel->generateGrnNumber();
|
||||
|
||||
return view('medical/grn_form', [
|
||||
'suppliers' => $suppliers,
|
||||
'approvedPos' => $approvedPos,
|
||||
'selectedPo' => $po,
|
||||
'poItems' => $poItems,
|
||||
'nextGrnNumber' => $nextGrnNumber,
|
||||
'grn' => null,
|
||||
'grnItems' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
public function grn_create()
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$supplierId = (int)$this->request->getPost('supplier_id');
|
||||
$poId = (int)($this->request->getPost('po_id') ?: 0);
|
||||
$challanNo = trim((string)$this->request->getPost('challan_no') ?: '');
|
||||
$supplierInvoiceNo = trim((string)$this->request->getPost('supplier_invoice_no') ?: '');
|
||||
$invoiceDate = trim((string)$this->request->getPost('invoice_date') ?: date('Y-m-d'));
|
||||
$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);
|
||||
|
||||
$itemsRaw = $this->request->getPost('items');
|
||||
$items = is_array($itemsRaw) ? $itemsRaw : [];
|
||||
|
||||
if ($supplierId <= 0) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Please select a supplier.']);
|
||||
}
|
||||
if (empty($supplierInvoiceNo)) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Supplier invoice / delivery bill number is required.']);
|
||||
}
|
||||
if (empty($items)) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Please add at least one received item.']);
|
||||
}
|
||||
|
||||
$grnNumber = $this->grnModel->generateGrnNumber();
|
||||
$userId = $this->getCurrentUserId();
|
||||
|
||||
$totalTaxable = 0.0;
|
||||
$totalTax = 0.0;
|
||||
$netTotal = 0.0;
|
||||
$validItems = [];
|
||||
|
||||
foreach ($items as $row) {
|
||||
$itemCode = (int)($row['item_code'] ?? 0);
|
||||
$itemName = trim((string)($row['item_name'] ?? ''));
|
||||
$batchNo = trim((string)($row['batch_no'] ?? ''));
|
||||
$expiryDate = trim((string)($row['expiry_date'] ?? ''));
|
||||
$packing = (int)($row['packing'] ?? 1);
|
||||
if ($packing <= 0) {
|
||||
$packing = 1;
|
||||
}
|
||||
|
||||
$orderPacks = (float)($row['order_packs'] ?? 0);
|
||||
$receivedPacks = (float)($row['received_packs'] ?? 0);
|
||||
$freePacks = (float)($row['free_packs'] ?? 0);
|
||||
$rejectedPacks = (float)($row['rejected_packs'] ?? 0);
|
||||
$rejectionReason = trim((string)($row['rejection_reason'] ?? ''));
|
||||
|
||||
$unitPurchaseRate = (float)($row['unit_purchase_rate'] ?? 0);
|
||||
$mrp = (float)($row['mrp'] ?? 0);
|
||||
$sellingUnitRate = (float)($row['selling_unit_rate'] ?? 0);
|
||||
if ($sellingUnitRate <= 0 && $mrp > 0) {
|
||||
$sellingUnitRate = round($mrp / $packing, 4);
|
||||
}
|
||||
|
||||
$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);
|
||||
$hsnCode = trim((string)($row['hsn_code'] ?? ''));
|
||||
$rackNo = trim((string)($row['rack_no'] ?? ''));
|
||||
$shelfNo = trim((string)($row['shelf_no'] ?? ''));
|
||||
$coldStorage = trim((string)($row['cold_storage'] ?? 'No'));
|
||||
$poItemId = (int)($row['po_item_id'] ?? 0);
|
||||
|
||||
if ($itemCode <= 0 || $itemName === '' || $batchNo === '' || empty($expiryDate)) {
|
||||
continue;
|
||||
}
|
||||
if ($receivedPacks <= 0 && $freePacks <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculations
|
||||
$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;
|
||||
|
||||
$totalTaxable += $lineTaxable;
|
||||
$totalTax += $lineTax;
|
||||
$netTotal += $lineTotal;
|
||||
|
||||
$validItems[] = [
|
||||
'po_item_id' => $poItemId > 0 ? $poItemId : null,
|
||||
'item_code' => $itemCode,
|
||||
'item_name' => $itemName,
|
||||
'batch_no' => $batchNo,
|
||||
'expiry_date' => $expiryDate,
|
||||
'packing' => $packing,
|
||||
'order_packs' => $orderPacks,
|
||||
'received_packs' => $receivedPacks,
|
||||
'free_packs' => $freePacks,
|
||||
'rejected_packs' => $rejectedPacks,
|
||||
'rejection_reason' => $rejectionReason ?: null,
|
||||
'unit_purchase_rate' => $unitPurchaseRate,
|
||||
'mrp' => $mrp,
|
||||
'selling_unit_rate' => $sellingUnitRate,
|
||||
'discount_percent' => $discPercent,
|
||||
'taxable_amount' => $lineTaxable,
|
||||
'cgst_percent' => $cgstPer,
|
||||
'cgst_amount' => $cgstAmt,
|
||||
'sgst_percent' => $sgstPer,
|
||||
'sgst_amount' => $sgstAmt,
|
||||
'igst_percent' => $igstPer,
|
||||
'igst_amount' => $igstAmt,
|
||||
'hsn_code' => $hsnCode ?: null,
|
||||
'total_amount' => $lineTotal,
|
||||
'rack_no' => $rackNo ?: null,
|
||||
'shelf_no' => $shelfNo ?: null,
|
||||
'cold_storage' => $coldStorage ?: null,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($validItems)) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'No valid item rows with batch and expiry were found.']);
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$grnHeader = [
|
||||
'grn_number' => $grnNumber,
|
||||
'store_id' => 1,
|
||||
'po_id' => $poId > 0 ? $poId : null,
|
||||
'supplier_id' => $supplierId,
|
||||
'challan_no' => $challanNo ?: null,
|
||||
'supplier_invoice_no' => $supplierInvoiceNo,
|
||||
'invoice_date' => $invoiceDate,
|
||||
'grn_date' => $grnDate,
|
||||
'total_received_amount' => $totalTaxable,
|
||||
'total_tax_amount' => $totalTax,
|
||||
'net_amount' => $netTotal,
|
||||
'status' => 'verified',
|
||||
'received_by' => $userId,
|
||||
'verified_by' => $userId,
|
||||
'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);
|
||||
$grnId = (int)$this->db->insertID();
|
||||
|
||||
foreach ($validItems as &$gItem) {
|
||||
$gItem['grn_id'] = $grnId;
|
||||
$this->grnItemModel->insert($gItem);
|
||||
}
|
||||
|
||||
// If requested to post directly to inventory
|
||||
if ($postToInventory === 1) {
|
||||
$this->executeGrnStockPosting($grnId);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
$err = $this->db->error();
|
||||
$errMsg = !empty($err['message']) ? ': ' . $err['message'] : '.';
|
||||
log_message('error', 'grn_create transaction failed' . $errMsg);
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Failed to save Goods Received Note' . $errMsg]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => "GRN {$grnNumber} created successfully!",
|
||||
'grn_id' => $grnId,
|
||||
'grn_number' => $grnNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
public function grn_edit(int $grnId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$grn = $this->db->table('mst_grns grn')
|
||||
->select('grn.*, s.name_supplier, po.po_number')
|
||||
->join('med_supplier s', 's.sid = grn.supplier_id', 'left')
|
||||
->join('mst_purchase_orders po', 'po.po_id = grn.po_id', 'left')
|
||||
->where('grn.grn_id', $grnId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$grn) {
|
||||
return view('medical/placeholder', ['title' => 'Goods Received Note Not Found']);
|
||||
}
|
||||
|
||||
$items = $this->grnItemModel->where('grn_id', $grnId)->findAll();
|
||||
|
||||
return view('medical/grn_detail', [
|
||||
'grn' => $grn,
|
||||
'grnItems' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
public function grn_post_inventory(int $grnId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$grn = $this->grnModel->find($grnId);
|
||||
if (!$grn) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'GRN not found.']);
|
||||
}
|
||||
if ($grn['status'] === 'posted_to_inventory') {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'This GRN has already been posted to live inventory.']);
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
$ok = $this->executeGrnStockPosting($grnId);
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false || !$ok) {
|
||||
return $this->response->setJSON(['status' => 0, 'message' => 'Failed to post GRN to inventory.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => 'GRN successfully posted to live store inventory & purchase registers!',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core bridge method that converts a verified GRN into live purchase_invoice and purchase_invoice_item
|
||||
*/
|
||||
private function executeGrnStockPosting(int $grnId): bool
|
||||
{
|
||||
$grn = $this->grnModel->find($grnId);
|
||||
if (!$grn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$supplier = $this->db->table('med_supplier')->where('sid', $grn['supplier_id'])->get()->getRowArray();
|
||||
$suppName = $supplier['name_supplier'] ?? 'Supplier';
|
||||
$userId = $this->getCurrentUserId();
|
||||
$userLabel = $this->getCurrentUserLabel();
|
||||
|
||||
// 1. Insert into purchase_invoice
|
||||
$purchaseInvoiceData = [
|
||||
'sid' => $grn['supplier_id'],
|
||||
'ischallan' => !empty($grn['challan_no']) ? 1 : 0,
|
||||
'monthyear' => date('m/Y', strtotime($grn['invoice_date'])),
|
||||
'supp_name' => $suppName,
|
||||
'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),
|
||||
'T_Net_Amount' => $grn['net_amount'],
|
||||
'inv_status' => 1,
|
||||
'inv_status_update_by' => $userId,
|
||||
];
|
||||
|
||||
$this->db->table('purchase_invoice')->insert($purchaseInvoiceData);
|
||||
$purchaseId = (int)$this->db->insertID();
|
||||
|
||||
if ($purchaseId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$medModel = new MedicalModel();
|
||||
$grnItems = $this->grnItemModel->where('grn_id', $grnId)->findAll();
|
||||
|
||||
// 2. Insert into purchase_invoice_item & trigger p_stock_update_purchase_id
|
||||
foreach ($grnItems as $gItem) {
|
||||
$packing = (int)($gItem['packing'] ?: 1);
|
||||
$receivedPacks = (float)$gItem['received_packs'];
|
||||
$freePacks = (float)$gItem['free_packs'];
|
||||
$totalUnits = (int)round(($receivedPacks + $freePacks) * $packing);
|
||||
|
||||
$purchasePricePerPack = (float)$gItem['unit_purchase_rate'];
|
||||
$purchaseUnitRate = $packing > 0 ? round($purchasePricePerPack / $packing, 4) : $purchasePricePerPack;
|
||||
|
||||
$piItemData = [
|
||||
'purchase_id' => $purchaseId,
|
||||
'item_code' => $gItem['item_code'],
|
||||
'Item_name' => $gItem['item_name'],
|
||||
'packing' => $packing,
|
||||
'batch_no' => $gItem['batch_no'],
|
||||
'purchase_price' => $purchasePricePerPack,
|
||||
'expiry_date' => $gItem['expiry_date'],
|
||||
'mrp' => $gItem['mrp'],
|
||||
'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'],
|
||||
'HSNCODE' => $gItem['hsn_code'] ?: '',
|
||||
'selling_price' => $gItem['selling_unit_rate'] * $packing,
|
||||
'total_unit' => $totalUnits,
|
||||
'total_sale_unit' => 0,
|
||||
'total_return_unit' => 0,
|
||||
'total_lost_unit' => 0,
|
||||
'selling_unit_rate' => $gItem['selling_unit_rate'],
|
||||
'purchase_unit_rate' => $purchaseUnitRate,
|
||||
'item_return' => 0,
|
||||
'remove_item' => 0,
|
||||
'stock_date' => $grn['invoice_date'],
|
||||
'rack_no' => $gItem['rack_no'] ?: '',
|
||||
'shelf_no' => $gItem['shelf_no'] ?: '',
|
||||
'cold_storage' => $gItem['cold_storage'] ?: 'No',
|
||||
'insert_by' => $userLabel,
|
||||
];
|
||||
|
||||
$this->db->table('purchase_invoice_item')->insert($piItemData);
|
||||
$purchaseItemId = (int)$this->db->insertID();
|
||||
|
||||
// Link purchase_item_id on GRN item row
|
||||
$this->grnItemModel->update($gItem['grn_item_id'], ['purchase_item_id' => $purchaseItemId]);
|
||||
|
||||
// Call stored procedure to sync live inventory
|
||||
$medModel->updatePurchaseStockById($purchaseItemId);
|
||||
|
||||
// Reconcile with PO item if linked
|
||||
if (!empty($gItem['po_item_id'])) {
|
||||
$poItem = $this->poItemModel->find($gItem['po_item_id']);
|
||||
if ($poItem) {
|
||||
$newReceived = (float)$poItem['received_packs'] + $receivedPacks;
|
||||
$orderPacks = (float)$poItem['order_packs'];
|
||||
$remaining = max(0, $orderPacks - $newReceived);
|
||||
$itemStatus = ($remaining <= 0) ? 'fulfilled' : 'partial';
|
||||
|
||||
$this->poItemModel->update($gItem['po_item_id'], [
|
||||
'received_packs' => $newReceived,
|
||||
'remaining_packs' => $remaining,
|
||||
'status' => $itemStatus,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update PO Header status if linked
|
||||
if (!empty($grn['po_id'])) {
|
||||
$unfulfilled = $this->poItemModel
|
||||
->where('po_id', $grn['po_id'])
|
||||
->where('remaining_packs >', 0)
|
||||
->countAllResults();
|
||||
|
||||
$newPoStatus = ($unfulfilled === 0) ? 'received' : 'partially_received';
|
||||
$this->poModel->update($grn['po_id'], [
|
||||
'status' => $newPoStatus,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
// 4. Update GRN status
|
||||
$this->grnModel->update($grnId, [
|
||||
'status' => 'posted_to_inventory',
|
||||
'purchase_id' => $purchaseId,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function grn_print(int $grnId)
|
||||
{
|
||||
if ($deny = $this->ensurePharmacyAccess()) {
|
||||
return $deny;
|
||||
}
|
||||
|
||||
$grn = $this->db->table('mst_grns grn')
|
||||
->select('grn.*, s.name_supplier, s.contact_no, s.city, s.state, s.gst_no, po.po_number')
|
||||
->join('med_supplier s', 's.sid = grn.supplier_id', 'left')
|
||||
->join('mst_purchase_orders po', 'po.po_id = grn.po_id', 'left')
|
||||
->where('grn.grn_id', $grnId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$grn) {
|
||||
return view('medical/placeholder', ['title' => 'Goods Received Note Not Found']);
|
||||
}
|
||||
|
||||
$items = $this->grnItemModel->where('grn_id', $grnId)->findAll();
|
||||
|
||||
$html = view('medical/grn_print_pdf', [
|
||||
'grn' => $grn,
|
||||
'items' => $items,
|
||||
]);
|
||||
|
||||
$mpdf = new Mpdf([
|
||||
'mode' => 'utf-8',
|
||||
'format' => 'A4',
|
||||
'orientation' => 'L',
|
||||
'margin_left' => 10,
|
||||
'margin_right' => 10,
|
||||
'margin_top' => 10,
|
||||
'margin_bottom' => 10,
|
||||
'tempDir' => WRITEPATH . 'cache',
|
||||
]);
|
||||
|
||||
$mpdf->SetTitle('GRN Inspection Slip - ' . $grn['grn_number']);
|
||||
$mpdf->WriteHTML($html);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setBody($mpdf->Output('GRN_' . $grn['grn_number'] . '.pdf', 'S'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreatePharmacyGrnAndEnhancePoTables extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// 1. Enhance mst_purchase_orders
|
||||
if ($db->tableExists('mst_purchase_orders')) {
|
||||
$existingFields = $db->getFieldNames('mst_purchase_orders');
|
||||
$addFields = [];
|
||||
|
||||
if (!in_array('payment_terms', $existingFields, true)) {
|
||||
$addFields['payment_terms'] = [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'expected_delivery_date',
|
||||
];
|
||||
}
|
||||
if (!in_array('tax_amount', $existingFields, true)) {
|
||||
$addFields['tax_amount'] = [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
'after' => 'total_estimated_amount',
|
||||
];
|
||||
}
|
||||
if (!in_array('grand_total', $existingFields, true)) {
|
||||
$addFields['grand_total'] = [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
'after' => 'tax_amount',
|
||||
];
|
||||
}
|
||||
if (!in_array('approved_by', $existingFields, true)) {
|
||||
$addFields['approved_by'] = [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'created_by',
|
||||
];
|
||||
}
|
||||
if (!in_array('approved_at', $existingFields, true)) {
|
||||
$addFields['approved_at'] = [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'approved_by',
|
||||
];
|
||||
}
|
||||
if (!in_array('cancelled_by', $existingFields, true)) {
|
||||
$addFields['cancelled_by'] = [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'approved_at',
|
||||
];
|
||||
}
|
||||
if (!in_array('cancelled_at', $existingFields, true)) {
|
||||
$addFields['cancelled_at'] = [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'cancelled_by',
|
||||
];
|
||||
}
|
||||
if (!in_array('cancelled_reason', $existingFields, true)) {
|
||||
$addFields['cancelled_reason'] = [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
'after' => 'cancelled_at',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($addFields)) {
|
||||
$this->forge->addColumn('mst_purchase_orders', $addFields);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Enhance mst_purchase_order_items
|
||||
if ($db->tableExists('mst_purchase_order_items')) {
|
||||
$existingItemFields = $db->getFieldNames('mst_purchase_order_items');
|
||||
$addItemFields = [];
|
||||
|
||||
if (!in_array('received_packs', $existingItemFields, true)) {
|
||||
$addItemFields['received_packs'] = [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
'after' => 'units_per_pack',
|
||||
];
|
||||
}
|
||||
if (!in_array('remaining_packs', $existingItemFields, true)) {
|
||||
$addItemFields['remaining_packs'] = [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
'after' => 'received_packs',
|
||||
];
|
||||
}
|
||||
if (!in_array('status', $existingItemFields, true)) {
|
||||
$addItemFields['status'] = [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'pending',
|
||||
'after' => 'estimated_amount',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($addItemFields)) {
|
||||
$this->forge->addColumn('mst_purchase_order_items', $addItemFields);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create mst_grns (Goods Received Note Header)
|
||||
if (!$db->tableExists('mst_grns')) {
|
||||
$this->forge->addField([
|
||||
'grn_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'grn_number' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
],
|
||||
'store_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 1,
|
||||
],
|
||||
'po_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'supplier_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
],
|
||||
'challan_no' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'supplier_invoice_no' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
],
|
||||
'invoice_date' => [
|
||||
'type' => 'DATE',
|
||||
],
|
||||
'grn_date' => [
|
||||
'type' => 'DATE',
|
||||
],
|
||||
'total_received_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'total_tax_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'net_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'draft', // draft, verified, posted_to_inventory, rejected, cancelled
|
||||
],
|
||||
'purchase_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'received_by' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
],
|
||||
'verified_by' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'verified_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'remarks' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('grn_id', true);
|
||||
$this->forge->addKey('grn_number');
|
||||
$this->forge->addKey('po_id');
|
||||
$this->forge->addKey('supplier_id');
|
||||
$this->forge->addKey('status');
|
||||
$this->forge->addKey('purchase_id');
|
||||
$this->forge->createTable('mst_grns', true);
|
||||
}
|
||||
|
||||
// 4. Create mst_grn_items (Goods Received Note Items)
|
||||
if (!$db->tableExists('mst_grn_items')) {
|
||||
$this->forge->addField([
|
||||
'grn_item_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'grn_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
],
|
||||
'po_item_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'item_code' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
],
|
||||
'item_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 200,
|
||||
],
|
||||
'batch_no' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
],
|
||||
'expiry_date' => [
|
||||
'type' => 'DATE',
|
||||
],
|
||||
'packing' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 1,
|
||||
],
|
||||
'order_packs' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'received_packs' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'free_packs' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'rejected_packs' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'rejection_reason' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'unit_purchase_rate' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'mrp' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'selling_unit_rate' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'discount_percent' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '5,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'taxable_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'cgst_percent' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '5,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'cgst_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'sgst_percent' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '5,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'sgst_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'igst_percent' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '5,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'igst_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '10,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'hsn_code' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'total_amount' => [
|
||||
'type' => 'DECIMAL',
|
||||
'constraint' => '12,2',
|
||||
'default' => 0.00,
|
||||
],
|
||||
'rack_no' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'shelf_no' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'cold_storage' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
'purchase_item_id' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => true,
|
||||
'default' => null,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('grn_item_id', true);
|
||||
$this->forge->addKey('grn_id');
|
||||
$this->forge->addKey('item_code');
|
||||
$this->forge->addKey('po_item_id');
|
||||
$this->forge->addKey('purchase_item_id');
|
||||
$this->forge->createTable('mst_grn_items', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('mst_grn_items', true);
|
||||
$this->forge->dropTable('mst_grns', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,16 +10,43 @@ class MedicalModel extends Model
|
|||
|
||||
public function updatePurchaseStockById(int $purchaseItemId): bool
|
||||
{
|
||||
if ($purchaseItemId <= 0) {
|
||||
if ($purchaseItemId <= 0 || ! $this->db->tableExists('purchase_invoice_item')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->query('CALL p_stock_update_purchase_id(?)', [$purchaseItemId]);
|
||||
$this->clearProcedureResults();
|
||||
// Recompute total sold units directly from sale items if available
|
||||
if ($this->db->tableExists('inv_med_item')) {
|
||||
$itemFields = $this->db->getFieldNames('inv_med_item') ?? [];
|
||||
$sumSql = in_array('sale_return', $itemFields, true)
|
||||
? 'SUM(CASE WHEN IFNULL(sale_return,0)=1 OR IFNULL(amount,0)<0 THEN IFNULL(qty,0)*-1 ELSE IFNULL(qty,0) END) AS sold_qty'
|
||||
: 'SUM(CASE WHEN IFNULL(amount,0)<0 THEN IFNULL(qty,0)*-1 ELSE IFNULL(qty,0) END) AS sold_qty';
|
||||
|
||||
$soldRow = $this->db->table('inv_med_item')
|
||||
->select($sumSql, false)
|
||||
->where('store_stock_id', $purchaseItemId)
|
||||
->get()
|
||||
->getRow();
|
||||
$soldQty = max(0.0, (float) ($soldRow->sold_qty ?? 0));
|
||||
|
||||
$stockRow = $this->db->table('purchase_invoice_item')
|
||||
->select('id, total_sale_unit')
|
||||
->where('id', $purchaseItemId)
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
if ($stockRow) {
|
||||
$currentSaleQty = (float) ($stockRow->total_sale_unit ?? 0);
|
||||
if (abs($currentSaleQty - $soldQty) >= 0.00001) {
|
||||
$this->db->table('purchase_invoice_item')
|
||||
->where('id', $purchaseItemId)
|
||||
->update(['total_sale_unit' => $soldQty]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'p_stock_update_purchase_id failed: ' . $e->getMessage());
|
||||
log_message('error', 'updatePurchaseStockById failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,11 +58,18 @@ class MedicalModel extends Model
|
|||
}
|
||||
|
||||
try {
|
||||
$this->db->query('CALL p_sale_transfer_ssno(?,?,?)', [$oldSsno, $newSsno, $qty]);
|
||||
$this->clearProcedureResults();
|
||||
if ($this->db->tableExists('inv_med_item')) {
|
||||
$this->db->table('inv_med_item')
|
||||
->where('store_stock_id', $oldSsno)
|
||||
->limit(1)
|
||||
->update(['store_stock_id' => $newSsno]);
|
||||
|
||||
$this->updatePurchaseStockById($oldSsno);
|
||||
$this->updatePurchaseStockById($newSsno);
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'p_sale_transfer_ssno failed: ' . $e->getMessage());
|
||||
log_message('error', 'transferSaleSsno failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
app/Models/PharmacyGrnItemModel.php
Normal file
45
app/Models/PharmacyGrnItemModel.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PharmacyGrnItemModel extends Model
|
||||
{
|
||||
protected $table = 'mst_grn_items';
|
||||
protected $primaryKey = 'grn_item_id';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $allowedFields = [
|
||||
'grn_id',
|
||||
'po_item_id',
|
||||
'item_code',
|
||||
'item_name',
|
||||
'batch_no',
|
||||
'expiry_date',
|
||||
'packing',
|
||||
'order_packs',
|
||||
'received_packs',
|
||||
'free_packs',
|
||||
'rejected_packs',
|
||||
'rejection_reason',
|
||||
'unit_purchase_rate',
|
||||
'mrp',
|
||||
'selling_unit_rate',
|
||||
'discount_percent',
|
||||
'taxable_amount',
|
||||
'cgst_percent',
|
||||
'cgst_amount',
|
||||
'sgst_percent',
|
||||
'sgst_amount',
|
||||
'igst_percent',
|
||||
'igst_amount',
|
||||
'hsn_code',
|
||||
'total_amount',
|
||||
'rack_no',
|
||||
'shelf_no',
|
||||
'cold_storage',
|
||||
'purchase_item_id',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
54
app/Models/PharmacyGrnModel.php
Normal file
54
app/Models/PharmacyGrnModel.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PharmacyGrnModel extends Model
|
||||
{
|
||||
protected $table = 'mst_grns';
|
||||
protected $primaryKey = 'grn_id';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $allowedFields = [
|
||||
'grn_number',
|
||||
'store_id',
|
||||
'po_id',
|
||||
'supplier_id',
|
||||
'challan_no',
|
||||
'supplier_invoice_no',
|
||||
'invoice_date',
|
||||
'grn_date',
|
||||
'total_received_amount',
|
||||
'total_tax_amount',
|
||||
'net_amount',
|
||||
'status',
|
||||
'purchase_id',
|
||||
'received_by',
|
||||
'verified_by',
|
||||
'verified_at',
|
||||
'remarks',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function generateGrnNumber(): string
|
||||
{
|
||||
$prefix = 'GRN-' . date('ym');
|
||||
$row = $this->like('grn_number', $prefix, 'after')
|
||||
->orderBy('grn_id', 'DESC')
|
||||
->first();
|
||||
|
||||
$seq = 1;
|
||||
if ($row && !empty($row['grn_number'])) {
|
||||
$parts = explode('-', $row['grn_number']);
|
||||
$lastSeq = (int) end($parts);
|
||||
if ($lastSeq > 0) {
|
||||
$seq = $lastSeq + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $prefix . '-' . str_pad((string)$seq, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
29
app/Models/PharmacyPurchaseOrderItemModel.php
Normal file
29
app/Models/PharmacyPurchaseOrderItemModel.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PharmacyPurchaseOrderItemModel extends Model
|
||||
{
|
||||
protected $table = 'mst_purchase_order_items';
|
||||
protected $primaryKey = 'po_item_id';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $allowedFields = [
|
||||
'po_id',
|
||||
'item_id',
|
||||
'item_name',
|
||||
'generic_name',
|
||||
'category',
|
||||
'order_packs',
|
||||
'units_per_pack',
|
||||
'received_packs',
|
||||
'remaining_packs',
|
||||
'estimated_ptr',
|
||||
'gst_rate',
|
||||
'estimated_amount',
|
||||
'status',
|
||||
];
|
||||
protected $useTimestamps = false;
|
||||
}
|
||||
55
app/Models/PharmacyPurchaseOrderModel.php
Normal file
55
app/Models/PharmacyPurchaseOrderModel.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PharmacyPurchaseOrderModel extends Model
|
||||
{
|
||||
protected $table = 'mst_purchase_orders';
|
||||
protected $primaryKey = 'po_id';
|
||||
protected $returnType = 'array';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $allowedFields = [
|
||||
'store_id',
|
||||
'supplier_id',
|
||||
'po_number',
|
||||
'po_date',
|
||||
'expected_delivery_date',
|
||||
'payment_terms',
|
||||
'total_items',
|
||||
'total_estimated_amount',
|
||||
'tax_amount',
|
||||
'grand_total',
|
||||
'status',
|
||||
'remarks',
|
||||
'created_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'cancelled_by',
|
||||
'cancelled_at',
|
||||
'cancelled_reason',
|
||||
];
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
|
||||
public function generatePoNumber(): string
|
||||
{
|
||||
$prefix = 'PO-' . date('ym');
|
||||
$row = $this->like('po_number', $prefix, 'after')
|
||||
->orderBy('po_id', 'DESC')
|
||||
->first();
|
||||
|
||||
$seq = 1;
|
||||
if ($row && !empty($row['po_number'])) {
|
||||
$parts = explode('-', $row['po_number']);
|
||||
$lastSeq = (int) end($parts);
|
||||
if ($lastSeq > 0) {
|
||||
$seq = $lastSeq + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $prefix . '-' . str_pad((string)$seq, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,12 @@
|
|||
<a class="btn btn-outline-primary btn-sm" href="javascript:load_form_div('<?= base_url('Medical/main_store') ?>','medical-main','Store Main :Pharmacy');">
|
||||
<i class="bi bi-display"></i> Store Main
|
||||
</a>
|
||||
<a class="btn btn-outline-info btn-sm" href="javascript:load_form_div('<?= base_url('Medical/purchase_orders') ?>','medical-main','Purchase Orders :Pharmacy');">
|
||||
<i class="bi bi-file-earmark-text"></i> Purchase Orders (PO)
|
||||
</a>
|
||||
<a class="btn btn-outline-success btn-sm" href="javascript:load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main','GRN Register :Pharmacy');">
|
||||
<i class="bi bi-box-seam"></i> Inward GRN
|
||||
</a>
|
||||
<a class="btn btn-outline-primary btn-sm" href="javascript:load_form_div('<?= base_url('Medical/master') ?>','medical-main','Master :Pharmacy');">
|
||||
<i class="bi bi-sliders"></i> Master
|
||||
</a>
|
||||
|
|
|
|||
177
app/Views/medical/grn_detail.php
Normal file
177
app/Views/medical/grn_detail.php
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<div class="pagetitle d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0 text-success">
|
||||
<i class="bi bi-card-checklist"></i> GRN Inspection Details
|
||||
<span class="badge bg-light text-dark border ms-2 font-monospace"><?= esc($grn['grn_number']) ?></span>
|
||||
</h4>
|
||||
<small class="text-muted">Inward Goods Inspection & Live Stock Posting</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main','GRN Register :Pharmacy');" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to GRN List
|
||||
</a>
|
||||
<a href="<?= base_url('Medical/grn_print/' . $grn['grn_id']) ?>" target="_blank" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-printer"></i> Print Slip
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted d-block">Supplier / Vendor</small>
|
||||
<strong><?= esc($grn['name_supplier']) ?></strong>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<small class="text-muted d-block">Vendor Invoice #</small>
|
||||
<strong><?= esc($grn['supplier_invoice_no']) ?></strong>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<small class="text-muted d-block">Invoice Date</small>
|
||||
<span><?= date('d-M-Y', strtotime($grn['invoice_date'])) ?></span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<small class="text-muted d-block">GRN Date</small>
|
||||
<span><?= date('d-M-Y', strtotime($grn['grn_date'])) ?></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted d-block">Linked PO</small>
|
||||
<?php if (!empty($grn['po_number'])): ?>
|
||||
<span class="badge bg-light text-primary border font-monospace"><?= esc($grn['po_number']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Direct Intake</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($grn['remarks'])): ?>
|
||||
<div class="alert alert-light border mt-3 mb-0 py-2">
|
||||
<small><strong>Inspection Remarks:</strong> <?= esc($grn['remarks']) ?></small>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
<div class="card-header bg-light py-2 d-flex justify-content-between align-items-center">
|
||||
<strong class="text-secondary"><i class="bi bi-capsule"></i> Verified Medicine Items</strong>
|
||||
<span class="badge bg-<?= ($grn['status'] === 'posted_to_inventory') ? 'success' : 'info text-dark' ?> text-uppercase">
|
||||
<?= str_replace('_', ' ', esc($grn['status'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th>Product Name</th>
|
||||
<th>Batch No</th>
|
||||
<th>Expiry Date</th>
|
||||
<th class="text-center">Pack</th>
|
||||
<th class="text-center">Rec. Packs</th>
|
||||
<th class="text-center">Free</th>
|
||||
<th class="text-center">Rejected</th>
|
||||
<th class="text-end">Rate/Pack</th>
|
||||
<th class="text-end">MRP</th>
|
||||
<th class="text-center">GST %</th>
|
||||
<th class="text-end">Line Total</th>
|
||||
<th class="text-center">Live Stock ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($grnItems)): ?>
|
||||
<?php foreach ($grnItems as $idx => $it): ?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td><strong><?= esc($it['item_name']) ?></strong></td>
|
||||
<td><span class="badge bg-light text-dark font-monospace border"><?= esc($it['batch_no']) ?></span></td>
|
||||
<td><?= date('d-M-Y', strtotime($it['expiry_date'])) ?></td>
|
||||
<td class="text-center"><?= (int)$it['packing'] ?>s</td>
|
||||
<td class="text-center fw-bold"><?= (float)$it['received_packs'] ?></td>
|
||||
<td class="text-center text-success"><?= (float)$it['free_packs'] ?></td>
|
||||
<td class="text-center <?= ((float)$it['rejected_packs'] > 0) ? 'text-danger fw-bold' : 'text-muted' ?>">
|
||||
<?= (float)$it['rejected_packs'] ?>
|
||||
<?php if (!empty($it['rejection_reason'])): ?>
|
||||
<small class="d-block text-danger">(<?= esc($it['rejection_reason']) ?>)</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['unit_purchase_rate'], 2) ?></td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['mrp'], 2) ?></td>
|
||||
<td class="text-center"><?= number_format((float)$it['cgst_percent'] * 2, 0) ?>%</td>
|
||||
<td class="text-end fw-semibold">₹<?= number_format((float)$it['total_amount'], 2) ?></td>
|
||||
<td class="text-center">
|
||||
<?php if (!empty($it['purchase_item_id'])): ?>
|
||||
<span class="badge bg-success font-monospace">Item #<?= (int)$it['purchase_item_id'] ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Pending Post</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="11" class="text-end">Taxable Amount:</th>
|
||||
<th class="text-end">₹<?= number_format((float)$grn['total_received_amount'], 2) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="11" class="text-end">Total GST:</th>
|
||||
<th class="text-end">₹<?= number_format((float)$grn['total_tax_amount'], 2) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr class="table-success">
|
||||
<th colspan="11" class="text-end fs-6">Net Inward Bill Total:</th>
|
||||
<th class="text-end fs-6 text-success fw-bold">₹<?= number_format((float)$grn['net_amount'], 2) ?></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-3 border-0 bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div>
|
||||
<?php if ($grn['status'] === 'posted_to_inventory'): ?>
|
||||
<span class="text-success fw-bold">
|
||||
<i class="bi bi-check-circle-fill"></i> Posted to Live Store Inventory (Purchase Bill Ref #<?= (int)$grn['purchase_id'] ?>)
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-warning fw-bold">
|
||||
<i class="bi bi-clock-history"></i> Verified by Pharmacist • Ready to Post to Live Hospital Inventory
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<?php if ($grn['status'] !== 'posted_to_inventory'): ?>
|
||||
<button type="button" class="btn btn-success px-4" onclick="postGrnToLiveInventory(<?= (int)$grn['grn_id'] ?>)">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Post to Live Inventory
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/PurchaseInvoiceEdit/' . (int)$grn['purchase_id']) ?>','medical-main');" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-receipt"></i> View Purchase Invoice
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function postGrnToLiveInventory(grnId) {
|
||||
if (!confirm('Confirm posting verified items into live pharmacy store inventory? This will generate live batches and update available stock.')) return;
|
||||
|
||||
$.post('<?= base_url('Medical/grn_post_inventory/') ?>' + grnId, {}, function(data) {
|
||||
alert(data.message || 'Done');
|
||||
if (data.status) {
|
||||
load_form_div('<?= base_url('Medical/grn_edit/') ?>' + grnId, 'medical-main', 'GRN Inspection Detail');
|
||||
}
|
||||
}, 'json').fail(function() {
|
||||
alert('Error posting GRN.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
462
app/Views/medical/grn_form.php
Normal file
462
app/Views/medical/grn_form.php
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
<?php
|
||||
$poId = !empty($selectedPo) ? (int)$selectedPo['po_id'] : 0;
|
||||
$supplierId = !empty($selectedPo) ? (int)$selectedPo['supplier_id'] : 0;
|
||||
$nextGrnNumber = $nextGrnNumber ?? 'GRN-NEW';
|
||||
?>
|
||||
|
||||
<div class="pagetitle d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0 text-success">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Create Goods Received Note (GRN)
|
||||
<span class="badge bg-light text-dark border ms-2 font-monospace"><?= esc($nextGrnNumber) ?></span>
|
||||
</h4>
|
||||
<small class="text-muted">Inward Stock Physical Inspection & Batch Entry</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main','GRN Register :Pharmacy');" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to GRN List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
<div class="card-header bg-light py-2">
|
||||
<strong class="text-secondary"><i class="bi bi-truck"></i> Inward Delivery Metadata</strong>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<form id="grn-create-form" onsubmit="event.preventDefault(); submitGrnForm();">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm fw-semibold">Linked Purchase Order (PO)</label>
|
||||
<select name="po_id" id="grn_po_id" class="form-select form-select-sm" onchange="handlePoSelection(this.value);">
|
||||
<option value="0">-- Direct Intake (No PO) --</option>
|
||||
<?php if (!empty($approvedPos)): ?>
|
||||
<?php foreach ($approvedPos as $p): ?>
|
||||
<option value="<?= (int)$p['po_id'] ?>" <?= ($poId == $p['po_id']) ? 'selected' : '' ?>>
|
||||
<?= esc($p['po_number']) ?> (<?= date('d/m/y', strtotime($p['po_date'])) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm fw-semibold">Supplier / Pharmaceutical Vendor <span class="text-danger">*</span></label>
|
||||
<select name="supplier_id" id="grn_supplier_id" class="form-select form-select-sm" required>
|
||||
<option value="">-- Select Supplier --</option>
|
||||
<?php if (!empty($suppliers)): ?>
|
||||
<?php foreach ($suppliers as $s): ?>
|
||||
<option value="<?= (int)$s['sid'] ?>" <?= ($supplierId == $s['sid']) ? 'selected' : '' ?>>
|
||||
<?= esc($s['name_supplier']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">Vendor Bill / Invoice # <span class="text-danger">*</span></label>
|
||||
<input type="text" name="supplier_invoice_no" id="supplier_invoice_no" class="form-control form-control-sm" placeholder="e.g., INV-88421" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">Bill Date</label>
|
||||
<input type="date" name="invoice_date" id="invoice_date" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">GRN Received Date</label>
|
||||
<input type="date" name="grn_date" id="grn_date" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm fw-semibold">Delivery Challan / Gate Pass #</label>
|
||||
<input type="text" name="challan_no" id="challan_no" class="form-control form-control-sm" placeholder="Optional Challan No.">
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<label class="form-label form-label-sm fw-semibold">Receiving Inspection Remarks</label>
|
||||
<input type="text" name="remarks" id="grn_remarks" class="form-control form-control-sm" placeholder="Package integrity verified, cold-chain temperature logged, etc.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Drug Search (for direct items or extra items) -->
|
||||
<div class="card border-success mt-3">
|
||||
<div class="card-header bg-success text-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-shield-check"></i> Physical Inspection & Batch Intake Table</span>
|
||||
<small class="text-white-50">Enter Batch, Expiry, Received Packs & Pricing</small>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Add Medicine / Drug Master</label>
|
||||
<input type="text" id="grn_drug_search" class="form-control form-control-sm" placeholder="Search medicine to add row..." autocomplete="off">
|
||||
<input type="hidden" id="grn_sel_item_code" value="">
|
||||
<input type="hidden" id="grn_sel_item_name" value="">
|
||||
<input type="hidden" id="grn_sel_packing" value="10">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Batch No.</label>
|
||||
<input type="text" id="grn_sel_batch" class="form-control form-control-sm" placeholder="e.g. BATCH-01">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Expiry Date</label>
|
||||
<input type="date" id="grn_sel_expiry" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Received Packs</label>
|
||||
<input type="number" id="grn_sel_rec_packs" class="form-control form-control-sm" value="10" min="1">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-success btn-sm w-100" id="btn-add-grn-row" onclick="addCustomItemToGrn();" disabled>
|
||||
<i class="bi bi-plus-lg"></i> Add Row
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm align-middle mb-0" id="grn-items-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 30px;">#</th>
|
||||
<th>Medicine Name</th>
|
||||
<th style="width: 110px;">Batch No <span class="text-danger">*</span></th>
|
||||
<th style="width: 120px;">Expiry <span class="text-danger">*</span></th>
|
||||
<th class="text-center" style="width: 70px;">Pack</th>
|
||||
<th class="text-center" style="width: 80px;">Ordered</th>
|
||||
<th class="text-center" style="width: 85px;">Rec. Packs <span class="text-danger">*</span></th>
|
||||
<th class="text-center" style="width: 75px;">Free</th>
|
||||
<th class="text-center" style="width: 75px;">Damaged</th>
|
||||
<th class="text-end" style="width: 100px;">Rate/Pack</th>
|
||||
<th class="text-end" style="width: 90px;">MRP</th>
|
||||
<th class="text-center" style="width: 70px;">GST %</th>
|
||||
<th class="text-end" style="width: 110px;">Line Total</th>
|
||||
<th style="width: 40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="grn-items-body">
|
||||
<?php if (!empty($poItems)): ?>
|
||||
<?php foreach ($poItems as $idx => $it): ?>
|
||||
<tr>
|
||||
<td class="row-num"><?= $idx + 1 ?></td>
|
||||
<td>
|
||||
<strong><?= esc($it['item_name']) ?></strong>
|
||||
<input type="hidden" name="items[<?= $idx ?>][item_code]" value="<?= (int)$it['item_id'] ?>">
|
||||
<input type="hidden" name="items[<?= $idx ?>][item_name]" value="<?= esc($it['item_name']) ?>">
|
||||
<input type="hidden" name="items[<?= $idx ?>][po_item_id]" value="<?= (int)$it['po_item_id'] ?>">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="items[<?= $idx ?>][batch_no]" class="form-control form-control-sm font-monospace line-batch" placeholder="Batch #" required>
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" name="items[<?= $idx ?>][expiry_date]" class="form-control form-control-sm line-expiry" required>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][packing]" class="form-control form-control-sm text-center line-pack" value="<?= (int)$it['units_per_pack'] ?>" min="1">
|
||||
</td>
|
||||
<td class="text-center text-muted">
|
||||
<?= (float)$it['remaining_packs'] ?>
|
||||
<input type="hidden" name="items[<?= $idx ?>][order_packs]" value="<?= (float)$it['remaining_packs'] ?>">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][received_packs]" class="form-control form-control-sm text-center line-rec" value="<?= (float)$it['remaining_packs'] ?>" min="0" step="1" onchange="recalculateGrnTotals();" required>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][free_packs]" class="form-control form-control-sm text-center line-free" value="0" min="0" step="1">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][rejected_packs]" class="form-control form-control-sm text-center line-rej" value="0" min="0" step="1">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[<?= $idx ?>][unit_purchase_rate]" class="form-control form-control-sm text-end line-rate" value="<?= number_format((float)$it['estimated_ptr'], 2, '.', '') ?>" min="0" step="0.01" onchange="recalculateGrnTotals();">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[<?= $idx ?>][mrp]" class="form-control form-control-sm text-end line-mrp" value="<?= number_format((float)$it['estimated_ptr'] * 1.3, 2, '.', '') ?>" min="0" step="0.01">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<select name="items[<?= $idx ?>][cgst_percent]" class="form-select form-select-sm text-center line-gst" onchange="recalculateGrnTotals();">
|
||||
<option value="6" <?= ((float)$it['gst_rate'] == 12) ? 'selected' : '' ?>>12%</option>
|
||||
<option value="2.5" <?= ((float)$it['gst_rate'] == 5) ? 'selected' : '' ?>>5%</option>
|
||||
<option value="9" <?= ((float)$it['gst_rate'] == 18) ? 'selected' : '' ?>>18%</option>
|
||||
<option value="0" <?= ((float)$it['gst_rate'] == 0) ? 'selected' : '' ?>>0%</option>
|
||||
</select>
|
||||
<input type="hidden" name="items[<?= $idx ?>][sgst_percent]" class="line-sgst" value="6">
|
||||
</td>
|
||||
<td class="text-end fw-semibold line-total-disp">₹0.00</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm py-0" onclick="removeGrnRow(this);">✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr id="grn-no-items">
|
||||
<td colspan="14" class="text-center py-4 text-muted">
|
||||
No items loaded. Select a PO above or use the Search box to add items.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="12" class="text-end">Taxable Amount:</th>
|
||||
<th class="text-end" id="grn-taxable-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="12" class="text-end">GST Tax Amount:</th>
|
||||
<th class="text-end" id="grn-tax-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr class="table-success">
|
||||
<th colspan="12" class="text-end fs-6">Net Inward Bill Total:</th>
|
||||
<th class="text-end fs-6 text-success fw-bold" id="grn-net-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-3 mt-3 bg-light border-0">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="post_to_inventory" id="post_to_inventory" value="1" checked>
|
||||
<label class="form-check-label fw-bold text-dark" for="post_to_inventory">
|
||||
Immediately Post Verified Quantities to Live Pharmacy Store Stock & Purchase Ledgers
|
||||
</label>
|
||||
<small class="d-block text-muted">Calls stored procedure <code>p_stock_update_purchase_id</code> to make batches immediately sellable.</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main');">Cancel</button>
|
||||
<button type="submit" class="btn btn-success px-4" id="btn-save-grn">
|
||||
<i class="bi bi-check-all"></i> Save & Complete Inward GRN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
if (window.jQuery && window.jQuery.ui && window.jQuery.ui.autocomplete) {
|
||||
initGrnDrugAutocomplete();
|
||||
} else {
|
||||
var s = document.createElement('script');
|
||||
s.src = '<?= base_url('assets/vendor/jquery-ui/jquery-ui.min.js') ?>';
|
||||
s.onload = initGrnDrugAutocomplete;
|
||||
document.body.appendChild(s);
|
||||
}
|
||||
|
||||
recalculateGrnTotals();
|
||||
})();
|
||||
|
||||
function initGrnDrugAutocomplete() {
|
||||
if (!window.jQuery || !window.jQuery.ui) return;
|
||||
var $ = window.jQuery;
|
||||
|
||||
$("#grn_drug_search").autocomplete({
|
||||
source: function(request, response) {
|
||||
$.getJSON("<?= base_url('Medical/get_drug') ?>", request, function(data) {
|
||||
response(data || []);
|
||||
});
|
||||
},
|
||||
minLength: 1,
|
||||
select: function(event, ui) {
|
||||
var item = ui.item || {};
|
||||
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;
|
||||
document.getElementById('btn-add-grn-row').disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handlePoSelection(poId) {
|
||||
if (poId && parseInt(poId) > 0) {
|
||||
load_form_div('<?= base_url('Medical/grn_new/') ?>' + poId, 'medical-main', 'Create GRN for PO');
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomItemToGrn() {
|
||||
var code = document.getElementById('grn_sel_item_code').value;
|
||||
var name = document.getElementById('grn_sel_item_name').value;
|
||||
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 packing = parseInt(document.getElementById('grn_sel_packing').value || 10);
|
||||
|
||||
if (!code || !name) {
|
||||
alert('Please search and select a medicine.');
|
||||
return;
|
||||
}
|
||||
if (!batch) {
|
||||
alert('Please enter a valid batch number for physical tracking.');
|
||||
document.getElementById('grn_sel_batch').focus();
|
||||
return;
|
||||
}
|
||||
if (!expiry) {
|
||||
alert('Please enter medicine expiry date.');
|
||||
document.getElementById('grn_sel_expiry').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var tbody = document.getElementById('grn-items-body');
|
||||
var noRow = document.getElementById('grn-no-items');
|
||||
if (noRow) noRow.remove();
|
||||
|
||||
var rowIdx = tbody.querySelectorAll('tr').length;
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="row-num">${rowIdx + 1}</td>
|
||||
<td>
|
||||
<strong>${escapeHtml(name)}</strong>
|
||||
<input type="hidden" name="items[${rowIdx}][item_code]" value="${code}">
|
||||
<input type="hidden" name="items[${rowIdx}][item_name]" value="${escapeHtml(name)}">
|
||||
<input type="hidden" name="items[${rowIdx}][po_item_id]" value="">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="items[${rowIdx}][batch_no]" class="form-control form-control-sm font-monospace line-batch" value="${escapeHtml(batch)}" required>
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" name="items[${rowIdx}][expiry_date]" class="form-control form-control-sm line-expiry" value="${expiry}" required>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][packing]" class="form-control form-control-sm text-center line-pack" value="${packing}" min="1">
|
||||
</td>
|
||||
<td class="text-center text-muted">0</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][received_packs]" class="form-control form-control-sm text-center line-rec" value="${packs}" min="1" onchange="recalculateGrnTotals();" required>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][free_packs]" class="form-control form-control-sm text-center line-free" value="0" min="0">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][rejected_packs]" class="form-control form-control-sm text-center line-rej" value="0" min="0">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[${rowIdx}][unit_purchase_rate]" class="form-control form-control-sm text-end line-rate" value="100.00" min="0" step="0.01" onchange="recalculateGrnTotals();">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[${rowIdx}][mrp]" class="form-control form-control-sm text-end line-mrp" value="130.00" min="0" step="0.01">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<select name="items[${rowIdx}][cgst_percent]" class="form-select form-select-sm text-center line-gst" onchange="recalculateGrnTotals();">
|
||||
<option value="6" selected>12%</option>
|
||||
<option value="2.5">5%</option>
|
||||
<option value="9">18%</option>
|
||||
<option value="0">0%</option>
|
||||
</select>
|
||||
<input type="hidden" name="items[${rowIdx}][sgst_percent]" class="line-sgst" value="6">
|
||||
</td>
|
||||
<td class="text-end fw-semibold line-total-disp">₹0.00</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm py-0" onclick="removeGrnRow(this);">✖</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
recalculateGrnTotals();
|
||||
|
||||
document.getElementById('grn_drug_search').value = '';
|
||||
document.getElementById('grn_sel_batch').value = '';
|
||||
document.getElementById('grn_sel_expiry').value = '';
|
||||
document.getElementById('btn-add-grn-row').disabled = true;
|
||||
document.getElementById('grn_drug_search').focus();
|
||||
}
|
||||
|
||||
function removeGrnRow(btn) {
|
||||
btn.closest('tr').remove();
|
||||
var tbody = document.getElementById('grn-items-body');
|
||||
var rows = tbody.querySelectorAll('tr');
|
||||
if (rows.length === 0) {
|
||||
tbody.innerHTML = '<tr id="grn-no-items"><td colspan="14" class="text-center py-4 text-muted">No items loaded.</td></tr>';
|
||||
} else {
|
||||
rows.forEach(function(r, idx) {
|
||||
var numTd = r.querySelector('.row-num');
|
||||
if (numTd) numTd.innerText = idx + 1;
|
||||
});
|
||||
}
|
||||
recalculateGrnTotals();
|
||||
}
|
||||
|
||||
function recalculateGrnTotals() {
|
||||
var rows = document.querySelectorAll('#grn-items-body tr:not(#grn-no-items)');
|
||||
var taxable = 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 lineSub = recPacks * rate;
|
||||
var lineTax = lineSub * (gstVal / 100);
|
||||
var lineTotal = lineSub + lineTax;
|
||||
|
||||
taxable += lineSub;
|
||||
totalTax += lineTax;
|
||||
|
||||
var totalDisp = r.querySelector('.line-total-disp');
|
||||
if (totalDisp) totalDisp.innerText = '₹' + lineTotal.toFixed(2);
|
||||
});
|
||||
|
||||
document.getElementById('grn-taxable-disp').innerText = '₹' + taxable.toFixed(2);
|
||||
document.getElementById('grn-tax-disp').innerText = '₹' + totalTax.toFixed(2);
|
||||
document.getElementById('grn-net-disp').innerText = '₹' + (taxable + totalTax).toFixed(2);
|
||||
}
|
||||
|
||||
function submitGrnForm() {
|
||||
var rows = document.querySelectorAll('#grn-items-body tr:not(#grn-no-items)');
|
||||
if (rows.length === 0) {
|
||||
alert('Please add at least one received item line.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that all batch and expiry dates are populated
|
||||
var missingBatch = false;
|
||||
rows.forEach(function(r) {
|
||||
var batch = r.querySelector('.line-batch')?.value.trim();
|
||||
var exp = r.querySelector('.line-expiry')?.value.trim();
|
||||
if (!batch || !exp) missingBatch = true;
|
||||
});
|
||||
|
||||
if (missingBatch) {
|
||||
alert('Please ensure every medicine row has a valid Batch No. and Expiration Date.');
|
||||
return;
|
||||
}
|
||||
|
||||
var form = document.getElementById('grn-create-form');
|
||||
var formData = new FormData(form);
|
||||
|
||||
var saveBtn = document.getElementById('btn-save-grn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing Inward Stock...';
|
||||
|
||||
fetch('<?= base_url('Medical/grn_create') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: formData
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-all"></i> Save & Complete Inward GRN';
|
||||
|
||||
if (data.status) {
|
||||
alert(data.message);
|
||||
load_form_div('<?= base_url('Medical/grn_list') ?>', 'medical-main', 'GRN Register :Pharmacy');
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Failed to save GRN.'));
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-all"></i> Save & Complete Inward GRN';
|
||||
alert('Server communication error.');
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
172
app/Views/medical/grn_list.php
Normal file
172
app/Views/medical/grn_list.php
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<div class="pagetitle d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0 text-primary"><i class="bi bi-box-seam"></i> Goods Received Notes (GRN)</h4>
|
||||
<small class="text-muted">Inward Stock Inspection & Physical Receipt Verification</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/main_store') ?>','medical-main','Store Main :Pharmacy');" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Store Main
|
||||
</a>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/purchase_orders') ?>','medical-main','Purchase Orders :Pharmacy');" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-file-earmark-text"></i> Purchase Orders
|
||||
</a>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/grn_new') ?>','medical-main','Create GRN :Pharmacy');" class="btn btn-success btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Create Inward GRN
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 shadow-sm border-0">
|
||||
<div class="card-body p-3">
|
||||
<form method="get" id="grn-filter-form" class="row g-2 align-items-center" onsubmit="event.preventDefault(); applyGrnFilter();">
|
||||
<div class="col-md-3">
|
||||
<input type="text" id="grn_search_text" class="form-control form-control-sm" placeholder="Search GRN #, Bill #, PO #..." value="<?= esc($search ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="grn_filter_supplier" class="form-select form-select-sm">
|
||||
<option value="0">All Suppliers</option>
|
||||
<?php if (!empty($suppliers)): ?>
|
||||
<?php foreach ($suppliers as $s): ?>
|
||||
<option value="<?= (int)$s['sid'] ?>" <?= ($supplierId == $s['sid']) ? 'selected' : '' ?>>
|
||||
<?= esc($s['name_supplier']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select id="grn_filter_status" class="form-select form-select-sm">
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="draft" <?= ($status === 'draft') ? 'selected' : '' ?>>Draft</option>
|
||||
<option value="verified" <?= ($status === 'verified') ? 'selected' : '' ?>>Verified</option>
|
||||
<option value="posted_to_inventory" <?= ($status === 'posted_to_inventory') ? 'selected' : '' ?>>Posted to Live Stock</option>
|
||||
<option value="cancelled" <?= ($status === 'cancelled') ? 'selected' : '' ?>>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" id="grn_filter_from" class="form-control form-control-sm" value="<?= esc($fromDate ?? '') ?>" title="From Date">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-dark btn-sm w-100"><i class="bi bi-funnel"></i> Filter</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetGrnFilter()"><i class="bi bi-arrow-counterclockwise"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped align-middle mb-0" id="grn-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>GRN Number</th>
|
||||
<th>Supplier / Vendor</th>
|
||||
<th>Supplier Bill / Challan</th>
|
||||
<th>GRN Date</th>
|
||||
<th>Linked PO</th>
|
||||
<th class="text-end">Net Amount</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th class="text-end" style="min-width: 170px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($grnList)): ?>
|
||||
<?php foreach ($grnList as $idx => $row): ?>
|
||||
<?php
|
||||
$statusBadge = 'bg-secondary';
|
||||
if ($row['status'] === 'draft') $statusBadge = 'bg-warning text-dark';
|
||||
elseif ($row['status'] === 'verified') $statusBadge = 'bg-info text-dark';
|
||||
elseif ($row['status'] === 'posted_to_inventory') $statusBadge = 'bg-success';
|
||||
elseif ($row['status'] === 'cancelled') $statusBadge = 'bg-danger';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td>
|
||||
<strong class="text-primary font-monospace"><?= esc($row['grn_number']) ?></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc($row['name_supplier'] ?: 'Unknown Vendor') ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><strong>Inv:</strong> <?= esc($row['supplier_invoice_no']) ?></div>
|
||||
<?php if (!empty($row['challan_no'])): ?>
|
||||
<small class="text-muted">Challan: <?= esc($row['challan_no']) ?></small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= !empty($row['grn_date']) ? date('d-M-Y', strtotime($row['grn_date'])) : '-' ?></td>
|
||||
<td>
|
||||
<?php if (!empty($row['po_number'])): ?>
|
||||
<span class="badge bg-light text-primary border"><?= esc($row['po_number']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Direct Intake</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end fw-bold">₹<?= number_format((float)$row['net_amount'], 2) ?></td>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= $statusBadge ?> px-2 py-1 text-uppercase" style="font-size: 11px;">
|
||||
<?= str_replace('_', ' ', esc($row['status'])) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="<?= base_url('Medical/grn_print/' . $row['grn_id']) ?>" target="_blank" class="btn btn-outline-secondary" title="Print GRN Slip">
|
||||
<i class="bi bi-printer"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="load_form_div('<?= base_url('Medical/grn_edit/' . $row['grn_id']) ?>','medical-main','GRN Inspection Detail');" title="View Inspection Details">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</button>
|
||||
<?php if ($row['status'] === 'verified'): ?>
|
||||
<button type="button" class="btn btn-success" onclick="postGrnToStock(<?= (int)$row['grn_id'] ?>)" title="Post to Live Hospital Stock">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Post Stock
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox fs-3 d-block mb-1"></i>
|
||||
No Goods Received Notes found. Click <strong>Create Inward GRN</strong> to record incoming material.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyGrnFilter() {
|
||||
var search = document.getElementById('grn_search_text').value;
|
||||
var supplier = document.getElementById('grn_filter_supplier').value;
|
||||
var status = document.getElementById('grn_filter_status').value;
|
||||
var from = document.getElementById('grn_filter_from').value;
|
||||
|
||||
var url = '<?= base_url('Medical/grn_list') ?>?q=' + encodeURIComponent(search)
|
||||
+ '&supplier_id=' + encodeURIComponent(supplier)
|
||||
+ '&status=' + encodeURIComponent(status)
|
||||
+ '&from=' + encodeURIComponent(from);
|
||||
|
||||
load_form_div(url, 'medical-main', 'GRN Register :Pharmacy');
|
||||
}
|
||||
|
||||
function resetGrnFilter() {
|
||||
load_form_div('<?= base_url('Medical/grn_list') ?>', 'medical-main', 'GRN Register :Pharmacy');
|
||||
}
|
||||
|
||||
function postGrnToStock(grnId) {
|
||||
if (!confirm('Are you sure you want to verify and post this GRN into live pharmacy inventory? This will update physical stock quantities and supplier purchase ledgers.')) return;
|
||||
|
||||
$.post('<?= base_url('Medical/grn_post_inventory/') ?>' + grnId, {}, function(data) {
|
||||
alert(data.message || 'Stock posted');
|
||||
if (data.status) applyGrnFilter();
|
||||
}, 'json').fail(function() {
|
||||
alert('Error posting GRN to inventory.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
133
app/Views/medical/grn_print_pdf.php
Normal file
133
app/Views/medical/grn_print_pdf.php
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 10px; color: #222; }
|
||||
.header { text-align: center; border-bottom: 2px solid #166534; padding-bottom: 8px; margin-bottom: 12px; }
|
||||
.header h2 { margin: 0; color: #166534; text-transform: uppercase; font-size: 17px; }
|
||||
.header p { margin: 2px 0 0; color: #555; font-size: 10px; }
|
||||
.title-bar { background-color: #f0fdf4; padding: 6px 10px; margin-bottom: 12px; border-left: 4px solid #166534; }
|
||||
.title-bar table { width: 100%; border-collapse: collapse; }
|
||||
.meta-table { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 10px; }
|
||||
.meta-table td { padding: 4px 6px; vertical-align: top; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
||||
.items-table th { background-color: #166534; color: #fff; padding: 5px 6px; text-align: left; font-size: 9px; text-transform: uppercase; border: 1px solid #166534; }
|
||||
.items-table td { padding: 5px 6px; border: 1px solid #cbd5e1; font-size: 9px; }
|
||||
.text-end { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.totals-table { width: 40%; margin-left: auto; border-collapse: collapse; margin-bottom: 20px; }
|
||||
.totals-table td { padding: 4px 8px; border-bottom: 1px solid #cbd5e1; font-size: 10px; }
|
||||
.grand-total { font-size: 12px; font-weight: bold; background-color: #f0fdf4; color: #166534; }
|
||||
.signatures { width: 100%; margin-top: 30px; }
|
||||
.signatures td { width: 33%; text-align: center; font-size: 10px; }
|
||||
.sig-line { border-top: 1px solid #333; margin-top: 30px; padding-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h2>Hospital Pharmacy Services</h2>
|
||||
<p>Inward Goods Physical Inspection & Receiving Quality Control</p>
|
||||
</div>
|
||||
|
||||
<div class="title-bar">
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong style="font-size: 13px; color: #166534;">GOODS RECEIVED NOTE (GRN)</strong></td>
|
||||
<td class="text-end"><strong>GRN NO:</strong> <span style="font-family: monospace; font-size: 12px; color: #166534;"><?= esc($grn['grn_number']) ?></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td style="width: 50%; border: 1px solid #e2e8f0; padding: 6px;">
|
||||
<strong style="color: #64748b; font-size: 8px; text-transform: uppercase;">SUPPLIER DETAILS</strong><br>
|
||||
<strong style="font-size: 11px; color: #0f172a;"><?= esc($grn['name_supplier'] ?: 'Supplier') ?></strong><br>
|
||||
<?php $loc = implode(', ', array_filter([$grn['city'] ?? '', $grn['state'] ?? ''])); if (!empty($loc)): ?><?= esc($loc) ?><br><?php endif; ?>
|
||||
Phone: <?= esc($grn['contact_no'] ?: '-') ?><br>
|
||||
<?php if (!empty($grn['gst_no'])): ?>GSTIN: <?= esc($grn['gst_no']) ?><?php endif; ?>
|
||||
</td>
|
||||
<td style="width: 50%; border: 1px solid #e2e8f0; padding: 6px;">
|
||||
<strong style="color: #64748b; font-size: 8px; text-transform: uppercase;">DELIVERY METADATA</strong><br>
|
||||
<strong>Supplier Bill #:</strong> <?= esc($grn['supplier_invoice_no']) ?> | <strong>Bill Date:</strong> <?= date('d-M-Y', strtotime($grn['invoice_date'])) ?><br>
|
||||
<strong>GRN Receipt Date:</strong> <?= date('d-M-Y', strtotime($grn['grn_date'])) ?><br>
|
||||
<strong>Linked PO #:</strong> <?= esc($grn['po_number'] ?: 'Direct Delivery') ?><br>
|
||||
<strong>Status:</strong> <span style="text-transform: uppercase; font-weight: bold;"><?= esc($grn['status']) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25px;">#</th>
|
||||
<th>Item Name</th>
|
||||
<th style="width: 75px;">Batch No</th>
|
||||
<th style="width: 65px;">Expiry</th>
|
||||
<th class="text-center" style="width: 45px;">Pack</th>
|
||||
<th class="text-center" style="width: 50px;">Ordered</th>
|
||||
<th class="text-center" style="width: 50px;">Received</th>
|
||||
<th class="text-center" style="width: 40px;">Free</th>
|
||||
<th class="text-center" style="width: 45px;">Damaged</th>
|
||||
<th class="text-end" style="width: 65px;">Rate/Pack</th>
|
||||
<th class="text-end" style="width: 60px;">MRP</th>
|
||||
<th class="text-end" style="width: 75px;">Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($items)): ?>
|
||||
<?php foreach ($items as $idx => $it): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td><strong><?= esc($it['item_name']) ?></strong></td>
|
||||
<td><?= esc($it['batch_no']) ?></td>
|
||||
<td><?= date('d-M-Y', strtotime($it['expiry_date'])) ?></td>
|
||||
<td class="text-center"><?= (int)$it['packing'] ?>s</td>
|
||||
<td class="text-center"><?= (float)$it['order_packs'] ?></td>
|
||||
<td class="text-center"><strong><?= (float)$it['received_packs'] ?></strong></td>
|
||||
<td class="text-center"><?= (float)$it['free_packs'] ?></td>
|
||||
<td class="text-center <?= ((float)$it['rejected_packs'] > 0) ? 'style=\"color:red;\"' : '' ?>">
|
||||
<?= (float)$it['rejected_packs'] ?>
|
||||
</td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['unit_purchase_rate'], 2) ?></td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['mrp'], 2) ?></td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['total_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="totals-table">
|
||||
<tr>
|
||||
<td>Taxable Amount:</td>
|
||||
<td class="text-end">₹<?= number_format((float)$grn['total_received_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GST Tax:</td>
|
||||
<td class="text-end">₹<?= number_format((float)$grn['total_tax_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<tr class="grand-total">
|
||||
<td>Net Payable Total:</td>
|
||||
<td class="text-end">₹<?= number_format((float)$grn['net_amount'], 2) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="signatures">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="sig-line">Physical Receiver (Store Assistant)</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="sig-line">Inspected & Quality Checked (Pharmacist)</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="sig-line">Store In-Charge Approval</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -59,9 +59,19 @@
|
|||
<div class="card-header">Inventory</div>
|
||||
<div class="card-body pt-3">
|
||||
<ul class="mb-0" style="list-style: disc;">
|
||||
<li class="mb-3">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/purchase_orders') ?>','medical-main','Purchase Orders :Pharmacy');">
|
||||
<i class="bi bi-file-earmark-text"></i> Purchase Order (PO)
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main','GRN Register :Pharmacy');">
|
||||
<i class="bi bi-box-seam"></i> Goods Received Note (GRN)
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/Purchase') ?>','medical-main','Purchase :Pharmacy');">
|
||||
<i class="bi bi-hand-index-thumb"></i> Purchase
|
||||
<i class="bi bi-hand-index-thumb"></i> Purchase Invoice
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
464
app/Views/medical/po_form.php
Normal file
464
app/Views/medical/po_form.php
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
<?php
|
||||
$isEdit = !empty($po);
|
||||
$formAction = $isEdit ? base_url('Medical/po_create') : base_url('Medical/po_create'); // we handle create / update
|
||||
$poNumber = $isEdit ? ($po['po_number'] ?? '') : ($nextPoNumber ?? 'PO-NEW');
|
||||
$supplierId = (int)($po['supplier_id'] ?? 0);
|
||||
$storeId = (int)($po['store_id'] ?? 1);
|
||||
$poDate = !empty($po['po_date']) ? $po['po_date'] : date('Y-m-d');
|
||||
$expDelivery = !empty($po['expected_delivery_date']) ? $po['expected_delivery_date'] : date('Y-m-d', strtotime('+7 days'));
|
||||
$paymentTerms = $po['payment_terms'] ?? 'Net 30 Days';
|
||||
$remarks = $po['remarks'] ?? '';
|
||||
$status = $po['status'] ?? 'ordered';
|
||||
?>
|
||||
|
||||
<div class="pagetitle d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0 text-primary">
|
||||
<i class="bi bi-file-earmark-plus"></i> <?= $isEdit ? 'Edit Purchase Order' : 'New Purchase Order' ?>
|
||||
<span class="badge bg-light text-dark border ms-2 font-monospace"><?= esc($poNumber) ?></span>
|
||||
</h4>
|
||||
<small class="text-muted">Procurement Requisition & Supplier Order</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/purchase_orders') ?>','medical-main','Purchase Orders :Pharmacy');" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to PO List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-3">
|
||||
<div class="card-header bg-light py-2">
|
||||
<strong class="text-secondary"><i class="bi bi-info-circle"></i> Purchase Order Details</strong>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<form id="po-create-form" onsubmit="event.preventDefault(); submitPoForm();">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label form-label-sm fw-semibold">Supplier / Pharmaceutical Vendor <span class="text-danger">*</span></label>
|
||||
<select name="supplier_id" id="po_supplier_id" class="form-select form-select-sm" required onchange="handleSupplierChange(this.value);">
|
||||
<option value="">-- Select Supplier --</option>
|
||||
<?php if (!empty($suppliers)): ?>
|
||||
<?php foreach ($suppliers as $s): ?>
|
||||
<option value="<?= (int)$s['sid'] ?>" <?= ($supplierId == $s['sid']) ? 'selected' : '' ?>>
|
||||
<?= esc($s['name_supplier']) ?> <?= !empty($s['contact_no']) ? '(' . esc($s['contact_no']) . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">PO Date</label>
|
||||
<input type="date" name="po_date" id="po_date" class="form-control form-control-sm" value="<?= esc($poDate) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">Expected Delivery</label>
|
||||
<input type="date" name="expected_delivery_date" id="expected_delivery_date" class="form-control form-control-sm" value="<?= esc($expDelivery) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">Payment Terms</label>
|
||||
<select name="payment_terms" id="payment_terms" class="form-select form-select-sm">
|
||||
<option value="Net 30 Days" <?= ($paymentTerms === 'Net 30 Days') ? 'selected' : '' ?>>Net 30 Days</option>
|
||||
<option value="Net 15 Days" <?= ($paymentTerms === 'Net 15 Days') ? 'selected' : '' ?>>Net 15 Days</option>
|
||||
<option value="Net 45 Days" <?= ($paymentTerms === 'Net 45 Days') ? 'selected' : '' ?>>Net 45 Days</option>
|
||||
<option value="Cash On Delivery (COD)" <?= ($paymentTerms === 'Cash On Delivery (COD)') ? 'selected' : '' ?>>Cash On Delivery (COD)</option>
|
||||
<option value="Advance" <?= ($paymentTerms === 'Advance') ? 'selected' : '' ?>>Advance</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm fw-semibold">Initial Status</label>
|
||||
<select name="status" id="po_status" class="form-select form-select-sm">
|
||||
<option value="ordered" <?= ($status === 'ordered') ? 'selected' : '' ?>>Approved & Issued</option>
|
||||
<option value="draft" <?= ($status === 'draft') ? 'selected' : '' ?>>Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label form-label-sm fw-semibold">Notes / Special Instructions</label>
|
||||
<input type="text" name="remarks" id="po_remarks" class="form-control form-control-sm" placeholder="e.g., Deliver to Main Pharmacy Warehouse before 5 PM" value="<?= esc($remarks) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Entry Panel -->
|
||||
<div class="card border-primary mt-3">
|
||||
<div class="card-header bg-primary text-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-capsule"></i> Add Items to Purchase Order</span>
|
||||
<button type="button" class="btn btn-warning btn-sm py-0" id="btn-reorder-fetch" onclick="fetchSupplierReorderItems();" style="display:none;">
|
||||
<i class="bi bi-lightning-charge"></i> Auto-Add Low Stock Items for this Vendor
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Search Medicine / Drug Master</label>
|
||||
<input type="text" id="quick_drug_search" class="form-control form-control-sm" placeholder="Type medicine brand name or generic salt..." autocomplete="off">
|
||||
<input type="hidden" id="selected_item_id" value="">
|
||||
<input type="hidden" id="selected_item_name" value="">
|
||||
<input type="hidden" id="selected_generic_name" value="">
|
||||
<input type="hidden" id="selected_category" value="">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Pack Size (Units)</label>
|
||||
<input type="number" id="quick_units_per_pack" class="form-control form-control-sm" value="10" min="1" step="1">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Order Packs</label>
|
||||
<input type="number" id="quick_order_packs" class="form-control form-control-sm" value="10" min="1" step="1">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1 fw-semibold">Est. Rate / Pack (₹)</label>
|
||||
<input type="number" id="quick_ptr" class="form-control form-control-sm" value="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-primary btn-sm w-100" id="btn-add-line" onclick="addItemToPoTable();" disabled>
|
||||
<i class="bi bi-plus-lg"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm align-middle mb-0" id="po-items-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th>Product Name</th>
|
||||
<th>Generic / Salt</th>
|
||||
<th>Formulation</th>
|
||||
<th class="text-center" style="width: 100px;">Units/Pack</th>
|
||||
<th class="text-center" style="width: 110px;">Order Packs</th>
|
||||
<th class="text-end" style="width: 120px;">Est. Rate (₹)</th>
|
||||
<th class="text-center" style="width: 90px;">GST %</th>
|
||||
<th class="text-end" style="width: 130px;">Line Total (₹)</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="po-items-body">
|
||||
<?php if (!empty($poItems)): ?>
|
||||
<?php foreach ($poItems as $idx => $it): ?>
|
||||
<tr data-item-id="<?= (int)$it['item_id'] ?>">
|
||||
<td class="row-num"><?= $idx + 1 ?></td>
|
||||
<td>
|
||||
<strong><?= esc($it['item_name']) ?></strong>
|
||||
<input type="hidden" name="items[<?= $idx ?>][item_id]" value="<?= (int)$it['item_id'] ?>">
|
||||
<input type="hidden" name="items[<?= $idx ?>][item_name]" value="<?= esc($it['item_name']) ?>">
|
||||
</td>
|
||||
<td>
|
||||
<?= esc($it['generic_name'] ?: '-') ?>
|
||||
<input type="hidden" name="items[<?= $idx ?>][generic_name]" value="<?= esc($it['generic_name']) ?>">
|
||||
</td>
|
||||
<td>
|
||||
<?= esc($it['category'] ?: 'Tablet') ?>
|
||||
<input type="hidden" name="items[<?= $idx ?>][category]" value="<?= esc($it['category']) ?>">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][units_per_pack]" class="form-control form-control-sm text-center line-units" value="<?= (int)$it['units_per_pack'] ?>" min="1">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[<?= $idx ?>][order_packs]" class="form-control form-control-sm text-center line-packs" value="<?= (float)$it['order_packs'] ?>" min="1" onchange="recalculatePoTotals();">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[<?= $idx ?>][estimated_ptr]" class="form-control form-control-sm text-end line-rate" value="<?= number_format((float)$it['estimated_ptr'], 2, '.', '') ?>" min="0" step="0.01" onchange="recalculatePoTotals();">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<select name="items[<?= $idx ?>][gst_rate]" class="form-select form-select-sm text-center line-gst" onchange="recalculatePoTotals();">
|
||||
<option value="0" <?= ((float)$it['gst_rate'] == 0) ? 'selected' : '' ?>>0%</option>
|
||||
<option value="5" <?= ((float)$it['gst_rate'] == 5) ? 'selected' : '' ?>>5%</option>
|
||||
<option value="12" <?= ((float)$it['gst_rate'] == 12) ? 'selected' : '' ?>>12%</option>
|
||||
<option value="18" <?= ((float)$it['gst_rate'] == 18) ? 'selected' : '' ?>>18%</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-end fw-semibold line-total-display">
|
||||
₹<?= number_format((float)$it['estimated_amount'], 2) ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm py-0" onclick="removePoRow(this);">✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr id="no-items-row">
|
||||
<td colspan="10" class="text-center py-4 text-muted">
|
||||
No items added yet. Search above to add items or auto-load low stock items.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="8" class="text-end">Estimated Subtotal:</th>
|
||||
<th class="text-end" id="po-subtotal-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="8" class="text-end">Estimated GST Tax:</th>
|
||||
<th class="text-end" id="po-tax-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr class="table-primary">
|
||||
<th colspan="8" class="text-end fs-6">Grand Total Commitment:</th>
|
||||
<th class="text-end fs-6 text-primary fw-bold" id="po-grand-total-disp">₹0.00</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="load_form_div('<?= base_url('Medical/purchase_orders') ?>','medical-main');">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary px-4" id="btn-save-po">
|
||||
<i class="bi bi-check-circle"></i> Save & Generate Purchase Order
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Ensure jQuery UI for drug autocomplete
|
||||
if (window.jQuery && window.jQuery.ui && window.jQuery.ui.autocomplete) {
|
||||
initPoDrugAutocomplete();
|
||||
} else {
|
||||
var s = document.createElement('script');
|
||||
s.src = '<?= base_url('assets/vendor/jquery-ui/jquery-ui.min.js') ?>';
|
||||
s.onload = initPoDrugAutocomplete;
|
||||
document.body.appendChild(s);
|
||||
}
|
||||
|
||||
recalculatePoTotals();
|
||||
})();
|
||||
|
||||
function initPoDrugAutocomplete() {
|
||||
if (!window.jQuery || !window.jQuery.ui) return;
|
||||
var $ = window.jQuery;
|
||||
|
||||
$("#quick_drug_search").autocomplete({
|
||||
source: function(request, response) {
|
||||
$.getJSON("<?= base_url('Medical/get_drug') ?>", request, 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_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('btn-add-line').disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleSupplierChange(suppId) {
|
||||
var reorderBtn = document.getElementById('btn-reorder-fetch');
|
||||
if (suppId && parseInt(suppId) > 0) {
|
||||
reorderBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
reorderBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function addItemToPoTable(prefill) {
|
||||
var item = prefill || {
|
||||
item_id: document.getElementById('selected_item_id').value,
|
||||
item_name: document.getElementById('selected_item_name').value,
|
||||
generic_name: document.getElementById('selected_generic_name').value,
|
||||
category: document.getElementById('selected_category').value || 'Tablet',
|
||||
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
|
||||
};
|
||||
|
||||
if (!item.item_id || !item.item_name) {
|
||||
alert('Please search and select a medicine from master first.');
|
||||
return;
|
||||
}
|
||||
|
||||
var tbody = document.getElementById('po-items-body');
|
||||
var noRow = document.getElementById('no-items-row');
|
||||
if (noRow) noRow.remove();
|
||||
|
||||
var existingRow = tbody.querySelector('tr[data-item-id="' + item.item_id + '"]');
|
||||
if (existingRow) {
|
||||
var packsInput = existingRow.querySelector('.line-packs');
|
||||
packsInput.value = parseFloat(packsInput.value || 0) + parseFloat(item.order_packs);
|
||||
recalculatePoTotals();
|
||||
resetQuickAddInputs();
|
||||
return;
|
||||
}
|
||||
|
||||
var rowIdx = tbody.querySelectorAll('tr').length;
|
||||
var tr = document.createElement('tr');
|
||||
tr.setAttribute('data-item-id', item.item_id);
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="row-num">${rowIdx + 1}</td>
|
||||
<td>
|
||||
<strong>${escapeHtml(item.item_name)}</strong>
|
||||
<input type="hidden" name="items[${rowIdx}][item_id]" value="${item.item_id}">
|
||||
<input type="hidden" name="items[${rowIdx}][item_name]" value="${escapeHtml(item.item_name)}">
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(item.generic_name || '-')}
|
||||
<input type="hidden" name="items[${rowIdx}][generic_name]" value="${escapeHtml(item.generic_name || '')}">
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(item.category || 'Tablet')}
|
||||
<input type="hidden" name="items[${rowIdx}][category]" value="${escapeHtml(item.category || '')}">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][units_per_pack]" class="form-control form-control-sm text-center line-units" value="${item.units_per_pack}" min="1">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="number" name="items[${rowIdx}][order_packs]" class="form-control form-control-sm text-center line-packs" value="${item.order_packs}" min="1" onchange="recalculatePoTotals();">
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<input type="number" name="items[${rowIdx}][estimated_ptr]" class="form-control form-control-sm text-end line-rate" value="${parseFloat(item.estimated_ptr).toFixed(2)}" min="0" step="0.01" onchange="recalculatePoTotals();">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<select name="items[${rowIdx}][gst_rate]" class="form-select form-select-sm text-center line-gst" onchange="recalculatePoTotals();">
|
||||
<option value="0" ${item.gst_rate == 0 ? 'selected' : ''}>0%</option>
|
||||
<option value="5" ${item.gst_rate == 5 ? 'selected' : ''}>5%</option>
|
||||
<option value="12" ${item.gst_rate == 12 ? 'selected' : ''}>12%</option>
|
||||
<option value="18" ${item.gst_rate == 18 ? 'selected' : ''}>18%</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-end fw-semibold line-total-display">₹0.00</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm py-0" onclick="removePoRow(this);">✖</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
recalculatePoTotals();
|
||||
resetQuickAddInputs();
|
||||
}
|
||||
|
||||
function resetQuickAddInputs() {
|
||||
document.getElementById('quick_drug_search').value = '';
|
||||
document.getElementById('selected_item_id').value = '';
|
||||
document.getElementById('selected_item_name').value = '';
|
||||
document.getElementById('selected_generic_name').value = '';
|
||||
document.getElementById('btn-add-line').disabled = true;
|
||||
document.getElementById('quick_drug_search').focus();
|
||||
}
|
||||
|
||||
function removePoRow(btn) {
|
||||
btn.closest('tr').remove();
|
||||
var tbody = document.getElementById('po-items-body');
|
||||
var rows = tbody.querySelectorAll('tr');
|
||||
if (rows.length === 0) {
|
||||
tbody.innerHTML = '<tr id="no-items-row"><td colspan="10" class="text-center py-4 text-muted">No items added yet.</td></tr>';
|
||||
} else {
|
||||
rows.forEach(function(r, idx) {
|
||||
var numTd = r.querySelector('.row-num');
|
||||
if (numTd) numTd.innerText = idx + 1;
|
||||
});
|
||||
}
|
||||
recalculatePoTotals();
|
||||
}
|
||||
|
||||
function recalculatePoTotals() {
|
||||
var rows = document.querySelectorAll('#po-items-body tr:not(#no-items-row)');
|
||||
var subtotal = 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 gst = parseFloat(r.querySelector('.line-gst')?.value || 0);
|
||||
|
||||
var lineSub = packs * rate;
|
||||
var lineTax = lineSub * (gst / 100);
|
||||
var lineTotal = lineSub + lineTax;
|
||||
|
||||
subtotal += lineSub;
|
||||
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-tax-disp').innerText = '₹' + totalTax.toFixed(2);
|
||||
document.getElementById('po-grand-total-disp').innerText = '₹' + (subtotal + totalTax).toFixed(2);
|
||||
}
|
||||
|
||||
function fetchSupplierReorderItems() {
|
||||
var suppId = document.getElementById('po_supplier_id').value;
|
||||
if (!suppId) return;
|
||||
|
||||
fetch('<?= base_url('Medical/po_reorder_items/') ?>' + suppId)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status && data.items && data.items.length > 0) {
|
||||
var added = 0;
|
||||
data.items.forEach(function(it) {
|
||||
addItemToPoTable({
|
||||
item_id: it.item_id,
|
||||
item_name: it.item_name,
|
||||
generic_name: it.generic_name,
|
||||
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
|
||||
});
|
||||
added++;
|
||||
});
|
||||
alert('Added ' + added + ' low stock items to PO.');
|
||||
} else {
|
||||
alert('No low-stock items detected for this supplier.');
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Error fetching supplier items.'));
|
||||
}
|
||||
|
||||
function submitPoForm() {
|
||||
var form = document.getElementById('po-create-form');
|
||||
var formData = new FormData(form);
|
||||
|
||||
var rows = document.querySelectorAll('#po-items-body tr:not(#no-items-row)');
|
||||
if (rows.length === 0) {
|
||||
alert('Please add at least one medicine line to the purchase order.');
|
||||
return;
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('btn-save-po');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Saving...';
|
||||
|
||||
fetch('<?= base_url('Medical/po_create') ?>', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body: formData
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-circle"></i> Save & Generate Purchase Order';
|
||||
|
||||
if (data.status) {
|
||||
alert(data.message);
|
||||
load_form_div('<?= base_url('Medical/purchase_orders') ?>', 'medical-main', 'Purchase Orders :Pharmacy');
|
||||
} else {
|
||||
alert('Error: ' + (data.message || 'Unable to save Purchase Order.'));
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="bi bi-check-circle"></i> Save & Generate Purchase Order';
|
||||
alert('Server communication error.');
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
191
app/Views/medical/po_list.php
Normal file
191
app/Views/medical/po_list.php
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<div class="pagetitle d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h4 class="mb-0 text-primary"><i class="bi bi-file-earmark-text"></i> Pharmacy Purchase Orders (PO)</h4>
|
||||
<small class="text-muted">Procurement & Supplier Requisitions</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/main_store') ?>','medical-main','Store Main :Pharmacy');" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Store Main
|
||||
</a>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/grn_list') ?>','medical-main','GRN Register :Pharmacy');" class="btn btn-outline-info btn-sm">
|
||||
<i class="bi bi-box-seam"></i> GRN Register
|
||||
</a>
|
||||
<a href="javascript:load_form_div('<?= base_url('Medical/po_new') ?>','medical-main','New Purchase Order :Pharmacy');" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle"></i> Create New PO
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 shadow-sm border-0">
|
||||
<div class="card-body p-3">
|
||||
<form method="get" id="po-filter-form" class="row g-2 align-items-center" onsubmit="event.preventDefault(); applyPoFilter();">
|
||||
<div class="col-md-3">
|
||||
<input type="text" id="po_search_text" class="form-control form-control-sm" placeholder="Search PO # or Supplier..." value="<?= esc($search ?? '') ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select id="po_filter_supplier" class="form-select form-select-sm">
|
||||
<option value="0">All Suppliers</option>
|
||||
<?php if (!empty($suppliers)): ?>
|
||||
<?php foreach ($suppliers as $s): ?>
|
||||
<option value="<?= (int)$s['sid'] ?>" <?= ($supplierId == $s['sid']) ? 'selected' : '' ?>>
|
||||
<?= esc($s['name_supplier']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select id="po_filter_status" class="form-select form-select-sm">
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="draft" <?= ($status === 'draft') ? 'selected' : '' ?>>Draft</option>
|
||||
<option value="ordered" <?= ($status === 'ordered') ? 'selected' : '' ?>>Ordered / Approved</option>
|
||||
<option value="partially_received" <?= ($status === 'partially_received') ? 'selected' : '' ?>>Partially Received</option>
|
||||
<option value="received" <?= ($status === 'received') ? 'selected' : '' ?>>Received / Completed</option>
|
||||
<option value="cancelled" <?= ($status === 'cancelled') ? 'selected' : '' ?>>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" id="po_filter_from" class="form-control form-control-sm" value="<?= esc($fromDate ?? '') ?>" title="From Date">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-dark btn-sm w-100"><i class="bi bi-funnel"></i> Filter</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetPoFilter()"><i class="bi bi-arrow-counterclockwise"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped align-middle mb-0" id="po-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>PO Number</th>
|
||||
<th>Supplier</th>
|
||||
<th>PO Date</th>
|
||||
<th>Delivery Due</th>
|
||||
<th class="text-center">Items</th>
|
||||
<th class="text-end">Total Amount</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th class="text-end" style="min-width: 170px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($orders)): ?>
|
||||
<?php foreach ($orders as $idx => $row): ?>
|
||||
<?php
|
||||
$statusBadge = 'bg-secondary';
|
||||
if ($row['status'] === 'draft') $statusBadge = 'bg-warning text-dark';
|
||||
elseif ($row['status'] === 'ordered' || $row['status'] === 'approved') $statusBadge = 'bg-primary';
|
||||
elseif ($row['status'] === 'partially_received') $statusBadge = 'bg-info text-dark';
|
||||
elseif ($row['status'] === 'received') $statusBadge = 'bg-success';
|
||||
elseif ($row['status'] === 'cancelled') $statusBadge = 'bg-danger';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $idx + 1 ?></td>
|
||||
<td>
|
||||
<strong class="text-primary font-monospace"><?= esc($row['po_number']) ?></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc($row['name_supplier'] ?: 'Unknown Vendor') ?></div>
|
||||
<small class="text-muted"><?= esc($row['supplier_phone'] ?: '') ?></small>
|
||||
</td>
|
||||
<td><?= !empty($row['po_date']) ? date('d-M-Y', strtotime($row['po_date'])) : '-' ?></td>
|
||||
<td><?= !empty($row['expected_delivery_date']) ? date('d-M-Y', strtotime($row['expected_delivery_date'])) : '<span class="text-muted">-</span>' ?></td>
|
||||
<td class="text-center"><span class="badge bg-light text-dark border"><?= (int)$row['total_items'] ?></span></td>
|
||||
<td class="text-end fw-bold">₹<?= number_format((float)$row['grand_total'] ?: (float)$row['total_estimated_amount'], 2) ?></td>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= $statusBadge ?> px-2 py-1 text-uppercase" style="font-size: 11px;">
|
||||
<?= str_replace('_', ' ', esc($row['status'])) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="<?= base_url('Medical/po_print/' . $row['po_id']) ?>" target="_blank" class="btn btn-outline-secondary" title="Print PO PDF">
|
||||
<i class="bi bi-printer"></i>
|
||||
</a>
|
||||
<?php if ($row['status'] === 'draft'): ?>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="load_form_div('<?= base_url('Medical/po_edit/' . $row['po_id']) ?>','medical-main','Edit PO');" title="Edit Draft">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success" onclick="updatePoStatus(<?= (int)$row['po_id'] ?>, 'ordered')" title="Approve & Issue PO">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($row['status'] === 'ordered' || $row['status'] === 'partially_received'): ?>
|
||||
<button type="button" class="btn btn-success" onclick="load_form_div('<?= base_url('Medical/grn_new/' . $row['po_id']) ?>','medical-main','Create GRN for PO');" title="Receive Goods (GRN)">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Receive
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($row['status'] !== 'cancelled' && $row['status'] !== 'received'): ?>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="cancelPo(<?= (int)$row['po_id'] ?>)" title="Cancel PO">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox fs-3 d-block mb-1"></i>
|
||||
No Purchase Orders found. Click <strong>Create New PO</strong> to draft your first procurement order.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyPoFilter() {
|
||||
var search = document.getElementById('po_search_text').value;
|
||||
var supplier = document.getElementById('po_filter_supplier').value;
|
||||
var status = document.getElementById('po_filter_status').value;
|
||||
var from = document.getElementById('po_filter_from').value;
|
||||
|
||||
var url = '<?= base_url('Medical/purchase_orders') ?>?q=' + encodeURIComponent(search)
|
||||
+ '&supplier_id=' + encodeURIComponent(supplier)
|
||||
+ '&status=' + encodeURIComponent(status)
|
||||
+ '&from=' + encodeURIComponent(from);
|
||||
|
||||
load_form_div(url, 'medical-main', 'Purchase Orders :Pharmacy');
|
||||
}
|
||||
|
||||
function resetPoFilter() {
|
||||
load_form_div('<?= base_url('Medical/purchase_orders') ?>', 'medical-main', 'Purchase Orders :Pharmacy');
|
||||
}
|
||||
|
||||
function updatePoStatus(poId, newStatus) {
|
||||
if (!confirm('Are you sure you want to approve & issue this purchase order to the vendor?')) return;
|
||||
|
||||
$.post('<?= base_url('Medical/po_status/') ?>' + poId, {
|
||||
status: newStatus
|
||||
}, function(data) {
|
||||
alert(data.message || 'Updated');
|
||||
if (data.status) applyPoFilter();
|
||||
}, 'json').fail(function() {
|
||||
alert('Error updating PO status.');
|
||||
});
|
||||
}
|
||||
|
||||
function cancelPo(poId) {
|
||||
var reason = prompt('Please enter cancellation reason:');
|
||||
if (reason === null) return;
|
||||
|
||||
$.post('<?= base_url('Medical/po_status/') ?>' + poId, {
|
||||
status: 'cancelled',
|
||||
reason: reason
|
||||
}, function(data) {
|
||||
alert(data.message || 'Cancelled');
|
||||
if (data.status) applyPoFilter();
|
||||
}, 'json').fail(function() {
|
||||
alert('Error cancelling PO.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
135
app/Views/medical/po_print_pdf.php
Normal file
135
app/Views/medical/po_print_pdf.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; color: #222; }
|
||||
.header { text-align: center; border-bottom: 2px solid #1a365d; padding-bottom: 8px; margin-bottom: 12px; }
|
||||
.header h2 { margin: 0; color: #1a365d; text-transform: uppercase; font-size: 18px; }
|
||||
.header p { margin: 2px 0 0; color: #555; font-size: 10px; }
|
||||
.title-bar { background-color: #f1f5f9; padding: 6px 10px; margin-bottom: 12px; border-left: 4px solid #1a365d; }
|
||||
.title-bar table { width: 100%; border-collapse: collapse; }
|
||||
.meta-table { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: 11px; }
|
||||
.meta-table td { padding: 4px 6px; vertical-align: top; }
|
||||
.items-table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
||||
.items-table th { background-color: #1a365d; color: #fff; padding: 6px 8px; text-align: left; font-size: 10px; text-transform: uppercase; border: 1px solid #1a365d; }
|
||||
.items-table td { padding: 6px 8px; border: 1px solid #cbd5e1; font-size: 10px; }
|
||||
.text-end { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.totals-table { width: 45%; margin-left: auto; border-collapse: collapse; margin-bottom: 20px; }
|
||||
.totals-table td { padding: 4px 8px; border-bottom: 1px solid #cbd5e1; font-size: 11px; }
|
||||
.grand-total { font-size: 13px; font-weight: bold; background-color: #f8fafc; color: #1a365d; }
|
||||
.terms { border: 1px dashed #94a3b8; padding: 8px 12px; font-size: 9px; color: #475569; margin-bottom: 30px; }
|
||||
.signatures { width: 100%; margin-top: 40px; }
|
||||
.signatures td { width: 33%; text-align: center; font-size: 10px; }
|
||||
.sig-line { border-top: 1px solid #333; margin-top: 35px; padding-top: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h2>Hospital Pharmacy Services</h2>
|
||||
<p>Central Medical Store & Drug Supply Division | Drug Lic. 20B/21B</p>
|
||||
</div>
|
||||
|
||||
<div class="title-bar">
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong style="font-size: 14px; color: #1a365d;">PURCHASE ORDER</strong></td>
|
||||
<td class="text-end"><strong>PO NO:</strong> <span style="font-family: monospace; font-size: 13px; color: #1a365d;"><?= esc($po['po_number']) ?></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td style="width: 50%; border: 1px solid #e2e8f0; padding: 8px;">
|
||||
<strong style="color: #64748b; font-size: 9px; text-transform: uppercase;">VENDOR DETAILS</strong><br>
|
||||
<strong style="font-size: 12px; color: #0f172a;"><?= esc($po['name_supplier'] ?: 'Supplier') ?></strong><br>
|
||||
<?php $loc = implode(', ', array_filter([$po['city'] ?? '', $po['state'] ?? ''])); if (!empty($loc)): ?><?= esc($loc) ?><br><?php endif; ?>
|
||||
Phone: <?= esc($po['contact_no'] ?: '-') ?><br>
|
||||
<?php if (!empty($po['gst_no'])): ?>GSTIN: <?= esc($po['gst_no']) ?><br><?php endif; ?>
|
||||
</td>
|
||||
<td style="width: 50%; border: 1px solid #e2e8f0; padding: 8px;">
|
||||
<strong style="color: #64748b; font-size: 9px; text-transform: uppercase;">ORDER METADATA</strong><br>
|
||||
<strong>PO Date:</strong> <?= date('d-M-Y', strtotime($po['po_date'])) ?><br>
|
||||
<strong>Expected Delivery:</strong> <?= !empty($po['expected_delivery_date']) ? date('d-M-Y', strtotime($po['expected_delivery_date'])) : 'Immediate' ?><br>
|
||||
<strong>Payment Terms:</strong> <?= esc($po['payment_terms'] ?: 'Net 30 Days') ?><br>
|
||||
<strong>Status:</strong> <span style="text-transform: uppercase; font-weight: bold;"><?= esc($po['status']) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;">#</th>
|
||||
<th>Item / Brand Name</th>
|
||||
<th>Salt Composition</th>
|
||||
<th class="text-center" style="width: 70px;">Pack Size</th>
|
||||
<th class="text-center" style="width: 70px;">Order Qty</th>
|
||||
<th class="text-end" style="width: 80px;">Rate/Pack</th>
|
||||
<th class="text-center" style="width: 50px;">GST %</th>
|
||||
<th class="text-end" style="width: 90px;">Total (₹)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($items)): ?>
|
||||
<?php foreach ($items as $idx => $it): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td><strong><?= esc($it['item_name']) ?></strong></td>
|
||||
<td><?= esc($it['generic_name'] ?: '-') ?></td>
|
||||
<td class="text-center"><?= (int)$it['units_per_pack'] ?>s</td>
|
||||
<td class="text-center"><strong><?= (float)$it['order_packs'] ?></strong></td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['estimated_ptr'], 2) ?></td>
|
||||
<td class="text-center"><?= (float)$it['gst_rate'] ?>%</td>
|
||||
<td class="text-end">₹<?= number_format((float)$it['estimated_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="totals-table">
|
||||
<tr>
|
||||
<td>Estimated Subtotal:</td>
|
||||
<td class="text-end">₹<?= number_format((float)$po['total_estimated_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Estimated GST Tax:</td>
|
||||
<td class="text-end">₹<?= number_format((float)$po['tax_amount'], 2) ?></td>
|
||||
</tr>
|
||||
<tr class="grand-total">
|
||||
<td>Grand Total (INR):</td>
|
||||
<td class="text-end">₹<?= number_format((float)$po['grand_total'] ?: ((float)$po['total_estimated_amount'] + (float)$po['tax_amount']), 2) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="terms">
|
||||
<strong>TERMS & CONDITIONS:</strong>
|
||||
<ol style="margin: 4px 0 0; padding-left: 15px;">
|
||||
<li>All supplies must match the specified brand, packing, and manufacturer specifications.</li>
|
||||
<li>Medicines must have at least 75% remaining shelf life from the date of manufacturing at the time of delivery.</li>
|
||||
<li>Deliveries must be accompanied by an original Tax Invoice and Delivery Challan referencing this PO number.</li>
|
||||
<li>Any broken, leaking, or damaged goods will be rejected at the time of Goods Received Note (GRN) verification.</li>
|
||||
<li>Cold chain items (2-8°C) must be delivered with unbroken temperature logs / ice packs.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<table class="signatures">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="sig-line">Prepared By (Pharmacist)</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="sig-line">Verified By (Store In-charge)</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="sig-line">Authorized Signatory</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user