|
|
|
|
@@ -292,86 +292,91 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|
|
|
|
defByID[d.ID] = d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Strict token->dim_id resolver (no inference/persistence). UI should not invent dim mappings.
|
|
|
|
|
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
|
|
|
|
|
out := map[string]int64{}
|
|
|
|
|
if len(tokens) == 0 {
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
|
SELECT token, dim_id
|
|
|
|
|
FROM mk_dim_token_map
|
|
|
|
|
WHERE dim_column=$1 AND token = ANY($2)
|
|
|
|
|
`, column, pq.Array(tokens))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var tok string
|
|
|
|
|
var id int64
|
|
|
|
|
if err := rows.Scan(&tok, &id); err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out[strings.TrimSpace(tok)] = id
|
|
|
|
|
}
|
|
|
|
|
return out, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load all known (product_code, dim1, dim3_key) combos for products in this response.
|
|
|
|
|
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
|
|
|
|
|
out := map[string]struct{}{}
|
|
|
|
|
if len(productCodes) == 0 {
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
|
SELECT product_code, dim1, dim3_key
|
|
|
|
|
FROM mk_mmitem_dim_combo
|
|
|
|
|
WHERE product_code = ANY($1)
|
|
|
|
|
`, pq.Array(productCodes))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var code string
|
|
|
|
|
var dim1, dim3Key int64
|
|
|
|
|
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), dim1, dim3Key)
|
|
|
|
|
out[key] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
return out, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
codes := setToSortedSlice(codeSet)
|
|
|
|
|
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
|
|
|
|
|
dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
|
|
|
|
dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
|
|
|
|
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
|
|
|
|
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
|
|
|
|
combos, _ := loadProductDimCombos(ctx, pg, codes)
|
|
|
|
|
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
|
|
|
|
|
|
|
|
|
|
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
|
|
|
|
|
inferCache := map[string]int64{}
|
|
|
|
|
inferDim3ForMmitem := func(mmitemID int64, token string) int64 {
|
|
|
|
|
mmitemID = int64(mmitemID)
|
|
|
|
|
tok := strings.ToUpper(normalizeDimParam(token))
|
|
|
|
|
if mmitemID <= 0 || tok == "" {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
key := fmt.Sprintf("%d|%s", mmitemID, tok)
|
|
|
|
|
if v, ok := inferCache[key]; ok {
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
// Use the same pattern approach as resolveDimvalFromFileNameToken, but scoped to src_id.
|
|
|
|
|
patterns := buildNameLikePatterns(tok)
|
|
|
|
|
if len(patterns) == 0 {
|
|
|
|
|
inferCache[key] = 0
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
var dimv string
|
|
|
|
|
err := pg.QueryRowContext(ctx, `
|
|
|
|
|
SELECT x.dimv
|
|
|
|
|
FROM (
|
|
|
|
|
SELECT COALESCE(dimval3::text, '') AS dimv, COUNT(*) AS cnt
|
|
|
|
|
FROM dfblob
|
|
|
|
|
WHERE src_table='mmitem'
|
|
|
|
|
AND typ='img'
|
|
|
|
|
AND src_id=$7
|
|
|
|
|
AND COALESCE(dimval3::text, '') <> ''
|
|
|
|
|
AND (
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $1 OR
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $2 OR
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $3 OR
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $4 OR
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $5 OR
|
|
|
|
|
UPPER(COALESCE(file_name,'')) LIKE $6
|
|
|
|
|
)
|
|
|
|
|
GROUP BY COALESCE(dimval3::text, '')
|
|
|
|
|
) x
|
|
|
|
|
ORDER BY x.cnt DESC, x.dimv
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], mmitemID).Scan(&dimv)
|
|
|
|
|
if err != nil {
|
|
|
|
|
inferCache[key] = 0
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
dimv = strings.TrimSpace(dimv)
|
|
|
|
|
if dimv == "" || dimv == "0" {
|
|
|
|
|
inferCache[key] = 0
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
id, err := strconv.ParseInt(dimv, 10, 64)
|
|
|
|
|
if err != nil || id <= 0 {
|
|
|
|
|
inferCache[key] = 0
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
inferCache[key] = id
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out := make([]productSeriesMappingRow, 0, len(grouped))
|
|
|
|
|
for _, row := range grouped {
|
|
|
|
|
row.MmitemID = mmitemByCode[row.ProductCode]
|
|
|
|
|
row.Dim1ID = dim1ByToken[row.ColorCode]
|
|
|
|
|
if row.Dim3Code != "" {
|
|
|
|
|
// dimval3 can be ambiguous, but if mk_dim_token_map has a row we treat it as source of truth.
|
|
|
|
|
// Strict: only accept explicit token map rows for dimval3.
|
|
|
|
|
if v := dim3ByToken[row.Dim3Code]; v > 0 {
|
|
|
|
|
row.Dim3ID = v
|
|
|
|
|
} else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 {
|
|
|
|
|
row.Dim3ID = inferred
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Only show variants that are also known/valid on the PG side (product_code + dim1 + dim3_key).
|
|
|
|
|
// This prevents UI from listing stock variants that don't exist in PG's authoritative item-dim combos.
|
|
|
|
|
row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
|
|
|
|
|
dim3Key := int64(0)
|
|
|
|
|
if row.Dim3Code != "" {
|
|
|
|
|
dim3Key = row.Dim3ID
|
|
|
|
|
}
|
|
|
|
|
if row.MappingReady {
|
|
|
|
|
_, ok := combos[fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)]
|
|
|
|
|
row.MappingReady = ok
|
|
|
|
|
}
|
|
|
|
|
if !row.MappingReady {
|
|
|
|
|
row.MappingWarning = "PG urun veya varyant token eslesmesi bulunamadi"
|
|
|
|
|
// Skip: do not surface "orphan" variants in the UI.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID)
|
|
|
|
|
appendSeries := func(key string) {
|
|
|
|
|
@@ -383,11 +388,6 @@ LIMIT 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
appendSeries(assignKey)
|
|
|
|
|
// Fallback: if we couldn't match dim3-specific assignments (dim3 token ambiguity / missing),
|
|
|
|
|
// show the default (dim3 NULL) assignments for the same product+color.
|
|
|
|
|
if len(row.SeriesIDs) == 0 && row.Dim3Code != "" && row.Dim1ID > 0 {
|
|
|
|
|
appendSeries(assignmentKey(row.ProductCode, row.Dim1ID, 0))
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code })
|
|
|
|
|
sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] })
|
|
|
|
|
out = append(out, *row)
|
|
|
|
|
@@ -416,6 +416,239 @@ LIMIT 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetProductSeriesOrphanMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
f := readStockAttrFilters(r)
|
|
|
|
|
limit := 0
|
|
|
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
|
|
|
|
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
|
|
|
|
|
limit = n
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
search := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("code"), r.URL.Query().Get("product_code"))
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
msRows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery,
|
|
|
|
|
f.kategori,
|
|
|
|
|
f.urunAnaGrubu,
|
|
|
|
|
joinFilterValues(f.urunAltGrubu),
|
|
|
|
|
joinFilterValues(f.renk),
|
|
|
|
|
joinFilterValues(f.renk2),
|
|
|
|
|
joinFilterValues(f.urunIcerigi),
|
|
|
|
|
joinFilterValues(f.fit),
|
|
|
|
|
joinFilterValues(f.drop),
|
|
|
|
|
joinFilterValues(f.beden),
|
|
|
|
|
strconv.Itoa(limit),
|
|
|
|
|
search,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Stok seri listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer msRows.Close()
|
|
|
|
|
|
|
|
|
|
grouped := map[string]*productSeriesMappingRow{}
|
|
|
|
|
codeSet := map[string]struct{}{}
|
|
|
|
|
colorSet := map[string]struct{}{}
|
|
|
|
|
dim3Set := map[string]struct{}{}
|
|
|
|
|
sizeSet := map[string]struct{}{}
|
|
|
|
|
for msRows.Next() {
|
|
|
|
|
var raw productSeriesStockRawRow
|
|
|
|
|
if err := msRows.Scan(
|
|
|
|
|
&raw.ProductCode,
|
|
|
|
|
&raw.ProductDescription,
|
|
|
|
|
&raw.ColorCode,
|
|
|
|
|
&raw.ColorTitle,
|
|
|
|
|
&raw.Dim3Code,
|
|
|
|
|
&raw.SizeCode,
|
|
|
|
|
&raw.Qty,
|
|
|
|
|
&raw.UrunAnaGrubu,
|
|
|
|
|
&raw.UrunAltGrubu,
|
|
|
|
|
&raw.Marka,
|
|
|
|
|
&raw.DropVal,
|
|
|
|
|
&raw.Fit,
|
|
|
|
|
&raw.UrunIcerigi,
|
|
|
|
|
&raw.Kategori,
|
|
|
|
|
); err != nil {
|
|
|
|
|
http.Error(w, "Stok satiri okunamadi: "+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
raw.ProductCode = strings.TrimSpace(raw.ProductCode)
|
|
|
|
|
raw.ColorCode = strings.TrimSpace(raw.ColorCode)
|
|
|
|
|
raw.Dim3Code = strings.TrimSpace(raw.Dim3Code)
|
|
|
|
|
raw.SizeCode = strings.TrimSpace(raw.SizeCode)
|
|
|
|
|
key := productSeriesKey(raw.ProductCode, raw.ColorCode, raw.Dim3Code)
|
|
|
|
|
row := grouped[key]
|
|
|
|
|
if row == nil {
|
|
|
|
|
row = &productSeriesMappingRow{
|
|
|
|
|
RowKey: key,
|
|
|
|
|
ProductCode: raw.ProductCode,
|
|
|
|
|
ProductDescription: strings.TrimSpace(raw.ProductDescription),
|
|
|
|
|
ColorCode: raw.ColorCode,
|
|
|
|
|
ColorTitle: strings.TrimSpace(raw.ColorTitle),
|
|
|
|
|
Dim3Code: raw.Dim3Code,
|
|
|
|
|
UrunAnaGrubu: strings.TrimSpace(raw.UrunAnaGrubu),
|
|
|
|
|
UrunAltGrubu: strings.TrimSpace(raw.UrunAltGrubu),
|
|
|
|
|
Marka: strings.TrimSpace(raw.Marka),
|
|
|
|
|
DropVal: strings.TrimSpace(raw.DropVal),
|
|
|
|
|
Fit: strings.TrimSpace(raw.Fit),
|
|
|
|
|
UrunIcerigi: strings.TrimSpace(raw.UrunIcerigi),
|
|
|
|
|
Kategori: strings.TrimSpace(raw.Kategori),
|
|
|
|
|
SizeQty: map[string]float64{},
|
|
|
|
|
SeriesIDs: []int64{},
|
|
|
|
|
Series: []productSeriesDefinition{},
|
|
|
|
|
}
|
|
|
|
|
grouped[key] = row
|
|
|
|
|
codeSet[raw.ProductCode] = struct{}{}
|
|
|
|
|
colorSet[raw.ColorCode] = struct{}{}
|
|
|
|
|
if raw.Dim3Code != "" {
|
|
|
|
|
dim3Set[raw.Dim3Code] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if raw.SizeCode != "" {
|
|
|
|
|
row.SizeQty[raw.SizeCode] = raw.Qty
|
|
|
|
|
sizeSet[raw.SizeCode] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
row.TotalQty += raw.Qty
|
|
|
|
|
}
|
|
|
|
|
if err := msRows.Err(); err != nil {
|
|
|
|
|
http.Error(w, "Stok satirlari okunamadi: "+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defs, err := listProductSeriesDefinitions(ctx, pg, true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Strict token map lookups (no inference).
|
|
|
|
|
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
|
|
|
|
|
out := map[string]int64{}
|
|
|
|
|
if len(tokens) == 0 {
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
|
SELECT token, dim_id
|
|
|
|
|
FROM mk_dim_token_map
|
|
|
|
|
WHERE dim_column=$1 AND token = ANY($2)
|
|
|
|
|
`, column, pq.Array(tokens))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var tok string
|
|
|
|
|
var id int64
|
|
|
|
|
if err := rows.Scan(&tok, &id); err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
out[strings.TrimSpace(tok)] = id
|
|
|
|
|
}
|
|
|
|
|
return out, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
|
|
|
|
|
out := map[string]struct{}{}
|
|
|
|
|
if len(productCodes) == 0 {
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
rows, err := pg.QueryContext(ctx, `
|
|
|
|
|
SELECT product_code, dim1, dim3_key
|
|
|
|
|
FROM mk_mmitem_dim_combo
|
|
|
|
|
WHERE product_code = ANY($1)
|
|
|
|
|
`, pq.Array(productCodes))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var code string
|
|
|
|
|
var dim1, dim3Key int64
|
|
|
|
|
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), dim1, dim3Key)
|
|
|
|
|
out[key] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
return out, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
codes := setToSortedSlice(codeSet)
|
|
|
|
|
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
|
|
|
|
|
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
|
|
|
|
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
|
|
|
|
combos, _ := loadProductDimCombos(ctx, pg, codes)
|
|
|
|
|
|
|
|
|
|
out := make([]productSeriesMappingRow, 0, len(grouped))
|
|
|
|
|
for _, row := range grouped {
|
|
|
|
|
if row.TotalQty <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
row.MmitemID = mmitemByCode[row.ProductCode]
|
|
|
|
|
row.Dim1ID = dim1ByToken[row.ColorCode]
|
|
|
|
|
if row.Dim3Code != "" {
|
|
|
|
|
if v := dim3ByToken[row.Dim3Code]; v > 0 {
|
|
|
|
|
row.Dim3ID = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine orphan reason.
|
|
|
|
|
baseReady := row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
|
|
|
|
|
dim3Key := int64(0)
|
|
|
|
|
if row.Dim3Code != "" {
|
|
|
|
|
dim3Key = row.Dim3ID
|
|
|
|
|
}
|
|
|
|
|
comboKey := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)
|
|
|
|
|
_, comboOK := combos[comboKey]
|
|
|
|
|
|
|
|
|
|
row.MappingReady = false
|
|
|
|
|
switch {
|
|
|
|
|
case row.MmitemID <= 0:
|
|
|
|
|
row.MappingWarning = "B2B'de urun yok (mmitem)"
|
|
|
|
|
case row.Dim1ID <= 0:
|
|
|
|
|
row.MappingWarning = "B2B'de renk token eslesmesi yok (mk_dim_token_map: dimval1)"
|
|
|
|
|
case row.Dim3Code != "" && row.Dim3ID <= 0:
|
|
|
|
|
row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)"
|
|
|
|
|
case !comboOK:
|
|
|
|
|
row.MappingWarning = "B2B'de varyant kombosu yok (mk_mmitem_dim_combo)"
|
|
|
|
|
default:
|
|
|
|
|
// Not an orphan; skip.
|
|
|
|
|
if baseReady && comboOK {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
row.MappingWarning = "B2B varyant eslesmesi bulunamadi"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out = append(out, *row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
productTotals := productSeriesTotalQtyByCode(out)
|
|
|
|
|
sort.Slice(out, func(i, j int) bool {
|
|
|
|
|
totalI := productTotals[out[i].ProductCode]
|
|
|
|
|
totalJ := productTotals[out[j].ProductCode]
|
|
|
|
|
if totalI != totalJ {
|
|
|
|
|
return totalI > totalJ
|
|
|
|
|
}
|
|
|
|
|
if out[i].ProductCode != out[j].ProductCode {
|
|
|
|
|
return out[i].ProductCode < out[j].ProductCode
|
|
|
|
|
}
|
|
|
|
|
if out[i].ColorCode != out[j].ColorCode {
|
|
|
|
|
return out[i].ColorCode < out[j].ColorCode
|
|
|
|
|
}
|
|
|
|
|
return out[i].Dim3Code < out[j].Dim3Code
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
writeJSON(w, productSeriesMappingsResponse{
|
|
|
|
|
Rows: out,
|
|
|
|
|
SizeColumns: sortSizeColumns(setToSortedSlice(sizeSet)),
|
|
|
|
|
Definitions: defs,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 {
|
|
|
|
|
out := make(map[string]float64, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
|
|