feat: сохранение и экспорт ручной цены (buy/sale) из вкладки Ценообразование
Сохранение: - restoreAutosaveDraftIfAny теперь восстанавливает pricing_ui из notes драфта - saveConfigOnExit привязан к pagehide и visibilitychange — цены сохраняются на сервер при уходе со страницы без явного нажатия «Сохранить» Экспорт CSV: - exportPricingCSV передаёт manual_price (buy для FOB, sale для DDP) - ProjectPricingExportOptions.ManualPrice *float64 — новое поле - distributeManualPrice распределяет ручную цену пропорционально estimate с коррекцией остатка на последней строке - Колонка «Ручная цена» в CSV (заголовок, строки, итог конфига) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,13 +53,14 @@ type ProjectExportData struct {
|
||||
}
|
||||
|
||||
type ProjectPricingExportOptions struct {
|
||||
IncludeLOT bool `json:"include_lot"`
|
||||
IncludeBOM bool `json:"include_bom"`
|
||||
IncludeEstimate bool `json:"include_estimate"`
|
||||
IncludeStock bool `json:"include_stock"`
|
||||
IncludeCompetitor bool `json:"include_competitor"`
|
||||
Basis string `json:"basis"` // "fob" or "ddp"; empty defaults to "fob"
|
||||
SaleMarkup float64 `json:"sale_markup"` // DDP multiplier; 0 defaults to 1.3
|
||||
IncludeLOT bool `json:"include_lot"`
|
||||
IncludeBOM bool `json:"include_bom"`
|
||||
IncludeEstimate bool `json:"include_estimate"`
|
||||
IncludeStock bool `json:"include_stock"`
|
||||
IncludeCompetitor bool `json:"include_competitor"`
|
||||
Basis string `json:"basis"` // "fob" or "ddp"; empty defaults to "fob"
|
||||
SaleMarkup float64 `json:"sale_markup"` // DDP multiplier; 0 defaults to 1.3
|
||||
ManualPrice *float64 `json:"manual_price"` // user-defined total price; distributed proportionally across rows
|
||||
}
|
||||
|
||||
func (o ProjectPricingExportOptions) saleMarkupFactor() float64 {
|
||||
@@ -87,14 +88,15 @@ type ProjectPricingExportConfig struct {
|
||||
}
|
||||
|
||||
type ProjectPricingExportRow struct {
|
||||
LotDisplay string
|
||||
VendorPN string
|
||||
Description string
|
||||
Quantity int
|
||||
BOMTotal *float64
|
||||
Estimate *float64
|
||||
Stock *float64
|
||||
Competitor *float64
|
||||
LotDisplay string
|
||||
VendorPN string
|
||||
Description string
|
||||
Quantity int
|
||||
BOMTotal *float64
|
||||
Estimate *float64
|
||||
Stock *float64
|
||||
Competitor *float64
|
||||
ManualPrice *float64 // proportional share of the user-defined total price
|
||||
}
|
||||
|
||||
// ToCSV writes project export data in the new structured CSV format.
|
||||
@@ -442,6 +444,9 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
|
||||
if opts.isDDP() {
|
||||
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
|
||||
}
|
||||
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
|
||||
distributeManualPrice(block.Rows, *opts.ManualPrice)
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
@@ -464,6 +469,9 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
|
||||
if opts.isDDP() {
|
||||
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
|
||||
}
|
||||
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
|
||||
distributeManualPrice(block.Rows, *opts.ManualPrice)
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
@@ -716,6 +724,44 @@ func computeMappingTotal(priceMap map[string]pricingLevels, mappings []localdb.V
|
||||
return floatPtr(total)
|
||||
}
|
||||
|
||||
// distributeManualPrice sets ManualPrice on each row proportionally based on the
|
||||
// row's Estimate share. The last row with a price absorbs rounding remainder so
|
||||
// the sum of ManualPrice values always equals manualPrice exactly.
|
||||
func distributeManualPrice(rows []ProjectPricingExportRow, manualPrice float64) {
|
||||
if manualPrice <= 0 || len(rows) == 0 {
|
||||
return
|
||||
}
|
||||
totalEstimate := 0.0
|
||||
for _, row := range rows {
|
||||
if row.Estimate != nil && *row.Estimate > 0 {
|
||||
totalEstimate += *row.Estimate
|
||||
}
|
||||
}
|
||||
if totalEstimate <= 0 {
|
||||
return
|
||||
}
|
||||
lastIdx := -1
|
||||
for i, row := range rows {
|
||||
if row.Estimate != nil && *row.Estimate > 0 {
|
||||
lastIdx = i
|
||||
}
|
||||
}
|
||||
assigned := 0.0
|
||||
for i, row := range rows {
|
||||
if row.Estimate == nil || *row.Estimate <= 0 {
|
||||
continue
|
||||
}
|
||||
var share float64
|
||||
if i == lastIdx {
|
||||
share = math.Round((manualPrice-assigned)*100) / 100
|
||||
} else {
|
||||
share = math.Round((*row.Estimate/totalEstimate)*manualPrice*100) / 100
|
||||
assigned += share
|
||||
}
|
||||
rows[i].ManualPrice = floatPtr(share)
|
||||
}
|
||||
}
|
||||
|
||||
func computeSingleLotTotal(priceMap map[string]pricingLevels, lotName string, qty int, selector func(pricingLevels) *float64) *float64 {
|
||||
price := selector(priceMap[lotName])
|
||||
if price == nil || *price <= 0 {
|
||||
@@ -744,7 +790,7 @@ func estimateOnlyTotal(estimatePrice *float64, fallbackUnitPrice float64, quanti
|
||||
}
|
||||
|
||||
func pricingCSVHeaders(opts ProjectPricingExportOptions) []string {
|
||||
headers := make([]string, 0, 8)
|
||||
headers := make([]string, 0, 9)
|
||||
headers = append(headers, "Line Item")
|
||||
if opts.IncludeLOT {
|
||||
headers = append(headers, "LOT")
|
||||
@@ -762,11 +808,14 @@ func pricingCSVHeaders(opts ProjectPricingExportOptions) []string {
|
||||
if opts.IncludeCompetitor {
|
||||
headers = append(headers, "Конкуренты")
|
||||
}
|
||||
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
|
||||
headers = append(headers, "Ручная цена")
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func pricingCSVRow(row ProjectPricingExportRow, opts ProjectPricingExportOptions) []string {
|
||||
record := make([]string, 0, 8)
|
||||
record := make([]string, 0, 9)
|
||||
record = append(record, "")
|
||||
if opts.IncludeLOT {
|
||||
record = append(record, emptyDash(row.LotDisplay))
|
||||
@@ -788,11 +837,14 @@ func pricingCSVRow(row ProjectPricingExportRow, opts ProjectPricingExportOptions
|
||||
if opts.IncludeCompetitor {
|
||||
record = append(record, formatMoneyValue(row.Competitor))
|
||||
}
|
||||
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
|
||||
record = append(record, formatMoneyValue(row.ManualPrice))
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
func pricingConfigSummaryRow(cfg ProjectPricingExportConfig, opts ProjectPricingExportOptions) []string {
|
||||
record := make([]string, 0, 8)
|
||||
record := make([]string, 0, 9)
|
||||
record = append(record, fmt.Sprintf("%d", cfg.Line))
|
||||
if opts.IncludeLOT {
|
||||
record = append(record, "")
|
||||
@@ -814,6 +866,9 @@ func pricingConfigSummaryRow(cfg ProjectPricingExportConfig, opts ProjectPricing
|
||||
if opts.IncludeCompetitor {
|
||||
record = append(record, formatMoneyValue(sumPricingColumn(cfg.Rows, func(row ProjectPricingExportRow) *float64 { return row.Competitor })))
|
||||
}
|
||||
if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
|
||||
record = append(record, formatMoneyValue(opts.ManualPrice))
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user