пофиксил неправильный пересчет фасовок в накладной

This commit is contained in:
2025-12-27 09:24:21 +03:00
parent dfd855cb6e
commit c2d382cb6a
12 changed files with 461 additions and 144 deletions

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
@@ -27,6 +28,7 @@ type Service struct {
catalogRepo catalog.Repository
accountRepo account.Repository
supplierRepo suppliers.Repository
invoiceRepo invoices.Repository
rmsFactory *rms.Factory
billingService *billing.Service
}
@@ -37,6 +39,7 @@ func NewService(
catalogRepo catalog.Repository,
accountRepo account.Repository,
supplierRepo suppliers.Repository,
invoiceRepo invoices.Repository,
rmsFactory *rms.Factory,
billingService *billing.Service,
) *Service {
@@ -46,6 +49,7 @@ func NewService(
catalogRepo: catalogRepo,
accountRepo: accountRepo,
supplierRepo: supplierRepo,
invoiceRepo: invoiceRepo,
rmsFactory: rmsFactory,
billingService: billingService,
}
@@ -220,6 +224,7 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price)
}
// CommitDraft отправляет накладную
// CommitDraft отправляет накладную
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
// 1. Получаем сервер и права
@@ -285,11 +290,39 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
sum = dItem.Quantity.Mul(dItem.Price)
}
// Инициализируем значениями из черновика (по умолчанию для базовых единиц)
amountToSend := dItem.Quantity
priceToSend := dItem.Price
// ЛОГИКА ПЕРЕСЧЕТА ДЛЯ ФАСОВОК (СОГЛАСНО ДОКУМЕНТАЦИИ IIKO)
// Если указан ContainerID, iiko требует:
// <amount> = кол-во упаковок * вес упаковки (итоговое кол-во в базовых единицах)
// <price> = цена за упаковку / вес упаковки (цена за базовую единицу)
// <containerId> = ID фасовки
if dItem.ContainerID != nil && *dItem.ContainerID != uuid.Nil {
// Проверяем, что Container загружен (Preload в репозитории)
if dItem.Container != nil {
if !dItem.Container.Count.IsZero() {
// 1. Пересчитываем кол-во: 5 ящиков * 10 кг = 50 кг
amountToSend = dItem.Quantity.Mul(dItem.Container.Count)
// 2. Пересчитываем цену: 1000 руб/ящ / 10 кг = 100 руб/кг
priceToSend = dItem.Price.Div(dItem.Container.Count)
}
} else {
// Если фасовка есть в ID, но не подгрузилась структура - это ошибка данных.
// Логируем варнинг, но пробуем отправить как есть (iiko может отвергнуть или посчитать криво)
logger.Log.Warn("Container struct is nil for item with ContainerID",
zap.String("item_id", dItem.ID.String()),
zap.String("container_id", dItem.ContainerID.String()))
}
}
invItem := invoices.InvoiceItem{
ProductID: *dItem.ProductID,
Amount: dItem.Quantity,
Price: dItem.Price,
Sum: sum,
Amount: amountToSend, // Отправляем ПЕРЕСЧИТАННЫЙ вес/объем
Price: priceToSend, // Отправляем ПЕРЕСЧИТАННУЮ цену за базовую ед.
Sum: sum, // Сумма остается неизменной (Total)
ContainerID: dItem.ContainerID,
}
inv.Items = append(inv.Items, invItem)
@@ -424,3 +457,99 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
return createdID, nil
}
// Добавим новый DTO для единого списка (Frontend Contract)
type UnifiedInvoiceDTO struct {
ID uuid.UUID `json:"id"`
Type string `json:"type"` // "DRAFT" или "SYNCED"
DocumentNumber string `json:"document_number"`
IncomingNumber string `json:"incoming_number"`
DateIncoming time.Time `json:"date_incoming"`
Status string `json:"status"`
TotalSum float64 `json:"total_sum"`
StoreName string `json:"store_name"`
ItemsCount int `json:"items_count"`
CreatedAt time.Time `json:"created_at"`
IsAppCreated bool `json:"is_app_created"`
}
func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("активный сервер не выбран")
}
// 1. Получаем черновики (их обычно немного, берем все активные)
draftsList, err := s.draftRepo.GetActive(server.ID)
if err != nil {
return nil, err
}
// 2. Получаем синхронизированные накладные за период
invoicesList, err := s.invoiceRepo.GetByPeriod(server.ID, from, to)
if err != nil {
return nil, err
}
result := make([]UnifiedInvoiceDTO, 0, len(draftsList)+len(invoicesList))
// Маппим черновики
for _, d := range draftsList {
var sum decimal.Decimal
for _, it := range d.Items {
if !it.Sum.IsZero() {
sum = sum.Add(it.Sum)
} else {
sum = sum.Add(it.Quantity.Mul(it.Price))
}
}
val, _ := sum.Float64()
date := time.Now()
if d.DateIncoming != nil {
date = *d.DateIncoming
}
result = append(result, UnifiedInvoiceDTO{
ID: d.ID,
Type: "DRAFT",
DocumentNumber: d.DocumentNumber,
IncomingNumber: "", // В черновиках пока не разделяем
DateIncoming: date,
Status: d.Status,
TotalSum: val,
StoreName: "", // Можно подгрузить из d.Store.Name если сделан Preload
ItemsCount: len(d.Items),
CreatedAt: d.CreatedAt,
IsAppCreated: true,
})
}
// Маппим проведенные
for _, inv := range invoicesList {
var sum decimal.Decimal
for _, it := range inv.Items {
sum = sum.Add(it.Sum)
}
val, _ := sum.Float64()
isOurs := strings.Contains(strings.ToUpper(inv.Comment), "RMSER")
result = append(result, UnifiedInvoiceDTO{
ID: inv.ID,
Type: "SYNCED",
DocumentNumber: inv.DocumentNumber,
IncomingNumber: inv.IncomingDocumentNumber,
DateIncoming: inv.DateIncoming,
Status: inv.Status,
TotalSum: val,
ItemsCount: len(inv.Items),
CreatedAt: inv.CreatedAt,
IsAppCreated: isOurs,
})
}
// Сортировка по дате накладной (desc)
// (Здесь можно добавить библиотеку sort или оставить как есть, если БД уже отсортировала части)
return result, nil
}

