diff --git a/repositories/productRepository.go b/repositories/productRepository.go index 63201b3..1f48a6a 100644 --- a/repositories/productRepository.go +++ b/repositories/productRepository.go @@ -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 ` diff --git a/services/productService.go b/services/productService.go index a74f0b1..db736ad 100644 --- a/services/productService.go +++ b/services/productService.go @@ -76,17 +76,9 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error return err } - idMap := make(map[int]struct{}) - var productIDs []int locMap := make(map[models.ProductLocationRef]struct{}) var locRefs []models.ProductLocationRef 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 // derived from the resulting balance rather than assumed from the // 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 { - if err := s.repo.UpdateProductStatus(productIDs, "available"); err != nil { - return err - } - } - + // products.productstatus is deliberately NOT touched here. It is a + // per-product lifecycle field holding "Active"/"Inactive", and receiving + // 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 err := s.repo.SyncProductLocationStatus(locRefs); err != nil { return err