Autosave: 20260216-155320
This commit is contained in:
parent
d19d0d272e
commit
5efe80f7c5
BIN
assets/pasted-20260216-140816-e8c8b7ad.png
Normal file
BIN
assets/pasted-20260216-140816-e8c8b7ad.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/pasted-20260216-153830-2a6e7f2f.png
Normal file
BIN
assets/pasted-20260216-153830-2a6e7f2f.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
1
db/migrations/20260216_add_credit_limit.sql
Normal file
1
db/migrations/20260216_add_credit_limit.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE customers ADD COLUMN credit_limit DECIMAL(15,3) DEFAULT 0.000 AFTER balance;
|
||||
585
index.php
585
index.php
@ -169,15 +169,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate actual paid amount (excluding credit)
|
||||
$actual_paid = 0;
|
||||
foreach ($payments as $p) {
|
||||
if ($p['method'] !== 'credit') {
|
||||
$actual_paid += (float)$p['amount'];
|
||||
}
|
||||
}
|
||||
|
||||
$status = 'paid';
|
||||
if ($actual_paid <= 0) {
|
||||
$status = 'unpaid';
|
||||
} elseif ($actual_paid < $net_amount - 0.001) {
|
||||
$status = 'partially_paid';
|
||||
}
|
||||
|
||||
$methods_str = implode(', ', array_unique(array_map(fn($p) => $p['method'], $payments)));
|
||||
|
||||
// Create Invoice
|
||||
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, total_with_vat, paid_amount, type) VALUES (?, CURDATE(), 'paid', ?, ?, 'sale')");
|
||||
$stmt->execute([$customer_id, $net_amount, $net_amount]);
|
||||
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, total_with_vat, paid_amount, type, payment_type) VALUES (?, CURDATE(), ?, ?, ?, 'sale', ?)");
|
||||
$stmt->execute([$customer_id, $status, $net_amount, $actual_paid, $methods_str]);
|
||||
$invoice_id = $db->lastInsertId();
|
||||
|
||||
// Add POS Transaction record
|
||||
$stmt = $db->prepare("INSERT INTO pos_transactions (transaction_no, customer_id, total_amount, discount_code_id, discount_amount, loyalty_points_earned, loyalty_points_redeemed, net_amount, payment_method) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$transaction_no = 'POS-' . time() . rand(100, 999);
|
||||
$methods_str = implode(', ', array_unique(array_map(fn($p) => $p['method'], $payments)));
|
||||
$stmt->execute([$transaction_no, $customer_id, $total_amount, $discount_code_id, $discount_amount, $loyalty_earned, $loyalty_redeemed, $net_amount, $methods_str]);
|
||||
$pos_id = $db->lastInsertId();
|
||||
|
||||
@ -214,6 +230,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Add Payments
|
||||
foreach ($payments as $p) {
|
||||
if ($p['method'] === 'credit') continue;
|
||||
$stmt = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, CURDATE(), ?, 'POS Transaction')");
|
||||
$stmt->execute([$invoice_id, (float)$p['amount'], $p['method']]);
|
||||
}
|
||||
@ -979,13 +996,17 @@ switch ($page) {
|
||||
}
|
||||
|
||||
$whereSql = implode(" AND ", $where);
|
||||
$stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id
|
||||
$stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id, c.phone as customer_phone
|
||||
FROM invoices v
|
||||
LEFT JOIN customers c ON v.customer_id = c.id
|
||||
WHERE $whereSql
|
||||
ORDER BY v.id DESC");
|
||||
$stmt->execute($params);
|
||||
$data['invoices'] = $stmt->fetchAll();
|
||||
foreach ($data['invoices'] as &$inv) {
|
||||
$inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']);
|
||||
}
|
||||
unset($inv);
|
||||
|
||||
$data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items ORDER BY name_en ASC")->fetchAll();
|
||||
$data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll();
|
||||
@ -1060,42 +1081,16 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<style>
|
||||
@media print {
|
||||
.sidebar, .topbar, .d-print-none, .btn, .modal-header, .modal-footer {
|
||||
display: none !important;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.modal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
visibility: visible;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.modal-content {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
.sidebar, .topbar, .d-print-none, .no-print { display: none !important; }
|
||||
.main-content { margin-left: 0 !important; padding: 0 !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -1759,6 +1754,16 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
customerPoints: 0,
|
||||
selectedPaymentMethod: 'cash',
|
||||
payments: [],
|
||||
allCreditCustomers: <?php
|
||||
$custData = [];
|
||||
foreach ($customers as $c) {
|
||||
$custData[] = [
|
||||
'value' => (string)$c['id'],
|
||||
'text' => $c['name'] . ' (' . ($c['phone'] ?? '') . ')'
|
||||
];
|
||||
}
|
||||
echo json_encode($custData);
|
||||
?>,
|
||||
add(product) {
|
||||
const existing = this.items.find(item => item.id === product.id);
|
||||
if (existing) {
|
||||
@ -2010,9 +2015,6 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
|
||||
this.payments = [];
|
||||
this.renderPayments();
|
||||
if (document.getElementById('creditCustomerSearch')) {
|
||||
document.getElementById('creditCustomerSearch').value = '';
|
||||
}
|
||||
document.getElementById('paymentAmountDue').innerText = 'OMR ' + total.toFixed(3);
|
||||
document.getElementById('partialAmount').value = total.toFixed(3);
|
||||
|
||||
@ -2020,8 +2022,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
const creditSection = document.getElementById('creditCustomerSection');
|
||||
if (this.selectedPaymentMethod === 'credit') {
|
||||
creditSection.style.display = 'block';
|
||||
this.filterCreditCustomers();
|
||||
document.getElementById('paymentCreditCustomer').value = customerSelect.value;
|
||||
const creditSelect = $('#paymentCreditCustomer');
|
||||
creditSelect.val(customerSelect.value).trigger('change');
|
||||
} else {
|
||||
creditSection.style.display = 'none';
|
||||
}
|
||||
@ -2039,12 +2041,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
const creditSection = document.getElementById('creditCustomerSection');
|
||||
if (method === 'credit') {
|
||||
creditSection.style.display = 'block';
|
||||
if (document.getElementById('creditCustomerSearch')) {
|
||||
document.getElementById('creditCustomerSearch').value = '';
|
||||
}
|
||||
this.filterCreditCustomers();
|
||||
// Sync with main customer select
|
||||
document.getElementById('paymentCreditCustomer').value = document.getElementById('posCustomer').value;
|
||||
const creditSelect = $('#paymentCreditCustomer');
|
||||
creditSelect.val(document.getElementById('posCustomer').value).trigger('change');
|
||||
} else {
|
||||
creditSection.style.display = 'none';
|
||||
}
|
||||
@ -2308,37 +2307,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
location.reload();
|
||||
}, { once: true });
|
||||
},
|
||||
filterCreditCustomers() {
|
||||
const searchInput = document.getElementById('creditCustomerSearch');
|
||||
if (!searchInput) return;
|
||||
const search = searchInput.value.toLowerCase();
|
||||
const select = document.getElementById('paymentCreditCustomer');
|
||||
if (!select) return;
|
||||
|
||||
if (!this.allCreditCustomers || (this.allCreditCustomers.length <= 1 && select.options.length > 1)) {
|
||||
this.allCreditCustomers = Array.from(select.options).map(opt => ({
|
||||
value: opt.value,
|
||||
text: opt.text,
|
||||
search: opt.getAttribute('data-search') || opt.text.toLowerCase()
|
||||
}));
|
||||
}
|
||||
|
||||
if (!this.allCreditCustomers) return;
|
||||
|
||||
const currentValue = select.value;
|
||||
select.innerHTML = '';
|
||||
|
||||
this.allCreditCustomers.forEach(opt => {
|
||||
if (opt.value === "" || opt.search.includes(search)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = opt.value;
|
||||
o.text = opt.text;
|
||||
o.setAttribute('data-search', opt.search);
|
||||
if (opt.value === currentValue) o.selected = true;
|
||||
select.appendChild(o);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async syncCustomer(val) {
|
||||
document.getElementById('posCustomer').value = val;
|
||||
const customerSelect = document.getElementById('posCustomer');
|
||||
@ -2417,6 +2386,18 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
if (bc) bc.focus();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#posCustomer').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select Customer'
|
||||
});
|
||||
$('#paymentCreditCustomer').select2({
|
||||
width: '100%',
|
||||
placeholder: 'Select Customer',
|
||||
dropdownParent: $('#posPaymentModal')
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php elseif ($page === 'sales' || $page === 'purchases'): ?>
|
||||
@ -2508,12 +2489,12 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
||||
<td class="text-end text-danger fw-bold">OMR <?= number_format((float)($inv['total_with_vat'] - $inv['paid_amount']), 3) ?></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-info view-invoice-btn" data-json='<?= json_encode($inv) ?>' data-bs-toggle="modal" data-bs-target="#viewInvoiceModal" title="View"><i class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-outline-primary edit-invoice-btn" data-json='<?= json_encode($inv) ?>' data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
||||
<button class="btn btn-outline-primary edit-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
|
||||
<?php if ($inv['status'] !== 'paid'): ?>
|
||||
<button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-outline-secondary" onclick='viewAndPrintA4Invoice(<?= json_encode($inv) ?>)' title="Print A4 Invoice"><i class="bi bi-printer"></i></button>
|
||||
<button class="btn btn-outline-secondary print-a4-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="Print A4 Invoice"><i class="bi bi-printer"></i></button>
|
||||
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this invoice?')">
|
||||
<input type="hidden" name="id" value="<?= $inv['id'] ?>">
|
||||
<button type="submit" name="delete_invoice" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
@ -3408,108 +3389,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// View Invoice Logic
|
||||
document.querySelectorAll('.view-invoice-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const data = JSON.parse(this.dataset.json);
|
||||
document.getElementById('invNumber').textContent = 'INV-' + data.id.toString().padStart(5, '0');
|
||||
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 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';
|
||||
// View and Print Invoice Logic
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.view-invoice-btn')) {
|
||||
const btn = e.target.closest('.view-invoice-btn');
|
||||
const data = JSON.parse(btn.dataset.json);
|
||||
if (window.viewAndPrintA4Invoice) {
|
||||
window.viewAndPrintA4Invoice(data, false);
|
||||
}
|
||||
|
||||
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.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
let statusAr = data.status;
|
||||
|
||||
if (data.status === 'paid') {
|
||||
statusClass = 'bg-success';
|
||||
statusAr = 'مدفوع';
|
||||
} else if (data.status === 'unpaid') {
|
||||
statusClass = 'bg-danger';
|
||||
statusAr = 'غير مدفوع';
|
||||
} else if (data.status === 'partially_paid') {
|
||||
statusClass = 'bg-warning text-dark';
|
||||
statusEn = 'Partially Paid';
|
||||
statusAr = 'مدفوع جزئياً';
|
||||
}
|
||||
if (e.target.closest('.print-a4-btn')) {
|
||||
const btn = e.target.closest('.print-a4-btn');
|
||||
const data = JSON.parse(btn.dataset.json);
|
||||
if (window.viewAndPrintA4Invoice) {
|
||||
window.viewAndPrintA4Invoice(data, true);
|
||||
}
|
||||
|
||||
statusLabel.textContent = statusEn;
|
||||
statusLabel.setAttribute('data-en', statusEn);
|
||||
statusLabel.setAttribute('data-ar', statusAr);
|
||||
statusLabel.className = 'badge text-uppercase ' + statusClass;
|
||||
|
||||
const body = document.getElementById('invItemsBody');
|
||||
body.innerHTML = '';
|
||||
data.items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.name_en} / ${item.name_ar}</td>
|
||||
<td class="text-center">${item.quantity}</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(3)}%</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
document.getElementById('invSubtotal').textContent = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
||||
document.getElementById('invVatAmount').textContent = 'OMR ' + (parseFloat(data.vat_amount) || 0).toFixed(3);
|
||||
const grandTotalValue = (parseFloat(data.total_with_vat) || parseFloat(data.total_amount));
|
||||
document.getElementById('invGrandTotal').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
|
||||
// Payment Info
|
||||
document.getElementById('invTotalInfo').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
document.getElementById('invPaidInfo').textContent = 'OMR ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
||||
document.getElementById('invBalanceInfo').textContent = 'OMR ' + balance.toFixed(3);
|
||||
|
||||
// Fetch and show payment history
|
||||
const paymentsBody = document.getElementById('invPaymentsBody');
|
||||
const paymentsSection = document.getElementById('invPaymentsSection');
|
||||
paymentsBody.innerHTML = '';
|
||||
|
||||
fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
|
||||
.then(res => res.json())
|
||||
.then(payments => {
|
||||
if (payments && payments.length > 0) {
|
||||
payments.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${p.payment_date}</td>
|
||||
<td>${p.payment_method}</td>
|
||||
<td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>
|
||||
`;
|
||||
paymentsBody.appendChild(tr);
|
||||
});
|
||||
paymentsSection.style.display = 'block';
|
||||
} else {
|
||||
paymentsSection.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Generate QR Code
|
||||
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'My Company') ?>";
|
||||
const vatNo = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
||||
const total = (parseFloat(data.total_with_vat) || parseFloat(data.total_amount)).toFixed(3);
|
||||
const qrData = `Seller: ${companyName}\nVAT: ${vatNo}\nInvoice: INV-${data.id.toString().padStart(5, '0')}\nDate: ${data.invoice_date}\nTotal: ${total}`;
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(qrData)}`;
|
||||
document.getElementById('invQrCode').innerHTML = `<img src="${qrUrl}" alt="QR Code" style="width: 100px; height: 100px;" class="border p-1 bg-white">`;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@ -3717,91 +3612,174 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
.no-print, .sidebar, .topbar, .card, .btn, .modal-header, .modal-footer, .d-print-none, .table-responsive,
|
||||
table:not(.table-formal), .bg-light:not(.invoice-info-card):not(.p-3), .modal-backdrop { display: none !important; }
|
||||
body { background: white !important; margin: 0 !important; padding: 0 !important; }
|
||||
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||
.modal { 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; }
|
||||
#viewInvoiceModal { display: block !important; background: white !important; }
|
||||
#viewInvoiceModal .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
|
||||
#viewInvoiceModal .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
|
||||
#viewInvoiceModal .modal-body { padding: 0 !important; margin: 0 !important; background: white !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 */
|
||||
body > *:not(.main-content):not(.modal) { display: none !important; }
|
||||
.main-content > *:not(#viewInvoiceModal):not(.modal) { display: none !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; }
|
||||
</style>
|
||||
|
||||
<div class="modal fade" id="viewInvoiceModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header d-print-none">
|
||||
<h5 class="modal-title" data-en="View Invoice" data-ar="عرض الفاتورة">View Invoice</h5>
|
||||
<button type="button" class="btn-close d-print-none" data-bs-dismiss="modal"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4" id="invoicePrintableArea">
|
||||
<div class="row mb-4">
|
||||
<div class="col-6">
|
||||
<h3 class="mb-0 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
||||
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
||||
<p class="text-muted small">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<h2 class="text-uppercase text-muted fw-bold mb-0">Invoice</h2>
|
||||
<div class="mb-2"><span id="invoiceTypeLabel" class="badge"></span></div>
|
||||
<p class="mb-0"><strong>No: <span id="invNumber"></span></strong></p>
|
||||
<p class="mb-0 text-muted">Date: <span id="invDate"></span></p>
|
||||
<p class="text-muted small">Status: <span id="invoiceStatusLabel"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4 p-3 bg-light rounded">
|
||||
<div class="col-6">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-1" id="invPartyLabel">Bill To</p>
|
||||
<p class="mb-0 fw-bold"><span id="invCustomerName"></span></p>
|
||||
<p class="small text-muted mb-0" id="invCustomerTaxIdContainer">VAT: <span id="invCustomerTaxId"></span></p>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-1">Payment Type</p>
|
||||
<p class="mb-0"><span id="invPaymentType"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-bordered">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<th class="text-center">Qty</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-end">VAT %</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invItemsBody"></tbody>
|
||||
</table>
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span id="invSubtotal" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">VAT Amount</span>
|
||||
<span id="invVatAmount" class="fw-bold"></span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="h5 fw-bold">Grand Total</span>
|
||||
<span id="invGrandTotal" class="h5 fw-bold text-primary"></span>
|
||||
</div>
|
||||
<div id="invPaymentsSection" class="mt-4 border-top pt-3">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-2">Payment History</p>
|
||||
<table class="table table-sm table-borderless small mb-0">
|
||||
<tbody id="invPaymentsBody"></tbody>
|
||||
</table>
|
||||
<div class="bg-light p-2 mt-2 rounded">
|
||||
<div class="d-flex justify-content-between small mb-1">
|
||||
<span>Total Amount</span>
|
||||
<span id="invTotalInfo"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small mb-1 text-success">
|
||||
<span>Paid Amount</span>
|
||||
<span id="invPaidInfo"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small fw-bold text-danger">
|
||||
<span>Remaining Balance</span>
|
||||
<span id="invBalanceInfo"></span>
|
||||
<div class="modal-body p-0" id="invoicePrintableArea">
|
||||
<div class="p-5">
|
||||
<div class="invoice-header mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-6">
|
||||
<?php
|
||||
$logo = $data['settings']['company_logo'] ?? $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
if ($logo):
|
||||
?>
|
||||
<img src="<?= htmlspecialchars($logo) ?>" alt="Logo" class="invoice-logo mb-3">
|
||||
<?php endif; ?>
|
||||
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
||||
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
||||
<p class="text-muted small mb-0">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
||||
<?php if (!empty($data['settings']['company_phone'])): ?>
|
||||
<p class="text-muted small mb-0">Tel: <?= htmlspecialchars($data['settings']['company_phone']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Tax Invoice</h1>
|
||||
<div class="mt-2"><span id="invoiceTypeLabel" class="badge"></span></div>
|
||||
<div class="mt-3">
|
||||
<p class="mb-0 fs-5">Invoice No: <strong id="invNumber" class="text-primary"></strong></p>
|
||||
<p class="mb-0">Date: <span id="invDate" class="fw-bold"></span></p>
|
||||
<p class="mb-0 small">Status: <span id="invoiceStatusLabel"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 pt-3 border-top text-center">
|
||||
<div id="invQrCode" class="mb-2"></div>
|
||||
<p class="text-muted small">Generated by <?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></p>
|
||||
|
||||
<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" id="invPartyLabel">Bill To</p>
|
||||
<h5 class="mb-1 fw-bold"><span id="invCustomerName"></span></h5>
|
||||
<p class="small text-muted mb-0" id="invCustomerTaxIdContainer">VAT: <span id="invCustomerTaxId"></span></p>
|
||||
<p class="small text-muted mb-0" id="invCustomerPhoneContainer">Phone: <span id="invCustomerPhone"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="invoice-info-card text-end">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">Payment Details</p>
|
||||
<p class="mb-1">Method: <strong id="invPaymentType"></strong></p>
|
||||
<p class="mb-0 small text-muted">Currency: <strong>OMR</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered table-formal">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<th class="text-center" style="width: 80px;">Qty</th>
|
||||
<th class="text-end" style="width: 120px;">Unit Price</th>
|
||||
<th class="text-end" style="width: 100px;">VAT %</th>
|
||||
<th class="text-end" style="width: 150px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invItemsBody"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-7">
|
||||
<div class="p-3 bg-light rounded" style="min-height: 100px;">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-1">Amount in Words</p>
|
||||
<p id="invAmountInWords" class="small fw-bold mb-0"></p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-1">Terms & Conditions</p>
|
||||
<ul class="small text-muted ps-3">
|
||||
<li>Goods once sold will not be taken back or exchanged.</li>
|
||||
<li>Payment is due within the agreed credit period.</li>
|
||||
<li>This is a computer-generated invoice and does not require a physical signature.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span id="invSubtotal" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">VAT Amount</span>
|
||||
<span id="invVatAmount" class="fw-bold"></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"></span>
|
||||
</div>
|
||||
|
||||
<div id="invPaymentsSection" class="mt-4 border-top pt-3">
|
||||
<p class="text-muted small text-uppercase fw-bold mb-2">Payment Tracking</p>
|
||||
<table class="table table-sm table-bordered small mb-3">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Method</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invPaymentsBody"></tbody>
|
||||
</table>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<div class="d-flex justify-content-between small mb-1">
|
||||
<span>Paid Amount</span>
|
||||
<span id="invPaidInfo" class="text-success fw-bold"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small fw-bold">
|
||||
<span>Balance Due</span>
|
||||
<span id="invBalanceInfo" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 pt-4 border-top text-center">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
||||
<p class="small text-muted">Customer Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div id="invQrCode" class="mb-2 d-flex justify-content-center"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
||||
<p class="small text-muted">Authorized Signatory</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted x-small mt-4 mb-0">Generated by <?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at <?= $_SERVER['HTTP_HOST'] ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer d-print-none">
|
||||
@ -3946,11 +3924,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<i class="bi bi-person-circle fs-3 text-secondary"></i>
|
||||
</div>
|
||||
<div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;">
|
||||
<label class="form-label smaller fw-bold mb-1">Select Credit Customer (Search by Name/Phone)</label>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="creditCustomerSearch" class="form-control" placeholder="Search..." oninput="cart.filterCreditCustomers()">
|
||||
</div>
|
||||
<label class="form-label smaller fw-bold mb-1">Select Credit Customer</label>
|
||||
<select id="paymentCreditCustomer" class="form-select form-select-sm" onchange="cart.syncCustomer(this.value)">
|
||||
<option value="">--- Select Customer ---</option>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
@ -4060,13 +4034,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div id="posPrintArea" class="d-none d-print-block"></div>
|
||||
|
||||
<script>
|
||||
window.viewAndPrintA4Invoice = function(data) {
|
||||
window.viewAndPrintA4Invoice = function(data, autoPrint = true) {
|
||||
if (!data) return;
|
||||
// Reuse view logic
|
||||
document.getElementById('invNumber').textContent = 'INV-' + data.id.toString().padStart(5, '0');
|
||||
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 taxIdEl = document.getElementById('invCustomerTaxId');
|
||||
const taxIdContainer = document.getElementById('invCustomerTaxIdContainer');
|
||||
if (data.customer_tax_id) {
|
||||
@ -4076,6 +4060,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
taxIdContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('invAmountInWords').textContent = data.total_in_words || '';
|
||||
|
||||
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' ? 'فاتورة إلى' : 'فاتورة من');
|
||||
@ -4084,7 +4070,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const statusLabel = document.getElementById('invoiceStatusLabel');
|
||||
let statusClass = 'bg-secondary';
|
||||
let statusEn = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
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') {
|
||||
@ -4097,48 +4083,63 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const body = document.getElementById('invItemsBody');
|
||||
body.innerHTML = '';
|
||||
data.items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.name_en} / ${item.name_ar}</td>
|
||||
<td class="text-center">${item.quantity}</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(3)}%</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
document.getElementById('invSubtotal').textContent = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
||||
document.getElementById('invVatAmount').textContent = 'OMR ' + (parseFloat(data.vat_amount) || 0).toFixed(3);
|
||||
if (data.items) {
|
||||
data.items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.name_en} / ${item.name_ar}</td>
|
||||
<td class="text-center">${item.quantity}</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.unit_price).toFixed(3)}</td>
|
||||
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(3)}%</td>
|
||||
<td class="text-end">OMR ${parseFloat(item.total_price).toFixed(3)}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').textContent = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
||||
if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').textContent = 'OMR ' + (parseFloat(data.vat_amount) || 0).toFixed(3);
|
||||
const grandTotalValue = (parseFloat(data.total_with_vat) || parseFloat(data.total_amount));
|
||||
document.getElementById('invGrandTotal').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
|
||||
document.getElementById('invTotalInfo').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
||||
document.getElementById('invPaidInfo').textContent = 'OMR ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').textContent = 'OMR ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
||||
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
||||
document.getElementById('invBalanceInfo').textContent = 'OMR ' + balance.toFixed(3);
|
||||
if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').textContent = 'OMR ' + balance.toFixed(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: INV-${data.id.toString().padStart(5, '0')}\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 = `<img src="${qrUrl}" alt="QR Code" style="width: 100px; height: 100px;" class="border p-1 bg-white">`;
|
||||
}
|
||||
|
||||
const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal'));
|
||||
viewModal.show();
|
||||
|
||||
if (autoPrint) {
|
||||
setTimeout(() => { window.print(); }, 1000);
|
||||
}
|
||||
|
||||
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');
|
||||
paymentsBody.innerHTML = '';
|
||||
if (paymentsBody) paymentsBody.innerHTML = '';
|
||||
if (payments && payments.length > 0) {
|
||||
payments.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${p.payment_date}</td><td>${p.payment_method}</td><td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>`;
|
||||
paymentsBody.appendChild(tr);
|
||||
});
|
||||
paymentsSection.style.display = 'block';
|
||||
if (paymentsBody) {
|
||||
payments.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${p.payment_date}</td><td>${p.payment_method}</td><td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>`;
|
||||
paymentsBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
if (paymentsSection) paymentsSection.style.display = 'block';
|
||||
} else {
|
||||
paymentsSection.style.display = 'none';
|
||||
if (paymentsSection) paymentsSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const viewModal = new bootstrap.Modal(document.getElementById('viewInvoiceModal'));
|
||||
viewModal.show();
|
||||
setTimeout(() => { window.print(); }, 1000);
|
||||
});
|
||||
}).catch(err => console.error('Error fetching payments:', err));
|
||||
};
|
||||
|
||||
window.printPosReceiptFromInvoice = function(inv) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user