Stop stock receipts clobbering products.productstatus

products.productstatus is a per-product lifecycle field holding
"Active"/"Inactive". CreateProductStock overwrote it with "available" on every
stock receipt — an availability value written into a lifecycle column — which
destroyed the real lifecycle state of the rows it touched. 136 products now
read "available" and 12 "outofstock" with no way to recover what they were.

A single column on products cannot express availability anyway: the same
product can be stocked at one outlet and empty at another. That fact belongs
to productlocations.status, which SyncProductLocationStatus already derives
from the ledger, so the receipt path now updates only that and leaves
productstatus alone. UpdateProductStatus remains available as an explicit
admin operation; it is simply no longer called as a side effect of stock
movement.

GetProductCount counted available/outofstock off the same corrupted column and
returned near-nonsense as a result: across 6245 products it matched
'available' on 136 and 'outofstock' on 12, leaving 6097 — the real answer —
uncounted under "Active". It now derives both from the ledger, counting a
product available when it holds positive stock at any of the tenant's outlets,
so total = available + outofstock (6245 = 22 + 6223).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:08:35 +05:30
parent 3b60a90009
commit c94ddd34c7
2 changed files with 30 additions and 17 deletions

View File

@@ -88,12 +88,29 @@ func (r *productRepository) GetProductSubCategory(categoryID, tenantID int) ([]m
func (r *productRepository) GetProductCount(tenantid, categoryid, subcategory int, approve string) ([]models.Productcount, error) {
var data []models.Productcount
// available/outofstock are counted from the ledger, not from
// products.productstatus. That column is a lifecycle field ("Active" /
// "Inactive") that a bug in the stock-receipt path used to overwrite with
// availability values, so counting it returned near-nonsense: of 6245
// products it matched 'available' on 136 and 'outofstock' on 12, with the
// rest — the real answer — invisible under "Active".
//
// A product counts as available when it holds positive stock at any one of
// the tenant's outlets, which is the only sensible tenant-wide reading of a
// quantity that is really per-outlet. total = available + outofstock.
baseQuery := `
SELECT
SELECT
COUNT(*) AS total,
SUM(CASE WHEN a.productstatus = 'available' THEN 1 ELSE 0 END) AS available,
SUM(CASE WHEN a.productstatus = 'outofstock' THEN 1 ELSE 0 END) AS outofstock
SUM(CASE WHEN COALESCE(s.balance, 0) > 0 THEN 1 ELSE 0 END) AS available,
SUM(CASE WHEN COALESCE(s.balance, 0) <= 0 THEN 1 ELSE 0 END) AS outofstock
FROM products a
LEFT JOIN (
SELECT productid, tenantid,
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END) AS balance
FROM productstocks
GROUP BY productid, tenantid
) s ON s.productid = a.productid AND s.tenantid = a.tenantid
WHERE 1 = 1
`