View File

@@ -54,8 +54,8 @@ func NewService(
}
// SyncAllData запускает полную синхронизацию для конкретного пользователя
func (s *Service) SyncAllData(userID uuid.UUID) error {
logger.Log.Info("Запуск полной синхронизации", zap.String("user_id", userID.String()))
func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force))
// 1. Получаем клиент и инфо о сервере
client, err := s.rmsFactory.GetClientForUser(userID)
@@ -92,7 +92,7 @@ func (s *Service) SyncAllData(userID uuid.UUID) error {
}
// 6. Накладные (история)
if err := s.syncInvoices(client, serverID); err != nil {
if err := s.syncInvoices(client, serverID, force); err != nil {
logger.Log.Error("Sync Invoices failed", zap.Error(err))
}
@@ -172,19 +172,24 @@ func (s *Service) syncRecipes(c rms.ClientI, serverID uuid.UUID) error {
return s.recipeRepo.SaveRecipes(recipesList)
}
func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error {
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
if err != nil {
return err
}
func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) error {
var from time.Time
to := time.Now()
if lastDate != nil {
from = *lastDate
if force {
// Принудительная перезагрузка за последние 40 дней
from = time.Now().AddDate(0, 0, -40)
logger.Log.Info("Force sync invoices", zap.String("from", from.Format("2006-01-02")))
} else {
from = time.Now().AddDate(0, 0, -45) // 45 дней по дефолту
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
if err != nil {
return err
}
if lastDate != nil {
from = *lastDate
} else {
from = time.Now().AddDate(0, 0, -45)
}
}
invs, err := c.FetchInvoices(from, to)
@@ -194,10 +199,10 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error {
for i := range invs {
invs[i].RMSServerID = serverID
// В Items пока не добавляли ServerID
}
if len(invs) > 0 {
// Репозиторий использует OnConflict(UpdateAll), поэтому существующие записи обновятся
return s.invoiceRepo.SaveInvoices(invs)
}
return nil