From 54f3f11fd2f62bbffa5f58333e120ab32f2e4d1d Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Sat, 23 May 2026 16:46:35 +0700 Subject: [PATCH] automate and view gl entries --- .../api/engine/get_gl_by_source.php | 34 ++++++ .../utils/classes_ac/GlQueryManager.php | 2 +- app/expense/purchase_invoice.php | 96 +++++++++++++++- app/finance/api/engine/manage_payment.php | 24 ++++ app/finance/api/engine/manage_receipt.php | 24 ++++ app/finance/payment.php | 86 ++++++++++++-- app/finance/receipt.php | 75 +++++++++++- app/order/api/engine/issue_invoice.php | 45 ++++++++ app/po/invoice.php | 66 ++++++++++- app/po/manage_purchase_invoice.php | 9 +- app/revenue/invoice.php | 107 +++++++++++++++++- app/revenue/manage_invoice.php | 9 +- 12 files changed, 551 insertions(+), 26 deletions(-) create mode 100644 app/accounting/api/engine/get_gl_by_source.php diff --git a/app/accounting/api/engine/get_gl_by_source.php b/app/accounting/api/engine/get_gl_by_source.php new file mode 100644 index 0000000..b116304 --- /dev/null +++ b/app/accounting/api/engine/get_gl_by_source.php @@ -0,0 +1,34 @@ +getBySource($doc_type, $source_id); + + if (!$entry) { + $answer['success'] = 1; + $answer['output'] = null; + exit(json_encode($answer)); + } + + $query = new GlQueryManager($pdo2, $company_id); + $detail = $query->getJournalDetail((int)$entry['id']); + + $answer['success'] = 1; + $answer['output'] = $detail; +} catch (Exception $e) { + $answer['message'] = $e->getMessage(); +} + +exit(json_encode($answer)); diff --git a/app/assets/utils/classes_ac/GlQueryManager.php b/app/assets/utils/classes_ac/GlQueryManager.php index 3510a10..553dc42 100644 --- a/app/assets/utils/classes_ac/GlQueryManager.php +++ b/app/assets/utils/classes_ac/GlQueryManager.php @@ -201,7 +201,7 @@ class GlQueryManager ) AS product_mapping_missing" : ", 0 AS product_mapping_missing"; - $where = ['i.company_id = :cid', 'i.doc_type = :doc_type', 'i.status != 4']; + $where = ['i.company_id = :cid', 'i.doc_type = :doc_type', 'i.status NOT IN (0, 4)']; $params = [':cid' => $this->companyId, ':doc_type' => $doc_type, ':source_type' => $doc_type]; if ($date_from) { $where[] = 'i.issued_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'i.issued_date <= :date_to'; $params[':date_to'] = $date_to; } diff --git a/app/expense/purchase_invoice.php b/app/expense/purchase_invoice.php index 46bec59..4bf0ecf 100644 --- a/app/expense/purchase_invoice.php +++ b/app/expense/purchase_invoice.php @@ -345,10 +345,16 @@ ${inv.due_date ? format_date(inv.due_date) : '—'} ${formula_name} ${invoice_status_badge(inv.status, inv.due_date)} - - + + + + + + + + `; }); @@ -387,10 +393,16 @@ ${format_number(scn.grand_total, 2)} ${formula_name} ${invoice_status_badge(scn.status, null)} - - + + + + + + + + `; }); @@ -410,7 +422,83 @@ await retrieve_invoices(); } }); + + function delete_invoice(id, doc_number) { + bootbox.confirm({ + message: 'Delete ' + escape_html(doc_number) + '?
This cannot be undone. The document number will be freed for reuse.', + buttons: { + confirm: { label: 'Delete', className: 'btn-danger' }, + cancel: { label: 'Back', className: 'btn-secondary' } + }, + callback: function(result) { + if (!result) return; + ajax_request({ + url: server_url + 'order/api/engine/delete_invoice.php', + autoPrepare: true, checkRequired: 0, action: 'delete', + data: { id: id }, + onSuccess: function() { retrieve_invoices(); } + }); + } + }); + } + + var _GL_SOURCE_LABELS = { invoice:'Invoice', credit_note:'Credit Note', purchase_invoice:'Purchase Invoice', supplier_credit_note:'Supplier Credit Note', receipt:'Receipt', payment:'Payment', manual:'Manual', reversal:'Reversal' }; + var _GL_SOURCE_BADGE = { invoice:'bg-success-subtle text-success', credit_note:'bg-warning-subtle text-warning', purchase_invoice:'bg-primary-subtle text-primary', supplier_credit_note:'bg-secondary-subtle text-secondary', receipt:'bg-info-subtle text-info', payment:'bg-danger-subtle text-danger', manual:'bg-dark-subtle text-dark', reversal:'bg-danger-subtle text-danger' }; + function _gl_source_badge(t) { return '' + (_GL_SOURCE_LABELS[t]||t) + ''; } + + function show_gl_modal(doc_type, source_id) { + $('#gl-modal-body').html('
Loading…
'); + $('#glModal').modal('show'); + ajax_request({ + url: server_url + 'accounting/api/engine/get_gl_by_source.php', + autoPrepare: true, checkRequired: 0, noLoading: true, action: 'read', + data: { doc_type: doc_type, source_id: source_id }, + onSuccess: function(res) { + if (!res.output) { $('#gl-modal-body').html('

