Derive stock and availability from the ledger, not stored fields
Stock shown in the console did not match the productstocks ledger, and two product endpoints were failing outright. Every cause was on the read side or in how the availability flag was maintained; the ledger writes themselves (CreateOrder's "out" entry, cancellation's "in" entry) were already correct. Read fixes, repositories/productRepository.go: - GetProductStocks returned SQLSTATE 42803 on every call: bare a.tenantid / a.stocktype / a.status under GROUP BY a.productid. The per-ledger-row columns are now aggregated and the grouping covers the identity columns. - FetchFilteredProducts filtered on an alias `e` that no query defines, so every /getallproducts call carrying a locationid failed with SQLSTATE 42P01 instead of returning products. - FetchFilteredProducts joined productlocations on productid alone and joined a (productid, locationid)-grouped stock subquery on productid alone, so a product carried by three outlets came back three times, each row showing another outlet's quantity and status. Both are now tenant-scoped subqueries collapsed to one row per product and scoped to the outlet when one is given. - GetProductStocks and FetchFilteredProducts compared stocktype = 'in' case-sensitively. Production holds 'in' and 'IN' both, so uppercase receipts were silently dropped from the balance: one outlet reported 0 for a product holding 50, another reported 0 for twelve products holding 200-840. - GetStockStatement summed opening over stockdate <= CURRENT_DATE, making it arithmetically identical to closing. The Inventory ledger showed the same number in both columns on every row, which reads as stock never moving. Availability flag: productlocations.status was maintained by two different rules — the order path derived it from the balance, the receiving path set 'available' on any "in" entry regardless of the resulting balance. A partial restock that left the balance at or below zero marked a product sellable, and a flag set by an old order never cleared for stock that arrived by a route the API did not own. Both paths now derive the flag from the live balance through one rule: SyncProductLocationStatus (receiving side) and syncProductLocationStatus (order side, inside the caller's transaction). ReactivateProductLocations is replaced by the former; the service no longer filters refs by stocktype, since the direction of the movement is no longer what decides the flag. A row that has already drifted now repairs itself on its next ledger entry. Verified against the live database: all four stock endpoints return matching balances, /getallproducts no longer duplicates rows, and the flag sync was exercised in both directions inside a rolled-back transaction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1007,6 +1007,36 @@ func (r *orderRepository) GetOrderDetails(orderHeaderID int) ([]models.OrderDeta
|
|||||||
return details, nil
|
return details, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// syncProductLocationStatus re-derives productlocations.status for one
|
||||||
|
// product at one outlet from the live productstocks balance — "available"
|
||||||
|
// above zero, "outofstock" at or below it. It runs inside the caller's
|
||||||
|
// transaction so the flag is committed together with the ledger entry that
|
||||||
|
// moved it, and it is the same rule productRepository.SyncProductLocationStatus
|
||||||
|
// applies on the receiving side; both paths agreeing is what stops the flag
|
||||||
|
// drifting away from the ledger over time.
|
||||||
|
//
|
||||||
|
// Status errors are deliberately swallowed: an order must not fail because a
|
||||||
|
// display flag could not be refreshed, and the next ledger entry re-derives it.
|
||||||
|
func syncProductLocationStatus(tx *gorm.DB, tenantid, locationid, productid int) {
|
||||||
|
if tenantid <= 0 || locationid <= 0 || productid <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := tx.Exec(`
|
||||||
|
UPDATE productlocations
|
||||||
|
SET status = CASE WHEN (
|
||||||
|
SELECT COALESCE(
|
||||||
|
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
|
||||||
|
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END), 0)
|
||||||
|
FROM productstocks
|
||||||
|
WHERE productid = ? AND tenantid = ? AND locationid = ?
|
||||||
|
) > 0 THEN 'available' ELSE 'outofstock' END
|
||||||
|
WHERE tenantid = ? AND locationid = ? AND productid = ?`,
|
||||||
|
productid, tenantid, locationid,
|
||||||
|
tenantid, locationid, productid).Error; err != nil {
|
||||||
|
log.Println("syncProductLocationStatus:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *orderRepository) UpdateOrder(order *models.Orders) error {
|
func (r *orderRepository) UpdateOrder(order *models.Orders) error {
|
||||||
tx := r.db.Begin()
|
tx := r.db.Begin()
|
||||||
|
|
||||||
@@ -1043,10 +1073,11 @@ func (r *orderRepository) UpdateOrder(order *models.Orders) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update productlocation status back to available if stock is restored
|
// Re-derive availability from the restored balance rather
|
||||||
tx.Table("productlocations").
|
// than assuming "available": cancelling one line of a
|
||||||
Where("productid = ? AND tenantid = ? AND locationid = ?", item.Productid, existingOrder.Tenantid, itemLocID).
|
// heavily oversold product can leave it still at or below
|
||||||
Update("status", "available")
|
// zero, in which case it must stay flagged outofstock.
|
||||||
|
syncProductLocationStatus(tx, existingOrder.Tenantid, itemLocID, item.Productid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1254,24 +1285,9 @@ func (r *orderRepository) CreateOrder(data models.Orders) (models.Orders, error)
|
|||||||
return models.Orders{}, err
|
return models.Orders{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update productlocation status to 'outofstock' if remaining stock drops to 0 or below
|
// Re-derive the location's availability flag from the ledger balance
|
||||||
var remainingStock int
|
// this "out" entry just produced.
|
||||||
remStockQuery := `
|
syncProductLocationStatus(tx, data.Tenantid, itemLocID, item.Productid)
|
||||||
SELECT COALESCE(
|
|
||||||
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
|
|
||||||
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
FROM productstocks
|
|
||||||
WHERE productid = ? AND tenantid = ? AND locationid = ?
|
|
||||||
`
|
|
||||||
if err := tx.Raw(remStockQuery, item.Productid, data.Tenantid, itemLocID).Scan(&remainingStock).Error; err == nil {
|
|
||||||
if remainingStock <= 0 {
|
|
||||||
tx.Table("productlocations").
|
|
||||||
Where("productid = ? AND tenantid = ? AND locationid = ?", item.Productid, data.Tenantid, itemLocID).
|
|
||||||
Update("status", "outofstock")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.updateSeqno(data.Tenantid, "ORD"); err != nil {
|
if err := r.updateSeqno(data.Tenantid, "ORD"); err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type ProductRepository interface {
|
|||||||
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error)
|
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error)
|
||||||
CreateProductStock(stocks []models.Productstock) error
|
CreateProductStock(stocks []models.Productstock) error
|
||||||
UpdateProductStatus(productIDs []int, status string) error
|
UpdateProductStatus(productIDs []int, status string) error
|
||||||
ReactivateProductLocations(refs []models.ProductLocationRef) error
|
SyncProductLocationStatus(refs []models.ProductLocationRef) error
|
||||||
CreateProduct(product models.Products) error
|
CreateProduct(product models.Products) error
|
||||||
UpdateProduct(product models.Products) error
|
UpdateProduct(product models.Products) error
|
||||||
DeleteProduct(productID int) error
|
DeleteProduct(productID int) error
|
||||||
@@ -222,16 +222,27 @@ func (r *productRepository) GetProductStocks(tenantID, locationID string) ([]mod
|
|||||||
var params []interface{}
|
var params []interface{}
|
||||||
var conditions []string
|
var conditions []string
|
||||||
|
|
||||||
|
// One row per product+location holding the live balance, so every
|
||||||
|
// per-ledger-row column has to be aggregated: this rolls up many
|
||||||
|
// productstocks rows and Postgres rejects a bare a.tenantid/a.stocktype
|
||||||
|
// under GROUP BY a.productid (that alone made this endpoint return a
|
||||||
|
// 42803 error instead of any stock at all).
|
||||||
|
//
|
||||||
|
// stocktype is matched case-insensitively because the ledger holds a mix
|
||||||
|
// of 'in' and 'IN' in production — a bare = 'in' silently dropped every
|
||||||
|
// uppercase receipt, which understated stock rather than erroring.
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
a.productid, a.tenantid, MAX(a.stockdate) AS stockdate, a.locationid, a.stocktype, a.maxquantity, a.minquantity, a.status,
|
a.productid, a.tenantid, a.locationid, MAX(a.stockdate) AS stockdate,
|
||||||
|
MAX(a.stocktype) AS stocktype, MAX(a.maxquantity) AS maxquantity,
|
||||||
|
MAX(a.minquantity) AS minquantity, MAX(a.status) AS status,
|
||||||
b.applocationid, b.categoryid, b.subcategoryid, b.catalogueid, b.addonid, b.discountid, b.pricingid,
|
b.applocationid, b.categoryid, b.subcategoryid, b.catalogueid, b.addonid, b.discountid, b.pricingid,
|
||||||
b.productname, b.productimage, b.productdesc, b.productsku, b.brandid, b.productbrand, b.productunit,
|
b.productname, b.productimage, b.productdesc, b.productsku, b.brandid, b.productbrand, b.productunit,
|
||||||
b.unitvalue, b.toppicks, b.productcost, b.taxamount, b.taxpercent, b.producttax, b.productstock,
|
b.unitvalue, b.toppicks, b.productcost, b.taxamount, b.taxpercent, b.producttax, b.productstock,
|
||||||
b.productcombo, b.variants, b.retailprice, b.diffprice, b.diffpercent, b.othercost, b.approve,
|
b.productcombo, b.variants, b.retailprice, b.diffprice, b.diffpercent, b.othercost, b.approve,
|
||||||
b.productstatus, b.created, b.updated, c.subcatname AS subcategoryname,
|
b.productstatus, b.created, b.updated, c.subcatname AS subcategoryname,
|
||||||
SUM(CASE WHEN a.stocktype = 'in' THEN a.quantity ELSE 0 END) -
|
SUM(CASE WHEN LOWER(a.stocktype) = 'in' THEN a.quantity ELSE 0 END) -
|
||||||
SUM(CASE WHEN a.stocktype = 'out' THEN a.quantity ELSE 0 END) AS quantity
|
SUM(CASE WHEN LOWER(a.stocktype) = 'out' THEN a.quantity ELSE 0 END) AS quantity
|
||||||
FROM productstocks a
|
FROM productstocks a
|
||||||
JOIN products b ON a.productid = b.productid
|
JOIN products b ON a.productid = b.productid
|
||||||
INNER JOIN productsubcategories c ON c.subcatid = b.subcategoryid
|
INNER JOIN productsubcategories c ON c.subcatid = b.subcategoryid
|
||||||
@@ -251,7 +262,8 @@ func (r *productRepository) GetProductStocks(tenantID, locationID string) ([]mod
|
|||||||
query += " WHERE " + strings.Join(conditions, " AND ")
|
query += " WHERE " + strings.Join(conditions, " AND ")
|
||||||
}
|
}
|
||||||
|
|
||||||
query += " GROUP BY a.productid"
|
// b.* / c.* ride along on the primary keys' functional dependency.
|
||||||
|
query += " GROUP BY a.productid, a.tenantid, a.locationid, b.productid, c.subcatid"
|
||||||
|
|
||||||
if err := r.db.Raw(query, params...).Scan(&stocks).Error; err != nil {
|
if err := r.db.Raw(query, params...).Scan(&stocks).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -264,17 +276,32 @@ func (r *productRepository) CreateProductStock(stocks []models.Productstock) err
|
|||||||
return r.db.Table("productstocks").Create(&stocks).Error
|
return r.db.Table("productstocks").Create(&stocks).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReactivateProductLocations flips productlocations.status back to
|
// SyncProductLocationStatus recomputes productlocations.status for each ref
|
||||||
// "available" for each ref — the counterpart to CreateOrder flagging a
|
// from the productstocks ledger: "available" when the live SUM(in)-SUM(out)
|
||||||
// location "outofstock" when its stock hits zero. Without this, a store
|
// balance is positive, "outofstock" when it is not.
|
||||||
// that runs out and then restocks via an approved stock request stays
|
//
|
||||||
// flagged outofstock forever, since receiving stock only ever added to the
|
// It replaces an earlier version that flipped the flag to "available"
|
||||||
// productstocks ledger and never touched this per-location flag.
|
// unconditionally on any receipt. That left the flag drifting from reality in
|
||||||
func (r *productRepository) ReactivateProductLocations(refs []models.ProductLocationRef) error {
|
// both directions — a receipt that only partly covered a negative balance
|
||||||
|
// marked the product sellable when it wasn't, and stock that arrived by any
|
||||||
|
// route the API didn't own (a direct insert, an import) never cleared an
|
||||||
|
// "outofstock" set by a much earlier order. Deriving the flag instead of
|
||||||
|
// assuming it means every write path converges on the same answer, and a row
|
||||||
|
// that has already drifted repairs itself on the next ledger entry.
|
||||||
|
func (r *productRepository) SyncProductLocationStatus(refs []models.ProductLocationRef) error {
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
if err := r.db.Table("productlocations").
|
if err := r.db.Exec(`
|
||||||
Where("tenantid = ? AND locationid = ? AND productid = ?", ref.Tenantid, ref.Locationid, ref.Productid).
|
UPDATE productlocations
|
||||||
Update("status", "available").Error; err != nil {
|
SET status = CASE WHEN (
|
||||||
|
SELECT COALESCE(
|
||||||
|
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
|
||||||
|
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END), 0)
|
||||||
|
FROM productstocks
|
||||||
|
WHERE productid = ? AND tenantid = ? AND locationid = ?
|
||||||
|
) > 0 THEN 'available' ELSE 'outofstock' END
|
||||||
|
WHERE tenantid = ? AND locationid = ? AND productid = ?`,
|
||||||
|
ref.Productid, ref.Tenantid, ref.Locationid,
|
||||||
|
ref.Tenantid, ref.Locationid, ref.Productid).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,9 +372,14 @@ func (r *productRepository) GetStockStatement(tenantID, locationID, subcategoryI
|
|||||||
|
|
||||||
params := []interface{}{tenantID, locationID}
|
params := []interface{}{tenantID, locationID}
|
||||||
|
|
||||||
|
// opening is the balance carried in from *before* today, so it stops at
|
||||||
|
// stockdate < CURRENT_DATE. It used to include today (<=), which made it
|
||||||
|
// arithmetically identical to closing — the Inventory ledger then showed
|
||||||
|
// the same number in both columns and looked like stock never moved, even
|
||||||
|
// on days with sales.
|
||||||
query := `SELECT a.productid,a.productname,a.productimage,a.categoryid,a.subcategoryid,a.productunit,a.unitvalue,a.productcost,a.taxpercent,a.taxamount,a.retailprice,b.tenantid,b.locationid,
|
query := `SELECT a.productid,a.productname,a.productimage,a.categoryid,a.subcategoryid,a.productunit,a.unitvalue,a.productcost,a.taxpercent,a.taxamount,a.retailprice,b.tenantid,b.locationid,
|
||||||
COALESCE( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date <= CURRENT_DATE THEN c.quantity ELSE 0 END) -
|
COALESCE( SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END) -
|
||||||
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date <= CURRENT_DATE THEN c.quantity ELSE 0 END),0 )
|
SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date < CURRENT_DATE THEN c.quantity ELSE 0 END),0 )
|
||||||
AS opening,
|
AS opening,
|
||||||
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS credit,
|
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS credit,
|
||||||
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS debit,
|
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' AND c.stockdate::date = CURRENT_DATE THEN c.quantity ELSE 0 END), 0) AS debit,
|
||||||
@@ -520,30 +552,55 @@ func (r *productRepository) FetchFilteredProducts(
|
|||||||
// Build product query
|
// Build product query
|
||||||
var products []models.Products
|
var products []models.Products
|
||||||
|
|
||||||
|
// Both derived tables collapse to one row per product before joining, and
|
||||||
|
// both are scoped by tenant + (optionally) location. Three things were
|
||||||
|
// wrong here and each one showed up as bad stock in the console:
|
||||||
|
// • productlocations was joined on productid alone, so a product stocked
|
||||||
|
// in three outlets came back three times, each row carrying another
|
||||||
|
// outlet's status.
|
||||||
|
// • the stock subquery grouped by (productid, locationid) but joined on
|
||||||
|
// productid only, so a product's quantity was whichever outlet's row
|
||||||
|
// the planner happened to pair it with — not this outlet's.
|
||||||
|
// • stocktype was compared case-sensitively against 'in'/'out' while the
|
||||||
|
// ledger stores a mix of 'in' and 'IN', so uppercase receipts were
|
||||||
|
// dropped from the balance.
|
||||||
|
// locationID 0 means "not scoped to an outlet": stock is then the tenant's
|
||||||
|
// total across outlets, which is what an unscoped listing should show.
|
||||||
query := r.db.
|
query := r.db.
|
||||||
Table("products a").
|
Table("products a").
|
||||||
Select(`
|
Select(`
|
||||||
a.*,
|
a.*,
|
||||||
b.status,
|
b.status,
|
||||||
c.categoryname,
|
b.locationid,
|
||||||
|
c.categoryname,
|
||||||
d.subcatname AS subcategoryname,
|
d.subcatname AS subcategoryname,
|
||||||
ps.locationid,
|
COALESCE(ps.quantity, 0) AS productstock,
|
||||||
COALESCE(ps.quantity, 0) AS quantity
|
COALESCE(ps.quantity, 0) AS quantity
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN productlocations b ON a.productid = b.productid").
|
Joins(`
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT productid, tenantid,
|
||||||
|
MAX(locationid) AS locationid,
|
||||||
|
MAX(status) AS status
|
||||||
|
FROM productlocations
|
||||||
|
WHERE (? = 0 OR locationid = ?)
|
||||||
|
GROUP BY productid, tenantid
|
||||||
|
) b ON b.productid = a.productid AND b.tenantid = a.tenantid
|
||||||
|
`, locationID, locationID).
|
||||||
Joins("LEFT JOIN productcategories c ON a.categoryid = c.categoryid").
|
Joins("LEFT JOIN productcategories c ON a.categoryid = c.categoryid").
|
||||||
Joins("LEFT JOIN productsubcategories d ON a.subcategoryid = d.subcatid").
|
Joins("LEFT JOIN productsubcategories d ON a.subcategoryid = d.subcatid").
|
||||||
Joins(`
|
Joins(`
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
productid,
|
productid,
|
||||||
locationid,
|
tenantid,
|
||||||
SUM(CASE WHEN stocktype = 'in' THEN quantity ELSE 0 END) -
|
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
|
||||||
SUM(CASE WHEN stocktype = 'out' THEN quantity ELSE 0 END) AS quantity
|
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END) AS quantity
|
||||||
FROM productstocks
|
FROM productstocks
|
||||||
GROUP BY productid, locationid
|
WHERE (? = 0 OR locationid = ?)
|
||||||
) ps ON ps.productid = a.productid
|
GROUP BY productid, tenantid
|
||||||
`).
|
) ps ON ps.productid = a.productid AND ps.tenantid = a.tenantid
|
||||||
|
`, locationID, locationID).
|
||||||
Where("a.tenantid = ?", tenantID).
|
Where("a.tenantid = ?", tenantID).
|
||||||
Order("a.productid DESC")
|
Order("a.productid DESC")
|
||||||
|
|
||||||
@@ -560,7 +617,13 @@ func (r *productRepository) FetchFilteredProducts(
|
|||||||
query = query.Where("a.productstatus = ?", productStatus)
|
query = query.Where("a.productstatus = ?", productStatus)
|
||||||
}
|
}
|
||||||
if locationID != 0 {
|
if locationID != 0 {
|
||||||
query = query.Where("e.locationid = ?", locationID)
|
// The outlet scope is already applied inside the productlocations and
|
||||||
|
// productstocks subqueries above; this only narrows the result to
|
||||||
|
// products actually carried by that outlet. It used to reference an
|
||||||
|
// alias `e` that no query in this file defines, so every call that
|
||||||
|
// passed a locationid failed outright with "missing FROM-clause entry
|
||||||
|
// for table e" instead of returning products.
|
||||||
|
query = query.Where("b.locationid = ?", locationID)
|
||||||
}
|
}
|
||||||
if approve != "" {
|
if approve != "" {
|
||||||
query = query.Where("a.approve = ?", approve)
|
query = query.Where("a.approve = ?", approve)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
"nearle/repositories"
|
"nearle/repositories"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,9 +87,12 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
productIDs = append(productIDs, stk.Productid)
|
productIDs = append(productIDs, stk.Productid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only "in" entries mean stock actually arrived — an "out" entry
|
// Every entry gets synced, "in" and "out" alike: the status is now
|
||||||
// (a sale) should never flip a location back to available.
|
// derived from the resulting balance rather than assumed from the
|
||||||
if stk.Productid > 0 && stk.Locationid > 0 && stk.Tenantid > 0 && strings.EqualFold(stk.Stocktype, "in") {
|
// direction of the movement, so an "out" that empties a location
|
||||||
|
// flags it outofstock and a partial "in" that leaves the balance at
|
||||||
|
// or below zero correctly does not mark it sellable.
|
||||||
|
if stk.Productid > 0 && stk.Locationid > 0 && stk.Tenantid > 0 {
|
||||||
ref := models.ProductLocationRef{Tenantid: stk.Tenantid, Locationid: stk.Locationid, Productid: stk.Productid}
|
ref := models.ProductLocationRef{Tenantid: stk.Tenantid, Locationid: stk.Locationid, Productid: stk.Productid}
|
||||||
if _, exists := locMap[ref]; !exists {
|
if _, exists := locMap[ref]; !exists {
|
||||||
locMap[ref] = struct{}{}
|
locMap[ref] = struct{}{}
|
||||||
@@ -106,7 +108,7 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(locRefs) > 0 {
|
if len(locRefs) > 0 {
|
||||||
if err := s.repo.ReactivateProductLocations(locRefs); err != nil {
|
if err := s.repo.SyncProductLocationStatus(locRefs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user