-
${c.cart_name}
+
+
+
${cartName}
- ${c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')}
+ ${customerName}
|
- ${new Date(c.created_at).toLocaleString()}
+ ${escapeHtml(createdAtText)}
-
-
+
+
${lang === 'ar' ? 'استرجاع' : 'Resume'}
-
+
@@ -8184,6 +8371,36 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
width: '700px',
customClass: {
container: 'held-carts-swal'
+ },
+ didOpen: (popup) => {
+ const triggerHeldCartAction = (action, id) => {
+ if (!Number.isFinite(id) || id <= 0) return;
+ if (action === 'delete') {
+ this.deleteHeld(id);
+ return;
+ }
+ this.resume(id);
+ };
+
+ popup.querySelectorAll('.held-cart-entry').forEach((row) => {
+ row.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ const id = Number.parseInt(row.getAttribute('data-held-cart-id') || '', 10);
+ triggerHeldCartAction('resume', id);
+ }
+ });
+ });
+
+ popup.addEventListener('click', (event) => {
+ const actionTarget = event.target.closest('[data-held-action]');
+ if (!actionTarget) return;
+ event.preventDefault();
+ event.stopPropagation();
+ const action = actionTarget.getAttribute('data-held-action');
+ const id = Number.parseInt(actionTarget.getAttribute('data-held-cart-id') || '', 10);
+ triggerHeldCartAction(action, id);
+ });
}
});
} catch (err) {
@@ -8271,7 +8488,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
`;
}).join('');
- const totals = this.calculateTotals(false);
+ const shouldSyncManualDiscountInput = !(document.activeElement && document.activeElement.id === 'manualDiscountAmount');
+ const totals = this.calculateTotals(shouldSyncManualDiscountInput);
const discountAmount = totals.discountAmount;
const loyaltyRedeemedValue = totals.loyaltyRedeemed;
const total = totals.total;
@@ -8514,6 +8732,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
const subtotal = totals.subtotal;
const totalVat = totals.totalVat;
const discountAmount = totals.discountAmount;
+ const manualDiscountAmount = totals.manualDiscount;
const loyaltyRedeemed = totals.loyaltyRedeemed;
const formData = new FormData();
@@ -8524,6 +8743,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
formData.append('tax_amount', totalVat);
formData.append('discount_code_id', this.discount ? this.discount.id : '');
formData.append('discount_amount', discountAmount);
+ formData.append('manual_discount_amount', manualDiscountAmount);
formData.append('loyalty_redeemed', loyaltyRedeemed);
formData.append('items', JSON.stringify(this.items.map(i => {
const vr = (i.vatRate !== undefined && i.vatRate !== null) ? i.vatRate : 0;
@@ -8722,6 +8942,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
nameEn: card.dataset.nameEn,
nameAr: card.dataset.nameAr,
price: parseFloat(card.dataset.price),
+ purchasePrice: parseFloat(card.dataset.purchasePrice) || 0,
sku: card.dataset.sku,
stock_quantity: parseFloat(card.dataset.stockQuantity),
vatRate: parseFloat(card.dataset.vatRate) || 0
diff --git a/pages/sales_purchases_invoice_actions_script.php b/pages/sales_purchases_invoice_actions_script.php
index 1e9ad7e..37f21bd 100644
--- a/pages/sales_purchases_invoice_actions_script.php
+++ b/pages/sales_purchases_invoice_actions_script.php
@@ -41,10 +41,12 @@
name_ar: item.name_ar || item.item_name_ar || '',
sku: item.sku || '',
vat_rate: item.vat_rate || 0,
+ purchase_price: item.purchase_price || 0,
stock_quantity: item.stock_quantity || 0
}, tableBody, null, null, grandTotalEl, subtotalEl, totalVatEl, {
quantity: item.quantity,
- unit_price: item.unit_price
+ unit_price: item.unit_price,
+ purchase_price: item.purchase_price || 0
});
});
};
diff --git a/pages/sales_purchases_invoice_form_helpers.php b/pages/sales_purchases_invoice_form_helpers.php
index a84869d..32a33fe 100644
--- a/pages/sales_purchases_invoice_form_helpers.php
+++ b/pages/sales_purchases_invoice_form_helpers.php
@@ -12,6 +12,51 @@
return value;
};
+ const getInvoiceManualDiscountPercent = () => {
+ let value = (typeof companySettings !== 'undefined' && companySettings && companySettings.manual_discount_profit_limit_percent !== undefined)
+ ? parseFloat(companySettings.manual_discount_profit_limit_percent)
+ : 5;
+ if (!Number.isFinite(value) || value < 0) value = 5;
+ return Math.min(100, Math.max(0, value));
+ };
+
+ const getInvoiceManualDiscountMetrics = (tableBody) => {
+ const percent = getInvoiceManualDiscountPercent();
+ if (!tableBody || invoiceType !== 'sale') {
+ return { percent, profitAmount: 0, maxDiscount: 0 };
+ }
+
+ let profitAmount = 0;
+ tableBody.querySelectorAll('.item-row').forEach(row => {
+ const qty = normalizeQuantity(row.querySelector('.item-qty')?.value || 0);
+ const unitPrice = parseFloat(row.querySelector('.item-price')?.value) || 0;
+ const purchasePrice = parseFloat(row.querySelector('.item-purchase-price')?.value) || 0;
+ profitAmount += (unitPrice - purchasePrice) * qty;
+ });
+
+ profitAmount = Math.max(0, profitAmount);
+ return {
+ percent,
+ profitAmount,
+ maxDiscount: Math.max(0, profitAmount * (percent / 100))
+ };
+ };
+
+ const updateInvoiceDiscountHelp = (tableBody, limitAmount = 0, metrics = null) => {
+ const input = getInvoiceDiscountInput(tableBody);
+ if (!input) return;
+ const form = tableBody && typeof tableBody.closest === 'function' ? tableBody.closest('form') : null;
+ const help = form ? form.querySelector('[data-invoice-discount-help]') : null;
+ if (!help) return;
+ const activeMetrics = metrics || getInvoiceManualDiscountMetrics(tableBody);
+ const safeLimit = Math.max(0, Number(limitAmount) || 0);
+ const percentLabel = activeMetrics.percent.toFixed(3).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
+ const isArabic = (document.documentElement.lang || 'en') === 'ar';
+ help.textContent = isArabic
+ ? `الحد الأقصى الآن: = __('currency') ?> ${safeLimit.toFixed(3)} (${percentLabel}% من ربح الفاتورة = __('currency') ?> ${activeMetrics.profitAmount.toFixed(3)})`
+ : `Max allowed now: = __('currency') ?> ${safeLimit.toFixed(3)} (${percentLabel}% of invoice profit = __('currency') ?> ${activeMetrics.profitAmount.toFixed(3)})`;
+ };
+
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
const searchInput = document.getElementById(searchInputId);
const suggestions = document.getElementById(suggestionsId);
@@ -206,12 +251,16 @@ ${text}` : title);
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
const qty = normalizeQuantity(customData ? customData.quantity : 1);
const vatRate = item.vat_rate || 0;
+ const purchasePrice = customData && customData.purchase_price !== undefined && customData.purchase_price !== null
+ ? customData.purchase_price
+ : (item.purchase_price || 0);
row.innerHTML = `
+
${item.name_en}
${item.name_ar} (${item.sku})
@@ -247,11 +296,14 @@ ${text}` : title);
const rawGrandTotal = subtotal + totalVat;
const discountInput = getInvoiceDiscountInput(tableBody);
+ const discountMetrics = getInvoiceManualDiscountMetrics(tableBody);
+ const maxDiscountAmount = discountInput ? Math.min(rawGrandTotal, discountMetrics.maxDiscount) : rawGrandTotal;
let discountAmount = getInvoiceDiscountValue(tableBody);
- if (discountAmount > rawGrandTotal) {
- discountAmount = rawGrandTotal;
+ if (discountAmount > maxDiscountAmount) {
+ discountAmount = maxDiscountAmount;
if (discountInput) discountInput.value = discountAmount.toFixed(3);
}
+ updateInvoiceDiscountHelp(tableBody, maxDiscountAmount, discountMetrics);
const grandTotal = Math.max(0, rawGrandTotal - discountAmount);
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
diff --git a/pages/sales_purchases_modals.php b/pages/sales_purchases_modals.php
index 73df5e9..817171e 100644
--- a/pages/sales_purchases_modals.php
+++ b/pages/sales_purchases_modals.php
@@ -496,6 +496,7 @@
= __('currency') ?>
+
@@ -612,6 +613,7 @@
= __('currency') ?>
+
diff --git a/pages/sales_purchases_print_script.php b/pages/sales_purchases_print_script.php
index f3abe80..69d6e6a 100644
--- a/pages/sales_purchases_print_script.php
+++ b/pages/sales_purchases_print_script.php
@@ -1,72 +1,200 @@
+ const escapeHtml = (value) => {
+ const stringValue = value == null ? '' : String(value);
+ return stringValue.replace(/[&<>"']/g, (character) => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ }[character] || character));
+ };
+
+ const humanizeInvoiceText = (value) => {
+ const raw = String(value ?? '').trim();
+ if (!raw) {
+ return '---';
+ }
+
+ return raw
+ .replace(/[_-]+/g, ' ')
+ .replace(/\s+/g, ' ')
+ .toLowerCase()
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
+ };
+
+ const formatInvoiceCurrency = (amount, decimals = 3) => {
+ const numericAmount = Number.isFinite(parseFloat(amount)) ? parseFloat(amount) : 0;
+ return `
= __('currency') ?> ${numericAmount.toFixed(decimals)}`;
+ };
+
+ const setInvoiceText = (id, value) => {
+ const element = document.getElementById(id);
+ if (element) {
+ element.textContent = value == null || value === '' ? '---' : String(value);
+ }
+ return element;
+ };
+
+ const toggleInvoiceField = (containerId, valueId, value) => {
+ const container = document.getElementById(containerId);
+ const valueElement = document.getElementById(valueId);
+ if (!container || !valueElement) {
+ return;
+ }
+
+ if (value) {
+ valueElement.textContent = value;
+ container.style.display = 'block';
+ } else {
+ valueElement.textContent = '';
+ container.style.display = 'none';
+ }
+ };
+
+ let invoicePrintOriginalParent = null;
+ let invoicePrintOriginalNextSibling = null;
+ let invoicePrintRestoreTimer = null;
+
+ const prepareInvoiceForPrint = () => {
+ const modal = document.getElementById('viewInvoiceModal');
+ if (!modal || !document.body) {
+ return;
+ }
+
+ if (!invoicePrintOriginalParent) {
+ invoicePrintOriginalParent = modal.parentNode;
+ invoicePrintOriginalNextSibling = modal.nextSibling;
+ }
+
+ if (modal.parentNode !== document.body) {
+ document.body.appendChild(modal);
+ }
+
+ document.body.classList.add('printing-invoice');
+ };
+
+ const restoreInvoiceAfterPrint = () => {
+ const modal = document.getElementById('viewInvoiceModal');
+ if (!document.body) {
+ return;
+ }
+
+ document.body.classList.remove('printing-invoice');
+
+ if (!modal || !invoicePrintOriginalParent || modal.parentNode === invoicePrintOriginalParent) {
+ return;
+ }
+
+ invoicePrintOriginalParent.insertBefore(modal, invoicePrintOriginalNextSibling);
+ };
+
+ window.printInvoiceDocument = function() {
+ prepareInvoiceForPrint();
+ window.print();
+
+ if (invoicePrintRestoreTimer) {
+ window.clearTimeout(invoicePrintRestoreTimer);
+ }
+
+ invoicePrintRestoreTimer = window.setTimeout(() => {
+ restoreInvoiceAfterPrint();
+ }, 1200);
+ };
+
+ window.addEventListener('beforeprint', prepareInvoiceForPrint);
+ window.addEventListener('afterprint', restoreInvoiceAfterPrint);
+
window.viewAndPrintA4Invoice = function(data, autoPrint = true) {
if (!data) return;
- // Reuse view logic
- const invoiceDisplayNo = data.document_no || data.transaction_no || ((data.type === 'purchase' ? 'PUR-' : 'INV-') + data.id.toString().padStart(5, '0'));
- document.getElementById('invNumber').textContent = invoiceDisplayNo;
- document.getElementById('invDate').textContent = data.invoice_date;
- document.getElementById('invPaymentType').textContent = data.payment_type ? data.payment_type.toUpperCase() : 'CASH';
- document.getElementById('invCustomerName').textContent = data.customer_name || '---';
-
- const phoneEl = document.getElementById('invCustomerPhone');
- const phoneContainer = document.getElementById('invCustomerPhoneContainer');
- if (data.customer_phone) {
- phoneEl.textContent = data.customer_phone;
- phoneContainer.style.display = 'block';
- } else {
- phoneContainer.style.display = 'none';
+
+ const invoiceType = String(data.type || 'sale').toLowerCase() === 'purchase' ? 'purchase' : 'sale';
+ const invoiceDisplayNo = data.document_no || data.transaction_no || ((invoiceType === 'purchase' ? 'PUR-' : 'INV-') + data.id.toString().padStart(5, '0'));
+ const paymentText = humanizeInvoiceText(data.payment_type || 'cash');
+ const statusKey = String(data.status || '').toLowerCase();
+ const statusText = statusKey === 'partially_paid' ? 'Partially Paid' : humanizeInvoiceText(statusKey);
+ const typeText = invoiceType === 'purchase' ? 'Purchase Invoice' : 'Sales Invoice';
+ const documentTitle = invoiceType === 'purchase' ? 'Purchase Invoice / فاتورة شراء' : 'Tax Invoice / فاتورة ضريبية';
+ const documentSubtitle = invoiceType === 'purchase' ? 'Official purchase record / مستند شراء رسمي' : 'Official tax document / مستند ضريبي رسمي';
+ const partyLabelText = invoiceType === 'purchase' ? 'Supplier Details / بيانات المورد' : 'Bill To / بيانات العميل';
+ const partyLabelEn = invoiceType === 'purchase' ? 'Supplier Details' : 'Bill To';
+ const partyLabelAr = invoiceType === 'purchase' ? 'بيانات المورد' : 'بيانات العميل';
+ const formatQty = typeof window.formatQuantity === 'function'
+ ? window.formatQuantity
+ : (value) => {
+ const numericValue = Number.isFinite(parseFloat(value)) ? parseFloat(value) : 0;
+ return Number.isInteger(numericValue)
+ ? String(numericValue)
+ : numericValue.toFixed(3).replace(/\.?0+$/, '');
+ };
+
+ setInvoiceText('invNumber', invoiceDisplayNo);
+ setInvoiceText('invDate', data.invoice_date || '---');
+ setInvoiceText('invPaymentType', paymentText);
+ setInvoiceText('invPaymentTypeSummary', paymentText);
+ setInvoiceText('invCustomerName', data.customer_name || '---');
+ setInvoiceText('invStatusText', statusText);
+ setInvoiceText('invDocumentTitle', documentTitle);
+ setInvoiceText('invDocumentSubtitle', documentSubtitle);
+ setInvoiceText('invAmountInWords', data.total_in_words || '---');
+
+ toggleInvoiceField('invCustomerPhoneContainer', 'invCustomerPhone', data.customer_phone || '');
+ toggleInvoiceField('invCustomerTaxIdContainer', 'invCustomerTaxId', data.customer_tax_id || '');
+ toggleInvoiceField('invOutletRow', 'invOutletName', data.outlet_name || '');
+
+ const partyLabel = document.getElementById('invPartyLabel');
+ if (partyLabel) {
+ partyLabel.textContent = partyLabelText;
+ partyLabel.setAttribute('data-en', partyLabelEn);
+ partyLabel.setAttribute('data-ar', partyLabelAr);
}
- const taxIdEl = document.getElementById('invCustomerTaxId');
- const taxIdContainer = document.getElementById('invCustomerTaxIdContainer');
- if (data.customer_tax_id) {
- taxIdEl.textContent = data.customer_tax_id;
- taxIdContainer.style.display = 'block';
- } else {
- taxIdContainer.style.display = 'none';
+ const invoiceTypeLabel = document.getElementById('invoiceTypeLabel');
+ if (invoiceTypeLabel) {
+ invoiceTypeLabel.textContent = typeText;
+ invoiceTypeLabel.className = 'invoice-pill ' + (invoiceType === 'purchase' ? 'invoice-pill--purchase' : 'invoice-pill--sale');
}
- document.getElementById('invAmountInWords').textContent = data.total_in_words || '';
-
- const invOutletEl = document.getElementById('invOutletName');
- if (invOutletEl) {
- invOutletEl.textContent = data.outlet_name ? (data.outlet_name) : '';
- invOutletEl.style.display = data.outlet_name ? 'block' : 'none';
- }
-
- document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To / فاتورة إلى' : 'Bill From / فاتورة من';
- document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From');
- document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من');
- document.getElementById('invoiceTypeLabel').textContent = data.type;
- document.getElementById('invoiceTypeLabel').className = 'badge text-uppercase ' + (data.type === 'sale' ? 'bg-success' : 'bg-warning');
-
const statusLabel = document.getElementById('invoiceStatusLabel');
- let statusClass = 'bg-secondary';
- let statusEn = data.status ? (data.status.charAt(0).toUpperCase() + data.status.slice(1)) : '---';
- if (data.status === 'paid') statusClass = 'bg-success';
- else if (data.status === 'unpaid') statusClass = 'bg-danger';
- else if (data.status === 'partially_paid') {
- statusClass = 'bg-warning text-dark';
- statusEn = 'Partially Paid';
+ let statusClass = 'invoice-pill--neutral';
+ if (statusKey === 'paid') {
+ statusClass = 'invoice-pill--paid';
+ } else if (statusKey === 'unpaid') {
+ statusClass = 'invoice-pill--unpaid';
+ } else if (statusKey === 'partially_paid') {
+ statusClass = 'invoice-pill--partial';
}
-
- statusLabel.textContent = statusEn;
- statusLabel.className = 'badge text-uppercase ' + statusClass;
-
+ if (statusLabel) {
+ statusLabel.textContent = statusText;
+ statusLabel.className = 'invoice-pill ' + statusClass;
+ }
+
const body = document.getElementById('invItemsBody');
- body.innerHTML = '';
- if (data.items) {
- data.items.forEach(item => {
- const tr = document.createElement('tr');
- tr.innerHTML = `
-
${item.name_en} / ${item.name_ar}
-
${formatQuantity(item.quantity)}
-
= __('currency') ?> ${parseFloat(item.unit_price).toFixed(3)}
-
${parseFloat(item.vat_rate || 0).toFixed(2)}%
-
= __('currency') ?> ${parseFloat(item.total_price).toFixed(3)}
- `;
- body.appendChild(tr);
- });
+ if (body) {
+ body.innerHTML = '';
+ if (Array.isArray(data.items) && data.items.length > 0) {
+ data.items.forEach((item, index) => {
+ const tr = document.createElement('tr');
+ const englishName = escapeHtml(item.name_en || item.name || 'Item');
+ const arabicName = escapeHtml(item.name_ar || '');
+ const itemNameHtml = arabicName
+ ? `
${englishName}
${arabicName}
`
+ : `
${englishName}
`;
+
+ tr.innerHTML = `
+
${index + 1}
+
${itemNameHtml}
+
${formatQty(item.quantity)}
+
${formatInvoiceCurrency(item.unit_price, 3)}
+
${(Number.isFinite(parseFloat(item.vat_rate)) ? parseFloat(item.vat_rate) : 0).toFixed(2)}%
+
${formatInvoiceCurrency(item.total_price, 3)}
+ `;
+ body.appendChild(tr);
+ });
+ } else {
+ body.innerHTML = '
No invoice items / لا توجد أصناف ';
+ }
}
+
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);
@@ -75,63 +203,88 @@
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;
+ const paidAmount = Number.isFinite(parseFloat(data.paid_amount)) ? parseFloat(data.paid_amount) : 0;
+ const balance = Math.max(0, grandTotalValue - paidAmount);
- if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').innerHTML = '
= __('currency') ?> ' + subtotalExVat.toFixed(3);
- if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').innerHTML = '
= __('currency') ?> ' + vatVal.toFixed(2);
+ const subtotalEl = document.getElementById('invSubtotal');
+ if (subtotalEl) subtotalEl.innerHTML = formatInvoiceCurrency(subtotalExVat, 3);
+
+ const vatEl = document.getElementById('invVatAmount');
+ if (vatEl) vatEl.innerHTML = formatInvoiceCurrency(vatVal, 3);
const discountRow = document.getElementById('invDiscountRow');
const discountAmountEl = document.getElementById('invDiscountAmount');
if (discountRow && discountAmountEl) {
if (discountVal > 0) {
discountRow.style.display = 'flex';
- discountAmountEl.innerHTML = '
= __('currency') ?> ' + discountVal.toFixed(3);
+ discountAmountEl.innerHTML = formatInvoiceCurrency(discountVal, 3);
} else {
discountRow.style.display = 'none';
discountAmountEl.innerHTML = '';
}
}
- if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').innerHTML = '
= __('currency') ?> ' + grandTotalValue.toFixed(3);
+ const grandTotalEl = document.getElementById('invGrandTotal');
+ if (grandTotalEl) grandTotalEl.innerHTML = formatInvoiceCurrency(grandTotalValue, 3);
- if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').innerHTML = '
= __('currency') ?> ' + parseFloat(data.paid_amount || 0).toFixed(3);
- const balance = Math.max(0, grandTotalValue - parseFloat(data.paid_amount || 0));
- if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').innerHTML = '
= __('currency') ?> ' + balance.toFixed(3);
+ const paidInfoEl = document.getElementById('invPaidInfo');
+ if (paidInfoEl) paidInfoEl.innerHTML = formatInvoiceCurrency(paidAmount, 3);
+
+ const balanceInfoEl = document.getElementById('invBalanceInfo');
+ if (balanceInfoEl) balanceInfoEl.innerHTML = formatInvoiceCurrency(balance, 3);
- // Generate QR Code for Zakat, Tax and Customs Authority (ZATCA) style or simple formal
const companyName = = json_encode($data['settings']['company_name'] ?? 'Accounting System') ?>;
const vatNo = = json_encode($data['settings']['vat_number'] ?? '') ?>;
const qrData = `Seller: ${companyName}\nVAT: ${vatNo}\nInvoice: ${invoiceDisplayNo}\nDate: ${data.invoice_date}\nTotal: ${grandTotalValue.toFixed(3)}`;
- const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(qrData)}`;
- if (document.getElementById('invQrCode')) {
- document.getElementById('invQrCode').innerHTML = `
`;
- }
-
- const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal'));
- viewModal.show();
-
- if (autoPrint) {
- setTimeout(() => { window.print(); }, 1000);
+ const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=112x112&data=${encodeURIComponent(qrData)}`;
+ const qrCode = document.getElementById('invQrCode');
+ if (qrCode) {
+ qrCode.innerHTML = `
`;
}
- fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
- .then(res => res.json())
- .then(payments => {
+ const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal'));
+ viewModal.show();
+
+ const paymentsPromise = fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
+ .then((res) => res.json())
+ .then((payments) => {
const paymentsBody = document.getElementById('invPaymentsBody');
const paymentsSection = document.getElementById('invPaymentsSection');
if (paymentsBody) paymentsBody.innerHTML = '';
- if (payments && payments.length > 0) {
+
+ if (Array.isArray(payments) && payments.length > 0) {
if (paymentsBody) {
- payments.forEach(p => {
- const tr = document.createElement('tr');
- tr.innerHTML = `
${p.payment_date} ${p.payment_method} OMR ${parseFloat(p.amount).toFixed(3)} `;
- paymentsBody.appendChild(tr);
+ payments.forEach((payment) => {
+ const paymentChip = document.createElement('div');
+ paymentChip.className = 'invoice-payment-pill';
+ paymentChip.innerHTML = `
+
${escapeHtml(payment.payment_date || '')}
+
•
+
${escapeHtml(humanizeInvoiceText(payment.payment_method || ''))}
+
•
+
${formatInvoiceCurrency(payment.amount, 3)}
+ `;
+ paymentsBody.appendChild(paymentChip);
});
}
if (paymentsSection) paymentsSection.style.display = 'block';
- } else {
- if (paymentsSection) paymentsSection.style.display = 'none';
+ } else if (paymentsSection) {
+ paymentsSection.style.display = 'none';
}
- }).catch(err => console.error('Error fetching payments:', err));
+ })
+ .catch((err) => {
+ console.error('Error fetching payments:', err);
+ const paymentsSection = document.getElementById('invPaymentsSection');
+ if (paymentsSection) paymentsSection.style.display = 'none';
+ });
+
+ if (autoPrint) {
+ paymentsPromise.finally(() => {
+ setTimeout(() => {
+ window.printInvoiceDocument();
+ }, 450);
+ });
+ }
};
@media print {
- .no-print, .sidebar, .topbar, .btn, .modal-header, .modal-footer, .d-print-none,
- .modal-backdrop { display: none !important; }
- body { background: white !important; margin: 0 !important; padding: 0 !important; overflow: visible !important; }
- .main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
-
- /* Hide all modals by default */
- .modal { display: none !important; }
-
- /* Show ONLY the active modal */
- .modal.show {
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- margin: 0 !important;
- padding: 0 !important;
- overflow: visible !important;
- display: block !important;
- visibility: visible !important;
- background: white !important;
- width: 100% !important;
+ .no-print,
+ .sidebar,
+ .topbar,
+ .btn,
+ .modal-header,
+ .modal-footer,
+ .d-print-none,
+ .modal-backdrop {
+ display: none !important;
}
-
- .modal.show .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
- .modal.show .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
- .modal.show .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
-
+
+ html,
+ body {
+ background: white !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: visible !important;
+ width: auto !important;
+ min-height: auto !important;
+ height: auto !important;
+ }
+
+ body.modal-open:not(.printing-receipt) {
+ visibility: hidden !important;
+ overflow: visible !important;
+ padding-right: 0 !important;
+ }
+
+ body.printing-invoice {
+ visibility: visible !important;
+ overflow: visible !important;
+ padding-right: 0 !important;
+ }
+
+ body.printing-invoice > :not(#viewInvoiceModal):not(script):not(style) {
+ display: none !important;
+ }
+
+ .main-content {
+ margin: 0 !important;
+ padding: 0 !important;
+ background: white !important;
+ }
+
+ .modal {
+ display: none !important;
+ }
+
+ .modal.show,
+ body.printing-invoice #viewInvoiceModal.modal.show {
+ position: static !important;
+ inset: auto !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: visible !important;
+ display: block !important;
+ visibility: visible !important;
+ background: white !important;
+ width: auto !important;
+ }
+
+ .modal.show .modal-dialog {
+ max-width: 100% !important;
+ width: 100% !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ }
+
+ .modal.show .modal-content {
+ border: none !important;
+ box-shadow: none !important;
+ background: white !important;
+ }
+
+ .modal.show .modal-body {
+ padding: 0 !important;
+ margin: 0 !important;
+ background: white !important;
+ }
+
+ .modal.show * {
+ visibility: visible !important;
+ }
+
@page {
size: A4 portrait;
- margin: 5mm;
+ margin: 7mm;
}
-
+
+ .invoice-print-shell {
+ padding: 0 !important;
+ background: white !important;
+ }
+
+ .invoice-paper,
+ .invoice-paper * {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ }
+
+ .invoice-paper {
+ border: none !important;
+ border-radius: 0 !important;
+ box-shadow: none !important;
+ }
+
+ .invoice-paper-body {
+ padding: 12px 12px 8px !important;
+ }
+
+ .invoice-header {
+ padding-bottom: 12px !important;
+ }
+
+ .invoice-paper-body .g-4,
+ .invoice-paper-body .g-3 {
+ --bs-gutter-y: 0.75rem !important;
+ }
+
+ .invoice-paper-body .g-4 {
+ --bs-gutter-x: 1rem !important;
+ }
+
+ .invoice-paper-body .g-3 {
+ --bs-gutter-x: 0.85rem !important;
+ }
+
+ .invoice-paper-body .mt-4 {
+ margin-top: 0.95rem !important;
+ }
+
+ .invoice-paper-body .mt-3 {
+ margin-top: 0.65rem !important;
+ }
+
+ .invoice-paper-body .mb-3 {
+ margin-bottom: 0.65rem !important;
+ }
+
+ .invoice-paper-body .mb-2 {
+ margin-bottom: 0.4rem !important;
+ }
+
+ .invoice-top-accent {
+ height: 5px !important;
+ }
+
+ .invoice-logo {
+ max-height: 58px !important;
+ }
+
+ .invoice-title {
+ font-size: 1.1rem !important;
+ letter-spacing: 0.06em !important;
+ }
+
+ .invoice-meta-grid {
+ gap: 0.6rem !important;
+ margin-top: 0.75rem !important;
+ }
+
+ .invoice-meta-card {
+ min-height: 72px !important;
+ padding: 0.7rem 0.85rem !important;
+ }
+
+ .invoice-meta-value {
+ font-size: 0.92rem !important;
+ }
+
+ .invoice-party-meta {
+ gap: 0.2rem 0.75rem !important;
+ margin-top: 0.25rem !important;
+ }
+
+ .invoice-inline-list {
+ gap: 0.3rem 0.7rem !important;
+ }
+
+ .invoice-inline-kv {
+ gap: 0.12rem !important;
+ min-width: 92px !important;
+ }
+
+ .invoice-inline-kv-label {
+ font-size: 0.54rem !important;
+ letter-spacing: 0.08em !important;
+ }
+
+ .invoice-inline-kv strong {
+ font-size: 0.8rem !important;
+ line-height: 1.2 !important;
+ }
+
+ .invoice-note-card--compact {
+ gap: 0.55rem !important;
+ }
+
+ .invoice-note-row {
+ gap: 0.28rem !important;
+ }
+
+ .invoice-note-value {
+ font-size: 0.78rem !important;
+ line-height: 1.35 !important;
+ }
+
+ .invoice-note-text {
+ font-size: 0.72rem !important;
+ line-height: 1.38 !important;
+ }
+
+ .invoice-summary-grid {
+ gap: 0.4rem !important;
+ grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
+ }
+
+ .invoice-summary-metric {
+ padding: 0.5rem 0.55rem !important;
+ border-radius: 12px !important;
+ gap: 0.22rem !important;
+ }
+
+ .invoice-summary-metric-label {
+ font-size: 0.54rem !important;
+ letter-spacing: 0.05em !important;
+ }
+
+ .invoice-summary-metric-value {
+ font-size: 0.84rem !important;
+ }
+
+ .invoice-summary-metric--total .invoice-summary-metric-value {
+ font-size: 1rem !important;
+ }
+
+ .invoice-payment-compact {
+ margin-top: 0.65rem !important;
+ padding-top: 0.55rem !important;
+ }
+
+ .invoice-payment-list {
+ gap: 0.32rem !important;
+ }
+
+ .invoice-payment-pill {
+ padding: 0.28rem 0.5rem !important;
+ font-size: 0.68rem !important;
+ }
+
+ .invoice-payment-pill strong {
+ font-size: 0.72rem !important;
+ }
+
+ .invoice-section-card,
+ .invoice-note-card,
+ .invoice-summary-card,
+ .invoice-payment-card {
+ padding: 0.8rem 0.9rem !important;
+ border-radius: 14px !important;
+ }
+
+ .invoice-table-wrap,
+ .invoice-section-card,
+ .invoice-note-card,
+ .invoice-summary-card,
+ .invoice-payment-card,
+ .invoice-meta-card,
+ .invoice-qr-card {
+ box-shadow: none !important;
+ }
+
.invoice-printable-container {
- padding: 10px !important;
+ padding: 0 !important;
}
-
- .mt-4 { margin-top: 1rem !important; }
- .mt-5 { margin-top: 1.5rem !important; }
- .mb-4 { margin-bottom: 1rem !important; }
- .p-5 { padding: 1.5rem !important; }
-
- .table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; }
- .bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
- .text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; }
- .badge { border: 1px solid #000; color: #000 !important; }
-
- /* Ensure the modal is the only thing visible ONLY when a modal is open */
- body.modal-open:not(.printing-receipt) { visibility: hidden !important; }
- body.modal-open:not(.printing-receipt) .modal.show {
- visibility: visible !important;
- display: block !important;
- position: absolute !important;
- left: 0 !important;
- top: 0 !important;
- width: 100% !important;
+
+ .table-formal thead th {
+ background: #0f172a !important;
+ color: #ffffff !important;
+ border-color: #0f172a !important;
}
- body.modal-open:not(.printing-receipt) .modal.show * { visibility: visible !important; }
-
- /* Old rules that caused blank pages for nested modals */
- /* body.modal-open:not(.printing-receipt) > *:not(.modal):not(.swal2-container) { display: none !important; } */
- /* body.modal-open:not(.printing-receipt) .main-content { display: none !important; } */
-
- /* POS Receipt printing specific */
- body.printing-receipt .modal { display: none !important; }
- body.printing-receipt .modal-backdrop { display: none !important; }
- body.printing-receipt #posPrintArea {
- display: flex !important;
+
+ .table-formal tbody tr:nth-child(even) {
+ background: #f8fafc !important;
+ }
+
+ .table-formal thead th {
+ padding: 0.6rem 0.55rem !important;
+ font-size: 0.65rem !important;
+ }
+
+ .table-formal tbody td {
+ padding: 0.55rem 0.55rem !important;
+ font-size: 0.82rem !important;
+ }
+
+ .invoice-summary-row {
+ padding: 0.35rem 0 !important;
+ font-size: 0.84rem !important;
+ }
+
+ .invoice-summary-row--total {
+ padding-top: 0.7rem !important;
+ font-size: 0.94rem !important;
+ }
+
+ .invoice-summary-row--total span:last-child {
+ font-size: 1.18rem !important;
+ }
+
+ .invoice-footer {
+ margin-top: 0.95rem !important;
+ padding-top: 0.8rem !important;
+ }
+
+ .invoice-signature-line {
+ width: 150px !important;
+ padding-top: 0.4rem !important;
+ }
+
+ .invoice-qr-card {
+ min-width: 112px !important;
+ min-height: 112px !important;
+ padding: 0.55rem !important;
+ border-radius: 14px !important;
+ }
+
+ .invoice-qr-card img {
+ width: 88px !important;
+ height: 88px !important;
+ }
+
+ .invoice-footer-note {
+ font-size: 0.72rem !important;
+ }
+
+ .invoice-pill,
+ .invoice-meta-card,
+ .invoice-summary-card,
+ .invoice-payment-card,
+ .invoice-note-card,
+ .invoice-section-card {
+ background-clip: padding-box !important;
+ }
+
+ .table-formal tr,
+ .invoice-section-card,
+ .invoice-summary-card,
+ .invoice-note-card,
+ .invoice-payment-card,
+ .invoice-footer {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ body.printing-receipt .modal,
+ body.printing-receipt .modal-backdrop {
+ display: none !important;
+ }
+
+ body.printing-receipt #posPrintArea {
+ display: flex !important;
visibility: visible !important;
justify-content: center !important;
align-items: flex-start !important;
@@ -77,9 +373,11 @@
z-index: 9999 !important;
background: white !important;
}
+
body.printing-receipt #posPrintArea * {
visibility: visible !important;
}
+
body.printing-receipt #posPrintArea .thermal-receipt-print {
position: static !important;
left: auto !important;
@@ -87,163 +385,769 @@
margin: 0 auto !important;
}
}
- .invoice-logo { max-height: 80px; width: auto; }
- .invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
- .invoice-title { font-size: 2.5rem; color: #333; letter-spacing: 2px; }
- .invoice-info-card { background: #f8f9fa; border-radius: 8px; padding: 15px; height: 100%; }
- .table-formal thead th { background: #333; color: #fff; border: none; text-transform: uppercase; font-size: 0.85rem; }
+
+ #viewInvoiceModal .modal-dialog.invoice-print-dialog {
+ max-width: 980px;
+ }
+
+ #viewInvoiceModal .modal-content {
+ background: transparent;
+ border: none;
+ }
+
+ .invoice-print-shell {
+ padding: 1.5rem;
+ background: linear-gradient(135deg, #eef4ff 0%, #f8fafc 48%, #ffffff 100%);
+ }
+
+ .invoice-paper {
+ background: #ffffff;
+ border: 1px solid #dbe5f0;
+ border-radius: 26px;
+ overflow: hidden;
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
+ }
+
+ .invoice-top-accent {
+ height: 8px;
+ background: linear-gradient(90deg, #0f172a 0%, #1d4ed8 55%, #0ea5e9 100%);
+ }
+
+ .invoice-paper-body {
+ padding: 2rem;
+ color: #0f172a;
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ }
+
+ .invoice-header {
+ border-bottom: 1px solid #dbe5f0;
+ padding-bottom: 1.5rem;
+ }
+
+ .invoice-logo {
+ max-height: 72px;
+ width: auto;
+ }
+
+ .invoice-brand-name {
+ font-size: 1.45rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: #0f172a;
+ line-height: 1.2;
+ }
+
+ .invoice-contact-line {
+ margin: 0;
+ font-size: 0.88rem;
+ line-height: 1.6;
+ color: #475569;
+ }
+
+ .invoice-outlet {
+ margin-top: 0.6rem;
+ font-size: 0.76rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: #1d4ed8;
+ }
+
+ .invoice-eyebrow {
+ margin: 0;
+ font-size: 0.68rem;
+ letter-spacing: 0.26em;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: #64748b;
+ }
+
+ .invoice-title {
+ font-size: 1.38rem;
+ line-height: 1.25;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #0f172a;
+ }
+
+ .invoice-pill-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: flex-start;
+ }
+
+ .invoice-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.42rem 0.85rem;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .invoice-pill--sale {
+ background: #e0f2fe;
+ color: #0c4a6e;
+ border-color: #bae6fd;
+ }
+
+ .invoice-pill--purchase {
+ background: #fef3c7;
+ color: #92400e;
+ border-color: #fde68a;
+ }
+
+ .invoice-pill--paid {
+ background: #dcfce7;
+ color: #166534;
+ border-color: #bbf7d0;
+ }
+
+ .invoice-pill--unpaid {
+ background: #fee2e2;
+ color: #991b1b;
+ border-color: #fecaca;
+ }
+
+ .invoice-pill--partial {
+ background: #fef3c7;
+ color: #9a3412;
+ border-color: #fcd34d;
+ }
+
+ .invoice-pill--neutral {
+ background: #e2e8f0;
+ color: #334155;
+ border-color: #cbd5e1;
+ }
+
+ .invoice-meta-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+ margin-top: 1rem;
+ }
+
+ .invoice-meta-card {
+ min-height: 84px;
+ padding: 0.9rem 1rem;
+ border: 1px solid #dbe5f0;
+ border-radius: 16px;
+ background: #f8fafc;
+ }
+
+ .invoice-meta-label {
+ display: block;
+ margin-bottom: 0.35rem;
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: #64748b;
+ }
+
+ .invoice-meta-value {
+ display: block;
+ font-size: 1rem;
+ font-weight: 700;
+ color: #0f172a;
+ word-break: break-word;
+ }
+
+ .invoice-party-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 1rem;
+ margin-top: 0.3rem;
+ }
+
+ .invoice-inline-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem 1rem;
+ align-items: flex-start;
+ }
+
+ .invoice-inline-kv {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ min-width: 118px;
+ }
+
+ .invoice-inline-kv-label {
+ font-size: 0.64rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: #64748b;
+ line-height: 1.3;
+ }
+
+ .invoice-inline-kv strong {
+ font-size: 0.94rem;
+ font-weight: 700;
+ color: #0f172a;
+ line-height: 1.3;
+ word-break: break-word;
+ }
+
+ .invoice-section-card,
+ .invoice-note-card,
+ .invoice-summary-card,
+ .invoice-payment-card {
+ border: 1px solid #dbe5f0;
+ border-radius: 18px;
+ padding: 1rem 1.15rem;
+ }
+
+ .invoice-section-card {
+ background: #ffffff;
+ height: 100%;
+ }
+
+ .invoice-note-card,
+ .invoice-payment-card {
+ background: #f8fafc;
+ }
+
+ .invoice-summary-card {
+ background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
+ }
+
+ .invoice-section-title {
+ margin: 0;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #64748b;
+ }
+
+ .invoice-party-name {
+ font-size: 1.12rem;
+ font-weight: 700;
+ color: #0f172a;
+ margin: 0.15rem 0 0.4rem;
+ }
+
+ .invoice-detail-text {
+ margin: 0;
+ font-size: 0.9rem;
+ line-height: 1.55;
+ color: #475569;
+ }
+
+ .invoice-detail-text strong {
+ color: #0f172a;
+ }
+
+ .invoice-table-wrap {
+ margin-top: 1.25rem;
+ border: 1px solid #dbe5f0;
+ border-radius: 18px;
+ overflow: hidden;
+ }
+
+ .table-formal {
+ margin-bottom: 0;
+ }
+
+ .table-formal thead th {
+ background: #0f172a;
+ color: #ffffff;
+ border-color: #0f172a;
+ padding: 0.88rem 0.75rem;
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ vertical-align: middle;
+ }
+
+ .table-formal tbody td {
+ padding: 0.85rem 0.75rem;
+ border-color: #e2e8f0;
+ vertical-align: top;
+ font-size: 0.9rem;
+ color: #1e293b;
+ }
+
+ .table-formal tbody tr:nth-child(even) {
+ background: #f8fafc;
+ }
+
+ .invoice-line-no {
+ width: 56px;
+ font-weight: 700;
+ color: #64748b;
+ }
+
+ .invoice-item-name {
+ font-weight: 700;
+ color: #0f172a;
+ }
+
+ .invoice-item-secondary {
+ margin-top: 0.2rem;
+ font-size: 0.78rem;
+ color: #64748b;
+ }
+
+ .invoice-amount {
+ white-space: nowrap;
+ font-weight: 700;
+ color: #0f172a;
+ }
+
+ .invoice-note-card--compact {
+ display: flex;
+ flex-direction: column;
+ gap: 0.85rem;
+ }
+
+ .invoice-note-row {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ }
+
+ .invoice-note-divider {
+ height: 1px;
+ background: #dbe5f0;
+ }
+
+ .invoice-note-value {
+ margin: 0;
+ font-size: 0.88rem;
+ line-height: 1.5;
+ color: #0f172a;
+ }
+
+ .invoice-note-text {
+ margin: 0;
+ font-size: 0.82rem;
+ line-height: 1.55;
+ color: #64748b;
+ }
+
+ .invoice-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.75rem;
+ }
+
+ .invoice-summary-metric {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ padding: 0.78rem 0.9rem;
+ border: 1px solid #dbe5f0;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.94);
+ min-width: 0;
+ }
+
+ .invoice-summary-metric-label {
+ font-size: 0.66rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: #64748b;
+ line-height: 1.35;
+ }
+
+ .invoice-summary-metric-value {
+ font-size: 1rem;
+ font-weight: 700;
+ color: #0f172a;
+ line-height: 1.25;
+ word-break: break-word;
+ }
+
+ .invoice-summary-metric--accent {
+ border-color: #fecaca;
+ background: #fff5f5;
+ }
+
+ .invoice-summary-metric--accent .invoice-summary-metric-value {
+ color: #b91c1c;
+ }
+
+ .invoice-summary-metric--total {
+ border-color: #bfdbfe;
+ background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
+ }
+
+ .invoice-summary-metric--total .invoice-summary-metric-value {
+ font-size: 1.22rem;
+ font-weight: 800;
+ color: #1d4ed8;
+ }
+
+ .invoice-payment-compact {
+ margin-top: 1rem;
+ padding-top: 0.95rem;
+ border-top: 1px solid #dbe5f0;
+ }
+
+ .invoice-payment-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.55rem;
+ }
+
+ .invoice-payment-pill {
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ padding: 0.45rem 0.72rem;
+ border-radius: 999px;
+ background: #e2e8f0;
+ color: #334155;
+ font-size: 0.76rem;
+ font-weight: 600;
+ line-height: 1.35;
+ }
+
+ .invoice-payment-pill strong {
+ font-size: 0.82rem;
+ font-weight: 700;
+ color: #0f172a;
+ }
+
+ .invoice-payment-sep {
+ color: #94a3b8;
+ }
+
+ .invoice-summary-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.45rem 0;
+ font-size: 0.92rem;
+ color: #334155;
+ }
+
+ .invoice-summary-row span:last-child {
+ font-weight: 700;
+ color: #0f172a;
+ }
+
+ .invoice-summary-row--accent span:last-child {
+ color: #b91c1c;
+ }
+
+ .invoice-summary-row--total {
+ margin-top: 0.25rem;
+ padding-top: 0.85rem;
+ border-top: 1px solid #cbd5e1;
+ font-size: 1.02rem;
+ font-weight: 700;
+ }
+
+ .invoice-summary-row--total span:last-child {
+ font-size: 1.35rem;
+ font-weight: 800;
+ color: #1d4ed8;
+ }
+
+ .invoice-payment-table {
+ margin-top: 0.75rem;
+ }
+
+ .invoice-payment-table thead th {
+ background: #e2e8f0;
+ color: #334155;
+ border-color: #dbe5f0;
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ }
+
+ .invoice-payment-table tbody td {
+ font-size: 0.82rem;
+ color: #475569;
+ border-color: #e2e8f0;
+ }
+
+ .invoice-terms-list {
+ margin: 0;
+ padding-left: 1.15rem;
+ color: #64748b;
+ font-size: 0.84rem;
+ }
+
+ .invoice-terms-list li + li {
+ margin-top: 0.4rem;
+ }
+
+ .invoice-footer {
+ margin-top: 1.5rem;
+ padding-top: 1.25rem;
+ border-top: 1px solid #dbe5f0;
+ }
+
+ .invoice-signature-line {
+ width: 180px;
+ margin: 0 auto;
+ padding-top: 0.55rem;
+ border-top: 1px solid #94a3b8;
+ }
+
+ .invoice-qr-card {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.45rem;
+ min-width: 132px;
+ min-height: 132px;
+ padding: 0.75rem;
+ border: 1px solid #dbe5f0;
+ border-radius: 18px;
+ background: #ffffff;
+ }
+
+ .invoice-footer-note {
+ font-size: 0.78rem;
+ color: #64748b;
+ }
+
+ .invoice-currency {
+ display: inline-block;
+ margin-inline-end: 0.22rem;
+ font-size: 0.72em;
+ font-weight: 600;
+ color: #64748b;
+ }
+
+ .x-small {
+ font-size: 0.72rem;
+ }
+
+ @media (min-width: 768px) {
+ .invoice-pill-group {
+ justify-content: flex-end;
+ }
+ }
+
+ @media (min-width: 992px) {
+ .invoice-summary-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+ }
+
+ @media (max-width: 767.98px) {
+ .invoice-print-shell {
+ padding: 0.85rem;
+ }
+
+ .invoice-paper-body {
+ padding: 1.25rem;
+ }
+
+ .invoice-title {
+ font-size: 1.18rem;
+ }
+
+ .invoice-meta-grid {
+ grid-template-columns: 1fr;
+ }
+ }
-
-
-
+
+
+
-
-
-
-
-
-
-
Bill To / فاتورة إلى
-
-
VAT / الضريبة:
-
Phone / الهاتف:
-
-
-
-
-
Payment Details / تفاصيل الدفع
-
Method / الطريقة:
-
Currency / العملة: OMR / ريال عماني
-
-
-
-
-
-
-
-
-
-
Amount in Words / المبلغ بالحروف
-
-
-
-
Terms & Conditions / الشروط والأحكام
-
- Goods once sold will not be taken back or exchanged.
- Payment is due within the agreed credit period.
-
-
-
-
-
- Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)
-
-
-
- VAT Amount / مبلغ الضريبة
-
-
-
- Discount / الخصم
-
-
-
- Grand Total / المجموع الكلي
-
-
-
-
-
Payment Tracking / تتبع الدفع
-
-
-
- Date / التاريخ
- Method / الطريقة
- Amount / المبلغ
-
-
-
-
-
-
-
Paid Amount / المبلغ المدفوع
-
+
+
-
-
-
-
-
Customer Signature / توقيع العميل
+
+
+
+
+
Bill To / بيانات العميل
+
---
+
+
+
+
+
+
Invoice Details / تفاصيل الفاتورة
+
+
+ Invoice No / رقم الفاتورة
+
+
+
+ Issue Date / تاريخ الإصدار
+
+
+
+ Payment Method / طريقة الدفع
+
+
+
+
+
-
-
-
-
-
Authorized Signatory / التوقيع المعتمد
+
+
+
+
+
+
+
+
+
+
+
Amount in Words / المبلغ كتابة
+
+
+
+
+
Terms & Conditions / الشروط والأحكام
+
Goods once sold will not be taken back or exchanged. Payment is due within the agreed credit period. Please verify invoice details upon receipt.
+
+
+
+
+
+
Financial Summary / الملخص المالي
+
+
+ Subtotal (Excl. VAT) / المجموع قبل الضريبة
+
+
+
+ VAT Amount / مبلغ الضريبة
+
+
+
+ Discount / الخصم
+
+
+
+ Grand Total / الإجمالي النهائي
+
+
+
+ Paid Amount / المدفوع
+
+
+
+ Balance Due / المتبقي
+
+
+
+
+
+
Payment Tracking / تتبع الدفع
+
+
+
+
+
+
+
+
-
Generated by = htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at = $_SERVER['HTTP_HOST'] ?>
-
+
diff --git a/pages/sales_purchases_save_logic.php b/pages/sales_purchases_save_logic.php
index efc523b..6b84d07 100644
--- a/pages/sales_purchases_save_logic.php
+++ b/pages/sales_purchases_save_logic.php
@@ -28,6 +28,7 @@
$total_subtotal = 0;
$total_vat = 0;
+ $profitLines = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
@@ -42,6 +43,11 @@
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
+ $profitLines[] = [
+ 'item_id' => (int)$item_id,
+ 'qty' => $qty,
+ 'unit_price' => $price,
+ ];
}
$gross_total = $total_subtotal + $total_vat;
@@ -50,6 +56,11 @@
$discount_amount = 0.0;
if ($type === 'sale' && $hasDiscountColumn && $manualDiscountEnabled) {
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
+ $discountMetrics = calculateManualDiscountProfitMetrics($profitLines, false);
+ $maxManualDiscount = min(max(0, $gross_total), max(0, (float)($discountMetrics['max_discount'] ?? 0)));
+ if ($discount_amount > ($maxManualDiscount + 0.0005)) {
+ throw new Exception(manualDiscountLimitMessage($discountMetrics, $discount_amount));
+ }
if ($discount_amount > $gross_total) {
$discount_amount = $gross_total;
}
@@ -143,6 +154,7 @@
$total_subtotal = 0;
$total_vat = 0;
+ $profitLines = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
@@ -157,6 +169,11 @@
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
+ $profitLines[] = [
+ 'item_id' => (int)$item_id,
+ 'qty' => $qty,
+ 'unit_price' => $price,
+ ];
}
$gross_total = $total_subtotal + $total_vat;
@@ -166,6 +183,11 @@
if ($hasDiscountColumn) {
if ($manualDiscountEnabled && array_key_exists('discount_amount', $_POST)) {
$discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0));
+ $discountMetrics = calculateManualDiscountProfitMetrics($profitLines, false);
+ $maxManualDiscount = min(max(0, $gross_total), max(0, (float)($discountMetrics['max_discount'] ?? 0)));
+ if ($discount_amount > ($maxManualDiscount + 0.0005)) {
+ throw new Exception(manualDiscountLimitMessage($discountMetrics, $discount_amount));
+ }
} else {
$existingDiscountStmt = $db->prepare("SELECT discount_amount FROM $table WHERE id = ? LIMIT 1");
$existingDiscountStmt->execute([$id]);
diff --git a/pages/settings_save_logic.php b/pages/settings_save_logic.php
index c6aa8dc..1cc41e6 100644
--- a/pages/settings_save_logic.php
+++ b/pages/settings_save_logic.php
@@ -106,6 +106,15 @@ if (isset($_POST['update_settings'])) {
$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';
+ $manualDiscountProfitLimitPercent = $settings['manual_discount_profit_limit_percent'] ?? '5';
+ $manualDiscountProfitLimitPercent = is_numeric($manualDiscountProfitLimitPercent) ? (float)$manualDiscountProfitLimitPercent : 5.0;
+ if ($manualDiscountProfitLimitPercent < 0) {
+ $manualDiscountProfitLimitPercent = 0.0;
+ }
+ if ($manualDiscountProfitLimitPercent > 100) {
+ $manualDiscountProfitLimitPercent = 100.0;
+ }
+ $settings['manual_discount_profit_limit_percent'] = number_format($manualDiscountProfitLimitPercent, 3, '.', '');
$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';
diff --git a/pages/settings_view.php b/pages/settings_view.php
index 944067e..f61e341 100644
--- a/pages/settings_view.php
+++ b/pages/settings_view.php
@@ -26,7 +26,56 @@ $timezoneIdentifiers = DateTimeZone::listIdentifiers();
$smtpConfigured = !empty($data['settings']['smtp_host']) && !empty($data['settings']['smtp_user']);
$wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data['settings']['wablas_token']) && !empty($data['settings']['wablas_security_key']);
?>
-
+
+
@@ -72,7 +121,7 @@ $wablasConfigured = !empty($data['settings']['wablas_api_url']) && !empty($data[
$tabMeta): ?>
Shows a fixed discount amount field on POS and Sales invoices.
+
+
Manual Discount Limit (% of Invoice Profit)
+
+
+ %
+
+
Example: 5 means users can apply manual discount up to 5% of the invoice profit margin. Uses each item's cost price. 0 blocks manual discounts; 100 allows the full profit margin.
+
Scale Barcode Mode