No GL entry posted for this document.

'); return; } + var h = res.output.header, lines = res.output.lines || []; + var total_debit = 0, total_credit = 0, lines_html = ''; + $.each(lines, function(i, l) { + total_debit += parseFloat(l.debit) || 0; + total_credit += parseFloat(l.credit) || 0; + var dept = l.dept_code ? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : '')) : '—'; + lines_html += '' + escape_html(l.account_code) + '' + escape_html(l.account_name) + '' + dept + '' + + '' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '—') + '' + + '' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '—') + '' + + '' + escape_html(l.description || '—') + ''; + }); + lines_html += 'Total' + format_number(total_debit, 2) + '' + format_number(total_credit, 2) + ''; + var balanced = Math.abs(total_debit - total_credit) < 0.01; + var bal_badge = balanced ? 'Balanced' : 'Unbalanced'; + $('#gl-modal-body').html( + '
' + + '
Source
' + _gl_source_badge(h.source_type) + '
' + + '
Period
' + escape_html(h.period) + '
' + + '
Formula
' + escape_html(h.formula_name || '—') + '
' + + '
Posted At
' + escape_html(h.posted_at) + '
' + + '
Updated At
' + escape_html(h.updated_at_fmt) + '
' + + '
Journal Lines' + bal_badge + '
' + + '' + lines_html + '
Account CodeAccount NameDepartmentDebitCreditDescription
' + ); + } + }); + } + + + diff --git a/app/finance/api/engine/manage_payment.php b/app/finance/api/engine/manage_payment.php index 5c6dc1f..c5c95a7 100644 --- a/app/finance/api/engine/manage_payment.php +++ b/app/finance/api/engine/manage_payment.php @@ -54,6 +54,30 @@ (new UsageGuard($pdo1, $company_id, $packages))->increment(); require_once '../../../assets/utils/notify_node.php'; notify_node('payment_posted', ['id' => $new_id], $company_id); + + // Auto-post GL + try { + require_once '../../../assets/utils/classes_ac/posting/BasePosting.php'; + require_once '../../../assets/utils/classes_ac/posting/PaymentPosting.php'; + $sth = $pdo2->prepare("SELECT formula_id FROM td_payment WHERE company_id = :cid AND id = :id LIMIT 1"); + $sth->execute([':cid' => $company_id, ':id' => $new_id]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + $posting = new PaymentPosting($pdo2, $company_id); + $built = $posting->build($new_id, $row['formula_id'] ?: null); + $guard = new PostingWindowGuard($pdo1, $company_id); + $gl = new GlManager($pdo2, $company_id, $guard); + $pdo2->beginTransaction(); + $gl->post('payment', $new_id, $built['formula_id'], $built['period'], $built['lines'], ['journal_date' => $built['doc_date']]); + $pdo2->commit(); + notify_node('gl_posted', ['doc_type' => 'payment', 'id' => $new_id, 'action' => 'posted'], $company_id); + $answer['gl_posted'] = true; + } catch (Exception $e) { + if ($pdo2->inTransaction()) $pdo2->rollBack(); + $answer['gl_posted'] = false; + $answer['gl_message'] = $e->getMessage(); + error_log("auto_gl [payment#{$new_id}]: " . $e->getMessage()); + } + exit(json_encode($answer)); } diff --git a/app/finance/api/engine/manage_receipt.php b/app/finance/api/engine/manage_receipt.php index 5467322..09671aa 100644 --- a/app/finance/api/engine/manage_receipt.php +++ b/app/finance/api/engine/manage_receipt.php @@ -54,6 +54,30 @@ (new UsageGuard($pdo1, $company_id, $packages))->increment(); require_once '../../../assets/utils/notify_node.php'; notify_node('receipt_posted', ['id' => $new_id], $company_id); + + // Auto-post GL + try { + require_once '../../../assets/utils/classes_ac/posting/BasePosting.php'; + require_once '../../../assets/utils/classes_ac/posting/ReceiptPosting.php'; + $sth = $pdo2->prepare("SELECT formula_id FROM td_receipt WHERE company_id = :cid AND id = :id LIMIT 1"); + $sth->execute([':cid' => $company_id, ':id' => $new_id]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + $posting = new ReceiptPosting($pdo2, $company_id); + $built = $posting->build($new_id, $row['formula_id'] ?: null); + $guard = new PostingWindowGuard($pdo1, $company_id); + $gl = new GlManager($pdo2, $company_id, $guard); + $pdo2->beginTransaction(); + $gl->post('receipt', $new_id, $built['formula_id'], $built['period'], $built['lines'], ['journal_date' => $built['doc_date']]); + $pdo2->commit(); + notify_node('gl_posted', ['doc_type' => 'receipt', 'id' => $new_id, 'action' => 'posted'], $company_id); + $answer['gl_posted'] = true; + } catch (Exception $e) { + if ($pdo2->inTransaction()) $pdo2->rollBack(); + $answer['gl_posted'] = false; + $answer['gl_message'] = $e->getMessage(); + error_log("auto_gl [receipt#{$new_id}]: " . $e->getMessage()); + } + exit(json_encode($answer)); } diff --git a/app/finance/payment.php b/app/finance/payment.php index 677f525..c6e0bbe 100644 --- a/app/finance/payment.php +++ b/app/finance/payment.php @@ -257,11 +257,14 @@ ${format_number(r.amount, 2)} ${formula_name} ${payment_status_badge(r.status)} - - + + - ${can_delete ? `` : ''} + + + + ${can_delete ? `` : ''} `; }); @@ -439,14 +442,24 @@ notes: $('#notes').val(), }, onSuccess: function(res) { + if (res.gl_posted === true) { + toastr.success('GL entry posted automatically.', 'GL Posted'); + } else if (res.gl_posted === false) { + toastr.warning('Payment posted, but GL posting failed: ' + (res.gl_message || 'Unknown error'), 'GL Skipped', { timeOut: 8000 }); + } + var delay = res.gl_posted === false ? 3000 : 800; if (prefill_billing_id > 0 && res.new_id) { - window.location.href = 'finance/manage_payment.php?id=' + res.new_id; + setTimeout(function() { + window.location.href = 'finance/manage_payment.php?id=' + res.new_id; + }, delay); return; } - hide_payment_form(); - $('#contact, #contact_id, #notes').val(''); - $('#allocation_tbody').html('Select a supplier and load open billings.'); - load_payments(); + setTimeout(function() { + hide_payment_form(); + $('#contact, #contact_id, #notes').val(''); + $('#allocation_tbody').html('Select a supplier and load open billings.'); + load_payments(); + }, delay); } }); } @@ -504,7 +517,64 @@ }); } }); + + var _GL_SOURCE_LABELS = { invoice:'Invoice', credit_note:'Credit Note', purchase_invoice:'Purchase Invoice', supplier_credit_note:'Supplier Credit Note', receipt:'Receipt', payment:'Payment', manual:'Manual', reversal:'Reversal' }; + var _GL_SOURCE_BADGE = { invoice:'bg-success-subtle text-success', credit_note:'bg-warning-subtle text-warning', purchase_invoice:'bg-primary-subtle text-primary', supplier_credit_note:'bg-secondary-subtle text-secondary', receipt:'bg-info-subtle text-info', payment:'bg-danger-subtle text-danger', manual:'bg-dark-subtle text-dark', reversal:'bg-danger-subtle text-danger' }; + function _gl_source_badge(t) { return '' + (_GL_SOURCE_LABELS[t]||t) + ''; } + + function show_gl_modal(doc_type, source_id) { + $('#gl-modal-body').html('
Loading…
'); + $('#glModal').modal('show'); + ajax_request({ + url: server_url + 'accounting/api/engine/get_gl_by_source.php', + autoPrepare: true, checkRequired: 0, noLoading: true, action: 'read', + data: { doc_type: doc_type, source_id: source_id }, + onSuccess: function(res) { + if (!res.output) { $('#gl-modal-body').html('

