Add partnumber book snapshots for QuoteForge integration

- Migrations 026-028: qt_partnumber_books + qt_partnumber_book_items
  tables; is_primary_pn on lot_partnumbers; version VARCHAR(30);
  description VARCHAR(10000) on items (required by QuoteForge sync)
- Service: CreateSnapshot expands bundles, filters empty lot_name and
  ignored PNs, copies description, activates new book atomically,
  applies GFS retention (7d/5w/12m/10y) with explicit item deletion
- Task type TaskTypePartnumberBookCreate; handlers ListPartnumberBooks
  and CreatePartnumberBook; routes GET/POST /api/admin/pricing/partnumber-books
- UI: snapshot list + "Создать снапшот сопоставлений" button with
  progress polling on /vendor-mappings page
- Bible: history, api, background-tasks, vendor-mapping updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 22:16:16 +03:00
parent 225e1beda9
commit a4457a0a28
13 changed files with 751 additions and 32 deletions

View File

@@ -57,16 +57,46 @@ func (StockLog) TableName() string {
// LotPartnumber maps external part numbers to internal lots.
type LotPartnumber struct {
Vendor string `gorm:"column:vendor;size:255;primaryKey" json:"vendor"`
Partnumber string `gorm:"column:partnumber;size:255;primaryKey" json:"partnumber"`
LotName string `gorm:"column:lot_name;size:255" json:"lot_name"`
Description *string `gorm:"column:description;size:10000" json:"description,omitempty"`
Vendor string `gorm:"column:vendor;size:255;primaryKey" json:"vendor"`
Partnumber string `gorm:"column:partnumber;size:255;primaryKey" json:"partnumber"`
LotName string `gorm:"column:lot_name;size:255" json:"lot_name"`
Description *string `gorm:"column:description;size:10000" json:"description,omitempty"`
IsPrimaryPN bool `gorm:"column:is_primary_pn;not null;default:true" json:"is_primary_pn"`
}
func (LotPartnumber) TableName() string {
return "lot_partnumbers"
}
// PartnumberBook is a versioned snapshot of the partnumber→LOT mapping.
// Written by PriceForge; QuoteForge reads via SELECT only.
type PartnumberBook struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Version string `gorm:"column:version;size:30;not null;uniqueIndex:uq_qt_partnumber_books_version" json:"version"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
CreatedBy string `gorm:"column:created_by;size:100;not null;default:''" json:"created_by"`
IsActive bool `gorm:"column:is_active;not null;default:false" json:"is_active"`
}
func (PartnumberBook) TableName() string {
return "qt_partnumber_books"
}
// PartnumberBookItem is one mapping row in a PartnumberBook snapshot.
// Bundles are expanded: a single partnumber may have multiple rows (one per LOT component).
type PartnumberBookItem struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
BookID uint64 `gorm:"column:book_id;not null" json:"book_id"`
Partnumber string `gorm:"column:partnumber;size:255;not null" json:"partnumber"`
LotName string `gorm:"column:lot_name;size:255;not null" json:"lot_name"`
IsPrimaryPN bool `gorm:"column:is_primary_pn;not null;default:true" json:"is_primary_pn"`
Description *string `gorm:"column:description;size:10000" json:"description,omitempty"`
}
func (PartnumberBookItem) TableName() string {
return "qt_partnumber_book_items"
}
type LotBundle struct {
BundleLotName string `gorm:"column:bundle_lot_name;size:255;primaryKey" json:"bundle_lot_name"`
IsActive bool `gorm:"column:is_active;default:true" json:"is_active"`