421 lines
22 KiB
PHP
421 lines
22 KiB
PHP
// LPO Form Logic
|
|
initInvoiceForm('lpoProductSearchInput', 'lpoSearchSuggestions', 'lpoItemsTableBody', 'lpo_grand_display', 'lpo_subtotal_display', 'lpo_vat_display');
|
|
initInvoiceForm('editLpoProductSearchInput', 'editLpoSearchSuggestions', 'editLpoItemsTableBody', 'edit_lpo_grand_display', 'edit_lpo_subtotal_display', 'edit_lpo_vat_display');
|
|
|
|
|
|
const parseLpoQuotationButtonPayload = (btn) => {
|
|
if (!btn || !btn.dataset || !btn.dataset.json) return {};
|
|
try {
|
|
return JSON.parse(btn.dataset.json);
|
|
} catch (error) {
|
|
console.warn('Failed to parse LPO/Quotation payload from button data.', error);
|
|
return {};
|
|
}
|
|
};
|
|
|
|
const renderExistingDocumentItems = (items, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
|
const tableBody = document.getElementById(tableBodyId);
|
|
const grandTotalEl = document.getElementById(grandTotalId);
|
|
const subtotalEl = document.getElementById(subtotalId);
|
|
const totalVatEl = document.getElementById(totalVatId);
|
|
|
|
if (!tableBody) return;
|
|
tableBody.innerHTML = '';
|
|
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
if (typeof recalculate === 'function') {
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
items.forEach(item => {
|
|
addItemToTable({
|
|
id: item.item_id || item.id,
|
|
name_en: item.name_en || item.item_name_en || 'Item',
|
|
name_ar: item.name_ar || item.item_name_ar || '',
|
|
sku: item.sku || '',
|
|
vat_rate: item.vat_rate || 0,
|
|
stock_quantity: item.stock_quantity || 0
|
|
}, tableBody, null, null, grandTotalEl, subtotalEl, totalVatEl, {
|
|
quantity: item.quantity,
|
|
unit_price: item.unit_price
|
|
});
|
|
});
|
|
};
|
|
|
|
document.querySelectorAll('.edit-lpo-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = parseLpoQuotationButtonPayload(this);
|
|
if (Object.keys(data).length === 0) return;
|
|
|
|
const supplierSelect = document.getElementById('edit_lpo_supplier_id');
|
|
const supplierId = data.supplier_id ?? '';
|
|
const supplierLabel = data.supplier_name || '';
|
|
|
|
invoiceEnsureSelectOption('edit_lpo_supplier_id', supplierId, supplierLabel);
|
|
|
|
const lpoIdInput = document.getElementById('edit_lpo_id');
|
|
const lpoDateInput = document.getElementById('edit_lpo_date');
|
|
const deliveryDateInput = document.getElementById('edit_lpo_delivery_date');
|
|
const statusSelect = document.getElementById('edit_lpo_status');
|
|
const termsInput = document.getElementById('edit_lpo_terms');
|
|
|
|
if (lpoIdInput) lpoIdInput.value = data.id || '';
|
|
if (supplierSelect) {
|
|
invoiceSetBlankSelectOptionLabel(supplierSelect, (supplierId === '' && supplierLabel) ? supplierLabel : '---');
|
|
supplierSelect.value = supplierId;
|
|
invoiceSyncSelect2Value(supplierSelect);
|
|
}
|
|
if (lpoDateInput) lpoDateInput.value = data.lpo_date || '';
|
|
if (deliveryDateInput) deliveryDateInput.value = data.delivery_date || '';
|
|
if (statusSelect) statusSelect.value = data.status || 'pending';
|
|
if (termsInput) termsInput.value = data.terms_conditions || '';
|
|
|
|
renderExistingDocumentItems(data.items || [], 'editLpoItemsTableBody', 'edit_lpo_grand_display', 'edit_lpo_subtotal_display', 'edit_lpo_vat_display');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.view-lpo-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = JSON.parse(this.dataset.json);
|
|
window.viewAndPrintLPO(data);
|
|
});
|
|
});
|
|
|
|
window.viewAndPrintLPO = function(data) {
|
|
const modal = new bootstrap.Modal(document.getElementById('viewLpoModal'));
|
|
const content = document.getElementById('lpoDetailsContent');
|
|
|
|
const logoUrl = companySettings.company_logo || '';
|
|
const companyHeader = `
|
|
<div class="row align-items-center mb-4">
|
|
<div class="col-6">
|
|
${logoUrl ? `<img src="${logoUrl}" alt="Logo" style="max-height: 80px;" class="mb-3">` : ''}
|
|
<h4 class="fw-bold mb-0">${companySettings.company_name || 'Your Company'}</h4>
|
|
<p class="text-muted mb-0 small">
|
|
${companySettings.company_address || ''}<br>
|
|
Phone: ${companySettings.company_phone || ''} | Email: ${companySettings.company_email || ''}
|
|
${companySettings.tax_number ? `<br>TRN: ${companySettings.tax_number}` : ''}
|
|
</p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-primary fw-bold mb-1">LOCAL PURCHASE ORDER</h2>
|
|
<p class="h5 mb-0 text-muted">LPO-${data.id.toString().padStart(5, '0')}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
let itemsHtml = '';
|
|
data.items.forEach((item, index) => {
|
|
itemsHtml += `
|
|
<tr>
|
|
<td>${index + 1}</td>
|
|
<td>${item.name_en}<br><small class="text-muted">${item.name_ar}</small></td>
|
|
<td class="text-center">${formatQuantity(item.quantity)}</td>
|
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
|
<td class="text-end">${parseFloat(item.total_amount).toFixed(3)}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
content.innerHTML = `
|
|
${companyHeader}
|
|
<hr>
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<h6 class="text-uppercase text-muted fw-bold mb-3" data-en="Supplier" data-ar="المورد">Supplier</h6>
|
|
<p class="h6 mb-1 fw-bold">${data.supplier_name}</p>
|
|
<p class="small text-muted mb-0">
|
|
${data.supplier_phone ? `Phone: ${data.supplier_phone}` : ''}
|
|
</p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h6 class="text-uppercase text-muted fw-bold mb-3" data-en="Details" data-ar="تفاصيل">Details</h6>
|
|
<div class="d-flex justify-content-end mb-1">
|
|
<span class="text-muted me-2">Date:</span>
|
|
<span class="fw-bold">${data.lpo_date}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-end mb-1">
|
|
<span class="text-muted me-2">Delivery:</span>
|
|
<span class="fw-bold">${data.delivery_date || '---'}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<span class="text-muted me-2">Status:</span>
|
|
<span class="badge ${data.status === 'pending' ? 'bg-warning text-dark' : 'bg-success'}">${data.status.toUpperCase()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-bordered">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th style="width: 5%">#</th>
|
|
<th style="width: 45%" data-en="Description" data-ar="الوصف">Description</th>
|
|
<th style="width: 10%" class="text-center" data-en="Qty" data-ar="الكمية">Qty</th>
|
|
<th style="width: 15%" class="text-end">Unit Price</th>
|
|
<th style="width: 10%" class="text-center">VAT</th>
|
|
<th style="width: 15%" class="text-end" data-en="Total" data-ar="الإجمالي">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${itemsHtml}</tbody>
|
|
<tfoot class="table-light">
|
|
<tr>
|
|
<td colspan="5" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
|
<td class="text-end fw-bold">OMR ${parseFloat(data.total_amount).toFixed(3)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="5" class="text-end fw-bold">VAT Amount</td>
|
|
<td class="text-end fw-bold">OMR ${parseFloat(data.vat_amount).toFixed(2)}</td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<td colspan="5" class="text-end fw-bold h5">Grand Total</td>
|
|
<td class="text-end fw-bold h5">OMR ${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
${data.terms_conditions ? `
|
|
<div class="card bg-light border-0 mt-4">
|
|
<div class="card-body p-3">
|
|
<h6 class="fw-bold mb-2 small text-uppercase text-muted">Terms & Conditions</h6>
|
|
<p class="small mb-0">${data.terms_conditions.replace(/\n/g, '<br>')}</p>
|
|
</div>
|
|
</div>` : ''}
|
|
|
|
<div class="row mt-5 pt-3">
|
|
<div class="col-4 text-center">
|
|
<div style="border-top: 1px solid #dee2e6; padding-top: 10px;">
|
|
<p class="small mb-0">Prepared By</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-4"></div>
|
|
<div class="col-4 text-center">
|
|
<div style="border-top: 1px solid #dee2e6; padding-top: 10px;">
|
|
<p class="small mb-0">Authorized Signature</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
window.printLPO = function() {
|
|
const printWindow = window.open('', '_blank');
|
|
printWindow.document.write('<html><head><title>LPO-' + data.id + '</title>');
|
|
printWindow.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap<?= $dir === 'rtl' ? '.rtl' : '' ?>.min.css" rel="stylesheet">');
|
|
printWindow.document.write('<style>body { padding: 40px; } @media print { .no-print { display: none; } }</style>');
|
|
printWindow.document.write('</head><body>');
|
|
printWindow.document.write(content.innerHTML);
|
|
printWindow.document.write('</body></html>');
|
|
printWindow.document.close();
|
|
setTimeout(() => {
|
|
printWindow.print();
|
|
printWindow.close();
|
|
}, 500);
|
|
};
|
|
|
|
modal.show();
|
|
};
|
|
|
|
// Quotation Form Logic
|
|
initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
|
initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
|
|
|
document.querySelectorAll('.edit-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = parseLpoQuotationButtonPayload(this);
|
|
if (Object.keys(data).length === 0) return;
|
|
|
|
const customerSelect = document.getElementById('edit_quot_customer_id');
|
|
const customerId = data.customer_id ?? '';
|
|
const customerLabel = data.customer_name || data.party_name || '';
|
|
|
|
invoiceEnsureSelectOption('edit_quot_customer_id', customerId, customerLabel);
|
|
|
|
const quotationIdInput = document.getElementById('edit_quotation_id');
|
|
const quotationDateInput = document.getElementById('edit_quot_date');
|
|
const validUntilInput = document.getElementById('edit_quot_valid');
|
|
const statusSelect = document.getElementById('edit_quot_status');
|
|
|
|
if (quotationIdInput) quotationIdInput.value = data.id || '';
|
|
if (customerSelect) {
|
|
invoiceSetBlankSelectOptionLabel(customerSelect, (customerId === '' && customerLabel) ? customerLabel : '---');
|
|
customerSelect.value = customerId;
|
|
invoiceSyncSelect2Value(customerSelect);
|
|
}
|
|
if (quotationDateInput) quotationDateInput.value = data.quotation_date || '';
|
|
if (validUntilInput) validUntilInput.value = data.valid_until || '';
|
|
if (statusSelect) statusSelect.value = data.status || 'pending';
|
|
|
|
renderExistingDocumentItems(data.items || [], 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.convert-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
if (confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
|
const f = document.createElement('form');
|
|
f.method = 'POST';
|
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${this.dataset.id}">`;
|
|
document.body.appendChild(f);
|
|
f.submit();
|
|
}
|
|
});
|
|
});
|
|
|
|
// View Quotation Logic
|
|
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
|
const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal'));
|
|
const content = document.getElementById('quotationPrintableArea');
|
|
|
|
let itemsHtml = '';
|
|
data.items.forEach((item, index) => {
|
|
itemsHtml += `
|
|
<tr>
|
|
<td>${index + 1}</td>
|
|
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
|
<td class="text-center">${formatQuantity(item.quantity)}</td>
|
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-center">${parseFloat(item.vat_rate || 0).toFixed(2)}%</td>
|
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
// Company Logo and Header Construction
|
|
const logoUrl = companySettings.company_logo || '';
|
|
const logoImg = logoUrl ? `<img src="${logoUrl}" alt="Logo" class="invoice-logo mb-3">` : '';
|
|
const companyName = companySettings.company_name || 'Accounting System';
|
|
const companyAddress = (companySettings.company_address || '').replace(/\n/g, '<br>');
|
|
const companyVat = companySettings.vat_number ? `<p class="text-muted small mb-0">VAT: ${companySettings.vat_number}</p>` : '';
|
|
const companyPhone = companySettings.company_phone ? `<p class="text-muted small mb-0">Tel: ${companySettings.company_phone}</p>` : '';
|
|
|
|
// Quotation Header Construction
|
|
const quotDate = data.quotation_date;
|
|
const quotValid = data.valid_until || 'N/A';
|
|
const quotNo = 'QUO-' + data.id.toString().padStart(5, '0');
|
|
const customerName = data.customer_name || 'Walk-in Customer';
|
|
const statusBadge = `<span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-secondary'}">${data.status.toUpperCase()}</span>`;
|
|
|
|
content.innerHTML = `
|
|
<div class="p-5">
|
|
<div class="invoice-header mb-4">
|
|
<div class="row align-items-center">
|
|
<div class="col-6">
|
|
${logoImg}
|
|
<h3 class="mb-1 fw-bold">${companyName}</h3>
|
|
<p class="text-muted small mb-0">${companyAddress}</p>
|
|
${companyVat}
|
|
${companyPhone}
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Quotation / عرض سعر</h1>
|
|
<div class="mt-2">${statusBadge}</div>
|
|
<div class="mt-3">
|
|
<p class="mb-0 fs-5">No / رقم: <strong class="text-primary">${quotNo}</strong></p>
|
|
<p class="mb-0">Date / التاريخ: <span class="fw-bold">${quotDate}</span></p>
|
|
<p class="mb-0">Valid Until / صالح لغاية: <span class="fw-bold">${quotValid}</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4 g-3">
|
|
<div class="col-6">
|
|
<div class="invoice-info-card">
|
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">To / إلى</p>
|
|
<h5 class="mb-1 fw-bold">${customerName}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table table-bordered table-formal">
|
|
<thead class="bg-dark text-white">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Item Description / وصف الصنف</th>
|
|
<th class="text-center">Qty / الكمية</th>
|
|
<th class="text-end">Unit Price / سعر الوحدة</th>
|
|
<th class="text-center">VAT / الضريبة</th>
|
|
<th class="text-end">Total / الإجمالي</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${itemsHtml}</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="5" class="text-end">Subtotal / المجموع الفرعي</th>
|
|
<td class="text-end fw-bold">${parseFloat(data.total_amount).toFixed(3)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th colspan="5" class="text-end">VAT Amount / مبلغ الضريبة</th>
|
|
<td class="text-end fw-bold">${parseFloat(data.vat_amount).toFixed(2)}</td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<th colspan="5" class="text-end h5">Grand Total (OMR) / المجموع الكلي (رع)</th>
|
|
<td class="text-end h5 fw-bold">${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<div class="mt-5 pt-3 border-top">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p class="small text-muted text-uppercase fw-bold">Terms & Conditions / الشروط والأحكام:</p>
|
|
<ul class="small text-muted">
|
|
<li>Quotation is valid until the date mentioned above. / عرض السعر صالح لغاية التاريخ المذكور أعلاه.</li>
|
|
<li>Prices are inclusive of VAT where applicable. / الأسعار تشمل ضريبة القيمة المضافة حيثما ينطبق ذلك.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-6 text-end pt-4">
|
|
<div class="border-top d-inline-block px-5">Authorized Signature / التوقيع المعتمد</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-center">
|
|
<p class="text-muted x-small mb-0">Generated by / تم إنشاؤه بواسطة ${companyName}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const actionButtons = document.getElementById('quotationActionButtons');
|
|
actionButtons.innerHTML = '';
|
|
if (data.status === 'pending') {
|
|
const convertBtn = document.createElement('button');
|
|
convertBtn.className = 'btn btn-success me-2';
|
|
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> <span data-en="Convert to Invoice" data-ar="تحويل إلى فاتورة">Convert to Invoice</span>';
|
|
convertBtn.onclick = function() {
|
|
if (confirm('Convert this quotation to an invoice?')) {
|
|
const f = document.createElement('form');
|
|
f.method = 'POST';
|
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
|
document.body.appendChild(f);
|
|
f.submit();
|
|
}
|
|
};
|
|
actionButtons.appendChild(convertBtn);
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.className = 'btn btn-primary';
|
|
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> <span data-en="Edit" data-ar="تعديل">Edit</span>';
|
|
editBtn.onclick = function() {
|
|
modal.hide();
|
|
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-id="${data.id}"]`);
|
|
if (originalEditBtn) originalEditBtn.click();
|
|
};
|
|
actionButtons.appendChild(editBtn);
|
|
}
|
|
|
|
modal.show();
|
|
if (autoPrint) {
|
|
setTimeout(() => { window.print(); }, 500);
|
|
}
|
|
};
|
|
|
|
document.querySelectorAll('.view-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = JSON.parse(this.dataset.json);
|
|
window.viewAndPrintQuotation(data, false);
|
|
});
|
|
});
|
|
|