feat: world-прайслист как заглушка для отсутствующих цен

Там, где для LOT нет цены в estimate/warehouse/competitor, подставляется
цена из world-прайслиста. Такие ячейки в таблицах «Цена покупки»/«Цена продажи»
подсвечиваются (amber), участвуют в «Итого» и убирают красную «*».
В CSV-экспорте добавлена колонка «Заглушка (world)» с перечнем столбцов,
где сработал фолбэк.

Добавлены Tx-версии GetLatestLocalPricelistBySource/GetLocalPricesForLots,
чтобы резолв прайслистов внутри транзакции не дедлочил single-connection пул.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-10 10:25:46 +03:00
co-authored by Claude Opus 4.8
parent 498cbf5490
commit 70a3ff255f
10 changed files with 586 additions and 52 deletions
+18 -2
View File
@@ -1351,8 +1351,17 @@ func (l *LocalDB) GetLatestLocalPricelist() (*LocalPricelist, error) {
// GetLatestLocalPricelistBySource returns the most recently synced active pricelist for a source.
func (l *LocalDB) GetLatestLocalPricelistBySource(source string) (*LocalPricelist, error) {
return GetLatestLocalPricelistBySourceTx(l.db, source)
}
// GetLatestLocalPricelistBySourceTx is the transaction-scoped variant of
// GetLatestLocalPricelistBySource. Callers already inside an l.db.Transaction(...) callback
// must use this with the given tx instead of the method above — the connection pool has a
// single connection (see New()), so querying via l.db while the transaction holds that
// connection deadlocks forever.
func GetLatestLocalPricelistBySourceTx(tx *gorm.DB, source string) (*LocalPricelist, error) {
var pricelist LocalPricelist
if err := l.db.
if err := tx.
Where("source = ? AND is_active = ?", source, true).
Where("EXISTS (SELECT 1 FROM local_pricelist_items WHERE local_pricelist_items.pricelist_id = local_pricelists.id)").
Order("created_at DESC, id DESC").
@@ -1545,6 +1554,13 @@ func (l *LocalDB) GetLocalPriceForLot(pricelistID uint, lotName string) (float64
// legacy rows that were stored in mixed case before normalization was enforced at sync time.
// Keys in the returned map are uppercased (matching the input lotNames).
func (l *LocalDB) GetLocalPricesForLots(pricelistID uint, lotNames []string) (map[string]float64, error) {
return GetLocalPricesForLotsTx(l.db, pricelistID, lotNames)
}
// GetLocalPricesForLotsTx is the transaction-scoped variant of GetLocalPricesForLots. See
// GetLatestLocalPricelistBySourceTx for why callers inside an l.db.Transaction(...) callback
// must use this with the given tx instead of the method above.
func GetLocalPricesForLotsTx(tx *gorm.DB, pricelistID uint, lotNames []string) (map[string]float64, error) {
result := make(map[string]float64, len(lotNames))
if len(lotNames) == 0 {
return result, nil
@@ -1556,7 +1572,7 @@ func (l *LocalDB) GetLocalPricesForLots(pricelistID uint, lotNames []string) (ma
}
var rows []row
// Use UPPER(lot_name) so rows synced before normalization (mixed-case) are still matched.
if err := l.db.Model(&LocalPricelistItem{}).
if err := tx.Model(&LocalPricelistItem{}).
Select("lot_name, price").
Where("pricelist_id = ? AND UPPER(lot_name) IN ?", pricelistID, lotNames).
Find(&rows).Error; err != nil {