fix: DDP-экспорт с проекта игнорировал свой аплифт каждой конфигурации

ProjectPricingExportOptions.SaleMarkup всегда был 0 при экспорте с
проекта, поэтому применялся единый дефолт 1,3 вместо сохранённого в
Notes конфигурации значения. buildPricingExportBlock теперь берёт
аплифт из настроек каждой конфигурации — экспорт с проекта идёт по
тому же конвейеру, что и экспорт с конкретной конфигурации.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-12 17:29:25 +03:00
co-authored by Claude Sonnet 5
parent 619967c954
commit c83e4ec9f9
3 changed files with 84 additions and 4 deletions
+32 -3
View File
@@ -3,6 +3,7 @@ package services
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"math"
@@ -68,13 +69,41 @@ type ProjectPricingExportOptions struct {
// other API callers that omit sale_markup.
const defaultSaleMarkup = 1.3
func (o ProjectPricingExportOptions) saleMarkupFactor() float64 {
// effectiveSaleMarkupFactor resolves the DDP estimate uplift for one configuration.
// An explicit opts.SaleMarkup (sent by the single-config pricing tab, which knows the
// live-edited value) always wins. Otherwise each configuration's own saved uplift
// (persisted in Notes by the "Ценообразование" tab, see index.html buildPricingState/
// restorePricingStateFromNotes) is used, so bulk project export matches what exporting
// that same configuration individually would produce — one pipeline, not a single
// project-wide constant.
func (o ProjectPricingExportOptions) effectiveSaleMarkupFactor(cfg *models.Configuration) float64 {
if o.SaleMarkup > 0 {
return o.SaleMarkup
}
if v := configSavedSaleUplift(cfg); v > 0 {
return v
}
return defaultSaleMarkup
}
// configSavedSaleUplift reads the per-configuration DDP estimate uplift persisted by the
// pricing tab. Notes stores {"pricing_ui":{"sale_uplift":...}} as JSON — see
// serializeConfigNotes/restorePricingStateFromNotes in index.html.
func configSavedSaleUplift(cfg *models.Configuration) float64 {
if cfg == nil || strings.TrimSpace(cfg.Notes) == "" {
return 0
}
var parsed struct {
PricingUI struct {
SaleUplift float64 `json:"sale_uplift"`
} `json:"pricing_ui"`
}
if err := json.Unmarshal([]byte(cfg.Notes), &parsed); err != nil {
return 0
}
return parsed.PricingUI.SaleUplift
}
func (o ProjectPricingExportOptions) isDDP() bool {
return strings.EqualFold(strings.TrimSpace(o.Basis), "ddp")
}
@@ -457,7 +486,7 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
})
}
if opts.isDDP() {
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
applyDDPMarkup(block.Rows, opts.effectiveSaleMarkupFactor(cfg))
}
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
distributeManualPrice(block.Rows, *opts.ManualPrice)
@@ -494,7 +523,7 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
}
if opts.isDDP() {
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
applyDDPMarkup(block.Rows, opts.effectiveSaleMarkupFactor(cfg))
}
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
distributeManualPrice(block.Rows, *opts.ManualPrice)