accounting workflows
This commit is contained in:
@@ -15,15 +15,16 @@
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
$auto_cn = (int)$csm->get('auto_invoice_and_credit_note') === 1;
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
$auto_cn = (int)$csm->get('auto_invoice_and_credit_note') === 1;
|
||||
$auto_approve = (int)$csm->get('default_stock_status') === 1;
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging, $uuid, $auto_cn) {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging, $uuid, $auto_cn, $auto_approve) {
|
||||
$ret = new ReturnManager($pdo, $company_id);
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$invMgmt = new InvoiceManager($pdo, $company_id);
|
||||
$ret->confirmReturn($id, $uuid, $logging, $whMgmt, $invMgmt, $auto_cn);
|
||||
$ret->confirmReturn($id, $uuid, $logging, $whMgmt, $invMgmt, $auto_cn, $auto_approve);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$id = (int)($data['id'] ?? ($data['invoice_id'] ?? 0));
|
||||
$due_date = trim((string)($data['due_date'] ?? ''));
|
||||
|
||||
if (!$id) {
|
||||
$answer['message'] = 'Invalid ID.';
|
||||
@@ -13,9 +14,9 @@
|
||||
}
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $due_date, $company_id, $logging) {
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$inv->issueInvoice($id, $logging);
|
||||
$inv->issueInvoice($id, $logging, $due_date ?: null);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
@@ -31,4 +32,4 @@
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
|
||||
try {
|
||||
$action = $data['action'] ?? 'update';
|
||||
|
||||
if ($action === 'save_formula') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$formula_id = isset($data['formula_id']) && (int)$data['formula_id'] > 0
|
||||
? (int)$data['formula_id'] : null;
|
||||
$inv = new InvoiceManager($pdo2, $company_id);
|
||||
$inv->saveFormula($id, $formula_id);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$inv->saveInvoice($data, $logging);
|
||||
|
||||
@@ -8,19 +8,17 @@
|
||||
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
|
||||
|
||||
try {
|
||||
$new_id = null;
|
||||
$quotation_id = (int)($data['quotation_id'] ?? 0);
|
||||
$new_id = null;
|
||||
$source = trim((string)($data['source'] ?? ''));
|
||||
$source_id = (int)($data['source_id'] ?? 0);
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $quotation_id, &$new_id) {
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $source, $source_id, &$new_id) {
|
||||
$order = new OrderManager($pdo, $company_id);
|
||||
$new_id = $order->saveOrder($data, $logging);
|
||||
|
||||
// On new order created from a quotation: link both records
|
||||
if ($new_id && $quotation_id > 0) {
|
||||
$pdo->prepare(
|
||||
"UPDATE ac_quotation SET order_id = :order_id, status = 5
|
||||
WHERE id = :id AND company_id = :cid AND status = 2"
|
||||
)->execute([':order_id' => $new_id, ':id' => $quotation_id, ':cid' => $company_id]);
|
||||
if ($new_id && $source === 'quotation' && $source_id > 0) {
|
||||
$order->linkQuotationToOrder($source_id, $new_id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -39,4 +37,4 @@
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -4,9 +4,21 @@
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/ReturnManager.php';
|
||||
|
||||
$action = $data['action'] ?? 'save';
|
||||
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
|
||||
|
||||
try {
|
||||
if ($action === 'save_formula') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$formula_id = isset($data['formula_id']) && (int)$data['formula_id'] > 0
|
||||
? (int)$data['formula_id'] : null;
|
||||
if (!$id) throw new Exception('Missing return id.');
|
||||
$ret = new ReturnManager($pdo2, $company_id);
|
||||
$ret->saveFormula($id, $formula_id);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$new_id = null;
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
require '../../../assets/utils/classes/InvoiceManager.php';
|
||||
|
||||
$return_id = (int)($data['return_id'] ?? 0);
|
||||
|
||||
if (!$return_id) {
|
||||
$answer['message'] = 'Invalid return ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$new_id = null;
|
||||
|
||||
dbTransaction($pdo2, function($pdo) use ($return_id, $company_id, $logging, &$new_id) {
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$new_id = $inv->createFromReturn($return_id, $logging);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['new_id'] = $new_id;
|
||||
|
||||
} 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));
|
||||
?>
|
||||
@@ -17,41 +17,23 @@
|
||||
|
||||
// Fetch linked credit notes for invoice detail page
|
||||
if ($result['doc_type'] === 'invoice') {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT * FROM td_invoice
|
||||
WHERE company_id = :company_id
|
||||
AND ref_invoice_id = :ref_id
|
||||
AND doc_type = 'credit_note'
|
||||
ORDER BY id DESC"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':ref_id' => $id]);
|
||||
$answer['credit_notes'] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
$answer['credit_notes'] = $inv->getLinkedCreditNotes($id, 'credit_note');
|
||||
}
|
||||
|
||||
if ($result['doc_type'] === 'purchase_invoice') {
|
||||
$answer['supplier_credit_notes'] = $inv->getLinkedCreditNotes($id, 'supplier_credit_note');
|
||||
}
|
||||
} else {
|
||||
$order_id = (int)($data['order_id'] ?? 0);
|
||||
$doc_type = $data['doc_type'] ?? '';
|
||||
$answer['output'] = $inv->getInvoiceList($order_id, $doc_type);
|
||||
|
||||
// For credit note tab — join ref invoice number
|
||||
if ($doc_type === 'credit_note') {
|
||||
$rows = $answer['output'];
|
||||
foreach ($rows as &$row) {
|
||||
if ((int)$row['ref_invoice_id'] > 0) {
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT invoice_number FROM td_invoice
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':id' => $row['ref_invoice_id'], ':company_id' => $company_id]);
|
||||
$row['ref_invoice_number'] = $sth->fetchColumn() ?: '—';
|
||||
} else {
|
||||
$row['ref_invoice_number'] = '—';
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
$answer['output'] = $rows;
|
||||
// For credit note / debit note tabs — join ref invoice number
|
||||
if ($doc_type === 'credit_note' || $doc_type === 'supplier_credit_note') {
|
||||
$answer['output'] = $inv->addReferenceInvoiceNumbers($answer['output']);
|
||||
}
|
||||
}
|
||||
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -16,85 +16,10 @@
|
||||
}
|
||||
$answer['output'] = $result;
|
||||
|
||||
// Check if an active invoice already exists for this order
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT COUNT(*) FROM td_invoice
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND doc_type = 'invoice'
|
||||
AND status != 4"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':order_id' => $id]);
|
||||
$answer['has_active_invoice'] = (int)$sth->fetchColumn() > 0;
|
||||
|
||||
// Calculate returnable items — order items minus already confirmed returns
|
||||
$order_items = $result['items'] ?? '[]';
|
||||
|
||||
// Aggregate returned qty per item_id from confirmed returns
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT items FROM td_return
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND status = 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':order_id' => $id]);
|
||||
$returned_qty = [];
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_COLUMN) as $items_json) {
|
||||
$items = json_decode($items_json, true) ?: [];
|
||||
foreach ($items as $item) {
|
||||
$item_id = (int)($item['item_id'] ?? -1);
|
||||
if ($item_id < 0) continue;
|
||||
$returned_qty[$item_id] = ($returned_qty[$item_id] ?? 0) + (float)$item['quantity'];
|
||||
}
|
||||
}
|
||||
|
||||
$returnable = [];
|
||||
foreach ($order_items as $i => $item) {
|
||||
$already_returned = $returned_qty[$i] ?? 0;
|
||||
$remaining = (float)$item['quantity'] - $already_returned;
|
||||
if ($remaining > 0) {
|
||||
$stock_out_warehouse_id = (int)($item['warehouse_id'] ?? 0);
|
||||
$stock_out_id = (int)($item['stock_out_id'] ?? 0);
|
||||
|
||||
if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) {
|
||||
$table = 'td_stock_' . $stock_out_warehouse_id;
|
||||
$sth = $pdo2->prepare(
|
||||
"SELECT id, status, zone, aisle, rack, lot_number, serial_number
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND id = :id
|
||||
AND type = 'out'
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $company_id,
|
||||
':id' => $stock_out_id,
|
||||
]);
|
||||
$stock_out = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($stock_out) {
|
||||
$item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? '');
|
||||
$item['aisle'] = $stock_out['aisle'] ?? ($item['aisle'] ?? '');
|
||||
$item['rack'] = $stock_out['rack'] ?? ($item['rack'] ?? '');
|
||||
$item['lot_number'] = $stock_out['lot_number'] ?? ($item['lot_number'] ?? '');
|
||||
$item['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? '');
|
||||
$item['stock_out_status'] = (int)$stock_out['status'];
|
||||
}
|
||||
}
|
||||
|
||||
$returnable[] = array_merge($item, [
|
||||
'item_id' => $i,
|
||||
'original_qty' => (float)$item['quantity'],
|
||||
'returned_qty' => $already_returned,
|
||||
'returnable_qty' => $remaining,
|
||||
'stock_out_warehouse_id' => $stock_out_warehouse_id,
|
||||
'stock_out_status' => (int)($item['stock_out_status'] ?? 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$answer['returnable_items'] = $returnable;
|
||||
$active_invoice_id = $order->getActiveInvoiceId($id);
|
||||
$answer['has_active_invoice'] = $active_invoice_id > 0;
|
||||
$answer['active_invoice_id'] = $active_invoice_id;
|
||||
$answer['returnable_items'] = $order->getReturnableItems($result);
|
||||
} else {
|
||||
$answer['output'] = $order->getOrderList();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
$answer['message'] = 'Return not found.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$active_cn_id = $ret->getActiveCreditNoteId($id);
|
||||
$result['has_active_cn'] = $active_cn_id > 0 ? 1 : 0;
|
||||
$result['active_cn_id'] = $active_cn_id;
|
||||
|
||||
$answer['output'] = $result;
|
||||
} else {
|
||||
$order_id = (int)($data['order_id'] ?? 0);
|
||||
@@ -21,4 +26,4 @@
|
||||
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// 2=paid 3=overdue 4=void — issue (0→1) handled by issue_invoice.php
|
||||
if (!in_array($status, [2, 3, 4], true)) {
|
||||
// Manual status changes are limited to void. Paid is handled by payments;
|
||||
// overdue is calculated from due_date and is not stored as a status.
|
||||
if ($status !== 4) {
|
||||
$answer['message'] = 'Invalid status value.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
@@ -22,38 +23,8 @@
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $status, $company_id, $logging) {
|
||||
|
||||
$sth = $pdo->prepare(
|
||||
"SELECT status, doc_type, `log` FROM td_invoice
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':id' => $id]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) throw new Exception("Invoice not found.");
|
||||
if ($row['doc_type'] !== 'invoice') throw new Exception("Only invoices can be updated this way.");
|
||||
|
||||
$current = (int)$row['status'];
|
||||
|
||||
if ($current === 4) throw new Exception("Invoice is already void.");
|
||||
if ($current === 0) throw new Exception("Issue the invoice before changing payment status.");
|
||||
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = array_merge($logging, [
|
||||
'action' => 'update_status',
|
||||
'status' => $status,
|
||||
'previous_status' => $current,
|
||||
]);
|
||||
|
||||
$pdo->prepare(
|
||||
"UPDATE td_invoice SET status = :status, `log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':status' => $status,
|
||||
':log' => json_encode($log),
|
||||
':id' => $id,
|
||||
':company_id' => $company_id,
|
||||
]);
|
||||
$inv = new InvoiceManager($pdo, $company_id);
|
||||
$inv->voidInvoice($id, array_merge($logging, ['action' => 'update_status', 'status' => $status]));
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
@@ -69,4 +40,4 @@
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$fulfillment_status = (int)($data['fulfillment_status'] ?? 0);
|
||||
$tracking_number = trim((string)($data['shipping_tracking_number'] ?? ''));
|
||||
|
||||
if (!$id) {
|
||||
@@ -19,9 +18,9 @@
|
||||
$auto_complete = (int)$csm->get('auto_complete_on_ship') === 1;
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $fulfillment_status, $tracking_number, $company_id, $logging, $auto_complete) {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $tracking_number, $company_id, $logging, $auto_complete) {
|
||||
$order = new OrderManager($pdo, $company_id);
|
||||
$order->updateFulfillmentStatus($id, $fulfillment_status, $tracking_number, $logging, $auto_complete);
|
||||
$order->updateShippingTracking($id, $tracking_number, $logging, $auto_complete);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$payment_status = (int)($data['payment_status'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
$answer['message'] = 'Invalid order ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
// Validate payment_status range: 0=unpaid 1=paid 2=partial
|
||||
if (!in_array($payment_status, [0, 1, 2], true)) {
|
||||
$answer['message'] = 'Invalid payment status.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $payment_status, $company_id, $logging) {
|
||||
|
||||
// Load order — verify ownership and that order is not cancelled
|
||||
$sth = $pdo->prepare(
|
||||
"SELECT status, payment_status, `log` FROM td_order
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':id' => $id]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) throw new Exception("Order not found.");
|
||||
if ((int)$row['status'] === -1) throw new Exception("Cannot update payment status of a cancelled order.");
|
||||
|
||||
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
||||
$log[] = array_merge($logging, [
|
||||
'action' => 'update_payment_status',
|
||||
'payment_status' => $payment_status,
|
||||
'previous_status' => (int)$row['payment_status'],
|
||||
]);
|
||||
|
||||
$pdo->prepare(
|
||||
"UPDATE td_order SET
|
||||
payment_status = :payment_status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':payment_status' => $payment_status,
|
||||
':log' => json_encode($log),
|
||||
':id' => $id,
|
||||
':company_id' => $company_id,
|
||||
]);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Payment status updated.';
|
||||
|
||||
} 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));
|
||||
?>
|
||||
+150
-134
@@ -11,61 +11,108 @@
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="row">
|
||||
<div class="row mb-6">
|
||||
<div class="col-12">
|
||||
<div class="mb-6 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Invoices</h1>
|
||||
<p class="mb-0">Invoices and credit notes</p>
|
||||
<p class="mb-0 text-muted">Invoices and credit notes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs" id="invoice_tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#" onclick="switch_tab('invoice'); return false;">
|
||||
<i class="ti ti-file-invoice me-1"></i>Invoices
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" onclick="switch_tab('credit_note'); return false;">
|
||||
<i class="ti ti-file-minus me-1"></i>Credit Notes
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-file-invoice fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Total</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_total">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-warning bg-opacity-10 text-warning rounded-2">
|
||||
<i class="ti ti-pencil fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Draft</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_draft">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-info bg-opacity-10 text-info rounded-2">
|
||||
<i class="ti ti-send fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Issued</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_issued">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-danger bg-opacity-10 text-danger rounded-2">
|
||||
<i class="ti ti-clock fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Overdue</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_overdue">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="row g-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search number or contact...">
|
||||
placeholder="Search invoice number or contact...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3" id="status_filter_wrap">
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_type" class="form-select">
|
||||
<option value="">All Types</option>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="credit_note">Credit Note</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Issued</option>
|
||||
<option value="2">Paid</option>
|
||||
<option value="3">Overdue</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
<option value="4">Void</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary w-100" onclick="retrieve_invoices()">
|
||||
<i class="ti ti-search me-1"></i>Search
|
||||
@@ -75,22 +122,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<div class="table-responsive">
|
||||
|
||||
<!-- Invoice table -->
|
||||
<table class="table table-hover mb-0 table-centered" id="invoice_list">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th class="py-3">Invoice #</th>
|
||||
<th class="py-3">Type</th>
|
||||
<th class="py-3">Order #</th>
|
||||
<th class="py-3">Contact</th>
|
||||
<th class="py-3">Department</th>
|
||||
<th class="py-3">Issued Date</th>
|
||||
<th class="py-3">Due Date</th>
|
||||
<th class="py-3">Grand Total</th>
|
||||
@@ -101,24 +145,6 @@
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
|
||||
<!-- Credit Note table -->
|
||||
<table class="table table-hover mb-0 table-centered d-none" id="cn_list">
|
||||
<thead class="table-warning border-light">
|
||||
<tr>
|
||||
<th class="py-3">CN #</th>
|
||||
<th class="py-3">Order #</th>
|
||||
<th class="py-3">Ref Invoice #</th>
|
||||
<th class="py-3">Contact</th>
|
||||
<th class="py-3">Date</th>
|
||||
<th class="py-3">Amount</th>
|
||||
<th class="py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,121 +158,108 @@
|
||||
|
||||
<script>
|
||||
|
||||
var current_tab = 'invoice';
|
||||
function doc_type_badge(doc_type) {
|
||||
const map = {
|
||||
'invoice': '<span class="badge bg-primary">Invoice</span>',
|
||||
'credit_note': '<span class="badge bg-warning text-dark">Credit Note</span>',
|
||||
};
|
||||
return map[doc_type] ?? '—';
|
||||
}
|
||||
|
||||
function is_overdue(inv) {
|
||||
if (String(inv.status) !== '1' || !inv.due_date) return false;
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
return inv.due_date < today;
|
||||
}
|
||||
|
||||
// ── Badges ────────────────────────────────────────────────────────────────
|
||||
function invoice_status_badge(status) {
|
||||
function invoice_status_badge(inv) {
|
||||
if (inv.payment_state === 'partial') return '<span class="badge bg-warning text-dark">Partial</span>';
|
||||
if (inv.payment_state === 'paid') {
|
||||
return inv.doc_type === 'credit_note'
|
||||
? '<span class="badge bg-success">Settled</span>'
|
||||
: '<span class="badge bg-success">Paid</span>';
|
||||
}
|
||||
if (is_overdue(inv)) return '<span class="badge bg-danger">Overdue</span>';
|
||||
const map = {
|
||||
'0': '<span class="badge bg-secondary">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Issued</span>',
|
||||
'2': '<span class="badge bg-success">Paid</span>',
|
||||
'3': '<span class="badge bg-danger">Overdue</span>',
|
||||
'4': '<span class="badge bg-light text-dark">Void</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
return map[String(inv.status)] ?? '—';
|
||||
}
|
||||
|
||||
|
||||
// ── Tab switch ────────────────────────────────────────────────────────────
|
||||
function switch_tab(tab) {
|
||||
current_tab = tab;
|
||||
|
||||
// Update nav tabs
|
||||
$('#invoice_tabs .nav-link').each(function() {
|
||||
var is_active = $(this).attr('onclick').includes("'" + tab + "'");
|
||||
$(this).toggleClass('active', is_active);
|
||||
});
|
||||
|
||||
// Show/hide tables
|
||||
$('#invoice_list').toggleClass('d-none', tab !== 'invoice');
|
||||
$('#cn_list').toggleClass('d-none', tab !== 'credit_note');
|
||||
|
||||
// Status filter — CN has no draft/paid/overdue, simpler
|
||||
if (tab === 'credit_note') {
|
||||
$('#filter_status').html(`
|
||||
<option value="">All Status</option>
|
||||
<option value="1">Issued</option>
|
||||
<option value="4">Void</option>
|
||||
`);
|
||||
} else {
|
||||
$('#filter_status').html(`
|
||||
<option value="">All Status</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Issued</option>
|
||||
<option value="2">Paid</option>
|
||||
<option value="3">Overdue</option>
|
||||
<option value="4">Void</option>
|
||||
`);
|
||||
}
|
||||
|
||||
retrieve_invoices();
|
||||
}
|
||||
|
||||
|
||||
// ── Retrieve ──────────────────────────────────────────────────────────────
|
||||
function retrieve_invoices() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { doc_type: current_tab },
|
||||
data: { doc_type_group: 'sales' },
|
||||
onSuccess: function(res) {
|
||||
var all = res.output || [];
|
||||
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_st = $('#filter_status').val();
|
||||
|
||||
var data = all.filter(function(i) {
|
||||
var match_kw = !keyword ||
|
||||
(i.invoice_number || '').toLowerCase().includes(keyword) ||
|
||||
(i.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_st === '' || String(i.status) === fil_st;
|
||||
return match_kw && match_st;
|
||||
var all = (res.output || []).filter(function(inv) {
|
||||
return inv.doc_type === 'invoice' || inv.doc_type === 'credit_note';
|
||||
});
|
||||
|
||||
if (current_tab === 'invoice') {
|
||||
render_invoice_table(data);
|
||||
} else {
|
||||
render_cn_table(data);
|
||||
}
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_type = $('#filter_type').val();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
|
||||
var data = all.filter(function(inv) {
|
||||
var match_kw = !keyword ||
|
||||
(inv.invoice_number || '').toLowerCase().includes(keyword) ||
|
||||
(inv.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_type = !fil_type || inv.doc_type === fil_type;
|
||||
var match_st = !fil_status
|
||||
|| String(inv.status) === fil_status
|
||||
|| (fil_status === 'overdue' && is_overdue(inv));
|
||||
var match_dept = !fil_dept || String(inv.department_id) === fil_dept;
|
||||
return match_kw && match_type && match_st && match_dept;
|
||||
});
|
||||
|
||||
var today = new Date().toISOString().slice(0, 10);
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_draft').text(format_number(all.filter(i => String(i.status) === '0').length));
|
||||
$('#stat_issued').text(format_number(all.filter(i => String(i.status) === '1').length));
|
||||
$('#stat_overdue').text(format_number(all.filter(i => is_overdue(i)).length));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS invoice_list');
|
||||
alasql.tables.invoice_list.data = data;
|
||||
$('table#invoice_list tfoot').html(generate_pagination('invoice_list', data.length));
|
||||
change_page_invoice_list(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Render invoice table ──────────────────────────────────────────────────
|
||||
function render_invoice_table(data) {
|
||||
alasql('DROP TABLE IF EXISTS invoice_list');
|
||||
alasql('CREATE TABLE invoice_list');
|
||||
alasql.tables.invoice_list.data = data;
|
||||
|
||||
var page = generate_pagination('invoice_list', data.length);
|
||||
$('table#invoice_list tfoot').html(page);
|
||||
|
||||
if (typeof change_page_invoice_list === 'function') {
|
||||
change_page_invoice_list(1);
|
||||
}
|
||||
}
|
||||
|
||||
function change_page_invoice_list(page_num) {
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var page_data = alasql(`SELECT * FROM invoice_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var rows = alasql(`SELECT * FROM invoice_list LIMIT ${prop_limit} OFFSET ${offset}`);
|
||||
var body = '';
|
||||
|
||||
$.each(page_data, function(i, inv) {
|
||||
var status = parseInt(inv.status);
|
||||
var can_void = status >= 1 && status <= 3;
|
||||
if (!rows.length) {
|
||||
$('table#invoice_list tbody').html(
|
||||
'<tr><td colspan="10" class="text-center py-5 text-muted">No invoices found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(rows, function(i, inv) {
|
||||
var status = parseInt(inv.status);
|
||||
var can_void = inv.doc_type === 'invoice' && status >= 1 && status !== 4;
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${inv.invoice_number}</td>
|
||||
<td class="py-3">${inv.order_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${inv.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3 fw-semibold">${escape_html(inv.invoice_number || '')}</td>
|
||||
<td class="py-3">${doc_type_badge(inv.doc_type)}</td>
|
||||
<td class="py-3">${inv.order_number
|
||||
? `<a href="<?php echo $server_url?>order/manage_order.php?id=${inv.order_id}">${escape_html(inv.order_number)}</a>`
|
||||
: '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${escape_html(inv.contact_name || '—')}</td>
|
||||
<td class="py-3">${get_dept_label(inv.department_id)}</td>
|
||||
<td class="py-3">${inv.issued_date ? format_date(inv.issued_date) : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${inv.due_date ? format_date(inv.due_date) : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(inv.grand_total, 2)}</td>
|
||||
<td class="py-3">${invoice_status_badge(inv.status)}</td>
|
||||
<td class="py-3">${invoice_status_badge(inv)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>order/manage_invoice.php?id=${inv.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
@@ -329,11 +342,14 @@
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(function() {
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_invoices();
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_invoices();
|
||||
});
|
||||
$('#filter_type, #filter_status, #filter_dept').on('change', retrieve_invoices);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
+125
-97
@@ -42,6 +42,8 @@
|
||||
<th>Product</th>
|
||||
<th class="text-end">Qty</th>
|
||||
<th class="text-end">Unit Price</th>
|
||||
<th class="text-end" style="width:60px;">Tax%</th>
|
||||
<th class="text-end">Tax Amt</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -93,15 +95,31 @@
|
||||
<span class="text-muted">Contact:</span>
|
||||
<span class="ms-1 fw-semibold" id="display_contact"></span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Department:</span>
|
||||
<span class="ms-1" id="display_department">—</span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<span class="text-muted">Issued:</span>
|
||||
<span class="ms-1" id="display_issued"></span>
|
||||
</div>
|
||||
<div class="mb-2 small">
|
||||
<div class="mb-2 small" id="due_date_row">
|
||||
<span class="text-muted">Due:</span>
|
||||
<input type="text" id="due_date" class="form-control form-control-sm ms-1 d-inline-block"
|
||||
style="width:140px;" placeholder="DD/MM/YYYY">
|
||||
</div>
|
||||
<div class="mt-3 small">
|
||||
<span class="text-muted">GL Formula:</span>
|
||||
<div class="d-flex gap-1 mt-1">
|
||||
<select id="formula_id" class="form-select form-select-sm">
|
||||
<option value="">— None —</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary btn-sm px-2" id="btn_save_formula"
|
||||
onclick="save_formula()" title="Save formula">
|
||||
<i class="ti ti-device-floppy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 small">
|
||||
<span class="text-muted">Notes:</span>
|
||||
<textarea id="notes" class="form-control form-control-sm mt-1" rows="2"></textarea>
|
||||
@@ -118,32 +136,15 @@
|
||||
<i class="ti ti-device-floppy me-1"></i>Save
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success w-100" id="btn_issue"
|
||||
onclick="issue_invoice()">
|
||||
<i class="ti ti-send me-1"></i>Issue Invoice
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary w-100" id="btn_print"
|
||||
onclick="print_invoice()">
|
||||
<i class="ti ti-printer me-1"></i>Print / Save PDF
|
||||
</button>
|
||||
|
||||
<!-- Status selector (issued and above) -->
|
||||
<div class="" id="invoice_status_group">
|
||||
<label class="form-label small text-muted mb-1">Invoice Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="invoice_status_select" class="form-select form-select-sm">
|
||||
<option value="1">Issued</option>
|
||||
<option value="2">Paid</option>
|
||||
<option value="3">Overdue</option>
|
||||
<option value="4">Void</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary text-nowrap"
|
||||
onclick="update_invoice_status($('#invoice_status_select').val())">
|
||||
<i class="ti ti-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger w-100" id="btn_void"
|
||||
onclick="void_invoice()">
|
||||
<i class="ti ti-ban me-1"></i>Void Invoice
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
@@ -157,6 +158,7 @@
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
// custom.js globals: format_number()
|
||||
|
||||
var invoice_id = <?php echo $invoice_id; ?>;
|
||||
var invoice_data = null;
|
||||
@@ -166,17 +168,34 @@
|
||||
const map = {
|
||||
'invoice': '<span class="badge bg-primary">Invoice</span>',
|
||||
'credit_note': '<span class="badge bg-warning text-dark">Credit Note</span>',
|
||||
'debit_note': '<span class="badge bg-info text-white">Debit Note</span>',
|
||||
'supplier_credit_note': '<span class="badge bg-info text-white">Supplier Credit Note</span>',
|
||||
};
|
||||
return map[doc_type] ?? '—';
|
||||
}
|
||||
|
||||
function invoice_status_badge(status) {
|
||||
function is_invoice_overdue(status, due_date) {
|
||||
if (String(status) !== '1' || !due_date) return false;
|
||||
var d = new Date();
|
||||
var today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
||||
return due_date < today;
|
||||
}
|
||||
|
||||
function invoice_status_badge(status, due_date, payment_state, doc_type) {
|
||||
if (payment_state === 'partial') {
|
||||
return '<span class="badge bg-warning text-dark">Partial</span>';
|
||||
}
|
||||
if (payment_state === 'paid') {
|
||||
return doc_type === 'credit_note'
|
||||
? '<span class="badge bg-success">Settled</span>'
|
||||
: '<span class="badge bg-success">Paid</span>';
|
||||
}
|
||||
if (is_invoice_overdue(status, due_date)) {
|
||||
return '<span class="badge bg-danger">Overdue</span>';
|
||||
}
|
||||
const map = {
|
||||
'0': '<span class="badge bg-secondary">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Issued</span>',
|
||||
'2': '<span class="badge bg-success">Paid</span>',
|
||||
'3': '<span class="badge bg-danger">Overdue</span>',
|
||||
'4': '<span class="badge bg-light text-dark">Void</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
@@ -184,31 +203,26 @@
|
||||
|
||||
|
||||
function set_mode(inv) {
|
||||
var status = parseInt(inv.status);
|
||||
var doc_type = inv.doc_type;
|
||||
var is_invoice = doc_type === 'invoice';
|
||||
var is_draft = status === 0;
|
||||
var is_active = status >= 1 && status <= 3;
|
||||
var status = parseInt(inv.status);
|
||||
var doc_type = inv.doc_type;
|
||||
var is_invoice = doc_type === 'invoice';
|
||||
var is_cn = doc_type === 'credit_note';
|
||||
var is_draft = status === 0;
|
||||
var is_void = status === 4;
|
||||
var is_editable = is_draft && (is_invoice || is_cn);
|
||||
|
||||
// Fields editable only on draft invoice
|
||||
$('#due_date, #notes').prop('disabled', !(is_draft && is_invoice));
|
||||
// Due date only relevant for invoices
|
||||
$('#due_date_row').toggleClass('d-none', is_cn);
|
||||
$('#due_date, #notes').prop('disabled', !is_editable);
|
||||
|
||||
// Save + Issue
|
||||
var is_draft_invoice = is_draft && is_invoice;
|
||||
var not_invoice_hint = doc_type !== 'invoice' ? 'Only original invoices can be edited' : '';
|
||||
set_btn_state('#btn_save', is_draft_invoice,
|
||||
not_invoice_hint || 'Only draft invoices can be edited');
|
||||
set_btn_state('#btn_issue', is_draft_invoice,
|
||||
not_invoice_hint || (status > 0 ? 'Invoice has already been issued' : 'Only draft invoices can be issued'));
|
||||
|
||||
// Status selector
|
||||
var can_change_status = is_active && is_invoice;
|
||||
var status_hint = !is_invoice ? 'Status cannot be changed on credit/debit notes'
|
||||
: status === 0 ? 'Issue the invoice first before changing status'
|
||||
: status === 4 ? 'Invoice is void' : '';
|
||||
|
||||
$('#invoice_status_select').prop('disabled', !can_change_status);
|
||||
var not_editable_hint = (!is_invoice && !is_cn) ? 'Only invoices and credit notes can be edited' : '';
|
||||
set_btn_state('#btn_save', is_editable,
|
||||
not_editable_hint || 'Only draft documents can be edited');
|
||||
|
||||
set_btn_state('#btn_void', is_invoice && status >= 1 && status !== 4,
|
||||
!is_invoice ? 'Only original invoices can be voided'
|
||||
: status === 0 ? 'Invoice has not been issued yet'
|
||||
: status === 4 ? 'Invoice is already void' : '');
|
||||
}
|
||||
|
||||
|
||||
@@ -225,12 +239,32 @@
|
||||
|
||||
$('#page_title').text(inv.invoice_number);
|
||||
$('#badge_doc_type').html(doc_type_badge(inv.doc_type));
|
||||
$('#badge_status').html(invoice_status_badge(inv.status));
|
||||
$('#badge_status').html(invoice_status_badge(inv.status, inv.due_date, inv.payment_state, inv.doc_type));
|
||||
$('#display_contact').text(inv.contact_name || '—');
|
||||
$('#display_department').text(get_dept_label(inv.department_id));
|
||||
$('#display_issued').text(format_date(inv.issued_date) || '—');
|
||||
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
|
||||
$('#notes').val(inv.notes || '');
|
||||
|
||||
var gl_type = inv.doc_type === 'credit_note' ? 'sales_credit_note' : 'sales_invoice';
|
||||
var selected_formula = inv.formula_id || '';
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'get_by_type',
|
||||
data: { document_type: gl_type },
|
||||
onSuccess: function(fres) {
|
||||
var opts = '<option value="">— None —</option>';
|
||||
$.each(fres.output || [], function(i, f) {
|
||||
var sel = String(f.id) === String(selected_formula) ? ' selected' : '';
|
||||
opts += '<option value="' + f.id + '"' + sel + '>' + escape_html(f.formula_name) + '</option>';
|
||||
});
|
||||
$('#formula_id').html(opts);
|
||||
}
|
||||
});
|
||||
$('#btn_save_formula').prop('disabled', parseInt(inv.status) === 4);
|
||||
|
||||
if (inv.order_id) {
|
||||
$('#link_order')
|
||||
.attr('href', '<?php echo $server_url?>order/manage_order.php?id=' + inv.order_id)
|
||||
@@ -244,19 +278,29 @@
|
||||
tbody += `<tr>
|
||||
<td>${item.product_name || item.product_sku}</td>
|
||||
<td class="text-end">${format_number(item.quantity, 2)}</td>
|
||||
<td class="text-end">${format_number(item.price, 2)}</td>
|
||||
<td class="text-end">${format_number(item.unit_price, 2)}</td>
|
||||
<td class="text-end">${format_number(item.tax_rate, 2)}</td>
|
||||
<td class="text-end">${format_number(item.tax_amount, 4)}</td>
|
||||
<td class="text-end">${format_number(item.total_price, 2)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#item_tbody').html(tbody);
|
||||
|
||||
// Totals
|
||||
var is_draft = parseInt(inv.status) === 0;
|
||||
var is_editable_doc = inv.doc_type === 'invoice' || inv.doc_type === 'credit_note';
|
||||
var tax_adj_val = parseFloat(inv.tax_adjustment) || 0;
|
||||
var tax_adj_cell = (is_draft && is_editable_doc)
|
||||
? `<input type="number" id="tax_adjustment" class="form-control form-control-sm text-end d-inline-block" style="width:90px;" min="-0.3" max="0.3" step="0.01" value="${tax_adj_val.toFixed(2)}">`
|
||||
: `${format_number(tax_adj_val, 2)}`;
|
||||
|
||||
var tfoot = `
|
||||
<tr><td colspan="3" class="text-muted border-bottom-0">Subtotal</td><td class="text-end border-bottom-0">${format_number(inv.subtotal, 2)}</td></tr>
|
||||
<tr><td colspan="3" class="text-muted border-bottom-0">Discount</td><td class="text-end border-bottom-0">${format_number(inv.discount, 2)}</td></tr>
|
||||
<tr><td colspan="3" class="text-muted border-bottom-0">Tax</td><td class="text-end border-bottom-0">${format_number(inv.tax, 2)}</td></tr>
|
||||
<tr><td colspan="3" class="text-muted">Shipping</td><td class="text-end">${format_number(inv.shipping_fee, 2)}</td></tr>
|
||||
<tr><td colspan="3" class="fw-bold">Grand Total</td><td class="text-end fw-bold">${format_number(inv.grand_total, 2)}</td></tr>`;
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Subtotal</td><td class="text-end border-bottom-0">${format_number(inv.subtotal, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Discount</td><td class="text-end border-bottom-0">${format_number(inv.discount, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Tax</td><td class="text-end border-bottom-0">${format_number(inv.tax, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted border-bottom-0">Tax Adj.</td><td class="text-end border-bottom-0">${tax_adj_cell}</td></tr>
|
||||
<tr><td colspan="5" class="text-muted">Shipping</td><td class="text-end">${format_number(inv.shipping_fee, 2)}</td></tr>
|
||||
<tr><td colspan="5" class="fw-bold">Grand Total</td><td class="text-end fw-bold">${format_number(inv.grand_total, 2)}</td></tr>`;
|
||||
$('#item_tfoot').html(tfoot);
|
||||
|
||||
// Linked credit notes
|
||||
@@ -267,7 +311,7 @@
|
||||
<td>${cn.invoice_number}</td>
|
||||
<td>${format_date(cn.issued_date)}</td>
|
||||
<td class="text-end text-danger">${format_number(cn.grand_total, 2)}</td>
|
||||
<td>${invoice_status_badge(cn.status)}</td>
|
||||
<td>${invoice_status_badge(cn.status, cn.due_date, cn.payment_state, cn.doc_type)}</td>
|
||||
<td><a href="<?php echo $server_url?>order/manage_invoice.php?id=${cn.id}"><i class="ti ti-eye fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
@@ -276,7 +320,6 @@
|
||||
}
|
||||
|
||||
set_mode(inv);
|
||||
$('#invoice_status_select').val(inv.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -289,50 +332,23 @@
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: invoice_id,
|
||||
due_date: $('#due_date').val().split('/').reverse().join('-'),
|
||||
notes: $('#notes').val(),
|
||||
id: invoice_id,
|
||||
due_date: $('#due_date').val().split('/').reverse().join('-'),
|
||||
notes: $('#notes').val(),
|
||||
tax_adjustment: parseFloat($('#tax_adjustment').val()) || 0,
|
||||
},
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function issue_invoice() {
|
||||
|
||||
|
||||
function void_invoice() {
|
||||
bootbox.confirm({
|
||||
message: 'Issue this invoice? It will be sent to the customer.',
|
||||
message: '<strong>Void this invoice?</strong><br>This action cannot be undone.',
|
||||
buttons: {
|
||||
confirm: { label: 'Issue', className: 'btn-success' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/issue_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { invoice_id: invoice_id },
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function update_invoice_status(status) {
|
||||
const labels = { 2: 'Mark as Paid', 3: 'Mark as Overdue', 4: 'Void Invoice' };
|
||||
const classes = { 2: 'btn-success', 3: 'btn-warning', 4: 'btn-danger' };
|
||||
const messages = {
|
||||
2: 'Mark this invoice as paid?',
|
||||
3: 'Mark this invoice as overdue?',
|
||||
4: '<strong>Void this invoice?</strong><br>This action cannot be undone.',
|
||||
};
|
||||
bootbox.confirm({
|
||||
message: messages[status],
|
||||
buttons: {
|
||||
confirm: { label: labels[status], className: classes[status] },
|
||||
confirm: { label: 'Void Invoice', className: 'btn-danger' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
@@ -342,7 +358,7 @@
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: { invoice_id: invoice_id, status: status },
|
||||
data: { invoice_id: invoice_id, status: 4 },
|
||||
onSuccess: function() { retrieve_invoice(); }
|
||||
});
|
||||
}
|
||||
@@ -350,6 +366,19 @@
|
||||
}
|
||||
|
||||
|
||||
function save_formula() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/manage_invoice.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'save_formula',
|
||||
data: { id: invoice_id, formula_id: $('#formula_id').val() || null },
|
||||
onSuccess: function() {
|
||||
bootbox.alert({ message: 'GL formula saved.', size: 'small' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function print_invoice() {
|
||||
if (!invoice_id) return;
|
||||
window.open('<?php echo $server_url?>order/print_invoice.php?id=' + invoice_id, '_blank');
|
||||
@@ -357,13 +386,12 @@
|
||||
|
||||
|
||||
$(function() {
|
||||
if (invoice_id) {
|
||||
retrieve_invoice();
|
||||
}
|
||||
load_dept_cache();
|
||||
if (invoice_id) retrieve_invoice();
|
||||
flatpickr('#due_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+173
-203
@@ -5,6 +5,8 @@
|
||||
|
||||
$order_id = (int)($_GET['id'] ?? 0);
|
||||
$is_edit = isset($_GET['edit']) || $order_id === 0;
|
||||
$source = $order_id ? '' : preg_replace('/[^a-z0-9_-]/i', '', $_GET['source'] ?? '');
|
||||
$source_id = $order_id ? 0 : (int)($_GET['source_id'] ?? 0);
|
||||
?>
|
||||
|
||||
<body>
|
||||
@@ -61,9 +63,10 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Shipping Tracking Number</label>
|
||||
<input type="text" id="shipping_tracking_number" class="form-control"
|
||||
placeholder="Carrier tracking number..." maxlength="100" autocomplete="off">
|
||||
<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">
|
||||
@@ -93,9 +96,10 @@
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Warehouse</th>
|
||||
<th style="width:120px;">Qty</th>
|
||||
<th style="width:130px;">Unit Price</th>
|
||||
<th style="width:130px;">Total</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>
|
||||
</thead>
|
||||
@@ -117,14 +121,26 @@
|
||||
<h2 class="fs-5 mb-3">Status</h2>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<span id="badge_status"></span>
|
||||
<span id="badge_payment"></span>
|
||||
<span id="badge_fulfillment"></span>
|
||||
</div>
|
||||
<p class="mb-0 small text-muted" id="order_number_display"></p>
|
||||
<p class="mb-0 small text-muted d-none" id="source_display"></p>
|
||||
<p class="mb-0 small text-muted d-none" id="tracking_number_display"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning d-none mb-5" id="pending_workflow_hint">
|
||||
<div class="d-flex gap-2">
|
||||
<i class="ti ti-building-warehouse mt-1"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Warehouse assignment required</div>
|
||||
<div class="small">
|
||||
Select a warehouse for every item, then save it. The order will move to Draft and can be confirmed.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totals card -->
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
@@ -140,8 +156,12 @@
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Tax</span>
|
||||
<input type="number" id="tax" class="form-control form-control-sm text-end"
|
||||
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
|
||||
<span id="summary_tax" class="fw-semibold">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<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>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="text-muted">Shipping</span>
|
||||
@@ -180,36 +200,10 @@
|
||||
<i class="ti ti-file-invoice me-1"></i>Proceed to Invoice
|
||||
</button>
|
||||
|
||||
<!-- Update fulfillment status (confirmed and above) -->
|
||||
<div id="fulfillment_status_group">
|
||||
<label class="form-label small text-muted mb-1">Fulfillment Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="fulfillment_status_select" class="form-select form-select-sm">
|
||||
<option value="1">Picking</option>
|
||||
<option value="2">Packed</option>
|
||||
<option value="3">Shipped</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary text-nowrap"
|
||||
onclick="update_fulfillment_status()">
|
||||
<i class="ti ti-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="payment_status_group">
|
||||
<label class="form-label small text-muted mb-1">Payment Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="payment_status_select" class="form-select form-select-sm">
|
||||
<option value="0">Unpaid</option>
|
||||
<option value="1">Paid</option>
|
||||
<option value="2">Partial</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary text-nowrap"
|
||||
onclick="update_payment_status()">
|
||||
<i class="ti ti-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-primary w-100" id="btn_tracking"
|
||||
onclick="update_tracking_number()">
|
||||
<i class="ti ti-truck-delivery me-1"></i>Save Tracking
|
||||
</button>
|
||||
<!-- Create Return (confirmed and above) -->
|
||||
<button class="btn btn-outline-warning w-100" id="btn_return"
|
||||
onclick="create_return()">
|
||||
@@ -234,8 +228,11 @@
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
// custom.js globals: remove_item(), init_product_search_inputs(), format_number()
|
||||
|
||||
var order_id = <?php echo $order_id; ?>;
|
||||
var order_source = <?php echo json_encode($source); ?>;
|
||||
var order_source_id = <?php echo (int)$source_id; ?>;
|
||||
var order_data = null;
|
||||
var warehouses = [];
|
||||
var item_index = 0;
|
||||
@@ -244,21 +241,10 @@
|
||||
// ── Status helpers ────────────────────────────────────────────────────────
|
||||
function order_status_badge(status) {
|
||||
const map = {
|
||||
'-2': '<span class="badge bg-danger">Pending — Awaiting Warehouse</span>',
|
||||
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Confirmed</span>',
|
||||
'2': '<span class="badge bg-primary">Processing</span>',
|
||||
'3': '<span class="badge bg-success">Completed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function payment_status_badge(status) {
|
||||
const map = {
|
||||
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
|
||||
'1': '<span class="badge bg-success">Paid</span>',
|
||||
'2': '<span class="badge bg-info text-white">Partial</span>',
|
||||
};
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
@@ -272,24 +258,45 @@
|
||||
return map[String(status)] ?? '';
|
||||
}
|
||||
|
||||
function source_display_label(source, source_id) {
|
||||
source = String(source || '').trim();
|
||||
source_id = parseInt(source_id) || 0;
|
||||
if (!source) return '';
|
||||
|
||||
const labels = {
|
||||
quotation: 'Quotation',
|
||||
api: 'API',
|
||||
marketplace: 'Marketplace',
|
||||
revenue: 'Revenue',
|
||||
};
|
||||
return (labels[source] || source) + (source_id > 0 ? ' #' + source_id : '');
|
||||
}
|
||||
|
||||
|
||||
// ── 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 shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
|
||||
var grand_total = subtotal - discount + tax + shipping_fee;
|
||||
var discount = parseFloat($('#discount').val()) || 0;
|
||||
var tax_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
|
||||
var shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
|
||||
var derived_tax = round_dp(tax_sum, 2);
|
||||
var grand_total = subtotal - discount + derived_tax + tax_adjustment + shipping_fee;
|
||||
|
||||
$('#summary_subtotal').text(format_number(subtotal, 2));
|
||||
$('#summary_tax').text(format_number(derived_tax, 2));
|
||||
$('#summary_grand_total').text(format_number(grand_total, 2));
|
||||
}
|
||||
|
||||
@@ -299,7 +306,7 @@
|
||||
item = item || {};
|
||||
var idx = item_index++;
|
||||
|
||||
var wh_options = warehouses.map(w =>
|
||||
var wh_options = '<option value="">Select warehouse</option>' + warehouses.map(w =>
|
||||
`<option value="${w.id}" ${item.warehouse_id == w.id ? 'selected' : ''}>${w.warehouse_name}</option>`
|
||||
).join('');
|
||||
|
||||
@@ -307,7 +314,7 @@
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm item_sku product_search"
|
||||
placeholder="SKU..." value="${item.product_sku || ''}" autocomplete="off"
|
||||
data-idx="${idx}">
|
||||
data-idx="${idx}" data-product-name-target=".item_name">
|
||||
<input type="hidden" class="item_name" value="${item.product_name || ''}">
|
||||
<input type="hidden" class="item_id" value="${item.item_id ?? idx}">
|
||||
<small class="text-muted item_name_display">${item.product_name || ''}</small>
|
||||
@@ -323,9 +330,14 @@
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_price"
|
||||
value="${item.price || 0}" min="0" step="any" oninput="recalc_totals()">
|
||||
value="${item.unit_price || item.price || 0}" min="0" step="any" oninput="recalc_totals()">
|
||||
</td>
|
||||
<td class="item_total text-end">${format_number((item.quantity || 1) * (item.price || 0), 2)}</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">${format_number((item.quantity || 1) * (item.unit_price || item.price || 0), 2)}</td>
|
||||
<td>
|
||||
<a href="javascript:void(0);" class="link-danger" onclick="remove_item(${idx})">
|
||||
<i class="ti ti-trash fs-5"></i>
|
||||
@@ -334,13 +346,12 @@
|
||||
</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 ────────────────────────────────────────────────
|
||||
@@ -348,12 +359,15 @@
|
||||
var items = [];
|
||||
var idx = 0;
|
||||
$('#item_tbody tr').each(function() {
|
||||
var sku = $(this).find('.item_sku').val().trim();
|
||||
var name = $(this).find('.item_name').val().trim();
|
||||
var wh = parseInt($(this).find('.item_warehouse').val()) || 0;
|
||||
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.item_price').val()) || 0;
|
||||
if (!sku || !wh || qty <= 0) { idx++; return; }
|
||||
var sku = $(this).find('.item_sku').val().trim();
|
||||
var name = $(this).find('.item_name').val().trim();
|
||||
var wh = parseInt($(this).find('.item_warehouse').val()) || 0;
|
||||
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 (!sku || qty <= 0) { idx++; return; }
|
||||
if (!wh && !(!order_id && order_source)) { idx++; return; }
|
||||
items.push({
|
||||
item_id: idx,
|
||||
product_sku: sku,
|
||||
@@ -365,8 +379,10 @@
|
||||
lot_number: '',
|
||||
serial_number: '',
|
||||
quantity: qty,
|
||||
price: price,
|
||||
total_price: qty * price,
|
||||
unit_price: price,
|
||||
total_price: round_dp(qty * price, 4),
|
||||
tax_rate: tax_rate,
|
||||
tax_amount: tax_amt,
|
||||
stock_out_id: 0,
|
||||
});
|
||||
idx++;
|
||||
@@ -387,42 +403,33 @@
|
||||
var can_confirm = status === 0; // only Draft can be confirmed (not Pending)
|
||||
var can_cancel = status === -2 || status === 0 || status === 1;
|
||||
var is_cancelled = status === -1;
|
||||
var is_completed = status === 3;
|
||||
var can_fulfill = status >= 1 && !is_cancelled && !is_completed;
|
||||
var can_update_tracking = status === 1 && !is_cancelled;
|
||||
|
||||
// Edit controls
|
||||
$('#btn_add_item, #contact, #order_date, #notes, #discount, #tax, #shipping_fee')
|
||||
$('#btn_add_item, #contact, #order_date, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee')
|
||||
.prop('disabled', !is_editable);
|
||||
$('#shipping_tracking_number').prop('disabled', !(is_editable || can_fulfill));
|
||||
$('.item_sku, .item_qty, .item_price, .item_warehouse').prop('disabled', !is_editable);
|
||||
$('.item_sku, .item_qty, .item_price, .item_tax_rate, .item_warehouse').prop('disabled', !is_editable);
|
||||
$('a[onclick^="remove_item"]').toggleClass('d-none', !is_editable);
|
||||
|
||||
// Pending hint
|
||||
if (is_pending) {
|
||||
$('#page_subtitle').text('Assign a warehouse for each item, then save to move this order to Draft.');
|
||||
$('#page_subtitle').text('Pending warehouse assignment');
|
||||
}
|
||||
$('#pending_workflow_hint').toggleClass('d-none', !is_pending);
|
||||
|
||||
// Action buttons
|
||||
set_btn_state('#btn_save', is_draft, 'Only draft orders can be edited');
|
||||
set_btn_state('#btn_confirm', can_confirm, 'Order must be in draft to confirm');
|
||||
set_btn_state('#btn_save', is_editable, 'Only pending or draft orders can be edited');
|
||||
$('#btn_save').html(is_pending
|
||||
? '<i class="ti ti-device-floppy me-1"></i>Save Warehouse'
|
||||
: '<i class="ti ti-device-floppy me-1"></i>Save Draft');
|
||||
set_btn_state('#btn_confirm', can_confirm,
|
||||
is_pending ? 'Assign warehouses and save first. The order will move to Draft.' : 'Order must be in draft to confirm');
|
||||
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
|
||||
is_cancelled ? 'Order is already cancelled' : 'Only draft or confirmed orders can be cancelled');
|
||||
|
||||
// Fulfillment status — confirmed and above, not cancelled, not completed
|
||||
$('#fulfillment_status_group').toggleClass('d-none', !can_fulfill);
|
||||
if (can_fulfill) {
|
||||
// Only show options ahead of current fulfillment
|
||||
$('#fulfillment_status_select option').each(function() {
|
||||
$(this).prop('disabled', parseInt($(this).val()) <= fulfillment_status);
|
||||
});
|
||||
// Default to next step
|
||||
var next = fulfillment_status + 1;
|
||||
if (next <= 3) $('#fulfillment_status_select').val(next);
|
||||
else $('#fulfillment_status_select').val(fulfillment_status);
|
||||
}
|
||||
|
||||
// Payment status selector
|
||||
$('#payment_status_select').prop('disabled', status < 1 || is_cancelled);
|
||||
set_btn_state('#btn_tracking', can_update_tracking,
|
||||
is_cancelled ? 'Order is cancelled'
|
||||
: status < 1 ? 'Confirm the order before adding a tracking number' : '');
|
||||
|
||||
// Create Return
|
||||
var can_return = status >= 1 && !is_cancelled && returnable_count > 0;
|
||||
@@ -464,21 +471,25 @@
|
||||
$('#page_subtitle').text('');
|
||||
$('#status_card').removeClass('d-none');
|
||||
$('#badge_status').html(order_status_badge(o.status));
|
||||
$('#badge_payment').html(payment_status_badge(o.payment_status));
|
||||
$('#badge_fulfillment').html(fulfillment_status_badge(o.fulfillment_status));
|
||||
$('#order_number_display').text(o.order_number);
|
||||
var source_label = source_display_label(o.source || '', o.source_id || 0);
|
||||
$('#source_display')
|
||||
.toggleClass('d-none', !source_label)
|
||||
.text(source_label ? 'Source: ' + source_label : '');
|
||||
var tracking = o.shipping_tracking_number || '';
|
||||
$('#tracking_number_display')
|
||||
.toggleClass('d-none', !o.shipping_tracking_number)
|
||||
.text(o.shipping_tracking_number ? 'Tracking: ' + o.shipping_tracking_number : '');
|
||||
.toggleClass('d-none', !tracking)
|
||||
.html(tracking ? '<i class="ti ti-truck-delivery me-1"></i>' + escape_html(tracking) : '');
|
||||
|
||||
// Fields
|
||||
$('#contact').val(o.contact_name || '');
|
||||
$('#contact_id').val(o.contact_id);
|
||||
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
|
||||
$('#shipping_tracking_number').val(o.shipping_tracking_number || '');
|
||||
$('#department_id').val(o.department_id || 0);
|
||||
$('#notes').val(o.notes || '');
|
||||
$('#discount').val(o.discount || 0);
|
||||
$('#tax').val(o.tax || 0);
|
||||
$('#tax_adjustment').val(o.tax_adjustment || 0);
|
||||
$('#shipping_fee').val(o.shipping_fee || 0);
|
||||
|
||||
// Items
|
||||
@@ -487,7 +498,6 @@
|
||||
|
||||
recalc_totals();
|
||||
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length, o.fulfillment_status);
|
||||
$('#payment_status_select').val(o.payment_status ?? 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -495,6 +505,17 @@
|
||||
|
||||
// ── Save draft ────────────────────────────────────────────────────────────
|
||||
function save_order() {
|
||||
var missing_warehouse = false;
|
||||
$('#item_tbody tr').each(function() {
|
||||
var sku = $(this).find('.item_sku').val().trim();
|
||||
var wh = parseInt($(this).find('.item_warehouse').val()) || 0;
|
||||
if (sku && !wh) missing_warehouse = true;
|
||||
});
|
||||
if (missing_warehouse && !(!order_id && order_source)) {
|
||||
bootbox.alert('Select a warehouse for every item before saving.');
|
||||
return;
|
||||
}
|
||||
|
||||
var items = collect_items();
|
||||
if (!items.length) {
|
||||
bootbox.alert('Please add at least one item.');
|
||||
@@ -511,15 +532,17 @@
|
||||
checkRequired: 0,
|
||||
action: order_id ? 'update' : 'create',
|
||||
data: {
|
||||
id: order_id,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
order_date: to_iso_date($('#order_date').val()),
|
||||
items: JSON.stringify(items),
|
||||
discount: $('#discount').val() || 0,
|
||||
tax: $('#tax').val() || 0,
|
||||
shipping_fee: $('#shipping_fee').val() || 0,
|
||||
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
|
||||
notes: $('#notes').val(),
|
||||
id: order_id,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
department_id: $('#department_id').val() || 0,
|
||||
order_date: to_iso_date($('#order_date').val()),
|
||||
items: JSON.stringify(items),
|
||||
discount: $('#discount').val() || 0,
|
||||
tax_adjustment: $('#tax_adjustment').val() || 0,
|
||||
shipping_fee: $('#shipping_fee').val() || 0,
|
||||
source: order_id ? '' : order_source,
|
||||
source_id: order_id ? 0 : order_source_id,
|
||||
notes: $('#notes').val(),
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
if (!order_id && res.new_id) {
|
||||
@@ -557,43 +580,33 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Update fulfillment status ─────────────────────────────────────────────
|
||||
function update_fulfillment_status() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/update_order_status.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: order_id,
|
||||
fulfillment_status: $('#fulfillment_status_select').val(),
|
||||
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
retrieve_order();
|
||||
// ── Update tracking number ────────────────────────────────────────────────
|
||||
function update_tracking_number() {
|
||||
var current = (order_data && order_data.shipping_tracking_number) ? order_data.shipping_tracking_number : '';
|
||||
bootbox.prompt({
|
||||
title: '<i class="ti ti-truck-delivery me-2"></i>Shipping Tracking Number',
|
||||
value: current,
|
||||
inputType: 'text',
|
||||
placeholder: 'Enter carrier tracking number...',
|
||||
callback: function(result) {
|
||||
if (result === null) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/update_order_status.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: order_id,
|
||||
shipping_tracking_number: result.trim(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
retrieve_order();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Update payment status ─────────────────────────────────────────────────
|
||||
function update_payment_status() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/update_payment_status.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
id: order_id,
|
||||
payment_status: $('#payment_status_select').val(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
retrieve_order();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Create return ─────────────────────────────────────────────────────────
|
||||
function create_return() {
|
||||
window.location.href = '<?php echo $server_url?>order/manage_return.php?order_id=' + order_id;
|
||||
@@ -656,67 +669,23 @@
|
||||
}
|
||||
|
||||
|
||||
// ── 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);
|
||||
});
|
||||
|
||||
|
||||
// ── Product autocomplete (per row) ────────────────────────────────────────
|
||||
function init_product_search() {
|
||||
$(document).on('input', '.item_sku', function() {
|
||||
var $input = $(this);
|
||||
var $row = $input.closest('tr');
|
||||
$input.autocomplete({
|
||||
source: function(req, res) {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>inventory/api/engine/product.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { keyword: req.term },
|
||||
onSuccess: function(data) {
|
||||
res((data.output || []).map(p => ({
|
||||
label: p.sku + ' — ' + p.product_name,
|
||||
value: p.sku,
|
||||
name: p.product_name,
|
||||
})));
|
||||
}
|
||||
});
|
||||
},
|
||||
select: function(e, ui) {
|
||||
$row.find('.item_name').val(ui.item.name);
|
||||
$row.find('.item_name_display').text(ui.item.name);
|
||||
recalc_totals();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
$(document).on('product:selected', '.item_sku', function(e, product) {
|
||||
var $row = $(this).closest('tr');
|
||||
$row.find('.item_name').val(product.product_name || '');
|
||||
$row.find('.item_name_display').text(product.product_name || '');
|
||||
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
|
||||
recalc_totals();
|
||||
});
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
load_departments('department_id');
|
||||
await retrieve_warehouses();
|
||||
|
||||
if (order_id) {
|
||||
@@ -727,14 +696,15 @@
|
||||
$('#order_date').val(today);
|
||||
add_item_row();
|
||||
set_mode(0);
|
||||
if (order_source) {
|
||||
$('#page_title').text('New Revenue Sales Order');
|
||||
$('#page_subtitle').text('Create a pending sales order for WMS warehouse assignment');
|
||||
}
|
||||
}
|
||||
|
||||
// Init flatpickr on order date
|
||||
flatpickr('#order_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
init_contact_search();
|
||||
init_product_search();
|
||||
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
+121
-31
@@ -70,6 +70,11 @@
|
||||
placeholder="DD/MM/YYYY" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Department</label>
|
||||
<p class="form-control-plaintext" id="display_department">—</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-12">
|
||||
<label class="form-label">Reason</label>
|
||||
<textarea id="reason" class="form-control" rows="2"
|
||||
@@ -99,13 +104,14 @@
|
||||
<th class="th_return_aisle" style="width:120px;">Aisle</th>
|
||||
<th class="th_return_rack" style="width:120px;">Location</th>
|
||||
<th class="text-end" style="width:110px;">Return Qty</th>
|
||||
<th class="text-end" style="width:120px;">Unit Price</th>
|
||||
<th class="text-end" style="width:120px;">Total</th>
|
||||
<th class="text-end" style="width:100px;">Unit Price</th>
|
||||
<th style="width:70px;">Tax%</th>
|
||||
<th class="text-end" style="width:110px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item_tbody">
|
||||
<tr id="loading_row">
|
||||
<td colspan="11" class="text-center text-muted py-4">
|
||||
<td colspan="12" class="text-center text-muted py-4">
|
||||
<i class="ti ti-loader me-1"></i>Loading items...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -134,6 +140,20 @@
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Refund Summary</h2>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Subtotal</span>
|
||||
<span id="summary_subtotal">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-muted">Tax</span>
|
||||
<span id="summary_tax">0.00</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<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;" min="-0.3" max="0.3" step="0.01" value="0" oninput="recalc_totals()">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between fw-bold fs-5">
|
||||
<span>Total Refund</span>
|
||||
<span id="summary_refund">0.00</span>
|
||||
@@ -155,6 +175,15 @@
|
||||
<i class="ti ti-check me-1"></i>Confirm Return
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning w-100" id="btn_create_cn"
|
||||
onclick="proceed_to_credit_note()">
|
||||
<i class="ti ti-file-plus me-1"></i>Create Credit Note
|
||||
</button>
|
||||
|
||||
<a href="#" class="btn btn-outline-warning w-100 d-none" id="btn_view_cn">
|
||||
<i class="ti ti-file-invoice me-1"></i>View Credit Note
|
||||
</a>
|
||||
|
||||
<button class="btn btn-outline-danger w-100" id="btn_cancel"
|
||||
onclick="cancel_return()">
|
||||
<i class="ti ti-x me-1"></i>Cancel Return
|
||||
@@ -171,6 +200,7 @@
|
||||
<?php require '../include_ending.php'; ?>
|
||||
|
||||
<script>
|
||||
// custom.js globals: escape_html(), format_number()
|
||||
|
||||
var return_id = <?php echo $return_id; ?>;
|
||||
var return_data = null;
|
||||
@@ -196,18 +226,23 @@
|
||||
|
||||
// ── Recalculate totals ────────────────────────────────────────────────────
|
||||
function recalc_totals() {
|
||||
var total = 0;
|
||||
var subtotal = 0, tax_sum = 0;
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var $row = $(this);
|
||||
var checked = $row.find('.item_check').is(':checked');
|
||||
if (!checked) return;
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var t = qty * price;
|
||||
var $row = $(this);
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
|
||||
var t = qty * price;
|
||||
$row.find('.item_total').text(format_number(t, 2));
|
||||
total += t;
|
||||
subtotal += t;
|
||||
tax_sum += round_dp(t * tax_rate / 100, 4);
|
||||
});
|
||||
$('#summary_refund').text(format_number(total, 2));
|
||||
var tax_adj = parseFloat($('#tax_adjustment').val()) || 0;
|
||||
var tax = round_dp(tax_sum, 2) + tax_adj;
|
||||
$('#summary_subtotal').text(format_number(subtotal, 2));
|
||||
$('#summary_tax').text(format_number(tax, 2));
|
||||
$('#summary_refund').text(format_number(subtotal + tax, 2));
|
||||
}
|
||||
|
||||
function escape_html(value) {
|
||||
@@ -235,7 +270,7 @@
|
||||
$('.th_return_zone').text(label_zone).toggleClass('d-none', !advanced);
|
||||
$('.th_return_aisle').text(label_aisle).toggleClass('d-none', !advanced);
|
||||
$('.th_return_rack').text(label_rack);
|
||||
$('#loading_row td').attr('colspan', 11);
|
||||
$('#loading_row td').attr('colspan', 12);
|
||||
}
|
||||
|
||||
function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_rack) {
|
||||
@@ -449,8 +484,9 @@
|
||||
var si = selected_map[item.item_id] || null;
|
||||
var checked = si ? 'checked' : '';
|
||||
var received = return_data && parseInt(return_data.status) === 1 && si;
|
||||
var qty = si ? si.quantity : item.returnable_qty;
|
||||
var price = item.price || 0;
|
||||
var qty = si ? si.quantity : item.returnable_qty;
|
||||
var price = item.price || 0;
|
||||
var tax_rate = si ? (parseFloat(si.tax_rate) || 0) : (parseFloat(item.tax_rate) || 0);
|
||||
var destination = {
|
||||
warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''),
|
||||
zone: si ? (si.zone || '') : (item.zone || ''),
|
||||
@@ -499,6 +535,13 @@
|
||||
${!checked ? 'disabled' : ''}
|
||||
oninput="recalc_totals()">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm item_tax_rate"
|
||||
value="${tax_rate.toFixed(2)}"
|
||||
min="0" max="100" step="0.01"
|
||||
${!checked ? 'disabled' : ''}
|
||||
oninput="recalc_totals()">
|
||||
</td>
|
||||
<td class="text-end item_total">${checked ? format_number(qty * price, 2) : '—'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
@@ -511,7 +554,7 @@
|
||||
function toggle_item_row(item_id) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var checked = $row.find('.item_check').is(':checked');
|
||||
$row.find('.item_qty, .item_price, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack').prop('disabled', !checked);
|
||||
$row.find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack').prop('disabled', !checked);
|
||||
if (!checked) {
|
||||
$row.find('.item_total').text('—');
|
||||
return;
|
||||
@@ -609,8 +652,9 @@
|
||||
var $row = $(this);
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var qty = parseFloat($row.find('.item_qty').val()) || 0;
|
||||
var price = parseFloat($row.find('.item_price').val()) || 0;
|
||||
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
|
||||
if (qty <= 0) return;
|
||||
var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0;
|
||||
var return_rack = $row.find('.item_return_rack').val() || '';
|
||||
@@ -634,9 +678,11 @@
|
||||
stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0,
|
||||
stock_out_warehouse_id: parseInt($row.find('.item_stock_out_wh').val()) || 0,
|
||||
stock_out_status: parseInt($row.find('.item_stock_out_status').val()) || 0,
|
||||
quantity: qty,
|
||||
price: price,
|
||||
total_price: qty * price,
|
||||
quantity: qty,
|
||||
price: price,
|
||||
tax_rate: tax_rate,
|
||||
tax_amount: round_dp(qty * price * tax_rate / 100, 4),
|
||||
total_price: round_dp(qty * price, 4),
|
||||
});
|
||||
});
|
||||
return items;
|
||||
@@ -657,14 +703,15 @@
|
||||
}
|
||||
|
||||
return {
|
||||
id: return_id,
|
||||
order_id: $('#order_id').val() || 0,
|
||||
invoice_id: $('#invoice_id').val() || 0,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
return_date: $('#return_date').val().split('/').reverse().join('-'),
|
||||
reason: $('#reason').val(),
|
||||
items: JSON.stringify(items),
|
||||
notes: '',
|
||||
id: return_id,
|
||||
order_id: $('#order_id').val() || 0,
|
||||
invoice_id: $('#invoice_id').val() || 0,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
return_date: $('#return_date').val().split('/').reverse().join('-'),
|
||||
reason: $('#reason').val(),
|
||||
items: JSON.stringify(items),
|
||||
notes: '',
|
||||
tax_adjustment: parseFloat($('#tax_adjustment').val()) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -686,13 +733,17 @@
|
||||
// ── Mode ──────────────────────────────────────────────────────────────────
|
||||
function set_mode(status) {
|
||||
status = parseInt(status);
|
||||
var is_draft = status === 0;
|
||||
var is_draft = status === 0;
|
||||
var is_confirmed = status === 1;
|
||||
var has_cn = return_data && return_data.has_active_cn;
|
||||
var cn_id = return_data && return_data.active_cn_id;
|
||||
|
||||
$('#return_date, #reason, #invoice_id').prop('disabled', !is_draft);
|
||||
$('#tax_adjustment').prop('disabled', !is_draft);
|
||||
$('.item_check').prop('disabled', !is_draft);
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var checked = $(this).find('.item_check').is(':checked');
|
||||
$(this).find('.item_qty, .item_price, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack')
|
||||
$(this).find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack')
|
||||
.prop('disabled', !is_draft || !checked);
|
||||
});
|
||||
|
||||
@@ -703,6 +754,16 @@
|
||||
if (!return_id) {
|
||||
$('#btn_confirm').prop('disabled', true);
|
||||
}
|
||||
|
||||
set_btn_state('#btn_create_cn', is_confirmed && !has_cn,
|
||||
!return_id ? 'Save the return first'
|
||||
: !is_confirmed ? 'Confirm the return before creating a credit note'
|
||||
: 'A credit note already exists for this return');
|
||||
if (has_cn && cn_id) {
|
||||
$('#btn_view_cn').attr('href', '<?php echo $server_url?>order/manage_invoice.php?id=' + cn_id);
|
||||
}
|
||||
$('#btn_view_cn').toggleClass('d-none', !(is_confirmed && has_cn));
|
||||
|
||||
set_btn_state('#btn_cancel', is_draft || status === 1,
|
||||
status === -1 ? 'Return is already cancelled' : 'Return is completed and cannot be cancelled');
|
||||
}
|
||||
@@ -728,6 +789,8 @@
|
||||
|
||||
$('#return_date').val(r.return_date ? format_date(r.return_date) : '');
|
||||
$('#reason').val(r.reason || '');
|
||||
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
||||
$('#display_department').text(get_dept_label(r.department_id));
|
||||
|
||||
// Load order context (which also renders items with existing selection)
|
||||
await load_order(r.order_id, r.items || []);
|
||||
@@ -750,6 +813,7 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Confirm return ────────────────────────────────────────────────────────
|
||||
function confirm_return() {
|
||||
bootbox.confirm({
|
||||
@@ -775,6 +839,31 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Proceed to credit note ────────────────────────────────────────────────
|
||||
function proceed_to_credit_note() {
|
||||
bootbox.confirm({
|
||||
message: 'Create a credit note for this return?',
|
||||
buttons: {
|
||||
confirm: { label: 'Create Credit Note', className: 'btn-warning' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
},
|
||||
callback: function(result) {
|
||||
if (!result) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/proceed_to_credit_note.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'create',
|
||||
data: { return_id: return_id },
|
||||
onSuccess: function(res) {
|
||||
window.location.href = '<?php echo $server_url?>order/manage_invoice.php?id=' + res.new_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Cancel return ─────────────────────────────────────────────────────────
|
||||
function cancel_return() {
|
||||
bootbox.confirm({
|
||||
@@ -802,6 +891,7 @@
|
||||
$(async function() {
|
||||
try {
|
||||
flatpickr('#return_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
await load_dept_cache();
|
||||
await load_company_setting();
|
||||
await retrieve_warehouses();
|
||||
|
||||
|
||||
+27
-35
@@ -72,11 +72,11 @@
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="card p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
|
||||
<i class="ti ti-circle-check fs-4"></i>
|
||||
<div class="icon-shape icon-md bg-secondary bg-opacity-10 text-secondary rounded-2">
|
||||
<i class="ti ti-x fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Completed</p>
|
||||
<p class="mb-0 text-muted small">Cancelled</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_completed">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,32 +92,27 @@
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search order number, contact, or tracking...">
|
||||
placeholder="Search order number, source, contact, or tracking...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="-2">Pending</option>
|
||||
<option value="-1">Cancelled</option>
|
||||
<option value="0">Draft</option>
|
||||
<option value="1">Confirmed</option>
|
||||
<option value="2">Processing</option>
|
||||
<option value="3">Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_payment" class="form-select">
|
||||
<option value="">All Payment</option>
|
||||
<option value="0">Unpaid</option>
|
||||
<option value="1">Paid</option>
|
||||
<option value="2">Partial</option>
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
@@ -139,14 +134,15 @@
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th class="py-3">Order #</th>
|
||||
<th class="py-3">Source</th>
|
||||
<th class="py-3">Date</th>
|
||||
<th class="py-3">Contact</th>
|
||||
<th class="py-3">Department</th>
|
||||
<th class="py-3">Items</th>
|
||||
<th class="py-3">Grand Total</th>
|
||||
<th class="py-3">Status</th>
|
||||
<th class="py-3">Fulfillment</th>
|
||||
<th class="py-3">Tracking #</th>
|
||||
<th class="py-3">Payment</th>
|
||||
<th class="py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -170,26 +166,14 @@
|
||||
function order_status_badge(status) {
|
||||
status = parseInt(status);
|
||||
const map = {
|
||||
'-2': '<span class="badge bg-danger">Pending</span>',
|
||||
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
|
||||
'-1': '<span class="badge bg-secondary">Cancelled</span>',
|
||||
'0': '<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1': '<span class="badge bg-info text-white">Confirmed</span>',
|
||||
'2': '<span class="badge bg-primary">Processing</span>',
|
||||
'3': '<span class="badge bg-success">Completed</span>',
|
||||
};
|
||||
return map[String(status)] ?? '<span class="badge bg-light text-dark">—</span>';
|
||||
}
|
||||
|
||||
function payment_status_badge(status) {
|
||||
status = parseInt(status);
|
||||
const map = {
|
||||
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
|
||||
'1': '<span class="badge bg-success">Paid</span>',
|
||||
'2': '<span class="badge bg-info text-white">Partial</span>',
|
||||
};
|
||||
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
|
||||
}
|
||||
|
||||
function fulfillment_status_badge(status) {
|
||||
const map = {
|
||||
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
|
||||
@@ -214,23 +198,24 @@
|
||||
// Client-side filter
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_status = $('#filter_status').val();
|
||||
var fil_payment = $('#filter_payment').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
|
||||
var data = all.filter(function(o) {
|
||||
var match_kw = !keyword ||
|
||||
(o.order_number || '').toLowerCase().includes(keyword) ||
|
||||
(o.source || 'direct').toLowerCase().includes(keyword) ||
|
||||
(o.contact_name || '').toLowerCase().includes(keyword) ||
|
||||
(o.shipping_tracking_number || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_status === '' || String(o.status) === fil_status;
|
||||
var match_pm = fil_payment === '' || String(o.payment_status) === fil_payment;
|
||||
return match_kw && match_st && match_pm;
|
||||
var match_st = fil_status === '' || String(o.status) === fil_status;
|
||||
var match_dept = !fil_dept || String(o.department_id) === fil_dept;
|
||||
return match_kw && match_st && match_dept;
|
||||
});
|
||||
|
||||
// Stat cards
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_draft').text(format_number(all.filter(o => parseInt(o.status) === 0).length));
|
||||
$('#stat_confirmed').text(format_number(all.filter(o => parseInt(o.status) === 1).length));
|
||||
$('#stat_completed').text(format_number(all.filter(o => parseInt(o.status) === 3).length));
|
||||
$('#stat_completed').text(format_number(all.filter(o => parseInt(o.status) === -1).length));
|
||||
|
||||
// Pagination via alasql
|
||||
alasql('CREATE TABLE IF NOT EXISTS order_list');
|
||||
@@ -261,18 +246,23 @@
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${o.order_number}</td>
|
||||
<td class="py-3">${o.source ? escape_html(o.source) : 'direct'}</td>
|
||||
<td class="py-3">${format_date(o.order_date)}</td>
|
||||
<td class="py-3">${o.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${get_dept_label(o.department_id)}</td>
|
||||
<td class="py-3">${item_count} item${item_count !== 1 ? 's' : ''}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(o.grand_total, 2)}</td>
|
||||
<td class="py-3">${order_status_badge(o.status)}</td>
|
||||
<td class="py-3">${fulfillment_status_badge(o.fulfillment_status)}</td>
|
||||
<td class="py-3">${o.shipping_tracking_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${payment_status_badge(o.payment_status)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>order/manage_order.php?id=${o.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="ms-2 text-secondary" title="Stock"
|
||||
onclick="show_stock_rows('order', ${o.id}, '${escape_html(o.order_number || '')}')">
|
||||
<i class="ti ti-packages fs-5"></i>
|
||||
</a>
|
||||
${can_cancel ? `
|
||||
<a href="javascript:void(0);" class="link-danger ms-2" title="Cancel"
|
||||
onclick="cancel_order(${o.id})">
|
||||
@@ -312,12 +302,14 @@
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(function() {
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_orders();
|
||||
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_orders();
|
||||
});
|
||||
$('#filter_status, #filter_dept').on('change', retrieve_orders);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
$doc_labels = [
|
||||
'invoice' => 'INVOICE',
|
||||
'credit_note' => 'CREDIT NOTE',
|
||||
'debit_note' => 'DEBIT NOTE',
|
||||
'supplier_credit_note' => 'SUPPLIER CREDIT NOTE',
|
||||
];
|
||||
$doc_label = $doc_labels[$inv['doc_type'] ?? 'invoice'] ?? 'INVOICE';
|
||||
|
||||
|
||||
+31
-11
@@ -19,9 +19,6 @@
|
||||
<h1 class="fs-3 mb-1">Returns</h1>
|
||||
<p class="mb-0">Manage customer return requests</p>
|
||||
</div>
|
||||
<a href="<?php echo $server_url?>order/manage_return.php" class="btn btn-primary">
|
||||
<i class="ti ti-plus me-1"></i>New Return
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,7 +29,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<div class="col-lg-4">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search return number or contact...">
|
||||
@@ -41,7 +38,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_status" class="form-select">
|
||||
<option value="">All Status</option>
|
||||
<option value="-1">Cancelled</option>
|
||||
@@ -49,6 +46,11 @@
|
||||
<option value="1">Confirmed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select id="filter_dept" class="form-select">
|
||||
<option value="">All Departments</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary w-100" onclick="retrieve_returns()">
|
||||
<i class="ti ti-search me-1"></i>Search
|
||||
@@ -72,10 +74,12 @@
|
||||
<th class="py-3">Return #</th>
|
||||
<th class="py-3">Order #</th>
|
||||
<th class="py-3">Contact</th>
|
||||
<th class="py-3">Department</th>
|
||||
<th class="py-3">Date</th>
|
||||
<th class="py-3">Items</th>
|
||||
<th class="py-3">Refund Amount</th>
|
||||
<th class="py-3">Status</th>
|
||||
<th class="py-3">Stock</th>
|
||||
<th class="py-3">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -104,6 +108,11 @@
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function receipt_status_badge(status) {
|
||||
if (parseInt(status) === 3) return '<span class="badge bg-success">Restocked</span>';
|
||||
return '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
|
||||
function retrieve_returns() {
|
||||
return ajax_request({
|
||||
@@ -114,15 +123,17 @@
|
||||
onSuccess: function(res) {
|
||||
var all = res.output || [];
|
||||
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_st = $('#filter_status').val();
|
||||
var keyword = $('#search_input').val().toLowerCase();
|
||||
var fil_st = $('#filter_status').val();
|
||||
var fil_dept = $('#filter_dept').val();
|
||||
|
||||
var data = all.filter(function(r) {
|
||||
var match_kw = !keyword ||
|
||||
var match_kw = !keyword ||
|
||||
(r.return_number || '').toLowerCase().includes(keyword) ||
|
||||
(r.contact_name || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_st === '' || String(r.status) === fil_st;
|
||||
return match_kw && match_st;
|
||||
var match_st = fil_st === '' || String(r.status) === fil_st;
|
||||
var match_dept = !fil_dept || String(r.department_id) === fil_dept;
|
||||
return match_kw && match_st && match_dept;
|
||||
});
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS return_list');
|
||||
@@ -152,14 +163,20 @@
|
||||
<td class="py-3 fw-semibold">${r.return_number}</td>
|
||||
<td class="py-3">${r.order_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${r.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${get_dept_label(r.department_id)}</td>
|
||||
<td class="py-3">${format_date(r.return_date)}</td>
|
||||
<td class="py-3">${items.length} item${items.length !== 1 ? 's' : ''}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(r.refund_amount, 2)}</td>
|
||||
<td class="py-3">${return_status_badge(r.status)}</td>
|
||||
<td class="py-3">${receipt_status_badge(r.receipt_status)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>order/manage_return.php?id=${r.id}" title="View">
|
||||
<i class="ti ti-eye fs-5"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" class="ms-2 text-secondary" title="Stock"
|
||||
onclick="show_stock_rows('return', ${r.id}, '${escape_html(r.return_number || '')}')">
|
||||
<i class="ti ti-packages fs-5"></i>
|
||||
</a>
|
||||
${can_cancel ? `
|
||||
<a href="javascript:void(0);" class="link-danger ms-2" title="Cancel"
|
||||
onclick="cancel_return(${r.id})">
|
||||
@@ -195,11 +212,14 @@
|
||||
}
|
||||
|
||||
|
||||
$(function() {
|
||||
$(async function() {
|
||||
await load_dept_cache();
|
||||
populate_dept_filter('filter_dept');
|
||||
retrieve_returns();
|
||||
$('#search_input').on('keyup', function(e) {
|
||||
if (e.key === 'Enter') retrieve_returns();
|
||||
});
|
||||
$('#filter_status, #filter_dept').on('change', retrieve_returns);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user