Добавил черновики накладных и OCR через Яндекс. LLM для расшифровки универсальный

This commit is contained in:
2025-12-17 03:38:24 +03:00
parent fda30276a5
commit e2df2350f7
32 changed files with 1785 additions and 214 deletions

View File

@@ -0,0 +1,151 @@
package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/internal/services/drafts"
"rmser/pkg/logger"
)
type DraftsHandler struct {
service *drafts.Service
}
func NewDraftsHandler(service *drafts.Service) *DraftsHandler {
return &DraftsHandler{service: service}
}
// GetDraft возвращает полные данные черновика
func (h *DraftsHandler) GetDraft(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
draft, err := h.service.GetDraft(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "draft not found"})
return
}
c.JSON(http.StatusOK, draft)
}
// GetStores возвращает список складов
func (h *DraftsHandler) GetStores(c *gin.Context) {
stores, err := h.service.GetActiveStores()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stores)
}
// UpdateItemDTO - тело запроса на изменение строки
type UpdateItemDTO struct {
ProductID *string `json:"product_id"`
ContainerID *string `json:"container_id"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
}
func (h *DraftsHandler) UpdateItem(c *gin.Context) {
draftID, _ := uuid.Parse(c.Param("id"))
itemID, _ := uuid.Parse(c.Param("itemId"))
var req UpdateItemDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var pID *uuid.UUID
if req.ProductID != nil && *req.ProductID != "" {
if uid, err := uuid.Parse(*req.ProductID); err == nil {
pID = &uid
}
}
var cID *uuid.UUID
if req.ContainerID != nil && *req.ContainerID != "" {
if uid, err := uuid.Parse(*req.ContainerID); err == nil {
cID = &uid
}
}
qty := decimal.NewFromFloat(req.Quantity)
price := decimal.NewFromFloat(req.Price)
if err := h.service.UpdateItem(draftID, itemID, pID, cID, qty, price); err != nil {
logger.Log.Error("Failed to update item", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
type CommitRequestDTO struct {
DateIncoming string `json:"date_incoming"` // YYYY-MM-DD
StoreID string `json:"store_id"`
SupplierID string `json:"supplier_id"`
Comment string `json:"comment"`
}
// CommitDraft сохраняет шапку и отправляет в RMS
func (h *DraftsHandler) CommitDraft(c *gin.Context) {
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
var req CommitRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Парсинг данных шапки
date, err := time.Parse("2006-01-02", req.DateIncoming)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format (YYYY-MM-DD)"})
return
}
storeID, err := uuid.Parse(req.StoreID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid store id"})
return
}
supplierID, err := uuid.Parse(req.SupplierID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid supplier id"})
return
}
// 1. Обновляем шапку
if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()})
return
}
// 2. Отправляем
docNum, err := h.service.CommitDraft(draftID)
if err != nil {
logger.Log.Error("Commit failed", zap.Error(err))
c.JSON(http.StatusBadGateway, gin.H{"error": "RMS error: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "completed",
"document_number": docNum,
})
}

View File

@@ -73,6 +73,25 @@ func (h *OCRHandler) SaveMatch(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "saved"})
}
// DeleteMatch удаляет связь
func (h *OCRHandler) DeleteMatch(c *gin.Context) {
// Получаем raw_name из query параметров, так как в URL path могут быть спецсимволы
// Пример: DELETE /api/ocr/match?raw_name=Хлеб%20Бородинский
rawName := c.Query("raw_name")
if rawName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "raw_name is required"})
return
}
if err := h.service.DeleteMatch(rawName); err != nil {
logger.Log.Error("Ошибка удаления матча", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}
// GetMatches возвращает список всех обученных связей
func (h *OCRHandler) GetMatches(c *gin.Context) {
matches, err := h.service.GetKnownMatches()