No GL entry posted for this document.

'); return; } + var h = res.output.header, lines = res.output.lines || []; + var total_debit = 0, total_credit = 0, lines_html = ''; + $.each(lines, function(i, l) { + total_debit += parseFloat(l.debit) || 0; + total_credit += parseFloat(l.credit) || 0; + var dept = l.dept_code ? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : '')) : '—'; + lines_html += '' + escape_html(l.account_code) + '' + escape_html(l.account_name) + '' + dept + '' + + '' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '—') + '' + + '' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '—') + '' + + '' + escape_html(l.description || '—') + ''; + }); + lines_html += 'Total' + format_number(total_debit, 2) + '' + format_number(total_credit, 2) + ''; + var balanced = Math.abs(total_debit - total_credit) < 0.01; + var bal_badge = balanced ? 'Balanced' : 'Unbalanced'; + $('#gl-modal-body').html( + '
' + + '
Source
' + _gl_source_badge(h.source_type) + '
' + + '
Period
' + escape_html(h.period) + '
' + + '
Formula
' + escape_html(h.formula_name || '—') + '
' + + '
Posted At
' + escape_html(h.posted_at) + '
' + + '
Updated At
' + escape_html(h.updated_at_fmt) + '
' + + '
Journal Lines' + bal_badge + '
' + + '' + lines_html + '
Account CodeAccount NameDepartmentDebitCreditDescription
' + ); + } + }); + } + + + diff --git a/app/finance/receipt.php b/app/finance/receipt.php index 8c47826..49c6abd 100644 --- a/app/finance/receipt.php +++ b/app/finance/receipt.php @@ -287,11 +287,14 @@ ${format_number(r.amount, 2)} ${formula_name} ${receipt_status_badge(r.status)} - - + + - ${can_delete ? `` : ''} + + + + ${can_delete ? `` : ''} `; }); @@ -470,7 +473,14 @@ }, onSuccess: function(res) { if (res.new_id) { - window.location.href = 'finance/manage_receipt.php?id=' + res.new_id; + if (res.gl_posted === true) { + toastr.success('GL entry posted automatically.', 'GL Posted'); + } else if (res.gl_posted === false) { + toastr.warning('Receipt posted, but GL posting failed: ' + (res.gl_message || 'Unknown error'), 'GL Skipped', { timeOut: 8000 }); + } + setTimeout(function() { + window.location.href = 'finance/manage_receipt.php?id=' + res.new_id; + }, res.gl_posted === false ? 3000 : 800); } } }); @@ -529,7 +539,64 @@ }); } }); + + var _GL_SOURCE_LABELS = { invoice:'Invoice', credit_note:'Credit Note', purchase_invoice:'Purchase Invoice', supplier_credit_note:'Supplier Credit Note', receipt:'Receipt', payment:'Payment', manual:'Manual', reversal:'Reversal' }; + var _GL_SOURCE_BADGE = { invoice:'bg-success-subtle text-success', credit_note:'bg-warning-subtle text-warning', purchase_invoice:'bg-primary-subtle text-primary', supplier_credit_note:'bg-secondary-subtle text-secondary', receipt:'bg-info-subtle text-info', payment:'bg-danger-subtle text-danger', manual:'bg-dark-subtle text-dark', reversal:'bg-danger-subtle text-danger' }; + function _gl_source_badge(t) { return '' + (_GL_SOURCE_LABELS[t]||t) + ''; } + + function show_gl_modal(doc_type, source_id) { + $('#gl-modal-body').html('
Loading…
'); + $('#glModal').modal('show'); + ajax_request({ + url: server_url + 'accounting/api/engine/get_gl_by_source.php', + autoPrepare: true, checkRequired: 0, noLoading: true, action: 'read', + data: { doc_type: doc_type, source_id: source_id }, + onSuccess: function(res) { + if (!res.output) { $('#gl-modal-body').html('

