366 lines
18 KiB
PHP
366 lines
18 KiB
PHP
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 `<span class="invoice-currency"><?= __('currency') ?></span>${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 = '';
|
|
} 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;
|
|
|
|
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 invoiceTypeLabel = document.getElementById('invoiceTypeLabel');
|
|
if (invoiceTypeLabel) {
|
|
invoiceTypeLabel.textContent = typeText;
|
|
invoiceTypeLabel.className = 'invoice-pill ' + (invoiceType === 'purchase' ? 'invoice-pill--purchase' : 'invoice-pill--sale');
|
|
}
|
|
|
|
const statusLabel = document.getElementById('invoiceStatusLabel');
|
|
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';
|
|
}
|
|
if (statusLabel) {
|
|
statusLabel.textContent = statusText;
|
|
statusLabel.className = 'invoice-pill ' + statusClass;
|
|
}
|
|
|
|
const body = document.getElementById('invItemsBody');
|
|
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 vatRateText = `${(Number.isFinite(parseFloat(item.vat_rate)) ? parseFloat(item.vat_rate) : 0).toFixed(2)}%`;
|
|
const itemDetails = [];
|
|
if (arabicName) {
|
|
itemDetails.push(arabicName);
|
|
}
|
|
itemDetails.push(`VAT ${vatRateText}`);
|
|
const itemNameHtml = `
|
|
<div class="invoice-item-name">${englishName}</div>
|
|
<div class="invoice-item-secondary">${itemDetails.join(' • ')}</div>
|
|
`;
|
|
|
|
tr.innerHTML = `
|
|
<td class="text-center invoice-line-no">${index + 1}</td>
|
|
<td>${itemNameHtml}</td>
|
|
<td class="text-center">${formatQty(item.quantity)}</td>
|
|
<td class="text-end invoice-amount">${formatInvoiceCurrency(item.unit_price, 3)}</td>
|
|
<td class="text-end invoice-amount">${formatInvoiceCurrency(item.total_price, 3)}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
} else {
|
|
body.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-4">No invoice items / لا توجد أصناف</td></tr>';
|
|
}
|
|
}
|
|
|
|
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;
|
|
const paidAmount = Number.isFinite(parseFloat(data.paid_amount)) ? parseFloat(data.paid_amount) : 0;
|
|
const balance = Math.max(0, grandTotalValue - paidAmount);
|
|
|
|
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 = formatInvoiceCurrency(discountVal, 3);
|
|
} else {
|
|
discountRow.style.display = 'none';
|
|
discountAmountEl.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
const grandTotalEl = document.getElementById('invGrandTotal');
|
|
if (grandTotalEl) grandTotalEl.innerHTML = formatInvoiceCurrency(grandTotalValue, 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);
|
|
|
|
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=112x112&data=${encodeURIComponent(qrData)}`;
|
|
const qrCode = document.getElementById('invQrCode');
|
|
if (qrCode) {
|
|
qrCode.innerHTML = `<img src="${qrUrl}" alt="QR Code" width="112" height="112" class="bg-white rounded" style="width: 112px; height: 112px;">`;
|
|
}
|
|
|
|
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 (Array.isArray(payments) && payments.length > 0) {
|
|
if (paymentsBody) {
|
|
payments.slice(0, 3).forEach((payment) => {
|
|
const paymentChip = document.createElement('div');
|
|
paymentChip.className = 'invoice-payment-pill';
|
|
paymentChip.innerHTML = `
|
|
<span>${escapeHtml(payment.payment_date || '')}</span>
|
|
<span class="invoice-payment-sep">•</span>
|
|
<span>${escapeHtml(humanizeInvoiceText(payment.payment_method || ''))}</span>
|
|
<span class="invoice-payment-sep">•</span>
|
|
<strong>${formatInvoiceCurrency(payment.amount, 3)}</strong>
|
|
`;
|
|
paymentsBody.appendChild(paymentChip);
|
|
});
|
|
|
|
if (payments.length > 3) {
|
|
const moreChip = document.createElement('div');
|
|
moreChip.className = 'invoice-payment-pill invoice-payment-pill--more';
|
|
moreChip.textContent = `+${payments.length - 3} more payments`;
|
|
paymentsBody.appendChild(moreChip);
|
|
}
|
|
}
|
|
if (paymentsSection) paymentsSection.style.display = 'block';
|
|
} else if (paymentsSection) {
|
|
paymentsSection.style.display = 'none';
|
|
}
|
|
})
|
|
.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);
|
|
});
|
|
}
|
|
};
|
|
|
|
<?php
|
|
$autoInvoicePayload = null;
|
|
if (
|
|
isset($_SESSION['trigger_invoice_modal'], $_SESSION['show_invoice_id'], $_SESSION['show_invoice_page']) &&
|
|
in_array($page, ['sales', 'purchases'], true) &&
|
|
$_SESSION['show_invoice_page'] === $page
|
|
) {
|
|
$autoInvoiceId = (int)$_SESSION['show_invoice_id'];
|
|
$autoInvoiceType = $page === 'purchases' ? 'purchase' : 'sale';
|
|
unset($_SESSION['trigger_invoice_modal'], $_SESSION['show_invoice_id'], $_SESSION['show_invoice_page']);
|
|
|
|
try {
|
|
$autoTable = $autoInvoiceType === 'purchase' ? 'purchases' : 'invoices';
|
|
$autoPartyTable = $autoInvoiceType === 'purchase' ? 'suppliers' : 'customers';
|
|
$autoPartyCol = $autoInvoiceType === 'purchase' ? 'supplier_id' : 'customer_id';
|
|
$autoTaxColumn = entity_tax_column($autoPartyTable);
|
|
$autoTaxSelect = $autoTaxColumn !== null ? "c.$autoTaxColumn AS customer_tax_id" : "'' AS customer_tax_id";
|
|
$autoOutletSelect = "'' AS outlet_name";
|
|
$autoOutletJoin = '';
|
|
|
|
if (db_column_exists($autoTable, 'outlet_id') && db_table_exists('outlets')) {
|
|
$autoOutletSelect = 'o.name AS outlet_name';
|
|
$autoOutletJoin = 'LEFT JOIN outlets o ON doc.outlet_id = o.id';
|
|
}
|
|
|
|
$autoStmt = db()->prepare("SELECT doc.*, c.name AS customer_name, c.phone AS customer_phone, $autoTaxSelect, $autoOutletSelect
|
|
FROM $autoTable doc
|
|
LEFT JOIN $autoPartyTable c ON doc.$autoPartyCol = c.id
|
|
$autoOutletJoin
|
|
WHERE doc.id = ?
|
|
LIMIT 1");
|
|
$autoStmt->execute([$autoInvoiceId]);
|
|
$autoInvoicePayload = $autoStmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($autoInvoicePayload) {
|
|
$autoItemSql = $autoInvoiceType === 'purchase'
|
|
? "SELECT pi.*, i.name_en, i.name_ar, i.vat_rate FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?"
|
|
: "SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?";
|
|
$autoItemsStmt = db()->prepare($autoItemSql);
|
|
$autoItemsStmt->execute([$autoInvoiceId]);
|
|
$autoInvoicePayload['items'] = $autoItemsStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$autoInvoicePayload['type'] = $autoInvoiceType;
|
|
$autoInvoicePayload['total_with_vat'] = (float)($autoInvoicePayload['total_with_vat'] ?? (($autoInvoicePayload['total_amount'] ?? 0) + ($autoInvoicePayload['vat_amount'] ?? 0)));
|
|
$autoInvoicePayload['paid_amount'] = (float)($autoInvoicePayload['paid_amount'] ?? 0);
|
|
$autoInvoicePayload['total_in_words'] = numberToWordsOMR($autoInvoicePayload['total_with_vat']);
|
|
$autoTransactionNo = trim((string)($autoInvoicePayload['transaction_no'] ?? ''));
|
|
$autoPrefix = $autoInvoiceType === 'purchase' ? 'PUR' : 'INV';
|
|
$autoInvoicePayload['document_no'] = ($autoInvoiceType === 'sale' && $autoTransactionNo !== '')
|
|
? $autoTransactionNo
|
|
: $autoPrefix . '-' . str_pad((string)$autoInvoicePayload['id'], 5, '0', STR_PAD_LEFT);
|
|
}
|
|
} catch (Throwable $e) {
|
|
$autoInvoicePayload = null;
|
|
}
|
|
}
|
|
?>
|
|
<?php if (!empty($autoInvoicePayload)): ?>
|
|
setTimeout(() => {
|
|
if (window.viewAndPrintA4Invoice) {
|
|
window.viewAndPrintA4Invoice(<?= json_encode($autoInvoicePayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>, true);
|
|
}
|
|
}, 300);
|
|
<?php endif; ?>
|