WIP: save current pricing and pricelist changes

This commit is contained in:
Mikhail Chusavitin
2026-02-06 19:07:22 +03:00
parent b27152b353
commit 65871a8b04
18 changed files with 1263 additions and 182 deletions

View File

@@ -30,6 +30,11 @@ type CreateProgress struct {
LotName string
}
type CreateItemInput struct {
LotName string
Price float64
}
func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository, pricingSvc *pricing.Service) *Service {
return &Service{
repo: repo,
@@ -41,14 +46,25 @@ func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo
// CreateFromCurrentPrices creates a new pricelist by taking a snapshot of current prices
func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist, error) {
return s.CreateFromCurrentPricesWithProgress(createdBy, nil)
return s.CreateFromCurrentPricesForSource(createdBy, string(models.PricelistSourceEstimate))
}
// CreateFromCurrentPricesForSource creates a new pricelist snapshot for one source.
func (s *Service) CreateFromCurrentPricesForSource(createdBy, source string) (*models.Pricelist, error) {
return s.CreateForSourceWithProgress(createdBy, source, nil, nil)
}
// CreateFromCurrentPricesWithProgress creates a pricelist and reports coarse-grained progress.
func (s *Service) CreateFromCurrentPricesWithProgress(createdBy string, onProgress func(CreateProgress)) (*models.Pricelist, error) {
func (s *Service) CreateFromCurrentPricesWithProgress(createdBy, source string, onProgress func(CreateProgress)) (*models.Pricelist, error) {
return s.CreateForSourceWithProgress(createdBy, source, nil, onProgress)
}
// CreateForSourceWithProgress creates a source pricelist from current estimate snapshot or explicit item list.
func (s *Service) CreateForSourceWithProgress(createdBy, source string, sourceItems []CreateItemInput, onProgress func(CreateProgress)) (*models.Pricelist, error) {
if s.repo == nil || s.db == nil {
return nil, fmt.Errorf("offline mode: cannot create pricelists")
}
source = string(models.NormalizePricelistSource(source))
report := func(p CreateProgress) {
if onProgress != nil {
@@ -58,7 +74,7 @@ func (s *Service) CreateFromCurrentPricesWithProgress(createdBy string, onProgre
report(CreateProgress{Current: 0, Total: 100, Status: "starting", Message: "Подготовка"})
updated, errs := 0, 0
if s.pricingSvc != nil {
if source == string(models.PricelistSourceEstimate) && s.pricingSvc != nil {
report(CreateProgress{Current: 1, Total: 100, Status: "recalculating", Message: "Обновление цен компонентов"})
updated, errs = s.pricingSvc.RecalculateAllPricesWithProgress(func(p pricing.RecalculateProgress) {
if p.Total <= 0 {
@@ -86,12 +102,13 @@ func (s *Service) CreateFromCurrentPricesWithProgress(createdBy string, onProgre
const maxCreateAttempts = 5
var pricelist *models.Pricelist
for attempt := 1; attempt <= maxCreateAttempts; attempt++ {
version, err := s.repo.GenerateVersion()
version, err := s.repo.GenerateVersionBySource(source)
if err != nil {
return nil, fmt.Errorf("generating version: %w", err)
}
pricelist = &models.Pricelist{
Source: source,
Version: version,
CreatedBy: createdBy,
IsActive: true,
@@ -113,28 +130,43 @@ func (s *Service) CreateFromCurrentPricesWithProgress(createdBy string, onProgre
break
}
// Get all components with prices from qt_lot_metadata
var metadata []models.LotMetadata
if err := s.db.Where("current_price IS NOT NULL AND current_price > 0").Find(&metadata).Error; err != nil {
return nil, fmt.Errorf("getting lot metadata: %w", err)
}
// Create pricelist items with all price settings
items := make([]models.PricelistItem, 0, len(metadata))
for _, m := range metadata {
if m.CurrentPrice == nil || *m.CurrentPrice <= 0 {
continue
items := make([]models.PricelistItem, 0)
if len(sourceItems) > 0 {
items = make([]models.PricelistItem, 0, len(sourceItems))
for _, srcItem := range sourceItems {
if strings.TrimSpace(srcItem.LotName) == "" || srcItem.Price <= 0 {
continue
}
items = append(items, models.PricelistItem{
PricelistID: pricelist.ID,
LotName: strings.TrimSpace(srcItem.LotName),
Price: srcItem.Price,
})
}
} else {
// Default snapshot source for estimate and backward compatibility.
var metadata []models.LotMetadata
if err := s.db.Where("current_price IS NOT NULL AND current_price > 0").Find(&metadata).Error; err != nil {
return nil, fmt.Errorf("getting lot metadata: %w", err)
}
// Create pricelist items with all price settings
items = make([]models.PricelistItem, 0, len(metadata))
for _, m := range metadata {
if m.CurrentPrice == nil || *m.CurrentPrice <= 0 {
continue
}
items = append(items, models.PricelistItem{
PricelistID: pricelist.ID,
LotName: m.LotName,
Price: *m.CurrentPrice,
PriceMethod: string(m.PriceMethod),
PricePeriodDays: m.PricePeriodDays,
PriceCoefficient: m.PriceCoefficient,
ManualPrice: m.ManualPrice,
MetaPrices: m.MetaPrices,
})
}
items = append(items, models.PricelistItem{
PricelistID: pricelist.ID,
LotName: m.LotName,
Price: *m.CurrentPrice,
PriceMethod: string(m.PriceMethod),
PricePeriodDays: m.PricePeriodDays,
PriceCoefficient: m.PriceCoefficient,
ManualPrice: m.ManualPrice,
MetaPrices: m.MetaPrices,
})
}
if err := s.repo.CreateItems(items); err != nil {
@@ -161,11 +193,17 @@ func isVersionConflictError(err error) bool {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "duplicate entry") && strings.Contains(msg, "idx_qt_pricelists_version")
return strings.Contains(msg, "duplicate entry") &&
(strings.Contains(msg, "idx_qt_pricelists_source_version") || strings.Contains(msg, "idx_qt_pricelists_version"))
}
// List returns pricelists with pagination
func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, error) {
return s.ListBySource(page, perPage, "")
}
// ListBySource returns pricelists with optional source filter.
func (s *Service) ListBySource(page, perPage int, source string) ([]models.PricelistSummary, int64, error) {
// If no database connection (offline mode), return empty list
if s.repo == nil {
return []models.PricelistSummary{}, 0, nil
@@ -178,11 +216,16 @@ func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, err
perPage = 20
}
offset := (page - 1) * perPage
return s.repo.List(offset, perPage)
return s.repo.ListBySource(source, offset, perPage)
}
// ListActive returns active pricelists with pagination.
func (s *Service) ListActive(page, perPage int) ([]models.PricelistSummary, int64, error) {
return s.ListActiveBySource(page, perPage, "")
}
// ListActiveBySource returns active pricelists with optional source filter.
func (s *Service) ListActiveBySource(page, perPage int, source string) ([]models.PricelistSummary, int64, error) {
if s.repo == nil {
return []models.PricelistSummary{}, 0, nil
}
@@ -193,7 +236,7 @@ func (s *Service) ListActive(page, perPage int) ([]models.PricelistSummary, int6
perPage = 20
}
offset := (page - 1) * perPage
return s.repo.ListActive(offset, perPage)
return s.repo.ListActiveBySource(source, offset, perPage)
}
// GetByID returns a pricelist by ID
@@ -261,10 +304,15 @@ func (s *Service) CanWriteDebug() (bool, string) {
// GetLatestActive returns the most recent active pricelist
func (s *Service) GetLatestActive() (*models.Pricelist, error) {
return s.GetLatestActiveBySource(string(models.PricelistSourceEstimate))
}
// GetLatestActiveBySource returns the latest active pricelist for a source.
func (s *Service) GetLatestActiveBySource(source string) (*models.Pricelist, error) {
if s.repo == nil {
return nil, fmt.Errorf("offline mode: pricelist service not available")
}
return s.repo.GetLatestActive()
return s.repo.GetLatestActiveBySource(source)
}
// CleanupExpired deletes expired and unused pricelists