diff --git a/bible-local/03-database.md b/bible-local/03-database.md index bc20d33..dd3bbc4 100644 --- a/bible-local/03-database.md +++ b/bible-local/03-database.md @@ -259,12 +259,9 @@ PK: lot_name | lot_category | varchar(50) | | | lot_description | varchar(10000) | added by migration 032; backfilled once from `lot.lot_description`. QF syncs this column directly — do not join `lot` per-row to get a description | | price | decimal(12,2) NOT NULL | | -| price_method | varchar(20) | synced to local and shown in the pricelist detail "Настройки" column | -| price_period_days | bigint DEFAULT 90 | synced to local, same as above | -| price_coefficient | decimal(5,2) DEFAULT 0 | synced to local, same as above | -| manual_price | decimal(12,2) | synced to local, same as above | -| meta_prices | varchar(1000) | synced to local, same as above | -| lead_time_weeks | int | exists in the real table; not read by QF — no corresponding field in `models.PricelistItem` | +| price_quality | tinyint unsigned, nullable | added by migration 033. Set by the external pricelist-building tool, 0-9, based on quote recency/count per its own per-lot pricing-period settings — QF only reads and displays it, never computes it. UI: 7-9 = no warning, 4-6 = amber "!", 0-3 = red "!" (`formatPriceQualityWarning` in `pricelist_detail.html`) | + +The real table also has `price_method`, `price_period_days`, `price_coefficient`, `manual_price`, `meta_prices` and `lead_time_weeks` columns, owned and written by the external pricing engine that maintains `qt_pricelist_items` — **keep them in the table for backward compatibility with that system; QF must never drop, rename, or write to them.** QF itself does not model, sync, or display any of them (tried once, removed as unused/dead in the client). Do not re-add them to `models.PricelistItem`/`LocalPricelistItem` without a concrete UI need. Fields QF never populates and does not model: `AvailableQty`/`Partnumbers` (removed from `models.PricelistItem` and `LocalPricelistItem` — no writer ever set them; always empty). diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index ad4548e..db244c8 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -180,16 +180,12 @@ func (h *PricelistHandler) GetItems(c *gin.Context) { resultItems := make([]gin.H, 0, len(items)) for _, item := range items { resultItems = append(resultItems, gin.H{ - "id": item.ID, - "lot_name": item.LotName, - "lot_description": item.LotDescription, - "price": item.Price, - "category": item.LotCategory, - "price_method": item.PriceMethod, - "price_period_days": item.PricePeriodDays, - "price_coefficient": item.PriceCoefficient, - "manual_price": item.ManualPrice, - "meta_prices": item.MetaPrices, + "id": item.ID, + "lot_name": item.LotName, + "lot_description": item.LotDescription, + "price": item.Price, + "category": item.LotCategory, + "price_quality": item.PriceQuality, }) } diff --git a/internal/localdb/converters.go b/internal/localdb/converters.go index 238b77e..71e80be 100644 --- a/internal/localdb/converters.go +++ b/internal/localdb/converters.go @@ -332,32 +332,24 @@ func LocalToPricelist(local *LocalPricelist) *models.Pricelist { // PricelistItemToLocal converts models.PricelistItem to LocalPricelistItem func PricelistItemToLocal(item *models.PricelistItem, localPricelistID uint) *LocalPricelistItem { return &LocalPricelistItem{ - PricelistID: localPricelistID, - LotName: models.NormalizeLotName(item.LotName), - LotCategory: item.LotCategory, - LotDescription: item.LotDescription, - Price: item.Price, - PriceMethod: item.PriceMethod, - PricePeriodDays: item.PricePeriodDays, - PriceCoefficient: item.PriceCoefficient, - ManualPrice: item.ManualPrice, - MetaPrices: item.MetaPrices, + PricelistID: localPricelistID, + LotName: models.NormalizeLotName(item.LotName), + LotCategory: item.LotCategory, + LotDescription: item.LotDescription, + Price: item.Price, + PriceQuality: item.PriceQuality, } } // LocalToPricelistItem converts LocalPricelistItem to models.PricelistItem func LocalToPricelistItem(local *LocalPricelistItem, serverPricelistID uint) *models.PricelistItem { return &models.PricelistItem{ - ID: local.ID, - PricelistID: serverPricelistID, - LotName: local.LotName, - LotCategory: local.LotCategory, - LotDescription: local.LotDescription, - Price: local.Price, - PriceMethod: local.PriceMethod, - PricePeriodDays: local.PricePeriodDays, - PriceCoefficient: local.PriceCoefficient, - ManualPrice: local.ManualPrice, - MetaPrices: local.MetaPrices, + ID: local.ID, + PricelistID: serverPricelistID, + LotName: local.LotName, + LotCategory: local.LotCategory, + LotDescription: local.LotDescription, + Price: local.Price, + PriceQuality: local.PriceQuality, } } diff --git a/internal/localdb/migrations.go b/internal/localdb/migrations.go index 3773079..31ab948 100644 --- a/internal/localdb/migrations.go +++ b/internal/localdb/migrations.go @@ -130,13 +130,18 @@ var localMigrations = []localMigration{ run: addLocalProjectRentalEnabled, }, { - id: "2026_07_22_pricelist_items_price_settings", - name: "Add lot_description and price settings columns to local_pricelist_items, drop unused available_qty/partnumbers", - run: addPricelistItemDescriptionAndPriceSettings, + id: "2026_07_22_pricelist_items_add_description_drop_unused", + name: "Add lot_description to local_pricelist_items, drop unused available_qty/partnumbers", + run: addPricelistItemDescriptionDropUnused, + }, + { + id: "2026_07_22_pricelist_items_price_quality", + name: "Add price_quality to local_pricelist_items", + run: addLocalPricelistItemPriceQuality, }, } -func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { +func addLocalPricelistItemPriceQuality(tx *gorm.DB) error { type columnInfo struct { Name string `gorm:"column:name"` } @@ -144,7 +149,33 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { var columns []columnInfo if err := tx.Raw(` SELECT name FROM pragma_table_info('local_pricelist_items') - WHERE name IN ('lot_description', 'price_method', 'price_period_days', 'price_coefficient', 'manual_price', 'meta_prices', 'available_qty', 'partnumbers') + WHERE name = 'price_quality' + `).Scan(&columns).Error; err != nil { + return fmt.Errorf("check local_pricelist_items(price_quality) existence: %w", err) + } + + if len(columns) > 0 { + slog.Info("price_quality already present on local_pricelist_items") + return nil + } + + if err := tx.Exec(`ALTER TABLE local_pricelist_items ADD COLUMN price_quality INTEGER`).Error; err != nil { + return fmt.Errorf("add local_pricelist_items.price_quality: %w", err) + } + + slog.Info("added price_quality to local_pricelist_items") + return nil +} + +func addPricelistItemDescriptionDropUnused(tx *gorm.DB) error { + type columnInfo struct { + Name string `gorm:"column:name"` + } + + var columns []columnInfo + if err := tx.Raw(` + SELECT name FROM pragma_table_info('local_pricelist_items') + WHERE name IN ('lot_description', 'available_qty', 'partnumbers') `).Scan(&columns).Error; err != nil { return fmt.Errorf("check local_pricelist_items columns: %w", err) } @@ -154,16 +185,13 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { have[c.Name] = true } - if have["lot_description"] && have["price_method"] && have["price_period_days"] && - have["price_coefficient"] && have["manual_price"] && have["meta_prices"] && - !have["available_qty"] && !have["partnumbers"] { - slog.Info("local_pricelist_items already migrated to description/price-settings schema") + if have["lot_description"] && !have["available_qty"] && !have["partnumbers"] { + slog.Info("local_pricelist_items already migrated to description-only schema") return nil } // SQLite: recreate table without the never-populated available_qty/partnumbers - // columns, adding lot_description and the price settings columns synced from - // qt_pricelist_items (needed to render the pricelist detail UI correctly). + // columns, adding lot_description (synced from qt_pricelist_items.lot_description). if err := tx.Exec(` CREATE TABLE local_pricelist_items_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -171,12 +199,7 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { lot_name TEXT NOT NULL, lot_category TEXT, lot_description TEXT, - price REAL NOT NULL, - price_method TEXT, - price_period_days INTEGER DEFAULT 90, - price_coefficient REAL DEFAULT 0, - manual_price REAL, - meta_prices TEXT + price REAL NOT NULL ) `).Error; err != nil { return fmt.Errorf("create new local_pricelist_items table: %w", err) @@ -198,13 +221,6 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { return fmt.Errorf("rename new local_pricelist_items table: %w", err) } - if err := tx.Exec(` - CREATE INDEX IF NOT EXISTS idx_local_pricelist_items_pricelist_lot - ON local_pricelist_items(pricelist_id, lot_name) - `).Error; err != nil { - return fmt.Errorf("recreate idx_local_pricelist_items_pricelist_lot: %w", err) - } - if err := tx.Exec(` CREATE UNIQUE INDEX IF NOT EXISTS idx_local_pricelist_items_pricelist_lot_unique ON local_pricelist_items(pricelist_id, lot_name) @@ -219,7 +235,7 @@ func addPricelistItemDescriptionAndPriceSettings(tx *gorm.DB) error { return fmt.Errorf("recreate idx_local_pricelist_items_lot_category: %w", err) } - slog.Info("added lot_description/price settings columns to local_pricelist_items and dropped available_qty/partnumbers") + slog.Info("added lot_description to local_pricelist_items and dropped available_qty/partnumbers") return nil } @@ -1227,4 +1243,3 @@ func deduplicatePricelistItemsAndAddUniqueIndex(tx *gorm.DB) error { slog.Info("deduplicated local_pricelist_items and added unique index") return nil } - diff --git a/internal/localdb/models.go b/internal/localdb/models.go index bf82519..3cbfcdd 100644 --- a/internal/localdb/models.go +++ b/internal/localdb/models.go @@ -197,13 +197,9 @@ type LocalPricelistItem struct { LotDescription string `gorm:"column:lot_description" json:"lot_description,omitempty"` Price float64 `gorm:"not null" json:"price"` - // Price calculation settings, mirrored from qt_pricelist_items for display - // in the pricelist detail UI (formatPriceSettings in pricelist_detail.html). - PriceMethod string `gorm:"column:price_method" json:"price_method,omitempty"` - PricePeriodDays int `gorm:"column:price_period_days;default:90" json:"price_period_days"` - PriceCoefficient float64 `gorm:"column:price_coefficient;default:0" json:"price_coefficient"` - ManualPrice *float64 `gorm:"column:manual_price" json:"manual_price,omitempty"` - MetaPrices string `gorm:"column:meta_prices" json:"meta_prices,omitempty"` + // PriceQuality mirrors qt_pricelist_items.price_quality: a 0-9 score set by + // the external pricing tool. 7-9 good, 4-6 warning, 0-3 poor. + PriceQuality *int `gorm:"column:price_quality" json:"price_quality,omitempty"` } func (LocalPricelistItem) TableName() string { diff --git a/internal/models/pricelist.go b/internal/models/pricelist.go index cd4c63d..a5b759e 100644 --- a/internal/models/pricelist.go +++ b/internal/models/pricelist.go @@ -61,14 +61,11 @@ type PricelistItem struct { LotCategory string `gorm:"column:lot_category;size:50" json:"lot_category,omitempty"` LotDescription string `gorm:"column:lot_description;size:10000" json:"lot_description,omitempty"` Price float64 `gorm:"type:decimal(12,2);not null" json:"price"` - PriceMethod string `gorm:"size:20" json:"price_method,omitempty"` - // Price calculation settings (snapshot from qt_lot_metadata), shown in the - // pricelist detail UI as a compact "settings" summary next to each item. - PricePeriodDays int `gorm:"default:90" json:"price_period_days"` - PriceCoefficient float64 `gorm:"type:decimal(5,2);default:0" json:"price_coefficient"` - ManualPrice *float64 `gorm:"type:decimal(12,2)" json:"manual_price,omitempty"` - MetaPrices string `gorm:"size:1000" json:"meta_prices,omitempty"` + // PriceQuality is a 0-9 score set by the external pricing tool that builds + // the pricelist (based on quote recency/count per its own per-lot settings). + // 7-9 = good, 4-6 = warning, 0-3 = poor. QF only displays it, never computes it. + PriceQuality *int `gorm:"column:price_quality" json:"price_quality,omitempty"` } func (PricelistItem) TableName() string { diff --git a/migrations/033_add_price_quality_to_pricelist_items.sql b/migrations/033_add_price_quality_to_pricelist_items.sql new file mode 100644 index 0000000..1abf690 --- /dev/null +++ b/migrations/033_add_price_quality_to_pricelist_items.sql @@ -0,0 +1,12 @@ +-- Tables affected: qt_pricelist_items +-- recovery.not-started: safe to re-run; ADD COLUMN IF NOT EXISTS +-- recovery.partial: ALTER TABLE qt_pricelist_items DROP COLUMN price_quality; +-- recovery.completed: no action needed +-- verify: price_quality column missing | SELECT 1 FROM information_schema.COLUMNS WHERE table_schema=DATABASE() AND table_name='qt_pricelist_items' AND column_name='price_quality' HAVING COUNT(*)=0 + +-- price_quality is a 0-9 score set by the external pricelist-building tool, +-- based on quote recency/count per its own per-lot pricing-period settings. +-- QuoteForge only reads and displays it (7-9 good, 4-6 warning, 0-3 poor); +-- it never computes or writes this value. +ALTER TABLE qt_pricelist_items + ADD COLUMN IF NOT EXISTS price_quality TINYINT UNSIGNED NULL; diff --git a/web/templates/pricelist_detail.html b/web/templates/pricelist_detail.html index 82eb6e8..5972da3 100644 --- a/web/templates/pricelist_detail.html +++ b/web/templates/pricelist_detail.html @@ -59,7 +59,6 @@