Autosave: 20260508-013014

This commit is contained in:
Flatlogic Bot 2026-05-08 01:30:14 +00:00
parent 40fd435ffd
commit 5a46bb40e1
10 changed files with 353 additions and 151 deletions

299
index.php
View File

@ -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>

View File

@ -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">

View File

@ -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 => {

View File

@ -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);

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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]);
}

View File

@ -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';

View File

@ -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">