No GL entry posted for this document.

'); return; } + var h = res.output.header, lines = res.output.lines || []; + var total_debit = 0, total_credit = 0, lines_html = ''; + $.each(lines, function(i, l) { + total_debit += parseFloat(l.debit) || 0; + total_credit += parseFloat(l.credit) || 0; + var dept = l.dept_code ? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : '')) : '—'; + lines_html += '' + escape_html(l.account_code) + '' + escape_html(l.account_name) + '' + dept + '' + + '' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '—') + '' + + '' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '—') + '' + + '' + escape_html(l.description || '—') + ''; + }); + lines_html += 'Total' + format_number(total_debit, 2) + '' + format_number(total_credit, 2) + ''; + var balanced = Math.abs(total_debit - total_credit) < 0.01; + var bal_badge = balanced ? 'Balanced' : 'Unbalanced'; + $('#gl-modal-body').html( + '
' + + '
Source
' + _gl_source_badge(h.source_type) + '
' + + '
Period
' + escape_html(h.period) + '
' + + '
Formula
' + escape_html(h.formula_name || '—') + '
' + + '
Posted At
' + escape_html(h.posted_at) + '
' + + '
Updated At
' + escape_html(h.updated_at_fmt) + '
' + + '
Journal Lines' + bal_badge + '
' + + '' + lines_html + '
Account CodeAccount NameDepartmentDebitCreditDescription
' + ); + } + }); + } + + + diff --git a/app/order/api/engine/issue_invoice.php b/app/order/api/engine/issue_invoice.php index cbec71c..2dd074f 100644 --- a/app/order/api/engine/issue_invoice.php +++ b/app/order/api/engine/issue_invoice.php @@ -13,6 +13,13 @@ exit(json_encode($answer)); } + $posting_map = [ + 'invoice' => 'InvoicePosting', + 'credit_note' => 'CreditNotePosting', + 'purchase_invoice' => 'PurchaseInvoicePosting', + 'supplier_credit_note' => 'SupplierCreditNotePosting', + ]; + try { dbTransaction($pdo2, function($pdo) use ($id, $due_date, $company_id, $logging) { $inv = new InvoiceManager($pdo, $company_id); @@ -22,6 +29,44 @@ $answer['success'] = 1; $answer['message'] = 'Invoice issued.'; + // Auto-post GL for this document + $sth = $pdo2->prepare("SELECT doc_type, formula_id FROM td_invoice WHERE company_id = :cid AND id = :id LIMIT 1"); + $sth->execute([':cid' => $company_id, ':id' => $id]); + $doc = $sth->fetch(PDO::FETCH_ASSOC); + + if ($doc && isset($posting_map[$doc['doc_type']])) { + try { + $class_name = $posting_map[$doc['doc_type']]; + require_once '../../../assets/utils/classes_ac/posting/BasePosting.php'; + require_once '../../../assets/utils/classes_ac/posting/' . $class_name . '.php'; + require_once '../../../assets/utils/notify_node.php'; + + $posting = new $class_name($pdo2, $company_id); + $built = $posting->build($id, $doc['formula_id'] ?: null); + + $guard = new PostingWindowGuard($pdo1, $company_id); + $gl = new GlManager($pdo2, $company_id, $guard); + $meta = ['journal_date' => $built['doc_date'] ?? null]; + $existing = $gl->getBySource($doc['doc_type'], $id); + + $pdo2->beginTransaction(); + if ($existing) { + $gl->replace($doc['doc_type'], $id, $built['formula_id'], $built['period'], $built['lines'], $meta); + } else { + $gl->post($doc['doc_type'], $id, $built['formula_id'], $built['period'], $built['lines'], $meta); + } + $pdo2->commit(); + + notify_node('gl_posted', ['doc_type' => $doc['doc_type'], 'id' => $id, 'action' => 'posted'], $company_id); + $answer['gl_posted'] = true; + } catch (Exception $e) { + if ($pdo2->inTransaction()) $pdo2->rollBack(); + $answer['gl_posted'] = false; + $answer['gl_message'] = $e->getMessage(); + error_log("auto_gl [{$doc['doc_type']}#{$id}]: " . $e->getMessage()); + } + } + } catch (PDOException $e) { $answer['message'] = 'Database error, please try again.'; http_response_code(500); diff --git a/app/po/invoice.php b/app/po/invoice.php index ea6d622..c4a981c 100644 --- a/app/po/invoice.php +++ b/app/po/invoice.php @@ -256,11 +256,14 @@ ${format_number(inv.grand_total, 2)} ${inv.due_date ? format_date(inv.due_date) : '—'} ${invoice_status_badge(inv.status, inv.due_date)} - - + + - ${can_delete ? `` : ''} + + + + ${can_delete ? `` : ''} `; }); @@ -286,6 +289,47 @@ }); } + var _GL_SOURCE_LABELS = { invoice:'Invoice', credit_note:'Credit Note', purchase_invoice:'Purchase Invoice', supplier_credit_note:'Supplier Credit Note', receipt:'Receipt', payment:'Payment', manual:'Manual', reversal:'Reversal' }; + var _GL_SOURCE_BADGE = { invoice:'bg-success-subtle text-success', credit_note:'bg-warning-subtle text-warning', purchase_invoice:'bg-primary-subtle text-primary', supplier_credit_note:'bg-secondary-subtle text-secondary', receipt:'bg-info-subtle text-info', payment:'bg-danger-subtle text-danger', manual:'bg-dark-subtle text-dark', reversal:'bg-danger-subtle text-danger' }; + function _gl_source_badge(t) { return '' + (_GL_SOURCE_LABELS[t]||t) + ''; } + + function show_gl_modal(doc_type, source_id) { + $('#gl-modal-body').html('
Loading…
'); + $('#glModal').modal('show'); + ajax_request({ + url: server_url + 'accounting/api/engine/get_gl_by_source.php', + autoPrepare: true, checkRequired: 0, noLoading: true, action: 'read', + data: { doc_type: doc_type, source_id: source_id }, + onSuccess: function(res) { + if (!res.output) { $('#gl-modal-body').html('

