Autosave: 20260508-013014
This commit is contained in:
parent
40fd435ffd
commit
5a46bb40e1
299
index.php
299
index.php
@ -1805,7 +1805,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
||||
if ($discount) {
|
||||
echo json_encode(['success' => true, 'discount' => $discount]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid or expired discount code']);
|
||||
echo json_encode(['success' => false, 'error' => ($lang === 'ar' ? 'رمز الخصم غير صالح أو منتهي الصلاحية' : 'Invalid or expired discount code')]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
@ -1977,7 +1977,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
||||
|
||||
if ($action === 'hold_pos_cart') {
|
||||
header('Content-Type: application/json');
|
||||
$name = $_POST['cart_name'] ?? 'Untitled Cart';
|
||||
$name = $_POST['cart_name'] ?? ($lang === 'ar' ? 'طلب غير مسمى' : 'Untitled Cart');
|
||||
$items = $_POST['items'] ?? '[]';
|
||||
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
|
||||
$stmt = db()->prepare("INSERT INTO pos_held_carts (cart_name, items_json, customer_id) VALUES (?, ?, ?)");
|
||||
@ -2013,9 +2013,17 @@ if (isset($_GET['action']) || isset($_POST['action'])) {
|
||||
$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;
|
||||
$discount_amount = (float)($_POST['discount_amount'] ?? 0);
|
||||
$loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0);
|
||||
$net_amount = $total_amount - $discount_amount - $loyalty_redeemed;
|
||||
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
|
||||
$manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1';
|
||||
if (!$manualDiscountEnabled && $discount_code_id === null) {
|
||||
$discount_amount = 0.0;
|
||||
}
|
||||
if ($discount_amount > $total_amount) {
|
||||
$discount_amount = max(0, $total_amount);
|
||||
}
|
||||
$loyalty_redeemed = max(0, (float)($_POST['loyalty_redeemed'] ?? 0));
|
||||
$loyalty_redeemed = min($loyalty_redeemed, max(0, $total_amount - $discount_amount));
|
||||
$net_amount = max(0, $total_amount - $discount_amount - $loyalty_redeemed);
|
||||
|
||||
$transaction_no = 'POS-' . time() . rand(10, 99);
|
||||
$session_id = $_SESSION['register_session_id'] ?? null;
|
||||
@ -7691,15 +7699,15 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<div class="bg-white p-3 rounded mb-3 shadow-sm d-flex gap-2">
|
||||
<div class="input-group flex-grow-1">
|
||||
<span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Search products by name or SKU..." data-en="Search products..." data-ar="بحث عن منتجات...">
|
||||
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="<?= $lang === 'ar' ? 'ابحث عن المنتجات بالاسم أو رمز الصنف...' : 'Search products by name or SKU...' ?>" data-en="Search products..." data-ar="بحث عن منتجات...">
|
||||
</div>
|
||||
<div class="input-group flex-grow-1">
|
||||
<span class="input-group-text bg-light border-end-0"><i class="bi bi-upc-scan"></i></span>
|
||||
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="Scan barcode..." data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
|
||||
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="<?= $lang === 'ar' ? 'امسح الباركود...' : 'Scan barcode...' ?>" data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
|
||||
</div>
|
||||
<button class="btn btn-warning d-flex align-items-center gap-2" onclick="cart.openHeldCartsModal()">
|
||||
<i class="bi bi-pause-btn-fill"></i>
|
||||
<span class="d-none d-xl-inline" data-en="Held List" data-ar="قائمة الانتظار">Held List</span>
|
||||
<span class="d-none d-xl-inline" data-en="Held List" data-ar="الطلبات المعلقة"><?= $lang === 'ar' ? 'الطلبات المعلقة' : 'Held List' ?></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="product-grid" id="productGrid">
|
||||
@ -7727,7 +7735,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"><?= format_quantity($p['stock_quantity']) ?> left</span>
|
||||
<span class="badge bg-light text-dark small"><?= format_quantity($p['stock_quantity']) ?> <?= $lang === 'ar' ? 'متبقٍ' : 'left' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
@ -7736,21 +7744,21 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
|
||||
<div class="pos-cart">
|
||||
<div class="p-3 border-bottom d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 fw-bold"><i class="bi bi-cart3 me-2"></i>Cart</h6>
|
||||
<h6 class="m-0 fw-bold"><i class="bi bi-cart3 me-2"></i><?= $lang === 'ar' ? 'السلة' : 'Cart' ?></h6>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-info" onclick="window.open('customer-display.php?v=<?= time() ?>', 'CustomerDisplay', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' + screen.availWidth + ',height=' + screen.availHeight + ',left=0,top=0')" title="Customer Display"><i class="bi bi-display me-1"></i> Customer Screen</button>
|
||||
<button class="btn btn-sm btn-outline-info" onclick="window.open('customer-display.php?v=<?= time() ?>', 'CustomerDisplay', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' + screen.availWidth + ',height=' + screen.availHeight + ',left=0,top=0')" title="<?= $lang === 'ar' ? 'شاشة العميل' : 'Customer Display' ?>"><i class="bi bi-display me-1"></i> <?= $lang === 'ar' ? 'شاشة العميل' : 'Customer Screen' ?></button>
|
||||
<?php if ($active_session): ?>
|
||||
<button class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#closeRegisterModal" title="Close Register"><i class="bi bi-x-circle me-1"></i data-en="Close" data-ar="إغلاق">Close</button>
|
||||
<button class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#closeRegisterModal" title="<?= $lang === 'ar' ? 'إغلاق الخزينة' : 'Close Register' ?>"><i class="bi bi-x-circle me-1"></i><span data-en="Close" data-ar="إغلاق"><?= $lang === 'ar' ? 'إغلاق' : 'Close' ?></span></button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="Held List"><i class="bi bi-list-task"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="Hold Cart"><i class="bi bi-pause-circle"></i></button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="cart.clear()" title="Clear Cart"><i class="bi bi-trash"></i></button>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="<?= $lang === 'ar' ? 'الطلبات المعلقة' : 'Held List' ?>"><i class="bi bi-list-task"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="<?= $lang === 'ar' ? 'تعليق الطلب' : 'Hold Cart' ?>"><i class="bi bi-pause-circle"></i></button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="cart.clear()" title="<?= $lang === 'ar' ? 'مسح السلة' : 'Clear Cart' ?>"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-light border-bottom">
|
||||
<div class="mb-2">
|
||||
<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="العميل"><?= $lang === '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>
|
||||
@ -7769,25 +7777,35 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<div id="loyaltyDisplay" class="mt-2 p-2 rounded bg-light border border-primary-subtle" style="display:none">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<div>
|
||||
<span id="tierBadge" class="badge text-uppercase">Bronze</span>
|
||||
<span class="fw-bold ms-1 text-primary"><span id="customerPoints">0</span> pts</span>
|
||||
<span id="tierBadge" class="badge text-uppercase"><?= $lang === 'ar' ? 'برونزي' : 'Bronze' ?></span>
|
||||
<span class="fw-bold ms-1 text-primary"><span id="customerPoints">0</span> <?= $lang === 'ar' ? 'نقطة' : 'pts' ?></span>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="redeemLoyalty" onchange="cart.render()">
|
||||
<label class="form-check-label small fw-bold" for="redeemLoyalty">Redeem</label>
|
||||
<label class="form-check-label small fw-bold" for="redeemLoyalty"><?= $lang === 'ar' ? 'استبدال' : 'Redeem' ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress" style="height: 4px;">
|
||||
<div id="tierProgress" class="progress-bar bg-primary" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div id="nextTierInfo" class="smaller text-muted mt-1">Spend more to unlock Silver</div>
|
||||
<div id="nextTierInfo" class="smaller text-muted mt-1"><?= $lang === 'ar' ? 'أنفق المزيد للوصول إلى الفضي' : 'Spend more to unlock Silver' ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="small fw-bold mb-1">Discount Code</label>
|
||||
<?php if (($data['settings']['manual_discount_enabled'] ?? '0') === '1'): ?>
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold mb-1" data-en="Manual Discount" data-ar="الخصم اليدوي"><?= $lang === 'ar' ? 'الخصم اليدوي' : 'Manual Discount' ?></label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="discountCode" class="form-control" placeholder="Code">
|
||||
<button class="btn btn-outline-primary" type="button" onclick="cart.applyDiscount()">Apply</button>
|
||||
<span class="input-group-text"><?= __('currency') ?></span>
|
||||
<input type="number" id="manualDiscountAmount" class="form-control" value="0.000" min="0" step="0.001" oninput="cart.onManualDiscountChange()">
|
||||
</div>
|
||||
<div class="smaller text-muted mt-1" data-en="Fixed amount discount applied to the cart total." data-ar="يتم تطبيق خصم ثابت على إجمالي السلة."><?= $lang === 'ar' ? 'يتم تطبيق خصم ثابت على إجمالي السلة.' : 'Fixed amount discount applied to the cart total.' ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<label class="small fw-bold mb-1" data-en="Discount Code" data-ar="رمز الخصم"><?= $lang === 'ar' ? 'رمز الخصم' : 'Discount Code' ?></label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="discountCode" class="form-control" placeholder="<?= $lang === 'ar' ? 'الرمز' : 'Code' ?>" data-en="Code" data-ar="الرمز">
|
||||
<button class="btn btn-outline-primary" type="button" onclick="cart.applyDiscount()" data-en="Apply" data-ar="تطبيق"><?= $lang === 'ar' ? 'تطبيق' : 'Apply' ?></button>
|
||||
</div>
|
||||
<div id="appliedDiscountInfo" class="smaller text-primary mt-1" style="display:none"></div>
|
||||
</div>
|
||||
@ -7797,31 +7815,44 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
<!-- Cart items will be injected here -->
|
||||
<div class="text-center text-muted mt-5">
|
||||
<i class="bi bi-cart-x" style="font-size: 3rem;"></i>
|
||||
<p>Cart is empty</p>
|
||||
<p data-en="Cart is empty" data-ar="السلة فارغة"><?= $lang === 'ar' ? 'السلة فارغة' : 'Cart is empty' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-total">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span data-en="Subtotal (Excl. VAT)" data-ar="المجموع (بدون الضريبة)">Subtotal (Excl. VAT)</span>
|
||||
<span data-en="Subtotal (Excl. VAT)" data-ar="المجموع (بدون الضريبة)"><?= $lang === 'ar' ? 'المجموع (بدون الضريبة)' : 'Subtotal (Excl. VAT)' ?></span>
|
||||
<span id="posSubtotal"><?= __('currency') ?> 0.000</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span data-en="VAT" data-ar="الضريبة">VAT</span>
|
||||
<span data-en="VAT" data-ar="الضريبة"><?= $lang === 'ar' ? 'الضريبة' : 'VAT' ?></span>
|
||||
<span id="posVat"><?= __('currency') ?> 0.000</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-3 fw-bold fs-5 border-top pt-2">
|
||||
<span data-en="Total" data-ar="الإجمالي">Total</span>
|
||||
<span data-en="Total" data-ar="الإجمالي"><?= $lang === 'ar' ? 'الإجمالي' : 'Total' ?></span>
|
||||
<span id="posTotal" class="text-primary"><?= __('currency') ?> 0.000</span>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100 py-2 fw-bold" id="checkoutBtn" onclick="cart.checkout()">
|
||||
PLACE ORDER
|
||||
<button class="btn btn-primary w-100 py-2 fw-bold" id="checkoutBtn" onclick="cart.checkout()" data-en="Place Order" data-ar="إتمام الطلب">
|
||||
<?= $lang === 'ar' ? 'إتمام الطلب' : 'PLACE ORDER' ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const posLang = document.documentElement.lang || <?= json_encode($lang, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const posIsArabic = posLang === 'ar';
|
||||
const posT = (en, ar) => (posIsArabic ? ar : en);
|
||||
const posTierLabel = (tier) => {
|
||||
switch (String(tier || '').toLowerCase()) {
|
||||
case 'gold':
|
||||
return posT('Gold', 'ذهبي');
|
||||
case 'silver':
|
||||
return posT('Silver', 'فضي');
|
||||
default:
|
||||
return posT('Bronze', 'برونزي');
|
||||
}
|
||||
};
|
||||
const cart = {
|
||||
items: [],
|
||||
discount: null,
|
||||
@ -7843,26 +7874,73 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
}
|
||||
echo json_encode($custData);
|
||||
?>,
|
||||
isManualDiscountEnabled() {
|
||||
return String((typeof companySettings !== 'undefined' && companySettings && companySettings.manual_discount_enabled !== undefined) ? companySettings.manual_discount_enabled : '0') === '1';
|
||||
},
|
||||
getCodeDiscountAmount(subtotal) {
|
||||
if (!this.discount) return 0;
|
||||
const rawDiscount = this.discount.type === 'percentage'
|
||||
? subtotal * (parseFloat(this.discount.value) / 100)
|
||||
: parseFloat(this.discount.value);
|
||||
const safeDiscount = Number.isFinite(rawDiscount) ? rawDiscount : 0;
|
||||
return Math.min(Math.max(0, safeDiscount), Math.max(0, subtotal));
|
||||
},
|
||||
getManualDiscountAmount(maxDiscount = null, syncInput = false) {
|
||||
if (!this.isManualDiscountEnabled()) return 0;
|
||||
const input = document.getElementById('manualDiscountAmount');
|
||||
let value = input ? parseFloat(input.value) : 0;
|
||||
if (!Number.isFinite(value) || value < 0) value = 0;
|
||||
if (Number.isFinite(maxDiscount)) {
|
||||
value = Math.min(value, Math.max(0, maxDiscount));
|
||||
}
|
||||
if (syncInput && input) {
|
||||
input.value = value.toFixed(3);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
calculateTotals(syncInput = false) {
|
||||
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 = normalizeQuantity(item.qty);
|
||||
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
||||
return sum + (price * qty * (vatRate / (100 + vatRate)));
|
||||
}, 0);
|
||||
const codeDiscount = this.getCodeDiscountAmount(subtotal);
|
||||
const manualDiscount = this.getManualDiscountAmount(subtotal - codeDiscount, syncInput);
|
||||
const discountAmount = Math.min(subtotal, codeDiscount + manualDiscount);
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
|
||||
const availableRedeemValue = (parseFloat(this.customerPoints) || 0) / redeemRate;
|
||||
const loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked)
|
||||
? Math.min(Math.max(0, subtotal - discountAmount), availableRedeemValue)
|
||||
: 0;
|
||||
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemed);
|
||||
return { subtotal, totalVat, codeDiscount, manualDiscount, discountAmount, loyaltyRedeemed, total };
|
||||
},
|
||||
onManualDiscountChange() {
|
||||
const manualDiscountInput = document.getElementById('manualDiscountAmount');
|
||||
const manualValue = manualDiscountInput ? parseFloat(manualDiscountInput.value) : 0;
|
||||
if (Number.isFinite(manualValue) && manualValue > 0 && this.discount) {
|
||||
this.discount = null;
|
||||
const discInput = document.getElementById('discountCode');
|
||||
if (discInput) discInput.value = '';
|
||||
const discInfo = document.getElementById('appliedDiscountInfo');
|
||||
if (discInfo) discInfo.style.display = 'none';
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
broadcast() {
|
||||
try {
|
||||
// Ensure items is an array
|
||||
if (!Array.isArray(this.items)) this.items = [];
|
||||
|
||||
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 = normalizeQuantity(item.qty);
|
||||
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
||||
return sum + (price * qty * (vatRate / (100 + vatRate)));
|
||||
}, 0);
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
||||
}
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
|
||||
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(Math.max(0, subtotal - discountAmount), (parseFloat(this.customerPoints) || 0) / redeemRate) : 0;
|
||||
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemed);
|
||||
const totals = this.calculateTotals(false);
|
||||
const subtotal = totals.subtotal;
|
||||
const totalVat = totals.totalVat;
|
||||
const discountAmount = totals.discountAmount;
|
||||
const loyaltyRedeemed = totals.loyaltyRedeemed;
|
||||
const total = totals.total;
|
||||
|
||||
const customerSelect = document.getElementById('posCustomer');
|
||||
const customerName = customerSelect ? customerSelect.options[customerSelect.selectedIndex].text : '';
|
||||
@ -7874,7 +7952,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const vatRate = (i.vatRate !== undefined && i.vatRate !== null) ? i.vatRate : 0;
|
||||
const vatAmount = price * qty * (vatRate / (100 + vatRate));
|
||||
return {
|
||||
name: (function(){ let n = ''; if(i.nameAr) n += '<div>'+i.nameAr+'</div>'; if(i.nameEn) n += '<div>'+i.nameEn+'</div>'; return n || 'Unknown Item'; })(),
|
||||
name: (function(){ let n = ''; if(i.nameAr) n += '<div>'+i.nameAr+'</div>'; if(i.nameEn) n += '<div>'+i.nameEn+'</div>'; return n || posT('Unknown Item', 'صنف غير معروف'); })(),
|
||||
price: price,
|
||||
qty: qty,
|
||||
vat: vatAmount
|
||||
@ -7905,14 +7983,14 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const existing = this.items.find(item => item.id === product.id);
|
||||
if (existing) {
|
||||
if (!allowZeroStock && (existing.qty + addQty) > currentStock) {
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error');
|
||||
return;
|
||||
}
|
||||
existing.qty = normalizeQuantity(existing.qty + addQty);
|
||||
existing.price = unitPrice;
|
||||
} else {
|
||||
if (!allowZeroStock && currentStock < addQty) {
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error');
|
||||
return;
|
||||
}
|
||||
this.items.push({...normalizedProduct, qty: normalizeQuantity(addQty)});
|
||||
@ -7921,13 +7999,12 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
this.render();
|
||||
|
||||
// Add visual feedback
|
||||
const lang = document.documentElement.lang || 'en';
|
||||
const displayName = lang === 'ar' ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr);
|
||||
const displayName = posIsArabic ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr);
|
||||
Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: 'success',
|
||||
title: (lang === 'ar' ? 'تم إضافة: ' : 'Added: ') + displayName,
|
||||
title: (posIsArabic ? 'تمت إضافة: ' : 'Added: ') + displayName,
|
||||
showConfirmButton: false,
|
||||
timer: 800
|
||||
});
|
||||
@ -7944,7 +8021,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const currentStock = normalizeQuantity(item.stock_quantity);
|
||||
|
||||
if (delta > 0 && !allowZeroStock && (item.qty + delta) > currentStock) {
|
||||
Swal.fire('Error', 'Insufficient stock!', 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -7961,6 +8038,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
if (discInput) discInput.value = '';
|
||||
const discInfo = document.getElementById('appliedDiscountInfo');
|
||||
if (discInfo) discInfo.style.display = 'none';
|
||||
const manualDiscountInput = document.getElementById('manualDiscountAmount');
|
||||
if (manualDiscountInput) manualDiscountInput.value = '0.000';
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
if (redeemSwitch) redeemSwitch.checked = false;
|
||||
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
|
||||
@ -7990,7 +8069,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
|
||||
document.getElementById('customerPoints').innerText = Math.floor(this.customerPoints);
|
||||
const badge = document.getElementById('tierBadge');
|
||||
badge.innerText = this.customerTier;
|
||||
badge.innerText = posTierLabel(this.customerTier);
|
||||
badge.className = 'badge text-uppercase ' + (this.customerTier === 'gold' ? 'bg-warning text-dark' : (this.customerTier === 'silver' ? 'bg-info text-dark' : 'bg-secondary'));
|
||||
|
||||
const progressBar = document.getElementById('tierProgress');
|
||||
@ -7998,13 +8077,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
let progress = 0;
|
||||
if (this.customerTier === 'bronze') {
|
||||
progress = (spent / 500) * 100;
|
||||
nextTierInfo.innerText = `Spend ${(500 - spent).toFixed(3)} OMR more for Silver (1.2x points)`;
|
||||
nextTierInfo.innerText = posIsArabic ? `أنفق ${(500 - spent).toFixed(3)} <?= __('currency') ?> للوصول إلى الفضي (1.2x نقطة)` : `Spend ${(500 - spent).toFixed(3)} OMR more for Silver (1.2x points)`;
|
||||
} else if (this.customerTier === 'silver') {
|
||||
progress = ((spent - 500) / 1000) * 100;
|
||||
nextTierInfo.innerText = `Spend ${(1500 - spent).toFixed(3)} OMR more for Gold (1.5x points)`;
|
||||
nextTierInfo.innerText = posIsArabic ? `أنفق ${(1500 - spent).toFixed(3)} <?= __('currency') ?> للوصول إلى الذهبي (1.5x نقطة)` : `Spend ${(1500 - spent).toFixed(3)} OMR more for Gold (1.5x points)`;
|
||||
} else {
|
||||
progress = 100;
|
||||
nextTierInfo.innerText = 'You are a Gold member! (1.5x points)';
|
||||
nextTierInfo.innerText = posT('You are a Gold member! (1.5x points)', 'أنت عضو ذهبي! (1.5x نقطة)');
|
||||
}
|
||||
progressBar.style.width = Math.min(100, progress) + '%';
|
||||
|
||||
@ -8019,23 +8098,27 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
this.discount = res.discount;
|
||||
const manualDiscountInput = document.getElementById('manualDiscountAmount');
|
||||
if (manualDiscountInput) manualDiscountInput.value = '0.000';
|
||||
const info = document.getElementById('appliedDiscountInfo');
|
||||
info.innerText = `Applied: ${this.discount.code} (${this.discount.type === 'percentage' ? this.discount.value + '%' : 'OMR ' + parseFloat(this.discount.value).toFixed(3)})`;
|
||||
info.innerText = `${posT('Applied', 'تم تطبيق')}: ${this.discount.code} (${this.discount.type === 'percentage' ? this.discount.value + '%' : '<?= __('currency') ?> ' + parseFloat(this.discount.value).toFixed(3)})`;
|
||||
info.style.display = 'block';
|
||||
this.render();
|
||||
} else {
|
||||
Swal.fire('Error', res.error, 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), res.error, 'error');
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
},
|
||||
async hold() {
|
||||
if (this.items.length === 0) return;
|
||||
const { value: name } = await Swal.fire({
|
||||
title: 'Hold Cart',
|
||||
title: posT('Hold Cart', 'تعليق الطلب'),
|
||||
input: 'text',
|
||||
inputLabel: 'Enter a name for this cart',
|
||||
inputValue: 'Cart ' + new Date().toLocaleTimeString(),
|
||||
showCancelButton: true
|
||||
inputLabel: posT('Enter a name for this cart', 'أدخل اسمًا لهذا الطلب'),
|
||||
inputValue: (posIsArabic ? 'طلب ' : 'Cart ') + new Date().toLocaleTimeString(),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: posT('Save', 'حفظ'),
|
||||
cancelButtonText: posT('Cancel', 'إلغاء')
|
||||
});
|
||||
if (name) {
|
||||
const formData = new FormData();
|
||||
@ -8047,7 +8130,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
this.clear();
|
||||
Swal.fire('Held', 'Cart has been parked', 'success');
|
||||
Swal.fire(posT('Held', 'تم التعليق'), posT('Cart has been parked', 'تم حفظ الطلب كمعلّق'), 'success');
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -8060,7 +8143,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
carts = JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse held carts:', text);
|
||||
throw new Error('Invalid server response');
|
||||
throw new Error(posT('Invalid server response', 'استجابة غير صالحة من الخادم'));
|
||||
}
|
||||
const lang = document.documentElement.lang || 'en';
|
||||
let html = '<div class="list-group list-group-flush shadow-sm rounded">';
|
||||
@ -8105,7 +8188,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Swal.fire('Error', 'Failed to load held carts: ' + err.message, 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), posT('Failed to load held carts:', 'تعذر تحميل الطلبات المعلقة:') + ' ' + err.message, 'error');
|
||||
}
|
||||
},
|
||||
async resume(id) {
|
||||
@ -8122,7 +8205,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
Swal.fire('Error', 'Failed to resume cart', 'error');
|
||||
Swal.fire(posT('Error', 'خطأ'), posT('Failed to resume cart', 'تعذر استعادة الطلب'), 'error');
|
||||
}
|
||||
},
|
||||
async deleteHeld(id, silent = false) {
|
||||
@ -8167,13 +8250,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
let nameHtml = '';
|
||||
if (item.nameAr) nameHtml += `<div>${item.nameAr}</div>`;
|
||||
if (item.nameEn) nameHtml += `<div>${item.nameEn}</div>`;
|
||||
if (!nameHtml) nameHtml = '<div>' + (item.nameAr || item.nameEn || 'Unknown Item') + '</div>';
|
||||
if (!nameHtml) nameHtml = '<div>' + (item.nameAr || item.nameEn || posT('Unknown Item', 'صنف غير معروف')) + '</div>';
|
||||
|
||||
return `
|
||||
<div class="cart-item">
|
||||
<div class="flex-grow-1">
|
||||
<div class="small">${nameHtml}</div>
|
||||
<div class="text-muted smaller"><?= __('currency') ?> ${price.toFixed(3)} <span class="badge bg-light text-dark smaller">VAT ${parseFloat(vatRate || 0).toFixed(2)}%</span></div>
|
||||
<div class="text-muted smaller"><?= __('currency') ?> ${price.toFixed(3)} <span class="badge bg-light text-dark smaller">${posT('VAT', 'الضريبة')} ${parseFloat(vatRate || 0).toFixed(2)}%</span></div>
|
||||
</div>
|
||||
<div class="qty-controls mx-3">
|
||||
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
|
||||
@ -8182,31 +8265,16 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
</div>
|
||||
<div class="text-end" style="min-width: 80px;">
|
||||
<div class="fw-bold small"><?= __('currency') ?> ${itemTotal.toFixed(3)}</div>
|
||||
<div class="smaller text-muted">VAT: ${itemVat.toFixed(2)}</div>
|
||||
<div class="smaller text-muted">${posT('VAT', 'الضريبة')}: ${itemVat.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
if (this.discount.type === 'percentage') {
|
||||
discountAmount = subtotal * (parseFloat(this.discount.value) / 100);
|
||||
} else {
|
||||
discountAmount = parseFloat(this.discount.value);
|
||||
}
|
||||
}
|
||||
|
||||
let loyaltyRedeemedValue = 0;
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
if (redeemSwitch && redeemSwitch.checked) {
|
||||
const maxRedeemValue = subtotal - discountAmount;
|
||||
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
|
||||
const availableRedeemValue = (parseFloat(this.customerPoints) || 0) / redeemRate;
|
||||
loyaltyRedeemedValue = Math.min(Math.max(0, maxRedeemValue), availableRedeemValue);
|
||||
}
|
||||
|
||||
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemedValue);
|
||||
const totals = this.calculateTotals(false);
|
||||
const discountAmount = totals.discountAmount;
|
||||
const loyaltyRedeemedValue = totals.loyaltyRedeemed;
|
||||
const total = totals.total;
|
||||
const multiplier = parseFloat(this.customerMultiplier) || 1.0;
|
||||
const pointsToEarn = Math.floor(total * multiplier);
|
||||
|
||||
@ -8217,12 +8285,12 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
if (vatDisplay) vatDisplay.innerText = '<?= __('currency') ?> ' + totalVat.toFixed(2);
|
||||
|
||||
let totalHtml = '';
|
||||
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- Disc: <?= __('currency') ?> ${discountAmount.toFixed(3)}</div>`;
|
||||
if (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- Loyalty: <?= __('currency') ?> ${loyaltyRedeemedValue.toFixed(3)}</div>`;
|
||||
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- ${posT('Discount', 'الخصم')}: <?= __('currency') ?> ${discountAmount.toFixed(3)}</div>`;
|
||||
if (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- ${posT('Loyalty', 'الولاء')}: <?= __('currency') ?> ${loyaltyRedeemedValue.toFixed(3)}</div>`;
|
||||
|
||||
const customerId = document.getElementById('posCustomer') ? document.getElementById('posCustomer').value : '';
|
||||
if (customerId) {
|
||||
totalHtml += `<div class="smaller text-info">+ Earn: ${pointsToEarn} pts</div>`;
|
||||
totalHtml += `<div class="smaller text-info">+ ${posT('Earn', 'تكسب')}: ${pointsToEarn} ${posT('pts', 'نقطة')}</div>`;
|
||||
}
|
||||
|
||||
totalHtml += '<?= __('currency') ?> ' + total.toFixed(3);
|
||||
@ -8246,14 +8314,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
||||
document.getElementById('paymentCustomerName').innerText = customerName;
|
||||
|
||||
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
||||
}
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
let loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / 100) : 0;
|
||||
const total = subtotal - discountAmount - loyaltyRedeemedValue;
|
||||
const totals = this.calculateTotals(false);
|
||||
const total = totals.total;
|
||||
|
||||
this.payments = [];
|
||||
this.renderPayments();
|
||||
@ -8318,14 +8380,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
this.updateRemaining();
|
||||
},
|
||||
getGrandTotal() {
|
||||
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
||||
}
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
let loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / this.loyaltySettings.redeemPointsPerUnit) : 0;
|
||||
return subtotal - discountAmount - loyaltyRedeemedValue;
|
||||
return this.calculateTotals(false).total;
|
||||
},
|
||||
getRemaining() {
|
||||
const total = this.getGrandTotal();
|
||||
@ -8455,17 +8510,11 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
|
||||
const savedWithoutPrintPrefix = <?= json_encode($lang === 'ar' ? 'تم حفظ الفاتورة بدون طباعة. الرقم:' : 'Invoice saved without printing. No:', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const okText = <?= json_encode($lang === 'ar' ? 'حسنًا' : 'OK', 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) => {
|
||||
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
||||
return sum + ((parseFloat(item.price) * item.qty) * (vatRate / (100 + vatRate)));
|
||||
}, 0);
|
||||
let discountAmount = 0;
|
||||
if (this.discount) {
|
||||
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
||||
}
|
||||
const redeemSwitch = document.getElementById('redeemLoyalty');
|
||||
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / this.loyaltySettings.redeemPointsPerUnit) : 0;
|
||||
const totals = this.calculateTotals(false);
|
||||
const subtotal = totals.subtotal;
|
||||
const totalVat = totals.totalVat;
|
||||
const discountAmount = totals.discountAmount;
|
||||
const loyaltyRedeemed = totals.loyaltyRedeemed;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'save_pos_transaction');
|
||||
@ -12259,13 +12308,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<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="<?= $lang === 'ar' ? 'إغلاق' : 'Close' ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2 p-2 border rounded bg-light shadow-sm">
|
||||
<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="small text-muted d-block" data-en="Customer" data-ar="العميل"><?= $lang === 'ar' ? 'العميل' : '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>
|
||||
@ -12321,7 +12370,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col">
|
||||
<label class="form-label smaller fw-bold mb-1" data-en="Amount" data-ar="المبلغ">Amount</label>
|
||||
<label class="form-label smaller fw-bold mb-1" data-en="Amount" data-ar="المبلغ"><?= $lang === 'ar' ? 'المبلغ' : 'Amount' ?></label>
|
||||
<div class="input-group">
|
||||
<input type="number" step="0.001" id="partialAmount" class="form-control" placeholder="0.000" oninput="cart.updateRemaining()">
|
||||
</div>
|
||||
@ -12350,7 +12399,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</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-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء"><?= $lang === 'ar' ? 'إلغاء' : 'Cancel' ?></button>
|
||||
<button type="button" class="btn btn-outline-primary pos-complete-order-btn" id="confirmPaymentSaveBtn" onclick="cart.completeOrder(false)" data-en="Save Without Print" data-ar="حفظ بدون طباعة">
|
||||
<?= $lang === 'ar' ? 'حفظ بدون طباعة' : 'SAVE WITHOUT PRINT' ?>
|
||||
</button>
|
||||
@ -12367,7 +12416,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<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="<?= $lang === 'ar' ? 'إغلاق' : 'Close' ?>"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<div id="posReceiptContent">
|
||||
@ -12375,8 +12424,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-primary w-100" onclick="printPosReceipt()">
|
||||
<i class="bi bi-printer me-2"></i>PRINT RECEIPT
|
||||
<button type="button" class="btn btn-primary w-100" onclick="printPosReceipt()" data-en="Print Receipt" data-ar="طباعة الإيصال">
|
||||
<i class="bi bi-printer me-2"></i><?= $lang === 'ar' ? 'طباعة الإيصال' : 'PRINT RECEIPT' ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -744,9 +744,9 @@
|
||||
window.printPosReceiptFromInvoice = function(inv) {
|
||||
const container = document.getElementById('posReceiptContent');
|
||||
const itemsHtml = inv.items.map(item => {
|
||||
const itemTotal = item.unit_price * item.quantity;
|
||||
const itemTotal = Number.isFinite(parseFloat(item.total_price)) ? parseFloat(item.total_price) : (parseFloat(item.unit_price || 0) * normalizeQuantity(item.quantity || 0));
|
||||
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
|
||||
const vatAmount = itemTotal * (vatRate / (100 + vatRate));
|
||||
const vatAmount = Number.isFinite(parseFloat(item.vat_amount)) ? parseFloat(item.vat_amount) : (itemTotal * (vatRate / (100 + vatRate)));
|
||||
return `
|
||||
<tr>
|
||||
<td>${item.name_en} / ${item.name_ar}<br><small>${formatQuantity(item.quantity)} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
|
||||
@ -756,12 +756,18 @@
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const totalVat = inv.items.reduce((sum, item) => {
|
||||
const itemTotal = item.unit_price * item.quantity;
|
||||
const vatRate = parseFloat(item.vat_rate !== undefined && item.vat_rate !== null ? item.vat_rate : 0);
|
||||
return sum + (itemTotal * (vatRate / (100 + vatRate)));
|
||||
}, 0);
|
||||
const subtotal = inv.items.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
|
||||
const totalVat = Number.isFinite(parseFloat(inv.vat_amount))
|
||||
? parseFloat(inv.vat_amount)
|
||||
: inv.items.reduce((sum, item) => sum + (Number.isFinite(parseFloat(item.vat_amount)) ? parseFloat(item.vat_amount) : 0), 0);
|
||||
const totalAmount = Number.isFinite(parseFloat(inv.total_amount)) ? parseFloat(inv.total_amount) : 0;
|
||||
const discountAmount = Math.max(0, Number.isFinite(parseFloat(inv.discount_amount)) ? parseFloat(inv.discount_amount) : 0);
|
||||
const loyaltyRedeemed = Math.max(0, Number.isFinite(parseFloat(inv.loyalty_points_redeemed)) ? parseFloat(inv.loyalty_points_redeemed) : 0);
|
||||
const isPosInvoice = String(inv.is_pos || '0') === '1';
|
||||
const grossBeforeDiscount = isPosInvoice ? totalAmount : (totalAmount + totalVat);
|
||||
const subtotalExVat = isPosInvoice ? Math.max(0, grossBeforeDiscount - totalVat) : totalAmount;
|
||||
const grandTotalValue = Number.isFinite(parseFloat(inv.total_with_vat)) ? parseFloat(inv.total_with_vat) : Math.max(0, grossBeforeDiscount - discountAmount - loyaltyRedeemed);
|
||||
const paidAmount = Number.isFinite(parseFloat(inv.paid_amount)) ? parseFloat(inv.paid_amount) : 0;
|
||||
const balanceAmount = Math.max(0, grandTotalValue - paidAmount);
|
||||
|
||||
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
||||
const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>";
|
||||
@ -805,7 +811,7 @@
|
||||
<div class="separator"></div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span>Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)</span>
|
||||
<span><?= __('currency') ?> ${(subtotal - totalVat).toFixed(3)}</span>
|
||||
<span><?= __('currency') ?> ${subtotalExVat.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span>VAT / الضريبة</span>
|
||||
@ -813,15 +819,21 @@
|
||||
</div>
|
||||
<div class="total-row d-flex justify-content-between">
|
||||
<span>TOTAL (Incl. VAT) / الإجمالي (شامل الضريبة)</span>
|
||||
<span><?= __('currency') ?> ${subtotal.toFixed(3)}</span>
|
||||
<span><?= __('currency') ?> ${(grossBeforeDiscount).toFixed(3)}</span>
|
||||
</div>
|
||||
${discountAmount > 0 ? `<div class="d-flex justify-content-between small text-danger"><span>DISCOUNT / الخصم</span><span>- <?= __('currency') ?> ${discountAmount.toFixed(3)}</span></div>` : ''}
|
||||
${loyaltyRedeemed > 0 ? `<div class="d-flex justify-content-between small text-success"><span>LOYALTY / الولاء</span><span>- <?= __('currency') ?> ${loyaltyRedeemed.toFixed(3)}</span></div>` : ''}
|
||||
<div class="d-flex justify-content-between total-row">
|
||||
<span>NET TOTAL / الإجمالي النهائي</span>
|
||||
<span><?= __('currency') ?> ${grandTotalValue.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span>PAID / المدفوع</span>
|
||||
<span><?= __('currency') ?> ${parseFloat(inv.paid_amount).toFixed(3)}</span>
|
||||
<span><?= __('currency') ?> ${paidAmount.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small fw-bold">
|
||||
<span>BALANCE / الرصيد</span>
|
||||
<span><?= __('currency') ?> ${(subtotal - inv.paid_amount).toFixed(3)}</span>
|
||||
<span><?= __('currency') ?> ${balanceAmount.toFixed(3)}</span>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="center small">
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
const statusSelect = document.getElementById('edit_status');
|
||||
const paidAmountInput = document.getElementById('edit_paid_amount');
|
||||
const paidAmountContainer = document.getElementById('editPaidAmountContainer');
|
||||
const discountAmountInput = document.getElementById('edit_discount_amount');
|
||||
|
||||
const partyId = data.customer_id ?? data.supplier_id ?? '';
|
||||
const partyLabel = data.party_name || data.customer_name || data.supplier_name || '';
|
||||
@ -77,11 +78,20 @@
|
||||
if (paymentTypeSelect) paymentTypeSelect.value = normalizeEditPaymentType(data.payment_type);
|
||||
if (statusSelect) statusSelect.value = data.status || 'unpaid';
|
||||
if (paidAmountInput) paidAmountInput.value = parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
if (discountAmountInput) discountAmountInput.value = parseFloat(data.discount_amount || 0).toFixed(3);
|
||||
if (paidAmountContainer) {
|
||||
paidAmountContainer.style.display = data.status === 'partially_paid' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
renderEditInvoiceItems(data.items || []);
|
||||
|
||||
const tableBody = document.getElementById('editInvoiceItemsTableBody');
|
||||
const grandTotalEl = document.getElementById('edit_grandTotal');
|
||||
const subtotalEl = document.getElementById('edit_subtotal');
|
||||
const totalVatEl = document.getElementById('edit_totalVat');
|
||||
if (tableBody && typeof recalculate === 'function') {
|
||||
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
|
||||
|
||||
@ -1,4 +1,17 @@
|
||||
// Invoice Form Logic
|
||||
const getInvoiceDiscountInput = (tableBody) => {
|
||||
const form = tableBody && typeof tableBody.closest === 'function' ? tableBody.closest('form') : null;
|
||||
return form ? form.querySelector('[data-invoice-discount-input]') : null;
|
||||
};
|
||||
|
||||
const getInvoiceDiscountValue = (tableBody) => {
|
||||
const input = getInvoiceDiscountInput(tableBody);
|
||||
if (!input) return 0;
|
||||
let value = parseFloat(input.value);
|
||||
if (!Number.isFinite(value) || value < 0) value = 0;
|
||||
return value;
|
||||
};
|
||||
|
||||
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
||||
const searchInput = document.getElementById(searchInputId);
|
||||
const suggestions = document.getElementById(suggestionsId);
|
||||
@ -9,6 +22,18 @@
|
||||
|
||||
if (!searchInput || !tableBody) return;
|
||||
|
||||
const discountInput = getInvoiceDiscountInput(tableBody);
|
||||
if (discountInput && discountInput.dataset.discountBound !== '1') {
|
||||
discountInput.dataset.discountBound = '1';
|
||||
discountInput.addEventListener('input', function() {
|
||||
const nextValue = parseFloat(this.value);
|
||||
if (Number.isFinite(nextValue) && nextValue < 0) {
|
||||
this.value = '0';
|
||||
}
|
||||
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
||||
});
|
||||
}
|
||||
|
||||
let timeout = null;
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(timeout);
|
||||
@ -219,7 +244,15 @@ ${text}` : title);
|
||||
subtotal += total;
|
||||
totalVat += vatAmount;
|
||||
});
|
||||
const grandTotal = subtotal + totalVat;
|
||||
|
||||
const rawGrandTotal = subtotal + totalVat;
|
||||
const discountInput = getInvoiceDiscountInput(tableBody);
|
||||
let discountAmount = getInvoiceDiscountValue(tableBody);
|
||||
if (discountAmount > rawGrandTotal) {
|
||||
discountAmount = rawGrandTotal;
|
||||
if (discountInput) discountInput.value = discountAmount.toFixed(3);
|
||||
}
|
||||
const grandTotal = Math.max(0, rawGrandTotal - discountAmount);
|
||||
|
||||
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
||||
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(2);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<?php $manualDiscountEnabled = $page === 'sales' && (($data['settings']['manual_discount_enabled'] ?? '0') === '1'); ?>
|
||||
<!-- View Return Details Modal -->
|
||||
<div class="modal fade" id="viewReturnDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
@ -487,6 +488,18 @@
|
||||
<td class="text-end fw-bold" id="totalVat"><?= __('currency') ?> 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php if ($manualDiscountEnabled): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Manual Discount" data-ar="الخصم اليدوي">Manual Discount</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><?= __('currency') ?></span>
|
||||
<input type="number" step="0.001" min="0" name="discount_amount" class="form-control text-end" value="0.000" data-invoice-discount-input>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="grandTotal"><?= __('currency') ?> 0.000</td>
|
||||
@ -591,6 +604,18 @@
|
||||
<td class="text-end fw-bold" id="edit_totalVat"><?= __('currency') ?> 0.000</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php if ($manualDiscountEnabled): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-end fw-bold" data-en="Manual Discount" data-ar="الخصم اليدوي">Manual Discount</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><?= __('currency') ?></span>
|
||||
<input type="number" step="0.001" min="0" name="discount_amount" id="edit_discount_amount" class="form-control text-end" value="0.000" data-invoice-discount-input>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr class="table-primary">
|
||||
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
||||
<td class="text-end fw-bold h5" id="edit_grandTotal"><?= __('currency') ?> 0.000</td>
|
||||
|
||||
@ -67,16 +67,34 @@
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
const vatVal = parseFloat(data.vat_amount || 0);
|
||||
const totalVal = parseFloat(data.total_amount || 0);
|
||||
const grandTotalValue = (parseFloat(data.total_with_vat) || (totalVal + vatVal));
|
||||
const vatVal = Number.isFinite(parseFloat(data.vat_amount)) ? parseFloat(data.vat_amount) : 0;
|
||||
const totalVal = Number.isFinite(parseFloat(data.total_amount)) ? parseFloat(data.total_amount) : 0;
|
||||
const discountVal = Math.max(0, Number.isFinite(parseFloat(data.discount_amount)) ? parseFloat(data.discount_amount) : 0);
|
||||
const isPosInvoice = String(data.is_pos || '0') === '1';
|
||||
const grossBeforeDiscount = isPosInvoice ? totalVal : (totalVal + vatVal);
|
||||
const grandTotalParsed = parseFloat(data.total_with_vat);
|
||||
const grandTotalValue = Number.isFinite(grandTotalParsed) ? grandTotalParsed : Math.max(0, grossBeforeDiscount - discountVal);
|
||||
const subtotalExVat = isPosInvoice ? Math.max(0, grossBeforeDiscount - vatVal) : totalVal;
|
||||
|
||||
if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').innerHTML = '<small><?= __('currency') ?></small> ' + (grandTotalValue - vatVal).toFixed(3);
|
||||
if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').innerHTML = '<small><?= __('currency') ?></small> ' + subtotalExVat.toFixed(3);
|
||||
if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').innerHTML = '<small><?= __('currency') ?></small> ' + vatVal.toFixed(2);
|
||||
|
||||
const discountRow = document.getElementById('invDiscountRow');
|
||||
const discountAmountEl = document.getElementById('invDiscountAmount');
|
||||
if (discountRow && discountAmountEl) {
|
||||
if (discountVal > 0) {
|
||||
discountRow.style.display = 'flex';
|
||||
discountAmountEl.innerHTML = '<small><?= __('currency') ?></small> ' + discountVal.toFixed(3);
|
||||
} else {
|
||||
discountRow.style.display = 'none';
|
||||
discountAmountEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').innerHTML = '<small><?= __('currency') ?></small> ' + grandTotalValue.toFixed(3);
|
||||
|
||||
if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').innerHTML = '<small><?= __('currency') ?></small> ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
||||
const balance = Math.max(0, grandTotalValue - parseFloat(data.paid_amount || 0));
|
||||
if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').innerHTML = '<small><?= __('currency') ?></small> ' + balance.toFixed(3);
|
||||
|
||||
// Generate QR Code for Zakat, Tax and Customs Authority (ZATCA) style or simple formal
|
||||
|
||||
@ -186,6 +186,10 @@
|
||||
<span class="text-muted">VAT Amount / مبلغ الضريبة</span>
|
||||
<span id="invVatAmount" class="fw-bold text-nowrap"></span>
|
||||
</div>
|
||||
<div id="invDiscountRow" class="d-flex justify-content-between mb-2 text-danger" style="display: none;">
|
||||
<span class="text-muted">Discount / الخصم</span>
|
||||
<span id="invDiscountAmount" class="fw-bold text-nowrap"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-3 border-top pt-2">
|
||||
<span class="h4 fw-bold">Grand Total / المجموع الكلي</span>
|
||||
<span id="invGrandTotal" class="h4 fw-bold text-primary text-nowrap"></span>
|
||||
|
||||
@ -44,12 +44,30 @@
|
||||
$total_vat += $vatAmount;
|
||||
}
|
||||
|
||||
$total_with_vat = $total_subtotal + $total_vat;
|
||||
$paid = (float)($_POST['paid_amount'] ?? 0);
|
||||
$gross_total = $total_subtotal + $total_vat;
|
||||
$manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1';
|
||||
$hasDiscountColumn = ($type === 'sale') && db_column_exists($table, 'discount_amount');
|
||||
$discount_amount = 0.0;
|
||||
if ($type === 'sale' && $hasDiscountColumn && $manualDiscountEnabled) {
|
||||
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
|
||||
if ($discount_amount > $gross_total) {
|
||||
$discount_amount = $gross_total;
|
||||
}
|
||||
}
|
||||
$total_with_vat = max(0, $gross_total - $discount_amount);
|
||||
$paid = max(0, (float)($_POST['paid_amount'] ?? 0));
|
||||
if ($paid > $total_with_vat) {
|
||||
$paid = $total_with_vat;
|
||||
}
|
||||
if ($status === 'paid') $paid = $total_with_vat;
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
|
||||
if ($hasDiscountColumn) {
|
||||
$stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, discount_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $discount_amount]);
|
||||
} else {
|
||||
$stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
|
||||
}
|
||||
$inv_id = $db->lastInsertId();
|
||||
if (db_column_exists($table, 'outlet_id')) {
|
||||
$db->prepare("UPDATE $table SET outlet_id = ? WHERE id = ?")->execute([current_outlet_id(), $inv_id]);
|
||||
@ -141,12 +159,36 @@
|
||||
$total_vat += $vatAmount;
|
||||
}
|
||||
|
||||
$total_with_vat = $total_subtotal + $total_vat;
|
||||
$paid = (float)($_POST['paid_amount'] ?? 0);
|
||||
$gross_total = $total_subtotal + $total_vat;
|
||||
$manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1';
|
||||
$hasDiscountColumn = ($type === 'sale') && db_column_exists($table, 'discount_amount');
|
||||
$discount_amount = 0.0;
|
||||
if ($hasDiscountColumn) {
|
||||
if ($manualDiscountEnabled && array_key_exists('discount_amount', $_POST)) {
|
||||
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
|
||||
} else {
|
||||
$existingDiscountStmt = $db->prepare("SELECT discount_amount FROM $table WHERE id = ? LIMIT 1");
|
||||
$existingDiscountStmt->execute([$id]);
|
||||
$discount_amount = max(0, (float)$existingDiscountStmt->fetchColumn());
|
||||
}
|
||||
if ($discount_amount > $gross_total) {
|
||||
$discount_amount = $gross_total;
|
||||
}
|
||||
}
|
||||
$total_with_vat = max(0, $gross_total - $discount_amount);
|
||||
$paid = max(0, (float)($_POST['paid_amount'] ?? 0));
|
||||
if ($paid > $total_with_vat) {
|
||||
$paid = $total_with_vat;
|
||||
}
|
||||
if ($status === 'paid') $paid = $total_with_vat;
|
||||
|
||||
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
|
||||
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
|
||||
if ($hasDiscountColumn) {
|
||||
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ?, discount_amount = ? WHERE id = ?")
|
||||
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $discount_amount, $id]);
|
||||
} else {
|
||||
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
|
||||
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
|
||||
}
|
||||
if (db_column_exists($table, 'outlet_id')) {
|
||||
$db->prepare("UPDATE $table SET outlet_id = COALESCE(outlet_id, ?) WHERE id = ?")->execute([current_outlet_id(), $id]);
|
||||
}
|
||||
|
||||
@ -105,6 +105,7 @@ if (isset($_POST['update_settings'])) {
|
||||
: date_default_timezone_get();
|
||||
|
||||
$settings['allow_zero_stock_sell'] = (($settings['allow_zero_stock_sell'] ?? '1') === '0') ? '0' : '1';
|
||||
$settings['manual_discount_enabled'] = (($settings['manual_discount_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['loyalty_enabled'] = (($settings['loyalty_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['smtp_enabled'] = (($settings['smtp_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
$settings['wablas_enabled'] = (($settings['wablas_enabled'] ?? '0') === '1') ? '1' : '0';
|
||||
|
||||
@ -198,6 +198,14 @@ $wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data[
|
||||
<option value="1" <?= ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1' ? 'selected' : '' ?> data-en="Allow selling out of stock" data-ar="السماح بالبيع عند نفاذ المخزون">Allow selling out of stock</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Manual Discount in Sales & POS" data-ar="الخصم اليدوي في المبيعات ونقاط البيع">Manual Discount in Sales & POS</label>
|
||||
<select name="settings[manual_discount_enabled]" class="form-select">
|
||||
<option value="0" <?= ($data['settings']['manual_discount_enabled'] ?? '0') === '1' ? '' : 'selected' ?> data-en="Disabled" data-ar="معطل">Disabled</option>
|
||||
<option value="1" <?= ($data['settings']['manual_discount_enabled'] ?? '0') === '1' ? 'selected' : '' ?> data-en="Enabled" data-ar="مفعل">Enabled</option>
|
||||
</select>
|
||||
<div class="form-text" data-en="Shows a fixed discount amount field on POS and Sales invoices." data-ar="يعرض حقل خصم ثابت في نقاط البيع وفواتير المبيعات.">Shows a fixed discount amount field on POS and Sales invoices.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small fw-semibold" data-en="Scale Barcode Mode" data-ar="وضع باركود الميزان">Scale Barcode Mode</label>
|
||||
<select name="settings[weight_barcode_mode]" class="form-select">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user