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:
@@ -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) {
|
func (r *productRepository) GetProductCount(tenantid, categoryid, subcategory int, approve string) ([]models.Productcount, error) {
|
||||||
var data []models.Productcount
|
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 := `
|
baseQuery := `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) AS total,
|
COUNT(*) AS total,
|
||||||
SUM(CASE WHEN a.productstatus = 'available' THEN 1 ELSE 0 END) AS available,
|
SUM(CASE WHEN COALESCE(s.balance, 0) > 0 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 outofstock
|
||||||
FROM products a
|
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
|
WHERE 1 = 1
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -76,17 +76,9 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
idMap := make(map[int]struct{})
|
|
||||||
var productIDs []int
|
|
||||||
locMap := make(map[models.ProductLocationRef]struct{})
|
locMap := make(map[models.ProductLocationRef]struct{})
|
||||||
var locRefs []models.ProductLocationRef
|
var locRefs []models.ProductLocationRef
|
||||||
for _, stk := range stocks {
|
for _, stk := range stocks {
|
||||||
if stk.Productid > 0 {
|
|
||||||
if _, exists := idMap[stk.Productid]; !exists {
|
|
||||||
idMap[stk.Productid] = struct{}{}
|
|
||||||
productIDs = append(productIDs, stk.Productid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Every entry gets synced, "in" and "out" alike: the status is now
|
// Every entry gets synced, "in" and "out" alike: the status is now
|
||||||
// derived from the resulting balance rather than assumed from the
|
// derived from the resulting balance rather than assumed from the
|
||||||
// direction of the movement, so an "out" that empties a location
|
// direction of the movement, so an "out" that empties a location
|
||||||
@@ -101,12 +93,16 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(productIDs) > 0 {
|
// products.productstatus is deliberately NOT touched here. It is a
|
||||||
if err := s.repo.UpdateProductStatus(productIDs, "available"); err != nil {
|
// per-product lifecycle field holding "Active"/"Inactive", and receiving
|
||||||
return err
|
// stock used to overwrite it with "available" — an availability value in a
|
||||||
}
|
// lifecycle column, which is how 136 products ended up reading "available"
|
||||||
}
|
// and 12 "outofstock" with their real lifecycle state destroyed.
|
||||||
|
//
|
||||||
|
// Availability is a per-outlet fact and belongs to productlocations.status,
|
||||||
|
// which SyncProductLocationStatus derives from the ledger below. A single
|
||||||
|
// column on products cannot express it anyway: the same product can be
|
||||||
|
// stocked at one outlet and empty at another.
|
||||||
if len(locRefs) > 0 {
|
if len(locRefs) > 0 {
|
||||||
if err := s.repo.SyncProductLocationStatus(locRefs); err != nil {
|
if err := s.repo.SyncProductLocationStatus(locRefs); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user