Fix customer return confirm, auto credit note VAT and quotation link
Store the put-away location on return lines (new td_return_item columns, needs setup.php), split auto credit notes into net and VAT with the parent's department, drop the write to a td_quotation column that does not exist, and have the demo seed confirm its customer return.
This commit is contained in:
@@ -905,18 +905,46 @@ class InvoiceManager {
|
||||
|
||||
$log = [array_merge($logging, ['action' => 'create_credit_note'])];
|
||||
|
||||
// Split the credited amount into net and VAT from the lines being
|
||||
// credited. This used to store the whole VAT-inclusive amount as the
|
||||
// subtotal with tax = 0, so the header disagreed with its own lines: the
|
||||
// VAT report missed the output-tax reversal, and a GL formula posting
|
||||
// from the header reversed revenue by the gross figure.
|
||||
//
|
||||
// The VAT is taken as "amount minus net" so the grand total still
|
||||
// equals the caller's amount exactly, including any rounding adjustment
|
||||
// the return carried; that adjustment is recorded as tax_adjustment.
|
||||
// With no priced lines to split by, the amount is kept whole as before.
|
||||
$net = round(array_reduce($items, fn($c, $i) => $c + (float)($i['total_price'] ?? 0), 0.0), 4);
|
||||
$line_tax = round(array_reduce($items, fn($c, $i) => $c + (float)($i['tax_amount'] ?? 0), 0.0), 2);
|
||||
|
||||
if ($net > 0 && $net <= abs($amount) + 0.005) {
|
||||
$subtotal = $net;
|
||||
$tax = round(abs($amount) - $net, 4);
|
||||
$tax_adj = round($tax - $line_tax, 2);
|
||||
} else {
|
||||
$subtotal = abs($amount);
|
||||
$tax = 0.0;
|
||||
$tax_adj = 0.0;
|
||||
}
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO td_invoice
|
||||
(company_id, uuid, source_id, `source`, doc_type, invoice_number, ref_invoice_id,
|
||||
order_id, contact_id, issued_date, due_date,
|
||||
subtotal, discount, tax, shipping_fee, grand_total,
|
||||
order_id, contact_id, department_id, issued_date, due_date,
|
||||
subtotal, discount, tax, tax_adjustment, shipping_fee, grand_total,
|
||||
status, notes, `log`)
|
||||
VALUES
|
||||
(:company_id, :uuid, :source_id, :source, 'credit_note', :invoice_number, :ref_invoice_id,
|
||||
:order_id, :contact_id, :issued_date, NULL,
|
||||
:amount, 0, 0, 0, :grand_total,
|
||||
:order_id, :contact_id, :department_id, :issued_date, NULL,
|
||||
:amount, 0, :tax, :tax_adjustment, 0, :grand_total,
|
||||
1, '', :log)"
|
||||
)->execute([
|
||||
// The credit note belongs to the same department as the invoice it
|
||||
// corrects; it was left at 0 before.
|
||||
':department_id' => (int)($parent['department_id'] ?? 0),
|
||||
':tax' => $tax,
|
||||
':tax_adjustment' => $tax_adj,
|
||||
':company_id' => $this->company_id,
|
||||
':uuid' => bin2hex(random_bytes(16)),
|
||||
':source_id' => $source_id,
|
||||
@@ -926,7 +954,7 @@ class InvoiceManager {
|
||||
':order_id' => (int)$parent['order_id'],
|
||||
':contact_id' => (int)$parent['contact_id'],
|
||||
':issued_date' => $issued_date,
|
||||
':amount' => $amount,
|
||||
':amount' => $subtotal,
|
||||
':grand_total' => -abs($amount), // negative for net-balance queries
|
||||
':log' => json_encode($log),
|
||||
]);
|
||||
|
||||
@@ -406,12 +406,23 @@ class OrderManager {
|
||||
return $returnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an accepted quotation as converted once an order has been created
|
||||
* from it.
|
||||
*
|
||||
* The link itself lives on the order (td_order.source = 'quotation',
|
||||
* source_id = quotation id), which saveOrder() has already written and
|
||||
* QuotationManager::getById() joins on. This used to also write
|
||||
* td_quotation.order_id — a column that has never existed — so saving an
|
||||
* order with source=quotation failed with "Unknown column" and rolled the
|
||||
* new order back with it.
|
||||
*/
|
||||
public function linkQuotationToOrder(int $quotation_id, int $order_id): void
|
||||
{
|
||||
$this->pdo->prepare(
|
||||
"UPDATE td_quotation SET order_id = :order_id, status = 5
|
||||
"UPDATE td_quotation SET status = 5
|
||||
WHERE id = :id AND company_id = :cid AND status = 2"
|
||||
)->execute([':order_id' => $order_id, ':id' => $quotation_id, ':cid' => $this->company_id]);
|
||||
)->execute([':id' => $quotation_id, ':cid' => $this->company_id]);
|
||||
}
|
||||
|
||||
public function assertRevenueOrderEditable(int $order_id): void
|
||||
|
||||
@@ -122,13 +122,28 @@ class ReturnManager {
|
||||
$sth = $this->pdo->prepare(
|
||||
"INSERT INTO td_return_item
|
||||
(company_id, return_id, item_id, product_sku, product_name,
|
||||
quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_out_id, stock_out_warehouse_id)
|
||||
quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_out_id, stock_out_warehouse_id,
|
||||
zone, aisle, bin)
|
||||
VALUES
|
||||
(:company_id, :return_id, :item_id, :product_sku, :product_name,
|
||||
:quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_out_id, :stock_out_warehouse_id)"
|
||||
:quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_out_id, :stock_out_warehouse_id,
|
||||
:zone, :aisle, :bin)"
|
||||
);
|
||||
foreach ($items as $pos => $item) {
|
||||
// Put-away location chosen on the form. In simple location mode
|
||||
// the page sends only the bin; zone and aisle mirror it, the same
|
||||
// convention stock-in and goods receipt use, because md_bin is
|
||||
// looked up on all three.
|
||||
$bin = trim((string)($item['bin'] ?? ''));
|
||||
$zone = trim((string)($item['zone'] ?? ''));
|
||||
$aisle = trim((string)($item['aisle'] ?? ''));
|
||||
if ($zone === '' && $bin !== '') $zone = $bin;
|
||||
if ($aisle === '' && $bin !== '') $aisle = $bin;
|
||||
|
||||
$sth->execute([
|
||||
':zone' => $zone,
|
||||
':aisle' => $aisle,
|
||||
':bin' => $bin,
|
||||
':company_id' => $this->company_id,
|
||||
':return_id' => $return_id,
|
||||
':item_id' => $pos + 1,
|
||||
@@ -474,6 +489,16 @@ class ReturnManager {
|
||||
throw new Exception("Item #{$i}: missing linked stock-out record.");
|
||||
}
|
||||
|
||||
// Returned goods are put back into a specific bin. A return saved
|
||||
// before the location columns existed has none recorded; name the
|
||||
// fix rather than letting occupyBin() fail on an empty location.
|
||||
if (trim((string)($item['bin'] ?? '')) === '') {
|
||||
$name = $item['product_name'] ?: $product_sku;
|
||||
throw new Exception(
|
||||
"\"{$name}\" has no return location. Open the return, choose where it goes back to, save, then confirm."
|
||||
);
|
||||
}
|
||||
|
||||
$stock_out_table = $this->stockTableNameFromWarehouseId($stock_out_wh);
|
||||
$stock_out_sth = $this->pdo->prepare(
|
||||
"SELECT status, lot_number, serial_number, price
|
||||
|
||||
Reference in New Issue
Block a user