fix(medical-store): remove third-party brand Marg from application and replace with generic Excel/CSV import
This commit is contained in:
parent
e635e823e5
commit
ca6926015a
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
### HMS Admin Management
|
### HMS Admin Management
|
||||||
- **URL:** [http://localhost:8080/setting/admin/medical-store](http://localhost:8080/setting/admin/medical-store)
|
- **URL:** [http://localhost:8080/setting/admin/medical-store](http://localhost:8080/setting/admin/medical-store)
|
||||||
- **Features:** Store configuration, DL 20B/21B licenses, GSTIN, Pharmacist details, ABDM HFR/HIP/HPR registries, 60-min OTP generator, terminal device authorization, and Marg ERP CSV import.
|
- **Features:** Store configuration, DL 20B/21B licenses, GSTIN, Pharmacist details, ABDM HFR/HIP/HPR registries, 60-min OTP generator, terminal device authorization, and Excel / CSV stock import.
|
||||||
|
|
||||||
### Direct Counter Desks (React PWA)
|
### Direct Counter Desks (React PWA)
|
||||||
- **Counter A (Main Hospital Block A):**
|
- **Counter A (Main Hospital Block A):**
|
||||||
|
|
@ -59,15 +59,15 @@
|
||||||
- 80mm Thermal Slip (Fast 1–5 item OTC/OPD).
|
- 80mm Thermal Slip (Fast 1–5 item OTC/OPD).
|
||||||
- A5 Invoice (Normal OPD/Walk-in with GST summary).
|
- A5 Invoice (Normal OPD/Walk-in with GST summary).
|
||||||
- A4 Hospital Tax Invoice (IPD admissions & large bills).
|
- A4 Hospital Tax Invoice (IPD admissions & large bills).
|
||||||
4. **Marg ERP Migration:**
|
4. **Excel / CSV Stock Migration:**
|
||||||
- Supported CSV import with auto-detected columns. Sample test file: `scratch/test_marg_sample.csv`.
|
- Supported CSV import with auto-detected columns.
|
||||||
5. **Double-Entry Accounting & Statutory Drug Register:**
|
5. **Double-Entry Accounting & Statutory Drug Register:**
|
||||||
- Automated ledger postings, Daily Cash Book, GSTR-1 outward reports, and mandatory Schedule H1 drug register.
|
- Automated ledger postings, Daily Cash Book, GSTR-1 outward reports, and mandatory Schedule H1 drug register.
|
||||||
6. **Retail Loose Tablet & Unit Dispensing:**
|
6. **Retail Loose Tablet & Unit Dispensing:**
|
||||||
- Exact per-tablet pricing (`Strip MRP / units_per_pack`), exact stock deduction in individual tablets, and support for Loose Tab, Strip, and Combo sales.
|
- Exact per-tablet pricing (`Strip MRP / units_per_pack`), exact stock deduction in individual tablets, and support for Loose Tab, Strip, and Combo sales.
|
||||||
7. **Split / Mixed Payment Mode & Audit:**
|
7. **Split / Mixed Payment Mode & Audit:**
|
||||||
- Supports partial payments (e.g. ₹35 Cash + ₹100 UPI) with 12-digit UTR and bank name capturing for bank reconciliation and audit.
|
- Supports partial payments (e.g. ₹35 Cash + ₹100 UPI) with 12-digit UTR and bank name capturing for bank reconciliation and audit.
|
||||||
8. **Keyboard-First Zero-Mouse POS Workflow (Marg ERP Standard):**
|
8. **Keyboard-First Zero-Mouse POS Workflow (Desktop Pharmacy Standard):**
|
||||||
- High-contrast single-line Quick Keys ribbon.
|
- High-contrast single-line Quick Keys ribbon.
|
||||||
- Hotkeys: `F2` (Medicine Search), `F4` (Patient Search), `F7` (Whole Discount), `F8` (Payment Mode), `F9 / Ctrl+Enter` (Complete Sale & Print), `Alt+T` (Loose Tab), `Alt+S` (Strip), `Shift+Delete` (Remove Item).
|
- Hotkeys: `F2` (Medicine Search), `F4` (Patient Search), `F7` (Whole Discount), `F8` (Payment Mode), `F9 / Ctrl+Enter` (Complete Sale & Print), `Alt+T` (Loose Tab), `Alt+S` (Strip), `Shift+Delete` (Remove Item).
|
||||||
- Auto-focus on Medicine Search on page load; `Enter` on quantity immediately commits and jumps focus back to Medicine Lookup for the next scan.
|
- Auto-focus on Medicine Search on page load; `Enter` on quantity immediately commits and jumps focus back to Medicine Lookup for the next scan.
|
||||||
|
|
|
||||||
|
|
@ -1426,7 +1426,7 @@ $routes->group('setting', static function($routes) {
|
||||||
$routes->post('admin/medical-store/generate-otp', 'Setting\\MedicalStoreAdmin::generateOtp');
|
$routes->post('admin/medical-store/generate-otp', 'Setting\\MedicalStoreAdmin::generateOtp');
|
||||||
$routes->post('admin/medical-store/regenerate-key', 'Setting\\MedicalStoreAdmin::regenerateSecurityKey');
|
$routes->post('admin/medical-store/regenerate-key', 'Setting\\MedicalStoreAdmin::regenerateSecurityKey');
|
||||||
$routes->post('admin/medical-store/revoke-device', 'Setting\\MedicalStoreAdmin::revokeDevice');
|
$routes->post('admin/medical-store/revoke-device', 'Setting\\MedicalStoreAdmin::revokeDevice');
|
||||||
$routes->post('admin/medical-store/import-marg', 'Setting\\MedicalStoreAdmin::importMarg');
|
$routes->post('admin/medical-store/import-excel', 'Setting\\MedicalStoreAdmin::importExcel');
|
||||||
|
|
||||||
$routes->get('admin/bed-management', 'Setting\\BedManagement::index', ['filter' => $settingsBedFilter]);
|
$routes->get('admin/bed-management', 'Setting\\BedManagement::index', ['filter' => $settingsBedFilter]);
|
||||||
$routes->get('admin/departments', 'Setting\\BedManagement::departments', ['filter' => $settingsBedFilter]);
|
$routes->get('admin/departments', 'Setting\\BedManagement::departments', ['filter' => $settingsBedFilter]);
|
||||||
|
|
@ -1702,7 +1702,7 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
||||||
$routes->get('medical-store/compliance/schedule-h1-register', 'MedicalStoreApi::getScheduleH1Register');
|
$routes->get('medical-store/compliance/schedule-h1-register', 'MedicalStoreApi::getScheduleH1Register');
|
||||||
$routes->post('medical-store/device/verify', 'MedicalStoreApi::verifyDevice');
|
$routes->post('medical-store/device/verify', 'MedicalStoreApi::verifyDevice');
|
||||||
$routes->post('medical-store/device/check', 'MedicalStoreApi::checkDeviceStatus');
|
$routes->post('medical-store/device/check', 'MedicalStoreApi::checkDeviceStatus');
|
||||||
$routes->post('medical-store/stock/import-marg', 'MedicalStoreApi::importMarg');
|
$routes->post('medical-store/stock/import-excel', 'MedicalStoreApi::importExcel');
|
||||||
$routes->get('medical-store/abdm/bundle/(:num)', 'MedicalStoreApi::getAbdmBundle/$1');
|
$routes->get('medical-store/abdm/bundle/(:num)', 'MedicalStoreApi::getAbdmBundle/$1');
|
||||||
$routes->post('medical-store/abdm/link-abha', 'MedicalStoreApi::linkAbha');
|
$routes->post('medical-store/abdm/link-abha', 'MedicalStoreApi::linkAbha');
|
||||||
$routes->get('medical-store/returns/lookup-invoice', 'MedicalStoreApi::lookupInvoiceForReturn');
|
$routes->get('medical-store/returns/lookup-invoice', 'MedicalStoreApi::lookupInvoiceForReturn');
|
||||||
|
|
|
||||||
|
|
@ -3481,10 +3481,10 @@ class MedicalStoreApi extends BaseController
|
||||||
return $this->response->setJSON(['status' => 0, 'authorized' => false, 'message' => 'Device authorization revoked or not found.']);
|
return $this->response->setJSON(['status' => 0, 'authorized' => false, 'message' => 'Device authorization revoked or not found.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function importMarg()
|
public function importExcel()
|
||||||
{
|
{
|
||||||
$admin = new \App\Controllers\Setting\MedicalStoreAdmin();
|
$admin = new \App\Controllers\Setting\MedicalStoreAdmin();
|
||||||
return $admin->importMarg();
|
return $admin->importExcel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -235,9 +235,9 @@ class MedicalStoreAdmin extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marg Pharmacy Software / Excel CSV Inventory Import
|
* Excel / CSV Inventory Import
|
||||||
*/
|
*/
|
||||||
public function importMarg()
|
public function importExcel()
|
||||||
{
|
{
|
||||||
$storeId = (int)($this->request->getPost('store_id') ?? 0);
|
$storeId = (int)($this->request->getPost('store_id') ?? 0);
|
||||||
if ($storeId <= 0) {
|
if ($storeId <= 0) {
|
||||||
|
|
@ -251,7 +251,7 @@ class MedicalStoreAdmin extends BaseController
|
||||||
|
|
||||||
$ext = strtolower($file->getClientExtension());
|
$ext = strtolower($file->getClientExtension());
|
||||||
if (!in_array($ext, ['csv', 'txt'])) {
|
if (!in_array($ext, ['csv', 'txt'])) {
|
||||||
return $this->response->setJSON(['ok' => false, 'error' => 'Please upload CSV formatted export from Marg ERP or Excel.']);
|
return $this->response->setJSON(['ok' => false, 'error' => 'Please upload CSV formatted export from Excel or CSV file.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle = fopen($file->getTempName(), 'r');
|
$handle = fopen($file->getTempName(), 'r');
|
||||||
|
|
@ -322,7 +322,7 @@ class MedicalStoreAdmin extends BaseController
|
||||||
$qty = ($colQty !== null) ? (int)preg_replace('/[^0-9]/', '', $row[$colQty] ?? '10') : 10;
|
$qty = ($colQty !== null) ? (int)preg_replace('/[^0-9]/', '', $row[$colQty] ?? '10') : 10;
|
||||||
$barcode = ($colBarcode !== null) ? trim($row[$colBarcode] ?? '') : '';
|
$barcode = ($colBarcode !== null) ? trim($row[$colBarcode] ?? '') : '';
|
||||||
|
|
||||||
// Parse Marg Expiry formats (MM/YY, MM/YYYY, DD-MM-YYYY, YYYY-MM-DD)
|
// Parse standard date and expiry formats (MM/YY, MM/YYYY, DD-MM-YYYY, YYYY-MM-DD)
|
||||||
$expiryDate = date('Y-12-31', strtotime('+1 year'));
|
$expiryDate = date('Y-12-31', strtotime('+1 year'));
|
||||||
if (!empty($expRaw)) {
|
if (!empty($expRaw)) {
|
||||||
if (preg_match('/^(\d{1,2})[\/\-](\d{2})$/', $expRaw, $m)) {
|
if (preg_match('/^(\d{1,2})[\/\-](\d{2})$/', $expRaw, $m)) {
|
||||||
|
|
@ -418,7 +418,7 @@ class MedicalStoreAdmin extends BaseController
|
||||||
|
|
||||||
// Record opening stock valuation journal voucher in double-entry ledgers
|
// Record opening stock valuation journal voucher in double-entry ledgers
|
||||||
if ($totalValuation > 0) {
|
if ($totalValuation > 0) {
|
||||||
$voucherNo = 'MARG-IMP-' . $storeId . '-' . date('YmdHis');
|
$voucherNo = 'CSV-IMP-' . $storeId . '-' . date('YmdHis');
|
||||||
$stockHead = $this->db->table('mst_account_heads')->where('head_code', '1003')->get()->getRowArray();
|
$stockHead = $this->db->table('mst_account_heads')->where('head_code', '1003')->get()->getRowArray();
|
||||||
$equityHead = $this->db->table('mst_account_heads')->where('head_code', '3004')->get()->getRowArray();
|
$equityHead = $this->db->table('mst_account_heads')->where('head_code', '3004')->get()->getRowArray();
|
||||||
|
|
||||||
|
|
@ -426,12 +426,12 @@ class MedicalStoreAdmin extends BaseController
|
||||||
$this->db->table('mst_ledger_entries')->insert([
|
$this->db->table('mst_ledger_entries')->insert([
|
||||||
'store_id' => $storeId, 'voucher_no' => $voucherNo, 'voucher_type' => 'OPENING_STOCK', 'voucher_date' => date('Y-m-d'),
|
'store_id' => $storeId, 'voucher_no' => $voucherNo, 'voucher_type' => 'OPENING_STOCK', 'voucher_date' => date('Y-m-d'),
|
||||||
'account_head_id' => $stockHead['head_id'], 'debit_amount' => $totalValuation, 'credit_amount' => 0,
|
'account_head_id' => $stockHead['head_id'], 'debit_amount' => $totalValuation, 'credit_amount' => 0,
|
||||||
'narration' => "Marg ERP Opening Stock Inward ($importedRows lines)"
|
'narration' => "Excel/CSV Opening Stock Inward ($importedRows lines)"
|
||||||
]);
|
]);
|
||||||
$this->db->table('mst_ledger_entries')->insert([
|
$this->db->table('mst_ledger_entries')->insert([
|
||||||
'store_id' => $storeId, 'voucher_no' => $voucherNo, 'voucher_type' => 'OPENING_STOCK', 'voucher_date' => date('Y-m-d'),
|
'store_id' => $storeId, 'voucher_no' => $voucherNo, 'voucher_type' => 'OPENING_STOCK', 'voucher_date' => date('Y-m-d'),
|
||||||
'account_head_id' => $equityHead['head_id'], 'debit_amount' => 0, 'credit_amount' => $totalValuation,
|
'account_head_id' => $equityHead['head_id'], 'debit_amount' => 0, 'credit_amount' => $totalValuation,
|
||||||
'narration' => "Marg ERP Opening Capital Credit"
|
'narration' => "Excel/CSV Opening Capital Credit"
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -440,7 +440,7 @@ class MedicalStoreAdmin extends BaseController
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'message' => "Successfully imported {$importedRows} inventory lines from Marg Pharmacy file.",
|
'message' => "Successfully imported {$importedRows} inventory lines from CSV file.",
|
||||||
'details' => [
|
'details' => [
|
||||||
'imported_rows' => $importedRows,
|
'imported_rows' => $importedRows,
|
||||||
'new_catalog_items'=> $newItems,
|
'new_catalog_items'=> $newItems,
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<small class="text-muted">Manage hospital building stores, Indian Drug Licenses (20B/21B), GSTIN, direct URLs, and machine terminal security</small>
|
<small class="text-muted">Manage hospital building stores, Indian Drug Licenses (20B/21B), GSTIN, direct URLs, and machine terminal security</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn btn-outline-success btn-sm" id="btn_open_marg_import">
|
<button type="button" class="btn btn-outline-success btn-sm" id="btn_open_excel_import">
|
||||||
<i class="bi bi-file-earmark-arrow-up me-1"></i> Marg ERP / Excel Import
|
<i class="bi bi-file-earmark-arrow-up me-1"></i> Excel / CSV Import
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary btn-sm" id="btn_add_store">
|
<button type="button" class="btn btn-primary btn-sm" id="btn_add_store">
|
||||||
<i class="bi bi-plus-lg me-1"></i> Add Medical Store
|
<i class="bi bi-plus-lg me-1"></i> Add Medical Store
|
||||||
|
|
@ -414,23 +414,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal 3: Marg ERP / Excel Inventory Import -->
|
<!-- Modal 3: Excel / CSV Inventory Import -->
|
||||||
<div class="modal fade" id="margImportModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="excelImportModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content border-0 shadow">
|
<div class="modal-content border-0 shadow">
|
||||||
<div class="modal-header bg-success text-white py-2.5">
|
<div class="modal-header bg-success text-white py-2.5">
|
||||||
<h6 class="modal-title fw-bold"><i class="bi bi-file-earmark-arrow-up me-1"></i> Marg ERP / Excel Inventory Import</h6>
|
<h6 class="modal-title fw-bold"><i class="bi bi-file-earmark-arrow-up me-1"></i> Excel / CSV Inventory Import</h6>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form id="margImportForm" enctype="multipart/form-data">
|
<form id="excelImportForm" enctype="multipart/form-data">
|
||||||
<div class="modal-body p-3">
|
<div class="modal-body p-3">
|
||||||
<p class="small text-muted mb-3">
|
<p class="small text-muted mb-3">
|
||||||
Quickly migrate stock from <strong>Marg Pharmacy Software</strong> or Microsoft Excel into the store's shared master catalog and batch stock.
|
Quickly migrate stock from Microsoft Excel or CSV files into the store's shared master catalog and batch stock.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small fw-bold mb-1">Target Medical Store Counter <span class="text-danger">*</span></label>
|
<label class="form-label small fw-bold mb-1">Target Medical Store Counter <span class="text-danger">*</span></label>
|
||||||
<select class="form-select form-select-sm" name="store_id" id="marg_store_id" required>
|
<select class="form-select form-select-sm" name="store_id" id="excel_store_id" required>
|
||||||
<?php foreach ($stores as $st): ?>
|
<?php foreach ($stores as $st): ?>
|
||||||
<option value="<?= (int)$st['store_id'] ?>"><?= esc($st['store_name']) ?> (<?= esc($st['building_name'] ?: 'Building') ?>)</option>
|
<option value="<?= (int)$st['store_id'] ?>"><?= esc($st['store_name']) ?> (<?= esc($st['building_name'] ?: 'Building') ?>)</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
@ -439,7 +439,7 @@
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label small fw-bold mb-1">Upload CSV / Excel Export <span class="text-danger">*</span></label>
|
<label class="form-label small fw-bold mb-1">Upload CSV / Excel Export <span class="text-danger">*</span></label>
|
||||||
<input type="file" class="form-control form-control-sm" name="import_file" id="marg_file" accept=".csv, .txt" required>
|
<input type="file" class="form-control form-control-sm" name="import_file" id="excel_file" accept=".csv, .txt" required>
|
||||||
<small class="text-muted" style="font-size: 11px;">
|
<small class="text-muted" style="font-size: 11px;">
|
||||||
Supported columns: Item Name, Generic, Batch No, Expiry (MM/YY), MRP, PTR/Cost, HSN, GST%, Opening Qty.
|
Supported columns: Item Name, Generic, Batch No, Expiry (MM/YY), MRP, PTR/Cost, HSN, GST%, Opening Qty.
|
||||||
</small>
|
</small>
|
||||||
|
|
@ -447,12 +447,12 @@
|
||||||
|
|
||||||
<div class="alert alert-light border py-2 mb-0" style="font-size: 11px;">
|
<div class="alert alert-light border py-2 mb-0" style="font-size: 11px;">
|
||||||
<i class="bi bi-download text-primary me-1"></i>
|
<i class="bi bi-download text-primary me-1"></i>
|
||||||
<a href="javascript:downloadSampleMargCsv()" class="text-decoration-none fw-bold">Download Sample Marg CSV Template</a>
|
<a href="javascript:downloadSampleCsv()" class="text-decoration-none fw-bold">Download Sample CSV Template</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer py-2 bg-light">
|
<div class="modal-footer py-2 bg-light">
|
||||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-success btn-sm" id="btn_submit_marg">
|
<button type="submit" class="btn btn-success btn-sm" id="btn_submit_excel">
|
||||||
<i class="bi bi-upload me-1"></i> Start Import
|
<i class="bi bi-upload me-1"></i> Start Import
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -528,8 +528,8 @@ document.getElementById('btn_add_store').addEventListener('click', function() {
|
||||||
modal.show();
|
modal.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn_open_marg_import').addEventListener('click', function() {
|
document.getElementById('btn_open_excel_import').addEventListener('click', function() {
|
||||||
var modal = new bootstrap.Modal(document.getElementById('margImportModal'));
|
var modal = new bootstrap.Modal(document.getElementById('excelImportModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -627,15 +627,15 @@ function deleteStore(storeId, storeName) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marg CSV Import Submit
|
// Excel / CSV Import Submit
|
||||||
$('#margImportForm').on('submit', function(e) {
|
$('#excelImportForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var formData = new FormData(this);
|
var formData = new FormData(this);
|
||||||
var btn = $('#btn_submit_marg');
|
var btn = $('#btn_submit_excel');
|
||||||
btn.prop('disabled', true).text('Importing records...');
|
btn.prop('disabled', true).text('Importing records...');
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '<?= base_url('setting/admin/medical-store/import-marg') ?>',
|
url: '<?= base_url('setting/admin/medical-store/import-excel') ?>',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
data: formData,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
|
|
@ -657,7 +657,7 @@ $('#margImportForm').on('submit', function(e) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function downloadSampleMargCsv() {
|
function downloadSampleCsv() {
|
||||||
var csvContent = "Item Name,Generic Name,Category,Packing,Batch No,Expiry Date,MRP,PTR,HSN Code,GST Rate,Opening Qty,Barcode\n" +
|
var csvContent = "Item Name,Generic Name,Category,Packing,Batch No,Expiry Date,MRP,PTR,HSN Code,GST Rate,Opening Qty,Barcode\n" +
|
||||||
"Augmentin 625 Duo Tablet,Amoxycillin + Clavulanic Acid,Tablet,10 Tab,AUG-991,10/27,204.50,155.00,3004,12,100,890103000001\n" +
|
"Augmentin 625 Duo Tablet,Amoxycillin + Clavulanic Acid,Tablet,10 Tab,AUG-991,10/27,204.50,155.00,3004,12,100,890103000001\n" +
|
||||||
"Pan 40 Tablet,Pantoprazole 40mg,Tablet,15 Tab,PAN-442,04/27,162.00,115.00,3004,12,150,890103000002\n" +
|
"Pan 40 Tablet,Pantoprazole 40mg,Tablet,15 Tab,PAN-442,04/27,162.00,115.00,3004,12,150,890103000002\n" +
|
||||||
|
|
@ -668,7 +668,7 @@ function downloadSampleMargCsv() {
|
||||||
var link = document.createElement("a");
|
var link = document.createElement("a");
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
link.setAttribute("href", url);
|
link.setAttribute("href", url);
|
||||||
link.setAttribute("download", "Marg_Pharmacy_Sample_Import.csv");
|
link.setAttribute("download", "Pharmacy_Sample_Stock_Import.csv");
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
|
||||||
|
|
@ -677,7 +677,7 @@ function App() {
|
||||||
<i className="bi bi-boxes me-1"></i> Live Stock & Batches
|
<i className="bi bi-boxes me-1"></i> Live Stock & Batches
|
||||||
</button>
|
</button>
|
||||||
<button className={`nav-link ${activeTab === 'purchase' ? 'active' : ''}`} onClick={() => setActiveTab('purchase')}>
|
<button className={`nav-link ${activeTab === 'purchase' ? 'active' : ''}`} onClick={() => setActiveTab('purchase')}>
|
||||||
<i className="bi bi-box-arrow-in-down me-1"></i> Purchases & Marg Import
|
<i className="bi bi-box-arrow-in-down me-1"></i> Purchases & Stock Import
|
||||||
</button>
|
</button>
|
||||||
<button className={`nav-link ${activeTab === 'transfer' ? 'active' : ''}`} onClick={() => setActiveTab('transfer')}>
|
<button className={`nav-link ${activeTab === 'transfer' ? 'active' : ''}`} onClick={() => setActiveTab('transfer')}>
|
||||||
<i className="bi bi-arrow-left-right me-1"></i> Inter-Store Indents
|
<i className="bi bi-arrow-left-right me-1"></i> Inter-Store Indents
|
||||||
|
|
@ -1532,7 +1532,7 @@ function PosCounter({ store, notify }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global Keyboard Shortcuts (Marg ERP / Desktop Pharmacy Standard)
|
// Global Keyboard Shortcuts (Desktop Pharmacy Standard)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleGlobalKeyDown = (e) => {
|
const handleGlobalKeyDown = (e) => {
|
||||||
// Dismiss Modals on Escape
|
// Dismiss Modals on Escape
|
||||||
|
|
@ -3495,7 +3495,7 @@ function PosCounter({ store, notify }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sales Return & Same-Invoice Exchange Modal (B2C Marg ERP Standard) */}
|
{/* Sales Return & Same-Invoice Exchange Modal (B2C Retail Standard) */}
|
||||||
{showReturnModal && (
|
{showReturnModal && (
|
||||||
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
|
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
|
||||||
<div className="modal-dialog modal-xl modal-dialog-scrollable">
|
<div className="modal-dialog modal-xl modal-dialog-scrollable">
|
||||||
|
|
@ -3506,7 +3506,7 @@ function PosCounter({ store, notify }) {
|
||||||
<div>
|
<div>
|
||||||
<h6 className="modal-title mb-0 fw-bold">Sales Return & Same-Invoice Exchange Desk</h6>
|
<h6 className="modal-title mb-0 fw-bold">Sales Return & Same-Invoice Exchange Desk</h6>
|
||||||
<small className="text-secondary" style={{ fontSize: '0.75rem' }}>
|
<small className="text-secondary" style={{ fontSize: '0.75rem' }}>
|
||||||
B2C Retail Returns • Stock Replenishment • GST Credit Note • Marg ERP Standard (F6 / Alt+R)
|
B2C Retail Returns • Stock Replenishment • GST Credit Note • Fast POS Standard (F6 / Alt+R)
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -4011,11 +4011,11 @@ function InventoryView({ store, notify }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 3. INWARD PURCHASES, SUPPLIERS & MARG ERP IMPORT
|
// 3. INWARD PURCHASES, SUPPLIERS & EXCEL / CSV IMPORT
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
function PurchaseView({ store, notify }) {
|
function PurchaseView({ store, notify }) {
|
||||||
if (!store) return null;
|
if (!store) return null;
|
||||||
const [subTab, setSubTab] = useState('inward'); // inward, marg, suppliers
|
const [subTab, setSubTab] = useState('inward'); // inward, excel, suppliers
|
||||||
const [suppliers, setSuppliers] = useState([]);
|
const [suppliers, setSuppliers] = useState([]);
|
||||||
const [selectedSupplierId, setSelectedSupplierId] = useState('');
|
const [selectedSupplierId, setSelectedSupplierId] = useState('');
|
||||||
const [invoiceNo, setInvoiceNo] = useState('');
|
const [invoiceNo, setInvoiceNo] = useState('');
|
||||||
|
|
@ -4759,8 +4759,8 @@ function PurchaseView({ store, notify }) {
|
||||||
<button className={`btn btn-sm ${subTab === 'suppliers' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('suppliers')}>
|
<button className={`btn btn-sm ${subTab === 'suppliers' ? 'btn-primary shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('suppliers')}>
|
||||||
<i className="bi bi-truck me-1"></i> Supplier Directory ({suppliers.length})
|
<i className="bi bi-truck me-1"></i> Supplier Directory ({suppliers.length})
|
||||||
</button>
|
</button>
|
||||||
<button className={`btn btn-sm ${subTab === 'marg' ? 'btn-success shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('marg')}>
|
<button className={`btn btn-sm ${subTab === 'excel' ? 'btn-success shadow-sm' : 'btn-outline-secondary'}`} onClick={() => setSubTab('excel')}>
|
||||||
<i className="bi bi-file-earmark-spreadsheet me-1"></i> Marg ERP / Excel Import
|
<i className="bi bi-file-earmark-spreadsheet me-1"></i> Excel / CSV Import
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -5035,22 +5035,22 @@ function PurchaseView({ store, notify }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{subTab === 'marg' && (
|
{subTab === 'excel' && (
|
||||||
<div className="counter-card">
|
<div className="counter-card">
|
||||||
<h5 className="fw-bold text-success mb-2"><i className="bi bi-file-earmark-spreadsheet me-1"></i> Marg ERP / Excel Inventory Importer</h5>
|
<h5 className="fw-bold text-success mb-2"><i className="bi bi-file-earmark-spreadsheet me-1"></i> Excel / CSV Inventory Importer</h5>
|
||||||
<p className="text-muted small mb-3">
|
<p className="text-muted small mb-3">
|
||||||
Upload your stock export from <strong>Marg Pharmacy Software</strong> or Microsoft Excel to seamlessly import medicines, batch numbers, MRP, PTR, HSN codes, and opening stock into <strong>{store.store_name}</strong>.
|
Upload your stock export from Microsoft Excel or CSV format to seamlessly import medicines, batch numbers, MRP, PTR, HSN codes, and opening stock into <strong>{store.store_name}</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="p-4 bg-light rounded-3 border text-center mb-3">
|
<div className="p-4 bg-light rounded-3 border text-center mb-3">
|
||||||
<i className="bi bi-cloud-arrow-up fs-1 text-success d-block mb-2"></i>
|
<i className="bi bi-cloud-arrow-up fs-1 text-success d-block mb-2"></i>
|
||||||
<h6 className="fw-bold">Upload Marg CSV / Excel File</h6>
|
<h6 className="fw-bold">Upload CSV / Excel File</h6>
|
||||||
<p className="small text-muted mb-3">Ensure file is exported in CSV format from Marg ERP.</p>
|
<p className="small text-muted mb-3">Ensure file is formatted as standard CSV or Excel-exported CSV.</p>
|
||||||
<input type="file" id="marg_file_input" accept=".csv, .txt" className="form-control form-control-sm w-50 mx-auto mb-3" />
|
<input type="file" id="excel_file_input" accept=".csv, .txt" className="form-control form-control-sm w-50 mx-auto mb-3" />
|
||||||
<button
|
<button
|
||||||
className="btn btn-success-custom"
|
className="btn btn-success-custom"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const fileInput = document.getElementById('marg_file_input');
|
const fileInput = document.getElementById('excel_file_input');
|
||||||
if (!fileInput.files || fileInput.files.length === 0) {
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
alert('Please select a CSV file first.');
|
alert('Please select a CSV file first.');
|
||||||
return;
|
return;
|
||||||
|
|
@ -5060,7 +5060,7 @@ function PurchaseView({ store, notify }) {
|
||||||
formData.append('import_file', fileInput.files[0]);
|
formData.append('import_file', fileInput.files[0]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API_BASE + 'stock/import-marg', {
|
const res = await fetch(API_BASE + 'stock/import-excel', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,187 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Test Terminal Verification and Marg ERP CSV Import
|
|
||||||
*/
|
|
||||||
|
|
||||||
$db = new mysqli('127.0.0.1', 'root', '', 'hms_data_ci4', 3306);
|
|
||||||
if ($db->connect_error) {
|
|
||||||
die("DB Connection failed: " . $db->connect_error . "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== 1. TESTING TERMINAL DEVICE VERIFICATION VIA SECURITY KEY ===\n";
|
|
||||||
|
|
||||||
// Get store 1
|
|
||||||
$storeRes = $db->query("SELECT * FROM mst_stores WHERE store_id = 1");
|
|
||||||
$store1 = $storeRes->fetch_assoc();
|
|
||||||
echo " - Store: {$store1['store_name']} (Slug: {$store1['store_slug']}, Key: {$store1['security_key']})\n";
|
|
||||||
|
|
||||||
// Verify with Security Key
|
|
||||||
$machineName = "OPD-Counter-Dell-01";
|
|
||||||
$fingerprint = "fp_" . md5("screen_1920x1080_win64_dell");
|
|
||||||
$token = bin2hex(random_bytes(32));
|
|
||||||
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_store_devices` (
|
|
||||||
`store_id`, `machine_name`, `device_token`, `device_fingerprint`,
|
|
||||||
`ip_address`, `user_agent`, `verified_by_method`, `status`
|
|
||||||
) VALUES (
|
|
||||||
1, '$machineName', '$token', '$fingerprint',
|
|
||||||
'192.168.1.101', 'Mozilla/5.0 (Windows NT 10.0; Win64)', 'SECURITY_KEY', 'authorized'
|
|
||||||
)
|
|
||||||
");
|
|
||||||
$deviceId = $db->insert_id;
|
|
||||||
echo " ✓ Machine '{$machineName}' registered (Device ID: {$deviceId}, Token: " . substr($token, 0, 16) . "...)\n";
|
|
||||||
|
|
||||||
// Verify status check
|
|
||||||
$chk = $db->query("SELECT * FROM mst_store_devices WHERE device_token = '$token' AND status = 'authorized'")->fetch_assoc();
|
|
||||||
if ($chk) {
|
|
||||||
echo " ✓ Device check passed: Machine is ACTIVE & AUTHORIZED for Store {$chk['store_id']}\n";
|
|
||||||
} else {
|
|
||||||
echo " ✗ Device check failed!\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n=== 2. TESTING OTP GENERATION & MACHINE VERIFICATION ===\n";
|
|
||||||
|
|
||||||
// Generate OTP for store 2
|
|
||||||
$store2Res = $db->query("SELECT * FROM mst_stores WHERE store_id = 2");
|
|
||||||
$store2 = $store2Res->fetch_assoc();
|
|
||||||
|
|
||||||
$testOtp = (string)rand(100000, 999999);
|
|
||||||
$testExpiry = date('Y-m-d H:i:s', strtotime('+60 minutes'));
|
|
||||||
$db->query("UPDATE mst_stores SET current_otp = '$testOtp', otp_expiry = '$testExpiry' WHERE store_id = 2");
|
|
||||||
echo " - Store: {$store2['store_name']} | Generated 60-min OTP: $testOtp (Expires: $testExpiry)\n";
|
|
||||||
|
|
||||||
// Machine verifies with OTP
|
|
||||||
$token2 = bin2hex(random_bytes(32));
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_store_devices` (
|
|
||||||
`store_id`, `machine_name`, `device_token`, `device_fingerprint`,
|
|
||||||
`ip_address`, `user_agent`, `verified_by_method`, `status`
|
|
||||||
) VALUES (
|
|
||||||
2, 'IPD-Counter-HP-02', '$token2', 'fp_hp_terminal_02',
|
|
||||||
'192.168.1.102', 'Mozilla/5.0 (Windows NT 10.0)', 'OTP', 'authorized'
|
|
||||||
)
|
|
||||||
");
|
|
||||||
$dev2Id = $db->insert_id;
|
|
||||||
echo " ✓ Machine 'IPD-Counter-HP-02' authorized using OTP (Device ID: {$dev2Id})\n";
|
|
||||||
|
|
||||||
// Revoke machine terminal by Admin
|
|
||||||
$db->query("UPDATE mst_store_devices SET status = 'revoked' WHERE device_id = $dev2Id");
|
|
||||||
$chkRevoked = $db->query("SELECT * FROM mst_store_devices WHERE device_token = '$token2' AND status = 'authorized'")->fetch_assoc();
|
|
||||||
if (!$chkRevoked) {
|
|
||||||
echo " ✓ Admin Revocation confirmed: Device token rejected when revoked.\n";
|
|
||||||
} else {
|
|
||||||
echo " ✗ Revocation check failed!\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n=== 3. TESTING MARG ERP CSV IMPORT SIMULATION ===\n";
|
|
||||||
|
|
||||||
$csvPath = __DIR__ . '/test_marg_sample.csv';
|
|
||||||
$handle = fopen($csvPath, 'r');
|
|
||||||
$header = fgetcsv($handle);
|
|
||||||
echo " - CSV Header: " . implode(', ', $header) . "\n";
|
|
||||||
|
|
||||||
$imported = 0;
|
|
||||||
while (($row = fgetcsv($handle)) !== false) {
|
|
||||||
$itemName = trim($row[0]);
|
|
||||||
$pack = trim($row[1]);
|
|
||||||
$batchNo = trim($row[2]);
|
|
||||||
$expStr = trim($row[3]);
|
|
||||||
$mrp = (float)$row[4];
|
|
||||||
$ptr = (float)$row[5];
|
|
||||||
$hsn = trim($row[6]);
|
|
||||||
$gst = (float)$row[7];
|
|
||||||
$qty = (int)$row[8];
|
|
||||||
|
|
||||||
// Format Expiry MM/YYYY -> YYYY-MM-lastday
|
|
||||||
$parts = explode('/', $expStr);
|
|
||||||
$expDate = date('Y-m-t', strtotime("{$parts[1]}-{$parts[0]}-01"));
|
|
||||||
|
|
||||||
// Check/create master drug catalog
|
|
||||||
$findItem = $db->query("SELECT item_id FROM mst_items WHERE item_name = '" . $db->real_escape_string($itemName) . "'")->fetch_assoc();
|
|
||||||
if ($findItem) {
|
|
||||||
$itemId = $findItem['item_id'];
|
|
||||||
} else {
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_items` (
|
|
||||||
`item_name`, `unit_pack`, `hsn_code`, `gst_rate`, `category`, `is_active`
|
|
||||||
) VALUES (
|
|
||||||
'" . $db->real_escape_string($itemName) . "', '$pack', '$hsn', $gst, 'Allopathy', 1
|
|
||||||
)
|
|
||||||
");
|
|
||||||
$itemId = $db->insert_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check/create batch for Store 1
|
|
||||||
$findBatch = $db->query("SELECT batch_id FROM mst_batches WHERE store_id = 1 AND item_id = $itemId AND batch_no = '$batchNo'")->fetch_assoc();
|
|
||||||
if ($findBatch) {
|
|
||||||
$batchId = $findBatch['batch_id'];
|
|
||||||
} else {
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_batches` (
|
|
||||||
`store_id`, `item_id`, `batch_no`, `expiry_date`, `ptr`, `mrp`, `gst_rate`
|
|
||||||
) VALUES (
|
|
||||||
1, $itemId, '$batchNo', '$expDate', $ptr, $mrp, $gst
|
|
||||||
)
|
|
||||||
");
|
|
||||||
$batchId = $db->insert_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or insert stock
|
|
||||||
$stockRow = $db->query("SELECT stock_id, current_qty FROM mst_stock WHERE store_id = 1 AND batch_id = $batchId")->fetch_assoc();
|
|
||||||
if ($stockRow) {
|
|
||||||
$newQty = $stockRow['current_qty'] + $qty;
|
|
||||||
$db->query("UPDATE mst_stock SET current_qty = $newQty WHERE stock_id = {$stockRow['stock_id']}");
|
|
||||||
} else {
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_stock` (
|
|
||||||
`store_id`, `item_id`, `batch_id`, `current_qty`
|
|
||||||
) VALUES (
|
|
||||||
1, $itemId, $batchId, $qty
|
|
||||||
)
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit log
|
|
||||||
$db->query("
|
|
||||||
INSERT INTO `mst_stock_audit` (
|
|
||||||
`store_id`, `item_id`, `batch_id`, `audit_type`, `system_qty`, `physical_qty`,
|
|
||||||
`variation_qty`, `rate`, `total_value`, `remarks`
|
|
||||||
) VALUES (
|
|
||||||
1, $itemId, $batchId, 'OPENING_STOCK', 0, $qty,
|
|
||||||
$qty, $ptr, " . ($ptr * $qty) . ", 'Imported via Marg ERP CSV'
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
$imported++;
|
|
||||||
echo " ✓ Imported '{$itemName}' | Batch: {$batchNo} | Exp: {$expDate} | MRP: ₹{$mrp} | Qty: {$qty}\n";
|
|
||||||
}
|
|
||||||
fclose($handle);
|
|
||||||
|
|
||||||
echo "\n - Total rows imported from Marg format: $imported\n";
|
|
||||||
|
|
||||||
echo "\n=== 4. VERIFYING ISOLATION OF STOCKS ACROSS STORES ===\n";
|
|
||||||
|
|
||||||
// Check that Store 2 does NOT see Store 1's imported stock
|
|
||||||
$st2Check = $db->query("
|
|
||||||
SELECT i.item_name, s.current_qty
|
|
||||||
FROM mst_stock s
|
|
||||||
JOIN mst_items i ON i.item_id = s.item_id
|
|
||||||
WHERE s.store_id = 2 AND i.item_name = 'Azithral 500 Tablet'
|
|
||||||
")->fetch_assoc();
|
|
||||||
|
|
||||||
if (!$st2Check) {
|
|
||||||
echo " ✓ Store Isolation Verified: 'Azithral 500 Tablet' has 0 stock in Store 2 (OPD Counter)\n";
|
|
||||||
} else {
|
|
||||||
echo " - Store 2 stock: " . ($st2Check['current_qty'] ?? 0) . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$st1Check = $db->query("
|
|
||||||
SELECT i.item_name, s.current_qty
|
|
||||||
FROM mst_stock s
|
|
||||||
JOIN mst_items i ON i.item_id = s.item_id
|
|
||||||
WHERE s.store_id = 1 AND i.item_name = 'Azithral 500 Tablet'
|
|
||||||
")->fetch_assoc();
|
|
||||||
echo " ✓ Store 1 stock: '{$st1Check['item_name']}' has {$st1Check['current_qty']} units in Store 1 (Main Store)\n";
|
|
||||||
|
|
||||||
echo "\n=== ALL VERIFICATIONS PASSED SUCCESSFULLY ===\n";
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
Item Name,Packing,Batch,Exp Date,MRP,PTR,HSN,GST%,Qty
|
|
||||||
Azithral 500 Tablet,1x5,AZI-901,12/2026,125.00,92.50,3004,12,50
|
|
||||||
Pantocid DSR Capsule,1x10,PNT-441,08/2027,210.00,155.00,3004,12,100
|
|
||||||
Paracetamol 650 Tablet,1x15,PCM-102,05/2027,32.50,22.00,3004,12,200
|
|
||||||
Betadine 10% Ointment 20gm,1x1,BT-882,03/2028,140.00,105.00,3004,12,30
|
|
||||||
Monocef 1gm Injection,1 Vial,MNC-774,11/2026,68.00,51.00,3004,12,80
|
|
||||||
|
Loading…
Reference in New Issue
Block a user