softDelete features
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user