accounting workflows
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_master_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Account Formulas</h1>
|
||||
<p class="mb-0 text-muted">Posting patterns for document journal entries</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="show_formula_form()">
|
||||
<i class="ti ti-plus me-1"></i>New Formula
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-4" id="account_formula_tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" id="tab_formulas" type="button" onclick="show_account_tab('formulas')">Formulas</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="tab_product_accounts" type="button" onclick="show_account_tab('product_accounts')">Product Accounts</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="row g-5 d-none" id="formula_form">
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-5">
|
||||
<input type="hidden" id="id">
|
||||
<h2 class="fs-5 mb-4">Formula Setup</h2>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Formula Name</label>
|
||||
<input type="text" id="formula_name" class="form-control" placeholder="e.g. Standard Sales Invoice">
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">Document Type</label>
|
||||
<select id="document_type" class="form-select">
|
||||
<option value="sales_invoice">Sales Invoice</option>
|
||||
<option value="sales_credit_note">Sales Credit Note</option>
|
||||
<option value="receipt">Receipt</option>
|
||||
<option value="purchase_invoice">Purchase Invoice</option>
|
||||
<option value="supplier_credit_note">Supplier Credit Note</option>
|
||||
<option value="payment">Payment</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Status</label>
|
||||
<select id="status" class="form-select">
|
||||
<option value="1">Active</option>
|
||||
<option value="0">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea id="description" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="is_default" value="1">
|
||||
<label class="form-check-label" for="is_default">Use as default for this document type</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fs-5 mb-0">Formula Items</h2>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="add_item_row()">
|
||||
<i class="ti ti-plus me-1"></i>Add Item
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:100px;">Side</th>
|
||||
<th>Account</th>
|
||||
<th style="width:150px;">Value</th>
|
||||
<th>Description</th>
|
||||
<th style="width:40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item_tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Available Values</h2>
|
||||
<div class="small d-flex flex-column gap-2">
|
||||
<div><span class="fw-semibold">grand_total</span><div class="text-muted">Final document total</div></div>
|
||||
<div><span class="fw-semibold">total</span><div class="text-muted">Subtotal less discount plus shipping</div></div>
|
||||
<div><span class="fw-semibold">tax</span><div class="text-muted">Document tax amount</div></div>
|
||||
<div><span class="fw-semibold">amount</span><div class="text-muted">Receipt or payment amount</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex flex-column gap-2">
|
||||
<button class="btn btn-primary w-100" onclick="save_formula()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save Formula
|
||||
</button>
|
||||
<button class="btn btn-light w-100" onclick="hide_formula_form()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="formula_list_section">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-8">
|
||||
<input type="search" id="search_input" class="form-control" placeholder="Search formula or document type...">
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<select id="filter_document_type" class="form-select">
|
||||
<option value="">All Document Types</option>
|
||||
<option value="sales_invoice">Sales Invoice</option>
|
||||
<option value="sales_credit_note">Sales Credit Note</option>
|
||||
<option value="receipt">Receipt</option>
|
||||
<option value="purchase_invoice">Purchase Invoice</option>
|
||||
<option value="supplier_credit_note">Supplier Credit Note</option>
|
||||
<option value="payment">Payment</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-centered" id="formula_list">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Document Type</th>
|
||||
<th>Items</th>
|
||||
<th>Default</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row d-none" id="product_account_section">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-8">
|
||||
<input type="search" id="product_account_search" class="form-control" placeholder="Search product, SKU, or account...">
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<select id="product_account_filter" class="form-select">
|
||||
<option value="">All Products</option>
|
||||
<option value="missing">Missing Mapping</option>
|
||||
<option value="mapped">Mapped Sales & Purchase</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 table-centered" id="product_account_list">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Sales Account</th>
|
||||
<th>Purchase Account</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal fade" id="product_account_modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title mb-0">Product Accounts</h5>
|
||||
<div class="small text-muted" id="product_account_modal_product"></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="product_account_product_id">
|
||||
<input type="hidden" id="product_account_sales_code">
|
||||
<input type="hidden" id="product_account_purchase_code">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Sales Account</label>
|
||||
<input type="text" class="form-control account_search" id="product_account_sales"
|
||||
placeholder="Search account..." autocomplete="off">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Purchase Account</label>
|
||||
<input type="text" class="form-control account_search" id="product_account_purchase"
|
||||
placeholder="Search account..." autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="save_product_account_mapping()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save Accounts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var all_accounts = [];
|
||||
var all_formulas = [];
|
||||
var all_products = [];
|
||||
var item_index = 0;
|
||||
|
||||
var doc_labels = {
|
||||
sales_invoice: 'Sales Invoice',
|
||||
sales_credit_note: 'Sales Credit Note',
|
||||
receipt: 'Receipt',
|
||||
purchase_invoice: 'Purchase Invoice',
|
||||
supplier_credit_note: 'Supplier Credit Note',
|
||||
payment: 'Payment',
|
||||
};
|
||||
|
||||
function account_display(account_code) {
|
||||
var account = all_accounts.find(a => String(a.account_code) === String(account_code));
|
||||
return account ? `${account.account_code} - ${account.account_name}` : (account_code || '');
|
||||
}
|
||||
|
||||
function account_badge(account_code) {
|
||||
var label = account_display(account_code);
|
||||
return label ? escape_html(label) : '<span class="badge bg-warning text-dark">Not mapped</span>';
|
||||
}
|
||||
|
||||
function product_status_badge(status) {
|
||||
return String(status) === '1' ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>';
|
||||
}
|
||||
|
||||
function product_account_row(product) {
|
||||
var product_name = product.product_name || product.name || '';
|
||||
return `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(product_name)}</td>
|
||||
<td class="py-3">${escape_html(product.sku || '')}</td>
|
||||
<td class="py-3">${account_badge(product.sales_account_code)}</td>
|
||||
<td class="py-3">${account_badge(product.purchase_account_code)}</td>
|
||||
<td class="py-3">${product_status_badge(product.status)}</td>
|
||||
<td class="py-3">
|
||||
<a href="javascript:;" onclick="open_product_account_modal(${product.id || 0})"><i class="ti ti-pencil fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function set_product_account_modal_field(input_selector, hidden_selector, account_code) {
|
||||
var label = account_display(account_code || '');
|
||||
$(input_selector)
|
||||
.val(label)
|
||||
.attr('data-account-code', account_code || '')
|
||||
.attr('secondary', label);
|
||||
$(hidden_selector).val(account_code || '');
|
||||
}
|
||||
|
||||
function open_product_account_modal(product_id) {
|
||||
var product = all_products.find(function(row) {
|
||||
return String(row.id) === String(product_id);
|
||||
});
|
||||
if (!product) return;
|
||||
|
||||
$('#product_account_product_id').val(product.id || '');
|
||||
$('#product_account_modal_product').text((product.product_name || product.name || '') + ' / ' + (product.sku || ''));
|
||||
set_product_account_modal_field('#product_account_sales', '#product_account_sales_code', product.sales_account_code || '');
|
||||
set_product_account_modal_field('#product_account_purchase', '#product_account_purchase_code', product.purchase_account_code || '');
|
||||
|
||||
if (window.init_account_search_inputs) window.init_account_search_inputs('#product_account_modal', all_accounts);
|
||||
$('#product_account_modal').modal('show');
|
||||
}
|
||||
|
||||
function save_product_account_mapping() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/product_account_mapping.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save',
|
||||
data: {
|
||||
id: $('#product_account_product_id').val(),
|
||||
sales_account_code: $('#product_account_sales_code').val(),
|
||||
purchase_account_code: $('#product_account_purchase_code').val(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
$('#product_account_modal').modal('hide');
|
||||
retrieve_products();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function show_account_tab(tab) {
|
||||
$('#formula_form').addClass('d-none');
|
||||
$('#formula_list_section').toggleClass('d-none', tab !== 'formulas');
|
||||
$('#product_account_section').toggleClass('d-none', tab !== 'product_accounts');
|
||||
$('#tab_formulas').toggleClass('active', tab === 'formulas');
|
||||
$('#tab_product_accounts').toggleClass('active', tab === 'product_accounts');
|
||||
if (tab === 'product_accounts') render_product_account_list();
|
||||
}
|
||||
|
||||
function amount_options(selected) {
|
||||
var keys = ['grand_total', 'total', 'tax', 'amount'];
|
||||
var opts = '';
|
||||
keys.forEach(function(key) {
|
||||
opts += `<option value="${key}" ${selected === key ? 'selected' : ''}>${key}</option>`;
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
|
||||
function add_item_row(item) {
|
||||
item = item || {};
|
||||
var idx = item_index++;
|
||||
var row = `<tr id="formula_item_${idx}">
|
||||
<td>
|
||||
<select class="form-select form-select-sm item_drcr">
|
||||
<option value="D" ${item.drcr === 'D' ? 'selected' : ''}>Debit</option>
|
||||
<option value="C" ${item.drcr === 'C' ? 'selected' : ''}>Credit</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm item_account account_search"
|
||||
value="${escape_html(account_display(item.account_code || ''))}"
|
||||
data-account-code-target=".item_account_code"
|
||||
data-account-code="${escape_html(item.account_code || '')}"
|
||||
${item.account_code ? `secondary="${escape_html(account_display(item.account_code))}"` : ''}
|
||||
placeholder="Search account..." autocomplete="off">
|
||||
<input type="hidden" class="item_account_code" value="${escape_html(item.account_code || '')}">
|
||||
</td>
|
||||
<td><select class="form-select form-select-sm item_amount">${amount_options(item.amount_key || 'grand_total')}</select></td>
|
||||
<td><input type="text" class="form-control form-control-sm item_description" value="${escape_html(item.description || '')}" placeholder="Line memo"></td>
|
||||
<td><a href="javascript:;" class="link-danger" onclick="remove_item(${idx})"><i class="ti ti-trash fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
$('#item_tbody').append(row);
|
||||
if (window.init_account_search_inputs) window.init_account_search_inputs(`#formula_item_${idx}`, all_accounts);
|
||||
}
|
||||
|
||||
function remove_item(idx) {
|
||||
$('#formula_item_' + idx).remove();
|
||||
}
|
||||
|
||||
function show_formula_form() {
|
||||
$('#formula_form').removeClass('d-none');
|
||||
$('#formula_list_section').addClass('d-none');
|
||||
$('#product_account_section').addClass('d-none');
|
||||
$('#tab_formulas').addClass('active');
|
||||
$('#tab_product_accounts').removeClass('active');
|
||||
reset_formula_form();
|
||||
}
|
||||
|
||||
function hide_formula_form() {
|
||||
$('#formula_form').addClass('d-none');
|
||||
show_account_tab('formulas');
|
||||
}
|
||||
|
||||
function reset_formula_form() {
|
||||
$('#id, #formula_name, #description').val('');
|
||||
$('#document_type').val('sales_invoice');
|
||||
$('#status').val('1');
|
||||
$('#is_default').prop('checked', false);
|
||||
$('#item_tbody').empty();
|
||||
item_index = 0;
|
||||
add_item_row({ drcr: 'D', amount_key: 'grand_total' });
|
||||
add_item_row({ drcr: 'C', amount_key: 'grand_total' });
|
||||
}
|
||||
|
||||
function collect_items() {
|
||||
var items = [];
|
||||
$('#item_tbody tr').each(function() {
|
||||
var row = {
|
||||
drcr: $(this).find('.item_drcr').val(),
|
||||
account_code: $(this).find('.item_account_code').val() || $(this).find('.item_account').attr('data-account-code') || '',
|
||||
amount_key: $(this).find('.item_amount').val(),
|
||||
description: $(this).find('.item_description').val(),
|
||||
};
|
||||
if (row.account_code) items.push(row);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function retrieve_accounts() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
all_accounts = res.output || [];
|
||||
window.account_search_accounts = all_accounts;
|
||||
if (window.init_account_search_inputs) window.init_account_search_inputs(document, all_accounts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_formulas() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
all_formulas = res.output || [];
|
||||
render_formula_list();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_products() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>inventory/api/engine/product.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
all_products = res.output || [];
|
||||
render_product_account_list();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_formula_list() {
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var type = $('#filter_document_type').val();
|
||||
var rows = all_formulas.filter(function(f) {
|
||||
var match_kw = !keyword ||
|
||||
String(f.formula_name || '').toLowerCase().includes(keyword) ||
|
||||
String(doc_labels[f.document_type] || f.document_type || '').toLowerCase().includes(keyword);
|
||||
return match_kw && (!type || f.document_type === type);
|
||||
});
|
||||
|
||||
var body = '';
|
||||
if (!rows.length) {
|
||||
$('table#formula_list tbody').html('<tr><td colspan="6" class="text-center py-5 text-muted">No account formulas found.</td></tr>');
|
||||
$('table#formula_list tfoot').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS account_formula_list');
|
||||
alasql.tables.account_formula_list.data = rows;
|
||||
$('table#formula_list tfoot').html(generate_pagination('formula_list', rows.length));
|
||||
|
||||
$.each(rows.slice(0, prop_limit), function(i, f) {
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(f.formula_name || '')}</td>
|
||||
<td class="py-3">${escape_html(doc_labels[f.document_type] || f.document_type || '')}</td>
|
||||
<td class="py-3">${format_number(f.item_count || 0)}</td>
|
||||
<td class="py-3">${String(f.is_default) === '1' ? '<span class="badge bg-primary">Default</span>' : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${String(f.status) === '1' ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
|
||||
<td class="py-3">
|
||||
<a href="javascript:;" onclick="edit_formula(${f.id})"><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger ms-2" onclick="delete_formula(${f.id})"><i class="ti ti-trash fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#formula_list tbody').html(body);
|
||||
}
|
||||
|
||||
function change_page_formula_list(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var rows = alasql(`SELECT * FROM account_formula_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
$.each(rows, function(i, f) {
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(f.formula_name || '')}</td>
|
||||
<td class="py-3">${escape_html(doc_labels[f.document_type] || f.document_type || '')}</td>
|
||||
<td class="py-3">${format_number(f.item_count || 0)}</td>
|
||||
<td class="py-3">${String(f.is_default) === '1' ? '<span class="badge bg-primary">Default</span>' : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${String(f.status) === '1' ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
|
||||
<td class="py-3">
|
||||
<a href="javascript:;" onclick="edit_formula(${f.id})"><i class="ti ti-eye fs-5"></i></a>
|
||||
<a href="javascript:;" class="link-danger ms-2" onclick="delete_formula(${f.id})"><i class="ti ti-trash fs-5"></i></a>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#formula_list tbody').html(body);
|
||||
}
|
||||
|
||||
function render_product_account_list() {
|
||||
var keyword = $('#product_account_search').val().toLowerCase();
|
||||
var filter = $('#product_account_filter').val();
|
||||
var rows = all_products.filter(function(product) {
|
||||
var sales_account = account_display(product.sales_account_code);
|
||||
var purchase_account = account_display(product.purchase_account_code);
|
||||
var has_sales = !!product.sales_account_code;
|
||||
var has_purchase = !!product.purchase_account_code;
|
||||
var match_kw = !keyword ||
|
||||
String(product.product_name || product.name || '').toLowerCase().includes(keyword) ||
|
||||
String(product.sku || '').toLowerCase().includes(keyword) ||
|
||||
String(sales_account || '').toLowerCase().includes(keyword) ||
|
||||
String(purchase_account || '').toLowerCase().includes(keyword);
|
||||
|
||||
if (!match_kw) return false;
|
||||
if (filter === 'missing') return !has_sales || !has_purchase;
|
||||
if (filter === 'mapped') return has_sales && has_purchase;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!rows.length) {
|
||||
$('table#product_account_list tbody').html('<tr><td colspan="6" class="text-center py-5 text-muted">No product account mappings found.</td></tr>');
|
||||
$('table#product_account_list tfoot').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS product_account_list');
|
||||
alasql.tables.product_account_list.data = rows;
|
||||
$('table#product_account_list tfoot').html(generate_pagination('product_account_list', rows.length));
|
||||
|
||||
var body = '';
|
||||
$.each(rows.slice(0, prop_limit), function(i, product) {
|
||||
body += product_account_row(product);
|
||||
});
|
||||
$('table#product_account_list tbody').html(body);
|
||||
}
|
||||
|
||||
function change_page_product_account_list(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var rows = alasql(`SELECT * FROM product_account_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
$.each(rows, function(i, product) {
|
||||
body += product_account_row(product);
|
||||
});
|
||||
$('table#product_account_list tbody').html(body);
|
||||
}
|
||||
|
||||
function edit_formula(id) {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'retrieve',
|
||||
data: { id: id },
|
||||
onSuccess: function(res) {
|
||||
var f = res.output || {};
|
||||
$('#formula_form').removeClass('d-none');
|
||||
$('#formula_list_section').addClass('d-none');
|
||||
$('#product_account_section').addClass('d-none');
|
||||
$('#tab_formulas').addClass('active');
|
||||
$('#tab_product_accounts').removeClass('active');
|
||||
$('#id').val(f.id || '');
|
||||
$('#formula_name').val(f.formula_name || '');
|
||||
$('#document_type').val(f.document_type || 'sales_invoice');
|
||||
$('#description').val(f.description || '');
|
||||
$('#status').val(String(f.status || 1));
|
||||
$('#is_default').prop('checked', String(f.is_default) === '1');
|
||||
$('#item_tbody').empty();
|
||||
item_index = 0;
|
||||
$.each(f.items || [], function(i, item) { add_item_row(item); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save_formula() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save',
|
||||
data: {
|
||||
id: $('#id').val(),
|
||||
formula_name: $('#formula_name').val(),
|
||||
document_type: $('#document_type').val(),
|
||||
description: $('#description').val(),
|
||||
is_default: $('#is_default').is(':checked') ? 1 : 0,
|
||||
status: $('#status').val(),
|
||||
items: JSON.stringify(collect_items()),
|
||||
},
|
||||
onSuccess: function() {
|
||||
hide_formula_form();
|
||||
retrieve_formulas();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delete_formula(id) {
|
||||
bootbox.confirm('Disable this account formula?', function(ok) {
|
||||
if (!ok) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'delete',
|
||||
data: { id: id },
|
||||
onSuccess: retrieve_formulas
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function() {
|
||||
retrieve_accounts().then(function() {
|
||||
retrieve_formulas();
|
||||
retrieve_products();
|
||||
});
|
||||
$('#search_input').on('input', render_formula_list);
|
||||
$('#filter_document_type').on('change', render_formula_list);
|
||||
$('#product_account_search').on('input', render_product_account_list);
|
||||
$('#product_account_filter').on('change', render_product_account_list);
|
||||
$('#product_account_sales').on('account:selected', function(e, account) {
|
||||
$('#product_account_sales_code').val(account.account_code || '');
|
||||
}).on('account:cleared', function() {
|
||||
$('#product_account_sales_code').val('');
|
||||
});
|
||||
$('#product_account_purchase').on('account:selected', function(e, account) {
|
||||
$('#product_account_purchase_code').val(account.account_code || '');
|
||||
}).on('account:cleared', function() {
|
||||
$('#product_account_purchase_code').val('');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes_ac/AccountFormulaManager.php';
|
||||
|
||||
try {
|
||||
$mgr = new AccountFormulaManager($pdo2, $company_id);
|
||||
$action = $data['action'] ?? ($_POST['action'] ?? 'read');
|
||||
|
||||
if ($action === 'get_by_type') {
|
||||
$doc_type = trim((string)($data['document_type'] ?? ''));
|
||||
if (!$doc_type) throw new Exception('document_type is required.');
|
||||
$answer['output'] = $mgr->getByType($doc_type);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($action === 'retrieve') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$row = $mgr->getById($id);
|
||||
if (!$row) throw new Exception('Formula not found.');
|
||||
$answer['output'] = $row;
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($action === 'save') {
|
||||
$new_id = null;
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, &$new_id) {
|
||||
$mgr = new AccountFormulaManager($pdo, $company_id);
|
||||
$new_id = $mgr->save($data);
|
||||
});
|
||||
$answer['new_id'] = $new_id;
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Formula saved.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
if ($id <= 0) throw new Exception('Missing formula id.');
|
||||
$mgr->delete($id);
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Formula disabled.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$answer['output'] = $mgr->getAll();
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/OperationLockManager.php';
|
||||
|
||||
try {
|
||||
$lock_manager = new OperationLockManager($pdo2, $company_id, $user_id);
|
||||
$result = $lock_manager->acquire(
|
||||
trim((string)($data['operation_type'] ?? '')),
|
||||
(int)($data['ttl_minutes'] ?? 120)
|
||||
);
|
||||
$answer = array_merge($answer, $result);
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/FinancialReports.php';
|
||||
|
||||
$as_of_date = trim((string)($data['as_of_date'] ?? ($data['as_of_period'] ?? date('Y-m-d'))));
|
||||
$dept_id = (int)($data['department_id'] ?? 0);
|
||||
|
||||
$reports = new FinancialReports($pdo2, $company_id);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $reports->getBalanceSheet($as_of_date, $dept_id);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/GlQueryManager.php';
|
||||
|
||||
try {
|
||||
$gl_query = new GlQueryManager($pdo2, $company_id);
|
||||
$answer['output'] = $gl_query->getPostableDocuments(
|
||||
trim((string)($data['doc_type'] ?? '')),
|
||||
trim((string)($data['date_from'] ?? '')),
|
||||
trim((string)($data['date_to'] ?? '')),
|
||||
(int)($data['formula_id'] ?? 0)
|
||||
);
|
||||
$answer['success'] = 1;
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/FinancialReports.php';
|
||||
|
||||
$account_code = trim((string)($data['account_code'] ?? ''));
|
||||
$from_date = trim((string)($data['from_date'] ?? ($data['from_period'] ?? date('Y-m-d'))));
|
||||
$to_date = trim((string)($data['to_date'] ?? ($data['to_period'] ?? $from_date)));
|
||||
$dept_id = (int)($data['department_id'] ?? 0);
|
||||
|
||||
if ($account_code === '') {
|
||||
$answer['message'] = 'account_code is required.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$reports = new FinancialReports($pdo2, $company_id);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $reports->getGLMovement($account_code, $from_date, $to_date, $dept_id);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/GlQueryManager.php';
|
||||
|
||||
try {
|
||||
$gl_query = new GlQueryManager($pdo2, $company_id);
|
||||
$answer['output'] = $gl_query->getJournalDetail((int)($data['gl_id'] ?? 0));
|
||||
$answer['success'] = 1;
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/GlQueryManager.php';
|
||||
|
||||
try {
|
||||
$gl_query = new GlQueryManager($pdo2, $company_id);
|
||||
$answer['output'] = $gl_query->getJournalListing(
|
||||
trim((string)($data['source_type'] ?? '')),
|
||||
trim((string)($data['date_from'] ?? '')),
|
||||
trim((string)($data['date_to'] ?? ''))
|
||||
);
|
||||
$answer['success'] = 1;
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/FinancialReports.php';
|
||||
|
||||
$from_date = trim((string)($data['from_date'] ?? ($data['from_period'] ?? date('Y-m-d'))));
|
||||
$to_date = trim((string)($data['to_date'] ?? ($data['to_period'] ?? $from_date)));
|
||||
$dept_id = (int)($data['department_id'] ?? 0);
|
||||
|
||||
$reports = new FinancialReports($pdo2, $company_id);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $reports->getProfitAndLoss($from_date, $to_date, $dept_id);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/FinancialReports.php';
|
||||
|
||||
$from_date = trim((string)($data['from_date'] ?? ($data['from_period'] ?? date('Y-m-d'))));
|
||||
$to_date = trim((string)($data['to_date'] ?? ($data['to_period'] ?? $from_date)));
|
||||
$dept_id = (int)($data['department_id'] ?? 0);
|
||||
|
||||
$reports = new FinancialReports($pdo2, $company_id);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $reports->getTrialBalance($from_date, $to_date, $dept_id);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/TaxReportManager.php';
|
||||
|
||||
$from_date = trim((string)($data['from_date'] ?? ($data['from_period'] ?? date('Y-m-d'))));
|
||||
$to_date = trim((string)($data['to_date'] ?? ($data['to_period'] ?? $from_date)));
|
||||
$dept_id = (int)($data['department_id'] ?? 0);
|
||||
$categories = $data['categories'] ?? [];
|
||||
if (is_string($categories)) {
|
||||
$decoded = json_decode($categories, true);
|
||||
$categories = is_array($decoded) ? $decoded : [$categories];
|
||||
}
|
||||
if (!is_array($categories)) $categories = [];
|
||||
|
||||
$tax = new TaxReportManager($pdo2, $company_id);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $tax->getVATReport($from_date, $to_date, $categories, $dept_id);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/BatchActionManager.php';
|
||||
|
||||
try {
|
||||
$batch_action = new BatchActionManager($pdo2, $company_id, $user_id);
|
||||
$batch_action->log($data);
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Batch action logged.';
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/PeriodManager.php';
|
||||
|
||||
$pm = new PeriodManager($pdo2, $company_id);
|
||||
$action = $data['action_type'] ?? '';
|
||||
|
||||
if ($action === 'create_year') {
|
||||
$year = (int)($data['year'] ?? date('Y'));
|
||||
if ($year < 2000 || $year > 2100) exit(json_encode(['message' => 'Invalid year']));
|
||||
$pm->createYear($year);
|
||||
$answer['message'] = "All 12 periods for {$year} created";
|
||||
|
||||
} elseif ($action === 'create_single') {
|
||||
$period = $data['period'] ?? '';
|
||||
if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period)) {
|
||||
exit(json_encode(['message' => 'Invalid period format (YYYY-MM)']));
|
||||
}
|
||||
$pm->create($period);
|
||||
$answer['message'] = "Period {$period} created";
|
||||
|
||||
} elseif ($action === 'open') {
|
||||
$pm->open((int)($data['id'] ?? 0));
|
||||
$answer['message'] = 'Period reopened';
|
||||
|
||||
} elseif ($action === 'close') {
|
||||
$pm->close((int)($data['id'] ?? 0));
|
||||
$answer['message'] = 'Period closed';
|
||||
|
||||
} elseif ($action === 'lock') {
|
||||
$pm->lock((int)($data['id'] ?? 0));
|
||||
$answer['message'] = 'Period locked';
|
||||
|
||||
} else {
|
||||
exit(json_encode(['message' => 'Unknown action']));
|
||||
}
|
||||
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/PeriodManager.php';
|
||||
|
||||
$pm = new PeriodManager($pdo2, $company_id);
|
||||
$answer['output'] = $pm->getAll();
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/PeriodManager.php';
|
||||
|
||||
$pm = new PeriodManager($pdo2, $company_id);
|
||||
$stats = $pm->getStats();
|
||||
$answer = array_merge(['success' => 1], $stats);
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/PostingWindowGuard.php';
|
||||
require '../../../assets/utils/classes_ac/GlManager.php';
|
||||
require '../../../assets/utils/classes_ac/posting/BasePosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/InvoicePosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/CreditNotePosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/PurchaseInvoicePosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/SupplierCreditNotePosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/ReceiptPosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/PaymentPosting.php';
|
||||
require '../../../assets/utils/classes_ac/posting/PurchasePosting.php';
|
||||
|
||||
$doc_type = trim((string)($data['doc_type'] ?? ''));
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$formula_id = (int)($data['formula_id'] ?? 0) ?: null;
|
||||
|
||||
$posting_map = [
|
||||
'invoice' => InvoicePosting::class,
|
||||
'credit_note' => CreditNotePosting::class,
|
||||
'purchase_invoice' => PurchaseInvoicePosting::class,
|
||||
'supplier_credit_note' => SupplierCreditNotePosting::class,
|
||||
'receipt' => ReceiptPosting::class,
|
||||
'payment' => PaymentPosting::class,
|
||||
'purchase_order' => PurchasePosting::class,
|
||||
];
|
||||
|
||||
if (!isset($posting_map[$doc_type]) || $id <= 0) {
|
||||
$answer['message'] = 'Invalid doc_type or id.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$posting_class = $posting_map[$doc_type];
|
||||
$posting = new $posting_class($pdo2, $company_id);
|
||||
$built = $posting->build($id, $formula_id);
|
||||
|
||||
$guard = new PostingWindowGuard($pdo1, $company_id);
|
||||
$gl = new GlManager($pdo2, $company_id, $guard);
|
||||
$meta = ['journal_date' => $built['doc_date'] ?? null];
|
||||
|
||||
$pdo2->beginTransaction();
|
||||
|
||||
$existing = $gl->getBySource($doc_type, $id);
|
||||
if ($existing) {
|
||||
$gl->replace($doc_type, $id, $built['formula_id'], $built['period'], $built['lines'], $meta);
|
||||
} else {
|
||||
$gl->post($doc_type, $id, $built['formula_id'], $built['period'], $built['lines'], $meta);
|
||||
}
|
||||
|
||||
$pdo2->commit();
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = $existing ? 'GL entry replaced.' : 'GL entry posted.';
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo2->inTransaction()) $pdo2->rollBack();
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||
require_role($user_role, ['owner', 'admin']);
|
||||
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
|
||||
if ($data['action'] === 'read') {
|
||||
$open_from = $csm->get('posting_open_from');
|
||||
if ($open_from === null || $open_from === '') {
|
||||
$open_from = $csm->get('gl_open_from') ?: '';
|
||||
}
|
||||
|
||||
$open_to = $csm->get('posting_open_to');
|
||||
if ($open_to === null || $open_to === '') {
|
||||
$open_to = $csm->get('gl_open_to') ?: '';
|
||||
}
|
||||
|
||||
$answer['output'] = [
|
||||
'open_from' => $open_from ?: '',
|
||||
'open_to' => $open_to ?: '',
|
||||
];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($data['action'] === 'save') {
|
||||
$from = trim((string)($data['open_from'] ?? ''));
|
||||
$to = trim((string)($data['open_to'] ?? ''));
|
||||
|
||||
if ($from !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
|
||||
$answer['message'] = 'Invalid open_from date. Use YYYY-MM-DD.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
if ($to !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
|
||||
$answer['message'] = 'Invalid open_to date. Use YYYY-MM-DD.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
if ($from && $to && $from > $to) {
|
||||
$answer['message'] = 'Open From must be on or before Open To.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$csm->set('posting_open_from', $from);
|
||||
$csm->set('posting_open_to', $to);
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Posting window saved.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$answer['message'] = 'Invalid action.';
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ProductManager.php';
|
||||
require '../../../assets/utils/classes_ac/ChartOfAccounts.php';
|
||||
|
||||
require_role($user_role, ['owner', 'admin']);
|
||||
|
||||
try {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
throw new Exception('Product is required.');
|
||||
}
|
||||
|
||||
$sales_account_code = trim((string)($data['sales_account_code'] ?? '')) ?: null;
|
||||
$purchase_account_code = trim((string)($data['purchase_account_code'] ?? '')) ?: null;
|
||||
|
||||
$coa = new ChartOfAccounts($pdo2, $company_id);
|
||||
foreach ([
|
||||
'Sales account' => $sales_account_code,
|
||||
'Purchase account' => $purchase_account_code,
|
||||
] as $label => $account_code) {
|
||||
if ($account_code !== null && !$coa->isPostingAccount($account_code)) {
|
||||
throw new Exception($label . ' must be an active posting account.');
|
||||
}
|
||||
}
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($company_id, $id, $sales_account_code, $purchase_account_code, $logging) {
|
||||
$product = new ProductManager($pdo, $company_id);
|
||||
$product->updateAccountMapping($id, $sales_account_code, $purchase_account_code, $logging);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer['success'] = 0;
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/OperationLockManager.php';
|
||||
|
||||
try {
|
||||
$lock_manager = new OperationLockManager($pdo2, $company_id, $user_id);
|
||||
$lock_manager->release(trim((string)($data['operation_type'] ?? '')));
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Lock released.';
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes_ac/PostingWindowGuard.php';
|
||||
require '../../../assets/utils/classes_ac/GlManager.php';
|
||||
|
||||
$gl_id = (int)($data['gl_id'] ?? 0);
|
||||
$reference = trim((string)($data['reference'] ?? ''));
|
||||
$description = trim((string)($data['description'] ?? ''));
|
||||
$journal_date = trim((string)($data['journal_date'] ?? ''));
|
||||
$lines_raw = $data['lines'] ?? [];
|
||||
|
||||
if (is_string($lines_raw)) {
|
||||
$lines_raw = json_decode($lines_raw, true) ?: [];
|
||||
}
|
||||
|
||||
// Convert d/m/Y → Y-m-d
|
||||
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $journal_date, $m)) {
|
||||
$journal_date = "{$m[3]}-{$m[2]}-{$m[1]}";
|
||||
}
|
||||
|
||||
if (!$journal_date || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $journal_date)) {
|
||||
$answer['message'] = 'Valid journal date is required.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$period = substr($journal_date, 0, 7); // YYYY-MM
|
||||
|
||||
// Validate and normalise lines
|
||||
$lines = [];
|
||||
$total_debit = 0;
|
||||
$total_credit = 0;
|
||||
|
||||
foreach ($lines_raw as $l) {
|
||||
$account_code = trim((string)($l['account_code'] ?? ''));
|
||||
$debit = (float)($l['debit'] ?? 0);
|
||||
$credit = (float)($l['credit'] ?? 0);
|
||||
$desc = trim((string)($l['description'] ?? ''));
|
||||
|
||||
if ($account_code === '') continue;
|
||||
if ($debit == 0 && $credit == 0) continue;
|
||||
|
||||
$lines[] = ['account_code' => $account_code, 'department_id' => 0, 'debit' => $debit, 'credit' => $credit, 'description' => $desc];
|
||||
$total_debit += $debit;
|
||||
$total_credit += $credit;
|
||||
}
|
||||
|
||||
if (count($lines) < 2) {
|
||||
$answer['message'] = 'At least two journal lines are required.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if (abs($total_debit - $total_credit) > 0.005) {
|
||||
$answer['message'] = 'Journal is not balanced. Debit ' . number_format($total_debit, 2) . ' ≠ Credit ' . number_format($total_credit, 2) . '.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo2->beginTransaction();
|
||||
$guard = new PostingWindowGuard($pdo1, $company_id);
|
||||
$gl = new GlManager($pdo2, $company_id, $guard);
|
||||
|
||||
if ($gl_id > 0) {
|
||||
$gl->replaceManual($gl_id, $reference, $description, $journal_date, $period, $lines);
|
||||
$answer['message'] = 'Journal entry updated.';
|
||||
} else {
|
||||
$gl_id = $gl->postManual($reference, $description, $journal_date, $period, $lines);
|
||||
$answer['message'] = 'Journal entry saved.';
|
||||
}
|
||||
|
||||
$pdo2->commit();
|
||||
$answer['success'] = 1;
|
||||
$answer['gl_id'] = $gl_id;
|
||||
} catch (Exception $e) {
|
||||
if ($pdo2->inTransaction()) $pdo2->rollBack();
|
||||
$answer['message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
@@ -11,19 +11,42 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">Balance Sheet</h1>
|
||||
<p class="mb-0 text-muted">Assets, liabilities, and equity</p>
|
||||
<p class="mb-0 text-muted">Cumulative asset, liability, and equity positions as of a date</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-table fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">Balance Sheet coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">As Of Date</label>
|
||||
<input type="text" id="f_as_of" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Department</label>
|
||||
<select id="f_department" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="0">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_report()">
|
||||
<i class="ti ti-search me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="ti ti-printer me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4" id="bs_body">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">Select a date and click Run.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,5 +55,117 @@
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var now = new Date();
|
||||
document.getElementById('f_as_of').value =
|
||||
now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
|
||||
flatpickr('#f_as_of', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
load_departments();
|
||||
|
||||
function load_departments() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/department.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var html = '<option value="0">All Departments</option>';
|
||||
(res.output || []).filter(function(d){ return String(d.status) === '1'; }).forEach(function(d) {
|
||||
html += '<option value="' + d.id + '">' + escape_html(d.dept_code + ' - ' + d.dept_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_department').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_report() {
|
||||
var as_of = document.getElementById('f_as_of').value;
|
||||
var dept = document.getElementById('f_department').value || '0';
|
||||
if (!as_of) { alert('Select a date.'); return; }
|
||||
|
||||
document.getElementById('bs_body').innerHTML =
|
||||
'<div class="col-12"><div class="card p-5 text-center text-muted"><span class="spinner-border spinner-border-sm me-2"></span>Loading...</div></div>';
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_balance_sheet.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
as_of_date: as_of,
|
||||
department_id: dept,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var rows = d.rows || [];
|
||||
|
||||
var assets = rows.filter(function(r){ return r.account_type === 'asset'; });
|
||||
var liabilities = rows.filter(function(r){ return r.account_type === 'liability'; });
|
||||
var equity = rows.filter(function(r){ return r.account_type === 'equity'; });
|
||||
|
||||
function sum_net(items, type) {
|
||||
return items.reduce(function(s, r) {
|
||||
var net = type === 'asset'
|
||||
? (parseFloat(r.total_debit) - parseFloat(r.total_credit))
|
||||
: (parseFloat(r.total_credit) - parseFloat(r.total_debit));
|
||||
return s + net;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function table_rows(items, type) {
|
||||
if (items.length === 0) return '<tr><td colspan="2" class="text-muted small ps-4">No entries.</td></tr>';
|
||||
return items.map(function(r) {
|
||||
var net = type === 'asset'
|
||||
? (parseFloat(r.total_debit) - parseFloat(r.total_credit))
|
||||
: (parseFloat(r.total_credit) - parseFloat(r.total_debit));
|
||||
return '<tr>' +
|
||||
'<td class="ps-3"><span class="text-muted small me-2">' + escape_html(r.account_code) + '</span>' +
|
||||
escape_html(r.account_name) + '</td>' +
|
||||
'<td class="text-end">' + format_number(net) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
var total_assets = sum_net(assets, 'asset');
|
||||
var total_liab = sum_net(liabilities, 'liability');
|
||||
var total_equity = sum_net(equity, 'equity') + (parseFloat(d.retained_earnings) || 0);
|
||||
|
||||
function section_card(title, items, type, total, extra_rows) {
|
||||
return '<div class="card">' +
|
||||
'<div class="card-body p-0">' +
|
||||
'<table class="table mb-0">' +
|
||||
'<thead class="table-primary border-light"><tr><th>' + title + '</th><th class="text-end">Amount</th></tr></thead>' +
|
||||
'<tbody>' +
|
||||
table_rows(items, type) +
|
||||
(extra_rows || '') +
|
||||
'<tr class="fw-semibold border-top">' +
|
||||
'<td class="text-end py-2">Total ' + title + '</td>' +
|
||||
'<td class="text-end py-2">' + format_number(total) + '</td>' +
|
||||
'</tr>' +
|
||||
'</tbody>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
var retained_row = '<tr class="table-light">' +
|
||||
'<td class="ps-3"><span class="text-muted small me-2">—</span>Retained Earnings (Cumulative P&L)</td>' +
|
||||
'<td class="text-end">' + format_number(parseFloat(d.retained_earnings) || 0) + '</td>' +
|
||||
'</tr>';
|
||||
|
||||
document.getElementById('bs_body').innerHTML =
|
||||
'<div class="col-12">' + section_card('Assets', assets, 'asset', total_assets, '') + '</div>' +
|
||||
'<div class="col-md-6">' + section_card('Liabilities', liabilities, 'liability', total_liab, '') + '</div>' +
|
||||
'<div class="col-md-6">' + section_card('Equity', equity, 'equity', total_equity, retained_row) + '</div>' +
|
||||
'<div class="col-12"><div class="card p-3 border-2 fw-bold d-flex flex-row justify-content-between align-items-center">' +
|
||||
'<span>Total Liabilities + Equity</span>' +
|
||||
'<span>' + format_number(total_liab + total_equity) + '</span>' +
|
||||
'</div></div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
<th>Code</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Level</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
@@ -154,6 +155,19 @@
|
||||
expense: '<span class="badge bg-warning bg-opacity-10 text-warning">Expense</span>',
|
||||
};
|
||||
|
||||
var category_labels = {
|
||||
ar: 'Account Receivable',
|
||||
ap: 'Account Payable',
|
||||
sales_tax: 'Sales Tax',
|
||||
purchase_tax: 'Purchase Tax',
|
||||
};
|
||||
|
||||
function category_badge(cat) {
|
||||
if (!cat) return '<span class="text-muted small">—</span>';
|
||||
var label = category_labels[cat] || cat;
|
||||
return '<span class="badge bg-secondary bg-opacity-10 text-secondary">' + escape_html(label) + '</span>';
|
||||
}
|
||||
|
||||
var level_badge = {
|
||||
item: '<span class="badge bg-primary">Item</span>',
|
||||
parent: '<span class="badge bg-secondary">Parent</span>',
|
||||
@@ -166,8 +180,8 @@
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
alasql('CREATE TABLE IF NOT EXISTS ac_account');
|
||||
alasql.tables.ac_account.data = res.output || [];
|
||||
alasql('CREATE TABLE IF NOT EXISTS md_account');
|
||||
alasql.tables.md_account.data = res.output || [];
|
||||
change_page(1);
|
||||
}
|
||||
});
|
||||
@@ -182,14 +196,14 @@
|
||||
if (active_type) conditions.push(`account_type = '${active_type}'`);
|
||||
|
||||
var where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '';
|
||||
var filtered = alasql(`SELECT * FROM ac_account ${where} ORDER BY account_code`);
|
||||
var filtered = alasql(`SELECT * FROM md_account ${where} ORDER BY account_code`);
|
||||
|
||||
var page = generate_pagination('ac_account', filtered.length);
|
||||
var page = generate_pagination('account_table', filtered.length);
|
||||
$('table#account_table tfoot').html(page);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
$('table#account_table tbody').html(
|
||||
'<tr><td colspan="7" class="text-center py-5 text-muted">No accounts found.</td></tr>'
|
||||
'<tr><td colspan="8" class="text-center py-5 text-muted">No accounts found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -204,6 +218,7 @@
|
||||
<td class="py-3 fw-semibold ${indent}">${prefix}${escape_html(item.account_code)}</td>
|
||||
<td class="py-3">${escape_html(item.account_name)}</td>
|
||||
<td class="py-3">${type_badge[item.account_type] || item.account_type}</td>
|
||||
<td class="py-3">${category_badge(item.account_category)}</td>
|
||||
<td class="py-3">${is_item ? level_badge.item : level_badge.parent}</td>
|
||||
<td class="py-3">${item.status ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-secondary">Inactive</span>'}</td>
|
||||
<td class="py-3">
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
alasql('CREATE TABLE IF NOT EXISTS ac_department');
|
||||
alasql.tables.ac_department.data = res.output || [];
|
||||
alasql('CREATE TABLE IF NOT EXISTS md_department');
|
||||
alasql.tables.md_department.data = res.output || [];
|
||||
change_page(1);
|
||||
}
|
||||
});
|
||||
@@ -140,10 +140,10 @@
|
||||
var kw = search_term.toLowerCase();
|
||||
|
||||
var filtered = kw
|
||||
? alasql(`SELECT * FROM ac_department WHERE LOWER(dept_code) LIKE '%${kw}%' OR LOWER(dept_name) LIKE '%${kw}%' ORDER BY dept_code`)
|
||||
: alasql(`SELECT * FROM ac_department ORDER BY dept_code`);
|
||||
? alasql(`SELECT * FROM md_department WHERE LOWER(dept_code) LIKE '%${kw}%' OR LOWER(dept_name) LIKE '%${kw}%' ORDER BY dept_code`)
|
||||
: alasql(`SELECT * FROM md_department ORDER BY dept_code`);
|
||||
|
||||
$('table#dept_table tfoot').html(generate_pagination('ac_department', filtered.length));
|
||||
$('table#dept_table tfoot').html(generate_pagination('dept_table', filtered.length));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
$('table#dept_table tbody').html('<tr><td colspan="5" class="text-center py-5 text-muted">No departments found.</td></tr>');
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar_ac.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">Batch GL Entries</h1>
|
||||
<p class="mb-0 text-muted">Create or replace general ledger entries for a date range</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs mb-4" id="glTabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#tab-invoice">Invoice</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-credit-note">Credit Note</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-purchase-invoice">Purchase Invoice</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-supplier-credit-note">Supplier Credit Note</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-receipt">Receipt</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tab-payment">Payment</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tab-invoice">
|
||||
<?php echo gl_tab_html('invoice', 'Invoice'); ?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-credit-note">
|
||||
<?php echo gl_tab_html('credit_note', 'Credit Note'); ?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-purchase-invoice">
|
||||
<?php echo gl_tab_html('purchase_invoice', 'Purchase Invoice'); ?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-supplier-credit-note">
|
||||
<?php echo gl_tab_html('supplier_credit_note', 'Supplier Credit Note'); ?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-receipt">
|
||||
<?php echo gl_tab_html('receipt', 'Receipt'); ?>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tab-payment">
|
||||
<?php echo gl_tab_html('payment', 'Payment'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<!-- Batch processing overlay -->
|
||||
<div id="batch-overlay" style="display:none; position:fixed; inset:0; z-index:9999;
|
||||
background:rgba(0,0,0,0.55); align-items:center; justify-content:center;">
|
||||
<div class="card shadow-lg p-4 text-center" style="min-width:380px; max-width:480px; width:90%;">
|
||||
|
||||
<div class="spinner-border text-primary mx-auto mb-3" role="status" style="width:2.5rem; height:2.5rem;"></div>
|
||||
|
||||
<div class="fw-semibold fs-6 mb-1" id="batch-overlay-title">Processing…</div>
|
||||
<div class="text-muted small mb-4" id="batch-overlay-sub"></div>
|
||||
|
||||
<!-- Large percentage number -->
|
||||
<div id="batch-overlay-pct"
|
||||
style="font-size:3rem; font-weight:700; line-height:1; letter-spacing:-1px; color:#0d6efd;">
|
||||
0%
|
||||
</div>
|
||||
<div class="text-muted small mb-3" id="batch-overlay-count">0 / 0 documents</div>
|
||||
|
||||
<!-- Progress bar (pure visual fill, no text inside) -->
|
||||
<div class="progress mb-2" style="height:10px; border-radius:6px;">
|
||||
<div id="batch-overlay-bar"
|
||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width:0%; border-radius:6px;"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small" id="batch-overlay-eta"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// (batch_process lives in custom.js)
|
||||
|
||||
var TAB_MAP = {
|
||||
'invoice': 'invoice',
|
||||
'credit_note': 'credit_note',
|
||||
'purchase_invoice': 'purchase_invoice',
|
||||
'supplier_credit_note': 'supplier_credit_note',
|
||||
'receipt': 'receipt',
|
||||
'payment': 'payment'
|
||||
};
|
||||
|
||||
// Stores the last loaded IDs per tab so Run doesn't need to re-fetch
|
||||
var _gl_loaded = {};
|
||||
// Stores full row data per tab for client-side pagination
|
||||
var _gl_rows = {};
|
||||
// Stores failed items from the last batch run per tab (for Retry Failed)
|
||||
var _gl_failed = {};
|
||||
|
||||
// ── init tab once ─────────────────────────────────────────────────────────
|
||||
var _inited = {};
|
||||
function init_gl_tab(doc_type) {
|
||||
if (_inited[doc_type]) return;
|
||||
_inited[doc_type] = true;
|
||||
load_formula_options('gl-formula-' + doc_type, doc_type, null);
|
||||
|
||||
var today = new Date();
|
||||
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var fmt = function(d) {
|
||||
return ('0' + d.getDate()).slice(-2) + '/' +
|
||||
('0' + (d.getMonth() + 1)).slice(-2) + '/' +
|
||||
d.getFullYear();
|
||||
};
|
||||
|
||||
flatpickr('#gl-date-from-' + doc_type, {
|
||||
dateFormat: 'd/m/Y', allowInput: true, defaultDate: firstDay
|
||||
});
|
||||
flatpickr('#gl-date-to-' + doc_type, {
|
||||
dateFormat: 'd/m/Y', allowInput: true, defaultDate: today
|
||||
});
|
||||
$('#gl-date-from-' + doc_type).val(fmt(firstDay));
|
||||
$('#gl-date-to-' + doc_type).val(fmt(today));
|
||||
}
|
||||
|
||||
// ── load documents into table ─────────────────────────────────────────────
|
||||
function load_gl_tab(doc_type) {
|
||||
var formula_id = $('#gl-formula-' + doc_type).val();
|
||||
if (!formula_id) { alert('Please select a GL formula first.'); return; }
|
||||
|
||||
var $tbody = $('#gl-tbody-' + doc_type);
|
||||
var $badge = $('#gl-badge-' + doc_type);
|
||||
|
||||
_gl_loaded[doc_type] = [];
|
||||
set_btn_state('#gl-btn-run-' + doc_type, false);
|
||||
$badge.text('');
|
||||
$tbody.html('<tr><td colspan="6" class="text-center text-muted py-4">'
|
||||
+ '<i class="ti ti-loader ti-spin me-1"></i>Loading…</td></tr>');
|
||||
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/get_gl_documents.php',
|
||||
action: 'get',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'get',
|
||||
doc_type: doc_type,
|
||||
date_from: $('#gl-date-from-' + doc_type).val(),
|
||||
date_to: $('#gl-date-to-' + doc_type).val(),
|
||||
formula_id: formula_id
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
var rows = res.output || [];
|
||||
var table_id = 'gl-table-' + doc_type; // HTML element id (hyphens ok)
|
||||
var ala_id = 'gl_table_' + doc_type; // alasql identifier (underscores only)
|
||||
|
||||
if (rows.length === 0) {
|
||||
$tbody.html('<tr><td colspan="6" class="text-center text-muted py-4">No documents found.</td></tr>');
|
||||
$('#' + table_id + ' tfoot').empty();
|
||||
$badge.text('0 documents');
|
||||
return;
|
||||
}
|
||||
|
||||
_gl_loaded[doc_type] = rows.map(function(r) { return r.id; });
|
||||
_gl_rows[doc_type] = rows;
|
||||
|
||||
// Register with alasql for client-side pagination
|
||||
alasql('CREATE TABLE IF NOT EXISTS ' + ala_id);
|
||||
alasql.tables[ala_id].data = rows;
|
||||
|
||||
var unposted = rows.filter(function(r) { return r.gl_status == 0; }).length;
|
||||
var posted = rows.length - unposted;
|
||||
var missing_mapping = rows.filter(function(r) {
|
||||
return Number(r.product_mapping_missing || 0) > 0;
|
||||
}).length;
|
||||
$badge.html(
|
||||
rows.length + ' document(s) · '
|
||||
+ '<span class="text-danger">' + unposted + ' unposted</span>'
|
||||
+ ' · '
|
||||
+ '<span class="text-success">' + posted + ' posted</span>'
|
||||
+ (missing_mapping > 0
|
||||
? ' · <span class="text-warning">' + missing_mapping + ' missing product account mapping</span>'
|
||||
: '')
|
||||
);
|
||||
|
||||
// Wire up dynamic change_page function keyed by HTML table id
|
||||
window['change_page_' + table_id] = function(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var page_rows = alasql('SELECT * FROM ' + ala_id + ' LIMIT ' + prop_limit + ' OFFSET ' + offset);
|
||||
var html = '';
|
||||
$.each(page_rows, function(i, r) {
|
||||
var status_badge = r.gl_status == 1
|
||||
? '<span class="badge bg-success-subtle text-success">Posted</span>'
|
||||
: '<span class="badge bg-danger-subtle text-danger">Unposted</span>';
|
||||
var mapping_badge = product_mapping_badge(r);
|
||||
html +=
|
||||
'<tr>' +
|
||||
'<td>' + escape_html(r.doc_number) + '</td>' +
|
||||
'<td>' + escape_html(r.contact_name || '—') + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.grand_total, 2) + '</td>' +
|
||||
'<td>' + escape_html(r.doc_date || '—') + '</td>' +
|
||||
'<td>' + mapping_badge + '</td>' +
|
||||
'<td>' + status_badge + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
$tbody.html(html);
|
||||
};
|
||||
|
||||
$('#' + table_id + ' tfoot').html(generate_pagination(table_id, rows.length));
|
||||
window['change_page_' + table_id](1);
|
||||
|
||||
set_btn_state('#gl-btn-run-' + doc_type, missing_mapping === 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function product_mapping_badge(row) {
|
||||
if (String(row.product_mapping_checked || '0') !== '1') {
|
||||
return '<span class="text-muted">—</span>';
|
||||
}
|
||||
var missing = Number(row.product_mapping_missing || 0);
|
||||
if (missing > 0) {
|
||||
return '<span class="badge bg-warning text-dark">' + missing + ' missing</span>';
|
||||
}
|
||||
return '<span class="badge bg-success-subtle text-success">Mapped</span>';
|
||||
}
|
||||
|
||||
// ── batch overlay helpers ─────────────────────────────────────────────────
|
||||
var _batch_start_ms = 0;
|
||||
|
||||
function show_batch_overlay(label, total) {
|
||||
_batch_start_ms = Date.now();
|
||||
$('#batch-overlay-title').text('Batch GL Posting — ' + label);
|
||||
$('#batch-overlay-sub').text('Starting…');
|
||||
$('#batch-overlay-pct').text('0%');
|
||||
$('#batch-overlay-bar').css('width', '0%').attr('aria-valuenow', 0);
|
||||
$('#batch-overlay-count').text('0 / ' + total + ' documents');
|
||||
$('#batch-overlay-eta').html(' ');
|
||||
$('#batch-overlay').css('display', 'flex');
|
||||
}
|
||||
|
||||
function update_batch_overlay(done, total) {
|
||||
var pct = Math.round(done / total * 100);
|
||||
var elapsed = (Date.now() - _batch_start_ms) / 1000;
|
||||
var eta_txt = ' ';
|
||||
if (done > 0) {
|
||||
var remaining = Math.round((elapsed / done) * (total - done));
|
||||
eta_txt = remaining > 0 ? ('~' + remaining + 's remaining') : 'almost done…';
|
||||
}
|
||||
$('#batch-overlay-pct').text(pct + '%');
|
||||
$('#batch-overlay-bar').css('width', pct + '%').attr('aria-valuenow', pct);
|
||||
$('#batch-overlay-count').text(done + ' / ' + total + ' documents');
|
||||
$('#batch-overlay-sub').text('Processing document ' + done + ' of ' + total + '…');
|
||||
$('#batch-overlay-eta').html(eta_txt);
|
||||
}
|
||||
|
||||
function hide_batch_overlay() {
|
||||
$('#batch-overlay').hide();
|
||||
}
|
||||
|
||||
// ── run batch over all loaded IDs ─────────────────────────────────────────
|
||||
var DOC_LABELS = {
|
||||
invoice: 'Invoice',
|
||||
credit_note: 'Credit Note',
|
||||
purchase_invoice: 'Purchase Invoice',
|
||||
supplier_credit_note: 'Supplier Credit Note',
|
||||
receipt: 'Receipt',
|
||||
payment: 'Payment'
|
||||
};
|
||||
|
||||
function run_gl_batch(doc_type) {
|
||||
var ids = _gl_loaded[doc_type] || [];
|
||||
if (!ids.length) return;
|
||||
|
||||
var missing_mapping = (_gl_rows[doc_type] || []).filter(function(r) {
|
||||
return Number(r.product_mapping_missing || 0) > 0;
|
||||
});
|
||||
if (missing_mapping.length) {
|
||||
alert('Product Account Mapping is incomplete for ' + missing_mapping.length + ' loaded document(s). Fill Sales/Purchase accounts on Account Formulas > Product Accounts, then reload this batch.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Estimate TTL: ids × (throttle delay + avg PHP response) + 20% buffer, min 2 min
|
||||
var per_item_ms = 200 + 150; // throttle + avg response
|
||||
var estimated_ms = ids.length * per_item_ms;
|
||||
var ttl_minutes = Math.max(2, Math.ceil(estimated_ms / 60000 * 1.2));
|
||||
|
||||
// Acquire DB lock before starting — blocks other tabs/users
|
||||
acquire_op_lock('gl_post', function() {
|
||||
_start_gl_batch(doc_type, ids);
|
||||
}, function(msg, res) {
|
||||
var wait_msg = msg || 'A GL posting batch is already running.';
|
||||
if (res && res.expires_at) {
|
||||
var remaining_s = Math.max(0, Math.round((new Date(res.expires_at) - new Date()) / 1000));
|
||||
var remaining_m = Math.ceil(remaining_s / 60);
|
||||
wait_msg += '\nEstimated wait: ~' + (remaining_s < 60 ? remaining_s + ' seconds' : remaining_m + ' minute(s)') + '.';
|
||||
}
|
||||
alert(wait_msg);
|
||||
}, ttl_minutes);
|
||||
}
|
||||
|
||||
function _start_gl_batch(doc_type, ids) {
|
||||
var formula_id = $('#gl-formula-' + doc_type).val();
|
||||
var $prog = $('#gl-progress-' + doc_type);
|
||||
var $bar = $prog.find('.progress-bar');
|
||||
var $msg = $('#gl-msg-' + doc_type);
|
||||
|
||||
set_btn_state('#gl-btn-load-' + doc_type, false);
|
||||
set_btn_state('#gl-btn-run-' + doc_type, false);
|
||||
$msg.addClass('d-none').empty();
|
||||
$prog.removeClass('d-none');
|
||||
$bar.css('width', '0%').text('0%');
|
||||
|
||||
show_batch_overlay(DOC_LABELS[doc_type] || doc_type, ids.length);
|
||||
|
||||
batch_process(
|
||||
'gl_post',
|
||||
ids,
|
||||
'<?php echo $server_url?>accounting/api/engine/post_gl_entry.php',
|
||||
function(id) {
|
||||
var ctx = document.getElementById('session-context');
|
||||
return {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'post',
|
||||
doc_type: doc_type,
|
||||
id: id,
|
||||
formula_id: formula_id
|
||||
};
|
||||
},
|
||||
function(done, total) {
|
||||
var pct = Math.round(done / total * 100);
|
||||
$bar.css('width', pct + '%').attr('aria-valuenow', pct).text(pct + '%');
|
||||
update_batch_overlay(done, total);
|
||||
},
|
||||
function(failed) {
|
||||
// Always release DB lock — whether succeeded or failed
|
||||
release_op_lock('gl_post');
|
||||
|
||||
hide_batch_overlay();
|
||||
$prog.addClass('d-none');
|
||||
var succeeded = ids.length - failed.length;
|
||||
|
||||
if (failed.length === 0) {
|
||||
_gl_failed[doc_type] = [];
|
||||
$msg.removeClass('d-none alert-danger').addClass('alert alert-success')
|
||||
.text('Done. ' + succeeded + ' document(s) posted successfully.');
|
||||
} else {
|
||||
_gl_failed[doc_type] = failed;
|
||||
var nums = failed.map(function(f) { return f.item; }).join(', ');
|
||||
var detail = failed.map(function(f) {
|
||||
return escape_html(f.item + (f.error ? ': ' + f.error : ''));
|
||||
}).join('<br>');
|
||||
$msg.removeClass('d-none alert-success').addClass('alert alert-danger')
|
||||
.html('<strong>' + failed.length + ' failed</strong> after 3 retries (IDs: '
|
||||
+ escape_html(nums) + '). ' + succeeded + ' succeeded.'
|
||||
+ '<div class="small mt-2">' + detail + '</div>'
|
||||
+ ' <button class="btn btn-sm btn-warning ms-2"'
|
||||
+ ' onclick="retry_gl_batch(\'' + doc_type + '\')">'
|
||||
+ '<i class="ti ti-refresh me-1"></i>Retry Failed</button>');
|
||||
}
|
||||
|
||||
// Log the batch action
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/log_batch_action.php',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'post',
|
||||
type: 'gl_post',
|
||||
doc_type: doc_type,
|
||||
total: ids.length,
|
||||
succeeded: succeeded,
|
||||
failed: failed.length,
|
||||
failed_ids: JSON.stringify(failed.map(function(f) { return f.item; }))
|
||||
},
|
||||
onSuccess: function() {}
|
||||
});
|
||||
|
||||
// Reload table to reflect updated GL status
|
||||
set_btn_state('#gl-btn-load-' + doc_type, true);
|
||||
load_gl_tab(doc_type);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ── retry only failed items ───────────────────────────────────────────────
|
||||
function retry_gl_batch(doc_type) {
|
||||
var failed = _gl_failed[doc_type] || [];
|
||||
if (!failed.length) return;
|
||||
var ids = failed.map(function(f) { return f.item; });
|
||||
_gl_failed[doc_type] = [];
|
||||
|
||||
var per_item_ms = 200 + 150;
|
||||
var ttl_minutes = Math.max(2, Math.ceil(ids.length * per_item_ms / 60000 * 1.2));
|
||||
|
||||
acquire_op_lock('gl_post', function() {
|
||||
_start_gl_batch(doc_type, ids);
|
||||
}, function(msg, res) {
|
||||
var wait_msg = msg || 'A GL posting batch is already running.';
|
||||
if (res && res.expires_at) {
|
||||
var remaining_s = Math.max(0, Math.round((new Date(res.expires_at) - new Date()) / 1000));
|
||||
var remaining_m = Math.ceil(remaining_s / 60);
|
||||
wait_msg += '\nEstimated wait: ~' + (remaining_s < 60 ? remaining_s + ' seconds' : remaining_m + ' minute(s)') + '.';
|
||||
}
|
||||
alert(wait_msg);
|
||||
}, ttl_minutes);
|
||||
}
|
||||
|
||||
// ── bootstrap ─────────────────────────────────────────────────────────────
|
||||
$(function() {
|
||||
init_gl_tab('invoice');
|
||||
|
||||
$('#glTabs a[data-bs-toggle="tab"]').on('shown.bs.tab', function(e) {
|
||||
var slug = $(e.target).attr('href').replace('#tab-', '').replace(/-/g, '_');
|
||||
var doc_type = TAB_MAP[slug] || slug;
|
||||
init_gl_tab(doc_type);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php
|
||||
function gl_tab_html(string $doc_type, string $label): string {
|
||||
return <<<HTML
|
||||
<div class="card p-4">
|
||||
|
||||
<!-- Filters + action buttons -->
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Date From</label>
|
||||
<input type="text" id="gl-date-from-{$doc_type}" class="form-control form-control-sm"
|
||||
placeholder="d/m/Y" style="width:140px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Date To</label>
|
||||
<input type="text" id="gl-date-to-{$doc_type}" class="form-control form-control-sm"
|
||||
placeholder="d/m/Y" style="width:140px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">GL Formula <span class="text-danger">*</span></label>
|
||||
<select id="gl-formula-{$doc_type}" class="form-select form-select-sm" style="width:220px;">
|
||||
<option value="">— Select formula —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button id="gl-btn-load-{$doc_type}" class="btn btn-secondary btn-sm"
|
||||
onclick="load_gl_tab('{$doc_type}')">
|
||||
<i class="ti ti-search me-1"></i>Load
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button id="gl-btn-run-{$doc_type}" class="btn btn-primary btn-sm" disabled
|
||||
onclick="run_gl_batch('{$doc_type}')">
|
||||
<i class="ti ti-send me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary badge -->
|
||||
<div id="gl-badge-{$doc_type}" class="text-muted small mb-2"></div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div id="gl-progress-{$doc_type}" class="progress mb-3 d-none" style="height:22px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width:0%" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100">0 / 0</div>
|
||||
</div>
|
||||
|
||||
<!-- Result message -->
|
||||
<div id="gl-msg-{$doc_type}" class="d-none mb-3"></div>
|
||||
|
||||
<!-- Document listing (informational, no selection) -->
|
||||
<table id="gl-table-{$doc_type}" class="table table-hover mb-0 table-centered">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Document No.</th>
|
||||
<th>Contact</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th>Date</th>
|
||||
<th>Product Accounts</th>
|
||||
<th>GL Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="gl-tbody-{$doc_type}">
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Use the filters above and click Load.</td></tr>
|
||||
</tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
?>
|
||||
@@ -11,19 +11,78 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">GL Movement</h1>
|
||||
<p class="mb-0 text-muted">General ledger movement report</p>
|
||||
<p class="mb-0 text-muted">Per-account ledger with opening balance and running total</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-arrows-exchange fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">GL Movement coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1 small">Account</label>
|
||||
<select id="f_account" class="form-select form-select-sm">
|
||||
<option value="">— Select account —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">From Date</label>
|
||||
<input type="text" id="f_from" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">To Date</label>
|
||||
<input type="text" id="f_to" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Department</label>
|
||||
<select id="f_department" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="0">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_report()">
|
||||
<i class="ti ti-search me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="ti ti-printer me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report -->
|
||||
<div id="glm_header_card" class="card p-4 mb-3" style="display:none!important;">
|
||||
<div class="d-flex gap-5">
|
||||
<div><p class="text-muted small mb-0">Account</p><strong id="glm_account_title">—</strong></div>
|
||||
<div><p class="text-muted small mb-0">Date Range</p><strong id="glm_period_title">—</strong></div>
|
||||
<div class="ms-auto text-end"><p class="text-muted small mb-0">Opening Balance</p><strong id="glm_opening">—</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0 table-sm">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Period</th>
|
||||
<th>Department</th>
|
||||
<th>Reference</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Debit</th>
|
||||
<th class="text-end">Credit</th>
|
||||
<th class="text-end">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="glm_tbody">
|
||||
<tr><td colspan="8" class="text-center text-muted py-5">Select an account and date range, then click Run.</td></tr>
|
||||
</tbody>
|
||||
<tfoot id="glm_tfoot"></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,5 +91,120 @@
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var now = new Date();
|
||||
var curMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
|
||||
document.getElementById('f_from').value = curMonth + '-01';
|
||||
document.getElementById('f_to').value = curMonth + '-' + String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2,'0');
|
||||
flatpickr('#f_from, #f_to', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
// Load posting accounts into dropdown
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/account.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var opts = '<option value="">— Select account —</option>';
|
||||
(res.output || []).filter(function(a){ return a.is_posting == 1 && a.status == 1; })
|
||||
.forEach(function(a) {
|
||||
opts += '<option value="' + escape_html(a.account_code) + '">' +
|
||||
escape_html(a.account_code + ' — ' + a.account_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_account').innerHTML = opts;
|
||||
}
|
||||
});
|
||||
|
||||
load_departments();
|
||||
|
||||
function load_departments() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/department.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var html = '<option value="0">All Departments</option>';
|
||||
(res.output || []).filter(function(d){ return String(d.status) === '1'; }).forEach(function(d) {
|
||||
html += '<option value="' + d.id + '">' + escape_html(d.dept_code + ' - ' + d.dept_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_department').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_report() {
|
||||
var code = document.getElementById('f_account').value;
|
||||
var from = document.getElementById('f_from').value;
|
||||
var to = document.getElementById('f_to').value;
|
||||
var dept = document.getElementById('f_department').value || '0';
|
||||
if (!code) { alert('Select an account.'); return; }
|
||||
if (!from || !to) { alert('Select both dates.'); return; }
|
||||
|
||||
document.getElementById('glm_tbody').innerHTML =
|
||||
'<tr><td colspan="8" class="text-center text-muted py-5"><span class="spinner-border spinner-border-sm me-2"></span>Loading...</td></tr>';
|
||||
document.getElementById('glm_tfoot').innerHTML = '';
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_gl_movement.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
account_code: code,
|
||||
from_date: from,
|
||||
to_date: to,
|
||||
department_id: dept,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var rows = d.rows || [];
|
||||
|
||||
document.getElementById('glm_account_title').textContent = d.account_code + ' — ' + d.account_name;
|
||||
document.getElementById('glm_period_title').textContent = d.from_date + ' → ' + d.to_date;
|
||||
document.getElementById('glm_opening').textContent = format_number(d.opening_balance);
|
||||
document.getElementById('glm_header_card').style.removeProperty('display');
|
||||
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('glm_tbody').innerHTML =
|
||||
'<tr><td colspan="8" class="text-center text-muted py-5">No movement in this date range.</td></tr>';
|
||||
document.getElementById('glm_tfoot').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
var running = parseFloat(d.opening_balance) || 0;
|
||||
var total_dr = 0, total_cr = 0;
|
||||
var html = '';
|
||||
rows.forEach(function(r) {
|
||||
var dr = parseFloat(r.debit) || 0;
|
||||
var cr = parseFloat(r.credit) || 0;
|
||||
running += dr - cr;
|
||||
total_dr += dr;
|
||||
total_cr += cr;
|
||||
var bal_color = running >= 0 ? '' : 'text-danger';
|
||||
html += '<tr>' +
|
||||
'<td class="small">' + escape_html(r.entry_date || '—') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.period) + '</td>' +
|
||||
'<td class="small">' + (r.dept_code ? escape_html(r.dept_code) : '<span class="text-muted">—</span>') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.reference || '—') + '</td>' +
|
||||
'<td class="small text-muted">' + escape_html(r.line_description || r.gl_description || '—') + '</td>' +
|
||||
'<td class="text-end small">' + (dr ? format_number(dr) : '—') + '</td>' +
|
||||
'<td class="text-end small">' + (cr ? format_number(cr) : '—') + '</td>' +
|
||||
'<td class="text-end small fw-semibold ' + bal_color + '">' + format_number(running) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
document.getElementById('glm_tbody').innerHTML = html;
|
||||
document.getElementById('glm_tfoot').innerHTML =
|
||||
'<tr class="table-light fw-semibold">' +
|
||||
'<td colspan="5" class="text-end">Closing Balance</td>' +
|
||||
'<td class="text-end">' + format_number(total_dr) + '</td>' +
|
||||
'<td class="text-end">' + format_number(total_cr) + '</td>' +
|
||||
'<td class="text-end">' + format_number(running) + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar_ac.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">GL Journal Listing</h1>
|
||||
<p class="mb-0 text-muted">Posted general ledger entries from all document types</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="open_manual_entry()">
|
||||
<i class="ti ti-plus me-1"></i>New Manual Entry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Date From</label>
|
||||
<input type="text" id="f-date-from" class="form-control form-control-sm" placeholder="d/m/Y" style="width:140px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Date To</label>
|
||||
<input type="text" id="f-date-to" class="form-control form-control-sm" placeholder="d/m/Y" style="width:140px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Source Type</label>
|
||||
<select id="f-source-type" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="">All Types</option>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="credit_note">Credit Note</option>
|
||||
<option value="purchase_invoice">Purchase Invoice</option>
|
||||
<option value="supplier_credit_note">Supplier Credit Note</option>
|
||||
<option value="receipt">Receipt</option>
|
||||
<option value="payment">Payment</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_journal_listing()">
|
||||
<i class="ti ti-search me-1"></i>Load
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<span id="jl-badge" class="text-muted small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table id="jl-table" class="table table-hover mb-0 table-centered">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Type</th>
|
||||
<th>Contact</th>
|
||||
<th>Period</th>
|
||||
<th>Formula</th>
|
||||
<th class="text-end">Debit</th>
|
||||
<th class="text-end">Credit</th>
|
||||
<th>Posted At</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jl-tbody">
|
||||
<tr><td colspan="9" class="text-center text-muted py-5">Use the filters above and click Load.</td></tr>
|
||||
</tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Manual entry modal -->
|
||||
<div class="modal fade" id="me-modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Manual Journal Entry</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label small">Journal Date <span class="text-danger">*</span></label>
|
||||
<input type="text" id="me-date" class="form-control form-control-sm" placeholder="d/m/Y">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label small">Reference</label>
|
||||
<input type="text" id="me-reference" class="form-control form-control-sm" placeholder="e.g. PCF-001">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" id="me-description" class="form-control form-control-sm" placeholder="e.g. Petty cash reimbursement">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Balance indicator -->
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<span class="text-muted small">Debit: <strong id="me-total-debit">0.00</strong></span>
|
||||
<span class="text-muted small">Credit: <strong id="me-total-credit">0.00</strong></span>
|
||||
<span id="me-balance-badge"></span>
|
||||
</div>
|
||||
|
||||
<!-- Lines table -->
|
||||
<table class="table table-sm table-hover table-centered mb-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:160px;">Account Code</th>
|
||||
<th>Description</th>
|
||||
<th style="width:160px;" class="text-end">Debit</th>
|
||||
<th style="width:160px;" class="text-end">Credit</th>
|
||||
<th style="width:40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="me-lines">
|
||||
<!-- rows injected by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="me_add_line()">
|
||||
<i class="ti ti-plus me-1"></i>Add Line
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div id="me-msg" class="text-danger small me-auto d-none"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="me-btn-save" onclick="save_manual_entry()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail modal -->
|
||||
<div class="modal fade" id="jl-modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="jl-modal-title">Journal Entry</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="jl-modal-body">
|
||||
<div class="text-center text-muted py-4"><i class="ti ti-loader ti-spin me-1"></i>Loading…</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="jl-btn-edit" class="btn btn-outline-primary btn-sm d-none"
|
||||
onclick="edit_manual_entry()">
|
||||
<i class="ti ti-pencil me-1"></i>Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var _jl_rows = [];
|
||||
|
||||
var SOURCE_LABELS = {
|
||||
invoice: 'Invoice',
|
||||
credit_note: 'Credit Note',
|
||||
purchase_invoice: 'Purchase Invoice',
|
||||
supplier_credit_note: 'Supplier Credit Note',
|
||||
receipt: 'Receipt',
|
||||
payment: 'Payment',
|
||||
manual: 'Manual'
|
||||
};
|
||||
|
||||
var 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'
|
||||
};
|
||||
|
||||
function source_badge(type) {
|
||||
var cls = SOURCE_BADGE[type] || 'bg-light text-dark';
|
||||
return '<span class="badge ' + cls + '">' + (SOURCE_LABELS[type] || type) + '</span>';
|
||||
}
|
||||
|
||||
// ── init ─────────────────────────────────────────────────────────────────
|
||||
$(function() {
|
||||
var today = new Date();
|
||||
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
var fmt = function(d) {
|
||||
return ('0' + d.getDate()).slice(-2) + '/' +
|
||||
('0' + (d.getMonth() + 1)).slice(-2) + '/' + d.getFullYear();
|
||||
};
|
||||
flatpickr('#f-date-from', { dateFormat: 'd/m/Y', allowInput: true, defaultDate: firstDay });
|
||||
flatpickr('#f-date-to', { dateFormat: 'd/m/Y', allowInput: true, defaultDate: today });
|
||||
$('#f-date-from').val(fmt(firstDay));
|
||||
$('#f-date-to').val(fmt(today));
|
||||
});
|
||||
|
||||
// ── load listing ─────────────────────────────────────────────────────────
|
||||
function load_journal_listing() {
|
||||
var $tbody = $('#jl-tbody');
|
||||
$tbody.html('<tr><td colspan="9" class="text-center text-muted py-4">'
|
||||
+ '<i class="ti ti-loader ti-spin me-1"></i>Loading…</td></tr>');
|
||||
$('#jl-badge').text('');
|
||||
$('#jl-table tfoot').empty();
|
||||
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/get_journal_listing.php',
|
||||
action: 'get',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'get',
|
||||
date_from: $('#f-date-from').val(),
|
||||
date_to: $('#f-date-to').val(),
|
||||
source_type: $('#f-source-type').val()
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
_jl_rows = res.output || [];
|
||||
|
||||
if (!_jl_rows.length) {
|
||||
$tbody.html('<tr><td colspan="9" class="text-center text-muted py-4">No GL entries found.</td></tr>');
|
||||
$('#jl-badge').text('0 entries');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#jl-badge').text(_jl_rows.length + ' entr' + (_jl_rows.length === 1 ? 'y' : 'ies'));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS jl_table');
|
||||
alasql.tables['jl_table'].data = _jl_rows;
|
||||
|
||||
window['change_page_jl_table'] = function(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var page_rows = alasql('SELECT * FROM jl_table LIMIT ' + prop_limit + ' OFFSET ' + offset);
|
||||
render_jl_rows(page_rows);
|
||||
};
|
||||
|
||||
$('#jl-table tfoot').html(generate_pagination('jl-table', _jl_rows.length));
|
||||
window['change_page_jl_table'](1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_jl_rows(rows) {
|
||||
var html = '';
|
||||
$.each(rows, function(i, r) {
|
||||
html +=
|
||||
'<tr>' +
|
||||
'<td class="fw-semibold">' + escape_html(r.doc_number || '—') + '</td>' +
|
||||
'<td>' + source_badge(r.source_type) + '</td>' +
|
||||
'<td>' + escape_html(r.contact_name || '—') + '</td>' +
|
||||
'<td>' + escape_html(r.period) + '</td>' +
|
||||
'<td class="text-muted small">' + escape_html(r.formula_name || '—') + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.total_debit, 2) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.total_credit, 2) + '</td>' +
|
||||
'<td class="text-muted small">' + escape_html(r.posted_at) + '</td>' +
|
||||
'<td>' +
|
||||
'<a href="javascript:;" onclick="show_journal_detail(' + r.id + ',\'' + escape_html(r.doc_number || '') + '\')" title="View lines">' +
|
||||
'<i class="ti ti-eye fs-5"></i></a>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
$('#jl-tbody').html(html);
|
||||
}
|
||||
|
||||
// ── manual entry form ─────────────────────────────────────────────────────
|
||||
var _me_line_idx = 0;
|
||||
var _me_editing_gl_id = 0; // 0 = new, >0 = editing
|
||||
|
||||
function edit_manual_entry() {
|
||||
if (!_jl_detail) return;
|
||||
$('#jl-modal').modal('hide');
|
||||
open_manual_entry(_jl_detail);
|
||||
}
|
||||
|
||||
function open_manual_entry(existing) {
|
||||
_me_line_idx = 0;
|
||||
_me_editing_gl_id = existing ? (int_val(existing.header.id)) : 0;
|
||||
$('#me-lines').empty();
|
||||
$('#me-msg').addClass('d-none').text('');
|
||||
$('#me-balance-badge').html('');
|
||||
|
||||
var title = _me_editing_gl_id > 0 ? 'Edit Manual Journal Entry' : 'New Manual Journal Entry';
|
||||
$('#me-modal .modal-title').text(title);
|
||||
|
||||
if (existing) {
|
||||
var h = existing.header;
|
||||
$('#me-reference').val(h.reference || '');
|
||||
$('#me-description').val(h.description || '');
|
||||
flatpickr('#me-date', { dateFormat: 'd/m/Y', allowInput: true, defaultDate: h.journal_date || new Date() });
|
||||
$('#me-date').val(h.journal_date_fmt || '');
|
||||
$.each(existing.lines, function(i, l) { me_add_line(l); });
|
||||
} else {
|
||||
$('#me-reference').val('');
|
||||
$('#me-description').val('');
|
||||
flatpickr('#me-date', { dateFormat: 'd/m/Y', allowInput: true, defaultDate: new Date() });
|
||||
me_add_line();
|
||||
me_add_line();
|
||||
}
|
||||
|
||||
me_update_totals();
|
||||
|
||||
// Load accounts if not yet fetched
|
||||
if (!window.account_search_accounts || !window.account_search_accounts.length) {
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account.php',
|
||||
action: 'get', queueLock: false, noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'get'
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
window.account_search_accounts = res.output || [];
|
||||
init_account_search_inputs('#me-modal', window.account_search_accounts);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
init_account_search_inputs('#me-modal', window.account_search_accounts);
|
||||
}
|
||||
|
||||
$('#me-modal').modal('show');
|
||||
}
|
||||
|
||||
function int_val(v) { return parseInt(v, 10) || 0; }
|
||||
|
||||
function me_add_line(prefill) {
|
||||
var idx = _me_line_idx++;
|
||||
var code = prefill ? escape_html(prefill.account_code) : '';
|
||||
var label = prefill ? escape_html(prefill.account_code + (prefill.account_name ? ' - ' + prefill.account_name : '')) : '';
|
||||
var desc = prefill ? escape_html(prefill.description) : '';
|
||||
var debit = prefill ? (parseFloat(prefill.debit) || '') : '';
|
||||
var credit = prefill ? (parseFloat(prefill.credit) || '') : '';
|
||||
|
||||
var row =
|
||||
'<tr id="me-line-' + idx + '">' +
|
||||
'<td><input type="text" class="form-control form-control-sm account_search" placeholder="Search account…" autocomplete="off"' +
|
||||
(prefill ? ' value="' + label + '" data-account-code="' + code + '" secondary="' + label + '"' : '') +
|
||||
' oninput="me_update_totals()"></td>' +
|
||||
'<td><input type="text" class="form-control form-control-sm" placeholder="Description" value="' + desc + '"></td>' +
|
||||
'<td><input type="number" class="form-control form-control-sm text-end me-debit" min="0" step="0.01" placeholder="0.00" value="' + debit + '" oninput="me_update_totals()"></td>' +
|
||||
'<td><input type="number" class="form-control form-control-sm text-end me-credit" min="0" step="0.01" placeholder="0.00" value="' + credit + '" oninput="me_update_totals()"></td>' +
|
||||
'<td><a href="javascript:;" onclick="$(\'#me-line-' + idx + '\').remove(); me_update_totals();" class="text-danger"><i class="ti ti-x"></i></a></td>' +
|
||||
'</tr>';
|
||||
$('#me-lines').append(row);
|
||||
|
||||
var $newRow = $('#me-line-' + idx);
|
||||
if (window.account_search_accounts && window.account_search_accounts.length) {
|
||||
init_account_search_inputs($newRow[0], window.account_search_accounts);
|
||||
}
|
||||
}
|
||||
|
||||
function me_update_totals() {
|
||||
var debit = 0, credit = 0;
|
||||
$('#me-lines tr').each(function() {
|
||||
debit += parseFloat($(this).find('.me-debit').val()) || 0;
|
||||
credit += parseFloat($(this).find('.me-credit').val()) || 0;
|
||||
});
|
||||
$('#me-total-debit').text(format_number(debit, 2));
|
||||
$('#me-total-credit').text(format_number(credit, 2));
|
||||
|
||||
var balanced = Math.abs(debit - credit) < 0.005 && debit > 0;
|
||||
$('#me-balance-badge').html(
|
||||
balanced
|
||||
? '<span class="badge bg-success-subtle text-success">Balanced</span>'
|
||||
: (debit > 0 || credit > 0
|
||||
? '<span class="badge bg-danger-subtle text-danger">Unbalanced</span>'
|
||||
: '')
|
||||
);
|
||||
}
|
||||
|
||||
function save_manual_entry() {
|
||||
var lines = [];
|
||||
$('#me-lines tr').each(function() {
|
||||
var $row = $(this);
|
||||
var account_code = $row.find('.account_search').attr('data-account-code') || '';
|
||||
lines.push({
|
||||
account_code: account_code.trim(),
|
||||
description: $row.find('input').eq(1).val().trim(),
|
||||
debit: parseFloat($row.find('.me-debit').val()) || 0,
|
||||
credit: parseFloat($row.find('.me-credit').val()) || 0
|
||||
});
|
||||
});
|
||||
|
||||
set_btn_state('#me-btn-save', false);
|
||||
$('#me-msg').addClass('d-none').text('');
|
||||
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/save_manual_journal.php',
|
||||
action: 'post',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'post',
|
||||
gl_id: _me_editing_gl_id,
|
||||
journal_date: $('#me-date').val(),
|
||||
reference: $('#me-reference').val().trim(),
|
||||
description: $('#me-description').val().trim(),
|
||||
lines: JSON.stringify(lines)
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
set_btn_state('#me-btn-save', true);
|
||||
if (res.success) {
|
||||
$('#me-modal').modal('hide');
|
||||
load_journal_listing();
|
||||
} else {
|
||||
$('#me-msg').removeClass('d-none').text(res.message || 'Failed to save.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── detail modal ──────────────────────────────────────────────────────────
|
||||
var _jl_detail = null; // cache last loaded detail for edit
|
||||
|
||||
function show_journal_detail(gl_id, doc_number) {
|
||||
_jl_detail = null;
|
||||
$('#jl-btn-edit').addClass('d-none');
|
||||
$('#jl-modal-title').text('Journal Entry — ' + (doc_number || 'GL #' + gl_id));
|
||||
$('#jl-modal-body').html('<div class="text-center text-muted py-4"><i class="ti ti-loader ti-spin me-1"></i>Loading…</div>');
|
||||
$('#jl-modal').modal('show');
|
||||
|
||||
var ctx = document.getElementById('session-context');
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/get_journal_detail.php',
|
||||
action: 'get',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
data: {
|
||||
otp: ctx ? ctx.dataset.otp : '',
|
||||
company_id: ctx ? ctx.dataset.companyId : '',
|
||||
action: 'get',
|
||||
gl_id: gl_id
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
_jl_detail = res.output;
|
||||
var h = res.output.header;
|
||||
var lines = res.output.lines;
|
||||
|
||||
if (h.source_type === 'manual') {
|
||||
$('#jl-btn-edit').removeClass('d-none');
|
||||
}
|
||||
|
||||
var total_debit = 0;
|
||||
var total_credit = 0;
|
||||
var lines_html = '';
|
||||
|
||||
$.each(lines, function(i, l) {
|
||||
total_debit += parseFloat(l.debit) || 0;
|
||||
total_credit += parseFloat(l.credit) || 0;
|
||||
var dept_label = l.dept_code
|
||||
? escape_html(l.dept_code + (l.dept_name ? ' - ' + l.dept_name : ''))
|
||||
: '<span class="text-muted">—</span>';
|
||||
lines_html +=
|
||||
'<tr>' +
|
||||
'<td class="fw-semibold">' + escape_html(l.account_code) + '</td>' +
|
||||
'<td>' + escape_html(l.account_name) + '</td>' +
|
||||
'<td>' + dept_label + '</td>' +
|
||||
'<td class="text-end">' + (parseFloat(l.debit) ? format_number(l.debit, 2) : '<span class="text-muted">—</span>') + '</td>' +
|
||||
'<td class="text-end">' + (parseFloat(l.credit) ? format_number(l.credit, 2) : '<span class="text-muted">—</span>') + '</td>' +
|
||||
'<td class="text-muted small">' + escape_html(l.description) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
lines_html +=
|
||||
'<tr class="table-light fw-semibold">' +
|
||||
'<td colspan="3">Total</td>' +
|
||||
'<td class="text-end">' + format_number(total_debit, 2) + '</td>' +
|
||||
'<td class="text-end">' + format_number(total_credit, 2) + '</td>' +
|
||||
'<td></td>' +
|
||||
'</tr>';
|
||||
|
||||
var balanced = Math.abs(total_debit - total_credit) < 0.01;
|
||||
var balance_indicator = balanced
|
||||
? '<span class="badge bg-success-subtle text-success ms-2">Balanced</span>'
|
||||
: '<span class="badge bg-danger-subtle text-danger ms-2">Unbalanced</span>';
|
||||
|
||||
var extra_fields = h.source_type === 'manual'
|
||||
? '<div class="col-sm-4"><div class="text-muted small">Reference</div><div class="fw-semibold">' + escape_html(h.reference || ('MJE-' + h.id)) + '</div></div>' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Journal Date</div><div>' + escape_html(h.journal_date_fmt || '—') + '</div></div>' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Description</div><div>' + escape_html(h.description || '—') + '</div></div>'
|
||||
: '<div class="col-sm-4"><div class="text-muted small">Formula</div><div>' + escape_html(h.formula_name) + '</div></div>' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Version</div><div>v' + h.current_version + (h.current_version > 1 ? ' <span class="text-muted small">(replaced)</span>' : '') + '</div></div>' +
|
||||
'<div class="col-sm-4"></div>';
|
||||
|
||||
var body =
|
||||
'<div class="row g-3 mb-4 text-sm">' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Source</div><div>' + source_badge(h.source_type) + '</div></div>' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Period</div><div class="fw-semibold">' + escape_html(h.period) + '</div></div>' +
|
||||
extra_fields +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Posted At</div><div class="text-muted small">' + escape_html(h.posted_at) + '</div></div>' +
|
||||
'<div class="col-sm-4"><div class="text-muted small">Updated At</div><div class="text-muted small">' + escape_html(h.updated_at_fmt) + '</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="d-flex align-items-center mb-2">' +
|
||||
'<span class="fw-semibold">Journal Lines</span>' + balance_indicator +
|
||||
'</div>' +
|
||||
'<table class="table table-sm table-hover mb-0 table-centered">' +
|
||||
'<thead class="table-light"><tr>' +
|
||||
'<th>Account Code</th><th>Account Name</th><th>Department</th>' +
|
||||
'<th class="text-end">Debit</th><th class="text-end">Credit</th><th>Description</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody>' + lines_html + '</tbody>' +
|
||||
'</table>';
|
||||
|
||||
$('#jl-modal-body').html(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -72,6 +72,18 @@
|
||||
<div class="form-text">Only parent-level accounts of the same type are shown.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Account Category</label>
|
||||
<select class="form-select" id="account_category">
|
||||
<option value="">— None —</option>
|
||||
<option value="ar">Account Receivable</option>
|
||||
<option value="ap">Account Payable</option>
|
||||
<option value="sales_tax">Sales Tax (Output VAT)</option>
|
||||
<option value="purchase_tax">Purchase Tax (Input VAT)</option>
|
||||
</select>
|
||||
<div class="form-text">Used by reports and modules to filter by sub-type.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status">
|
||||
@@ -217,6 +229,7 @@
|
||||
$('#account_code').val(item.account_code);
|
||||
$('#account_name').val(item.account_name);
|
||||
$('#account_type').val(item.account_type);
|
||||
$('#account_category').val(item.account_category || '');
|
||||
$('#is_posting').val(item.is_posting);
|
||||
$('#status').val(item.status);
|
||||
if (item.is_posting == 0) $('#parent_row').hide();
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_master_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Accounting Periods</h1>
|
||||
<p class="mb-0">Open, close, and lock monthly accounting periods</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-primary" onclick="show_create_year_modal();">
|
||||
<i class="ti ti-calendar-plus me-1"></i>Generate Year
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="show_create_modal();">
|
||||
<i class="ti ti-plus me-1"></i>Add Period
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-calendar fs-4"></i>
|
||||
</div>
|
||||
<div><span>Total</span><h2 class="fw-bold mb-0" id="stat_total">—</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
|
||||
<i class="ti ti-lock-open fs-4"></i>
|
||||
</div>
|
||||
<div><span>Open</span><h2 class="fw-bold mb-0" id="stat_open">—</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-warning bg-opacity-10 text-warning rounded-2">
|
||||
<i class="ti ti-lock fs-4"></i>
|
||||
</div>
|
||||
<div><span>Closed</span><h2 class="fw-bold mb-0" id="stat_closed">—</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-danger bg-opacity-10 text-danger rounded-2">
|
||||
<i class="ti ti-lock-filled fs-4"></i>
|
||||
</div>
|
||||
<div><span>Locked</span><h2 class="fw-bold mb-0" id="stat_locked">—</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body p-6">
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="period_table">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Status</th>
|
||||
<th>Closed At</th>
|
||||
<th>Locked At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add Single Period Modal -->
|
||||
<div class="modal fade" id="createPeriodModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" style="max-width:340px;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">Add Period</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">Period (YYYY-MM)</label>
|
||||
<input type="month" class="form-control" id="new_period">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="create_single();">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Year Modal -->
|
||||
<div class="modal fade" id="createYearModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" style="max-width:340px;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">Generate Year Periods</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">Year</label>
|
||||
<input type="number" class="form-control" id="new_year" value="<?php echo date('Y'); ?>" min="2000" max="2100">
|
||||
<div class="form-text">Creates all 12 months for the selected year (skips existing ones).</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="create_year();">Generate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var status_badge = {
|
||||
open: '<span class="badge bg-success">Open</span>',
|
||||
closed: '<span class="badge bg-warning text-dark">Closed</span>',
|
||||
locked: '<span class="badge bg-danger">Locked</span>',
|
||||
};
|
||||
|
||||
function retrieve_periods() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/period.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var rows = res.output || [];
|
||||
if (rows.length === 0) {
|
||||
$('table#period_table tbody').html('<tr><td colspan="5" class="text-center py-5 text-muted">No periods yet. Use Generate Year to create them.</td></tr>');
|
||||
return;
|
||||
}
|
||||
var body = '';
|
||||
$.each(rows, function(i, item) {
|
||||
var actions = '';
|
||||
if (item.status === 'open') {
|
||||
actions = `<button class="btn btn-sm btn-outline-warning" onclick="do_action('close', ${item.id})">Close</button>`;
|
||||
} else if (item.status === 'closed') {
|
||||
actions = `<button class="btn btn-sm btn-outline-success me-1" onclick="do_action('open', ${item.id})">Reopen</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="do_action('lock', ${item.id})">Lock</button>`;
|
||||
} else {
|
||||
actions = '<span class="text-muted small">Permanent</span>';
|
||||
}
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${escape_html(item.period)}</td>
|
||||
<td class="py-3">${status_badge[item.status] || item.status}</td>
|
||||
<td class="py-3 text-muted small">${item.closed_at || '—'}</td>
|
||||
<td class="py-3 text-muted small">${item.locked_at || '—'}</td>
|
||||
<td class="py-3">${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#period_table tbody').html(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function do_action(action_type, id) {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'manage',
|
||||
data: { action_type: action_type, id: id },
|
||||
onSuccess: function() {
|
||||
retrieve_periods();
|
||||
retrieve_stats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function create_single() {
|
||||
var period = $('#new_period').val();
|
||||
if (!period) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'manage',
|
||||
data: { action_type: 'create_single', period: period },
|
||||
onSuccess: function() {
|
||||
$('#createPeriodModal').modal('hide');
|
||||
retrieve_periods();
|
||||
retrieve_stats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function create_year() {
|
||||
var year = $('#new_year').val();
|
||||
if (!year) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/manage_period.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'manage',
|
||||
data: { action_type: 'create_year', year: year },
|
||||
onSuccess: function() {
|
||||
$('#createYearModal').modal('hide');
|
||||
retrieve_periods();
|
||||
retrieve_stats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_stats() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/period_stats.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
$('#stat_total').text(res.total || 0);
|
||||
$('#stat_open').text(res.open_count || 0);
|
||||
$('#stat_closed').text(res.closed_count || 0);
|
||||
$('#stat_locked').text(res.locked_count || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function show_create_modal() { $('#new_period').val(''); $('#createPeriodModal').modal('show'); }
|
||||
function show_create_year_modal() { $('#createYearModal').modal('show'); }
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_stats();
|
||||
await retrieve_periods();
|
||||
} catch(e) { console.log(e); }
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,26 +11,150 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">P&L Statement</h1>
|
||||
<p class="mb-0 text-muted">Profit and loss statement</p>
|
||||
<h1 class="fs-3 mb-1">Profit & Loss Statement</h1>
|
||||
<p class="mb-0 text-muted">Revenue and expense accounts for a date range</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-chart-line fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">P&L Statement coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">From Date</label>
|
||||
<input type="text" id="f_from" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">To Date</label>
|
||||
<input type="text" id="f_to" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Department</label>
|
||||
<select id="f_department" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="0">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_report()">
|
||||
<i class="ti ti-search me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="ti ti-printer me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report body -->
|
||||
<div id="pl_body">
|
||||
<div class="card p-5 text-center text-muted">Select a date range and click Run.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var now = new Date();
|
||||
var curMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
|
||||
document.getElementById('f_from').value = curMonth + '-01';
|
||||
document.getElementById('f_to').value = curMonth + '-' + String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2,'0');
|
||||
flatpickr('#f_from, #f_to', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
load_departments();
|
||||
|
||||
function load_departments() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/department.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var html = '<option value="0">All Departments</option>';
|
||||
(res.output || []).filter(function(d){ return String(d.status) === '1'; }).forEach(function(d) {
|
||||
html += '<option value="' + d.id + '">' + escape_html(d.dept_code + ' - ' + d.dept_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_department').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_report() {
|
||||
var from = document.getElementById('f_from').value;
|
||||
var to = document.getElementById('f_to').value;
|
||||
var dept = document.getElementById('f_department').value || '0';
|
||||
if (!from || !to) { alert('Select both dates.'); return; }
|
||||
|
||||
document.getElementById('pl_body').innerHTML =
|
||||
'<div class="card p-5 text-center text-muted"><span class="spinner-border spinner-border-sm me-2"></span>Loading...</div>';
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_pl_statement.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
from_date: from,
|
||||
to_date: to,
|
||||
department_id: dept,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var rows = d.rows || [];
|
||||
var revenues = rows.filter(function(r){ return r.account_type === 'revenue'; });
|
||||
var expenses = rows.filter(function(r){ return r.account_type === 'expense'; });
|
||||
|
||||
function section_rows(items, is_revenue) {
|
||||
if (items.length === 0) return '<tr><td colspan="3" class="text-muted small ps-4">No entries.</td></tr>';
|
||||
return items.map(function(r) {
|
||||
var net = is_revenue ? (parseFloat(r.total_credit) - parseFloat(r.total_debit))
|
||||
: (parseFloat(r.total_debit) - parseFloat(r.total_credit));
|
||||
return '<tr>' +
|
||||
'<td class="ps-4">' + escape_html(r.account_code) + '</td>' +
|
||||
'<td>' + escape_html(r.account_name) + '</td>' +
|
||||
'<td class="text-end">' + format_number(net) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
var net_color = d.net_profit >= 0 ? 'text-success' : 'text-danger';
|
||||
|
||||
var html =
|
||||
'<div class="card">' +
|
||||
'<div class="card-body p-0">' +
|
||||
'<table class="table mb-0">' +
|
||||
'<thead class="table-primary border-light"><tr><th>Code</th><th>Account</th><th class="text-end">Amount</th></tr></thead>' +
|
||||
'<tbody>' +
|
||||
'<tr class="table-light"><td colspan="3" class="fw-semibold py-2">Revenue</td></tr>' +
|
||||
section_rows(revenues, true) +
|
||||
'<tr class="fw-semibold border-top">' +
|
||||
'<td colspan="2" class="text-end py-2">Total Revenue</td>' +
|
||||
'<td class="text-end text-success py-2">' + format_number(d.total_revenue) + '</td>' +
|
||||
'</tr>' +
|
||||
'<tr class="table-light"><td colspan="3" class="fw-semibold py-2">Expenses</td></tr>' +
|
||||
section_rows(expenses, false) +
|
||||
'<tr class="fw-semibold border-top">' +
|
||||
'<td colspan="2" class="text-end py-2">Total Expenses</td>' +
|
||||
'<td class="text-end text-danger py-2">' + format_number(d.total_expense) + '</td>' +
|
||||
'</tr>' +
|
||||
'<tr class="fw-bold border-top border-2">' +
|
||||
'<td colspan="2" class="text-end py-3 fs-6">Net Profit / (Loss)</td>' +
|
||||
'<td class="text-end py-3 fs-6 ' + net_color + '">' + format_number(d.net_profit) + '</td>' +
|
||||
'</tr>' +
|
||||
'</tbody>' +
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.getElementById('pl_body').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_master_sidebar.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Posting Window</h1>
|
||||
<p class="mb-0">Define the date range within which GL postings and approved inventory entries can be created, edited, voided, or reversed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
|
||||
<!-- Window card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-5">
|
||||
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-calendar-event fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0 fw-semibold">Open Posting Window</h5>
|
||||
<p class="mb-0 text-muted small">Dates outside this range are locked for GL and inventory movement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current status -->
|
||||
<div id="window_display" class="alert alert-secondary mb-5">Loading...</div>
|
||||
|
||||
<div class="alert alert-info d-flex gap-2 mb-5">
|
||||
<i class="ti ti-info-circle mt-1"></i>
|
||||
<div>
|
||||
This window applies to both accounting GL postings and approved inventory entries.
|
||||
Draft inventory rows can still be edited or cancelled before they affect stock balance.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Open From <span class="text-danger">*</span></label>
|
||||
<input type="text" id="open_from" class="form-control">
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Open To <span class="text-danger">*</span></label>
|
||||
<input type="text" id="open_to" class="form-control">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text">Example: <code>01/01/2026</code> to <code>31/12/2026</code> opens all of 2026.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-primary" onclick="save_window()">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save Window
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="clear_window()">
|
||||
<i class="ti ti-lock-open me-1"></i>Remove Restrictions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Guide card -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 bg-light h-100">
|
||||
<div class="card-body p-5">
|
||||
<h6 class="fw-semibold mb-3"><i class="ti ti-info-circle me-1"></i>How it works</h6>
|
||||
<ul class="small mb-0 ps-3">
|
||||
<li class="mb-2">Journal entries, GL postings, and approved inventory movements are only allowed if their date falls <strong>within</strong> the open window.</li>
|
||||
<li class="mb-2">Dates <strong>before Open From</strong> or <strong>after Open To</strong> are rejected.</li>
|
||||
<li class="mb-2">Inventory entries that already affected stock balance use the same window when they are voided, cancelled, or reversed.</li>
|
||||
<li class="mb-2">To open a full year: set <strong>01/01/YYYY</strong> → <strong>31/12/YYYY</strong>.</li>
|
||||
<li class="mb-2">To roll the window forward (e.g. close January): change Open From to <strong>01/02/YYYY</strong>.</li>
|
||||
<li class="mb-2"><strong>Remove Restrictions</strong> clears both dates — all dates become postable.</li>
|
||||
<li>Existing posted GL and approved inventory entries are not changed automatically — the window is checked when a user creates, edits, voids, cancels, or reverses them.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var open_from_picker = null;
|
||||
var open_to_picker = null;
|
||||
|
||||
function init_window_pickers() {
|
||||
var config = {
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'd/m/Y',
|
||||
allowInput: true,
|
||||
onReady: function(_, __, fp) {
|
||||
fp.altInput.classList.add('form-control');
|
||||
}
|
||||
};
|
||||
|
||||
open_from_picker = flatpickr('#open_from', config);
|
||||
open_to_picker = flatpickr('#open_to', config);
|
||||
}
|
||||
|
||||
function load_window() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/posting_window.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
if (open_from_picker) open_from_picker.setDate(d.open_from || '', false, 'Y-m-d');
|
||||
if (open_to_picker) open_to_picker.setDate(d.open_to || '', false, 'Y-m-d');
|
||||
render_display(d.open_from, d.open_to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_display(from, to) {
|
||||
var el = document.getElementById('window_display');
|
||||
if (!from && !to) {
|
||||
el.className = 'alert alert-success mb-5';
|
||||
el.innerHTML = '<i class="ti ti-lock-open me-2"></i><strong>No restrictions</strong> — all dates are open for posting.';
|
||||
} else {
|
||||
el.className = 'alert alert-primary mb-5';
|
||||
var f = from ? '<strong>' + escape_html(from) + '</strong>' : '<span class="text-muted">no lower limit</span>';
|
||||
var t = to ? '<strong>' + escape_html(to) + '</strong>' : '<span class="text-muted">no upper limit</span>';
|
||||
el.innerHTML = '<i class="ti ti-calendar-event me-2"></i>Posting open from ' + f + ' to ' + t;
|
||||
}
|
||||
}
|
||||
|
||||
function save_window() {
|
||||
var from = document.getElementById('open_from').value;
|
||||
var to = document.getElementById('open_to').value;
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/posting_window.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save',
|
||||
open_from: from,
|
||||
open_to: to,
|
||||
onSuccess: function() {
|
||||
render_display(from, to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clear_window() {
|
||||
if (open_from_picker) open_from_picker.clear();
|
||||
if (open_to_picker) open_to_picker.clear();
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/posting_window.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save',
|
||||
open_from: '',
|
||||
open_to: '',
|
||||
onSuccess: function() {
|
||||
render_display('', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init_window_pickers();
|
||||
load_window();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,19 +11,65 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">Trial Balance</h1>
|
||||
<p class="mb-0 text-muted">Period trial balance report</p>
|
||||
<p class="mb-0 text-muted">Brought-forward balances, date-range movement, and carry-forward balances for posting accounts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-scale fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">Trial Balance coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">From Date</label>
|
||||
<input type="text" id="f_from" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">To Date</label>
|
||||
<input type="text" id="f_to" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Department</label>
|
||||
<select id="f_department" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="0">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_report()">
|
||||
<i class="ti ti-search me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="ti ti-printer me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0 table-centered" id="tb_table">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Account Name</th>
|
||||
<th>Type</th>
|
||||
<th class="text-end">B/F Debit</th>
|
||||
<th class="text-end">B/F Credit</th>
|
||||
<th class="text-end">Period Debit</th>
|
||||
<th class="text-end">Period Credit</th>
|
||||
<th class="text-end">C/F Debit</th>
|
||||
<th class="text-end">C/F Credit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tb_tbody">
|
||||
<tr><td colspan="9" class="text-center text-muted py-5">Select a date range and click Run.</td></tr>
|
||||
</tbody>
|
||||
<tfoot id="tb_tfoot"></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,5 +78,92 @@
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var type_labels = { asset:'Asset', liability:'Liability', equity:'Equity', revenue:'Revenue', expense:'Expense' };
|
||||
|
||||
// Default: current month date range
|
||||
var now = new Date();
|
||||
var curMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
|
||||
document.getElementById('f_from').value = curMonth + '-01';
|
||||
document.getElementById('f_to').value = curMonth + '-' + String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2,'0');
|
||||
flatpickr('#f_from, #f_to', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
load_departments();
|
||||
|
||||
function load_departments() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/department.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var html = '<option value="0">All Departments</option>';
|
||||
(res.output || []).filter(function(d){ return String(d.status) === '1'; }).forEach(function(d) {
|
||||
html += '<option value="' + d.id + '">' + escape_html(d.dept_code + ' - ' + d.dept_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_department').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_report() {
|
||||
var from = document.getElementById('f_from').value;
|
||||
var to = document.getElementById('f_to').value;
|
||||
var dept = document.getElementById('f_department').value || '0';
|
||||
if (!from || !to) { alert('Select both dates.'); return; }
|
||||
|
||||
document.getElementById('tb_tbody').innerHTML =
|
||||
'<tr><td colspan="9" class="text-center text-muted py-5"><span class="spinner-border spinner-border-sm me-2"></span>Loading...</td></tr>';
|
||||
document.getElementById('tb_tfoot').innerHTML = '';
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_trial_balance.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
from_date: from,
|
||||
to_date: to,
|
||||
department_id: dept,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var rows = d.rows || [];
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('tb_tbody').innerHTML =
|
||||
'<tr><td colspan="9" class="text-center text-muted py-5">No data for this date range.</td></tr>';
|
||||
document.getElementById('tb_tfoot').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
rows.forEach(function(r) {
|
||||
html += '<tr>' +
|
||||
'<td class="fw-semibold">' + escape_html(r.account_code) + '</td>' +
|
||||
'<td>' + escape_html(r.account_name) + '</td>' +
|
||||
'<td><span class="badge bg-secondary bg-opacity-10 text-secondary">' + escape_html(type_labels[r.account_type] || r.account_type) + '</span></td>' +
|
||||
'<td class="text-end">' + format_number(r.opening_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.opening_credit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.period_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.period_credit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.closing_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(r.closing_credit) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
document.getElementById('tb_tbody').innerHTML = html;
|
||||
document.getElementById('tb_tfoot').innerHTML =
|
||||
'<tr class="table-light fw-semibold">' +
|
||||
'<td colspan="3">Total</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_opening_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_opening_credit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_period_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_period_credit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_closing_debit) + '</td>' +
|
||||
'<td class="text-end">' + format_number(d.total_closing_credit) + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,19 +11,101 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mb-6">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="fs-3 mb-1">VAT Report</h1>
|
||||
<p class="mb-0 text-muted">Value added tax report</p>
|
||||
<p class="mb-0 text-muted">Output VAT (Sales Tax) and Input VAT (Purchase Tax) from posted journal entries</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card p-5 text-center text-muted">
|
||||
<i class="ti ti-receipt-tax fs-1 mb-3 d-block"></i>
|
||||
<p class="mb-0">VAT Report coming soon.</p>
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">From Date</label>
|
||||
<input type="text" id="f_from" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">To Date</label>
|
||||
<input type="text" id="f_to" class="form-control form-control-sm" placeholder="DD/MM/YYYY" style="width:160px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Category</label>
|
||||
<select id="f_cat" class="form-select form-select-sm" style="width:180px;">
|
||||
<option value="">All</option>
|
||||
<option value="sales_tax">Output VAT (Sales)</option>
|
||||
<option value="purchase_tax">Input VAT (Purchase)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label mb-1 small">Department</label>
|
||||
<select id="f_department" class="form-select form-select-sm" style="width:200px;">
|
||||
<option value="0">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary btn-sm" onclick="load_report()">
|
||||
<i class="ti ti-search me-1"></i>Run
|
||||
</button>
|
||||
</div>
|
||||
<div class="col ms-auto d-flex align-items-end justify-content-end">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||
<i class="ti ti-printer me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div class="row g-3 mb-4" id="vat_summary" style="display:none;">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Total Debit</p>
|
||||
<h4 class="mb-0 fw-bold" id="vat_debit">—</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Total Credit</p>
|
||||
<h4 class="mb-0 fw-bold" id="vat_credit">—</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Net VAT (Credit − Debit)</p>
|
||||
<h4 class="mb-0 fw-bold" id="vat_net">—</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0 table-sm">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Period</th>
|
||||
<th>Type</th>
|
||||
<th>Department</th>
|
||||
<th>Reference</th>
|
||||
<th>Account</th>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Debit</th>
|
||||
<th class="text-end">Credit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vat_tbody">
|
||||
<tr><td colspan="10" class="text-center text-muted py-5">Select a date range and click Run.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,5 +114,95 @@
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
var now = new Date();
|
||||
var curMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
|
||||
document.getElementById('f_from').value = curMonth + '-01';
|
||||
document.getElementById('f_to').value = curMonth + '-' + String(new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()).padStart(2,'0');
|
||||
flatpickr('#f_from, #f_to', { dateFormat: 'Y-m-d', altInput: true, altFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
var cat_labels = { sales_tax: 'Output VAT', purchase_tax: 'Input VAT' };
|
||||
var src_labels = {
|
||||
invoice: 'Invoice', credit_note: 'Credit Note',
|
||||
purchase_invoice: 'Purchase Invoice', supplier_credit_note: 'Supplier Credit Note',
|
||||
receipt: 'Receipt', payment: 'Payment', manual: 'Manual',
|
||||
};
|
||||
|
||||
load_departments();
|
||||
|
||||
function load_departments() {
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/department.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var html = '<option value="0">All Departments</option>';
|
||||
(res.output || []).filter(function(d){ return String(d.status) === '1'; }).forEach(function(d) {
|
||||
html += '<option value="' + d.id + '">' + escape_html(d.dept_code + ' - ' + d.dept_name) + '</option>';
|
||||
});
|
||||
document.getElementById('f_department').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_report() {
|
||||
var from = document.getElementById('f_from').value;
|
||||
var to = document.getElementById('f_to').value;
|
||||
var cat = document.getElementById('f_cat').value;
|
||||
var dept = document.getElementById('f_department').value || '0';
|
||||
if (!from || !to) { alert('Select both dates.'); return; }
|
||||
|
||||
document.getElementById('vat_tbody').innerHTML =
|
||||
'<tr><td colspan="10" class="text-center text-muted py-5"><span class="spinner-border spinner-border-sm me-2"></span>Loading...</td></tr>';
|
||||
document.getElementById('vat_summary').style.display = 'none';
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'accounting/api/engine/get_vat_report.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
from_date: from,
|
||||
to_date: to,
|
||||
categories: cat ? [cat] : [],
|
||||
department_id: dept,
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
var rows = d.rows || [];
|
||||
|
||||
document.getElementById('vat_debit').textContent = format_number(d.total_debit);
|
||||
document.getElementById('vat_credit').textContent = format_number(d.total_credit);
|
||||
document.getElementById('vat_net').textContent = format_number(parseFloat(d.total_credit) - parseFloat(d.total_debit));
|
||||
document.getElementById('vat_summary').style.display = '';
|
||||
|
||||
if (rows.length === 0) {
|
||||
document.getElementById('vat_tbody').innerHTML =
|
||||
'<tr><td colspan="10" class="text-center text-muted py-5">No VAT entries found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
rows.forEach(function(r) {
|
||||
html += '<tr>' +
|
||||
'<td class="small">' + escape_html(r.entry_date || '—') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.period) + '</td>' +
|
||||
'<td class="small"><span class="badge bg-secondary bg-opacity-10 text-secondary">' + escape_html(src_labels[r.source_type] || r.source_type) + '</span></td>' +
|
||||
'<td class="small">' + (r.dept_code ? escape_html(r.dept_code) : '<span class="text-muted">—</span>') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.reference || '—') + '</td>' +
|
||||
'<td class="small">' + escape_html(r.account_code + ' ' + r.account_name) + '</td>' +
|
||||
'<td class="small"><span class="badge bg-info bg-opacity-10 text-info">' + escape_html(cat_labels[r.account_category] || r.account_category) + '</span></td>' +
|
||||
'<td class="small text-muted">' + escape_html(r.description || '—') + '</td>' +
|
||||
'<td class="text-end small">' + (parseFloat(r.debit) ? format_number(r.debit) : '—') + '</td>' +
|
||||
'<td class="text-end small">' + (parseFloat(r.credit) ? format_number(r.credit) : '—') + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
document.getElementById('vat_tbody').innerHTML = html;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user