some updates
This commit is contained in:
parent
82a5fde6bc
commit
8c0d2453dd
@ -504,8 +504,10 @@ body:not(.theme-default) .form-select:focus {
|
||||
/* Thermal Receipt Styles */
|
||||
.thermal-receipt {
|
||||
width: 80mm;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
@ -527,11 +529,14 @@ body:not(.theme-default) .form-select:focus {
|
||||
}
|
||||
.thermal-receipt table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.thermal-receipt table th {
|
||||
text-align: left;
|
||||
border-bottom: 1px dashed #000;
|
||||
font-size: 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.thermal-receipt.rtl table th {
|
||||
text-align: right;
|
||||
@ -539,6 +544,7 @@ body:not(.theme-default) .form-select:focus {
|
||||
.thermal-receipt table td {
|
||||
padding: 5px 0;
|
||||
font-size: 11px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.thermal-receipt .total-row {
|
||||
font-weight: bold;
|
||||
@ -547,9 +553,11 @@ body:not(.theme-default) .form-select:focus {
|
||||
|
||||
@media print {
|
||||
.thermal-receipt-print {
|
||||
width: 80mm !important;
|
||||
margin: 0 !important;
|
||||
padding: 5mm !important;
|
||||
width: 76mm !important;
|
||||
max-width: 76mm !important;
|
||||
margin: 0 auto !important;
|
||||
padding: 2mm !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
body.printing-receipt * {
|
||||
visibility: hidden;
|
||||
@ -560,12 +568,21 @@ body:not(.theme-default) .form-select:focus {
|
||||
}
|
||||
body.printing-receipt #posPrintArea {
|
||||
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 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
top: auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
34
includes/quantity_helper.php
Normal file
34
includes/quantity_helper.php
Normal 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(), '.', '');
|
||||
}
|
||||
}
|
||||
@ -16,8 +16,10 @@ if (!function_exists('update_stock')) {
|
||||
// However, for extra safety or validation, we could check.
|
||||
// But for now, simple update is best.
|
||||
|
||||
$normalizedQty = function_exists('normalize_quantity') ? normalize_quantity($qty) : round((float)$qty, 2);
|
||||
|
||||
$db = db();
|
||||
$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
250
index.php
@ -467,6 +467,7 @@ if (!function_exists('register_wablas_helper_fallback')) {
|
||||
runtime_debug_boot_mark('boot:loading_core_dependencies');
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/SimpleXLSX.php';
|
||||
require_once __DIR__ . '/includes/quantity_helper.php';
|
||||
require_once __DIR__ . '/includes/stock_helper.php';
|
||||
$wablasHelperPath = __DIR__ . '/includes/wablas_helper.php';
|
||||
if (is_file($wablasHelperPath)) {
|
||||
@ -1860,7 +1861,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
||||
|
||||
// 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']): ?>
|
||||
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
||||
<?php else: ?>
|
||||
@ -1883,7 +1884,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
||||
<?php endif; ?>
|
||||
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
||||
</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>
|
||||
<?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.']);
|
||||
exit;
|
||||
}
|
||||
$qty = round(((float)$weightBarcode['value']) / (float)$p['sale_price'], 3);
|
||||
$qty = normalize_quantity(((float)$weightBarcode['value']) / (float)$p['sale_price']);
|
||||
} else {
|
||||
$qty = round((float)$weightBarcode['value'], 3);
|
||||
$qty = normalize_quantity((float)$weightBarcode['value']);
|
||||
}
|
||||
|
||||
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;
|
||||
$payments = json_decode($_POST['payments'] ?? '[]', 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);
|
||||
$tax_amount = (float)($_POST['tax_amount'] ?? 0);
|
||||
$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);
|
||||
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
|
||||
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
|
||||
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
|
||||
$stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
|
||||
$min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
|
||||
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
|
||||
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
|
||||
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
|
||||
@ -2869,8 +2876,8 @@ function getPromotionalPrice($item) {
|
||||
}
|
||||
$sale_price = (float)($_POST['sale_price'] ?? 0);
|
||||
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
|
||||
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
|
||||
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
|
||||
$stock_quantity = normalize_quantity($_POST['stock_quantity'] ?? 0);
|
||||
$min_stock_level = normalize_quantity($_POST['min_stock_level'] ?? 0);
|
||||
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
|
||||
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
|
||||
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
|
||||
@ -3014,7 +3021,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3045,7 +3052,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3080,7 +3087,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3103,7 +3110,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3145,7 +3152,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3177,7 +3184,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3213,7 +3220,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3235,7 +3242,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3303,7 +3310,7 @@ function getPromotionalPrice($item) {
|
||||
$name_ar = trim((string)($row[2] ?? ''));
|
||||
$sale_price = (float)($row[3] ?? 0);
|
||||
$purchase_price = (float)($row[4] ?? 0);
|
||||
$qty = (float)($row[5] ?? 0);
|
||||
$qty = normalize_quantity($row[5] ?? 0);
|
||||
$vat_rate = (float)($row[6] ?? 0);
|
||||
|
||||
$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) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3612,7 +3619,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3651,7 +3658,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3673,7 +3680,7 @@ function getPromotionalPrice($item) {
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -3889,7 +3896,7 @@ if (isset($_POST['add_hr_department'])) {
|
||||
|
||||
$total_return = 0;
|
||||
foreach ($quantities as $i => $qty) {
|
||||
$total_return += (float)$qty * (float)$prices[$i];
|
||||
$total_return += normalize_quantity($qty) * (float)$prices[$i];
|
||||
}
|
||||
|
||||
// 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 = ?");
|
||||
|
||||
foreach ($item_ids as $i => $item_id) {
|
||||
$qty = (float)$quantities[$i];
|
||||
$qty = normalize_quantity($quantities[$i] ?? 0);
|
||||
if ($qty > 0) {
|
||||
$price = (float)$prices[$i];
|
||||
$line_total = $qty * $price;
|
||||
@ -3949,7 +3956,7 @@ if (isset($_POST['add_hr_department'])) {
|
||||
|
||||
$total_return = 0;
|
||||
foreach ($quantities as $i => $qty) {
|
||||
$total_return += (float)$qty * (float)$prices[$i];
|
||||
$total_return += normalize_quantity($qty) * (float)$prices[$i];
|
||||
}
|
||||
|
||||
// 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 = ?");
|
||||
|
||||
foreach ($item_ids as $i => $item_id) {
|
||||
$qty = (float)$quantities[$i];
|
||||
$qty = normalize_quantity($quantities[$i] ?? 0);
|
||||
if ($qty > 0) {
|
||||
$price = (float)$prices[$i];
|
||||
$line_total = $qty * $price;
|
||||
@ -7333,8 +7340,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-end">
|
||||
<strong><?= number_format((float)$item['stock_quantity'], 3) ?></strong>
|
||||
<div class="small text-muted">Min: <?= number_format((float)$item['min_stock_level'], 3) ?></div>
|
||||
<strong><?= format_quantity($item['stock_quantity']) ?></strong>
|
||||
<div class="small text-muted">Min: <?= format_quantity($item['min_stock_level']) ?></div>
|
||||
<?php if ($item['stock_quantity'] <= $item['min_stock_level']): ?>
|
||||
<span class="badge bg-danger" data-en="Low Stock" data-ar="مخزون منخفض">Low Stock</span>
|
||||
<?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="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">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>
|
||||
</table>
|
||||
</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="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="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="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="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="<?= 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="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>
|
||||
@ -7498,7 +7505,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div>
|
||||
</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>
|
||||
<?php if ($is_expired): ?>
|
||||
@ -7555,10 +7562,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
</td>
|
||||
<td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></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>
|
||||
<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>
|
||||
</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 class="product-grid" id="productGrid">
|
||||
<?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']): ?>
|
||||
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
||||
<?php else: ?>
|
||||
@ -7720,7 +7727,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<?php endif; ?>
|
||||
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
||||
</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>
|
||||
<?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>
|
||||
<div class="d-flex gap-2">
|
||||
<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): ?>
|
||||
<option value="<?= $c['id'] ?>"
|
||||
data-points="<?= $c['loyalty_points'] ?>"
|
||||
@ -7841,10 +7848,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
// Ensure items is an array
|
||||
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 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;
|
||||
return sum + (price * qty * (vatRate / (100 + vatRate)));
|
||||
}, 0);
|
||||
@ -7890,8 +7897,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
add(product) {
|
||||
if (!this.items) this.items = [];
|
||||
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
||||
const currentStock = parseFloat(product.stock_quantity) || 0;
|
||||
const addQty = Math.max(parseFloat(product.qty) || 1, 0.001);
|
||||
const currentStock = normalizeQuantity(product.stock_quantity);
|
||||
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 normalizedProduct = {...product, price: unitPrice};
|
||||
|
||||
@ -7901,14 +7908,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
return;
|
||||
}
|
||||
existing.qty = Number((existing.qty + addQty).toFixed(3));
|
||||
existing.qty = normalizeQuantity(existing.qty + addQty);
|
||||
existing.price = unitPrice;
|
||||
} else {
|
||||
if (!allowZeroStock && currentStock < addQty) {
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
return;
|
||||
}
|
||||
this.items.push({...normalizedProduct, qty: Number(addQty.toFixed(3))});
|
||||
this.items.push({...normalizedProduct, qty: normalizeQuantity(addQty)});
|
||||
}
|
||||
|
||||
this.render();
|
||||
@ -7934,14 +7941,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const item = this.items.find(i => i.id === id);
|
||||
if (item) {
|
||||
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) {
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
item.qty += delta;
|
||||
item.qty = normalizeQuantity(item.qty + delta);
|
||||
if (item.qty <= 0) this.remove(id);
|
||||
else this.render();
|
||||
}
|
||||
@ -8107,7 +8114,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const carts = await resp.json();
|
||||
const c = carts.find(x => x.id == id);
|
||||
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 || '';
|
||||
await this.onCustomerChange();
|
||||
await this.deleteHeld(id, true);
|
||||
@ -8150,7 +8157,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
let totalVat = 0;
|
||||
container.innerHTML = items.map(item => {
|
||||
const price = parseFloat(item.price) || 0;
|
||||
const qty = parseFloat(item.qty) || 0;
|
||||
const qty = normalizeQuantity(item.qty);
|
||||
const itemTotal = price * qty;
|
||||
subtotal += itemTotal;
|
||||
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 class="qty-controls mx-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-end" style="min-width: 80px;">
|
||||
@ -8328,10 +8335,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
renderPayments() {
|
||||
const container = document.getElementById('paymentList');
|
||||
const methodLabels = {
|
||||
'cash': 'Cash',
|
||||
'card': 'Credit Card',
|
||||
'credit': 'Credit',
|
||||
'transfer': 'Bank Transfer'
|
||||
'cash': <?= json_encode($lang === 'ar' ? 'نقدًا' : 'Cash', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||
'card': <?= json_encode($lang === 'ar' ? 'بطاقة ائتمان' : 'Credit Card', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||
'credit': <?= json_encode($lang === 'ar' ? 'آجل' : 'Credit', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||
'transfer': <?= json_encode($lang === 'ar' ? 'تحويل بنكي' : 'Bank Transfer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
|
||||
};
|
||||
container.innerHTML = this.payments.map((p, i) => `
|
||||
<div class="payment-line">
|
||||
@ -8381,7 +8388,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
},
|
||||
async completeOrder() {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -8404,20 +8411,20 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
|
||||
const remaining = this.getRemaining();
|
||||
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;
|
||||
}
|
||||
|
||||
const customerId = document.getElementById('posCustomer').value;
|
||||
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;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('confirmPaymentBtn');
|
||||
const originalText = btn.innerText;
|
||||
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 totalVat = this.items.reduce((sum, item) => {
|
||||
@ -8454,7 +8461,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
result = JSON.parse(text);
|
||||
} catch (e) {
|
||||
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) {
|
||||
@ -8462,13 +8469,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
if (payModal) payModal.hide();
|
||||
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
|
||||
} 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.innerText = originalText;
|
||||
}
|
||||
} catch (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.innerText = originalText;
|
||||
}
|
||||
@ -8476,7 +8483,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) {
|
||||
const container = document.getElementById('posReceiptContent');
|
||||
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 => {
|
||||
let m = p.method.toLowerCase();
|
||||
let methodAr = m === 'cash' ? 'نقد' : (m === 'card' ? 'بطاقة ائتمان' : (m === 'credit' ? 'آجل' : (m === 'transfer' ? 'تحويل بنكي' : m)));
|
||||
@ -8500,7 +8507,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<td>
|
||||
<div class="fw-bold">${item.nameAr || ''}</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 style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</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() {
|
||||
$('#posCustomer').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select Customer'
|
||||
placeholder: <?= json_encode($lang === 'ar' ? 'اختر العميل' : 'Select Customer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
|
||||
});
|
||||
$('#paymentCreditCustomer').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select Customer',
|
||||
placeholder: <?= json_encode($lang === 'ar' ? 'اختر العميل' : 'Select Customer', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||
dropdownParent: $('#posPaymentModal')
|
||||
});
|
||||
|
||||
@ -8723,13 +8730,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="open_register" value="1">
|
||||
<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>
|
||||
<?php if (isset($registers)): foreach ($registers as $r): ?>
|
||||
<option value="<?= $r['id'] ?>"><?= htmlspecialchars($r['name']) ?></option>
|
||||
@ -8737,7 +8744,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
</select>
|
||||
</div>
|
||||
<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">
|
||||
<span class="input-group-text">OMR</span>
|
||||
<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 class="modal-footer">
|
||||
<a href="<?= htmlspecialchars(page_url('dashboard')) ?>" class="btn btn-secondary">Cancel & Go to Dashboard</a>
|
||||
<button type="submit" class="btn btn-primary">Open Session</button>
|
||||
<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" data-en="Open Session" data-ar="فتح الجلسة"><?= $lang === 'ar' ? 'فتح الجلسة' : 'Open Session' ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -8760,7 +8767,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
</div>
|
||||
<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="session_id" value="<?= $_SESSION['register_session_id'] ?? '' ?>">
|
||||
<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">
|
||||
<span class="input-group-text">OMR</span>
|
||||
<input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="3" placeholder="Any discrepancies or notes..."></textarea>
|
||||
<label class="form-label" data-en="Notes" data-ar="ملاحظات"><?= $lang === 'ar' ? 'ملاحظات' : 'Notes' ?></label>
|
||||
<textarea name="notes" class="form-control" rows="3" placeholder="<?= htmlspecialchars($lang === 'ar' ? 'أي فروقات أو ملاحظات...' : 'Any discrepancies or notes...', ENT_QUOTES) ?>"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</form>
|
||||
</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="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="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="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="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="<?= 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="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>
|
||||
@ -11970,9 +11977,72 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
</div>
|
||||
|
||||
<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() {
|
||||
console.log("DOM Content Loaded - Accounting System");
|
||||
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
|
||||
$('.select2').each(function() {
|
||||
$(this).select2({
|
||||
@ -12136,7 +12206,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@ -12144,14 +12214,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<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>
|
||||
<i class="bi bi-person-circle fs-3 text-secondary"></i>
|
||||
</div>
|
||||
<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)">
|
||||
<option value="">--- Select Customer ---</option>
|
||||
<option value=""><?= $lang === 'ar' ? '--- اختر العميل ---' : '--- Select Customer ---' ?></option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
@ -12162,11 +12232,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="amount-due-box mb-2">
|
||||
<div class="d-flex justify-content-between px-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
@ -12177,23 +12247,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
|
||||
<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-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)">
|
||||
<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 class="payment-method-btn" data-method="card" onclick="cart.selectMethod('card', this)">
|
||||
<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 class="payment-method-btn" data-method="credit" onclick="cart.selectMethod('credit', this)">
|
||||
<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 class="payment-method-btn" data-method="transfer" onclick="cart.selectMethod('transfer', this)">
|
||||
<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>
|
||||
|
||||
@ -12205,8 +12275,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()">
|
||||
<i class="bi bi-plus-lg"></i data-en="Add" data-ar="إضافة">ADD</button>
|
||||
<button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()" data-en="Add" data-ar="إضافة">
|
||||
<i class="bi bi-plus-lg"></i><?= $lang === 'ar' ? 'إضافة' : 'Add' ?></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12221,16 +12291,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</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 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-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()">
|
||||
PAY & COMPLETE
|
||||
<?= $lang === 'ar' ? 'ادفع وأكمل' : 'PAY & COMPLETE' ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -749,7 +749,7 @@
|
||||
const vatAmount = itemTotal * (vatRate / (100 + vatRate));
|
||||
return `
|
||||
<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;">${itemTotal.toFixed(3)}</td>
|
||||
</tr>
|
||||
@ -764,27 +764,30 @@
|
||||
const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
|
||||
|
||||
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 companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
||||
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 = `
|
||||
<div class="thermal-receipt">
|
||||
<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;">` : ''}
|
||||
<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>` : ''}
|
||||
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
|
||||
<div class="separator"></div>
|
||||
<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 class="separator"></div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Customer / العميل:</strong> ${inv.customer_name || 'Walk-in / عميل عابر'}
|
||||
<strong>Customer / العميل:</strong> ${receiptCustomer}
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<table>
|
||||
@ -831,10 +834,28 @@
|
||||
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() {
|
||||
const content = document.getElementById('posReceiptContent').innerHTML;
|
||||
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');
|
||||
window.print();
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
<tr>
|
||||
<td>${index + 1}</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-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||
<td class="text-end">${parseFloat(item.total_amount).toFixed(3)}</td>
|
||||
@ -274,7 +274,7 @@
|
||||
<tr>
|
||||
<td>${index + 1}</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-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
|
||||
@ -47,12 +47,13 @@ if (isset($_POST['convert_to_invoice'])) {
|
||||
|
||||
$items_for_journal = [];
|
||||
foreach ($qItems as $item) {
|
||||
$qty = normalize_quantity($item['quantity'] ?? 0);
|
||||
$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 (?, ?, ?, ?, ?, ?)")
|
||||
->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']);
|
||||
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
|
||||
update_stock($item['item_id'], -$qty);
|
||||
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $qty];
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
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 (?, ?, ?, ?, ?, ?)")
|
||||
->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]);
|
||||
|
||||
@ -118,3 +118,185 @@
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
btn.innerHTML = `
|
||||
<div class="d-flex justify-content-between">
|
||||
<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>
|
||||
`;
|
||||
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
||||
@ -152,17 +152,17 @@ ${text}` : title);
|
||||
if (searchInput) searchInput.value = '';
|
||||
|
||||
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) {
|
||||
const existingInTable = Array.from(tableBody.querySelectorAll('.item-row')).find(row => row.querySelector('.item-id-input').value == item.id);
|
||||
let currentQtyInTable = 0;
|
||||
if (existingInTable) {
|
||||
currentQtyInTable = parseFloat(existingInTable.querySelector('.item-qty').value) || 0;
|
||||
currentQtyInTable = normalizeQuantity(existingInTable.querySelector('.item-qty').value);
|
||||
}
|
||||
|
||||
if (currentQtyInTable + 1 > currentStock) {
|
||||
alert('Insufficient stock! Available: ' + currentStock);
|
||||
alert('Insufficient stock! Available: ' + formatQuantity(currentStock));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -171,7 +171,7 @@ ${text}` : title);
|
||||
if (existingRow && !customData) {
|
||||
const row = existingRow.closest('tr');
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@ -179,18 +179,18 @@ ${text}` : title);
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'item-row';
|
||||
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;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<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}">
|
||||
<div><strong>${item.name_en}</strong></div>
|
||||
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
||||
</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="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>
|
||||
@ -198,6 +198,7 @@ ${text}` : title);
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
if (typeof syncQuantityInputs === 'function') syncQuantityInputs(row);
|
||||
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||
}
|
||||
@ -206,7 +207,8 @@ ${text}` : title);
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
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 vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
||||
|
||||
@ -228,11 +230,11 @@ ${text}` : title);
|
||||
row.querySelector('.item-qty').addEventListener('input', function() {
|
||||
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
||||
if (invoiceType === 'sale' && !allowZeroStock) {
|
||||
const stock = parseFloat(row.querySelector('.item-row-stock').value) || 0;
|
||||
const qty = parseFloat(this.value) || 0;
|
||||
const stock = normalizeQuantity(row.querySelector('.item-row-stock').value);
|
||||
const qty = normalizeQuantity(this.value);
|
||||
if (qty > stock) {
|
||||
alert('Insufficient stock! Available: ' + stock);
|
||||
this.value = stock;
|
||||
alert('Insufficient stock! Available: ' + formatQuantity(stock));
|
||||
this.value = formatQuantity(stock);
|
||||
}
|
||||
}
|
||||
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<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">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
||||
<td class="text-end"><small><?= __('currency') ?></small> ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
|
||||
@ -65,18 +65,27 @@
|
||||
body.printing-receipt .modal { display: none !important; }
|
||||
body.printing-receipt .modal-backdrop { display: none !important; }
|
||||
body.printing-receipt #posPrintArea {
|
||||
display: block !important;
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 0 !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: white !important;
|
||||
}
|
||||
body.printing-receipt #posPrintArea * {
|
||||
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-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
$items_for_journal = [];
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -128,7 +128,7 @@
|
||||
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
$stmtOld->execute([$id]);
|
||||
$oldItems = $stmtOld->fetchAll();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@
|
||||
// Insert new items and update stock
|
||||
foreach ($items as $i => $item_id) {
|
||||
if (!$item_id) continue;
|
||||
$qty = (float)$qtys[$i];
|
||||
$qty = normalize_quantity($qtys[$i] ?? 0);
|
||||
$price = (float)$prices[$i];
|
||||
$subtotal = $qty * $price;
|
||||
|
||||
|
||||
@ -166,13 +166,13 @@
|
||||
<td class="text-end d-print-none">
|
||||
<?php $invoiceJson = (string)(json_encode($inv, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) ?: '{}'); ?>
|
||||
<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-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'): ?>
|
||||
<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; ?>
|
||||
<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) ?>">
|
||||
<input type="hidden" name="id" value="<?= $inv['id'] ?>">
|
||||
<input type="hidden" name="delete_invoice" value="1">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user