feat: redesign project pricing export — FOB/DDP basis, variant filename, article column

- Add FOB/DDP basis to export options; DDP multiplies all prices ×1.3
- Rename export file from "pricing" to "{FOB|DDP} {variant}" (e.g. "FOB v1")
- Fix server article missing from CSV summary row (PN вендора column)
- Skip per-row breakdown when neither LOT nor BOM is selected
- Remove empty separator rows between configurations
- Redesign export modal: split into Артикул / Цены / Базис поставки sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-04-03 17:55:26 +03:00
parent a360992a01
commit 7f6be786a8
3 changed files with 121 additions and 47 deletions

View File

@@ -56,11 +56,24 @@ 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"`
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
}
func (o ProjectPricingExportOptions) saleMarkupFactor() float64 {
if o.SaleMarkup > 0 {
return o.SaleMarkup
}
return 1.3
}
func (o ProjectPricingExportOptions) isDDP() bool {
return strings.EqualFold(strings.TrimSpace(o.Basis), "ddp")
}
type ProjectPricingExportData struct {
@@ -251,18 +264,16 @@ func (s *ExportService) ToPricingCSV(w io.Writer, data *ProjectPricingExportData
return fmt.Errorf("failed to write pricing header: %w", err)
}
for idx, cfg := range data.Configs {
writeRows := opts.IncludeLOT || opts.IncludeBOM
for _, cfg := range data.Configs {
if err := csvWriter.Write(pricingConfigSummaryRow(cfg, opts)); err != nil {
return fmt.Errorf("failed to write config summary row: %w", err)
}
for _, row := range cfg.Rows {
if err := csvWriter.Write(pricingCSVRow(row, opts)); err != nil {
return fmt.Errorf("failed to write pricing row: %w", err)
}
}
if idx < len(data.Configs)-1 {
if err := csvWriter.Write([]string{}); err != nil {
return fmt.Errorf("failed to write separator row: %w", err)
if writeRows {
for _, row := range cfg.Rows {
if err := csvWriter.Write(pricingCSVRow(row, opts)); err != nil {
return fmt.Errorf("failed to write pricing row: %w", err)
}
}
}
}
@@ -424,6 +435,9 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
Competitor: totalForUnitPrice(priceMap[item.LotName].Competitor, item.Quantity),
})
}
if opts.isDDP() {
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
}
return block, nil
}
@@ -443,9 +457,29 @@ func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts
})
}
if opts.isDDP() {
applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
}
return block, nil
}
func applyDDPMarkup(rows []ProjectPricingExportRow, factor float64) {
for i := range rows {
rows[i].Estimate = scaleFloatPtr(rows[i].Estimate, factor)
rows[i].Stock = scaleFloatPtr(rows[i].Stock, factor)
rows[i].Competitor = scaleFloatPtr(rows[i].Competitor, factor)
}
}
func scaleFloatPtr(v *float64, factor float64) *float64 {
if v == nil {
return nil
}
result := *v * factor
return &result
}
// resolveCategories returns lot_name → category map.
// Primary source: pricelist items (lot_category). Fallback: local_components table.
func (s *ExportService) resolveCategories(pricelistID *uint, lotNames []string) map[string]string {
@@ -735,7 +769,7 @@ func pricingConfigSummaryRow(cfg ProjectPricingExportConfig, opts ProjectPricing
record = append(record, "")
}
record = append(record,
"",
emptyDash(cfg.Article),
emptyDash(cfg.Name),
fmt.Sprintf("%d", exportPositiveInt(cfg.ServerCount, 1)),
)