Добавил черновики накладных и 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

@@ -7,6 +7,7 @@ import (
"os"
"regexp"
"rmser/internal/domain/catalog"
"rmser/internal/domain/drafts"
"rmser/internal/domain/invoices"
"rmser/internal/domain/ocr"
"rmser/internal/domain/operations"
@@ -48,10 +49,13 @@ func NewPostgresDB(dsn string) *gorm.DB {
&catalog.Product{},
&catalog.MeasureUnit{},
&catalog.ProductContainer{},
&catalog.Store{},
&recipes.Recipe{},
&recipes.RecipeItem{},
&invoices.Invoice{},
&invoices.InvoiceItem{},
&drafts.DraftInvoice{},
&drafts.DraftInvoiceItem{},
&operations.StoreOperation{},
&recommendations.Recommendation{},
&ocr.ProductMatch{},

View File

@@ -116,3 +116,19 @@ func (r *pgRepository) GetActiveGoods() ([]catalog.Product, error) {
Find(&products).Error
return products, err
}
func (r *pgRepository) SaveStores(stores []catalog.Store) error {
if len(stores) == 0 {
return nil
}
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).CreateInBatches(stores, 100).Error
}
func (r *pgRepository) GetActiveStores() ([]catalog.Store, error) {
var stores []catalog.Store
err := r.db.Where("is_deleted = ?", false).Order("name ASC").Find(&stores).Error
return stores, err
}

View File

@@ -0,0 +1,85 @@
package drafts
import (
"rmser/internal/domain/drafts"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type pgRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) drafts.Repository {
return &pgRepository{db: db}
}
func (r *pgRepository) Create(draft *drafts.DraftInvoice) error {
return r.db.Create(draft).Error
}
func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) {
var draft drafts.DraftInvoice
err := r.db.
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("draft_invoice_items.raw_name ASC")
}).
Preload("Items.Product").
Preload("Items.Product.MainUnit"). // Нужно для отображения единиц
Preload("Items.Container").
Where("id = ?", id).
First(&draft).Error
if err != nil {
return nil, err
}
return &draft, nil
}
func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
// Обновляем только основные поля шапки
return r.db.Model(draft).Updates(map[string]interface{}{
"status": draft.Status,
"document_number": draft.DocumentNumber,
"date_incoming": draft.DateIncoming,
"supplier_id": draft.SupplierID,
"store_id": draft.StoreID,
"comment": draft.Comment,
"rms_invoice_id": draft.RMSInvoiceID,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
func (r *pgRepository) CreateItems(items []drafts.DraftInvoiceItem) error {
if len(items) == 0 {
return nil
}
return r.db.CreateInBatches(items, 100).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 задан - значит сматчено
isMatched := productID != nil
updates := map[string]interface{}{
"product_id": productID,
"container_id": containerID,
"quantity": qty,
"price": price,
"sum": sum,
"is_matched": isMatched,
}
return r.db.Model(&drafts.DraftInvoiceItem{}).
Where("id = ?", itemID).
Updates(updates).Error
}
func (r *pgRepository) Delete(id uuid.UUID) error {
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
}

View File

@@ -45,6 +45,11 @@ func (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID, quantity d
})
}
func (r *pgRepository) DeleteMatch(rawName string) error {
normalized := strings.ToLower(strings.TrimSpace(rawName))
return r.db.Where("raw_name = ?", normalized).Delete(&ocr.ProductMatch{}).Error
}
func (r *pgRepository) FindMatch(rawName string) (*ocr.ProductMatch, error) {
normalized := strings.ToLower(strings.TrimSpace(rawName))
var match ocr.ProductMatch

View File

@@ -32,6 +32,7 @@ type ClientI interface {
Auth() error
Logout() error
FetchCatalog() ([]catalog.Product, error)
FetchStores() ([]catalog.Store, error)
FetchMeasureUnits() ([]catalog.MeasureUnit, error)
FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error)
FetchInvoices(from, to time.Time) ([]invoices.Invoice, error)
@@ -337,6 +338,52 @@ func (c *Client) FetchCatalog() ([]catalog.Product, error) {
return products, nil
}
// FetchStores загружает список складов (Account -> INVENTORY_ASSETS)
func (c *Client) FetchStores() ([]catalog.Store, error) {
resp, err := c.doRequest("GET", "/resto/api/v2/entities/list", map[string]string{
"rootType": "Account",
"includeDeleted": "false",
})
if err != nil {
return nil, fmt.Errorf("get stores error: %w", err)
}
defer resp.Body.Close()
var dtos []AccountDTO
if err := json.NewDecoder(resp.Body).Decode(&dtos); err != nil {
return nil, fmt.Errorf("json decode stores error: %w", err)
}
var stores []catalog.Store
for _, d := range dtos {
// Фильтруем только склады
if d.Type != "INVENTORY_ASSETS" {
continue
}
id, err := uuid.Parse(d.ID)
if err != nil {
continue
}
var parentCorpID uuid.UUID
if d.ParentCorporateID != nil {
if parsed, err := uuid.Parse(*d.ParentCorporateID); err == nil {
parentCorpID = parsed
}
}
stores = append(stores, catalog.Store{
ID: id,
Name: d.Name,
ParentCorporateID: parentCorpID,
IsDeleted: d.Deleted,
})
}
return stores, nil
}
// FetchMeasureUnits загружает справочник единиц измерения
func (c *Client) FetchMeasureUnits() ([]catalog.MeasureUnit, error) {
// rootType=MeasureUnit согласно документации iiko

View File

@@ -28,6 +28,15 @@ type GenericEntityDTO struct {
Deleted bool `json:"deleted"`
}
// AccountDTO используется для парсинга складов (INVENTORY_ASSETS)
type AccountDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // Нас интересует "INVENTORY_ASSETS"
ParentCorporateID *string `json:"parentCorporateId"`
Deleted bool `json:"deleted"`
}
// ContainerDTO - фасовка из iiko
type ContainerDTO struct {
ID string `json:"id"`