softDelete features

This commit is contained in:
Thanakorn S
2026-05-22 14:07:22 +07:00
parent f04554793e
commit 2410f7bade
59 changed files with 1783 additions and 16 deletions
@@ -1,7 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_once '../../../assets/utils/classes/CompanySettingManager.php';
$settings = new CompanySettingManager($pdo1, $company_id);
$open_from = $settings->get('posting_open_from') ?: $settings->get('gl_open_from') ?: '';
+1 -1
View File
@@ -1,7 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_once '../../../assets/utils/classes/CompanySettingManager.php';
require_role($user_role, ['owner', 'admin']);
$csm = new CompanySettingManager($pdo1, $company_id);
+5 -2
View File
@@ -47,6 +47,7 @@
<option value="receipt">Receipt</option>
<option value="payment">Payment</option>
<option value="manual">Manual</option>
<option value="reversal">Reversal</option>
</select>
</div>
<div class="col-auto">
@@ -186,7 +187,8 @@
supplier_credit_note: 'Supplier Credit Note',
receipt: 'Receipt',
payment: 'Payment',
manual: 'Manual'
manual: 'Manual',
reversal: 'Reversal'
};
var SOURCE_BADGE = {
@@ -196,7 +198,8 @@
supplier_credit_note: 'bg-secondary-subtle text-secondary',
receipt: 'bg-info-subtle text-info',
payment: 'bg-danger-subtle text-danger',
manual: 'bg-dark-subtle text-dark'
manual: 'bg-dark-subtle text-dark',
reversal: 'bg-danger-subtle text-danger'
};
function source_badge(type) {
+106
View File
@@ -1158,6 +1158,112 @@ class InvoiceManager {
* @param array $logging Audit entry.
* @throws Exception
*/
/**
* Soft-delete an invoice/credit_note/purchase_invoice/supplier_credit_note by negating
* company_id. Blocked if any active (non-soft-deleted) downstream documents exist.
* Deletes the GL entry if one was posted (subject to posting window).
*/
public function softDelete(int $id): void
{
$sth = $this->pdo->prepare(
"SELECT id, doc_type, issued_date FROM td_invoice
WHERE company_id = :cid AND id = :id LIMIT 1"
);
$sth->execute([':cid' => $this->company_id, ':id' => $id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception('Invoice not found.');
$doc_type = (string)$row['doc_type'];
$this->assertPostingWindow($row['issued_date'] ?: date('Y-m-d'), ucfirst(str_replace('_', ' ', $doc_type)) . ' deletion');
if (in_array($doc_type, ['invoice', 'credit_note'], true)) {
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_receipt_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — receipts are allocated to this document. Delete the receipts first.');
}
$sth3 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_payment_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth3->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth3->fetchColumn() > 0) {
throw new Exception('Cannot delete — payments are allocated to this document. Delete the payments first.');
}
$sth4 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_receipt_billing_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth4->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth4->fetchColumn() > 0) {
throw new Exception('Cannot delete — receipt billings reference this document. Delete the receipt billings first.');
}
$sth5 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_payment_billing_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth5->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth5->fetchColumn() > 0) {
throw new Exception('Cannot delete — payment billings reference this document. Delete the payment billings first.');
}
if ($doc_type === 'invoice') {
$sth6 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :cid AND ref_invoice_id = :id AND doc_type = 'credit_note'"
);
$sth6->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth6->fetchColumn() > 0) {
throw new Exception('Cannot delete — credit notes reference this invoice. Delete the credit notes first.');
}
}
}
if (in_array($doc_type, ['purchase_invoice', 'supplier_credit_note'], true)) {
$sth7 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_payment_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth7->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth7->fetchColumn() > 0) {
throw new Exception('Cannot delete — payments are allocated to this document. Delete the payments first.');
}
$sth8 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_payment_billing_item
WHERE company_id = :cid AND invoice_id = :id"
);
$sth8->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth8->fetchColumn() > 0) {
throw new Exception('Cannot delete — payment billings reference this document. Delete the payment billings first.');
}
}
// Delete GL entry if posted (no-op if none exists; throws if period is closed)
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$gl = new GlManager($this->pdo, $this->company_id, $guard);
$gl->delete($doc_type, $id);
}
$this->pdo->prepare(
"UPDATE td_invoice_item SET company_id = company_id * -1
WHERE invoice_id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_invoice SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
}
public function voidInvoice(int $id, array $logging): void
{
$sth = $this->pdo->prepare(
+106
View File
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* OrderManager
@@ -992,6 +993,111 @@ class OrderManager {
]);
}
/**
* Soft-delete an order by negating company_id on the header, items, and all
* linked stock-out rows. Approved stock-out side effects are reversed first.
* Blocked if any invoice or return (not yet soft-deleted) exists for this order.
*/
public function softDelete(int $id): void
{
$sth = $this->pdo->prepare(
"SELECT * FROM td_order WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->company_id, ':id' => $id]);
$order = $sth->fetch(PDO::FETCH_ASSOC);
if (!$order) throw new Exception('Sales order not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($order['order_date'] ?: date('Y-m-d'), 'Order deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :cid AND order_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — this order has linked invoices. Delete the invoices first.');
}
$sth3 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_return
WHERE company_id = :cid AND order_id = :id"
);
$sth3->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth3->fetchColumn() > 0) {
throw new Exception('Cannot delete — this order has linked returns. Delete the returns first.');
}
// Block if any approved stock-out rows exist; user must reverse via ICS first
$sth4 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth4->execute();
$tables = $sth4->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
"SELECT COUNT(*) FROM `{$table}`
WHERE company_id = :cid AND source = 'order' AND source_id = :id AND type = 'out' AND status = 1"
);
$chk->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$chk->fetchColumn() > 0) {
throw new Exception('Cannot delete — stock-out for this order has been approved. Reverse via ICS first.');
}
}
// Negate draft/pending stock-out rows (status != 1)
foreach ($tables as $table) {
$this->pdo->prepare(
"UPDATE `{$table}` SET company_id = company_id * -1
WHERE company_id = :cid AND source = 'order' AND source_id = :id AND type = 'out' AND status != 1"
)->execute([':cid' => $this->company_id, ':id' => $id]);
}
// Release converted_qty on source quotation
$source = (string)($order['source'] ?? '');
$source_id = (int)($order['source_id'] ?? 0);
if ($source === 'quotation' && $source_id > 0) {
$sth5 = $this->pdo->prepare(
"SELECT item_id, quantity FROM td_order_item
WHERE order_id = :id AND company_id = :cid ORDER BY item_id"
);
$sth5->execute([':id' => $id, ':cid' => $this->company_id]);
$items = $sth5->fetchAll(PDO::FETCH_ASSOC);
$dec = $this->pdo->prepare(
"UPDATE td_quotation_item
SET converted_qty = GREATEST(0, converted_qty - :qty)
WHERE quotation_id = :qid AND item_id = :item_id AND company_id = :cid"
);
foreach ($items as $item) {
$dec->execute([
':qty' => (float)($item['quantity'] ?? 0),
':qid' => $source_id,
':item_id' => (int)($item['item_id'] ?? 0),
':cid' => $this->company_id,
]);
}
$this->pdo->prepare(
"UPDATE td_quotation SET status = 2
WHERE id = :id AND company_id = :cid AND status = 5"
)->execute([':id' => $source_id, ':cid' => $this->company_id]);
}
$this->pdo->prepare(
"UPDATE td_order_item SET company_id = company_id * -1
WHERE order_id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_order SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
}
/**
* Update shipping tracking number. Fulfillment itself is derived from
* linked stock-out rows and tracking, not selected manually.
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
class PaymentBillingManager {
@@ -391,5 +392,44 @@ class PaymentBillingManager {
':id' => $billing_id,
]);
}
/**
* Soft-delete a payment billing by negating company_id on the header and all items.
* Blocked if any payment (not yet soft-deleted) is linked to this billing.
*/
public function softDelete(int $billing_id): void
{
$sth = $this->pdo->prepare(
"SELECT id, billing_date FROM td_payment_billing WHERE id = :id AND company_id = :cid LIMIT 1"
);
$sth->execute([':id' => $billing_id, ':cid' => $this->company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception('Payment billing not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($row['billing_date'] ?: date('Y-m-d'), 'Payment billing deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_payment
WHERE company_id = :cid AND payment_billing_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $billing_id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — payments are linked to this billing. Delete the payments first.');
}
$this->pdo->prepare(
"UPDATE td_payment_billing_item SET company_id = company_id * -1
WHERE billing_id = :id AND company_id = :cid"
)->execute([':id' => $billing_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_payment_billing SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $billing_id, ':cid' => $this->company_id]);
}
}
?>
@@ -513,5 +513,53 @@ class PaymentManager {
':company_id' => $this->company_id,
]);
}
/**
* Soft-delete a payment by negating company_id on the header and all items.
* Deletes the GL entry if posted. Refreshes settlement and billing status.
*/
public function softDelete(int $payment_id): void
{
$sth = $this->pdo->prepare(
"SELECT id, payment_billing_id, payment_date FROM td_payment
WHERE company_id = :cid AND id = :id LIMIT 1"
);
$sth->execute([':cid' => $this->company_id, ':id' => $payment_id]);
$payment = $sth->fetch(PDO::FETCH_ASSOC);
if (!$payment) throw new Exception('Payment not found.');
$this->assertPostingWindow($payment['payment_date'] ?: date('Y-m-d'), 'Payment deletion');
// Collect linked invoice ids before negating
$sth2 = $this->pdo->prepare(
"SELECT invoice_id FROM td_payment_item
WHERE company_id = :cid AND payment_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $payment_id]);
$invoice_ids = $sth2->fetchAll(PDO::FETCH_COLUMN);
// Delete GL entry if posted (no-op if none; throws if period closed)
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$gl = new GlManager($this->pdo, $this->company_id, $guard);
$gl->delete('payment', $payment_id);
}
$this->pdo->prepare(
"UPDATE td_payment_item SET company_id = company_id * -1
WHERE payment_id = :id AND company_id = :cid"
)->execute([':id' => $payment_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_payment SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $payment_id, ':cid' => $this->company_id]);
foreach ($invoice_ids as $invoice_id) {
$this->refreshInvoiceSettlementStatus((int)$invoice_id);
}
$this->refreshPaymentBillingStatus((int)$payment['payment_billing_id']);
}
}
?>
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* PurchaseOrderManager
@@ -851,4 +852,123 @@ class PurchaseOrderManager {
)->execute([':id' => $source_id, ':cid' => $this->company_id]);
}
}
/**
* Soft-delete a purchase order by negating company_id on the header, items, and all
* linked draft stock-in rows. Blocked if any purchase invoice, supplier return, or
* approved (received) stock-in rows exist.
*/
public function softDelete(int $po_id): void
{
$sth = $this->pdo->prepare(
"SELECT * FROM td_purchase_order WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->company_id, ':id' => $po_id]);
$po = $sth->fetch(PDO::FETCH_ASSOC);
if (!$po) throw new Exception('Purchase order not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($po['po_date'] ?: date('Y-m-d'), 'Purchase order deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :cid AND source = 'po' AND source_id = :id AND doc_type = 'purchase_invoice'"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $po_id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — this PO has linked purchase invoices. Delete the invoices first.');
}
$sth3 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_supplier_return
WHERE company_id = :cid AND po_id = :id AND status != -1"
);
$sth3->execute([':cid' => $this->company_id, ':id' => $po_id]);
if ((int)$sth3->fetchColumn() > 0) {
throw new Exception('Cannot delete — this PO has linked supplier returns. Delete or cancel the returns first.');
}
$sth4 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth4->execute();
$tables = $sth4->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$sth5 = $this->pdo->prepare(
"SELECT COUNT(*) FROM `{$table}`
WHERE company_id = :cid AND source = 'po' AND source_id = :id AND status = 1"
);
$sth5->execute([':cid' => $this->company_id, ':id' => $po_id]);
if ((int)$sth5->fetchColumn() > 0) {
throw new Exception('Cannot delete — received stock from this PO must be reversed via ICS first.');
}
}
// Release racks from draft stock-in rows and negate them
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
foreach ($tables as $table) {
if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) continue;
$warehouse_id = (int)$matches[1];
$row_sth = $this->pdo->prepare(
"SELECT id, zone, aisle, rack FROM `{$table}`
WHERE company_id = :cid AND source = 'po' AND source_id = :id AND status = 0"
);
$row_sth->execute([':cid' => $this->company_id, ':id' => $po_id]);
$draft_rows = $row_sth->fetchAll(PDO::FETCH_ASSOC);
foreach ($draft_rows as $row) {
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
}
$this->pdo->prepare(
"UPDATE `{$table}` SET company_id = company_id * -1
WHERE company_id = :cid AND source = 'po' AND source_id = :id"
)->execute([':cid' => $this->company_id, ':id' => $po_id]);
}
// Release converted_qty on source purchase request
$source = (string)($po['source'] ?? '');
$source_id = (int)($po['source_id'] ?? 0);
if ($source === 'purchase_request' && $source_id > 0) {
$sth6 = $this->pdo->prepare(
"SELECT item_id, quantity FROM td_purchase_order_item
WHERE order_id = :id AND company_id = :cid ORDER BY item_id"
);
$sth6->execute([':id' => $po_id, ':cid' => $this->company_id]);
$po_items = $sth6->fetchAll(PDO::FETCH_ASSOC);
$dec = $this->pdo->prepare(
"UPDATE td_purchase_request_item
SET converted_qty = GREATEST(0, converted_qty - :qty)
WHERE request_id = :rid AND item_id = :item_id AND company_id = :cid"
);
foreach ($po_items as $item) {
$dec->execute([
':qty' => (float)($item['quantity'] ?? 0),
':rid' => $source_id,
':item_id' => (int)($item['item_id'] ?? 0),
':cid' => $this->company_id,
]);
}
$this->pdo->prepare(
"UPDATE td_purchase_request SET status = 2
WHERE id = :id AND company_id = :cid AND status = 5"
)->execute([':id' => $source_id, ':cid' => $this->company_id]);
}
$this->pdo->prepare(
"UPDATE td_purchase_order_item SET company_id = company_id * -1
WHERE order_id = :id AND company_id = :cid"
)->execute([':id' => $po_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_purchase_order SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $po_id, ':cid' => $this->company_id]);
}
}
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* PurchaseRequestManager
@@ -347,4 +348,43 @@ class PurchaseRequestManager
)->execute([':id' => $request_id, ':cid' => $this->company_id]);
}
}
/**
* Soft-delete a purchase request by negating company_id on the header and all items.
* Blocked if any purchase order (not yet soft-deleted) was converted from this request.
*/
public function softDelete(int $id): void
{
$sth = $this->pdo->prepare(
"SELECT id, request_date FROM td_purchase_request WHERE id = :id AND company_id = :cid LIMIT 1"
);
$sth->execute([':id' => $id, ':cid' => $this->company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception('Purchase request not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($row['request_date'] ?: date('Y-m-d'), 'Purchase request deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_purchase_order
WHERE company_id = :cid AND source = 'purchase_request' AND source_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — this request has linked purchase orders. Delete the POs first.');
}
$this->pdo->prepare(
"UPDATE td_purchase_request_item SET company_id = company_id * -1
WHERE request_id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_purchase_request SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
}
}
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* QuotationManager
@@ -304,6 +305,45 @@ class QuotationManager
)->execute([':status' => $t['to'], ':id' => $id, ':cid' => $this->company_id]);
}
/**
* Soft-delete a quotation by negating company_id on the header and all items.
* Blocked if any sales order (not yet soft-deleted) was converted from this quotation.
*/
public function softDelete(int $id): void
{
$sth = $this->pdo->prepare(
"SELECT id, quotation_date FROM td_quotation WHERE id = :id AND company_id = :cid LIMIT 1"
);
$sth->execute([':id' => $id, ':cid' => $this->company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception('Quotation not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($row['quotation_date'] ?: date('Y-m-d'), 'Quotation deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_order
WHERE company_id = :cid AND source = 'quotation' AND source_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — this quotation has linked sales orders. Delete the orders first.');
}
$this->pdo->prepare(
"UPDATE td_quotation_item SET company_id = company_id * -1
WHERE quotation_id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_quotation SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $id, ':cid' => $this->company_id]);
}
/**
* Increment converted_qty per item after a successful order conversion.
* Automatically sets quotation status = 5 when all items are fully converted.
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
class ReceiptBillingManager {
@@ -389,5 +390,44 @@ class ReceiptBillingManager {
':id' => $billing_id,
]);
}
/**
* Soft-delete a receipt billing by negating company_id on the header and all items.
* Blocked if any receipt (not yet soft-deleted) is linked to this billing.
*/
public function softDelete(int $billing_id): void
{
$sth = $this->pdo->prepare(
"SELECT id, billing_date FROM td_receipt_billing WHERE id = :id AND company_id = :cid LIMIT 1"
);
$sth->execute([':id' => $billing_id, ':cid' => $this->company_id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception('Receipt billing not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($row['billing_date'] ?: date('Y-m-d'), 'Receipt billing deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_receipt
WHERE company_id = :cid AND receipt_billing_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $billing_id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — receipts are linked to this billing. Delete the receipts first.');
}
$this->pdo->prepare(
"UPDATE td_receipt_billing_item SET company_id = company_id * -1
WHERE billing_id = :id AND company_id = :cid"
)->execute([':id' => $billing_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_receipt_billing SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $billing_id, ':cid' => $this->company_id]);
}
}
?>
@@ -511,5 +511,53 @@ class ReceiptManager {
':company_id' => $this->company_id,
]);
}
/**
* Soft-delete a receipt by negating company_id on the header and all items.
* Deletes the GL entry if posted. Refreshes settlement and billing status.
*/
public function softDelete(int $receipt_id): void
{
$sth = $this->pdo->prepare(
"SELECT id, receipt_billing_id, receipt_date FROM td_receipt
WHERE company_id = :cid AND id = :id LIMIT 1"
);
$sth->execute([':cid' => $this->company_id, ':id' => $receipt_id]);
$receipt = $sth->fetch(PDO::FETCH_ASSOC);
if (!$receipt) throw new Exception('Receipt not found.');
$this->assertPostingWindow($receipt['receipt_date'] ?: date('Y-m-d'), 'Receipt deletion');
// Collect linked invoice ids before negating
$sth2 = $this->pdo->prepare(
"SELECT invoice_id FROM td_receipt_item
WHERE company_id = :cid AND receipt_id = :id"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $receipt_id]);
$invoice_ids = $sth2->fetchAll(PDO::FETCH_COLUMN);
// Delete GL entry if posted (no-op if none; throws if period closed)
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$gl = new GlManager($this->pdo, $this->company_id, $guard);
$gl->delete('receipt', $receipt_id);
}
$this->pdo->prepare(
"UPDATE td_receipt_item SET company_id = company_id * -1
WHERE receipt_id = :id AND company_id = :cid"
)->execute([':id' => $receipt_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_receipt SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $receipt_id, ':cid' => $this->company_id]);
foreach ($invoice_ids as $invoice_id) {
$this->refreshInvoiceSettlementStatus((int)$invoice_id);
}
$this->refreshReceiptBillingStatus((int)$receipt['receipt_billing_id']);
}
}
?>
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* ReturnManager
@@ -713,4 +714,74 @@ class ReturnManager {
':company_id' => $this->company_id,
]);
}
/**
* Soft-delete a return by negating company_id on the header, items, and all linked
* stock-in rows. Approved stock-in side effects are reversed first.
* Blocked if any credit note (not yet soft-deleted) exists for the linked order.
*/
public function softDelete(int $return_id): void
{
$sth = $this->pdo->prepare(
"SELECT * FROM td_return WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->company_id, ':id' => $return_id]);
$return = $sth->fetch(PDO::FETCH_ASSOC);
if (!$return) throw new Exception('Return not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($return['return_date'] ?: date('Y-m-d'), 'Return deletion');
}
$order_id = (int)$return['order_id'];
if ($order_id > 0) {
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :cid AND order_id = :order_id AND doc_type = 'credit_note'"
);
$sth2->execute([':cid' => $this->company_id, ':order_id' => $order_id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — a credit note exists for this return\'s order. Delete the credit note first.');
}
}
// Block if any approved stock-in rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth3->execute();
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
"SELECT COUNT(*) FROM `{$table}`
WHERE company_id = :cid AND source = 'return' AND source_id = :id AND status = 1"
);
$chk->execute([':cid' => $this->company_id, ':id' => $return_id]);
if ((int)$chk->fetchColumn() > 0) {
throw new Exception('Cannot delete — stock-in for this return has been approved. Reverse via ICS first.');
}
}
// Negate draft/pending stock-in rows (status != 1)
foreach ($tables as $table) {
$this->pdo->prepare(
"UPDATE `{$table}` SET company_id = company_id * -1
WHERE company_id = :cid AND source = 'return' AND source_id = :id AND status != 1"
)->execute([':cid' => $this->company_id, ':id' => $return_id]);
}
$this->pdo->prepare(
"UPDATE td_return_item SET company_id = company_id * -1
WHERE return_id = :id AND company_id = :cid"
)->execute([':id' => $return_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_return SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $return_id, ':cid' => $this->company_id]);
}
}
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
/**
* SupplierReturnManager
@@ -616,4 +617,72 @@ class SupplierReturnManager {
':company_id' => $this->company_id,
]);
}
/**
* Soft-delete a supplier return by negating company_id on the header, items, and all
* linked stock-out rows. Approved stock-out side effects are reversed first.
* Blocked if any supplier credit note (not yet soft-deleted) exists for this return.
*/
public function softDelete(int $return_id): void
{
$sth = $this->pdo->prepare(
"SELECT * FROM td_supplier_return WHERE company_id = :cid AND id = :id"
);
$sth->execute([':cid' => $this->company_id, ':id' => $return_id]);
$return = $sth->fetch(PDO::FETCH_ASSOC);
if (!$return) throw new Exception('Supplier return not found.');
global $pdo1;
if (isset($pdo1) && $pdo1 instanceof PDO) {
$guard = new PostingWindowGuard($pdo1, $this->company_id);
$guard->assertOpenDate($return['return_date'] ?: date('Y-m-d'), 'Supplier return deletion');
}
$sth2 = $this->pdo->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :cid AND source = 'supplier_return' AND source_id = :id
AND doc_type = 'supplier_credit_note'"
);
$sth2->execute([':cid' => $this->company_id, ':id' => $return_id]);
if ((int)$sth2->fetchColumn() > 0) {
throw new Exception('Cannot delete — a supplier credit note exists for this return. Delete the credit note first.');
}
// Block if any approved stock-out rows exist; user must reverse via ICS first
$sth3 = $this->pdo->prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
);
$sth3->execute();
$tables = $sth3->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
$chk = $this->pdo->prepare(
"SELECT COUNT(*) FROM `{$table}`
WHERE company_id = :cid AND source = 'supplier_return' AND source_id = :id AND status = 1"
);
$chk->execute([':cid' => $this->company_id, ':id' => $return_id]);
if ((int)$chk->fetchColumn() > 0) {
throw new Exception('Cannot delete — stock-out for this supplier return has been approved. Reverse via ICS first.');
}
}
// Negate draft/pending stock-out rows (status != 1)
foreach ($tables as $table) {
$this->pdo->prepare(
"UPDATE `{$table}` SET company_id = company_id * -1
WHERE company_id = :cid AND source = 'supplier_return' AND source_id = :id AND status != 1"
)->execute([':cid' => $this->company_id, ':id' => $return_id]);
}
$this->pdo->prepare(
"UPDATE td_supplier_return_item SET company_id = company_id * -1
WHERE return_id = :id AND company_id = :cid"
)->execute([':id' => $return_id, ':cid' => $this->company_id]);
$this->pdo->prepare(
"UPDATE td_supplier_return SET company_id = company_id * -1
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $return_id, ':cid' => $this->company_id]);
}
}
@@ -319,7 +319,7 @@ class FinancialReports
SUM(i.credit) AS total_credit
FROM td_gl g
JOIN td_gl_item i ON i.gl_id = g.id AND i.company_id = g.company_id
WHERE g.company_id = :cid
WHERE g.company_id = :cid AND g.source_type != 'voided'
GROUP BY g.id, g.source_type, g.reference, g.description, g.journal_date, g.period, g.created_at
ORDER BY g.created_at DESC
LIMIT {$limit}
+6 -6
View File
@@ -8,7 +8,9 @@
* Lifecycle:
* post() — first-time GL creation; throws if a record already exists.
* replace() — snapshot current lines into td_gl.history, then delete + re-insert.
* delete() — create a reversal journal entry (audit trail), then hard-delete the original.
* delete() — create a reversal journal entry (debit/credit swapped), then mark the original
* source_type = 'voided' so it stays in the table for audit trail but is invisible
* to getBySource() lookups.
*/
class GlManager
{
@@ -265,12 +267,10 @@ class GlManager
$this->insertLines((int)$this->pdo->lastInsertId(), $reversal_lines);
}
// Mark original as voided — keeps audit trail; getBySource() won't match 'voided' source_type
$this->pdo->prepare(
"DELETE FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id"
)->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]);
$this->pdo->prepare(
"DELETE FROM td_gl WHERE id = :id AND company_id = :cid"
"UPDATE td_gl SET source_type = 'voided', updated_at = NOW()
WHERE id = :id AND company_id = :cid"
)->execute([':id' => $gl_id, ':cid' => $this->companyId]);
}
@@ -7,7 +7,7 @@ class GlQueryManager
private array $invoiceTypes = ['invoice', 'credit_note', 'purchase_invoice', 'supplier_credit_note'];
private array $mappingTypes = ['invoice', 'credit_note', 'purchase_invoice', 'supplier_credit_note', 'purchase_order'];
private array $postingTypes = ['invoice', 'credit_note', 'purchase_invoice', 'supplier_credit_note', 'receipt', 'payment', 'purchase_order'];
private array $journalTypes = ['invoice', 'credit_note', 'purchase_invoice', 'supplier_credit_note', 'receipt', 'payment', 'manual', 'purchase_order'];
private array $journalTypes = ['invoice', 'credit_note', 'purchase_invoice', 'supplier_credit_note', 'receipt', 'payment', 'manual', 'purchase_order', 'reversal'];
public function __construct(PDO $pdo, int $company_id)
{
@@ -50,7 +50,7 @@ class GlQueryManager
$date_from = $this->parseDate($date_from);
$date_to = $this->parseDate($date_to);
$where = ['g.company_id = :cid'];
$where = ['g.company_id = :cid', "g.source_type != 'voided'"];
$params = [':cid' => $this->companyId];
if ($source_type && in_array($source_type, $this->journalTypes, true)) {
+25
View File
@@ -129,6 +129,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="cancel_po()">
<i class="ti ti-x me-1"></i>Cancel PO
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_po()">
<i class="ti ti-trash me-1"></i>Delete PO
</button>
</div>
</div>
</div>
@@ -139,6 +142,8 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var po_id = <?php echo $po_id; ?>;
var po_data = null;
var item_index = 0;
@@ -238,6 +243,7 @@
$('.remove_item_btn').toggleClass('d-none', !editable);
$('#btn_save').toggleClass('d-none', !editable);
$('#btn_cancel').toggleClass('d-none', !(status === -2 || status === 0));
$('#btn_delete').toggleClass('d-none', !can_delete || !po_id);
if (status === -2) {
$('#wms_status_title').text('Pending Warehouse receiving setup');
@@ -320,6 +326,25 @@
});
}
function delete_po() {
bootbox.confirm({
message: 'Delete this PO? Unconfirmed stock-in reservations will be released.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_po.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: po_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>expense/purchase_order.php'; }
});
}
});
}
function cancel_po() {
bootbox.confirm('Cancel this purchase order?', function(ok) {
if (!ok) return;
+23
View File
@@ -118,6 +118,7 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_reject" onclick="change_status('reject')"><i class="ti ti-x me-1"></i>Reject</button>
<button class="btn btn-outline-secondary w-100 d-none" id="btn_reopen" onclick="change_status('reopen')"><i class="ti ti-refresh me-1"></i>Reopen as Draft</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="change_status('cancel')"><i class="ti ti-ban me-1"></i>Cancel</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_request()"><i class="ti ti-trash me-1"></i>Delete Request</button>
<hr class="d-none" id="hr_convert">
<button class="btn btn-success w-100 d-none" id="btn_convert" onclick="convert_to_po()"><i class="ti ti-transfer me-1"></i>Convert to PO</button>
<a href="#" class="btn btn-outline-primary w-100 d-none" id="btn_view_po"><i class="ti ti-clipboard-list me-1"></i>View PO</a>
@@ -131,6 +132,8 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var request_id = <?php echo $request_id; ?>;
var request_data = null;
var item_index = 0;
@@ -225,6 +228,7 @@
$('#btn_approve, #btn_reject').toggleClass('d-none', !is_submitted);
$('#btn_reopen').toggleClass('d-none', is_draft || is_closed);
$('#btn_cancel').toggleClass('d-none', !(is_draft || is_submitted) || !request_id);
$('#btn_delete').toggleClass('d-none', !can_delete || !request_id);
$('#hr_convert, #btn_convert').toggleClass('d-none', !is_approved);
$('#btn_view_po').toggleClass('d-none', !(request_data && parseInt(request_data.po_id) > 0))
.attr('href', request_data && request_data.po_id ? '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + request_data.po_id : '#');
@@ -295,6 +299,25 @@
});
}
function delete_request() {
bootbox.confirm({
message: 'Delete this purchase request? This cannot be undone.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_purchase_request.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: request_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>expense/purchase_request.php'; }
});
}
});
}
function change_status(action_type) {
var labels = {
submit: 'Submit this purchase request?',
+22
View File
@@ -145,6 +145,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function po_status_badge(status) {
const map = {
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
@@ -229,12 +232,31 @@
<a href="<?php echo $server_url?>expense/manage_purchase_order.php?id=${po.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_po(${po.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
$('table#po_list tbody').html(body);
}
function delete_po(id) {
bootbox.confirm({
message: 'Delete this PO? Unconfirmed stock-in reservations will be released.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_po.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_pos(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+22
View File
@@ -128,6 +128,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var status_badge = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
@@ -195,12 +198,31 @@
<a href="<?php echo $server_url?>expense/manage_purchase_request.php?id=${r.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_request(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
$('table#request_list tbody').html(body);
}
function delete_request(id) {
bootbox.confirm({
message: 'Delete this purchase request? This cannot be undone.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_purchase_request.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_requests(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/PaymentManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid payment ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new PaymentManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Payment deleted.';
} 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,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/PaymentBillingManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid payment billing ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new PaymentBillingManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Payment billing deleted.';
} 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));
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/ReceiptManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid receipt ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new ReceiptManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Receipt deleted.';
} 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,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/ReceiptBillingManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid receipt billing ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new ReceiptBillingManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Receipt billing deleted.';
} 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));
+26
View File
@@ -123,6 +123,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="void_payment()">
<i class="ti ti-ban me-1"></i>Void Payment
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_payment()">
<i class="ti ti-trash me-1"></i>Delete Payment
</button>
</div>
</div>
</div>
@@ -133,6 +136,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var payment_id = <?php echo $payment_id; ?>;
var payment = null;
@@ -184,6 +190,7 @@
$('#display_method').html(display_text(method_label(payment.payment_method)));
$('#display_notes').html(payment.notes ? escape_html(payment.notes).replace(/\n/g, '<br>') : '<span class="text-muted">—</span>');
$('#btn_void').toggleClass('d-none', String(payment.status) === '4');
$('#btn_delete').toggleClass('d-none', !can_delete || !payment_id);
var selected_formula = payment.formula_id || '';
ajax_request({
@@ -245,6 +252,25 @@
});
}
function delete_payment() {
bootbox.confirm({
message: 'Delete this payment? The GL entry and invoice allocations will be reversed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_payment.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: payment_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>finance/payment.php'; }
});
}
});
}
function void_payment() {
bootbox.confirm('Void this payment? Allocated purchase invoices and supplier credit notes will be reopened if not fully settled by other payments.', function(ok) {
if (!ok) return;
+26
View File
@@ -88,6 +88,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="void_billing()">
<i class="ti ti-ban me-1"></i>Void Billing
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_billing()">
<i class="ti ti-trash me-1"></i>Delete Billing
</button>
</div>
</div>
</div>
@@ -98,6 +101,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var billing_id = <?php echo $billing_id; ?>;
var billing = null;
@@ -148,6 +154,7 @@
$('#btn_payment').attr('href', '<?php echo $server_url?>finance/payment.php?billing_id=' + billing.id);
$('#btn_payment').toggleClass('d-none', String(billing.status) === '4' || parseFloat(billing.balance_amount || 0) <= 0.0001);
$('#btn_void').toggleClass('d-none', String(billing.status) === '4' || parseFloat(billing.paid_amount || 0) > 0.0001);
$('#btn_delete').toggleClass('d-none', !can_delete || !billing_id);
var rows = '';
$.each(billing.allocations || [], function(i, line) {
@@ -168,6 +175,25 @@
$('#allocation_tbody').html(rows || '<tr><td colspan="5" class="text-center py-5 text-muted">No documents found.</td></tr>');
}
function delete_billing() {
bootbox.confirm({
message: 'Delete this billing? All linked payments must be deleted first.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_payment_billing.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: billing_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>finance/payment_billing.php'; }
});
}
});
}
function void_billing() {
bootbox.confirm('Void this payment billing?', function(ok) {
if (!ok) return;
+26
View File
@@ -123,6 +123,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="void_receipt()">
<i class="ti ti-ban me-1"></i>Void Receipt
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_receipt()">
<i class="ti ti-trash me-1"></i>Delete Receipt
</button>
</div>
</div>
</div>
@@ -133,6 +136,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var receipt_id = <?php echo $receipt_id; ?>;
var receipt = null;
@@ -184,6 +190,7 @@
$('#display_method').html(display_text(method_label(receipt.payment_method)));
$('#display_notes').html(receipt.notes ? escape_html(receipt.notes).replace(/\n/g, '<br>') : '<span class="text-muted">—</span>');
$('#btn_void').toggleClass('d-none', String(receipt.status) === '4');
$('#btn_delete').toggleClass('d-none', !can_delete || !receipt_id);
var selected_formula = receipt.formula_id || '';
ajax_request({
@@ -245,6 +252,25 @@
});
}
function delete_receipt() {
bootbox.confirm({
message: 'Delete this receipt? The GL entry and invoice allocations will be reversed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_receipt.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: receipt_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>finance/receipt.php'; }
});
}
});
}
function void_receipt() {
bootbox.confirm('Void this receipt? Allocated invoices and credit notes will be reopened if not fully settled by other receipts.', function(ok) {
if (!ok) return;
+26
View File
@@ -88,6 +88,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="void_billing()">
<i class="ti ti-ban me-1"></i>Void Billing
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_billing()">
<i class="ti ti-trash me-1"></i>Delete Billing
</button>
</div>
</div>
</div>
@@ -98,6 +101,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var billing_id = <?php echo $billing_id; ?>;
var billing = null;
@@ -148,6 +154,7 @@
$('#btn_receipt').attr('href', '<?php echo $server_url?>finance/receipt.php?billing_id=' + billing.id);
$('#btn_receipt').toggleClass('d-none', String(billing.status) === '4' || parseFloat(billing.balance_amount || 0) <= 0.0001);
$('#btn_void').toggleClass('d-none', String(billing.status) === '4' || parseFloat(billing.received_amount || 0) > 0.0001);
$('#btn_delete').toggleClass('d-none', !can_delete || !billing_id);
var rows = '';
$.each(billing.allocations || [], function(i, line) {
@@ -168,6 +175,25 @@
$('#allocation_tbody').html(rows || '<tr><td colspan="5" class="text-center py-5 text-muted">No documents found.</td></tr>');
}
function delete_billing() {
bootbox.confirm({
message: 'Delete this billing? All linked receipts must be deleted first.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_receipt_billing.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: billing_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>finance/receipt_billing.php'; }
});
}
});
}
function void_billing() {
bootbox.confirm('Void this receipt billing?', function(ok) {
if (!ok) return;
+22
View File
@@ -160,6 +160,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var open_billings = [];
var prefill_billing_id = <?php echo $prefill_billing_id; ?>;
var formula_map = {};
@@ -258,6 +261,7 @@
<a href="<?php echo $server_url?>finance/manage_payment.php?id=${r.id}" title="View payment">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_payment(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -457,6 +461,24 @@
$(this).removeAttr('data-id');
});
function delete_payment(id) {
bootbox.confirm({
message: 'Delete this payment? The GL entry and invoice allocations will be reversed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_payment.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { load_payments(); }
});
}
});
}
$(async function() {
flatpickr('#payment_date', { dateFormat: 'd/m/Y', allowInput: true });
load_departments('department_id');
+22
View File
@@ -138,6 +138,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var billable_documents = [];
function show_billing_form() {
@@ -207,6 +210,7 @@
<a href="<?php echo $server_url?>finance/manage_payment_billing.php?id=${r.id}" title="View payment billing">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_billing(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -347,6 +351,24 @@
$(this).removeAttr('data-id');
});
function delete_billing(id) {
bootbox.confirm({
message: 'Delete this billing? All linked payments must be deleted first.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_payment_billing.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { load_billings(); }
});
}
});
}
$(function() {
flatpickr('#billing_date', { dateFormat: 'd/m/Y', allowInput: true });
load_billings();
+22
View File
@@ -188,6 +188,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var open_billings = [];
var prefill_billing_id = <?php echo $prefill_billing_id; ?>;
var formula_map = {};
@@ -288,6 +291,7 @@
<a href="<?php echo $server_url?>finance/manage_receipt.php?id=${r.id}" title="View receipt">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_receipt(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -482,6 +486,24 @@
$(this).removeAttr('data-id');
});
function delete_receipt(id) {
bootbox.confirm({
message: 'Delete this receipt? The GL entry and invoice allocations will be reversed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_receipt.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { load_receipts(); }
});
}
});
}
$(async function() {
flatpickr('#receipt_date', { dateFormat: 'd/m/Y', allowInput: true });
load_departments('department_id');
+22
View File
@@ -138,6 +138,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var billable_documents = [];
function show_billing_form() {
@@ -207,6 +210,7 @@
<a href="<?php echo $server_url?>finance/manage_receipt_billing.php?id=${r.id}" title="View receipt billing">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_billing(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -347,6 +351,24 @@
$(this).removeAttr('data-id');
});
function delete_billing(id) {
bootbox.confirm({
message: 'Delete this billing? All linked receipts must be deleted first.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>finance/api/engine/delete_receipt_billing.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { load_billings(); }
});
}
});
}
$(function() {
flatpickr('#billing_date', { dateFormat: 'd/m/Y', allowInput: true });
load_billings();
+1 -1
View File
@@ -1,7 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_once '../../../assets/utils/classes/CompanySettingManager.php';
require '../../../assets/utils/classes/BarcodeManager.php';
$barcode = trim($data['barcode'] ?? '');
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid invoice ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new InvoiceManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Document deleted.';
} 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));
+34
View File
@@ -0,0 +1,34 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new OrderManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Sales order deleted.';
} 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));
+34
View File
@@ -0,0 +1,34 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/ReturnManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid return ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new ReturnManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Return deleted.';
} 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));
+1 -1
View File
@@ -3,7 +3,7 @@
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_once '../../../assets/utils/classes/CompanySettingManager.php';
$id = (int)($data['id'] ?? 0);
$tracking_number = trim((string)($data['shipping_tracking_number'] ?? ''));
+22
View File
@@ -158,6 +158,8 @@
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function doc_type_badge(doc_type) {
const map = {
'invoice': '<span class="badge bg-primary">Invoice</span>',
@@ -269,6 +271,7 @@
onclick="void_invoice(${inv.id})">
<i class="ti ti-ban fs-5"></i>
</a>` : ''}
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_invoice(${inv.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -310,6 +313,7 @@
<a href="<?php echo $server_url?>order/manage_invoice.php?id=${cn.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_invoice(${cn.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -342,6 +346,24 @@
// ── Boot ─────────────────────────────────────────────────────────────────
function delete_invoice(id) {
bootbox.confirm({
message: 'Delete this document? Any GL entry will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_invoice.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_invoices(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+26
View File
@@ -146,6 +146,10 @@
<i class="ti ti-ban me-1"></i>Void Invoice
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete"
onclick="delete_invoice()">
<i class="ti ti-trash me-1"></i>Delete
</button>
</div>
</div>
@@ -160,6 +164,8 @@
<script>
// custom.js globals: format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var invoice_id = <?php echo $invoice_id; ?>;
var invoice_data = null;
@@ -223,6 +229,7 @@
!is_invoice ? 'Only original invoices can be voided'
: status === 0 ? 'Invoice has not been issued yet'
: status === 4 ? 'Invoice is already void' : '');
$('#btn_delete').toggleClass('d-none', !can_delete || !invoice_id);
}
@@ -344,6 +351,25 @@
function delete_invoice() {
bootbox.confirm({
message: 'Delete this document? Any GL entry will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_invoice.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: invoice_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>order/invoice.php'; }
});
}
});
}
function void_invoice() {
bootbox.confirm({
message: '<strong>Void this invoice?</strong><br>This action cannot be undone.',
+27
View File
@@ -216,6 +216,11 @@
<i class="ti ti-x me-1"></i>Cancel Order
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete"
onclick="delete_order()">
<i class="ti ti-trash me-1"></i>Delete Order
</button>
</div>
</div>
@@ -230,6 +235,8 @@
<script>
// custom.js globals: remove_item(), init_product_search_inputs(), format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var order_id = <?php echo $order_id; ?>;
var order_source = <?php echo json_encode($source); ?>;
var order_source_id = <?php echo (int)$source_id; ?>;
@@ -426,6 +433,7 @@
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');
$('#btn_delete').toggleClass('d-none', !can_delete || !order_id);
set_btn_state('#btn_tracking', can_update_tracking,
is_cancelled ? 'Order is cancelled'
@@ -631,6 +639,25 @@
// ── Cancel order ──────────────────────────────────────────────────────────
function delete_order() {
bootbox.confirm({
message: 'Delete this order? Unconfirmed stock-out rows will also be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_order.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: order_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>order/order.php'; }
});
}
});
}
function cancel_order() {
bootbox.confirm({
message: 'Cancel this order? All pending stock-out rows will be cancelled.',
+27
View File
@@ -189,6 +189,11 @@
<i class="ti ti-x me-1"></i>Cancel Return
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete"
onclick="delete_return()">
<i class="ti ti-trash me-1"></i>Delete Return
</button>
</div>
</div>
@@ -202,6 +207,8 @@
<script>
// custom.js globals: escape_html(), format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var return_id = <?php echo $return_id; ?>;
var return_data = null;
var order_data = null;
@@ -766,6 +773,7 @@
set_btn_state('#btn_cancel', is_draft || status === 1,
status === -1 ? 'Return is already cancelled' : 'Return is completed and cannot be cancelled');
$('#btn_delete').toggleClass('d-none', !can_delete || !return_id);
}
@@ -865,6 +873,25 @@
// ── Cancel return ─────────────────────────────────────────────────────────
function delete_return() {
bootbox.confirm({
message: 'Delete this return? Unconfirmed stock rows will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_return.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: return_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>order/return.php'; }
});
}
});
}
function cancel_return() {
bootbox.confirm({
message: 'Cancel this return request?',
+22
View File
@@ -162,6 +162,8 @@
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
// ── Status helpers ────────────────────────────────────────────────────────
function order_status_badge(status) {
status = parseInt(status);
@@ -268,6 +270,7 @@
onclick="cancel_order(${o.id})">
<i class="ti ti-x fs-5"></i>
</a>` : ''}
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_order(${o.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -301,6 +304,25 @@
}
function delete_order(id) {
bootbox.confirm({
message: 'Delete this order? Unconfirmed stock-out rows will also be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_order.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_orders(); }
});
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(async function() {
await load_dept_cache();
+21
View File
@@ -99,6 +99,8 @@
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function return_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
@@ -182,6 +184,7 @@
onclick="cancel_return(${r.id})">
<i class="ti ti-x fs-5"></i>
</a>` : ''}
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_return(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -212,6 +215,24 @@
}
function delete_return(id) {
bootbox.confirm({
message: 'Delete this return? Unconfirmed stock rows will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_return.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_returns(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+34
View File
@@ -0,0 +1,34 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/PurchaseOrderManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid PO ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new PurchaseOrderManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Purchase order deleted.';
} 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,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/PurchaseRequestManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid purchase request ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new PurchaseRequestManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Purchase request deleted.';
} 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,34 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/SupplierReturnManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid supplier return ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new SupplierReturnManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Supplier return deleted.';
} 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));
+22
View File
@@ -156,6 +156,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function doc_type_badge(doc_type) {
const map = {
'purchase_invoice': '<span class="badge bg-primary">Purchase Invoice</span>',
@@ -257,6 +260,7 @@
<a href="<?php echo $server_url?>po/manage_purchase_invoice.php?id=${inv.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_invoice(${inv.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -264,6 +268,24 @@
$('table#invoice_list tbody').html(body);
}
function delete_invoice(id) {
bootbox.confirm({
message: 'Delete this document? Any GL entry will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_invoice.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_invoices(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+26
View File
@@ -226,6 +226,10 @@
<i class="ti ti-x me-1"></i>Cancel PO
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_po()">
<i class="ti ti-trash me-1"></i>Delete PO
</button>
</div>
</div>
@@ -240,6 +244,8 @@
<script>
// custom.js globals: remove_item(), load_location_config(), init_product_search_inputs(), init_lot_search_inputs(), escape_html(), format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var po_id = <?php echo $po_id; ?>;
var po_data = null;
var warehouses = [];
@@ -635,6 +641,7 @@
is_pending ? 'Assign a default warehouse and save first. The PO will move to Draft.' : 'PO must be in draft to confirm');
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
is_cancelled ? 'PO is already cancelled' : 'Only draft or confirmed POs can be cancelled');
$('#btn_delete').toggleClass('d-none', !can_delete || !po_id);
$('#receive_card').toggleClass('d-none', !can_receive);
$('#btn_close_po').toggleClass('d-none', !can_close);
@@ -878,6 +885,25 @@
});
}
function delete_po() {
bootbox.confirm({
message: 'Delete this PO? Unconfirmed stock-in reservations will be released.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_po.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: po_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>po/po.php'; }
});
}
});
}
function cancel_po() {
bootbox.confirm({
message: 'Cancel this purchase order? Any pending (unapproved) stock-in rows will be cancelled.',
+27
View File
@@ -140,6 +140,10 @@
<i class="ti ti-ban me-1"></i>Void
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_invoice()">
<i class="ti ti-trash me-1"></i>Delete
</button>
</div>
</div>
@@ -151,6 +155,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var invoice_id = <?php echo $invoice_id; ?>;
var invoice_data = null;
@@ -199,6 +206,7 @@
!is_pi ? 'Only purchase invoices can be voided'
: status === 0 ? 'Issue the document before voiding'
: status === 4 ? 'Already void' : '');
$('#btn_delete').toggleClass('d-none', !can_delete || !invoice_id);
}
function retrieve_invoice() {
@@ -362,6 +370,25 @@
});
}
function delete_invoice() {
bootbox.confirm({
message: 'Delete this document? Any GL entry will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_invoice.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: invoice_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>po/invoice.php'; }
});
}
});
}
function void_invoice() {
bootbox.confirm({
message: '<strong>Void this purchase invoice?</strong><br>This action cannot be undone.',
+26
View File
@@ -182,6 +182,10 @@
<i class="ti ti-x me-1"></i>Cancel Return
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_supplier_return()">
<i class="ti ti-trash me-1"></i>Delete Return
</button>
</div>
</div>
@@ -193,6 +197,8 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var return_id = <?php echo $return_id; ?>;
var return_data = null;
var po_data = null;
@@ -404,6 +410,7 @@
set_btn_state('#btn_cancel', is_draft || status === 1,
status === -1 ? 'Return is already cancelled' : 'Cannot cancel');
$('#btn_delete').toggleClass('d-none', !can_delete || !return_id);
}
@@ -587,6 +594,25 @@
});
}
function delete_supplier_return() {
bootbox.confirm({
message: 'Delete this return? Unconfirmed stock rows will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_supplier_return.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: return_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>po/supplier_returns.php'; }
});
}
});
}
function cancel_return() {
bootbox.confirm({
message: 'Cancel this supplier return?',
+22
View File
@@ -158,6 +158,8 @@
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function po_status_badge(status) {
status = parseInt(status);
const map = {
@@ -251,6 +253,7 @@
onclick="cancel_po(${o.id})">
<i class="ti ti-x fs-5"></i>
</a>` : ''}
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_po(${o.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -283,6 +286,25 @@
}
function delete_po(id) {
bootbox.confirm({
message: 'Delete this PO? Unconfirmed stock-in reservations will be released.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_po.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_pos(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+22
View File
@@ -148,6 +148,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function return_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
@@ -238,6 +241,7 @@
onclick="show_stock_rows('supplier_return', ${r.id}, '${escape_html(r.return_number || '')}')">
<i class="ti ti-packages fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_supplier_return(${r.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -245,6 +249,24 @@
$('table#return_list tbody').html(body);
}
function delete_supplier_return(id) {
bootbox.confirm({
message: 'Delete this return? Unconfirmed stock rows will be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/delete_supplier_return.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_returns(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin']);
require '../../../assets/utils/classes/QuotationManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid quotation ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$mgr = new QuotationManager($pdo, $company_id);
$mgr->softDelete($id);
});
$answer['success'] = 1;
$answer['message'] = 'Quotation deleted.';
} 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));
+25
View File
@@ -157,6 +157,9 @@
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="cancel_order()">
<i class="ti ti-x me-1"></i>Cancel Order
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_order()">
<i class="ti ti-trash me-1"></i>Delete Order
</button>
<a href="<?php echo $server_url?>revenue/order.php" class="btn btn-light w-100">
Back
</a>
@@ -172,6 +175,8 @@
<script>
// custom.js globals: remove_item(), init_product_search_inputs(), escape_html(), format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var order_id = <?php echo $order_id; ?>;
var order_data = null;
var item_index = 0;
@@ -306,6 +311,7 @@
}
$('#btn_cancel').toggleClass('d-none', status !== -2);
$('#btn_delete').toggleClass('d-none', !can_delete || !order_id);
var is_existing = order_id > 0;
var can_invoice = is_existing && status >= 1 && status !== -1 && !has_active_invoice;
@@ -426,6 +432,25 @@
});
}
function delete_order() {
bootbox.confirm({
message: 'Delete this order? Unconfirmed stock-out rows will also be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_order.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: order_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>revenue/order.php'; }
});
}
});
}
function cancel_order() {
bootbox.confirm({
message: 'Cancel this pending sales order? It will be removed from WMS queue.',
+27
View File
@@ -187,6 +187,11 @@
<i class="ti ti-ban me-1"></i>Cancel Quotation
</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete"
onclick="delete_quotation()">
<i class="ti ti-trash me-1"></i>Delete Quotation
</button>
<hr class="d-none" id="hr_convert">
<a href="javascript:void(0);" class="btn btn-success w-100 d-none" id="btn_convert"
onclick="convert_to_order()">
@@ -207,6 +212,8 @@
<script>
// custom.js globals: remove_item(), init_product_search_inputs(), escape_html(), format_number()
const can_delete = ['admin', 'owner'].includes(user_role);
var quotation_id = <?php echo $quotation_id; ?>;
var quotation_data = null;
var item_index = 0;
@@ -342,6 +349,7 @@
$('#btn_reject').toggleClass('d-none', !is_sent);
$('#btn_reopen').toggleClass('d-none', is_draft || is_closed);
$('#btn_cancel').toggleClass('d-none', is_closed || (is_draft && !quotation_id));
$('#btn_delete').toggleClass('d-none', !can_delete || !quotation_id);
// Convert to Order — only on Accepted, not yet converted
var show_convert = is_accepted && !is_converted;
@@ -481,6 +489,25 @@
}
function delete_quotation() {
bootbox.confirm({
message: 'Delete this quotation? This cannot be undone.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/delete_quotation.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: quotation_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>revenue/quotation.php'; }
});
}
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
+22
View File
@@ -152,6 +152,9 @@
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
function order_status_badge(status) {
const map = {
'-2': '<span class="badge bg-danger">Pending Warehouse</span>',
@@ -243,6 +246,7 @@
<a href="<?php echo $server_url?>revenue/manage_order.php?id=${o.id}" title="View SO">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_order(${o.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -250,6 +254,24 @@
$('table#revenue_order_list tbody').html(body);
}
function delete_order(id) {
bootbox.confirm({
message: 'Delete this order? Unconfirmed stock-out rows will also be removed.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/delete_order.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_orders(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+21
View File
@@ -158,6 +158,8 @@
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var status_badge = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
@@ -224,6 +226,7 @@
<a href="<?php echo $server_url?>revenue/manage_quotation.php?id=${q.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_delete ? `<a href="javascript:void(0);" class="link-danger ms-2" title="Delete" onclick="delete_quotation(${q.id})"><i class="ti ti-trash fs-5"></i></a>` : ''}
</td>
</tr>`;
});
@@ -231,6 +234,24 @@
$('table#quotation_list tbody').html(body);
}
function delete_quotation(id) {
bootbox.confirm({
message: 'Delete this quotation? This cannot be undone.',
buttons: { confirm: { label: 'Delete', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>revenue/api/engine/delete_quotation.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: id },
onSuccess: function() { retrieve_quotations(); }
});
}
});
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
+1 -1
View File
@@ -1,7 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
require_once '../../../assets/utils/classes/CompanySettingManager.php';
if (($data['action'] ?? '') === 'update') {
if ($user_role !== 'owner') { http_response_code(403); exit(json_encode(['success' => 0, 'message' => 'Only the owner can modify this setting.'])); }