Deduplicate vendor seen by partnumber and fix vendor mappings list

This commit is contained in:
Mikhail Chusavitin
2026-02-20 15:37:41 +03:00
parent 7d402b756d
commit 60563509e4
8 changed files with 396 additions and 91 deletions

View File

@@ -132,18 +132,40 @@ func (s *VendorMappingService) UpsertMapping(vendor, partnumber, lotName, descri
}
now := time.Now()
var existing models.VendorPartnumberSeen
err = tx.Where("LOWER(TRIM(partnumber)) = LOWER(TRIM(?))", partnumber).First(&existing).Error
if err == nil {
updates := map[string]any{
"last_seen_at": now,
}
if strings.TrimSpace(existing.SourceType) != "stock" {
updates["source_type"] = "manual"
}
if strings.TrimSpace(existing.Vendor) == "" && vendor != "" {
updates["vendor"] = vendor
}
if descPtr != nil {
updates["description"] = *descPtr
}
return tx.Model(&models.VendorPartnumberSeen{}).Where("id = ?", existing.ID).Updates(updates).Error
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
seen := models.VendorPartnumberSeen{
SourceType: "manual",
Vendor: vendor,
Partnumber: partnumber,
LastSeenAt: now,
Description: func() *string {
if descPtr == nil {
return nil
}
v := *descPtr
return &v
}(),
}
if descPtr != nil {
seen.Description = descPtr
}
return tx.Where("source_type = ? AND vendor = ? AND partnumber = ?", "manual", vendor, partnumber).
Assign(seen).
FirstOrCreate(&seen).Error
return tx.Create(&seen).Error
})
}
@@ -168,7 +190,9 @@ func (s *VendorMappingService) SetIgnore(vendor, partnumber, ignoredBy string, i
updates["ignored_at"] = nil
updates["ignored_by"] = nil
}
res := tx.Model(&models.VendorPartnumberSeen{}).Where("vendor = ? AND partnumber = ?", vendor, partnumber).Updates(updates)
res := tx.Model(&models.VendorPartnumberSeen{}).
Where("LOWER(TRIM(partnumber)) = LOWER(TRIM(?))", partnumber).
Updates(updates)
if res.Error != nil {
return res.Error
}
@@ -242,51 +266,71 @@ func (s *VendorMappingService) List(page, perPage int, search string, unmappedOn
offset := (page - 1) * perPage
base := s.db.Table(`(
SELECT
COALESCE(TRIM(vendor), '') AS vendor,
TRIM(partnumber) AS partnumber
SELECT TRIM(partnumber) AS partnumber
FROM lot_partnumbers
WHERE TRIM(COALESCE(partnumber, '')) <> ''
UNION
SELECT
COALESCE(TRIM(vendor), '') AS vendor,
TRIM(partnumber) AS partnumber
SELECT TRIM(partnumber) AS partnumber
FROM qt_vendor_partnumber_seen
WHERE TRIM(COALESCE(partnumber, '')) <> ''
) k`).
Select(`
k.vendor,
COALESCE(NULLIF(sa.vendor, ''), NULLIF(lp.vendor, ''), '') as vendor,
k.partnumber,
COALESCE(lp_exact.lot_name, lp_fallback.lot_name, '') as lot_name,
COALESCE(lp_exact.description, lp_fallback.description, sa.description, '') as description,
COALESCE(lp.lot_name, '') as lot_name,
COALESCE(lp.description, sa.description, '') as description,
COALESCE(sa.ignored, 0) as ignored,
COALESCE(sa.sources, '') as sources,
sa.last_seen_at as last_seen_at,
CASE WHEN COALESCE(lp_exact.lot_name, lp_fallback.lot_name, '') = '' THEN 1 ELSE 0 END as unmapped,
CASE WHEN COALESCE(lp.lot_name, '') = '' THEN 1 ELSE 0 END as unmapped,
CASE WHEN b.bundle_lot_name IS NULL THEN 'single' ELSE 'bundle' END as mapping_type,
COALESCE((SELECT COUNT(*) FROM qt_lot_bundle_items bi WHERE bi.bundle_lot_name = COALESCE(lp_exact.lot_name, lp_fallback.lot_name, '')), 0) as bundle_item_count
COALESCE((SELECT COUNT(*) FROM qt_lot_bundle_items bi WHERE bi.bundle_lot_name = COALESCE(lp.lot_name, '')), 0) as bundle_item_count
`).
Joins("LEFT JOIN lot_partnumbers lp_exact ON lp_exact.vendor = k.vendor AND lp_exact.partnumber = k.partnumber").
Joins("LEFT JOIN lot_partnumbers lp_fallback ON lp_fallback.vendor = '' AND lp_fallback.partnumber = k.partnumber AND k.vendor <> ''").
Joins(`LEFT JOIN (
SELECT
vendor,
partnumber,
SUBSTRING_INDEX(
GROUP_CONCAT(COALESCE(TRIM(vendor), '') ORDER BY (TRIM(COALESCE(vendor, '')) <> '') DESC, vendor ASC SEPARATOR '\n'),
'\n',
1
) AS vendor,
SUBSTRING_INDEX(
GROUP_CONCAT(COALESCE(TRIM(lot_name), '') ORDER BY (TRIM(COALESCE(vendor, '')) <> '') DESC, vendor ASC SEPARATOR '\n'),
'\n',
1
) AS lot_name,
SUBSTRING_INDEX(
GROUP_CONCAT(COALESCE(TRIM(description), '') ORDER BY (TRIM(COALESCE(vendor, '')) <> '') DESC, vendor ASC SEPARATOR '\n'),
'\n',
1
) AS description
FROM lot_partnumbers
WHERE TRIM(COALESCE(partnumber, '')) <> ''
GROUP BY partnumber
) lp ON lp.partnumber = k.partnumber`).
Joins(`LEFT JOIN (
SELECT
partnumber,
SUBSTRING_INDEX(
GROUP_CONCAT(COALESCE(TRIM(vendor), '') ORDER BY (TRIM(COALESCE(vendor, '')) <> '') DESC, (source_type = 'stock') DESC, last_seen_at DESC, id DESC SEPARATOR '\n'),
'\n',
1
) AS vendor,
MAX(is_ignored) AS ignored,
GROUP_CONCAT(DISTINCT source_type ORDER BY source_type SEPARATOR ',') AS sources,
MAX(last_seen_at) AS last_seen_at,
MAX(description) AS description
FROM qt_vendor_partnumber_seen
GROUP BY vendor, partnumber
) sa ON sa.vendor = k.vendor AND sa.partnumber = k.partnumber`).
Joins("LEFT JOIN qt_lot_bundles b ON b.bundle_lot_name = COALESCE(lp_exact.lot_name, lp_fallback.lot_name)")
GROUP BY partnumber
) sa ON sa.partnumber = k.partnumber`).
Joins("LEFT JOIN qt_lot_bundles b ON b.bundle_lot_name = COALESCE(lp.lot_name, '')")
if q := strings.TrimSpace(search); q != "" {
like := "%" + q + "%"
base = base.Where("k.vendor LIKE ? OR k.partnumber LIKE ? OR COALESCE(lp_exact.lot_name, lp_fallback.lot_name, '') LIKE ? OR COALESCE(lp_exact.description, lp_fallback.description, sa.description, '') LIKE ?", like, like, like, like)
base = base.Where("COALESCE(NULLIF(sa.vendor, ''), NULLIF(lp.vendor, ''), '') LIKE ? OR k.partnumber LIKE ? OR COALESCE(lp.lot_name, '') LIKE ? OR COALESCE(lp.description, sa.description, '') LIKE ?", like, like, like, like)
}
if unmappedOnly {
base = base.Where("COALESCE(lp_exact.lot_name, lp_fallback.lot_name, '') = ''").
base = base.Where("COALESCE(lp.lot_name, '') = ''").
Where("COALESCE(sa.ignored, 0) = 0")
}
if ignoredOnly {
@@ -317,7 +361,7 @@ func (s *VendorMappingService) List(page, perPage int, search string, unmappedOn
}
var rows []row
if err := base.Order("k.partnumber ASC").Order("k.vendor ASC").Offset(offset).Limit(perPage).Scan(&rows).Error; err != nil {
if err := base.Order("k.partnumber ASC").Offset(offset).Limit(perPage).Scan(&rows).Error; err != nil {
return nil, 0, err
}
items := make([]VendorMappingListRow, 0, len(rows))
@@ -375,15 +419,9 @@ func (s *VendorMappingService) GetDetail(vendor, partnumber string) (*VendorMapp
}
var seenRows []models.VendorPartnumberSeen
if err := s.db.Where("vendor = ? AND partnumber = ?", vendor, partnumber).Find(&seenRows).Error; err != nil {
if err := s.db.Where("LOWER(TRIM(partnumber)) = LOWER(TRIM(?))", partnumber).Find(&seenRows).Error; err != nil {
return nil, err
}
if vendor != "" {
var fallbackSeen []models.VendorPartnumberSeen
if err := s.db.Where("vendor = '' AND partnumber = ?", partnumber).Find(&fallbackSeen).Error; err == nil {
seenRows = append(seenRows, fallbackSeen...)
}
}
ignored := false
sourceSet := make(map[string]struct{})
desc := ""