fix: регистронезависимое сопоставление BOM↔корзина (фронт + CSV)
LOT из BOM-маппинга мог быть в смешанном регистре, а корзина — в каноничном UPPERCASE, из-за чего позиции дублировались (в таблице «Цена покупки» и в экспорте CSV). - localdb.NormalizeLotMappings: единая каноничная нормализация LOT-маппингов (UPPERCASE + схлопывание дублей с суммированием qty). Убраны две разошедшиеся копии normalizeLotMappings (handlers и services — последняя только тримила, что и было причиной бага в CSV). - export.go: BOM-ветка использует общую функцию + канонизирует LOT корзины для coverage/lookup. Удалена мёртвая computeMappingTotal. - index.html (renderPricingTab): сопоставление/дедуп LOT через каноничный ключ UPPERCASE; аксессоры _getRowBaseLot/_getRowAllocations возвращают канон. - Добавлен регресс-тест TestNormalizeLotMappings_*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,42 @@ import (
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
)
|
||||
|
||||
// NormalizeLotMappings is the single canonical normalizer for vendor BOM LOT
|
||||
// mappings. LOT names are canonicalized to their uppercase form (see
|
||||
// models.NormalizeLotName) so that all BOM↔cart matching is case-insensitive,
|
||||
// duplicate LOTs are merged (summing quantity-per-PN), and quantities are at
|
||||
// least 1. Returns nil for an empty result. Both the persistence path
|
||||
// (handlers) and the CSV export path must use this — do not reimplement it.
|
||||
func NormalizeLotMappings(in []VendorSpecLotMapping) []VendorSpecLotMapping {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
merged := make(map[string]int, len(in))
|
||||
order := make([]string, 0, len(in))
|
||||
for _, m := range in {
|
||||
lot := models.NormalizeLotName(m.LotName)
|
||||
if lot == "" {
|
||||
continue
|
||||
}
|
||||
qty := m.QuantityPerPN
|
||||
if qty < 1 {
|
||||
qty = 1
|
||||
}
|
||||
if _, exists := merged[lot]; !exists {
|
||||
order = append(order, lot)
|
||||
}
|
||||
merged[lot] += qty
|
||||
}
|
||||
if len(order) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]VendorSpecLotMapping, 0, len(order))
|
||||
for _, lot := range order {
|
||||
out = append(out, VendorSpecLotMapping{LotName: lot, QuantityPerPN: merged[lot]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ConfigurationToLocal converts models.Configuration to LocalConfiguration
|
||||
func ConfigurationToLocal(cfg *models.Configuration) *LocalConfiguration {
|
||||
items := make(LocalConfigItems, len(cfg.Items))
|
||||
|
||||
Reference in New Issue
Block a user