diff --git a/internal/handlers/export.go b/internal/handlers/export.go index aeb5383..58c35e3 100644 --- a/internal/handlers/export.go +++ b/internal/handlers/export.go @@ -48,11 +48,13 @@ type ExportRequest struct { } type ProjectExportOptionsRequest 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" + SaleMarkup float64 `json:"sale_markup"` // DDP multiplier; 0 defaults to 1.3 } func (h *ExportHandler) ExportCSV(c *gin.Context) { @@ -252,6 +254,8 @@ func (h *ExportHandler) ExportProjectPricingCSV(c *gin.Context) { IncludeEstimate: req.IncludeEstimate, IncludeStock: req.IncludeStock, IncludeCompetitor: req.IncludeCompetitor, + Basis: req.Basis, + SaleMarkup: req.SaleMarkup, } data, err := h.exportService.ProjectToPricingExportData(result.Configs, opts) @@ -260,7 +264,15 @@ func (h *ExportHandler) ExportProjectPricingCSV(c *gin.Context) { return } - filename := fmt.Sprintf("%s (%s) pricing.csv", time.Now().Format("2006-01-02"), project.Code) + basisLabel := "FOB" + if strings.EqualFold(strings.TrimSpace(req.Basis), "ddp") { + basisLabel = "DDP" + } + variantLabel := strings.TrimSpace(project.Variant) + if variantLabel == "" { + variantLabel = "main" + } + filename := fmt.Sprintf("%s (%s) %s %s.csv", time.Now().Format("2006-01-02"), project.Code, basisLabel, variantLabel) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) diff --git a/internal/services/export.go b/internal/services/export.go index bdcc5d0..a8b8b37 100644 --- a/internal/services/export.go +++ b/internal/services/export.go @@ -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)), ) diff --git a/web/templates/project_detail.html b/web/templates/project_detail.html index f296608..fdc35e7 100644 --- a/web/templates/project_detail.html +++ b/web/templates/project_detail.html @@ -113,33 +113,60 @@