some updates

This commit is contained in:
Flatlogic Bot 2026-05-07 08:08:50 +00:00
parent 82a5fde6bc
commit 8c0d2453dd
13 changed files with 475 additions and 136 deletions

View File

@ -504,8 +504,10 @@ body:not(.theme-default) .form-select:focus {
/* Thermal Receipt Styles */ /* Thermal Receipt Styles */
.thermal-receipt { .thermal-receipt {
width: 80mm; width: 80mm;
max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 10px;
box-sizing: border-box;
font-family: 'Courier New', Courier, monospace; font-family: 'Courier New', Courier, monospace;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
@ -527,11 +529,14 @@ body:not(.theme-default) .form-select:focus {
} }
.thermal-receipt table { .thermal-receipt table {
width: 100%; width: 100%;
table-layout: fixed;
border-collapse: collapse;
} }
.thermal-receipt table th { .thermal-receipt table th {
text-align: left; text-align: left;
border-bottom: 1px dashed #000; border-bottom: 1px dashed #000;
font-size: 10px; font-size: 10px;
word-break: break-word;
} }
.thermal-receipt.rtl table th { .thermal-receipt.rtl table th {
text-align: right; text-align: right;
@ -539,6 +544,7 @@ body:not(.theme-default) .form-select:focus {
.thermal-receipt table td { .thermal-receipt table td {
padding: 5px 0; padding: 5px 0;
font-size: 11px; font-size: 11px;
word-break: break-word;
} }
.thermal-receipt .total-row { .thermal-receipt .total-row {
font-weight: bold; font-weight: bold;
@ -547,9 +553,11 @@ body:not(.theme-default) .form-select:focus {
@media print { @media print {
.thermal-receipt-print { .thermal-receipt-print {
width: 80mm !important; width: 76mm !important;
margin: 0 !important; max-width: 76mm !important;
padding: 5mm !important; margin: 0 auto !important;
padding: 2mm !important;
box-sizing: border-box !important;
} }
body.printing-receipt * { body.printing-receipt * {
visibility: hidden; visibility: hidden;
@ -560,12 +568,21 @@ body:not(.theme-default) .form-select:focus {
} }
body.printing-receipt #posPrintArea { body.printing-receipt #posPrintArea {
visibility: visible !important; visibility: visible !important;
display: block !important; display: flex !important;
justify-content: center !important;
align-items: flex-start !important;
position: fixed !important;
inset: 0 !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
z-index: 9999 !important;
background: #fff !important;
} }
body.printing-receipt .thermal-receipt-print { body.printing-receipt .thermal-receipt-print {
position: absolute; position: static !important;
left: 0; left: auto !important;
top: 0; top: auto !important;
display: block !important; display: block !important;
} }
} }

View File

@ -0,0 +1,34 @@
<?php
// includes/quantity_helper.php
if (!function_exists('quantity_precision')) {
function quantity_precision(): int
{
return 2;
}
}
if (!function_exists('quantity_step')) {
function quantity_step(): string
{
return '0.01';
}
}
if (!function_exists('normalize_quantity')) {
function normalize_quantity($value): float
{
if ($value === null || $value === '') {
return 0.0;
}
return round((float)$value, quantity_precision());
}
}
if (!function_exists('format_quantity')) {
function format_quantity($value): string
{
return number_format(normalize_quantity($value), quantity_precision(), '.', '');
}
}

View File

@ -16,8 +16,10 @@ if (!function_exists('update_stock')) {
// However, for extra safety or validation, we could check. // However, for extra safety or validation, we could check.
// But for now, simple update is best. // But for now, simple update is best.
$normalizedQty = function_exists('normalize_quantity') ? normalize_quantity($qty) : round((float)$qty, 2);
$db = db(); $db = db();
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); $stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
$stmt->execute([$qty, $item_id]); $stmt->execute([$normalizedQty, $item_id]);
} }
} }

250
index.php
View File

@ -467,6 +467,7 @@ if (!function_exists('register_wablas_helper_fallback')) {
runtime_debug_boot_mark('boot:loading_core_dependencies'); runtime_debug_boot_mark('boot:loading_core_dependencies');
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/SimpleXLSX.php'; require_once __DIR__ . '/includes/SimpleXLSX.php';
require_once __DIR__ . '/includes/quantity_helper.php';
require_once __DIR__ . '/includes/stock_helper.php'; require_once __DIR__ . '/includes/stock_helper.php';
$wablasHelperPath = __DIR__ . '/includes/wablas_helper.php'; $wablasHelperPath = __DIR__ . '/includes/wablas_helper.php';
if (is_file($wablasHelperPath)) { if (is_file($wablasHelperPath)) {
@ -1860,7 +1861,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
// Render Card HTML // Render Card HTML
?> ?>
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>" data-vat-rate="<?= $p['vat_rate'] ?>"> <div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
<?php if ($p['image_path']): ?> <?php if ($p['image_path']): ?>
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>"> <img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
<?php else: ?> <?php else: ?>
@ -1883,7 +1884,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
<?php endif; ?> <?php endif; ?>
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span> <span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
</div> </div>
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span> <span class="badge bg-light text-dark small"><?= format_quantity($p['stock_quantity']) ?> left</span>
</div> </div>
</div> </div>
<?php <?php
@ -1919,9 +1920,9 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
echo json_encode(['error' => 'This item cannot use price-based scale barcodes because its sale price is zero.']); echo json_encode(['error' => 'This item cannot use price-based scale barcodes because its sale price is zero.']);
exit; exit;
} }
$qty = round(((float)$weightBarcode['value']) / (float)$p['sale_price'], 3); $qty = normalize_quantity(((float)$weightBarcode['value']) / (float)$p['sale_price']);
} else { } else {
$qty = round((float)$weightBarcode['value'], 3); $qty = normalize_quantity((float)$weightBarcode['value']);
} }
if ($qty <= 0) { if ($qty <= 0) {
@ -2003,6 +2004,12 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null; $customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$payments = json_decode($_POST['payments'] ?? '[]', true); $payments = json_decode($_POST['payments'] ?? '[]', true);
$items = json_decode($_POST['items'] ?? '[]', true); $items = json_decode($_POST['items'] ?? '[]', true);
if (!is_array($items)) {
$items = [];
}
foreach ($items as $itemIndex => $item) {
$items[$itemIndex]['qty'] = normalize_quantity($item['qty'] ?? 0);
}
$total_amount = (float)($_POST['total_amount'] ?? 0); $total_amount = (float)($_POST['total_amount'] ?? 0);
$tax_amount = (float)($_POST['tax_amount'] ?? 0); $tax_amount = (float)($_POST['tax_amount'] ?? 0);
$discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null; $discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null;
@ -2833,8 +2840,8 @@ function getPromotionalPrice($item) {
} }
$sale_price = (float)($_POST['sale_price'] ?? 0); $sale_price = (float)($_POST['sale_price'] ?? 0);
$purchase_price = (float)($_POST['purchase_price'] ?? 0); $purchase_price = (float)($_POST['purchase_price'] ?? 0);
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0); $stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0); $min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
$vat_rate = (float)($_POST['vat_rate'] ?? 0); $vat_rate = (float)($_POST['vat_rate'] ?? 0);
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null; $expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0; $is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
@ -2869,8 +2876,8 @@ function getPromotionalPrice($item) {
} }
$sale_price = (float)($_POST['sale_price'] ?? 0); $sale_price = (float)($_POST['sale_price'] ?? 0);
$purchase_price = (float)($_POST['purchase_price'] ?? 0); $purchase_price = (float)($_POST['purchase_price'] ?? 0);
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0); $stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0); $min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
$vat_rate = (float)($_POST['vat_rate'] ?? 0); $vat_rate = (float)($_POST['vat_rate'] ?? 0);
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null; $expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0; $is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
@ -3014,7 +3021,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3045,7 +3052,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3080,7 +3087,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3103,7 +3110,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3145,7 +3152,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3177,7 +3184,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3213,7 +3220,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3235,7 +3242,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3303,7 +3310,7 @@ function getPromotionalPrice($item) {
$name_ar = trim((string)($row[2] ?? '')); $name_ar = trim((string)($row[2] ?? ''));
$sale_price = (float)($row[3] ?? 0); $sale_price = (float)($row[3] ?? 0);
$purchase_price = (float)($row[4] ?? 0); $purchase_price = (float)($row[4] ?? 0);
$qty = (float)($row[5] ?? 0); $qty = normalize_quantity($row[5] ?? 0);
$vat_rate = (float)($row[6] ?? 0); $vat_rate = (float)($row[6] ?? 0);
$check = db()->prepare("SELECT id FROM stock_items WHERE sku = ? AND outlet_id = ?"); $check = db()->prepare("SELECT id FROM stock_items WHERE sku = ? AND outlet_id = ?");
@ -3580,7 +3587,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3612,7 +3619,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3651,7 +3658,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3673,7 +3680,7 @@ function getPromotionalPrice($item) {
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -3889,7 +3896,7 @@ if (isset($_POST['add_hr_department'])) {
$total_return = 0; $total_return = 0;
foreach ($quantities as $i => $qty) { foreach ($quantities as $i => $qty) {
$total_return += (float)$qty * (float)$prices[$i]; $total_return += normalize_quantity($qty) * (float)$prices[$i];
} }
// Insert Sales Return // Insert Sales Return
@ -3911,7 +3918,7 @@ if (isset($_POST['add_hr_department'])) {
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?"); // $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) { foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i]; $qty = normalize_quantity($quantities[$i] ?? 0);
if ($qty > 0) { if ($qty > 0) {
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$line_total = $qty * $price; $line_total = $qty * $price;
@ -3949,7 +3956,7 @@ if (isset($_POST['add_hr_department'])) {
$total_return = 0; $total_return = 0;
foreach ($quantities as $i => $qty) { foreach ($quantities as $i => $qty) {
$total_return += (float)$qty * (float)$prices[$i]; $total_return += normalize_quantity($qty) * (float)$prices[$i];
} }
// Insert Purchase Return // Insert Purchase Return
@ -3971,7 +3978,7 @@ if (isset($_POST['add_hr_department'])) {
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?"); // $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) { foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i]; $qty = normalize_quantity($quantities[$i] ?? 0);
if ($qty > 0) { if ($qty > 0) {
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$line_total = $qty * $price; $line_total = $qty * $price;
@ -7333,8 +7340,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</td> </td>
<td> <td>
<div class="text-end"> <div class="text-end">
<strong><?= number_format((float)$item['stock_quantity'], 3) ?></strong> <strong><?= format_quantity($item['stock_quantity']) ?></strong>
<div class="small text-muted">Min: <?= number_format((float)$item['min_stock_level'], 3) ?></div> <div class="small text-muted">Min: <?= format_quantity($item['min_stock_level']) ?></div>
<?php if ($item['stock_quantity'] <= $item['min_stock_level']): ?> <?php if ($item['stock_quantity'] <= $item['min_stock_level']): ?>
<span class="badge bg-danger" data-en="Low Stock" data-ar="مخزون منخفض">Low Stock</span> <span class="badge bg-danger" data-en="Low Stock" data-ar="مخزون منخفض">Low Stock</span>
<?php endif; ?> <?php endif; ?>
@ -7385,7 +7392,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<tr><th class="text-muted" data-en="Category" data-ar="الفئة">Category</th><td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td></tr> <tr><th class="text-muted" data-en="Category" data-ar="الفئة">Category</th><td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td></tr>
<tr><th class="text-muted" data-en="Supplier" data-ar="المورد">Supplier</th><td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td></tr> <tr><th class="text-muted" data-en="Supplier" data-ar="المورد">Supplier</th><td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td></tr>
<tr><th class="text-muted">Sale Price</th><td>OMR <?= number_format((float)$item['sale_price'], 3) ?></td></tr> <tr><th class="text-muted">Sale Price</th><td>OMR <?= number_format((float)$item['sale_price'], 3) ?></td></tr>
<tr><th class="text-muted">Stock Level</th><td><?= number_format((float)$item['stock_quantity'], 3) ?></td></tr> <tr><th class="text-muted">Stock Level</th><td><?= format_quantity($item['stock_quantity']) ?></td></tr>
<tr><th class="text-muted">VAT Rate</th><td><?= number_format((float)$item['vat_rate'], 2) ?>%</td></tr> <tr><th class="text-muted">VAT Rate</th><td><?= number_format((float)$item['vat_rate'], 2) ?>%</td></tr>
</table> </table>
</div> </div>
@ -7420,8 +7427,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>" <?= $s['id'] == $item['supplier_id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div> <div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>" <?= $s['id'] == $item['supplier_id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>"></div> <div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>"></div>
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>"></div> <div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>"></div>
<div class="col-md-4"><label class="form-label" data-en="Stock Qty" data-ar="كمية المخزون">Stock Qty</label><input type="number" step="0.001" name="stock_quantity" class="form-control" value="<?= (float)$item['stock_quantity'] ?>"></div> <div class="col-md-4"><label class="form-label" data-en="Stock Qty" data-ar="كمية المخزون">Stock Qty</label><input type="number" step="<?= quantity_step() ?>" name="stock_quantity" class="form-control" value="<?= format_quantity($item['stock_quantity']) ?>"></div>
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="0.001" name="min_stock_level" class="form-control" value="<?= (float)$item['min_stock_level'] ?>"></div> <div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="<?= quantity_step() ?>" name="min_stock_level" class="form-control" value="<?= format_quantity($item['min_stock_level']) ?>"></div>
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= number_format((float)$item['vat_rate'], 2, '.', '') ?>"></div> <div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= number_format((float)$item['vat_rate'], 2, '.', '') ?>"></div>
<div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div> <div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div>
<div class="col-12 full-width"><hr><h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6></div> <div class="col-12 full-width"><hr><h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6></div>
@ -7498,7 +7505,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div> <div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div>
</td> </td>
<td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td> <td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
<td><?= number_format((float)$item['stock_quantity'], 3) ?></td> <td><?= format_quantity($item['stock_quantity']) ?></td>
<td><?= $expiry_date !== '' ? htmlspecialchars((string)$expiry_date) : '---' ?></td> <td><?= $expiry_date !== '' ? htmlspecialchars((string)$expiry_date) : '---' ?></td>
<td> <td>
<?php if ($is_expired): ?> <?php if ($is_expired): ?>
@ -7555,10 +7562,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</td> </td>
<td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td> <td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
<td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td> <td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td>
<td><?= number_format((float)$item['min_stock_level'], 2) ?></td> <td><?= format_quantity($item['min_stock_level']) ?></td>
<td> <td>
<span class="badge <?= (float)$item['stock_quantity'] <= 0 ? 'bg-danger' : 'bg-warning text-dark' ?>"> <span class="badge <?= (float)$item['stock_quantity'] <= 0 ? 'bg-danger' : 'bg-warning text-dark' ?>">
<?= number_format((float)$item['stock_quantity'], 3) ?> <?= format_quantity($item['stock_quantity']) ?>
</span> </span>
</td> </td>
<td class="fw-bold text-danger"><?= number_format((float)$shortage, 3) ?></td> <td class="fw-bold text-danger"><?= number_format((float)$shortage, 3) ?></td>
@ -7697,7 +7704,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</div> </div>
<div class="product-grid" id="productGrid"> <div class="product-grid" id="productGrid">
<?php foreach ($products as $p): ?> <?php foreach ($products as $p): ?>
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>" data-vat-rate="<?= $p['vat_rate'] ?>"> <div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= format_quantity($p['stock_quantity']) ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
<?php if ($p['image_path']): ?> <?php if ($p['image_path']): ?>
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>"> <img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
<?php else: ?> <?php else: ?>
@ -7720,7 +7727,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<?php endif; ?> <?php endif; ?>
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span> <span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
</div> </div>
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span> <span class="badge bg-light text-dark small"><?= format_quantity($p['stock_quantity']) ?> left</span>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
@ -7746,7 +7753,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<label class="small fw-bold mb-1" data-en="Customer" data-ar="العميل">Customer</label> <label class="small fw-bold mb-1" data-en="Customer" data-ar="العميل">Customer</label>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<select id="posCustomer" class="form-select form-select-sm" onchange="cart.onCustomerChange()"> <select id="posCustomer" class="form-select form-select-sm" onchange="cart.onCustomerChange()">
<option value="">Walk-in Customer</option> <option value=""><?= __('walk_in_customer') ?></option>
<?php foreach ($customers as $c): ?> <?php foreach ($customers as $c): ?>
<option value="<?= $c['id'] ?>" <option value="<?= $c['id'] ?>"
data-points="<?= $c['loyalty_points'] ?>" data-points="<?= $c['loyalty_points'] ?>"
@ -7841,10 +7848,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
// Ensure items is an array // Ensure items is an array
if (!Array.isArray(this.items)) this.items = []; if (!Array.isArray(this.items)) this.items = [];
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * parseFloat(item.qty)), 0); const subtotal = this.items.reduce((sum, item) => sum + ((parseFloat(item.price) || 0) * normalizeQuantity(item.qty)), 0);
const totalVat = this.items.reduce((sum, item) => { const totalVat = this.items.reduce((sum, item) => {
const price = parseFloat(item.price) || 0; const price = parseFloat(item.price) || 0;
const qty = parseFloat(item.qty) || 0; const qty = normalizeQuantity(item.qty);
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0; const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
return sum + (price * qty * (vatRate / (100 + vatRate))); return sum + (price * qty * (vatRate / (100 + vatRate)));
}, 0); }, 0);
@ -7890,8 +7897,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
add(product) { add(product) {
if (!this.items) this.items = []; if (!this.items) this.items = [];
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1'); const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(product.stock_quantity) || 0; const currentStock = normalizeQuantity(product.stock_quantity);
const addQty = Math.max(parseFloat(product.qty) || 1, 0.001); const addQty = Math.max(normalizeQuantity((product.qty !== undefined && product.qty !== null) ? product.qty : 1), 0.01);
const unitPrice = (product.price !== undefined && product.price !== null) ? (parseFloat(product.price) || 0) : (parseFloat(product.sale_price) || 0); const unitPrice = (product.price !== undefined && product.price !== null) ? (parseFloat(product.price) || 0) : (parseFloat(product.sale_price) || 0);
const normalizedProduct = {...product, price: unitPrice}; const normalizedProduct = {...product, price: unitPrice};
@ -7901,14 +7908,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
Swal.fire('Error', 'Insufficient stock!', 'error'); Swal.fire('Error', 'Insufficient stock!', 'error');
return; return;
} }
existing.qty = Number((existing.qty + addQty).toFixed(3)); existing.qty = normalizeQuantity(existing.qty + addQty);
existing.price = unitPrice; existing.price = unitPrice;
} else { } else {
if (!allowZeroStock && currentStock < addQty) { if (!allowZeroStock && currentStock < addQty) {
Swal.fire('Error', 'Insufficient stock!', 'error'); Swal.fire('Error', 'Insufficient stock!', 'error');
return; return;
} }
this.items.push({...normalizedProduct, qty: Number(addQty.toFixed(3))}); this.items.push({...normalizedProduct, qty: normalizeQuantity(addQty)});
} }
this.render(); this.render();
@ -7934,14 +7941,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
const item = this.items.find(i => i.id === id); const item = this.items.find(i => i.id === id);
if (item) { if (item) {
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1'); const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(item.stock_quantity) || 0; const currentStock = normalizeQuantity(item.stock_quantity);
if (delta > 0 && !allowZeroStock && (item.qty + delta) > currentStock) { if (delta > 0 && !allowZeroStock && (item.qty + delta) > currentStock) {
Swal.fire('Error', 'Insufficient stock!', 'error'); Swal.fire('Error', 'Insufficient stock!', 'error');
return; return;
} }
item.qty += delta; item.qty = normalizeQuantity(item.qty + delta);
if (item.qty <= 0) this.remove(id); if (item.qty <= 0) this.remove(id);
else this.render(); else this.render();
} }
@ -8107,7 +8114,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
const carts = await resp.json(); const carts = await resp.json();
const c = carts.find(x => x.id == id); const c = carts.find(x => x.id == id);
if (c) { if (c) {
this.items = JSON.parse(c.items_json); this.items = (JSON.parse(c.items_json) || []).map(item => ({...item, qty: normalizeQuantity(item.qty)}));
document.getElementById('posCustomer').value = c.customer_id || ''; document.getElementById('posCustomer').value = c.customer_id || '';
await this.onCustomerChange(); await this.onCustomerChange();
await this.deleteHeld(id, true); await this.deleteHeld(id, true);
@ -8150,7 +8157,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
let totalVat = 0; let totalVat = 0;
container.innerHTML = items.map(item => { container.innerHTML = items.map(item => {
const price = parseFloat(item.price) || 0; const price = parseFloat(item.price) || 0;
const qty = parseFloat(item.qty) || 0; const qty = normalizeQuantity(item.qty);
const itemTotal = price * qty; const itemTotal = price * qty;
subtotal += itemTotal; subtotal += itemTotal;
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0; const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
@ -8170,7 +8177,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</div> </div>
<div class="qty-controls mx-3"> <div class="qty-controls mx-3">
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button> <button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
<span class="small fw-bold">${qty}</span> <span class="small fw-bold">${formatQuantity(qty)}</span>
<button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button> <button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button>
</div> </div>
<div class="text-end" style="min-width: 80px;"> <div class="text-end" style="min-width: 80px;">
@ -8328,10 +8335,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
renderPayments() { renderPayments() {
const container = document.getElementById('paymentList'); const container = document.getElementById('paymentList');
const methodLabels = { const methodLabels = {
'cash': 'Cash', 'cash': <?= json_encode($lang === 'ar' ? 'نقدًا' : 'Cash', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
'card': 'Credit Card', 'card': <?= json_encode($lang === 'ar' ? 'بطاقة ائتمان' : 'Credit Card', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
'credit': 'Credit', 'credit': <?= json_encode($lang === 'ar' ? 'آجل' : 'Credit', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
'transfer': 'Bank Transfer' 'transfer': <?= json_encode($lang === 'ar' ? 'تحويل بنكي' : 'Bank Transfer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
}; };
container.innerHTML = this.payments.map((p, i) => ` container.innerHTML = this.payments.map((p, i) => `
<div class="payment-line"> <div class="payment-line">
@ -8381,7 +8388,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
}, },
async completeOrder() { async completeOrder() {
if (this.items.length === 0) { if (this.items.length === 0) {
Swal.fire('Error', 'Cart is empty', 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'السلة فارغة' : 'Cart is empty', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
return; return;
} }
@ -8404,20 +8411,20 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
const remaining = this.getRemaining(); const remaining = this.getRemaining();
if (remaining > 0.001) { if (remaining > 0.001) {
Swal.fire('Error', 'Payment is incomplete', 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'الدفع غير مكتمل' : 'Payment is incomplete', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
return; return;
} }
const customerId = document.getElementById('posCustomer').value; const customerId = document.getElementById('posCustomer').value;
if (this.payments.some(p => p.method === 'credit') && !customerId) { if (this.payments.some(p => p.method === 'credit') && !customerId) {
Swal.fire('Error', 'Credit payment is only allowed for registered customers', 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, <?= json_encode($lang === 'ar' ? 'الدفع الآجل مسموح للعملاء المسجلين فقط' : 'Credit payment is only allowed for registered customers', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
return; return;
} }
const btn = document.getElementById('confirmPaymentBtn'); const btn = document.getElementById('confirmPaymentBtn');
const originalText = btn.innerText; const originalText = btn.innerText;
btn.disabled = true; btn.disabled = true;
btn.innerText = 'PROCESSING...'; btn.innerText = <?= json_encode($lang === 'ar' ? 'جارٍ المعالجة...' : 'PROCESSING...', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0); const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0);
const totalVat = this.items.reduce((sum, item) => { const totalVat = this.items.reduce((sum, item) => {
@ -8454,7 +8461,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
result = JSON.parse(text); result = JSON.parse(text);
} catch (e) { } catch (e) {
console.error('Invalid JSON response:', text); console.error('Invalid JSON response:', text);
throw new Error('Server returned an invalid response'); throw new Error(<?= json_encode($lang === 'ar' ? 'أعاد الخادم استجابة غير صالحة' : 'Server returned an invalid response', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>);
} }
if (result.success) { if (result.success) {
@ -8462,13 +8469,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
if (payModal) payModal.hide(); if (payModal) payModal.hide();
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no); this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
} else { } else {
Swal.fire('Error', result.error, 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, result.error, 'error');
btn.disabled = false; btn.disabled = false;
btn.innerText = originalText; btn.innerText = originalText;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
Swal.fire('Error', err.message || 'Something went wrong', 'error'); Swal.fire(<?= json_encode($lang === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, err.message || <?= json_encode($lang === 'ar' ? 'حدث خطأ ما' : 'Something went wrong', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, 'error');
btn.disabled = false; btn.disabled = false;
btn.innerText = originalText; btn.innerText = originalText;
} }
@ -8476,7 +8483,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) { showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) {
const container = document.getElementById('posReceiptContent'); const container = document.getElementById('posReceiptContent');
const customerSelect = document.getElementById('posCustomer'); const customerSelect = document.getElementById('posCustomer');
const customerName = (customerSelect && customerSelect.selectedIndex >= 0 && customerSelect.value !== '') ? customerSelect.options[customerSelect.selectedIndex].text : '<?= $translations['ar']['walk_in_customer'] ?> / <?= $translations['en']['walk_in_customer'] ?>'; const customerName = (customerSelect && customerSelect.selectedIndex >= 0 && customerSelect.value !== '') ? customerSelect.options[customerSelect.selectedIndex].text : <?= json_encode(__('walk_in_customer'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const paymentsHtml = this.payments.map(p => { const paymentsHtml = this.payments.map(p => {
let m = p.method.toLowerCase(); let m = p.method.toLowerCase();
let methodAr = m === 'cash' ? 'نقد' : (m === 'card' ? 'بطاقة ائتمان' : (m === 'credit' ? 'آجل' : (m === 'transfer' ? 'تحويل بنكي' : m))); let methodAr = m === 'cash' ? 'نقد' : (m === 'card' ? 'بطاقة ائتمان' : (m === 'credit' ? 'آجل' : (m === 'transfer' ? 'تحويل بنكي' : m)));
@ -8500,7 +8507,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<td> <td>
<div class="fw-bold">${item.nameAr || ''}</div> <div class="fw-bold">${item.nameAr || ''}</div>
<div>${item.nameEn}</div> <div>${item.nameEn}</div>
<small>${item.qty} x ${parseFloat(item.price).toFixed(3)}</small> <small>${formatQuantity(item.qty)} x ${parseFloat(item.price).toFixed(3)}</small>
</td> </td>
<td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td> <td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td>
<td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td> <td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td>
@ -8702,11 +8709,11 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
$(document).ready(function() { $(document).ready(function() {
$('#posCustomer').select2({ $('#posCustomer').select2({
width: '100%', width: '100%',
placeholder: 'Select Customer' placeholder: <?= json_encode($lang === 'ar' ? 'اختر العميل' : 'Select Customer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
}); });
$('#paymentCreditCustomer').select2({ $('#paymentCreditCustomer').select2({
width: '100%', width: '100%',
placeholder: 'Select Customer', placeholder: <?= json_encode($lang === 'ar' ? 'اختر العميل' : 'Select Customer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
dropdownParent: $('#posPaymentModal') dropdownParent: $('#posPaymentModal')
}); });
@ -8723,13 +8730,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Open Cash Register</h5> <h5 class="modal-title" data-en="Open Cash Register" data-ar="فتح الخزينة"><?= $lang === 'ar' ? 'فتح الخزينة' : 'Open Cash Register' ?></h5>
</div> </div>
<form method="POST"> <form method="POST">
<div class="modal-body"> <div class="modal-body">
<input type="hidden" name="open_register" value="1"> <input type="hidden" name="open_register" value="1">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Select Register</label> <label class="form-label" data-en="Select Register" data-ar="اختر الخزينة"><?= $lang === 'ar' ? 'اختر الخزينة' : 'Select Register' ?></label>
<select name="register_id" class="form-select" required> <select name="register_id" class="form-select" required>
<?php if (isset($registers)): foreach ($registers as $r): ?> <?php if (isset($registers)): foreach ($registers as $r): ?>
<option value="<?= $r['id'] ?>"><?= htmlspecialchars($r['name']) ?></option> <option value="<?= $r['id'] ?>"><?= htmlspecialchars($r['name']) ?></option>
@ -8737,7 +8744,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Opening Balance</label> <label class="form-label" data-en="Opening Balance" data-ar="الرصيد الافتتاحي"><?= $lang === 'ar' ? 'الرصيد الافتتاحي' : 'Opening Balance' ?></label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text">OMR</span> <span class="input-group-text">OMR</span>
<input type="number" step="0.001" name="opening_balance" class="form-control" required placeholder="0.000"> <input type="number" step="0.001" name="opening_balance" class="form-control" required placeholder="0.000">
@ -8745,8 +8752,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="<?= htmlspecialchars(page_url('dashboard')) ?>" class="btn btn-secondary">Cancel & Go to Dashboard</a> <a href="<?= htmlspecialchars(page_url('dashboard')) ?>" class="btn btn-secondary" data-en="Cancel & Go to Dashboard" data-ar="إلغاء والعودة إلى لوحة التحكم"><?= $lang === 'ar' ? 'إلغاء والعودة إلى لوحة التحكم' : 'Cancel & Go to Dashboard' ?></a>
<button type="submit" class="btn btn-primary">Open Session</button> <button type="submit" class="btn btn-primary" data-en="Open Session" data-ar="فتح الجلسة"><?= $lang === 'ar' ? 'فتح الجلسة' : 'Open Session' ?></button>
</div> </div>
</form> </form>
</div> </div>
@ -8760,7 +8767,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Close Cash Register</h5> <h5 class="modal-title" data-en="Close Cash Register" data-ar="إغلاق الخزينة"><?= $lang === 'ar' ? 'إغلاق الخزينة' : 'Close Cash Register' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<form method="POST"> <form method="POST">
@ -8769,20 +8776,20 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<input type="hidden" name="redirect_to" value="dashboard"> <input type="hidden" name="redirect_to" value="dashboard">
<input type="hidden" name="session_id" value="<?= $_SESSION['register_session_id'] ?? '' ?>"> <input type="hidden" name="session_id" value="<?= $_SESSION['register_session_id'] ?? '' ?>">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Cash in Hand (Counted)</label> <label class="form-label" data-en="Cash in Hand (Counted)" data-ar="النقد في الخزينة (المعدود)"><?= $lang === 'ar' ? 'النقد في الخزينة (المعدود)' : 'Cash in Hand (Counted)' ?></label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text">OMR</span> <span class="input-group-text">OMR</span>
<input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000"> <input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000">
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Notes</label> <label class="form-label" data-en="Notes" data-ar="ملاحظات"><?= $lang === 'ar' ? 'ملاحظات' : 'Notes' ?></label>
<textarea name="notes" class="form-control" rows="3" placeholder="Any discrepancies or notes..."></textarea> <textarea name="notes" class="form-control" rows="3" placeholder="<?= htmlspecialchars($lang === 'ar' ? 'أي فروقات أو ملاحظات...' : 'Any discrepancies or notes...', ENT_QUOTES) ?>"></textarea>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" class="btn btn-danger">Close Session</button> <button type="submit" class="btn btn-danger" data-en="Close Session" data-ar="إغلاق الجلسة"><?= $lang === 'ar' ? 'إغلاق الجلسة' : 'Close Session' ?></button>
</div> </div>
</form> </form>
</div> </div>
@ -11680,8 +11687,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div> <div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="0.000"></div> <div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="0.000"></div>
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="0.000"></div> <div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="0.000"></div>
<div class="col-md-4"><label class="form-label" data-en="Initial Stock" data-ar="المخزون الحالي">Initial Stock</label><input type="number" step="0.001" name="stock_quantity" class="form-control" value="0.000"></div> <div class="col-md-4"><label class="form-label" data-en="Initial Stock" data-ar="المخزون الحالي">Initial Stock</label><input type="number" step="<?= quantity_step() ?>" name="stock_quantity" class="form-control" value="0.00"></div>
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="0.001" name="min_stock_level" class="form-control" value="0.000"></div> <div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="<?= quantity_step() ?>" name="min_stock_level" class="form-control" value="0.00"></div>
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="0"></div> <div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="0"></div>
<div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div> <div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div>
<div class="col-md-12 full-width"><div class="form-check form-switch mt-4"><input class="form-check-input" type="checkbox" id="hasExpiryToggle"><label class="form-check-label" for="hasExpiryToggle" data-en="Has Expiry Date?" data-ar="هل له تاريخ انتهاء؟">Has Expiry Date?</label></div></div> <div class="col-md-12 full-width"><div class="form-check form-switch mt-4"><input class="form-check-input" type="checkbox" id="hasExpiryToggle"><label class="form-check-label" for="hasExpiryToggle" data-en="Has Expiry Date?" data-ar="هل له تاريخ انتهاء؟">Has Expiry Date?</label></div></div>
@ -11970,9 +11977,72 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
</div> </div>
<script> <script>
window.quantityPrecision = 2;
window.quantityStep = '0.01';
window.quantityFieldSelector = 'input[name="stock_quantity"], input[name="min_stock_level"], input[name="quantities[]"], input.item-qty';
window.normalizeQuantity = window.normalizeQuantity || function(value) {
const parsed = Number.parseFloat(value);
if (!Number.isFinite(parsed)) {
return 0;
}
return Number(parsed.toFixed(window.quantityPrecision));
};
window.formatQuantity = window.formatQuantity || function(value) {
return window.normalizeQuantity(value).toFixed(window.quantityPrecision);
};
window.trimQuantityInput = window.trimQuantityInput || function(input) {
if (!input) return '';
let value = String(input.value ?? '').replace(/[^0-9.]/g, '');
if (value === '') {
input.value = '';
return '';
}
const firstDot = value.indexOf('.');
if (firstDot !== -1) {
value = value.slice(0, firstDot + 1) + value.slice(firstDot + 1).replace(/\./g, '');
}
const parts = value.split('.');
if (parts.length === 2) {
value = parts[0] + '.' + parts[1].slice(0, window.quantityPrecision);
}
input.value = value;
return value;
};
window.isQuantityInput = window.isQuantityInput || function(input) {
return !!(input && typeof input.matches === 'function' && input.matches(window.quantityFieldSelector));
};
window.syncQuantityInputs = window.syncQuantityInputs || function(root = document) {
const scope = root && typeof root.querySelectorAll === 'function' ? root : document;
scope.querySelectorAll(window.quantityFieldSelector).forEach((input) => {
input.step = window.quantityStep;
input.setAttribute('inputmode', 'decimal');
});
};
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log("DOM Content Loaded - Accounting System"); console.log("DOM Content Loaded - Accounting System");
try { try {
if (typeof window.syncQuantityInputs === 'function') {
window.syncQuantityInputs();
}
if (!window.quantityInputListenersBound) {
document.addEventListener('input', function(event) {
if (window.isQuantityInput && window.isQuantityInput(event.target)) {
window.trimQuantityInput(event.target);
}
});
document.addEventListener('blur', function(event) {
if (window.isQuantityInput && window.isQuantityInput(event.target) && event.target.value !== '') {
event.target.value = window.formatQuantity(event.target.value);
}
}, true);
window.quantityInputListenersBound = true;
}
// Initialize Select2 for all searchable dropdowns // Initialize Select2 for all searchable dropdowns
$('.select2').each(function() { $('.select2').each(function() {
$(this).select2({ $(this).select2({
@ -12136,7 +12206,7 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content border-0"> <div class="modal-content border-0">
<div class="modal-header border-0 pb-0"> <div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Payment</h5> <h5 class="modal-title fw-bold" data-en="Payment" data-ar="الدفع"><?= $lang === 'ar' ? 'الدفع' : 'Payment' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -12144,14 +12214,14 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<span class="small text-muted d-block" data-en="Customer" data-ar="العميل">Customer</span> <span class="small text-muted d-block" data-en="Customer" data-ar="العميل">Customer</span>
<span class="h6 fw-bold m-0 text-primary" id="paymentCustomerName">Walk-in Customer</span> <span class="h6 fw-bold m-0 text-primary" id="paymentCustomerName"><?= __('walk_in_customer') ?></span>
</div> </div>
<i class="bi bi-person-circle fs-3 text-secondary"></i> <i class="bi bi-person-circle fs-3 text-secondary"></i>
</div> </div>
<div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;"> <div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;">
<label class="form-label smaller fw-bold mb-1">Select Credit Customer</label> <label class="form-label smaller fw-bold mb-1" data-en="Select Credit Customer" data-ar="اختر عميل الآجل"><?= $lang === 'ar' ? 'اختر عميل الآجل' : 'Select Credit Customer' ?></label>
<select id="paymentCreditCustomer" class="form-select form-select-sm select2" onchange="cart.syncCustomer(this.value)"> <select id="paymentCreditCustomer" class="form-select form-select-sm select2" onchange="cart.syncCustomer(this.value)">
<option value="">--- Select Customer ---</option> <option value=""><?= $lang === 'ar' ? '--- اختر العميل ---' : '--- Select Customer ---' ?></option>
<?php foreach ($customers as $c): ?> <?php foreach ($customers as $c): ?>
<option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option> <option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option>
<?php endforeach; ?> <?php endforeach; ?>
@ -12162,11 +12232,11 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="amount-due-box mb-2"> <div class="amount-due-box mb-2">
<div class="d-flex justify-content-between px-3"> <div class="d-flex justify-content-between px-3">
<div class="text-start"> <div class="text-start">
<div class="label">Amount Due</div> <div class="label" data-en="Amount Due" data-ar="المبلغ المستحق"><?= $lang === 'ar' ? 'المبلغ المستحق' : 'Amount Due' ?></div>
<div class="value" id="paymentAmountDue">0.000</div> <div class="value" id="paymentAmountDue">0.000</div>
</div> </div>
<div class="text-end"> <div class="text-end">
<div class="label text-danger">Remaining</div> <div class="label text-danger" data-en="Remaining" data-ar="المتبقي"><?= $lang === 'ar' ? 'المتبقي' : 'Remaining' ?></div>
<div class="value text-danger" id="paymentRemaining">0.000</div> <div class="value text-danger" id="paymentRemaining">0.000</div>
</div> </div>
</div> </div>
@ -12177,23 +12247,23 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
<div class="mb-2 p-2 border rounded bg-light"> <div class="mb-2 p-2 border rounded bg-light">
<label class="form-label small fw-bold mb-1">Add Payment Method</label> <label class="form-label small fw-bold mb-1" data-en="Add Payment Method" data-ar="إضافة طريقة دفع"><?= $lang === 'ar' ? 'إضافة طريقة دفع' : 'Add Payment Method' ?></label>
<div class="payment-methods-grid mb-2"> <div class="payment-methods-grid mb-2">
<div class="payment-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)"> <div class="payment-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)">
<i class="bi bi-cash-stack"></i> <i class="bi bi-cash-stack"></i>
<span class="small fw-bold">Cash</span> <span class="small fw-bold" data-en="Cash" data-ar="نقدًا"><?= $lang === 'ar' ? 'نقدًا' : 'Cash' ?></span>
</div> </div>
<div class="payment-method-btn" data-method="card" onclick="cart.selectMethod('card', this)"> <div class="payment-method-btn" data-method="card" onclick="cart.selectMethod('card', this)">
<i class="bi bi-credit-card"></i> <i class="bi bi-credit-card"></i>
<span class="small fw-bold">Credit Card</span> <span class="small fw-bold" data-en="Credit Card" data-ar="بطاقة ائتمان"><?= $lang === 'ar' ? 'بطاقة ائتمان' : 'Credit Card' ?></span>
</div> </div>
<div class="payment-method-btn" data-method="credit" onclick="cart.selectMethod('credit', this)"> <div class="payment-method-btn" data-method="credit" onclick="cart.selectMethod('credit', this)">
<i class="bi bi-person-badge"></i> <i class="bi bi-person-badge"></i>
<span class="small fw-bold">Credit</span> <span class="small fw-bold" data-en="Credit" data-ar="آجل"><?= $lang === 'ar' ? 'آجل' : 'Credit' ?></span>
</div> </div>
<div class="payment-method-btn" data-method="transfer" onclick="cart.selectMethod('transfer', this)"> <div class="payment-method-btn" data-method="transfer" onclick="cart.selectMethod('transfer', this)">
<i class="bi bi-bank"></i> <i class="bi bi-bank"></i>
<span class="small fw-bold">Bank Transfer</span> <span class="small fw-bold" data-en="Bank Transfer" data-ar="تحويل بنكي"><?= $lang === 'ar' ? 'تحويل بنكي' : 'Bank Transfer' ?></span>
</div> </div>
</div> </div>
@ -12205,8 +12275,8 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()"> <button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()" data-en="Add" data-ar="إضافة">
<i class="bi bi-plus-lg"></i data-en="Add" data-ar="إضافة">ADD</button> <i class="bi bi-plus-lg"></i><?= $lang === 'ar' ? 'إضافة' : 'Add' ?></button>
</div> </div>
</div> </div>
@ -12221,16 +12291,16 @@ document.addEventListener('DOMContentLoaded', function() {
<div id="cashPaymentSection" style="display: none;"> <div id="cashPaymentSection" style="display: none;">
<div class="d-flex justify-content-between align-items-center p-3 bg-primary-subtle rounded border border-primary-subtle"> <div class="d-flex justify-content-between align-items-center p-3 bg-primary-subtle rounded border border-primary-subtle">
<span class="fw-bold">Total Tendered (Cash)</span> <span class="fw-bold" data-en="Total Tendered (Cash)" data-ar="إجمالي المقبوض نقدًا"><?= $lang === 'ar' ? 'إجمالي المقبوض نقدًا' : 'Total Tendered (Cash)' ?></span>
<span class="h6 m-0 fw-bold text-primary" id="changeDue">0.000</span> <span class="h6 m-0 fw-bold text-primary" id="changeDue">0.000</span>
</div> </div>
<div class="small text-muted mt-1">* Change is calculated based on cash payments only.</div> <div class="small text-muted mt-1" data-en="* Change is calculated based on cash payments only." data-ar="* يتم احتساب الباقي بناءً على الدفعات النقدية فقط."><?= $lang === 'ar' ? '* يتم احتساب الباقي بناءً على الدفعات النقدية فقط.' : '* Change is calculated based on cash payments only.' ?></div>
</div> </div>
</div> </div>
<div class="modal-footer border-0"> <div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button> <button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="button" class="btn btn-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()"> <button type="button" class="btn btn-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()">
PAY & COMPLETE <?= $lang === 'ar' ? 'ادفع وأكمل' : 'PAY & COMPLETE' ?>
</button> </button>
</div> </div>
</div> </div>

View File

@ -749,7 +749,7 @@
const vatAmount = itemTotal * (vatRate / (100 + vatRate)); const vatAmount = itemTotal * (vatRate / (100 + vatRate));
return ` return `
<tr> <tr>
<td>${item.name_en} / ${item.name_ar}<br><small>${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)}</small></td> <td>${item.name_en} / ${item.name_ar}<br><small>${formatQuantity(item.quantity)} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
<td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td> <td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td>
<td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td> <td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td>
</tr> </tr>
@ -764,27 +764,30 @@
const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0); const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>"; const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>"; const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>";
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>"; const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>"; const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
const companyLogo = "<?= htmlspecialchars($data['settings']['company_logo'] ?? '') ?>"; const companyLogo = "<?= htmlspecialchars($data['settings']['company_logo'] ?? '') ?>";
const receiptNumber = inv.document_no || inv.transaction_no || ('INV-' + inv.id.toString().padStart(5, '0'));
const receiptCustomer = inv.customer_name || inv.party_name || 'Walk-in / عميل عابر';
const receiptOutlet = inv.outlet_name || outletName || '';
container.innerHTML = ` container.innerHTML = `
<div class="thermal-receipt"> <div class="thermal-receipt">
<div class="center"> <div class="center">
${companyLogo ? `<img src="${companyLogo}" alt="Logo" style="max-height: 60px; width: auto; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;">` : ''} ${companyLogo ? `<img src="${companyLogo}" alt="Logo" style="max-height: 60px; width: auto; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;">` : ''}
<h5 class="mb-0 fw-bold">${companyName}</h5> <h5 class="mb-0 fw-bold">${companyName}</h5>
${inv.outlet_name ? `<div class="fw-bold text-uppercase">${inv.outlet_name}</div>` : ''} ${receiptOutlet ? `<div class="fw-bold text-uppercase">${receiptOutlet}</div>` : ''}
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''} ${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''} ${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
<div class="separator"></div> <div class="separator"></div>
<h6 class="fw-bold">TAX INVOICE / فاتورة ضريبية</h6> <h6 class="fw-bold">TAX INVOICE / فاتورة ضريبية</h6>
<div>Inv / رقم: INV-${inv.id.toString().padStart(5, '0')}</div> <div>Inv / رقم: ${receiptNumber}</div>
<div>Date / التاريخ: ${inv.invoice_date}</div> <div>Date / التاريخ: ${inv.invoice_date}</div>
<div class="separator"></div> <div class="separator"></div>
</div> </div>
<div> <div>
<strong>Customer / العميل:</strong> ${inv.customer_name || 'Walk-in / عميل عابر'} <strong>Customer / العميل:</strong> ${receiptCustomer}
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<table> <table>
@ -831,10 +834,28 @@
posModal.show(); posModal.show();
}; };
window.prepareReceiptPrintArea = function(sourceHtml, printArea) {
if (!printArea) {
return;
}
const wrapper = document.createElement('div');
wrapper.innerHTML = (sourceHtml || '').trim();
const receipt = wrapper.querySelector('.thermal-receipt');
if (receipt) {
receipt.classList.add('thermal-receipt-print');
printArea.innerHTML = wrapper.innerHTML;
return;
}
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${sourceHtml || ''}</div>`;
};
function printPosReceipt() { function printPosReceipt() {
const content = document.getElementById('posReceiptContent').innerHTML; const content = document.getElementById('posReceiptContent').innerHTML;
const printArea = document.getElementById('posPrintArea'); const printArea = document.getElementById('posPrintArea');
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${content}</div>`; window.prepareReceiptPrintArea(content, printArea);
document.body.classList.add('printing-receipt'); document.body.classList.add('printing-receipt');
window.print(); window.print();

View File

@ -112,7 +112,7 @@
<tr> <tr>
<td>${index + 1}</td> <td>${index + 1}</td>
<td>${item.name_en}<br><small class="text-muted">${item.name_ar}</small></td> <td>${item.name_en}<br><small class="text-muted">${item.name_ar}</small></td>
<td class="text-center">${item.quantity}</td> <td class="text-center">${formatQuantity(item.quantity)}</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td> <td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td> <td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
<td class="text-end">${parseFloat(item.total_amount).toFixed(3)}</td> <td class="text-end">${parseFloat(item.total_amount).toFixed(3)}</td>
@ -274,7 +274,7 @@
<tr> <tr>
<td>${index + 1}</td> <td>${index + 1}</td>
<td>${item.name_en}<br><small>${item.name_ar}</small></td> <td>${item.name_en}<br><small>${item.name_ar}</small></td>
<td class="text-center">${item.quantity}</td> <td class="text-center">${formatQuantity(item.quantity)}</td>
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td> <td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td> <td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td> <td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>

View File

@ -47,12 +47,13 @@ if (isset($_POST['convert_to_invoice'])) {
$items_for_journal = []; $items_for_journal = [];
foreach ($qItems as $item) { foreach ($qItems as $item) {
$qty = normalize_quantity($item['quantity'] ?? 0);
$lineVatAmount = line_item_vat_amount($db, $item); $lineVatAmount = line_item_vat_amount($db, $item);
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)") $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")
->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $lineVatAmount, $item['total_price']]); ->execute([$inv_id, $item['item_id'], $qty, $item['unit_price'], $lineVatAmount, $item['total_price']]);
update_stock($item['item_id'], -$item['quantity']); update_stock($item['item_id'], -$qty);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']]; $items_for_journal[] = ['id' => $item['item_id'], 'qty' => $qty];
} }
$db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]); $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
@ -93,10 +94,11 @@ if (isset($_POST['convert_lpo_to_purchase'])) {
$pur_id = $db->lastInsertId(); $pur_id = $db->lastInsertId();
foreach ($lItems as $item) { foreach ($lItems as $item) {
$qty = normalize_quantity($item['quantity'] ?? 0);
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)") $db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")
->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]); ->execute([$pur_id, $item['item_id'], $qty, $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
update_stock($item['item_id'], $item['quantity']); update_stock($item['item_id'], $qty);
} }
$db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]); $db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);

View File

@ -118,3 +118,185 @@
}); });
// View and Print Invoice Logic // View and Print Invoice Logic
const invoiceActionErrorTitle = <?= json_encode(($lang ?? 'en') === 'ar' ? 'خطأ' : 'Error', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoiceActionLoadError = <?= json_encode(($lang ?? 'en') === 'ar' ? 'تعذر تحميل بيانات الفاتورة' : 'Failed to load invoice details', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintChooserTitle = <?= json_encode(($lang ?? 'en') === 'ar' ? 'اختر طريقة الطباعة' : 'Choose print format', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintChooserText = <?= json_encode(($lang ?? 'en') === 'ar' ? 'هل تريد طباعة الفاتورة كإيصال أو كفاتورة عادية؟' : 'Do you want to print this sale as a receipt or a normal invoice?', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintNormalLabel = <?= json_encode(($lang ?? 'en') === 'ar' ? 'فاتورة عادية' : 'Normal Invoice', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintReceiptLabel = <?= json_encode(($lang ?? 'en') === 'ar' ? 'إيصال' : 'Receipt', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintCancelLabel = <?= json_encode(($lang ?? 'en') === 'ar' ? 'إلغاء' : 'Cancel', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const invoicePrintFallbackPrompt = <?= json_encode(($lang ?? 'en') === 'ar' ? 'اضغط موافق لطباعة الفاتورة العادية، أو إلغاء لطباعة الإيصال.' : 'Press OK for a normal invoice, or Cancel for a receipt.', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const buildInvoiceDocumentNo = (data) => {
const prefix = (data && data.type === 'purchase') ? 'PUR-' : 'INV-';
const numericId = parseInt(data && data.id ? data.id : 0, 10);
return prefix + String(Number.isFinite(numericId) && numericId > 0 ? numericId : 0).padStart(5, '0');
};
const normalizeInvoiceActionData = (payload, fallback = {}) => {
const merged = {
...(fallback || {}),
...(payload || {})
};
merged.type = String(merged.type || invoiceType || 'sale').toLowerCase() === 'purchase' ? 'purchase' : 'sale';
merged.party_name = merged.party_name || merged.customer_name || merged.supplier_name || (merged.type === 'sale' && merged.is_pos ? 'Walk-in Customer' : '---');
merged.customer_name = merged.customer_name || merged.supplier_name || merged.party_name || '---';
merged.customer_phone = merged.customer_phone || merged.party_phone || '';
merged.customer_tax_id = merged.customer_tax_id || merged.party_tax_id || '';
merged.outlet_name = merged.outlet_name || '';
merged.items = Array.isArray(merged.items) ? merged.items : (Array.isArray(fallback.items) ? fallback.items : []);
merged.document_no = merged.document_no || merged.transaction_no || (merged.id ? buildInvoiceDocumentNo(merged) : '');
merged.total_in_words = merged.total_in_words || fallback.total_in_words || '';
return merged;
};
const showInvoiceActionError = (message = invoiceActionLoadError) => {
if (window.Swal) {
Swal.fire(invoiceActionErrorTitle, message, 'error');
return;
}
window.alert(message);
};
const loadInvoiceActionData = async (btn) => {
const fallbackData = normalizeInvoiceActionData(parseInvoiceButtonPayload(btn));
const invoiceId = btn?.dataset?.id || fallbackData.id || '';
const type = btn?.dataset?.type || fallbackData.type || invoiceType || 'sale';
const baseData = normalizeInvoiceActionData({ ...fallbackData, id: invoiceId || fallbackData.id, type }, fallbackData);
if (!invoiceId) {
return baseData;
}
try {
const response = await fetch(`index.php?action=get_invoice_details&invoice_id=${encodeURIComponent(invoiceId)}&type=${encodeURIComponent(type)}`);
const data = await response.json();
if (data && data.id) {
return normalizeInvoiceActionData(data, baseData);
}
if (data && data.error) {
throw new Error(data.error);
}
} catch (error) {
console.error('Failed to load invoice details for view/print.', error);
}
return baseData;
};
const printInvoiceAsReceipt = (data) => {
if (typeof window.printPosReceiptFromInvoice !== 'function') {
if (typeof window.viewAndPrintA4Invoice === 'function') {
window.viewAndPrintA4Invoice(data, true);
}
return;
}
window.printPosReceiptFromInvoice(data);
setTimeout(() => {
const receiptContent = document.getElementById('posReceiptContent');
const printArea = document.getElementById('posPrintArea');
if (!receiptContent || !printArea) {
return;
}
if (typeof window.prepareReceiptPrintArea === 'function') {
window.prepareReceiptPrintArea(receiptContent.innerHTML, printArea);
} else {
printArea.innerHTML = receiptContent.innerHTML;
const receipt = printArea.querySelector('.thermal-receipt');
if (receipt) {
receipt.classList.add('thermal-receipt-print');
}
}
document.body.classList.add('printing-receipt');
window.print();
document.body.classList.remove('printing-receipt');
printArea.innerHTML = '';
const receiptModalEl = document.getElementById('posReceiptModal');
const receiptModal = receiptModalEl ? bootstrap.Modal.getInstance(receiptModalEl) : null;
if (receiptModal) {
receiptModal.hide();
}
}, 250);
};
const promptInvoicePrintMode = async (data) => {
const isSaleInvoice = String(data?.type || invoiceType || 'sale').toLowerCase() === 'sale';
if (!isSaleInvoice || typeof window.printPosReceiptFromInvoice !== 'function') {
if (typeof window.viewAndPrintA4Invoice === 'function') {
window.viewAndPrintA4Invoice(data, true);
}
return;
}
if (window.Swal) {
const result = await Swal.fire({
title: invoicePrintChooserTitle,
text: invoicePrintChooserText,
icon: 'question',
showCancelButton: true,
showDenyButton: true,
confirmButtonText: invoicePrintNormalLabel,
denyButtonText: invoicePrintReceiptLabel,
cancelButtonText: invoicePrintCancelLabel,
reverseButtons: true
});
if (result.isConfirmed) {
if (typeof window.viewAndPrintA4Invoice === 'function') {
setTimeout(() => window.viewAndPrintA4Invoice(data, true), 120);
}
} else if (result.isDenied) {
setTimeout(() => printInvoiceAsReceipt(data), 120);
}
return;
}
if (window.confirm(invoicePrintFallbackPrompt)) {
if (typeof window.viewAndPrintA4Invoice === 'function') {
window.viewAndPrintA4Invoice(data, true);
}
} else {
printInvoiceAsReceipt(data);
}
};
document.querySelectorAll('.view-invoice-btn').forEach(btn => {
btn.addEventListener('click', async function(event) {
event.preventDefault();
const data = await loadInvoiceActionData(this);
if (!data || !data.id) {
showInvoiceActionError();
return;
}
if (typeof window.viewAndPrintA4Invoice === 'function') {
window.viewAndPrintA4Invoice(data, false);
}
});
});
document.querySelectorAll('.print-a4-btn').forEach(btn => {
btn.addEventListener('click', async function(event) {
event.preventDefault();
const data = await loadInvoiceActionData(this);
if (!data || !data.id) {
showInvoiceActionError();
return;
}
await promptInvoicePrintMode(data);
});
});

View File

@ -31,7 +31,7 @@
btn.innerHTML = ` btn.innerHTML = `
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<span><strong>${item.sku}</strong> - ${item.name_en} / ${item.name_ar}</span> <span><strong>${item.sku}</strong> - ${item.name_en} / ${item.name_ar}</span>
<span class="text-muted small">Stock: ${item.stock_quantity}</span> <span class="text-muted small">Stock: ${formatQuantity(item.stock_quantity)}</span>
</div> </div>
`; `;
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl); btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
@ -152,17 +152,17 @@ ${text}` : title);
if (searchInput) searchInput.value = ''; if (searchInput) searchInput.value = '';
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1'); const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
const currentStock = parseFloat(item.stock_quantity) || 0; const currentStock = normalizeQuantity(item.stock_quantity);
if (invoiceType === 'sale' && !allowZeroStock && !customData) { if (invoiceType === 'sale' && !allowZeroStock && !customData) {
const existingInTable = Array.from(tableBody.querySelectorAll('.item-row')).find(row => row.querySelector('.item-id-input').value == item.id); const existingInTable = Array.from(tableBody.querySelectorAll('.item-row')).find(row => row.querySelector('.item-id-input').value == item.id);
let currentQtyInTable = 0; let currentQtyInTable = 0;
if (existingInTable) { if (existingInTable) {
currentQtyInTable = parseFloat(existingInTable.querySelector('.item-qty').value) || 0; currentQtyInTable = normalizeQuantity(existingInTable.querySelector('.item-qty').value);
} }
if (currentQtyInTable + 1 > currentStock) { if (currentQtyInTable + 1 > currentStock) {
alert('Insufficient stock! Available: ' + currentStock); alert('Insufficient stock! Available: ' + formatQuantity(currentStock));
return; return;
} }
} }
@ -171,7 +171,7 @@ ${text}` : title);
if (existingRow && !customData) { if (existingRow && !customData) {
const row = existingRow.closest('tr'); const row = existingRow.closest('tr');
const qtyInput = row.querySelector('.item-qty'); const qtyInput = row.querySelector('.item-qty');
qtyInput.value = parseFloat(qtyInput.value) + 1; qtyInput.value = formatQuantity((parseFloat(qtyInput.value) || 0) + 1);
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl); recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
return; return;
} }
@ -179,18 +179,18 @@ ${text}` : title);
const row = document.createElement('tr'); const row = document.createElement('tr');
row.className = 'item-row'; row.className = 'item-row';
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price); const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
const qty = customData ? customData.quantity : 1; const qty = normalizeQuantity(customData ? customData.quantity : 1);
const vatRate = item.vat_rate || 0; const vatRate = item.vat_rate || 0;
row.innerHTML = ` row.innerHTML = `
<td> <td>
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}"> <input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
<input type="hidden" class="item-row-stock" value="${item.stock_quantity}"> <input type="hidden" class="item-row-stock" value="${formatQuantity(item.stock_quantity)}">
<input type="hidden" class="item-vat-rate" value="${vatRate}"> <input type="hidden" class="item-vat-rate" value="${vatRate}">
<div><strong>${item.name_en}</strong></div> <div><strong>${item.name_en}</strong></div>
<div class="small text-muted">${item.name_ar} (${item.sku})</div> <div class="small text-muted">${item.name_ar} (${item.sku})</div>
</td> </td>
<td><input type="number" step="0.001" name="quantities[]" class="form-control item-qty" value="${qty}" required></td> <td><input type="number" step="0.01" name="quantities[]" class="form-control item-qty" value="${formatQuantity(qty)}" required></td>
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td> <td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
<td><input type="text" class="form-control bg-light" value="${parseFloat(vatRate || 0).toFixed(2)}%" readonly></td> <td><input type="text" class="form-control bg-light" value="${parseFloat(vatRate || 0).toFixed(2)}%" readonly></td>
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td> <td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
@ -198,6 +198,7 @@ ${text}` : title);
`; `;
tableBody.appendChild(row); tableBody.appendChild(row);
if (typeof syncQuantityInputs === 'function') syncQuantityInputs(row);
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl); attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl); recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
} }
@ -206,7 +207,8 @@ ${text}` : title);
let subtotal = 0; let subtotal = 0;
let totalVat = 0; let totalVat = 0;
tableBody.querySelectorAll('.item-row').forEach(row => { tableBody.querySelectorAll('.item-row').forEach(row => {
const qty = parseFloat(row.querySelector('.item-qty').value) || 0; const qtyInput = row.querySelector('.item-qty');
const qty = normalizeQuantity(qtyInput ? qtyInput.value : 0);
const price = parseFloat(row.querySelector('.item-price').value) || 0; const price = parseFloat(row.querySelector('.item-price').value) || 0;
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0; const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
@ -228,11 +230,11 @@ ${text}` : title);
row.querySelector('.item-qty').addEventListener('input', function() { row.querySelector('.item-qty').addEventListener('input', function() {
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1'); const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
if (invoiceType === 'sale' && !allowZeroStock) { if (invoiceType === 'sale' && !allowZeroStock) {
const stock = parseFloat(row.querySelector('.item-row-stock').value) || 0; const stock = normalizeQuantity(row.querySelector('.item-row-stock').value);
const qty = parseFloat(this.value) || 0; const qty = normalizeQuantity(this.value);
if (qty > stock) { if (qty > stock) {
alert('Insufficient stock! Available: ' + stock); alert('Insufficient stock! Available: ' + formatQuantity(stock));
this.value = stock; this.value = formatQuantity(stock);
} }
} }
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl); recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);

View File

@ -59,7 +59,7 @@
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = ` tr.innerHTML = `
<td>${item.name_en} / ${item.name_ar}</td> <td>${item.name_en} / ${item.name_ar}</td>
<td class="text-center">${item.quantity}</td> <td class="text-center">${formatQuantity(item.quantity)}</td>
<td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.unit_price).toFixed(3)}</td> <td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.unit_price).toFixed(3)}</td>
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td> <td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
<td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.total_price).toFixed(3)}</td> <td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.total_price).toFixed(3)}</td>

View File

@ -65,18 +65,27 @@
body.printing-receipt .modal { display: none !important; } body.printing-receipt .modal { display: none !important; }
body.printing-receipt .modal-backdrop { display: none !important; } body.printing-receipt .modal-backdrop { display: none !important; }
body.printing-receipt #posPrintArea { body.printing-receipt #posPrintArea {
display: block !important; display: flex !important;
visibility: visible !important; visibility: visible !important;
position: absolute !important; justify-content: center !important;
left: 0 !important; align-items: flex-start !important;
top: 0 !important; position: fixed !important;
inset: 0 !important;
width: 100% !important; width: 100% !important;
margin: 0 !important;
padding: 0 !important;
z-index: 9999 !important; z-index: 9999 !important;
background: white !important; background: white !important;
} }
body.printing-receipt #posPrintArea * { body.printing-receipt #posPrintArea * {
visibility: visible !important; visibility: visible !important;
} }
body.printing-receipt #posPrintArea .thermal-receipt-print {
position: static !important;
left: auto !important;
top: auto !important;
margin: 0 auto !important;
}
} }
.invoice-logo { max-height: 80px; width: auto; } .invoice-logo { max-height: 80px; width: auto; }
.invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; } .invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }

View File

@ -31,7 +31,7 @@
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -58,7 +58,7 @@
$items_for_journal = []; $items_for_journal = [];
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -128,7 +128,7 @@
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;
@ -156,7 +156,7 @@
$stmtOld->execute([$id]); $stmtOld->execute([$id]);
$oldItems = $stmtOld->fetchAll(); $oldItems = $stmtOld->fetchAll();
foreach ($oldItems as $old) { foreach ($oldItems as $old) {
$change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity']; $change = ($type === 'sale') ? normalize_quantity($old['quantity'] ?? 0) : -normalize_quantity($old['quantity'] ?? 0);
update_stock($old['item_id'], $change); update_stock($old['item_id'], $change);
} }
@ -166,7 +166,7 @@
// Insert new items and update stock // Insert new items and update stock
foreach ($items as $i => $item_id) { foreach ($items as $i => $item_id) {
if (!$item_id) continue; if (!$item_id) continue;
$qty = (float)$qtys[$i]; $qty = normalize_quantity($qtys[$i] ?? 0);
$price = (float)$prices[$i]; $price = (float)$prices[$i];
$subtotal = $qty * $price; $subtotal = $qty * $price;

View File

@ -166,13 +166,13 @@
<td class="text-end d-print-none"> <td class="text-end d-print-none">
<?php $invoiceJson = (string)(json_encode($inv, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) ?: '{}'); ?> <?php $invoiceJson = (string)(json_encode($inv, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) ?: '{}'); ?>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="View"><i class="bi bi-eye"></i></button> <button class="btn btn-outline-info view-invoice-btn" data-id="<?= $inv['id'] ?>" data-type="<?= htmlspecialchars($inv['type']) ?>" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="View"><i class="bi bi-eye"></i></button>
<button class="btn btn-outline-warning return-invoice-btn" data-id="<?= $inv['id'] ?>" data-bs-toggle="modal" data-bs-target="<?= $page === 'sales' ? '#addSalesReturnModal' : '#addPurchaseReturnModal' ?>" title="Return"><i class="bi bi-arrow-return-left"></i></button> <button class="btn btn-outline-warning return-invoice-btn" data-id="<?= $inv['id'] ?>" data-bs-toggle="modal" data-bs-target="<?= $page === 'sales' ? '#addSalesReturnModal' : '#addPurchaseReturnModal' ?>" title="Return"><i class="bi bi-arrow-return-left"></i></button>
<button class="btn btn-outline-primary edit-invoice-btn" data-id="<?= $inv['id'] ?>" data-type="<?= htmlspecialchars($inv['type']) ?>" data-json="<?= htmlspecialchars($invoiceJson) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button> <button class="btn btn-outline-primary edit-invoice-btn" data-id="<?= $inv['id'] ?>" data-type="<?= htmlspecialchars($inv['type']) ?>" data-json="<?= htmlspecialchars($invoiceJson) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
<?php if ($inv['status'] !== 'paid'): ?> <?php if ($inv['status'] !== 'paid'): ?>
<button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button> <button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button>
<?php endif; ?> <?php endif; ?>
<button class="btn btn-outline-secondary print-a4-btn" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="Print A4 Invoice"><i class="bi bi-printer"></i></button> <button class="btn btn-outline-secondary print-a4-btn" data-id="<?= $inv['id'] ?>" data-type="<?= htmlspecialchars($inv['type']) ?>" data-json="<?= htmlspecialchars($invoiceJson) ?>" title="<?= $page === 'sales' ? 'Print Options' : 'Print A4 Invoice' ?>"><i class="bi bi-printer"></i></button>
<form method="POST" class="d-inline js-swal-confirm-form" data-confirm-title="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'هل تريد حذف هذه الفاتورة؟' : 'Delete this invoice?', ENT_QUOTES) ?>" data-confirm-text="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'سيتم حذف هذه الفاتورة نهائياً.' : 'This invoice will be permanently removed.', ENT_QUOTES) ?>" data-confirm-button="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'نعم، احذفها' : 'Yes, delete it', ENT_QUOTES) ?>" data-cancel-button="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'إلغاء' : 'Keep it', ENT_QUOTES) ?>"> <form method="POST" class="d-inline js-swal-confirm-form" data-confirm-title="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'هل تريد حذف هذه الفاتورة؟' : 'Delete this invoice?', ENT_QUOTES) ?>" data-confirm-text="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'سيتم حذف هذه الفاتورة نهائياً.' : 'This invoice will be permanently removed.', ENT_QUOTES) ?>" data-confirm-button="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'نعم، احذفها' : 'Yes, delete it', ENT_QUOTES) ?>" data-cancel-button="<?= htmlspecialchars(($lang ?? 'en') === 'ar' ? 'إلغاء' : 'Keep it', ENT_QUOTES) ?>">
<input type="hidden" name="id" value="<?= $inv['id'] ?>"> <input type="hidden" name="id" value="<?= $inv['id'] ?>">
<input type="hidden" name="delete_invoice" value="1"> <input type="hidden" name="delete_invoice" value="1">