From 3c46cd7bf0757d55b407b2500a534801f193a6b3 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Fri, 20 Feb 2026 19:15:24 +0300 Subject: [PATCH] Fix auto pricelist resolution and latest-price selection; update Bible --- bible/02-architecture.md | 20 ++++ bible/03-database.md | 1 + bible/04-api.md | 6 +- bible/07-dev.md | 3 +- internal/handlers/pricelist.go | 36 +++++- internal/handlers/pricelist_test.go | 77 +++++++++++++ internal/localdb/localdb.go | 12 +- internal/localdb/pricelist_latest_test.go | 128 ++++++++++++++++++++++ internal/repository/pricelist.go | 10 +- internal/repository/pricelist_test.go | 95 ++++++++++++++++ web/templates/index.html | 59 +++++++--- 11 files changed, 419 insertions(+), 28 deletions(-) create mode 100644 internal/localdb/pricelist_latest_test.go diff --git a/bible/02-architecture.md b/bible/02-architecture.md index 33a29e3..b5d6078 100644 --- a/bible/02-architecture.md +++ b/bible/02-architecture.md @@ -118,6 +118,26 @@ A configuration can reference up to three pricelists simultaneously: Pricelist sources: `estimate` | `warehouse` | `competitor` +### "Auto" Pricelist Selection + +Configurator supports explicit and automatic selection per source (`estimate`, `warehouse`, `competitor`): + +- **Explicit mode:** concrete `pricelist_id` is set by user in settings. +- **Auto mode:** client sends no explicit ID for that source; backend resolves the current latest active pricelist. + +`auto` must stay `auto` after price-level refresh and after manual "refresh prices": +- resolved IDs are runtime-only and must not overwrite user's mode; +- switching to explicit selection must clear runtime auto resolution for that source. + +### Latest Pricelist Resolution Rules + +For both server (`qt_pricelists`) and local cache (`local_pricelists`), "latest by source" is resolved with: + +1. only pricelists that have at least one item (`EXISTS ...pricelist_items`); +2. deterministic sort: `created_at DESC, id DESC`. + +This prevents selecting empty/incomplete snapshots and removes nondeterministic ties. + --- ## Configuration Versioning diff --git a/bible/03-database.md b/bible/03-database.md index 2a45dd5..b8c4c1e 100644 --- a/bible/03-database.md +++ b/bible/03-database.md @@ -45,6 +45,7 @@ File: `qfs.db` in the user-state directory (see [05-config.md](05-config.md)). INDEX local_pricelist_items(pricelist_id) UNIQUE INDEX local_pricelists(server_id) INDEX local_pricelists(source, created_at) -- used for "latest by source" queries +-- latest-by-source runtime query also applies deterministic tie-break by id DESC -- Configurations INDEX local_configurations(pricelist_id) diff --git a/bible/04-api.md b/bible/04-api.md index 33810e4..5b996cb 100644 --- a/bible/04-api.md +++ b/bible/04-api.md @@ -31,12 +31,14 @@ | Method | Endpoint | Purpose | |--------|----------|---------| -| GET | `/api/pricelists` | List pricelists | +| GET | `/api/pricelists` | List pricelists (`source`, `active_only`, pagination) | | GET | `/api/pricelists/latest` | Latest pricelist by source | | GET | `/api/pricelists/:id` | Pricelist by ID | | GET | `/api/pricelists/:id/items` | Pricelist line items | | GET | `/api/pricelists/:id/lots` | Lot names in pricelist | +`GET /api/pricelists?active_only=true` returns only pricelists that have synced items (`item_count > 0`). + ### Configurations | Method | Endpoint | Purpose | @@ -46,7 +48,7 @@ | GET | `/api/configs/:uuid` | Get configuration | | PUT | `/api/configs/:uuid` | Update configuration | | DELETE | `/api/configs/:uuid` | Archive configuration | -| POST | `/api/configs/:uuid/refresh` | Refresh prices from pricelist | +| POST | `/api/configs/:uuid/refresh-prices` | Refresh prices from pricelist | | POST | `/api/configs/:uuid/clone` | Clone configuration | | POST | `/api/configs/:uuid/reactivate` | Restore archived configuration | | POST | `/api/configs/:uuid/rename` | Rename configuration | diff --git a/bible/07-dev.md b/bible/07-dev.md index 55d4fd9..f3995ca 100644 --- a/bible/07-dev.md +++ b/bible/07-dev.md @@ -130,6 +130,7 @@ if found && price > 0 { **Problem: configuration refresh does not update prices** 1. Refresh uses the latest estimate pricelist by default -2. `local_pricelist_items` must have data +2. Latest resolution ignores pricelists without items (`EXISTS local_pricelist_items`) 3. Old prices in `config.items` are preserved if a line item is not found in the pricelist 4. To force a pricelist update: set `configuration.pricelist_id` +5. In configurator, `Авто` must remain auto-mode (runtime resolved ID must not be persisted as explicit selection) diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index 7b9324f..6807006 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -46,8 +46,30 @@ func (h *PricelistHandler) List(c *gin.Context) { } localPLs = filtered } - if activeOnly { - // Local cache stores only active snapshots for normal operations. + type pricelistWithCount struct { + pricelist localdb.LocalPricelist + itemCount int64 + usageCount int + } + withCounts := make([]pricelistWithCount, 0, len(localPLs)) + for _, lpl := range localPLs { + itemCount := h.localDB.CountLocalPricelistItems(lpl.ID) + if activeOnly && itemCount == 0 { + continue + } + usageCount := 0 + if lpl.IsUsed { + usageCount = 1 + } + withCounts = append(withCounts, pricelistWithCount{ + pricelist: lpl, + itemCount: itemCount, + usageCount: usageCount, + }) + } + localPLs = localPLs[:0] + for _, row := range withCounts { + localPLs = append(localPLs, row.pricelist) } sort.SliceStable(localPLs, func(i, j int) bool { return localPLs[i].CreatedAt.After(localPLs[j].CreatedAt) }) total := len(localPLs) @@ -62,10 +84,14 @@ func (h *PricelistHandler) List(c *gin.Context) { pageSlice := localPLs[start:end] summaries := make([]map[string]interface{}, 0, len(pageSlice)) for _, lpl := range pageSlice { - itemCount := h.localDB.CountLocalPricelistItems(lpl.ID) + itemCount := int64(0) usageCount := 0 - if lpl.IsUsed { - usageCount = 1 + for _, row := range withCounts { + if row.pricelist.ID == lpl.ID { + itemCount = row.itemCount + usageCount = row.usageCount + break + } } summaries = append(summaries, map[string]interface{}{ "id": lpl.ServerID, diff --git a/internal/handlers/pricelist_test.go b/internal/handlers/pricelist_test.go index 09d0b5d..fa9ec4c 100644 --- a/internal/handlers/pricelist_test.go +++ b/internal/handlers/pricelist_test.go @@ -82,3 +82,80 @@ func TestPricelistGetItems_ReturnsLotCategoryFromLocalPricelistItems(t *testing. } } +func TestPricelistList_ActiveOnlyExcludesPricelistsWithoutItems(t *testing.T) { + gin.SetMode(gin.TestMode) + + local, err := localdb.New(filepath.Join(t.TempDir(), "local_active_only.db")) + if err != nil { + t.Fatalf("init local db: %v", err) + } + t.Cleanup(func() { _ = local.Close() }) + + if err := local.SaveLocalPricelist(&localdb.LocalPricelist{ + ServerID: 10, + Source: "estimate", + Version: "E-1", + Name: "with-items", + CreatedAt: time.Now().Add(-time.Minute), + SyncedAt: time.Now().Add(-time.Minute), + }); err != nil { + t.Fatalf("save with-items pricelist: %v", err) + } + withItems, err := local.GetLocalPricelistByServerID(10) + if err != nil { + t.Fatalf("load with-items pricelist: %v", err) + } + if err := local.SaveLocalPricelistItems([]localdb.LocalPricelistItem{ + { + PricelistID: withItems.ID, + LotName: "CPU_X", + LotCategory: "CPU", + Price: 100, + }, + }); err != nil { + t.Fatalf("save with-items pricelist items: %v", err) + } + + if err := local.SaveLocalPricelist(&localdb.LocalPricelist{ + ServerID: 11, + Source: "estimate", + Version: "E-2", + Name: "without-items", + CreatedAt: time.Now(), + SyncedAt: time.Now(), + }); err != nil { + t.Fatalf("save without-items pricelist: %v", err) + } + + h := NewPricelistHandler(local) + + req, _ := http.NewRequest("GET", "/api/pricelists?source=estimate&active_only=true", nil) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + h.List(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp struct { + Pricelists []struct { + ID uint `json:"id"` + } `json:"pricelists"` + Total int `json:"total"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + if resp.Total != 1 { + t.Fatalf("expected total=1, got %d", resp.Total) + } + if len(resp.Pricelists) != 1 { + t.Fatalf("expected 1 pricelist, got %d", len(resp.Pricelists)) + } + if resp.Pricelists[0].ID != 10 { + t.Fatalf("expected pricelist id=10, got %d", resp.Pricelists[0].ID) + } +} diff --git a/internal/localdb/localdb.go b/internal/localdb/localdb.go index 110312d..bede8a9 100644 --- a/internal/localdb/localdb.go +++ b/internal/localdb/localdb.go @@ -692,7 +692,11 @@ func (l *LocalDB) CountLocalPricelists() int64 { // GetLatestLocalPricelist returns the most recently synced pricelist func (l *LocalDB) GetLatestLocalPricelist() (*LocalPricelist, error) { var pricelist LocalPricelist - if err := l.db.Where("source = ?", "estimate").Order("created_at DESC").First(&pricelist).Error; err != nil { + if err := l.db. + Where("source = ?", "estimate"). + Where("EXISTS (SELECT 1 FROM local_pricelist_items WHERE local_pricelist_items.pricelist_id = local_pricelists.id)"). + Order("created_at DESC, id DESC"). + First(&pricelist).Error; err != nil { return nil, err } return &pricelist, nil @@ -701,7 +705,11 @@ func (l *LocalDB) GetLatestLocalPricelist() (*LocalPricelist, error) { // GetLatestLocalPricelistBySource returns the most recently synced pricelist for a source. func (l *LocalDB) GetLatestLocalPricelistBySource(source string) (*LocalPricelist, error) { var pricelist LocalPricelist - if err := l.db.Where("source = ?", source).Order("created_at DESC").First(&pricelist).Error; err != nil { + if err := l.db. + Where("source = ?", source). + Where("EXISTS (SELECT 1 FROM local_pricelist_items WHERE local_pricelist_items.pricelist_id = local_pricelists.id)"). + Order("created_at DESC, id DESC"). + First(&pricelist).Error; err != nil { return nil, err } return &pricelist, nil diff --git a/internal/localdb/pricelist_latest_test.go b/internal/localdb/pricelist_latest_test.go new file mode 100644 index 0000000..86d2291 --- /dev/null +++ b/internal/localdb/pricelist_latest_test.go @@ -0,0 +1,128 @@ +package localdb + +import ( + "path/filepath" + "testing" + "time" +) + +func TestGetLatestLocalPricelistBySource_SkipsPricelistWithoutItems(t *testing.T) { + local, err := New(filepath.Join(t.TempDir(), "latest_without_items.db")) + if err != nil { + t.Fatalf("open localdb: %v", err) + } + t.Cleanup(func() { _ = local.Close() }) + + base := time.Now().Add(-time.Minute) + withItems := &LocalPricelist{ + ServerID: 1001, + Source: "estimate", + Version: "E-1", + Name: "with-items", + CreatedAt: base, + SyncedAt: base, + } + if err := local.SaveLocalPricelist(withItems); err != nil { + t.Fatalf("save pricelist with items: %v", err) + } + storedWithItems, err := local.GetLocalPricelistByServerID(withItems.ServerID) + if err != nil { + t.Fatalf("load pricelist with items: %v", err) + } + if err := local.SaveLocalPricelistItems([]LocalPricelistItem{ + { + PricelistID: storedWithItems.ID, + LotName: "CPU_A", + Price: 100, + }, + }); err != nil { + t.Fatalf("save pricelist items: %v", err) + } + + withoutItems := &LocalPricelist{ + ServerID: 1002, + Source: "estimate", + Version: "E-2", + Name: "without-items", + CreatedAt: base.Add(2 * time.Second), + SyncedAt: base.Add(2 * time.Second), + } + if err := local.SaveLocalPricelist(withoutItems); err != nil { + t.Fatalf("save pricelist without items: %v", err) + } + + got, err := local.GetLatestLocalPricelistBySource("estimate") + if err != nil { + t.Fatalf("GetLatestLocalPricelistBySource: %v", err) + } + if got.ServerID != withItems.ServerID { + t.Fatalf("expected server_id=%d, got %d", withItems.ServerID, got.ServerID) + } +} + +func TestGetLatestLocalPricelistBySource_TieBreaksByID(t *testing.T) { + local, err := New(filepath.Join(t.TempDir(), "latest_tie_break.db")) + if err != nil { + t.Fatalf("open localdb: %v", err) + } + t.Cleanup(func() { _ = local.Close() }) + + base := time.Now().Add(-time.Minute) + first := &LocalPricelist{ + ServerID: 2001, + Source: "warehouse", + Version: "S-1", + Name: "first", + CreatedAt: base, + SyncedAt: base, + } + if err := local.SaveLocalPricelist(first); err != nil { + t.Fatalf("save first pricelist: %v", err) + } + storedFirst, err := local.GetLocalPricelistByServerID(first.ServerID) + if err != nil { + t.Fatalf("load first pricelist: %v", err) + } + if err := local.SaveLocalPricelistItems([]LocalPricelistItem{ + { + PricelistID: storedFirst.ID, + LotName: "CPU_A", + Price: 101, + }, + }); err != nil { + t.Fatalf("save first items: %v", err) + } + + second := &LocalPricelist{ + ServerID: 2002, + Source: "warehouse", + Version: "S-2", + Name: "second", + CreatedAt: base, + SyncedAt: base, + } + if err := local.SaveLocalPricelist(second); err != nil { + t.Fatalf("save second pricelist: %v", err) + } + storedSecond, err := local.GetLocalPricelistByServerID(second.ServerID) + if err != nil { + t.Fatalf("load second pricelist: %v", err) + } + if err := local.SaveLocalPricelistItems([]LocalPricelistItem{ + { + PricelistID: storedSecond.ID, + LotName: "CPU_A", + Price: 102, + }, + }); err != nil { + t.Fatalf("save second items: %v", err) + } + + got, err := local.GetLatestLocalPricelistBySource("warehouse") + if err != nil { + t.Fatalf("GetLatestLocalPricelistBySource: %v", err) + } + if got.ServerID != second.ServerID { + t.Fatalf("expected server_id=%d, got %d", second.ServerID, got.ServerID) + } +} diff --git a/internal/repository/pricelist.go b/internal/repository/pricelist.go index ff373b0..3ca9344 100644 --- a/internal/repository/pricelist.go +++ b/internal/repository/pricelist.go @@ -40,7 +40,7 @@ func (r *PricelistRepository) ListBySource(source string, offset, limit int) ([] } var pricelists []models.Pricelist - if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil { + if err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil { return nil, 0, fmt.Errorf("listing pricelists: %w", err) } @@ -67,7 +67,7 @@ func (r *PricelistRepository) ListActiveBySource(source string, offset, limit in } var pricelists []models.Pricelist - if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil { + if err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(limit).Find(&pricelists).Error; err != nil { return nil, 0, fmt.Errorf("listing active pricelists: %w", err) } @@ -148,7 +148,11 @@ func (r *PricelistRepository) GetLatestActive() (*models.Pricelist, error) { // GetLatestActiveBySource returns the most recent active pricelist by source. func (r *PricelistRepository) GetLatestActiveBySource(source string) (*models.Pricelist, error) { var pricelist models.Pricelist - if err := r.db.Where("is_active = ? AND source = ?", true, source).Order("created_at DESC").First(&pricelist).Error; err != nil { + if err := r.db. + Where("is_active = ? AND source = ?", true, source). + Where("EXISTS (SELECT 1 FROM qt_pricelist_items WHERE qt_pricelist_items.pricelist_id = qt_pricelists.id)"). + Order("created_at DESC, id DESC"). + First(&pricelist).Error; err != nil { return nil, fmt.Errorf("getting latest pricelist: %w", err) } return &pricelist, nil diff --git a/internal/repository/pricelist_test.go b/internal/repository/pricelist_test.go index 1d10ee8..2c09216 100644 --- a/internal/repository/pricelist_test.go +++ b/internal/repository/pricelist_test.go @@ -126,6 +126,101 @@ func TestGetItems_WarehouseAvailableQtyUsesPrefixResolver(t *testing.T) { } } +func TestGetLatestActiveBySource_SkipsPricelistsWithoutItems(t *testing.T) { + repo := newTestPricelistRepository(t) + db := repo.db + ts := time.Now().Add(-time.Minute) + source := "test-estimate-skip-empty" + + emptyLatest := models.Pricelist{ + Source: source, + Version: "E-empty", + CreatedBy: "test", + IsActive: true, + CreatedAt: ts.Add(2 * time.Second), + } + if err := db.Create(&emptyLatest).Error; err != nil { + t.Fatalf("create empty pricelist: %v", err) + } + + withItems := models.Pricelist{ + Source: source, + Version: "E-with-items", + CreatedBy: "test", + IsActive: true, + CreatedAt: ts, + } + if err := db.Create(&withItems).Error; err != nil { + t.Fatalf("create pricelist with items: %v", err) + } + if err := db.Create(&models.PricelistItem{ + PricelistID: withItems.ID, + LotName: "CPU_A", + Price: 100, + }).Error; err != nil { + t.Fatalf("create pricelist item: %v", err) + } + + got, err := repo.GetLatestActiveBySource(source) + if err != nil { + t.Fatalf("GetLatestActiveBySource: %v", err) + } + if got.ID != withItems.ID { + t.Fatalf("expected pricelist with items id=%d, got id=%d", withItems.ID, got.ID) + } +} + +func TestGetLatestActiveBySource_TieBreaksByID(t *testing.T) { + repo := newTestPricelistRepository(t) + db := repo.db + ts := time.Now().Add(-time.Minute) + source := "test-warehouse-tie-break" + + first := models.Pricelist{ + Source: source, + Version: "S-1", + CreatedBy: "test", + IsActive: true, + CreatedAt: ts, + } + if err := db.Create(&first).Error; err != nil { + t.Fatalf("create first pricelist: %v", err) + } + if err := db.Create(&models.PricelistItem{ + PricelistID: first.ID, + LotName: "CPU_A", + Price: 101, + }).Error; err != nil { + t.Fatalf("create first item: %v", err) + } + + second := models.Pricelist{ + Source: source, + Version: "S-2", + CreatedBy: "test", + IsActive: true, + CreatedAt: ts, + } + if err := db.Create(&second).Error; err != nil { + t.Fatalf("create second pricelist: %v", err) + } + if err := db.Create(&models.PricelistItem{ + PricelistID: second.ID, + LotName: "CPU_A", + Price: 102, + }).Error; err != nil { + t.Fatalf("create second item: %v", err) + } + + got, err := repo.GetLatestActiveBySource(source) + if err != nil { + t.Fatalf("GetLatestActiveBySource: %v", err) + } + if got.ID != second.ID { + t.Fatalf("expected later inserted pricelist id=%d, got id=%d", second.ID, got.ID) + } +} + func newTestPricelistRepository(t *testing.T) *PricelistRepository { t.Helper() diff --git a/web/templates/index.html b/web/templates/index.html index 5a61715..91663b0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -433,6 +433,11 @@ let selectedPricelistIds = { warehouse: null, competitor: null }; +let resolvedAutoPricelistIds = { + estimate: null, + warehouse: null, + competitor: null +}; let disablePriceRefresh = false; let onlyInStock = false; let activePricelistsBySource = { @@ -498,6 +503,22 @@ function formatDelta(abs, pct) { return sign + formatMoney(absValue) + ' (' + pctSign + Math.round(Math.abs(pct)) + '%)'; } +function getEffectivePricelistID(source) { + const explicit = selectedPricelistIds[source]; + if (Number.isFinite(explicit) && explicit > 0) { + return Number(explicit); + } + const resolvedAuto = resolvedAutoPricelistIds[source]; + if (Number.isFinite(resolvedAuto) && resolvedAuto > 0) { + return Number(resolvedAuto); + } + const fallback = activePricelistsBySource[source]?.[0]?.id; + if (Number.isFinite(fallback) && fallback > 0) { + return Number(fallback); + } + return null; +} + async function refreshPriceLevels(options = {}) { const force = options.force === true; const noCache = options.noCache === true; @@ -543,12 +564,10 @@ async function refreshPriceLevels(options = {}) { if (data.resolved_pricelist_ids) { ['estimate', 'warehouse', 'competitor'].forEach(source => { if (!selectedPricelistIds[source] && data.resolved_pricelist_ids[source]) { - selectedPricelistIds[source] = data.resolved_pricelist_ids[source]; + resolvedAutoPricelistIds[source] = Number(data.resolved_pricelist_ids[source]); } }); - syncPriceSettingsControls(); renderPricelistSettingsSummary(); - persistLocalPriceSettings(); } } catch(e) { console.error('Failed to refresh price levels', e); @@ -581,11 +600,7 @@ function schedulePriceLevelsRefresh(options = {}) { } function currentWarehousePricelistID() { - const id = selectedPricelistIds.warehouse; - if (Number.isFinite(id) && id > 0) return Number(id); - const fallback = activePricelistsBySource.warehouse?.[0]?.id; - if (Number.isFinite(fallback) && fallback > 0) return Number(fallback); - return null; + return getEffectivePricelistID('warehouse'); } async function loadWarehouseInStockLots() { @@ -823,9 +838,7 @@ async function loadActivePricelists(force = false) { if (existing && activePricelistsBySource[source].some(pl => Number(pl.id) === Number(existing))) { return; } - selectedPricelistIds[source] = activePricelistsBySource[source].length > 0 - ? Number(activePricelistsBySource[source][0].id) - : null; + selectedPricelistIds[source] = null; } catch (e) { activePricelistsBySource[source] = []; selectedPricelistIds[source] = null; @@ -961,6 +974,15 @@ function applyPriceSettings() { selectedPricelistIds.estimate = Number.isFinite(estimateVal) && estimateVal > 0 ? estimateVal : null; selectedPricelistIds.warehouse = Number.isFinite(warehouseVal) && warehouseVal > 0 ? warehouseVal : null; selectedPricelistIds.competitor = Number.isFinite(competitorVal) && competitorVal > 0 ? competitorVal : null; + if (selectedPricelistIds.estimate) { + resolvedAutoPricelistIds.estimate = null; + } + if (selectedPricelistIds.warehouse) { + resolvedAutoPricelistIds.warehouse = null; + } + if (selectedPricelistIds.competitor) { + resolvedAutoPricelistIds.competitor = null; + } disablePriceRefresh = disableVal; onlyInStock = inStockVal; @@ -1861,7 +1883,8 @@ async function previewArticle() { if (!el) return; const model = serverModelForQuote.trim(); - if (!model || !selectedPricelistIds.estimate || cart.length === 0) { + const estimatePricelistID = getEffectivePricelistID('estimate'); + if (!model || !estimatePricelistID || cart.length === 0) { currentArticle = ''; el.textContent = 'Артикул: —'; return; @@ -1874,7 +1897,7 @@ async function previewArticle() { body: JSON.stringify({ server_model: serverModelForQuote, support_code: supportCode, - pricelist_id: selectedPricelistIds.estimate, + pricelist_id: estimatePricelistID, items: cart.map(item => ({ lot_name: item.lot_name, quantity: item.quantity, @@ -2408,13 +2431,19 @@ async function refreshPrices() { updatePriceUpdateDate(config.price_updated_at); } if (config.pricelist_id) { - selectedPricelistIds.estimate = config.pricelist_id; + if (selectedPricelistIds.estimate) { + selectedPricelistIds.estimate = config.pricelist_id; + } else { + resolvedAutoPricelistIds.estimate = Number(config.pricelist_id); + } if (!activePricelistsBySource.estimate.some(opt => Number(opt.id) === Number(config.pricelist_id))) { await loadActivePricelists(); } syncPriceSettingsControls(); renderPricelistSettingsSummary(); - persistLocalPriceSettings(); + if (selectedPricelistIds.estimate) { + persistLocalPriceSettings(); + } } // Re-render UI