Implement warehouse/lot pricing updates and configurator performance fixes

This commit is contained in:
2026-02-07 05:20:35 +03:00
parent 72ff842f5d
commit b629af9742
26 changed files with 1701 additions and 305 deletions

View File

@@ -997,6 +997,7 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
"conflicts": result.Conflicts,
"fallback_matches": result.FallbackMatches,
"parse_errors": result.ParseErrors,
"qty_parse_errors": result.QtyParseErrors,
"ignored": result.Ignored,
"mapping_suggestions": result.MappingSuggestions,
"import_date": result.ImportDate.Format("2006-01-02"),
@@ -1031,6 +1032,7 @@ func (h *PricingHandler) ImportStockLog(c *gin.Context) {
"conflicts": p.Conflicts,
"fallback_matches": p.FallbackMatches,
"parse_errors": p.ParseErrors,
"qty_parse_errors": p.QtyParseErrors,
"ignored": p.Ignored,
"mapping_suggestions": p.MappingSuggestions,
"import_date": p.ImportDate,
@@ -1183,6 +1185,231 @@ func (h *PricingHandler) DeleteStockIgnoreRule(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
}
type LotTableRow struct {
LotName string `json:"lot_name"`
LotDescription string `json:"lot_description"`
Category string `json:"category"`
Partnumbers []string `json:"partnumbers"`
Popularity float64 `json:"popularity"`
EstimateCount int64 `json:"estimate_count"`
StockQty *float64 `json:"stock_qty"`
}
func (h *PricingHandler) ListLotsTable(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Список LOT доступен только в онлайн режиме",
"offline": true,
})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
search := strings.TrimSpace(c.Query("search"))
sortFieldParam := c.DefaultQuery("sort", "lot_name")
sortDirParam := strings.ToUpper(c.DefaultQuery("dir", "asc"))
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 200 {
perPage = 50
}
if sortDirParam != "ASC" && sortDirParam != "DESC" {
sortDirParam = "ASC"
}
type lotRow struct {
LotName string `gorm:"column:lot_name"`
LotDescription string `gorm:"column:lot_description"`
CategoryCode *string `gorm:"column:category_code"`
Popularity *float64 `gorm:"column:popularity_score"`
}
baseQuery := h.db.Table("lot").
Select("lot.lot_name, lot.lot_description, qt_categories.code as category_code, qt_lot_metadata.popularity_score").
Joins("LEFT JOIN qt_lot_metadata ON qt_lot_metadata.lot_name = lot.lot_name").
Joins("LEFT JOIN qt_categories ON qt_categories.id = qt_lot_metadata.category_id")
if search != "" {
baseQuery = baseQuery.Where("lot.lot_name LIKE ? OR lot.lot_description LIKE ?", "%"+search+"%", "%"+search+"%")
}
var total int64
if err := baseQuery.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
allowedDBSorts := map[string]string{
"lot_name": "lot.lot_name",
"category": "qt_categories.code",
"popularity_score": "qt_lot_metadata.popularity_score",
}
needsComputedSort := sortFieldParam == "estimate_count" || sortFieldParam == "stock_qty"
var rows []lotRow
rowsQuery := baseQuery.Session(&gorm.Session{})
if needsComputedSort {
rowsQuery = rowsQuery.Order("lot.lot_name ASC")
} else {
orderCol, ok := allowedDBSorts[sortFieldParam]
if !ok {
orderCol = "lot.lot_name"
}
rowsQuery = rowsQuery.Order(orderCol + " " + sortDirParam).Order("lot.lot_name ASC")
rowsQuery = rowsQuery.Offset((page - 1) * perPage).Limit(perPage)
}
if err := rowsQuery.Find(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(rows) == 0 {
c.JSON(http.StatusOK, gin.H{
"lots": []LotTableRow{},
"total": total,
"page": page,
"per_page": perPage,
})
return
}
// Collect lot names for batch subqueries
lotNames := make([]string, len(rows))
for i, r := range rows {
lotNames[i] = r.LotName
}
type countRow struct {
Lot string `gorm:"column:lot"`
Count int64 `gorm:"column:cnt"`
}
var estimateCounts []countRow
if err := h.db.Raw("SELECT lot, COUNT(*) as cnt FROM lot_log WHERE lot IN ? GROUP BY lot", lotNames).Scan(&estimateCounts).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
estimateMap := make(map[string]int64, len(estimateCounts))
for _, ec := range estimateCounts {
estimateMap[ec.Lot] = ec.Count
}
type stockRow struct {
LotName string `gorm:"column:lot_name"`
Qty *float64 `gorm:"column:total_qty"`
}
var stockRows []stockRow
if err := h.db.Raw(`
SELECT lp.lot_name, SUM(sl.qty) as total_qty
FROM stock_log sl
INNER JOIN lot_partnumbers lp ON LOWER(TRIM(lp.partnumber)) = LOWER(TRIM(sl.partnumber))
INNER JOIN (SELECT MAX(date) as max_date FROM stock_log) md ON sl.date = md.max_date
WHERE lp.lot_name IN ?
GROUP BY lp.lot_name
`, lotNames).Scan(&stockRows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
stockMap := make(map[string]*float64, len(stockRows))
for _, sr := range stockRows {
qty := sr.Qty
stockMap[sr.LotName] = qty
}
type pnRow struct {
LotName string `gorm:"column:lot_name"`
Partnumber string `gorm:"column:partnumber"`
}
var pnRows []pnRow
if err := h.db.Raw("SELECT lot_name, partnumber FROM lot_partnumbers WHERE lot_name IN ? ORDER BY lot_name, partnumber", lotNames).Scan(&pnRows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
pnMap := make(map[string][]string, len(pnRows))
for _, pn := range pnRows {
pnMap[pn.LotName] = append(pnMap[pn.LotName], pn.Partnumber)
}
result := make([]LotTableRow, len(rows))
for i, r := range rows {
cat := ""
if r.CategoryCode != nil {
cat = *r.CategoryCode
}
pop := 0.0
if r.Popularity != nil {
pop = *r.Popularity
}
result[i] = LotTableRow{
LotName: r.LotName,
LotDescription: r.LotDescription,
Category: cat,
Partnumbers: pnMap[r.LotName],
Popularity: pop,
EstimateCount: estimateMap[r.LotName],
StockQty: stockMap[r.LotName],
}
if result[i].Partnumbers == nil {
result[i].Partnumbers = []string{}
}
}
if needsComputedSort {
sort.SliceStable(result, func(i, j int) bool {
if sortFieldParam == "estimate_count" {
if result[i].EstimateCount == result[j].EstimateCount {
if sortDirParam == "DESC" {
return result[i].LotName > result[j].LotName
}
return result[i].LotName < result[j].LotName
}
if sortDirParam == "DESC" {
return result[i].EstimateCount > result[j].EstimateCount
}
return result[i].EstimateCount < result[j].EstimateCount
}
qi := 0.0
if result[i].StockQty != nil {
qi = *result[i].StockQty
}
qj := 0.0
if result[j].StockQty != nil {
qj = *result[j].StockQty
}
if qi == qj {
if sortDirParam == "DESC" {
return result[i].LotName > result[j].LotName
}
return result[i].LotName < result[j].LotName
}
if sortDirParam == "DESC" {
return qi > qj
}
return qi < qj
})
start := (page - 1) * perPage
if start >= len(result) {
result = []LotTableRow{}
} else {
end := start + perPage
if end > len(result) {
end = len(result)
}
result = result[start:end]
}
}
c.JSON(http.StatusOK, gin.H{
"lots": result,
"total": total,
"page": page,
"per_page": perPage,
})
}
func (h *PricingHandler) ListLots(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{