добавил редактируемую сумму и пересчет треугольником

This commit is contained in:
2026-01-19 03:38:20 +03:00
parent 37f4e3e9ee
commit dd5bb7ad1a
6 changed files with 349 additions and 227 deletions

View File

@@ -18,6 +18,14 @@ const (
StatusDeleted = "DELETED"
)
type EditedField string
const (
FieldQuantity EditedField = "quantity"
FieldPrice EditedField = "price"
FieldSum EditedField = "sum"
)
type DraftInvoice struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
@@ -63,6 +71,10 @@ type DraftInvoiceItem struct {
Price decimal.Decimal `gorm:"type:numeric(19,4);default:0" json:"price"`
Sum decimal.Decimal `gorm:"type:numeric(19,4);default:0" json:"sum"`
// Два последних отредактированных поля (для автопересчёта)
LastEditedField1 EditedField `gorm:"column:last_edited_field1;type:varchar(20);default:'quantity'" json:"last_edited_field_1"`
LastEditedField2 EditedField `gorm:"column:last_edited_field2;type:varchar(20);default:'price'" json:"last_edited_field_2"`
IsMatched bool `gorm:"default:false" json:"is_matched"`
}
@@ -70,9 +82,10 @@ type Repository interface {
Create(draft *DraftInvoice) error
GetByID(id uuid.UUID) (*DraftInvoice, error)
GetByRMSInvoiceID(rmsInvoiceID uuid.UUID) (*DraftInvoice, error)
Update(draft *DraftInvoice) error
GetItemByID(itemID uuid.UUID) (*DraftInvoiceItem, error)
CreateItems(items []DraftInvoiceItem) error
UpdateItem(itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error
Update(draft *DraftInvoice) error
UpdateItem(itemID uuid.UUID, updates map[string]interface{}) error
CreateItem(item *DraftInvoiceItem) error
DeleteItem(itemID uuid.UUID) error
Delete(id uuid.UUID) error

View File

@@ -4,7 +4,6 @@ import (
"rmser/internal/domain/drafts"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@@ -56,7 +55,7 @@ func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
return r.db.Model(draft).Updates(map[string]interface{}{
"status": draft.Status,
"document_number": draft.DocumentNumber,
"incoming_document_number": draft.IncomingDocumentNumber, // Добавлено поле для входящего номера документа
"incoming_document_number": draft.IncomingDocumentNumber,
"date_incoming": draft.DateIncoming,
"supplier_id": draft.SupplierID,
"store_id": draft.StoreID,
@@ -82,27 +81,27 @@ func (r *pgRepository) DeleteItem(itemID uuid.UUID) error {
return r.db.Delete(&drafts.DraftInvoiceItem{}, itemID).Error
}
func (r *pgRepository) UpdateItem(itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error {
sum := qty.Mul(price)
isMatched := productID != nil
// GetItemByID - новый метод
func (r *pgRepository) GetItemByID(itemID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
var item drafts.DraftInvoiceItem
err := r.db.Where("id = ?", itemID).First(&item).Error
if err != nil {
return nil, err
}
return &item, nil
}
// UpdateItem - обновленный метод, принимает map
func (r *pgRepository) UpdateItem(itemID uuid.UUID, updates map[string]interface{}) error {
return r.db.Model(&drafts.DraftInvoiceItem{}).
Where("id = ?", itemID).
Updates(map[string]interface{}{
"product_id": productID,
"container_id": containerID,
"quantity": qty,
"price": price,
"sum": sum,
"is_matched": isMatched,
}).Error
Updates(updates).Error
}
func (r *pgRepository) Delete(id uuid.UUID) error {
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
}
// GetActive возвращает черновики для конкретного СЕРВЕРА
func (r *pgRepository) GetActive(serverID uuid.UUID) ([]drafts.DraftInvoice, error) {
var list []drafts.DraftInvoice
@@ -116,14 +115,13 @@ func (r *pgRepository) GetActive(serverID uuid.UUID) ([]drafts.DraftInvoice, err
err := r.db.
Preload("Items").
Preload("Store").
Where("rms_server_id = ? AND status IN ?", serverID, activeStatuses). // Фильтр по серверу
Where("rms_server_id = ? AND status IN ?", serverID, activeStatuses).
Order("created_at DESC").
Find(&list).Error
return list, err
}
// GetRMSInvoiceIDToPhotoURLMap возвращает мапу rms_invoice_id -> sender_photo_url для сервера, где rms_invoice_id не NULL
func (r *pgRepository) GetRMSInvoiceIDToPhotoURLMap(serverID uuid.UUID) (map[uuid.UUID]string, error) {
var draftsList []drafts.DraftInvoice
err := r.db.

View File

@@ -55,7 +55,6 @@ func NewService(
}
}
// checkWriteAccess проверяет, что пользователь имеет право редактировать данные на сервере (ADMIN/OWNER)
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
role, err := s.accountRepo.GetUserRole(userID, serverID)
if err != nil {
@@ -73,8 +72,6 @@ func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, err
return nil, err
}
// Проверяем, что черновик принадлежит активному серверу пользователя
// И пользователь не Оператор (операторы вообще не ходят в API)
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("нет активного сервера")
@@ -92,30 +89,24 @@ func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, err
}
func (s *Service) GetActiveDrafts(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
// 1. Узнаем активный сервер
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("активный сервер не выбран")
}
// 2. Проверяем роль (Security)
// Операторам список недоступен
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
// 3. Возвращаем все черновики СЕРВЕРА
return s.draftRepo.GetActive(server.ID)
}
// GetDictionaries возвращает Склады и Поставщиков для пользователя
func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("active server not found")
}
// Словари нужны только тем, кто редактирует
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
@@ -134,10 +125,6 @@ func (s *Service) DeleteDraft(id uuid.UUID) (string, error) {
if err != nil {
return "", err
}
// TODO: Здесь тоже бы проверить userID и права, но пока оставим как есть,
// так как DeleteDraft вызывается из хендлера, где мы можем добавить проверку,
// но лучше передавать userID в сигнатуру DeleteDraft(id, userID).
// Для скорости пока оставим, полагаясь на то, что фронт не покажет кнопку.
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusDeleted
@@ -168,18 +155,19 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
return s.draftRepo.Update(draft)
}
// AddItem добавляет пустую строку в черновик
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
newItem := &drafts.DraftInvoiceItem{
ID: uuid.New(),
DraftID: draftID,
RawName: "Новая позиция",
RawAmount: decimal.NewFromFloat(1),
RawPrice: decimal.Zero,
Quantity: decimal.NewFromFloat(1),
Price: decimal.Zero,
Sum: decimal.Zero,
IsMatched: false,
ID: uuid.New(),
DraftID: draftID,
RawName: "Новая позиция",
RawAmount: decimal.NewFromFloat(1),
RawPrice: decimal.Zero,
Quantity: decimal.NewFromFloat(1),
Price: decimal.Zero,
Sum: decimal.Zero,
IsMatched: false,
LastEditedField1: drafts.FieldQuantity,
LastEditedField2: drafts.FieldPrice,
}
if err := s.draftRepo.CreateItem(newItem); err != nil {
@@ -188,7 +176,6 @@ func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
return newItem, nil
}
// DeleteItem удаляет строку и возвращает обновленную сумму черновика
func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
if err := s.draftRepo.DeleteItem(itemID); err != nil {
return 0, err
@@ -212,23 +199,104 @@ func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
return sumFloat, nil
}
func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error {
// RecalculateItemFields - логика пересчета Qty/Price/Sum
func (s *Service) RecalculateItemFields(item *drafts.DraftInvoiceItem, editedField drafts.EditedField) {
if item.LastEditedField1 != editedField {
item.LastEditedField2 = item.LastEditedField1
item.LastEditedField1 = editedField
}
fieldsToKeep := map[drafts.EditedField]bool{
item.LastEditedField1: true,
item.LastEditedField2: true,
}
var fieldToRecalc drafts.EditedField
fieldToRecalc = drafts.FieldSum // Default fallback
for _, f := range []drafts.EditedField{drafts.FieldQuantity, drafts.FieldPrice, drafts.FieldSum} {
if !fieldsToKeep[f] {
fieldToRecalc = f
break
}
}
switch fieldToRecalc {
case drafts.FieldQuantity:
if !item.Price.IsZero() {
item.Quantity = item.Sum.Div(item.Price)
} else {
item.Quantity = decimal.Zero
}
case drafts.FieldPrice:
if !item.Quantity.IsZero() {
item.Price = item.Sum.Div(item.Quantity)
} else {
item.Price = decimal.Zero
}
case drafts.FieldSum:
item.Sum = item.Quantity.Mul(item.Price)
}
}
// UpdateItem обновлен для поддержки динамического пересчета
func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price, sum decimal.Decimal, editedField string) error {
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return err
}
// Автосмена статуса
currentItem, err := s.draftRepo.GetItemByID(itemID)
if err != nil {
return err
}
if productID != nil {
currentItem.ProductID = productID
currentItem.IsMatched = true
}
if containerID != nil {
// Если пришел UUID.Nil, значит сброс
if *containerID == uuid.Nil {
currentItem.ContainerID = nil
} else {
currentItem.ContainerID = containerID
}
}
field := drafts.EditedField(editedField)
switch field {
case drafts.FieldQuantity:
currentItem.Quantity = qty
case drafts.FieldPrice:
currentItem.Price = price
case drafts.FieldSum:
currentItem.Sum = sum
}
s.RecalculateItemFields(currentItem, field)
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
s.draftRepo.Update(draft)
}
return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price)
updates := map[string]interface{}{
"product_id": currentItem.ProductID,
"container_id": currentItem.ContainerID,
"quantity": currentItem.Quantity,
"price": currentItem.Price,
"sum": currentItem.Sum,
"last_edited_field1": currentItem.LastEditedField1,
"last_edited_field2": currentItem.LastEditedField2,
"is_matched": currentItem.IsMatched,
}
return s.draftRepo.UpdateItem(itemID, updates)
}
// CommitDraft отправляет накладную
// CommitDraft отправляет накладную
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
// 1. Получаем сервер и права
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
@@ -238,18 +306,15 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", err
}
// --- BILLING CHECK ---
if can, err := s.billingService.CanProcessInvoice(server.ID); !can {
return "", fmt.Errorf("ошибка биллинга: %w", err)
}
// 2. Черновик
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return "", err
}
// Проверка принадлежности черновика серверу
if draft.RMSServerID != server.ID {
return "", errors.New("черновик принадлежит другому серверу")
}
@@ -258,7 +323,6 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", errors.New("накладная уже отправлена")
}
// 3. Клиент (использует права текущего юзера - Админа/Владельца)
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", err
@@ -269,7 +333,6 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
targetStatus = "PROCESSED"
}
// 4. Сборка Invoice
inv := invoices.Invoice{
ID: uuid.Nil,
DocumentNumber: draft.DocumentNumber,
@@ -284,7 +347,7 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
for _, dItem := range draft.Items {
if dItem.ProductID == nil {
continue // Skip unrecognized
continue
}
sum := dItem.Sum
@@ -292,28 +355,16 @@ 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()))
@@ -322,9 +373,9 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
invItem := invoices.InvoiceItem{
ProductID: *dItem.ProductID,
Amount: amountToSend, // Отправляем ПЕРЕСЧИТАННЫЙ вес/объем
Price: priceToSend, // Отправляем ПЕРЕСЧИТАННУЮ цену за базовую ед.
Sum: sum, // Сумма остается неизменной (Total)
Amount: amountToSend,
Price: priceToSend,
Sum: sum,
ContainerID: dItem.ContainerID,
}
inv.Items = append(inv.Items, invItem)
@@ -334,13 +385,11 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", errors.New("нет распознанных позиций для отправки")
}
// 5. Отправка в RMS
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
}
// 6. Поиск UUID созданной накладной
invoices, err := client.FetchInvoices(*draft.DateIncoming, *draft.DateIncoming)
if err != nil {
logger.Log.Warn("Не удалось получить список накладных для поиска UUID", zap.Error(err), zap.Time("date", *draft.DateIncoming))
@@ -358,11 +407,9 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
}
}
// 7. Обновление статуса черновика
draft.Status = drafts.StatusCompleted
s.draftRepo.Update(draft)
// --- БИЛЛИНГ: Списание баланса и инкремент счетчика ---
if err := s.accountRepo.DecrementBalance(server.ID); err != nil {
logger.Log.Error("Billing decrement failed", zap.Error(err), zap.String("server_id", server.ID.String()))
}
@@ -371,7 +418,6 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
logger.Log.Error("Billing increment failed", zap.Error(err))
}
// 7. Запуск обучения
go s.learnFromDraft(draft, server.ID)
return docNum, nil
@@ -408,7 +454,6 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
return uuid.Nil, fmt.Errorf("error fetching product: %w", err)
}
// Валидация на дубли
targetCount, _ := count.Float64()
for _, c := range fullProduct.Containers {
if !c.Deleted && (c.Name == name || (c.Count == targetCount)) {
@@ -418,7 +463,6 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
}
}
// Next Num
maxNum := 0
for _, c := range fullProduct.Containers {
if n, err := strconv.Atoi(c.Num); err == nil {
@@ -429,7 +473,6 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
}
nextNum := strconv.Itoa(maxNum + 1)
// Add
newContainerDTO := rms.ContainerFullDTO{
ID: nil,
Num: nextNum,
@@ -440,13 +483,11 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
}
fullProduct.Containers = append(fullProduct.Containers, newContainerDTO)
// Update RMS
updatedProduct, err := client.UpdateProduct(*fullProduct)
if err != nil {
return uuid.Nil, fmt.Errorf("error updating product: %w", err)
}
// Find created ID
var createdID uuid.UUID
found := false
for _, c := range updatedProduct.Containers {
@@ -465,7 +506,6 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
return uuid.Nil, errors.New("container created but id not found")
}
// Save Local
newLocalContainer := catalog.ProductContainer{
ID: createdID,
RMSServerID: server.ID,
@@ -478,10 +518,9 @@ 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"
Type string `json:"type"`
DocumentNumber string `json:"document_number"`
IncomingNumber string `json:"incoming_number"`
DateIncoming time.Time `json:"date_incoming"`
@@ -501,19 +540,16 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
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
}
// 3. Получаем мапу rms_invoice_id -> sender_photo_url
photoMap, err := s.draftRepo.GetRMSInvoiceIDToPhotoURLMap(server.ID)
if err != nil {
return nil, err
@@ -521,7 +557,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
result := make([]UnifiedInvoiceDTO, 0, len(draftsList)+len(invoicesList))
// Маппим черновики
for _, d := range draftsList {
var sum decimal.Decimal
for _, it := range d.Items {
@@ -538,7 +573,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
date = *d.DateIncoming
}
// Формируем ItemsPreview для черновиков
var itemsPreview string
if len(d.Items) > 0 {
names := make([]string, 0, 3)
@@ -555,11 +589,11 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
ID: d.ID,
Type: "DRAFT",
DocumentNumber: d.DocumentNumber,
IncomingNumber: "", // В черновиках пока не разделяем
IncomingNumber: "",
DateIncoming: date,
Status: d.Status,
TotalSum: val,
StoreName: "", // Можно подгрузить из d.Store.Name если сделан Preload
StoreName: "",
ItemsCount: len(d.Items),
CreatedAt: d.CreatedAt,
IsAppCreated: true,
@@ -568,7 +602,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
})
}
// Маппим проведенные
for _, inv := range invoicesList {
var sum decimal.Decimal
for _, it := range inv.Items {
@@ -576,7 +609,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
}
val, _ := sum.Float64()
// Определяем IsAppCreated и PhotoURL через мапу
isAppCreated := false
photoURL := ""
if url, exists := photoMap[inv.ID]; exists {
@@ -584,7 +616,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
photoURL = url
}
// Формируем ItemsPreview для синхронизированных накладных
var itemsPreview string
if len(inv.Items) > 0 {
names := make([]string, 0, 3)
@@ -592,7 +623,6 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
if i >= 3 {
break
}
// Предполагаем, что Product подгружен, иначе нужно добавить Preload
if it.Product.Name != "" {
names = append(names, it.Product.Name)
}
@@ -616,19 +646,15 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
})
}
// Сортировка по дате накладной (desc)
// (Здесь можно добавить библиотеку sort или оставить как есть, если БД уже отсортировала части)
return result, nil
}
func (s *Service) GetInvoiceDetails(invoiceID, userID uuid.UUID) (*invoices.Invoice, string, error) {
// Получить накладную
inv, err := s.invoiceRepo.GetByID(invoiceID)
if err != nil {
return nil, "", err
}
// Проверить, что пользователь имеет доступ к серверу накладной
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, "", errors.New("нет активного сервера")
@@ -638,7 +664,6 @@ func (s *Service) GetInvoiceDetails(invoiceID, userID uuid.UUID) (*invoices.Invo
return nil, "", errors.New("накладная не принадлежит активному серверу")
}
// Попытаться найти черновик по rms_invoice_id
draft, err := s.draftRepo.GetByRMSInvoiceID(invoiceID)
if err != nil {
return nil, "", err

View File

@@ -21,7 +21,6 @@ func NewDraftsHandler(service *drafts.Service) *DraftsHandler {
return &DraftsHandler{service: service}
}
// GetDraft
func (h *DraftsHandler) GetDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
idStr := c.Param("id")
@@ -39,7 +38,6 @@ func (h *DraftsHandler) GetDraft(c *gin.Context) {
c.JSON(http.StatusOK, draft)
}
// GetDictionaries (бывший GetStores)
func (h *DraftsHandler) GetDictionaries(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
@@ -52,12 +50,9 @@ func (h *DraftsHandler) GetDictionaries(c *gin.Context) {
c.JSON(http.StatusOK, data)
}
// GetStores - устаревший метод для обратной совместимости
// Возвращает массив складов
func (h *DraftsHandler) GetStores(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
// Используем логику из GetDictionaries, но возвращаем только stores
dict, err := h.service.GetDictionaries(userID)
if err != nil {
logger.Log.Error("GetStores error", zap.Error(err))
@@ -65,19 +60,19 @@ func (h *DraftsHandler) GetStores(c *gin.Context) {
return
}
// dict["stores"] уже содержит []catalog.Store
c.JSON(http.StatusOK, dict["stores"])
}
// UpdateItemDTO
// UpdateItemDTO обновлен: float64 -> *float64, добавлен edited_field
type UpdateItemDTO struct {
ProductID *string `json:"product_id"`
ContainerID *string `json:"container_id"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
ProductID *string `json:"product_id"`
ContainerID *string `json:"container_id"`
Quantity *float64 `json:"quantity"`
Price *float64 `json:"price"`
Sum *float64 `json:"sum"`
EditedField string `json:"edited_field"` // "quantity", "price", "sum"
}
// AddDraftItem - POST /api/drafts/:id/items
func (h *DraftsHandler) AddDraftItem(c *gin.Context) {
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
@@ -95,7 +90,6 @@ func (h *DraftsHandler) AddDraftItem(c *gin.Context) {
c.JSON(http.StatusOK, item)
}
// DeleteDraftItem - DELETE /api/drafts/:id/items/:itemId
func (h *DraftsHandler) DeleteDraftItem(c *gin.Context) {
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
@@ -122,8 +116,8 @@ func (h *DraftsHandler) DeleteDraftItem(c *gin.Context) {
})
}
// UpdateItem обновлен
func (h *DraftsHandler) UpdateItem(c *gin.Context) {
// userID := c.MustGet("userID").(uuid.UUID) // Пока не используется в UpdateItem, но можно добавить проверку владельца
draftID, _ := uuid.Parse(c.Param("id"))
itemID, _ := uuid.Parse(c.Param("itemId"))
@@ -141,16 +135,44 @@ func (h *DraftsHandler) UpdateItem(c *gin.Context) {
}
var cID *uuid.UUID
if req.ContainerID != nil && *req.ContainerID != "" {
if uid, err := uuid.Parse(*req.ContainerID); err == nil {
if req.ContainerID != nil {
if *req.ContainerID == "" {
// Сброс фасовки
empty := uuid.Nil
cID = &empty
} else if uid, err := uuid.Parse(*req.ContainerID); err == nil {
cID = &uid
}
}
qty := decimal.NewFromFloat(req.Quantity)
price := decimal.NewFromFloat(req.Price)
qty := decimal.Zero
if req.Quantity != nil {
qty = decimal.NewFromFloat(*req.Quantity)
}
if err := h.service.UpdateItem(draftID, itemID, pID, cID, qty, price); err != nil {
price := decimal.Zero
if req.Price != nil {
price = decimal.NewFromFloat(*req.Price)
}
sum := decimal.Zero
if req.Sum != nil {
sum = decimal.NewFromFloat(*req.Sum)
}
// Дефолт, если фронт не прислал (для совместимости)
editedField := req.EditedField
if editedField == "" {
if req.Sum != nil {
editedField = "sum"
} else if req.Price != nil {
editedField = "price"
} else {
editedField = "quantity"
}
}
if err := h.service.UpdateItem(draftID, itemID, pID, cID, qty, price, sum, editedField); err != nil {
logger.Log.Error("Failed to update item", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -245,28 +267,14 @@ func (h *DraftsHandler) AddContainer(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "created", "container_id": newID.String()})
}
type DraftListItemDTO struct {
ID string `json:"id"`
DocumentNumber string `json:"document_number"`
DateIncoming string `json:"date_incoming"`
Status string `json:"status"`
ItemsCount int `json:"items_count"`
TotalSum float64 `json:"total_sum"`
StoreName string `json:"store_name"`
CreatedAt string `json:"created_at"`
IsAppCreated bool `json:"is_app_created"`
}
func (h *DraftsHandler) GetDrafts(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
// Читаем параметры периода из Query (default: 30 days)
fromStr := c.DefaultQuery("from", time.Now().AddDate(0, 0, -30).Format("2006-01-02"))
toStr := c.DefaultQuery("to", time.Now().Format("2006-01-02"))
from, _ := time.Parse("2006-01-02", fromStr)
to, _ := time.Parse("2006-01-02", toStr)
// Устанавливаем конец дня для 'to'
to = to.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
list, err := h.service.GetUnifiedList(userID, from, to)
@@ -279,7 +287,6 @@ func (h *DraftsHandler) GetDrafts(c *gin.Context) {
}
func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
// userID := c.MustGet("userID").(uuid.UUID) // Можно добавить проверку владельца
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {