Add pharmacy invoice permissions and discount print fixes
This commit is contained in:
parent
7422f9c170
commit
252f911fad
|
|
@ -53,6 +53,10 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'title' => 'Developer',
|
||||
'description' => 'Site programmers.',
|
||||
],
|
||||
'pharmacy_admin' => [
|
||||
'title' => 'Pharmacy Admin',
|
||||
'description' => 'Full pharmacy controls including old bill edits and discount overrides.',
|
||||
],
|
||||
'stock_manager' => [
|
||||
'title' => 'Stock Manager',
|
||||
'description' => 'Manages stock masters, approvals, and procurement.',
|
||||
|
|
@ -148,6 +152,11 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'doctor_work.advice.manage' => 'Can manage OPD advice master',
|
||||
'doctor_work.template_workspace.access' => 'Can access clinical templates workspace',
|
||||
'pharmacy.access' => 'Can access pharmacy module',
|
||||
'pharmacy.invoice.admin' => 'Can fully manage pharmacy invoice controls (old bills and discount overrides)',
|
||||
'pharmacy.invoice.discount.10' => 'Can apply pharmacy invoice discount up to 10%',
|
||||
'pharmacy.invoice.discount.20' => 'Can apply pharmacy invoice discount up to 20%',
|
||||
'pharmacy.invoice.discount.30' => 'Can apply pharmacy invoice discount up to 30%',
|
||||
'pharmacy.invoice.edit-old' => 'Can edit past Walk-in/Registered pharmacy invoices',
|
||||
'pharmacy.purchase.status-update' => 'Can update pharmacy purchase invoice status',
|
||||
'template.pathology' => 'Can access pathology templates',
|
||||
'template.ultrasound' => 'Can access ultrasound templates',
|
||||
|
|
@ -247,6 +256,9 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'hospital_stock.*',
|
||||
'abdm.*',
|
||||
],
|
||||
'pharmacy_admin' => [
|
||||
'pharmacy.*',
|
||||
],
|
||||
'stock_manager' => [
|
||||
'hospital_stock.access',
|
||||
'hospital_stock.master.manage',
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ class Medical extends BaseController
|
|||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$permission !== 'pharmacy.invoice.admin'
|
||||
&& $user
|
||||
&& method_exists($user, 'can')
|
||||
&& $user->can('pharmacy.invoice.admin')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user && method_exists($user, 'inGroup') && $user->inGroup('superadmin', 'admin', 'developer')) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -53,6 +62,89 @@ class Medical extends BaseController
|
|||
return false;
|
||||
}
|
||||
|
||||
private function getHospitalSettingValue(string $name, string $default = ''): string
|
||||
{
|
||||
$name = trim($name);
|
||||
if ($name === '' || ! $this->db->tableExists('hospital_setting')) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$row = $this->db->table('hospital_setting')
|
||||
->select('s_value')
|
||||
->where('s_name', $name)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (! is_array($row) || ! array_key_exists('s_value', $row)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = trim((string) ($row['s_value'] ?? ''));
|
||||
return $value !== '' ? $value : $default;
|
||||
}
|
||||
|
||||
private function getPharmacyBaseDiscountPercent(): float
|
||||
{
|
||||
$raw = $this->getHospitalSettingValue('PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT', '10');
|
||||
$percent = is_numeric($raw) ? (float) $raw : 10.0;
|
||||
if ($percent < 0) {
|
||||
$percent = 0.0;
|
||||
}
|
||||
if ($percent > 100) {
|
||||
$percent = 100.0;
|
||||
}
|
||||
|
||||
return round($percent, 2);
|
||||
}
|
||||
|
||||
private function getPharmacyMaxDiscountPercent(): float
|
||||
{
|
||||
if ($this->canPharmacyPermission('pharmacy.invoice.admin')) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
$percent = $this->getPharmacyBaseDiscountPercent();
|
||||
|
||||
if ($this->canPharmacyPermission('pharmacy.invoice.discount.10')) {
|
||||
$percent = max($percent, 10.0);
|
||||
}
|
||||
if ($this->canPharmacyPermission('pharmacy.invoice.discount.20')) {
|
||||
$percent = max($percent, 20.0);
|
||||
}
|
||||
if ($this->canPharmacyPermission('pharmacy.invoice.discount.30')) {
|
||||
$percent = max($percent, 30.0);
|
||||
}
|
||||
|
||||
if ($percent < 0) {
|
||||
$percent = 0.0;
|
||||
}
|
||||
if ($percent > 100) {
|
||||
$percent = 100.0;
|
||||
}
|
||||
|
||||
return round($percent, 2);
|
||||
}
|
||||
|
||||
private function validatePharmacyDiscountAmount(float $grossAmount, float $discountAmount, ?string &$errorText = null): bool
|
||||
{
|
||||
$errorText = null;
|
||||
|
||||
if ($discountAmount < 0) {
|
||||
$errorText = 'Discount amount cannot be negative.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxPercent = $this->getPharmacyMaxDiscountPercent();
|
||||
$maxAmount = round(max(0.0, $grossAmount) * $maxPercent / 100, 2);
|
||||
|
||||
if ($discountAmount > $maxAmount + 0.00001) {
|
||||
$errorText = 'Discount Amount is greater than allowed limit. Max : ' . number_format($maxAmount, 2, '.', '') . ' (' . rtrim(rtrim(number_format($maxPercent, 2, '.', ''), '0'), '.') . '%)';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function ensureMedicalAdminActionLogTable(): void
|
||||
{
|
||||
if ($this->db->tableExists('medical_admin_action_log')) {
|
||||
|
|
@ -2064,6 +2156,8 @@ class Medical extends BaseController
|
|||
$message = trim((string) $this->request->getGet('msg'));
|
||||
$editBlockReason = null;
|
||||
$canEditInvoice = $this->canEditInvoiceRecord((array) $invoice, $editBlockReason);
|
||||
$discountPercentLimit = $this->getPharmacyMaxDiscountPercent();
|
||||
$discountAmountLimit = round((float) ($invoice->gross_amount ?? 0) * $discountPercentLimit / 100, 2);
|
||||
|
||||
return view('medical/medical_final_invoice', [
|
||||
'invoice' => $invoice,
|
||||
|
|
@ -2077,6 +2171,8 @@ class Medical extends BaseController
|
|||
'message' => $message,
|
||||
'canEditInvoice' => $canEditInvoice,
|
||||
'editBlockReason' => (string) ($editBlockReason ?? ''),
|
||||
'discountPercentLimit' => $discountPercentLimit,
|
||||
'discountAmountLimit' => $discountAmountLimit,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -2669,6 +2765,12 @@ class Medical extends BaseController
|
|||
return service('response')->setJSON(['update' => 0, 'msg_text' => 'No invoice found']);
|
||||
}
|
||||
|
||||
$grossAmount = (float) ($invoice->gross_amount ?? 0);
|
||||
$discountError = null;
|
||||
if (! $this->validatePharmacyDiscountAmount($grossAmount, $discount, $discountError)) {
|
||||
return service('response')->setJSON(['update' => 0, 'msg_text' => (string) $discountError]);
|
||||
}
|
||||
|
||||
$invoiceId = (int) ($invoice->id ?? 0);
|
||||
$update = [];
|
||||
if (in_array('discount_amount', $fields, true)) {
|
||||
|
|
@ -3110,6 +3212,15 @@ class Medical extends BaseController
|
|||
$discountAmount = (float) $this->request->getPost('input_dis_amt');
|
||||
$discountRemark = trim((string) $this->request->getPost('input_dis_desc'));
|
||||
|
||||
$grossAmount = (float) ($invoice->gross_amount ?? 0);
|
||||
$discountError = null;
|
||||
if (! $this->validatePharmacyDiscountAmount($grossAmount, $discountAmount, $discountError)) {
|
||||
return service('response')->setJSON([
|
||||
'update' => 0,
|
||||
'error_text' => (string) $discountError,
|
||||
]);
|
||||
}
|
||||
|
||||
$user = service('auth')->user();
|
||||
$userId = (int) ($user->id ?? 0);
|
||||
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
|
||||
|
|
@ -3441,7 +3552,12 @@ class Medical extends BaseController
|
|||
$caseId = (int) ($invoiceRow['case_id'] ?? 0);
|
||||
$isWalkInOrRegistered = $ipdId === 0 && $caseId === 0 && in_array($customerType, [0, 1], true);
|
||||
|
||||
if ($isPastDate && $isWalkInOrRegistered && ! $this->canPharmacyPermission('pharmacy.invoice.edit-old')) {
|
||||
if (
|
||||
$isPastDate
|
||||
&& $isWalkInOrRegistered
|
||||
&& ! $this->canPharmacyPermission('pharmacy.invoice.edit-old')
|
||||
&& ! $this->canPharmacyPermission('pharmacy.invoice.admin')
|
||||
) {
|
||||
$reason = 'No permission to edit past Walk-in/Registered pharmacy invoice.';
|
||||
return false;
|
||||
}
|
||||
|
|
@ -6209,7 +6325,6 @@ class Medical extends BaseController
|
|||
|
||||
$key = $this->normalizeCompactKey($value);
|
||||
$map = [
|
||||
'tab' => 'Tablet',
|
||||
'tabs' => 'Tablet',
|
||||
'tablet' => 'Tablet',
|
||||
'tablets' => 'Tablet',
|
||||
|
|
|
|||
|
|
@ -51,11 +51,12 @@
|
|||
<div class="mt-4">
|
||||
<label class="form-label">Permissions</label>
|
||||
<div class="alert alert-info py-2 mb-2">
|
||||
Pharmacy module access is controlled by <strong>pharmacy.access</strong>.
|
||||
Pharmacy module access is controlled by <strong>pharmacy.access</strong>. Editing past Walk-in/Registered pharmacy invoices requires <strong>pharmacy.invoice.edit-old</strong> (or <strong>pharmacy.invoice.admin</strong>). Discount caps can be granted with <strong>pharmacy.invoice.discount.10</strong>, <strong>pharmacy.invoice.discount.20</strong>, <strong>pharmacy.invoice.discount.30</strong>, or full override using <strong>pharmacy.invoice.admin</strong>. Base user discount can be set in hospital setting key <strong>PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT</strong>.
|
||||
</div>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
<?php
|
||||
$selectedPermissions = [];
|
||||
$permissions = $permissions ?? [];
|
||||
if (! empty($selectedUser)) {
|
||||
$selectedPermissions = $selectedUser->getPermissions() ?? [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,19 @@
|
|||
|
||||
$doctorName = trim((string) ($invoice->doc_name ?? ''));
|
||||
$doctorLabel = $doctorName !== '' ? (preg_match('/^dr\.?\s*/i', $doctorName) ? $doctorName : 'Dr. ' . $doctorName) : '-';
|
||||
$items = $items ?? [];
|
||||
$rowCount = count($items);
|
||||
$baseGrossTotal = 0.0;
|
||||
$baseDiscountTotal = 0.0;
|
||||
if (! empty($items)) {
|
||||
foreach ($items as $printItem) {
|
||||
$baseGrossTotal += (float) ($printItem->amount ?? 0);
|
||||
$baseDiscountTotal += (float) ($printItem->disc_amount ?? 0);
|
||||
}
|
||||
}
|
||||
$invoiceDiscountTotal = (float) (($invoice->discount_amount ?? 0) + ($invoice->disc_amount ?? 0) + ($invoice->item_discount_amount ?? 0));
|
||||
$extraInvoiceDiscountTotal = max(0.0, $invoiceDiscountTotal - $baseDiscountTotal);
|
||||
$allocatedExtraDiscountTotal = 0.0;
|
||||
?>
|
||||
<div class="print-actions">
|
||||
<button onclick="window.print()">Print</button>
|
||||
|
|
@ -79,8 +92,23 @@
|
|||
<?php foreach ($items as $idx => $item): ?>
|
||||
<?php
|
||||
$rowGross = (float) ($item->amount ?? 0);
|
||||
$rowDisc = (float) ($item->disc_amount ?? 0);
|
||||
$rowNet = (float) ($item->tamount ?? 0);
|
||||
$baseRowDisc = (float) ($item->disc_amount ?? 0);
|
||||
$baseRowNet = (float) ($item->twdisc_amount ?? $item->tamount ?? ($item->net_amount ?? 0));
|
||||
$remainingExtraDiscount = max(0.0, $extraInvoiceDiscountTotal - $allocatedExtraDiscountTotal);
|
||||
$allocExtraDisc = 0.0;
|
||||
if ($remainingExtraDiscount > 0 && $baseGrossTotal > 0) {
|
||||
if ((int) $idx === $rowCount - 1) {
|
||||
$allocExtraDisc = $remainingExtraDiscount;
|
||||
} else {
|
||||
$allocExtraDisc = round($extraInvoiceDiscountTotal * ($rowGross / $baseGrossTotal), 2);
|
||||
if ($allocExtraDisc > $remainingExtraDiscount) {
|
||||
$allocExtraDisc = $remainingExtraDiscount;
|
||||
}
|
||||
}
|
||||
}
|
||||
$allocatedExtraDiscountTotal += $allocExtraDisc;
|
||||
$rowDisc = $baseRowDisc + $allocExtraDisc;
|
||||
$rowNet = max(0.0, $baseRowNet - $allocExtraDisc);
|
||||
$gross += $rowGross;
|
||||
$discount += $rowDisc;
|
||||
$net += $rowNet;
|
||||
|
|
@ -103,17 +131,26 @@
|
|||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<?php
|
||||
$printGrossTotal = (float) ($invoice->gross_amount ?? $gross);
|
||||
$printDiscountTotal = (float) (($invoice->discount_amount ?? 0) + ($invoice->disc_amount ?? 0) + ($invoice->item_discount_amount ?? 0));
|
||||
$printDiscountTotal = $printDiscountTotal > 0 ? $printDiscountTotal : $discount;
|
||||
$printNetTotal = $printGrossTotal - $printDiscountTotal;
|
||||
if ($printNetTotal < 0) {
|
||||
$printNetTotal = 0.0;
|
||||
}
|
||||
?>
|
||||
<tr class="totals">
|
||||
<td colspan="6" class="text-end">Gross</td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format((float)($invoice->gross_amount ?? $gross), 2)) ?></td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format($printGrossTotal, 2)) ?></td>
|
||||
</tr>
|
||||
<tr class="totals">
|
||||
<td colspan="6" class="text-end">Discount</td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format((float)($invoice->disc_amount ?? $discount), 2)) ?></td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format($printDiscountTotal, 2)) ?></td>
|
||||
</tr>
|
||||
<tr class="totals">
|
||||
<td colspan="6" class="text-end">Net Amount</td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format((float)($invoice->net_amount ?? $net), 2)) ?></td>
|
||||
<td colspan="2" class="text-end"><?= esc(number_format($printNetTotal, 2)) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ $customerCode = trim((string) ($invoice->patient_code ?? ''));
|
|||
$customerPhone = trim((string) ($invoice->inv_phone_number ?? ($patient->mphone1 ?? '')));
|
||||
$doctorName = trim((string) ($invoice->doc_name ?? ''));
|
||||
$doctorLabel = $doctorName !== '' ? (preg_match('/^dr\.?\s*/i', $doctorName) ? $doctorName : 'Dr. ' . $doctorName) : '-';
|
||||
$items = $items ?? [];
|
||||
$addressLine = $pharmacyAddress;
|
||||
if ($pharmacyState !== '') {
|
||||
$addressLine .= ($addressLine !== '' ? ', ' : '') . $pharmacyState;
|
||||
|
|
@ -26,6 +27,18 @@ $discountTotal = 0.0;
|
|||
$cgstTotal = 0.0;
|
||||
$sgstTotal = 0.0;
|
||||
$netTotal = 0.0;
|
||||
$invoiceLevelDiscountTotal = (float) (($invoice->discount_amount ?? 0) + ($invoice->disc_amount ?? 0) + ($invoice->item_discount_amount ?? 0));
|
||||
$itemCount = count($items);
|
||||
$baseGrossTotal = 0.0;
|
||||
$baseDiscountTotal = 0.0;
|
||||
if (! empty($items)) {
|
||||
foreach ($items as $printItem) {
|
||||
$baseGrossTotal += (float) ($printItem->amount ?? 0);
|
||||
$baseDiscountTotal += (float) ($printItem->disc_amount ?? 0);
|
||||
}
|
||||
}
|
||||
$extraInvoiceDiscountTotal = max(0.0, $invoiceLevelDiscountTotal - $baseDiscountTotal);
|
||||
$allocatedExtraDiscountTotal = 0.0;
|
||||
|
||||
$compact = in_array((int) ($printFormat ?? 0), [1, 2, 3], true);
|
||||
$baseFont = $compact ? '10px' : '11px';
|
||||
|
|
@ -129,8 +142,25 @@ $tdPad = $compact ? '4px 4px' : '5px 5px';
|
|||
<?php foreach ($items as $index => $item): ?>
|
||||
<?php
|
||||
$gross = (float) ($item->amount ?? 0);
|
||||
$disc = (float) ($item->disc_amount ?? ($item->twdisc_amount ?? 0));
|
||||
$net = (float) ($item->twdisc_amount ?? $item->tamount ?? ($item->net_amount ?? 0));
|
||||
$baseDisc = (float) ($item->disc_amount ?? 0);
|
||||
$baseNet = (float) ($item->twdisc_amount ?? $item->tamount ?? ($item->net_amount ?? 0));
|
||||
|
||||
$remainingExtraDiscount = max(0.0, $extraInvoiceDiscountTotal - $allocatedExtraDiscountTotal);
|
||||
$allocExtraDisc = 0.0;
|
||||
if ($remainingExtraDiscount > 0 && $baseGrossTotal > 0) {
|
||||
if ((int) $index === $itemCount - 1) {
|
||||
$allocExtraDisc = $remainingExtraDiscount;
|
||||
} else {
|
||||
$allocExtraDisc = round($extraInvoiceDiscountTotal * ($gross / $baseGrossTotal), 2);
|
||||
if ($allocExtraDisc > $remainingExtraDiscount) {
|
||||
$allocExtraDisc = $remainingExtraDiscount;
|
||||
}
|
||||
}
|
||||
}
|
||||
$allocatedExtraDiscountTotal += $allocExtraDisc;
|
||||
|
||||
$disc = $baseDisc + $allocExtraDisc;
|
||||
$net = max(0.0, $baseNet - $allocExtraDisc);
|
||||
|
||||
$cgstPer = (float) ($item->CGST_per ?? 0);
|
||||
$sgstPer = (float) ($item->SGST_per ?? 0);
|
||||
|
|
@ -179,8 +209,10 @@ $tdPad = $compact ? '4px 4px' : '5px 5px';
|
|||
|
||||
<?php
|
||||
$grossFinal = (float) ($invoice->gross_amount ?? $grossTotal);
|
||||
$discFinal = (float) ($invoice->disc_amount ?? $discountTotal);
|
||||
$netFinal = (float) ($invoice->net_amount ?? $netTotal);
|
||||
$invoiceDiscount = (float) (($invoice->discount_amount ?? 0) + ($invoice->disc_amount ?? 0) + ($invoice->item_discount_amount ?? 0));
|
||||
$discFinal = $invoiceDiscount > 0 ? $invoiceDiscount : $discountTotal;
|
||||
$netFromSummary = $grossFinal - $discFinal;
|
||||
$netFinal = $netFromSummary >= 0 ? $netFromSummary : 0.0;
|
||||
?>
|
||||
|
||||
<table class="summary">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
$deduction = (float) ($paymentSummary['extra_discount'] ?? ($invoice->discount_amount ?? 0));
|
||||
$deductionRemark = (string) ($paymentSummary['discount_remark'] ?? ($invoice->discount_remark ?? ''));
|
||||
$allowPaymentMode = ((int) ($invoice->ipd_credit ?? 0) === 0) && ((int) ($invoice->case_credit ?? 0) === 0) && ((int) ($invoice->group_invoice_id ?? 0) === 0);
|
||||
$discountPercentLimit = (float) ($discountPercentLimit ?? 10.0);
|
||||
$discountAmountLimit = (float) ($discountAmountLimit ?? (((float) ($invoice->gross_amount ?? 0)) * $discountPercentLimit / 100));
|
||||
?>
|
||||
|
||||
<div class="card border-0" id="medical-final-invoice">
|
||||
|
|
@ -145,6 +147,9 @@
|
|||
</div>
|
||||
|
||||
<div class="table-responsive mb-3">
|
||||
<div class="small text-muted mb-2">
|
||||
Max allowed deduction for your login: <?= esc(number_format($discountPercentLimit, 2)) ?>% (<?= esc(number_format($discountAmountLimit, 2)) ?>)
|
||||
</div>
|
||||
<table class="table table-sm table-striped table-bordered align-middle mb-0">
|
||||
<tr>
|
||||
<th style="width: 18px">#</th>
|
||||
|
|
@ -335,9 +340,10 @@
|
|||
btnDed.addEventListener('click', function () {
|
||||
var grossAmount = parseFloat('<?= (float)($invoice->gross_amount ?? 0) ?>');
|
||||
var discountAmount = parseFloat(document.getElementById('input_dis_amt').value || '0');
|
||||
var maxDiscount = grossAmount * 0.12;
|
||||
var discountPercentLimit = parseFloat('<?= (float) ($discountPercentLimit ?? 10.0) ?>');
|
||||
var maxDiscount = grossAmount * discountPercentLimit / 100;
|
||||
if (discountAmount > maxDiscount) {
|
||||
showError('Discount Amount is greater than allowed limit. Max : ' + maxDiscount.toFixed(2));
|
||||
showError('Discount Amount is greater than allowed limit. Max : ' + maxDiscount.toFixed(2) + ' (' + discountPercentLimit.toFixed(2).replace(/\.00$/, '') + '%)');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user