feat: implement vendor spec BOM import and PN→LOT resolution (Phase 1)
- Migration 029: local_partnumber_books, local_partnumber_book_items, vendor_spec TEXT column on local_configurations - Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec, VendorSpecItem with JSON Valuer/Scanner - Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber, SaveBook/Items, ListBooks, CountBookItems) - Service: VendorSpecResolver 3-step resolution (book → manual suggestion → unresolved) + AggregateLOTs with is_primary_pn qty logic - Sync: PullPartnumberBooks append-only pull from qt_partnumber_books - Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler - Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books, /api/sync/partnumber-books, /partnumber-books page - UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste, PN resolution, inline LOT autocomplete, pricing table - Bible: 03-database.md updated, 09-vendor-spec.md added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
66
internal/repository/partnumber_book.go
Normal file
66
internal/repository/partnumber_book.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PartnumberBookRepository provides read-only access to local partnumber book snapshots.
|
||||
type PartnumberBookRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPartnumberBookRepository(db *gorm.DB) *PartnumberBookRepository {
|
||||
return &PartnumberBookRepository{db: db}
|
||||
}
|
||||
|
||||
// GetActiveBook returns the most recently active local partnumber book.
|
||||
func (r *PartnumberBookRepository) GetActiveBook() (*localdb.LocalPartnumberBook, error) {
|
||||
var book localdb.LocalPartnumberBook
|
||||
err := r.db.Where("is_active = 1").Order("created_at DESC, id DESC").First(&book).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &book, nil
|
||||
}
|
||||
|
||||
// GetBookItems returns all items for the given local book ID.
|
||||
func (r *PartnumberBookRepository) GetBookItems(bookID uint) ([]localdb.LocalPartnumberBookItem, error) {
|
||||
var items []localdb.LocalPartnumberBookItem
|
||||
err := r.db.Where("book_id = ?", bookID).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// FindLotByPartnumber looks up a partnumber in the active book and returns the matching items.
|
||||
func (r *PartnumberBookRepository) FindLotByPartnumber(bookID uint, partnumber string) ([]localdb.LocalPartnumberBookItem, error) {
|
||||
var items []localdb.LocalPartnumberBookItem
|
||||
err := r.db.Where("book_id = ? AND partnumber = ?", bookID, partnumber).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// ListBooks returns all local partnumber books ordered newest first.
|
||||
func (r *PartnumberBookRepository) ListBooks() ([]localdb.LocalPartnumberBook, error) {
|
||||
var books []localdb.LocalPartnumberBook
|
||||
err := r.db.Order("created_at DESC, id DESC").Find(&books).Error
|
||||
return books, err
|
||||
}
|
||||
|
||||
// SaveBook saves a new partnumber book snapshot.
|
||||
func (r *PartnumberBookRepository) SaveBook(book *localdb.LocalPartnumberBook) error {
|
||||
return r.db.Save(book).Error
|
||||
}
|
||||
|
||||
// SaveBookItems bulk-inserts items for a book snapshot.
|
||||
func (r *PartnumberBookRepository) SaveBookItems(items []localdb.LocalPartnumberBookItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.CreateInBatches(items, 500).Error
|
||||
}
|
||||
|
||||
// CountBookItems returns the number of items for a given local book ID.
|
||||
func (r *PartnumberBookRepository) CountBookItems(bookID uint) int64 {
|
||||
var count int64
|
||||
r.db.Model(&localdb.LocalPartnumberBookItem{}).Where("book_id = ?", bookID).Count(&count)
|
||||
return count
|
||||
}
|
||||
Reference in New Issue
Block a user