173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
package services
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/csv"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||
)
|
||
|
||
type ExportService struct {
|
||
config config.ExportConfig
|
||
categoryRepo *repository.CategoryRepository
|
||
}
|
||
|
||
func NewExportService(cfg config.ExportConfig, categoryRepo *repository.CategoryRepository) *ExportService {
|
||
return &ExportService{
|
||
config: cfg,
|
||
categoryRepo: categoryRepo,
|
||
}
|
||
}
|
||
|
||
type ExportData struct {
|
||
Name string
|
||
Article string
|
||
Items []ExportItem
|
||
Total float64
|
||
Notes string
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type ExportItem struct {
|
||
LotName string
|
||
Description string
|
||
Category string
|
||
Quantity int
|
||
UnitPrice float64
|
||
TotalPrice float64
|
||
}
|
||
|
||
func (s *ExportService) ToCSV(w io.Writer, data *ExportData) error {
|
||
// Write UTF-8 BOM for Excel compatibility
|
||
if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
||
return fmt.Errorf("failed to write BOM: %w", err)
|
||
}
|
||
|
||
csvWriter := csv.NewWriter(w)
|
||
// Use semicolon as delimiter for Russian Excel locale
|
||
csvWriter.Comma = ';'
|
||
defer csvWriter.Flush()
|
||
|
||
// Header
|
||
headers := []string{"Артикул", "Описание", "Категория", "Количество", "Цена за единицу", "Сумма"}
|
||
if err := csvWriter.Write(headers); err != nil {
|
||
return fmt.Errorf("failed to write header: %w", err)
|
||
}
|
||
|
||
// Get category hierarchy for sorting
|
||
categoryOrder := make(map[string]int)
|
||
if s.categoryRepo != nil {
|
||
categories, err := s.categoryRepo.GetAll()
|
||
if err == nil {
|
||
for _, cat := range categories {
|
||
categoryOrder[cat.Code] = cat.DisplayOrder
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort items by category display order
|
||
sortedItems := make([]ExportItem, len(data.Items))
|
||
copy(sortedItems, data.Items)
|
||
|
||
// Sort using category display order (items without category go to the end)
|
||
for i := 0; i < len(sortedItems)-1; i++ {
|
||
for j := i + 1; j < len(sortedItems); j++ {
|
||
orderI, hasI := categoryOrder[sortedItems[i].Category]
|
||
orderJ, hasJ := categoryOrder[sortedItems[j].Category]
|
||
|
||
// Items without category go to the end
|
||
if !hasI && hasJ {
|
||
sortedItems[i], sortedItems[j] = sortedItems[j], sortedItems[i]
|
||
} else if hasI && hasJ {
|
||
// Both have categories, sort by display order
|
||
if orderI > orderJ {
|
||
sortedItems[i], sortedItems[j] = sortedItems[j], sortedItems[i]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Items
|
||
for _, item := range sortedItems {
|
||
row := []string{
|
||
item.LotName,
|
||
item.Description,
|
||
item.Category,
|
||
fmt.Sprintf("%d", item.Quantity),
|
||
strings.ReplaceAll(fmt.Sprintf("%.2f", item.UnitPrice), ".", ","),
|
||
strings.ReplaceAll(fmt.Sprintf("%.2f", item.TotalPrice), ".", ","),
|
||
}
|
||
if err := csvWriter.Write(row); err != nil {
|
||
return fmt.Errorf("failed to write row: %w", err)
|
||
}
|
||
}
|
||
|
||
// Total row
|
||
totalStr := strings.ReplaceAll(fmt.Sprintf("%.2f", data.Total), ".", ",")
|
||
if err := csvWriter.Write([]string{data.Article, "", "", "", "ИТОГО:", totalStr}); err != nil {
|
||
return fmt.Errorf("failed to write total row: %w", err)
|
||
}
|
||
|
||
csvWriter.Flush()
|
||
if err := csvWriter.Error(); err != nil {
|
||
return fmt.Errorf("csv writer error: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ToCSVBytes is a backward-compatible wrapper that returns CSV data as bytes
|
||
func (s *ExportService) ToCSVBytes(data *ExportData) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
if err := s.ToCSV(&buf, data); err != nil {
|
||
return nil, err
|
||
}
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
func (s *ExportService) ConfigToExportData(config *models.Configuration, componentService *ComponentService) *ExportData {
|
||
items := make([]ExportItem, len(config.Items))
|
||
var total float64
|
||
|
||
for i, item := range config.Items {
|
||
itemTotal := item.UnitPrice * float64(item.Quantity)
|
||
|
||
// Получаем информацию о компоненте для заполнения категории
|
||
componentView, err := componentService.GetByLotName(item.LotName)
|
||
if err != nil {
|
||
// Если не удалось получить информацию о компоненте, используем только основные данные
|
||
items[i] = ExportItem{
|
||
LotName: item.LotName,
|
||
Quantity: item.Quantity,
|
||
UnitPrice: item.UnitPrice,
|
||
TotalPrice: itemTotal,
|
||
}
|
||
} else {
|
||
items[i] = ExportItem{
|
||
LotName: item.LotName,
|
||
Description: componentView.Description,
|
||
Category: componentView.Category,
|
||
Quantity: item.Quantity,
|
||
UnitPrice: item.UnitPrice,
|
||
TotalPrice: itemTotal,
|
||
}
|
||
}
|
||
total += itemTotal
|
||
}
|
||
|
||
return &ExportData{
|
||
Name: config.Name,
|
||
Article: "",
|
||
Items: items,
|
||
Total: total,
|
||
Notes: config.Notes,
|
||
CreatedAt: config.CreatedAt,
|
||
}
|
||
}
|