accounting workflows

This commit is contained in:
Thanakorn S
2026-05-20 10:17:02 +07:00
parent 04a683bd02
commit 7396db6ffc
199 changed files with 23539 additions and 5345 deletions
+91 -45
View File
@@ -2,60 +2,104 @@
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/QuotationManager.php';
$quotation_id = (int)($data['quotation_id'] ?? 0);
$convert_items = $data['convert_items'] ?? [];
$quotation_id = (int)($data['quotation_id'] ?? 0);
if (!$quotation_id) {
exit(json_encode(['success' => 0, 'message' => 'Quotation ID required']));
exit(json_encode(['success' => 0, 'message' => 'Quotation ID required.']));
}
// Load quotation — must be Accepted (status=2)
$sth = $pdo2->prepare(
"SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.id = :id AND q.company_id = :cid AND q.status = 2
LIMIT 1"
);
$sth->execute([':id' => $quotation_id, ':cid' => $company_id]);
$q = $sth->fetch(PDO::FETCH_ASSOC);
$qm = new QuotationManager($pdo2, $company_id);
if (!$q) {
exit(json_encode(['success' => 0, 'message' => 'Quotation not found or not in Accepted status']));
// Load quotation — must be Accepted
$q = $qm->getById($quotation_id);
if (!$q || (int)$q['status'] !== 2) {
exit(json_encode(['success' => 0, 'message' => 'Quotation not found or not in Accepted status.']));
}
// Check not already converted
if ((int)$q['order_id'] > 0) {
exit(json_encode(['success' => 0, 'message' => 'Quotation has already been converted to an order']));
$qt_items = $q['items'];
if (empty($qt_items)) {
exit(json_encode(['success' => 0, 'message' => 'Quotation has no items.']));
}
// Map quotation items → order items (warehouse_id left 0 = to be filled by WMS)
$qt_items = json_decode($q['items'] ?? '[]', true) ?: [];
if ((float)$q['total_remaining'] <= 0.000001) {
exit(json_encode(['success' => 0, 'message' => 'Quotation is already fully converted.']));
}
// Index by item_id for validation
$qt_by_id = [];
foreach ($qt_items as $qi) {
$qt_by_id[(int)$qi['item_id']] = $qi;
}
// Default to all remaining when caller sends no selection
if (empty($convert_items)) {
$convert_items = [];
foreach ($qt_items as $qi) {
$remaining = (float)$qi['remaining_qty'];
if ($remaining > 0.000001) {
$convert_items[] = ['item_id' => (int)$qi['item_id'], 'quantity' => $remaining];
}
}
}
// Validate quantities and build order items
$ord_items = [];
foreach ($qt_items as $i => $qi) {
$validated = [];
foreach ($convert_items as $ci) {
$item_id = (int)($ci['item_id'] ?? 0);
$qty = (float)($ci['quantity'] ?? 0);
if ($qty <= 0) continue;
if (!isset($qt_by_id[$item_id])) {
exit(json_encode(['success' => 0, 'message' => "Item #{$item_id} not found in this quotation."]));
}
$qi = $qt_by_id[$item_id];
$remaining = (float)$qi['remaining_qty'];
if ($qty - $remaining > 0.000001) {
$name = $qi['product_name'] ?: $qi['product_sku'];
exit(json_encode([
'success' => 0,
'message' => "Cannot convert {$qty} for \"{$name}\": only {$remaining} remaining.",
]));
}
$unit_price = (float)$qi['unit_price'];
$total_price = round($qty * $unit_price, 4);
$tax_amount = round($total_price * (float)($qi['tax_amount'] / max((float)$qi['total_price'], 0.000001)), 4);
$ord_items[] = [
'item_id' => $i,
'product_sku' => $qi['product_sku'] ?? '',
'product_name' => $qi['description'] ?? '',
'warehouse_id' => 0,
'zone' => '',
'aisle' => '',
'rack' => '',
'lot_number' => '',
'serial_number' => '',
'quantity' => (float)($qi['quantity'] ?? 0),
'price' => (float)($qi['unit_price'] ?? 0),
'total_price' => (float)($qi['total_price'] ?? 0),
'stock_out_id' => 0,
'item_id' => $item_id - 1,
'product_sku' => $qi['product_sku'],
'product_name' => $qi['product_name'],
'warehouse_id' => 0,
'lot_number' => '',
'quantity' => $qty,
'unit_price' => $unit_price,
'total_price' => $total_price,
'tax_amount' => $tax_amount,
'stock_out_id' => 0,
];
$validated[] = ['item_id' => $item_id, 'quantity' => $qty];
}
if (empty($ord_items)) {
exit(json_encode(['success' => 0, 'message' => 'No valid items to convert.']));
}
$order_data = [
'quotation_id' => $quotation_id,
'source' => 'quotation',
'source_id' => $quotation_id,
'contact_id' => (int)$q['contact_id'],
'department_id'=> (int)($q['department_id'] ?? 0),
'order_date' => date('Y-m-d'),
'items' => $ord_items,
'discount' => (float)$q['discount'],
'tax' => (float)$q['tax'],
'discount' => 0,
'shipping_fee' => 0,
'shipping_tracking_number' => '',
'notes' => $q['notes'] ?? '',
@@ -64,21 +108,23 @@ $order_data = [
try {
$new_order_id = null;
dbTransaction($pdo2, function($pdo) use ($order_data, $company_id, $logging, $quotation_id, &$new_order_id) {
$order = new OrderManager($pdo, $company_id);
$new_order_id = $order->saveOrder($order_data, $logging);
dbTransaction($pdo2, function($pdo) use (
$order_data, $company_id, $logging, $quotation_id, $validated, $qm, &$new_order_id
) {
$om = new OrderManager($pdo, $company_id);
$new_order_id = $om->saveOrder($order_data, $logging);
// Mark quotation as Converted and link order_id
$pdo->prepare(
"UPDATE ac_quotation SET status = 5, order_id = :oid
WHERE id = :id AND company_id = :cid"
)->execute([':oid' => $new_order_id, ':id' => $quotation_id, ':cid' => $company_id]);
$qm2 = new QuotationManager($pdo, $company_id);
$qm2->incrementConvertedQty($quotation_id, $validated);
});
$answer['success'] = 1;
$answer['message'] = 'Order created successfully';
$answer['message'] = 'Order created successfully.';
$answer['order_id'] = $new_order_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);
@@ -0,0 +1,29 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
$inv = new InvoiceManager($pdo, $company_id);
$new_id = $inv->createManualCreditNote($data, $logging);
});
$answer['success'] = 1;
$answer['new_id'] = $new_id;
$answer['message'] = 'Credit note created.';
} 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));
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
$action = $data['action'] ?? '';
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
if (!in_array($action, ['create', 'update'], true)) {
exit(json_encode(['success' => 0, 'message' => 'Unknown action']));
}
if (empty($data['items'])) {
exit(json_encode(['success' => 0, 'message' => 'At least one item is required']));
}
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $action, $company_id, $logging, &$new_id) {
$order = new OrderManager($pdo, $company_id);
if ($action === 'create') {
$data['source'] = 'revenue';
$data['source_id'] = 0;
} else {
$id = (int)($data['id'] ?? 0);
if (!$id) {
throw new Exception('Order ID required.');
}
$order->assertRevenueOrderEditable($id);
}
$new_id = $order->saveOrder($data, $logging);
});
$answer['success'] = 1;
$answer['message'] = $action === 'create' ? 'Sales order created' : 'Sales order updated';
if ($new_id) {
$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));
?>
+24 -96
View File
@@ -1,107 +1,35 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/QuotationManager.php';
$action = $data['action'] ?? '';
$id = (int)($data['id'] ?? 0);
$items = json_decode($data['items'] ?? '[]', true) ?: [];
// ── Generate quotation number ─────────────────────────────────────────────────
function generate_quotation_number(PDO $pdo, int $cid): string {
$prefix = 'QT-' . date('Ym') . '-';
$sql = "SELECT quotation_number FROM ac_quotation
WHERE company_id = :cid AND quotation_number LIKE :prefix
ORDER BY id DESC LIMIT 1";
$sth = $pdo->prepare($sql);
$sth->execute([':cid' => $cid, ':prefix' => $prefix . '%']);
$last = $sth->fetchColumn();
$seq = $last ? ((int)substr($last, -4) + 1) : 1;
return $prefix . str_pad($seq, 4, '0', STR_PAD_LEFT);
}
$qm = new QuotationManager($pdo2, $company_id);
// ── Compute totals from items ─────────────────────────────────────────────────
function compute_totals(array $items, float $discount, float $tax): array {
$subtotal = array_sum(array_map(fn($i) => (float)($i['total_price'] ?? 0), $items));
$grand = $subtotal - $discount + $tax;
return [round($subtotal, 4), round(max(0, $grand), 4)];
}
if ($action === 'create') {
$items = json_decode($data['items'] ?? '[]', true) ?: [];
$discount = (float)($data['discount'] ?? 0);
$tax = (float)($data['tax'] ?? 0);
[$subtotal, $grand] = compute_totals($items, $discount, $tax);
$qnum = generate_quotation_number($pdo2, $company_id);
$sql = "INSERT INTO ac_quotation
(company_id, uuid, quotation_number, contact_id, quotation_date, valid_until,
status, subtotal, discount, tax, grand_total, items, notes, log, created_at)
VALUES
(:cid, :uuid, :qnum, :contact_id, :qdate, :valid_until,
0, :sub, :disc, :tax, :grand, :items, :notes, :log, NOW())";
$sth = $pdo2->prepare($sql);
$sth->execute([
':cid' => $company_id,
':uuid' => $uuid,
':qnum' => $qnum,
':contact_id' => (int)($data['contact_id'] ?? 0),
':qdate' => $data['quotation_date'] ?: null,
':valid_until'=> $data['valid_until'] ?: null,
':sub' => $subtotal,
':disc' => $discount,
':tax' => $tax,
':grand' => $grand,
':items' => json_encode($items),
':notes' => $data['notes'] ?? '',
':log' => json_encode($logging),
]);
$answer['success'] = 1;
$answer['message'] = 'Quotation created';
$answer['new_id'] = (int)$pdo2->lastInsertId();
$answer['quotation_number'] = $qnum;
} elseif ($action === 'update') {
if (!$id) { exit(json_encode(['success' => 0, 'message' => 'ID required'])); }
$items = json_decode($data['items'] ?? '[]', true) ?: [];
$discount = (float)($data['discount'] ?? 0);
$tax = (float)($data['tax'] ?? 0);
[$subtotal, $grand] = compute_totals($items, $discount, $tax);
$sql = "UPDATE ac_quotation SET
contact_id = :contact_id,
quotation_date = :qdate,
valid_until = :valid_until,
subtotal = :sub,
discount = :disc,
tax = :tax,
grand_total = :grand,
items = :items,
notes = :notes
WHERE id = :id AND company_id = :cid AND status = 0";
$sth = $pdo2->prepare($sql);
$sth->execute([
':contact_id' => (int)($data['contact_id'] ?? 0),
':qdate' => $data['quotation_date'] ?: null,
':valid_until' => $data['valid_until'] ?: null,
':sub' => $subtotal,
':disc' => $discount,
':tax' => $tax,
':grand' => $grand,
':items' => json_encode($items),
':notes' => $data['notes'] ?? '',
':id' => $id,
':cid' => $company_id,
]);
$answer['success'] = 1;
$answer['message'] = 'Quotation updated';
} else {
$answer['message'] = 'Unknown action';
try {
if ($action === 'create') {
require_role($user_role, ['owner', 'admin', 'staff']);
$new_id = $qm->save(array_merge($data, ['id' => 0, 'items' => $items]), $logging);
$answer['success'] = 1;
$answer['message'] = 'Quotation created.';
$answer['new_id'] = $new_id;
} elseif ($action === 'update') {
require_role($user_role, ['owner', 'admin', 'staff']);
$qm->save(array_merge($data, ['id' => $id, 'items' => $items]), $logging);
$answer['success'] = 1;
$answer['message'] = 'Quotation updated.';
} else {
throw new Exception('Unknown action.');
}
} catch (PDOException $e) {
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -0,0 +1,40 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$order_id = (int)($data['order_id'] ?? 0);
if (!$order_id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($order_id, $company_id, $logging, &$new_id) {
$order = new OrderManager($pdo, $company_id);
$order->assertRevenueOrder($order_id);
$inv = new InvoiceManager($pdo, $company_id);
$new_id = $inv->createFromOrder($order_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));
?>
+8 -13
View File
@@ -1,19 +1,14 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/QuotationManager.php';
$sql = "SELECT
COUNT(*) AS total,
SUM(status = 0) AS draft,
SUM(status = 1) AS sent,
SUM(status = 2) AS accepted,
SUM(status = 3) AS rejected
FROM ac_quotation
WHERE company_id = :cid";
$sth = $pdo2->prepare($sql);
$sth->execute([':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
try {
$quotation = new QuotationManager($pdo2, $company_id);
$answer['output'] = $quotation->getStats();
$answer['success'] = 1;
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
}
$answer['success'] = 1;
$answer['output'] = $row;
exit(json_encode($answer));
+8 -20
View File
@@ -1,32 +1,20 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/QuotationManager.php';
$id = (int)($data['id'] ?? 0);
$qm = new QuotationManager($pdo2, $company_id);
if ($id) {
// Single quotation
$sql = "SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.id = :id AND q.company_id = :cid
LIMIT 1";
$sth = $pdo2->prepare($sql);
$sth->execute([':id' => $id, ':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) { exit(json_encode(['success' => 0, 'message' => 'Not found'])); }
$row['items'] = json_decode($row['items'] ?? '[]', true);
$row = $qm->getById($id);
if (!$row) {
http_response_code(404);
exit(json_encode(['success' => 0, 'message' => 'Not found.']));
}
$answer['output'] = $row;
} else {
// List
$sql = "SELECT q.*, c.contact_name
FROM ac_quotation q
LEFT JOIN md_contact c ON c.id = q.contact_id
WHERE q.company_id = :cid
ORDER BY q.id DESC";
$sth = $pdo2->prepare($sql);
$sth->execute([':cid' => $company_id]);
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
$answer['output'] = $qm->getList();
}
$answer['success'] = 1;
@@ -1,42 +1,27 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/QuotationManager.php';
$id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
$id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
if (!$id || !$action) {
exit(json_encode(['success' => 0, 'message' => 'ID and action required']));
exit(json_encode(['success' => 0, 'message' => 'ID and action are required.']));
}
// Load current status
$sth = $pdo2->prepare("SELECT status FROM ac_quotation WHERE id = :id AND company_id = :cid LIMIT 1");
$sth->execute([':id' => $id, ':cid' => $company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) { exit(json_encode(['success' => 0, 'message' => 'Not found'])); }
$qm = new QuotationManager($pdo2, $company_id);
$cur = (int)$row['status'];
$transitions = [
'send' => ['from' => [0], 'to' => 1, 'label' => 'Sent'],
'accept' => ['from' => [1], 'to' => 2, 'label' => 'Accepted'],
'reject' => ['from' => [1], 'to' => 3, 'label' => 'Rejected'],
'reopen' => ['from' => [1,2,3],'to' => 0, 'label' => 'Draft'],
'cancel' => ['from' => [0,1], 'to' => -1,'label' => 'Cancelled'],
];
if (!isset($transitions[$action])) {
exit(json_encode(['success' => 0, 'message' => 'Invalid action']));
try {
$qm->updateStatus($id, $action, $logging);
$answer['success'] = 1;
$answer['message'] = '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);
}
$t = $transitions[$action];
if (!in_array($cur, $t['from'], true)) {
exit(json_encode(['success' => 0, 'message' => 'Transition not allowed from current status']));
}
$sth = $pdo2->prepare("UPDATE ac_quotation SET status = :status WHERE id = :id AND company_id = :cid");
$sth->execute([':status' => $t['to'], ':id' => $id, ':cid' => $company_id]);
$answer['success'] = 1;
$answer['message'] = 'Status updated to ' . $t['label'];
exit(json_encode($answer));