refactor lot matching into shared module
This commit is contained in:
@@ -3,12 +3,12 @@ package repository
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/warehouse"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -288,60 +288,11 @@ func (r *PricelistRepository) enrichWarehouseItems(items []models.PricelistItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
lotSet := make(map[string]struct{}, len(lots))
|
||||
for _, lot := range lots {
|
||||
lotSet[lot] = struct{}{}
|
||||
}
|
||||
|
||||
resolver, err := r.newWarehouseLotResolver()
|
||||
qtyByLot, partnumbersByLot, err := warehouse.LoadLotMetrics(r.db, lots, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var logs []struct {
|
||||
Partnumber string `gorm:"column:partnumber"`
|
||||
Qty *float64 `gorm:"column:qty"`
|
||||
}
|
||||
if err := r.db.Model(&models.StockLog{}).Select("partnumber, qty").Find(&logs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
qtyByLot := make(map[string]float64, len(lots))
|
||||
for _, row := range logs {
|
||||
if row.Qty == nil {
|
||||
continue
|
||||
}
|
||||
lot, err := resolver.resolve(row.Partnumber)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := lotSet[lot]; !ok {
|
||||
continue
|
||||
}
|
||||
qtyByLot[lot] += *row.Qty
|
||||
}
|
||||
|
||||
var mappings []models.LotPartnumber
|
||||
if err := r.db.Where("lot_name IN ? AND TRIM(lot_name) <> ''", lots).
|
||||
Order("partnumber ASC").
|
||||
Find(&mappings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
partnumbersByLot := make(map[string][]string, len(lots))
|
||||
seenPair := make(map[string]struct{}, len(mappings))
|
||||
for _, m := range mappings {
|
||||
lot := strings.TrimSpace(m.LotName)
|
||||
pn := strings.TrimSpace(m.Partnumber)
|
||||
if lot == "" || pn == "" {
|
||||
continue
|
||||
}
|
||||
key := lot + "\x00" + strings.ToLower(pn)
|
||||
if _, ok := seenPair[key]; ok {
|
||||
continue
|
||||
}
|
||||
seenPair[key] = struct{}{}
|
||||
partnumbersByLot[lot] = append(partnumbersByLot[lot], pn)
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
if qty, ok := qtyByLot[items[i].LotName]; ok {
|
||||
q := qty
|
||||
@@ -352,131 +303,6 @@ func (r *PricelistRepository) enrichWarehouseItems(items []models.PricelistItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errWarehouseResolveConflict = errors.New("multiple lot matches")
|
||||
errWarehouseResolveNotFound = errors.New("lot not found")
|
||||
)
|
||||
|
||||
type warehouseLotResolver struct {
|
||||
partnumberToLots map[string][]string
|
||||
exactLots map[string]string
|
||||
allLots []string
|
||||
}
|
||||
|
||||
func (r *PricelistRepository) newWarehouseLotResolver() (*warehouseLotResolver, error) {
|
||||
var mappings []models.LotPartnumber
|
||||
if err := r.db.Find(&mappings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
partnumberToLots := make(map[string][]string, len(mappings))
|
||||
for _, m := range mappings {
|
||||
pn := normalizeWarehouseResolverKey(m.Partnumber)
|
||||
lot := strings.TrimSpace(m.LotName)
|
||||
if pn == "" || lot == "" {
|
||||
continue
|
||||
}
|
||||
partnumberToLots[pn] = append(partnumberToLots[pn], lot)
|
||||
}
|
||||
for key, vals := range partnumberToLots {
|
||||
partnumberToLots[key] = uniqueWarehouseStrings(vals)
|
||||
}
|
||||
|
||||
var allLotsRows []models.Lot
|
||||
if err := r.db.Select("lot_name").Find(&allLotsRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exactLots := make(map[string]string, len(allLotsRows))
|
||||
allLots := make([]string, 0, len(allLotsRows))
|
||||
for _, row := range allLotsRows {
|
||||
lot := strings.TrimSpace(row.LotName)
|
||||
if lot == "" {
|
||||
continue
|
||||
}
|
||||
exactLots[normalizeWarehouseResolverKey(lot)] = lot
|
||||
allLots = append(allLots, lot)
|
||||
}
|
||||
sort.Slice(allLots, func(i, j int) bool {
|
||||
li := len([]rune(allLots[i]))
|
||||
lj := len([]rune(allLots[j]))
|
||||
if li == lj {
|
||||
return allLots[i] < allLots[j]
|
||||
}
|
||||
return li > lj
|
||||
})
|
||||
|
||||
return &warehouseLotResolver{
|
||||
partnumberToLots: partnumberToLots,
|
||||
exactLots: exactLots,
|
||||
allLots: allLots,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *warehouseLotResolver) resolve(partnumber string) (string, error) {
|
||||
key := normalizeWarehouseResolverKey(partnumber)
|
||||
if key == "" {
|
||||
return "", errWarehouseResolveNotFound
|
||||
}
|
||||
|
||||
if mapped := r.partnumberToLots[key]; len(mapped) > 0 {
|
||||
if len(mapped) == 1 {
|
||||
return mapped[0], nil
|
||||
}
|
||||
return "", errWarehouseResolveConflict
|
||||
}
|
||||
if exact, ok := r.exactLots[key]; ok {
|
||||
return exact, nil
|
||||
}
|
||||
|
||||
best := ""
|
||||
bestLen := -1
|
||||
tie := false
|
||||
for _, lot := range r.allLots {
|
||||
lotKey := normalizeWarehouseResolverKey(lot)
|
||||
if lotKey == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(key, lotKey) {
|
||||
l := len([]rune(lotKey))
|
||||
if l > bestLen {
|
||||
best = lot
|
||||
bestLen = l
|
||||
tie = false
|
||||
} else if l == bestLen && !strings.EqualFold(best, lot) {
|
||||
tie = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if best == "" {
|
||||
return "", errWarehouseResolveNotFound
|
||||
}
|
||||
if tie {
|
||||
return "", errWarehouseResolveConflict
|
||||
}
|
||||
return best, nil
|
||||
}
|
||||
|
||||
func normalizeWarehouseResolverKey(v string) string {
|
||||
return strings.ToLower(strings.TrimSpace(v))
|
||||
}
|
||||
|
||||
func uniqueWarehouseStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
n := strings.TrimSpace(v)
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(n)
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetPriceForLot returns item price for a lot within a pricelist.
|
||||
func (r *PricelistRepository) GetPriceForLot(pricelistID uint, lotName string) (float64, error) {
|
||||
var item models.PricelistItem
|
||||
|
||||
Reference in New Issue
Block a user