accounting workflows

This commit is contained in:
Thanakorn S
2026-05-20 10:17:02 +07:00
parent 04a683bd02
commit 7396db6ffc
199 changed files with 23539 additions and 5345 deletions
+79 -59
View File
@@ -45,7 +45,7 @@
<div class="mb-3 col-lg-6">
<label class="form-label">Contact</label>
<input type="text" id="contact" class="form-control"
<input type="text" id="contact" class="form-control contact_search"
placeholder="Search contact..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
@@ -62,6 +62,13 @@
placeholder="DD/MM/YYYY (optional)" autocomplete="off">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Department</label>
<select id="department_id" class="form-select">
<option value="0">— No Department —</option>
</select>
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Notes / Terms</label>
<textarea id="notes" class="form-control" rows="2"
@@ -91,6 +98,7 @@
<th>Description</th>
<th style="width:90px;">Qty</th>
<th style="width:120px;">Unit Price</th>
<th style="width:80px;">Tax%</th>
<th style="width:120px;">Total</th>
<th style="width:40px;"></th>
</tr>
@@ -129,10 +137,14 @@
<input type="number" id="discount" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Tax</span>
<span id="summary_tax" class="fw-semibold">0.00</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted">Tax (VAT)</span>
<input type="number" id="tax" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()">
<span class="text-muted small">Tax Adj.</span>
<input type="number" id="tax_adjustment" class="form-control form-control-sm text-end"
style="width:90px;" value="0" min="-0.3" max="0.3" step="0.01" oninput="recalc_totals()">
</div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5">
@@ -193,6 +205,7 @@
<?php require '../include_ending.php'; ?>
<script>
// custom.js globals: remove_item(), init_product_search_inputs(), escape_html(), format_number()
var quotation_id = <?php echo $quotation_id; ?>;
var quotation_data = null;
@@ -211,19 +224,26 @@
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
var subtotal = 0;
var tax_sum = 0;
$('#item_tbody tr').each(function() {
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var total = qty * price;
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
var total = qty * price;
var tax_amt = round_dp(total * tax_rate / 100, 4);
$(this).find('.item_total').text(format_number(total, 2));
$(this).find('.item_tax_amount').val(tax_amt);
subtotal += total;
tax_sum += tax_amt;
});
var discount = parseFloat($('#discount').val()) || 0;
var tax = parseFloat($('#tax').val()) || 0;
var grand = subtotal - discount + tax;
var discount = parseFloat($('#discount').val()) || 0;
var tax_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
var derived_tax = round_dp(tax_sum, 2);
var grand = subtotal - discount + derived_tax + tax_adjustment;
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_tax').text(format_number(derived_tax, 2));
$('#summary_grand_total').text(format_number(grand, 2));
}
@@ -234,12 +254,13 @@
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_sku"
placeholder="SKU..." value="${escape_html(item.product_sku || '')}" autocomplete="off">
<input type="text" class="form-control form-control-sm item_sku product_search"
placeholder="SKU..." value="${escape_html(item.product_sku || '')}" autocomplete="off"
data-product-name-target=".item_desc">
</td>
<td>
<input type="text" class="form-control form-control-sm item_desc"
placeholder="Description..." value="${escape_html(item.description || '')}" autocomplete="off">
placeholder="Description..." value="${escape_html(item.product_name || item.description || '')}" autocomplete="off">
</td>
<td>
<input type="number" class="form-control form-control-sm item_qty"
@@ -249,6 +270,11 @@
<input type="number" class="form-control form-control-sm item_price"
value="${item.unit_price || 0}" min="0" step="any" oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_tax_rate"
value="${parseFloat(item.tax_rate || 0).toFixed(2)}" min="0" max="100" step="0.01" oninput="recalc_totals()">
<input type="hidden" class="item_tax_amount" value="${item.tax_amount || 0}">
</td>
<td class="item_total text-end align-middle">
${format_number((item.quantity || 1) * (item.unit_price || 0), 2)}
</td>
@@ -259,30 +285,34 @@
</td>
</tr>`;
$('#item_tbody').append(row);
if (window.init_product_search_inputs) {
window.init_product_search_inputs(`#item_row_${idx}`);
}
recalc_totals();
}
function remove_item(idx) {
$(`#item_row_${idx}`).remove();
recalc_totals();
}
// ── Collect items from DOM ────────────────────────────────────────────────
function collect_items() {
var items = [];
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var desc = $(this).find('.item_desc').val().trim();
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var sku = $(this).find('.item_sku').val().trim();
var desc = $(this).find('.item_desc').val().trim();
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
var tax_amt = parseFloat($(this).find('.item_tax_amount').val()) || 0;
if (!desc && !sku && qty === 0) return;
items.push({
product_sku: sku,
description: desc,
quantity: qty,
unit_price: price,
total_price: parseFloat((qty * price).toFixed(4)),
product_sku: sku,
product_name: desc,
description: desc,
quantity: qty,
unit_price: price,
total_price: round_dp(qty * price, 4),
tax_rate: tax_rate,
tax_amount: tax_amt,
});
});
return items;
@@ -300,9 +330,9 @@
var is_editable = is_draft;
// Edit controls
$('#contact, #quotation_date, #valid_until, #notes, #discount, #tax, #btn_add_item')
$('#contact, #quotation_date, #valid_until, #department_id, #notes, #discount, #tax_adjustment, #btn_add_item')
.prop('disabled', !is_editable);
$('.item_sku, .item_desc, .item_qty, .item_price').prop('disabled', !is_editable);
$('.item_sku, .item_desc, .item_qty, .item_price, .item_tax_rate').prop('disabled', !is_editable);
$('.remove_item_btn').toggleClass('d-none', !is_editable);
// Action buttons
@@ -343,9 +373,10 @@
$('#contact_id').val(q.contact_id || 0);
$('#quotation_date').val(q.quotation_date ? format_date(q.quotation_date) : '');
$('#valid_until').val(q.valid_until ? format_date(q.valid_until) : '');
$('#department_id').val(q.department_id || 0);
$('#notes').val(q.notes || '');
$('#discount').val(parseFloat(q.discount) || 0);
$('#tax').val(parseFloat(q.tax) || 0);
$('#tax_adjustment').val(parseFloat(q.tax_adjustment) || 0);
$('#item_tbody').empty();
item_index = 0;
@@ -374,12 +405,13 @@
data: {
id: quotation_id,
contact_id: $('#contact_id').val() || 0,
department_id: $('#department_id').val() || 0,
quotation_date: to_iso_date($('#quotation_date').val()),
valid_until: to_iso_date($('#valid_until').val()),
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax: $('#tax').val() || 0,
notes: $('#notes').val(),
discount: $('#discount').val() || 0,
tax_adjustment: $('#tax_adjustment').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!quotation_id && res.new_id) {
@@ -441,7 +473,7 @@
action: 'create',
data: { quotation_id: quotation_id },
onSuccess: function(res) {
retrieve_quotation(); // reload — status now shows Converted
window.location.href = '<?php echo $server_url?>revenue/manage_order.php?id=' + res.order_id;
}
});
}
@@ -449,35 +481,14 @@
}
// ── Contact autocomplete ──────────────────────────────────────────────────
function init_contact_search() {
$('#contact').autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>contact/api/engine/contact.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res((data.output || []).map(c => ({
label: c.contact_name,
value: c.contact_name,
id: c.id,
})));
}
});
},
select: function(e, ui) {
$('#contact_id').val(ui.item.id);
}
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
// ── Boot ──────────────────────────────────────────────────────────────────
$(async function() {
try {
load_departments('department_id');
if (quotation_id) {
await retrieve_quotation();
} else {
@@ -489,11 +500,20 @@
flatpickr('#quotation_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#valid_until', { dateFormat: 'd/m/Y', allowInput: true });
init_contact_search();
} catch(e) { console.log(e); }
});
$(document).on('product:selected', '.item_sku', function(e, product) {
var $row = $(this).closest('tr');
var price = parseFloat(product.price || 0);
if (!(parseFloat($row.find('.item_price').val()) > 0) && price > 0) {
$row.find('.item_price').val(price);
}
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
recalc_totals();
});
</script>
</body>