No GL entry posted for this document.

'); return; } + var h = res.output.header, lines = res.output.lines || []; + var total_debit = 0, total_credit = 0, lines_html = ''; + $.each(lines, function(i, l) { + total_debit += parseFloat(l.debit) || 0; + total_credit += parseFloat(l.credit) || 0; + var dept = l.dept_code ? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : '')) : '—'; + lines_html += '' + escape_html(l.account_code) + '' + escape_html(l.account_name) + '' + dept + '' + + '' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '—') + '' + + '' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '—') + '' + + '' + escape_html(l.description || '—') + ''; + }); + lines_html += 'Total' + format_number(total_debit, 2) + '' + format_number(total_credit, 2) + ''; + var balanced = Math.abs(total_debit - total_credit) < 0.01; + var bal_badge = balanced ? 'Balanced' : 'Unbalanced'; + $('#gl-modal-body').html( + '
' + + '
Source
' + _gl_source_badge(h.source_type) + '
' + + '
Period
' + escape_html(h.period) + '
' + + '
Formula
' + escape_html(h.formula_name || '—') + '
' + + '
Posted At
' + escape_html(h.posted_at) + '
' + + '
Updated At
' + escape_html(h.updated_at_fmt) + '
' + + '
Journal Lines' + bal_badge + '
' + + '' + lines_html + '
Account CodeAccount NameDepartmentDebitCreditDescription
' + ); + } + }); + } + $(async function() { await load_dept_cache(); populate_dept_filter('filter_dept'); @@ -297,5 +341,21 @@ }); + + + diff --git a/app/po/manage_purchase_invoice.php b/app/po/manage_purchase_invoice.php index 45f9b93..44e16d0 100644 --- a/app/po/manage_purchase_invoice.php +++ b/app/po/manage_purchase_invoice.php @@ -364,7 +364,14 @@ invoice_id: invoice_id, due_date: is_dn ? '' : due_date.split('/').reverse().join('-') }, - onSuccess: function() { retrieve_invoice(); } + onSuccess: function(res) { + retrieve_invoice(); + if (res.gl_posted === true) { + toastr.success('GL entry posted automatically.', 'GL Posted'); + } else if (res.gl_posted === false) { + toastr.warning('Document issued, but GL posting failed: ' + (res.gl_message || 'Unknown error'), 'GL Skipped', { timeOut: 8000 }); + } + } }); } }); diff --git a/app/revenue/invoice.php b/app/revenue/invoice.php index 352aa03..f6a09b7 100644 --- a/app/revenue/invoice.php +++ b/app/revenue/invoice.php @@ -355,10 +355,16 @@ ${format_number(inv.grand_total, 2)} ${formula_name} ${invoice_status_badge(inv.status, inv.due_date, inv.payment_state, inv.doc_type)} - - + + + + + + + + `; }); @@ -397,16 +403,93 @@ ${format_number(cn.grand_total, 2)} ${formula_name} ${invoice_status_badge(cn.status, cn.due_date, cn.payment_state, cn.doc_type)} - - + + + + + + + + `; }); $('table#cn_list tbody').html(body); } + function delete_invoice(id, doc_number) { + bootbox.confirm({ + message: 'Delete ' + escape_html(doc_number) + '?
This cannot be undone. The document number will be freed for reuse.', + buttons: { + confirm: { label: 'Delete', className: 'btn-danger' }, + cancel: { label: 'Back', className: 'btn-secondary' } + }, + callback: function(result) { + if (!result) return; + ajax_request({ + url: server_url + 'order/api/engine/delete_invoice.php', + autoPrepare: true, checkRequired: 0, action: 'delete', + data: { id: id }, + onSuccess: function() { retrieve_invoices(); } + }); + } + }); + } + + var _GL_SOURCE_LABELS = { invoice:'Invoice', credit_note:'Credit Note', purchase_invoice:'Purchase Invoice', supplier_credit_note:'Supplier Credit Note', receipt:'Receipt', payment:'Payment', manual:'Manual', reversal:'Reversal' }; + var _GL_SOURCE_BADGE = { invoice:'bg-success-subtle text-success', credit_note:'bg-warning-subtle text-warning', purchase_invoice:'bg-primary-subtle text-primary', supplier_credit_note:'bg-secondary-subtle text-secondary', receipt:'bg-info-subtle text-info', payment:'bg-danger-subtle text-danger', manual:'bg-dark-subtle text-dark', reversal:'bg-danger-subtle text-danger' }; + function _gl_source_badge(t) { return '' + (_GL_SOURCE_LABELS[t]||t) + ''; } + + function show_gl_modal(doc_type, source_id) { + $('#gl-modal-body').html('
Loading…
'); + $('#glModal').modal('show'); + ajax_request({ + url: server_url + 'accounting/api/engine/get_gl_by_source.php', + autoPrepare: true, checkRequired: 0, noLoading: true, action: 'read', + data: { doc_type: doc_type, source_id: source_id }, + onSuccess: function(res) { + if (!res.output) { + $('#gl-modal-body').html('

