3001-фух.это был сильный спринт.

сервис стал сильно лучше
черновики сохраняются одним запросом
дто черновиков вынесен отдельно
This commit is contained in:
2026-01-30 02:24:26 +03:00
parent 4da5fdd130
commit 10882f55c8
8 changed files with 306 additions and 51 deletions

View File

@@ -0,0 +1,19 @@
package drafts
// UpdateDraftRequest DTO для пакетного обновления черновика
type UpdateDraftRequest struct {
DateIncoming *string `json:"date_incoming"` // YYYY-MM-DD
StoreID *string `json:"store_id"` // UUID или пустая строка для сброса
SupplierID *string `json:"supplier_id"` // UUID или пустая строка для сброса
Comment *string `json:"comment"`
IncomingDocumentNumber *string `json:"incoming_document_number"`
Items []struct {
ID *string `json:"id"` // Обязательный ID строки
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"
} `json:"items"`
}

View File

@@ -958,3 +958,205 @@ func (s *Service) ReorderItems(draftID uuid.UUID, items []struct {
// Вызываем метод репозитория для обновления порядка
return s.draftRepo.ReorderItems(draftID, items)
}
// SaveDraftFull обновляет черновик (шапку и позиции) пакетно в одной транзакции
func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraftRequest) error {
// Получаем черновик для проверки прав доступа
draft, err := s.GetDraft(draftID, userID)
if err != nil {
return err
}
// Проверяем, что черновик не завершен
if draft.Status == drafts.StatusCompleted {
return errors.New("черновик уже отправлен")
}
// Обновляем шапку черновика, если переданы поля
headerUpdated := false
// 1. Дата (Обязательное поле, не может быть nil)
if req.DateIncoming != nil && *req.DateIncoming != "" {
parsedDate, err := time.Parse("2006-01-02", *req.DateIncoming)
if err != nil {
return fmt.Errorf("invalid date format: %w", err)
}
draft.DateIncoming = &parsedDate
headerUpdated = true
}
// 2. Склад (Может быть nil/сброшен)
if req.StoreID != nil {
if *req.StoreID == "" {
draft.StoreID = nil
} else {
uid, err := uuid.Parse(*req.StoreID)
if err != nil {
return fmt.Errorf("invalid store_id: %w", err)
}
draft.StoreID = &uid
}
headerUpdated = true
}
// 3. Поставщик (Может быть nil/сброшен)
if req.SupplierID != nil {
if *req.SupplierID == "" {
draft.SupplierID = nil
} else {
uid, err := uuid.Parse(*req.SupplierID)
if err != nil {
return fmt.Errorf("invalid supplier_id: %w", err)
}
draft.SupplierID = &uid
}
headerUpdated = true
}
// 4. Комментарий
if req.Comment != nil {
draft.Comment = *req.Comment
headerUpdated = true
}
// 5. Входящий номер
if req.IncomingDocumentNumber != nil {
draft.IncomingDocumentNumber = *req.IncomingDocumentNumber
headerUpdated = true
}
// Если были изменения в шапке — сохраняем
if headerUpdated {
if err := s.draftRepo.Update(draft); err != nil {
return fmt.Errorf("failed to update draft header: %w", err)
}
}
// Обновляем позиции, если переданы
if len(req.Items) > 0 {
for _, itemReq := range req.Items {
if itemReq.ID == nil || *itemReq.ID == "" {
return errors.New("item id is required")
}
itemID, err := uuid.Parse(*itemReq.ID)
if err != nil {
return fmt.Errorf("invalid item id: %s", *itemReq.ID)
}
// Получаем текущую позицию
currentItem, err := s.draftRepo.GetItemByID(itemID)
if err != nil {
return fmt.Errorf("item not found: %s", itemID.String())
}
// Проверяем, что позиция принадлежит черновику
if currentItem.DraftID != draftID {
return fmt.Errorf("item %s does not belong to draft %s", itemID.String(), draftID.String())
}
// Обновляем поля позиции
if itemReq.ProductID != nil {
if *itemReq.ProductID == "" {
currentItem.ProductID = nil
currentItem.IsMatched = false
currentItem.ContainerID = nil // Если убрали товар, фасовку тоже надо обнулить
} else {
parsedID, err := uuid.Parse(*itemReq.ProductID)
if err != nil {
return fmt.Errorf("invalid product_id for item %s", itemID.String())
}
currentItem.ProductID = &parsedID
currentItem.IsMatched = true
}
}
if itemReq.ContainerID != nil {
if *itemReq.ContainerID == "" {
// Сброс фасовки
currentItem.ContainerID = nil
} else {
parsedID, err := uuid.Parse(*itemReq.ContainerID)
if err != nil {
return fmt.Errorf("invalid container_id for item %s", itemID.String())
}
currentItem.ContainerID = &parsedID
}
}
// Определяем, какое поле редактируется
editedField := itemReq.EditedField
if editedField == "" {
if itemReq.Sum != nil {
editedField = "sum"
} else if itemReq.Price != nil {
editedField = "price"
} else if itemReq.Quantity != nil {
editedField = "quantity"
}
}
// Обновляем числовые поля
qty := decimal.Zero
if itemReq.Quantity != nil {
qty = decimal.NewFromFloat(*itemReq.Quantity)
} else {
qty = currentItem.Quantity
}
price := decimal.Zero
if itemReq.Price != nil {
price = decimal.NewFromFloat(*itemReq.Price)
} else {
price = currentItem.Price
}
sum := decimal.Zero
if itemReq.Sum != nil {
sum = decimal.NewFromFloat(*itemReq.Sum)
} else {
sum = currentItem.Sum
}
// Применяем изменения в зависимости от редактируемого поля
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
if err := s.draftRepo.Update(draft); err != nil {
return fmt.Errorf("failed to update draft status: %w", err)
}
}
// Сохраняем обновленную позицию
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,
}
if err := s.draftRepo.UpdateItem(itemID, updates); err != nil {
return fmt.Errorf("failed to update item %s: %w", itemID.String(), err)
}
}
}
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/shopspring/decimal"
"go.uber.org/zap"
draftsdomain "rmser/internal/domain/drafts"
"rmser/internal/services/drafts"
"rmser/internal/services/ocr"
"rmser/pkg/logger"
@@ -361,6 +362,30 @@ func (h *DraftsHandler) ReorderItems(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UpdateDraft обновляет черновик (шапку и позиции) пакетно
func (h *DraftsHandler) UpdateDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
var req draftsdomain.UpdateDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.SaveDraftFull(draftID, userID, req); err != nil {
logger.Log.Error("Failed to update draft", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось сохранить изменения. Проверьте данные."})
return
}
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
// Upload обрабатывает загрузку файла и прогоняет через OCR
func (h *DraftsHandler) Upload(c *gin.Context) {
// Лимит размера тела запроса (20MB)