diff --git a/cmd/qfs/main.go b/cmd/qfs/main.go index dccec4c..8d3a490 100644 --- a/cmd/qfs/main.go +++ b/cmd/qfs/main.go @@ -1126,7 +1126,12 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect configs.POST("/:uuid/refresh-prices", func(c *gin.Context) { uuid := c.Param("uuid") - config, err := configService.RefreshPricesNoAuth(uuid) + var req struct { + PricelistID *uint `json:"pricelist_id"` + } + // Ignore bind error — pricelist_id is optional + _ = c.ShouldBindJSON(&req) + config, err := configService.RefreshPricesNoAuth(uuid, req.PricelistID) if err != nil { respondError(c, http.StatusInternalServerError, "internal server error", err) return diff --git a/internal/services/local_configuration.go b/internal/services/local_configuration.go index 6e86d5a..e1bf6b3 100644 --- a/internal/services/local_configuration.go +++ b/internal/services/local_configuration.go @@ -399,17 +399,29 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str return nil, ErrConfigForbidden } - // Refresh local pricelists when online and use latest active/local pricelist for recalculation. + // Refresh local pricelists when online. if s.isOnline() { _ = s.syncService.SyncPricelistsIfNeeded() } - latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist() + + // Use the pricelist stored in the config; fall back to latest if unavailable. + var pricelist *localdb.LocalPricelist + if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 { + if pl, err := s.localDB.GetLocalPricelistByServerID(*localCfg.PricelistID); err == nil { + pricelist = pl + } + } + if pricelist == nil { + if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil { + pricelist = pl + } + } // Update prices for all items from pricelist updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items)) for i, item := range localCfg.Items { - if latestErr == nil && latestPricelist != nil { - price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName) + if pricelist != nil { + price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName) if err == nil && price > 0 { updatedItems[i] = localdb.LocalConfigItem{ LotName: item.LotName, @@ -434,8 +446,8 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str } localCfg.TotalPrice = &total - if latestErr == nil && latestPricelist != nil { - localCfg.PricelistID = &latestPricelist.ServerID + if pricelist != nil { + localCfg.PricelistID = &pricelist.ServerID } // Set price update timestamp and mark for sync @@ -762,8 +774,10 @@ func (s *LocalConfigurationService) ListTemplates(page, perPage int) ([]models.C return templates[start:end], total, nil } -// RefreshPricesNoAuth updates all component prices in the configuration without ownership check -func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) { +// RefreshPricesNoAuth updates all component prices in the configuration without ownership check. +// pricelistServerID optionally specifies which pricelist to use; if nil, the config's stored +// pricelist is used; if that is also absent, the latest local pricelist is used as a fallback. +func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string, pricelistServerID *uint) (*models.Configuration, error) { // Get configuration from local SQLite localCfg, err := s.localDB.GetConfigurationByUUID(uuid) if err != nil { @@ -773,13 +787,36 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co if s.isOnline() { _ = s.syncService.SyncPricelistsIfNeeded() } - latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist() + + // Resolve which pricelist to use: + // 1. Explicitly requested pricelist (from UI selection) + // 2. Pricelist stored in the configuration + // 3. Latest local pricelist as last-resort fallback + var targetServerID *uint + if pricelistServerID != nil && *pricelistServerID > 0 { + targetServerID = pricelistServerID + } else if localCfg.PricelistID != nil && *localCfg.PricelistID > 0 { + targetServerID = localCfg.PricelistID + } + + var pricelist *localdb.LocalPricelist + if targetServerID != nil { + if pl, err := s.localDB.GetLocalPricelistByServerID(*targetServerID); err == nil { + pricelist = pl + } + } + if pricelist == nil { + // Fallback: use latest local pricelist + if pl, err := s.localDB.GetLatestLocalPricelist(); err == nil { + pricelist = pl + } + } // Update prices for all items from pricelist updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items)) for i, item := range localCfg.Items { - if latestErr == nil && latestPricelist != nil { - price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName) + if pricelist != nil { + price, err := s.localDB.GetLocalPriceForLot(pricelist.ID, item.LotName) if err == nil && price > 0 { updatedItems[i] = localdb.LocalConfigItem{ LotName: item.LotName, @@ -804,8 +841,8 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co } localCfg.TotalPrice = &total - if latestErr == nil && latestPricelist != nil { - localCfg.PricelistID = &latestPricelist.ServerID + if pricelist != nil { + localCfg.PricelistID = &pricelist.ServerID } // Set price update timestamp and mark for sync diff --git a/web/templates/index.html b/web/templates/index.html index fc58cb1..7a9c025 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -925,14 +925,9 @@ async function loadActivePricelists(force = false) { const resp = await fetch(`/api/pricelists?active_only=true&source=${source}&per_page=200`); const data = await resp.json(); activePricelistsBySource[source] = data.pricelists || []; - const existing = selectedPricelistIds[source]; - if (existing && activePricelistsBySource[source].some(pl => Number(pl.id) === Number(existing))) { - return; - } - selectedPricelistIds[source] = null; + // Do not reset the stored pricelist — it may be inactive but must be preserved } catch (e) { activePricelistsBySource[source] = []; - selectedPricelistIds[source] = null; } })); activePricelistsLoadedAt = Date.now(); @@ -954,11 +949,25 @@ function renderPricelistSelectOptions(selectId, source) { select.value = ''; return; } - select.innerHTML = `` + pricelists.map(pl => { + select.innerHTML = pricelists.map(pl => { return ``; }).join(''); const current = selectedPricelistIds[source]; - select.value = current ? String(current) : ''; + if (current) { + select.value = String(current); + // Stored pricelist may be inactive — add it as a virtual option if not found + if (!select.value) { + const opt = document.createElement('option'); + opt.value = String(current); + opt.textContent = `ID ${current} (неактивный)`; + select.prepend(opt); + select.value = String(current); + } + } else if (pricelists.length > 0) { + // New config: pre-select the first (latest) pricelist + selectedPricelistIds[source] = Number(pricelists[0].id); + select.value = String(pricelists[0].id); + } } function syncPriceSettingsControls() { @@ -984,9 +993,9 @@ function getPricelistVersionById(source, id) { function renderPricelistSettingsSummary() { const summary = document.getElementById('pricelist-settings-summary'); if (!summary) return; - const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : 'авто'; - const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : 'авто'; - const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : 'авто'; + const estimate = selectedPricelistIds.estimate ? getPricelistVersionById('estimate', selectedPricelistIds.estimate) || `ID ${selectedPricelistIds.estimate}` : '—'; + const warehouse = selectedPricelistIds.warehouse ? getPricelistVersionById('warehouse', selectedPricelistIds.warehouse) || `ID ${selectedPricelistIds.warehouse}` : '—'; + const competitor = selectedPricelistIds.competitor ? getPricelistVersionById('competitor', selectedPricelistIds.competitor) || `ID ${selectedPricelistIds.competitor}` : '—'; const refreshState = disablePriceRefresh ? ' | Обновление цен: выкл' : ''; const stockFilterState = onlyInStock ? ' | Только наличие: вкл' : ''; summary.textContent = `Estimate: ${estimate}, Склад: ${warehouse}, Конкуренты: ${competitor}${refreshState}${stockFilterState}`; @@ -1062,16 +1071,16 @@ function applyPriceSettings() { const inStockVal = Boolean(document.getElementById('settings-only-in-stock')?.checked); const prevWarehouseID = currentWarehousePricelistID(); - 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) { + if (Number.isFinite(estimateVal) && estimateVal > 0) { + selectedPricelistIds.estimate = estimateVal; resolvedAutoPricelistIds.estimate = null; } - if (selectedPricelistIds.warehouse) { + if (Number.isFinite(warehouseVal) && warehouseVal > 0) { + selectedPricelistIds.warehouse = warehouseVal; resolvedAutoPricelistIds.warehouse = null; } - if (selectedPricelistIds.competitor) { + if (Number.isFinite(competitorVal) && competitorVal > 0) { + selectedPricelistIds.competitor = competitorVal; resolvedAutoPricelistIds.competitor = null; } disablePriceRefresh = disableVal; @@ -2508,11 +2517,14 @@ async function refreshPrices() { } try { + const refreshPayload = {}; + if (selectedPricelistIds.estimate) { + refreshPayload.pricelist_id = selectedPricelistIds.estimate; + } const resp = await fetch('/api/configs/' + configUUID + '/refresh-prices', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - } + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(refreshPayload) }); if (!resp.ok) {