No GL entry posted for this document.

'); + return; + } + var h = res.output.header, lines = res.output.lines || []; + var total_debit = 0, total_credit = 0, lines_html = ''; + $.each(lines, function(i, l) { + total_debit += parseFloat(l.debit) || 0; + total_credit += parseFloat(l.credit) || 0; + var dept = l.dept_code ? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : '')) : '—'; + lines_html += '' + + '' + escape_html(l.account_code) + '' + + '' + escape_html(l.account_name) + '' + + '' + dept + '' + + '' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '—') + '' + + '' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '—') + '' + + '' + escape_html(l.description || '—') + ''; + }); + lines_html += 'Total' + + '' + format_number(total_debit, 2) + '' + + '' + format_number(total_credit, 2) + ''; + var balanced = Math.abs(total_debit - total_credit) < 0.01; + var bal_badge = balanced ? 'Balanced' : 'Unbalanced'; + $('#gl-modal-body').html( + '
' + + '
Source
' + _gl_source_badge(h.source_type) + '
' + + '
Period
' + escape_html(h.period) + '
' + + '
Formula
' + escape_html(h.formula_name || '—') + '
' + + '
Posted At
' + escape_html(h.posted_at) + '
' + + '
Updated At
' + escape_html(h.updated_at_fmt) + '
' + + '
' + + '
Journal Lines' + bal_badge + '
' + + '' + + '' + + '' + lines_html + '
Account CodeAccount NameDepartmentDebitCreditDescription
' + ); + } + }); + } + $(async function() { try { await load_dept_cache(); @@ -426,5 +509,21 @@ }); + + + diff --git a/app/revenue/manage_invoice.php b/app/revenue/manage_invoice.php index afc2571..9042597 100644 --- a/app/revenue/manage_invoice.php +++ b/app/revenue/manage_invoice.php @@ -337,7 +337,14 @@ id: invoice_id, due_date: due_date ? due_date.split('/').reverse().join('-') : '' }, - onSuccess: function() { retrieve_invoice(); } + onSuccess: function(res) { + retrieve_invoice(); + if (res.gl_posted === true) { + toastr.success('GL entry posted automatically.', 'GL Posted'); + } else if (res.gl_posted === false) { + toastr.warning('Invoice issued, but GL posting failed: ' + (res.gl_message || 'Unknown error'), 'GL Skipped', { timeOut: 8000 }); + } + } }); } });