diff --git a/cmd/main.go b/cmd/main.go index bc085dd..799d545 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,7 @@ import ( // Services billingServicePkg "rmser/internal/services/billing" draftsServicePkg "rmser/internal/services/drafts" + invoicesServicePkg "rmser/internal/services/invoices" ocrServicePkg "rmser/internal/services/ocr" recServicePkg "rmser/internal/services/recommend" "rmser/internal/services/sync" @@ -93,6 +94,7 @@ func main() { recService := recServicePkg.NewService(recRepo) ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, pyClient, cfg.App.StoragePath) draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, invoicesRepo, rmsFactory, billingService) + invoicesService := invoicesServicePkg.NewService(invoicesRepo, draftsRepo, supplierRepo, rmsFactory) // 7. Handlers draftsHandler := handlers.NewDraftsHandler(draftsService) @@ -100,6 +102,7 @@ func main() { ocrHandler := handlers.NewOCRHandler(ocrService) recommendHandler := handlers.NewRecommendationsHandler(recService) settingsHandler := handlers.NewSettingsHandler(accountRepo, catalogRepo) + invoicesHandler := handlers.NewInvoiceHandler(invoicesService) // 8. Telegram Bot (Передаем syncService) if cfg.Telegram.Token != "" { @@ -168,8 +171,12 @@ func main() { api.POST("/ocr/match", ocrHandler.SaveMatch) api.DELETE("/ocr/match", ocrHandler.DeleteMatch) api.GET("/ocr/unmatched", ocrHandler.GetUnmatched) + api.DELETE("/ocr/unmatched", ocrHandler.DiscardUnmatched) api.GET("/ocr/search", ocrHandler.SearchProducts) + // Invoices + api.GET("/invoices/:id", invoicesHandler.GetInvoice) + // Manual Sync Trigger api.POST("/sync/all", func(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) diff --git a/go.mod b/go.mod index 73834d6..484cb5c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module rmser -go 1.25.1 +go 1.25.5 require ( github.com/gin-contrib/cors v1.7.6 diff --git a/internal/domain/drafts/entity.go b/internal/domain/drafts/entity.go index a320fe0..7ef5639 100644 --- a/internal/domain/drafts/entity.go +++ b/internal/domain/drafts/entity.go @@ -28,9 +28,11 @@ type DraftInvoice struct { SenderPhotoURL string `gorm:"type:text" json:"photo_url"` Status string `gorm:"type:varchar(50);default:'PROCESSING'" json:"status"` - DocumentNumber string `gorm:"type:varchar(100)" json:"document_number"` - DateIncoming *time.Time `json:"date_incoming"` - SupplierID *uuid.UUID `gorm:"type:uuid" json:"supplier_id"` + DocumentNumber string `gorm:"type:varchar(100)" json:"document_number"` + // Входящий номер документа + IncomingDocumentNumber string `gorm:"type:varchar(100)" json:"incoming_document_number"` + DateIncoming *time.Time `json:"date_incoming"` + SupplierID *uuid.UUID `gorm:"type:uuid" json:"supplier_id"` StoreID *uuid.UUID `gorm:"type:uuid" json:"store_id"` Store *catalog.Store `gorm:"foreignKey:StoreID" json:"store,omitempty"` @@ -67,6 +69,7 @@ type DraftInvoiceItem struct { type Repository interface { Create(draft *DraftInvoice) error GetByID(id uuid.UUID) (*DraftInvoice, error) + GetByRMSInvoiceID(rmsInvoiceID uuid.UUID) (*DraftInvoice, error) Update(draft *DraftInvoice) error CreateItems(items []DraftInvoiceItem) error UpdateItem(itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error @@ -76,4 +79,7 @@ type Repository interface { // GetActive возвращает активные черновики для СЕРВЕРА (а не юзера) GetActive(serverID uuid.UUID) ([]DraftInvoice, error) + + // GetRMSInvoiceIDToPhotoURLMap возвращает мапу rms_invoice_id -> sender_photo_url для сервера, где rms_invoice_id не NULL + GetRMSInvoiceIDToPhotoURLMap(serverID uuid.UUID) (map[uuid.UUID]string, error) } diff --git a/internal/domain/invoices/entity.go b/internal/domain/invoices/entity.go index 6947aba..32ec187 100644 --- a/internal/domain/invoices/entity.go +++ b/internal/domain/invoices/entity.go @@ -42,6 +42,7 @@ type InvoiceItem struct { } type Repository interface { + GetByID(id uuid.UUID) (*Invoice, error) GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error) GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]Invoice, error) SaveInvoices(invoices []Invoice) error diff --git a/internal/domain/suppliers/entity.go b/internal/domain/suppliers/entity.go index 6752b4c..8dac126 100644 --- a/internal/domain/suppliers/entity.go +++ b/internal/domain/suppliers/entity.go @@ -23,6 +23,7 @@ type Supplier struct { type Repository interface { SaveBatch(suppliers []Supplier) error + GetByID(id uuid.UUID) (*Supplier, error) // GetRankedByUsage возвращает поставщиков, отсортированных по частоте использования в накладных за N дней GetRankedByUsage(serverID uuid.UUID, daysLookBack int) ([]Supplier, error) Count(serverID uuid.UUID) (int64, error) diff --git a/internal/infrastructure/repository/drafts/postgres.go b/internal/infrastructure/repository/drafts/postgres.go index 356bb66..61a5b6e 100644 --- a/internal/infrastructure/repository/drafts/postgres.go +++ b/internal/infrastructure/repository/drafts/postgres.go @@ -38,17 +38,32 @@ func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) { return &draft, nil } +func (r *pgRepository) GetByRMSInvoiceID(rmsInvoiceID uuid.UUID) (*drafts.DraftInvoice, error) { + var draft drafts.DraftInvoice + err := r.db. + Where("rms_invoice_id = ?", rmsInvoiceID). + First(&draft).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, 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, - "rms_server_id": draft.RMSServerID, - "updated_at": gorm.Expr("NOW()"), + "status": draft.Status, + "document_number": draft.DocumentNumber, + "incoming_document_number": draft.IncomingDocumentNumber, // Добавлено поле для входящего номера документа + "date_incoming": draft.DateIncoming, + "supplier_id": draft.SupplierID, + "store_id": draft.StoreID, + "comment": draft.Comment, + "rms_invoice_id": draft.RMSInvoiceID, + "rms_server_id": draft.RMSServerID, + "updated_at": gorm.Expr("NOW()"), }).Error } @@ -107,3 +122,23 @@ func (r *pgRepository) GetActive(serverID uuid.UUID) ([]drafts.DraftInvoice, err 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. + Select("rms_invoice_id", "sender_photo_url"). + Where("rms_server_id = ? AND rms_invoice_id IS NOT NULL", serverID). + Find(&draftsList).Error + if err != nil { + return nil, err + } + + result := make(map[uuid.UUID]string) + for _, d := range draftsList { + if d.RMSInvoiceID != nil { + result[*d.RMSInvoiceID] = d.SenderPhotoURL + } + } + return result, nil +} diff --git a/internal/infrastructure/repository/invoices/postgres.go b/internal/infrastructure/repository/invoices/postgres.go index aff9cb5..4782502 100644 --- a/internal/infrastructure/repository/invoices/postgres.go +++ b/internal/infrastructure/repository/invoices/postgres.go @@ -18,6 +18,19 @@ func NewRepository(db *gorm.DB) invoices.Repository { return &pgRepository{db: db} } +func (r *pgRepository) GetByID(id uuid.UUID) (*invoices.Invoice, error) { + var inv invoices.Invoice + err := r.db. + Preload("Items"). + Preload("Items.Product"). + Where("id = ?", id). + First(&inv).Error + if err != nil { + return nil, err + } + return &inv, nil +} + func (r *pgRepository) GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error) { var inv invoices.Invoice // Ищем последнюю накладную только для этого сервера @@ -35,7 +48,8 @@ func (r *pgRepository) GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]in var list []invoices.Invoice err := r.db. Preload("Items"). - Where("rms_server_id = ? AND date_incoming BETWEEN ? AND ?", serverID, from, to). + Preload("Items.Product"). + Where("rms_server_id = ? AND date_incoming BETWEEN ? AND ? AND status != ?", serverID, from, to, "DELETED"). Order("date_incoming DESC"). Find(&list).Error return list, err diff --git a/internal/infrastructure/repository/ocr/postgres.go b/internal/infrastructure/repository/ocr/postgres.go index 72824c1..e045cc3 100644 --- a/internal/infrastructure/repository/ocr/postgres.go +++ b/internal/infrastructure/repository/ocr/postgres.go @@ -81,6 +81,7 @@ func (r *pgRepository) GetAllMatches(serverID uuid.UUID) ([]ocr.ProductMatch, er err := r.db. Preload("Product"). Preload("Product.MainUnit"). + Preload("Product.Containers"). Preload("Container"). Where("rms_server_id = ?", serverID). Order("updated_at DESC"). diff --git a/internal/infrastructure/repository/suppliers/postgres.go b/internal/infrastructure/repository/suppliers/postgres.go index 99263b6..2e73b0b 100644 --- a/internal/infrastructure/repository/suppliers/postgres.go +++ b/internal/infrastructure/repository/suppliers/postgres.go @@ -51,6 +51,15 @@ func (r *pgRepository) GetRankedByUsage(serverID uuid.UUID, daysLookBack int) ([ return result, err } +func (r *pgRepository) GetByID(id uuid.UUID) (*suppliers.Supplier, error) { + var supplier suppliers.Supplier + err := r.db.Where("id = ? AND is_deleted = ?", id, false).First(&supplier).Error + if err != nil { + return nil, err + } + return &supplier, nil +} + func (r *pgRepository) Count(serverID uuid.UUID) (int64, error) { var count int64 err := r.db.Model(&suppliers.Supplier{}). diff --git a/internal/infrastructure/rms/client.go b/internal/infrastructure/rms/client.go index 768167c..54b1ae5 100644 --- a/internal/infrastructure/rms/client.go +++ b/internal/infrastructure/rms/client.go @@ -572,12 +572,13 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) { } reqDTO := IncomingInvoiceImportXML{ - DocumentNumber: inv.DocumentNumber, - DateIncoming: inv.DateIncoming.Format("02.01.2006"), - DefaultStore: inv.DefaultStoreID.String(), - Supplier: inv.SupplierID.String(), - Status: status, - Comment: comment, + DocumentNumber: inv.DocumentNumber, + IncomingDocumentNumber: inv.IncomingDocumentNumber, // Присваиваем входящий номер документа из домена + DateIncoming: inv.DateIncoming.Format("02.01.2006"), + DefaultStore: inv.DefaultStoreID.String(), + Supplier: inv.SupplierID.String(), + Status: status, + Comment: comment, } if inv.ID != uuid.Nil { diff --git a/internal/infrastructure/rms/dto.go b/internal/infrastructure/rms/dto.go index 4850c62..859e846 100644 --- a/internal/infrastructure/rms/dto.go +++ b/internal/infrastructure/rms/dto.go @@ -195,10 +195,11 @@ type StoreReportItemXML struct { // IncomingInvoiceImportXML описывает структуру для POST запроса импорта type IncomingInvoiceImportXML struct { - XMLName xml.Name `xml:"document"` - ID string `xml:"id,omitempty"` // GUID, если редактируем - DocumentNumber string `xml:"documentNumber,omitempty"` - DateIncoming string `xml:"dateIncoming,omitempty"` // Format: dd.MM.yyyy + XMLName xml.Name `xml:"document"` + ID string `xml:"id,omitempty"` // GUID, если редактируем + DocumentNumber string `xml:"documentNumber,omitempty"` + IncomingDocumentNumber string `xml:"incomingDocumentNumber,omitempty"` // Входящий номер документа + DateIncoming string `xml:"dateIncoming,omitempty"` // Format: dd.MM.yyyy Invoice string `xml:"invoice,omitempty"` // Номер счет-фактуры DefaultStore string `xml:"defaultStore"` // GUID склада (обязательно) Supplier string `xml:"supplier"` // GUID поставщика (обязательно) diff --git a/internal/services/drafts/service.go b/internal/services/drafts/service.go index f7fdc5f..f9f2898 100644 --- a/internal/services/drafts/service.go +++ b/internal/services/drafts/service.go @@ -152,7 +152,7 @@ func (s *Service) DeleteDraft(id uuid.UUID) (string, error) { return draft.Status, nil } -func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID *uuid.UUID, date time.Time, comment string) error { +func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID *uuid.UUID, date time.Time, comment string, incomingDocNum string) error { draft, err := s.draftRepo.GetByID(id) if err != nil { return err @@ -164,6 +164,7 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID draft.SupplierID = supplierID draft.DateIncoming = &date draft.Comment = comment + draft.IncomingDocumentNumber = incomingDocNum return s.draftRepo.Update(draft) } @@ -270,14 +271,15 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) { // 4. Сборка Invoice inv := invoices.Invoice{ - ID: uuid.Nil, - DocumentNumber: draft.DocumentNumber, - DateIncoming: *draft.DateIncoming, - SupplierID: *draft.SupplierID, - DefaultStoreID: *draft.StoreID, - Status: targetStatus, - Comment: draft.Comment, - Items: make([]invoices.InvoiceItem, 0, len(draft.Items)), + ID: uuid.Nil, + DocumentNumber: draft.DocumentNumber, + DateIncoming: *draft.DateIncoming, + SupplierID: *draft.SupplierID, + DefaultStoreID: *draft.StoreID, + Status: targetStatus, + Comment: draft.Comment, + IncomingDocumentNumber: draft.IncomingDocumentNumber, + Items: make([]invoices.InvoiceItem, 0, len(draft.Items)), } for _, dItem := range draft.Items { @@ -338,7 +340,25 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) { return "", err } - // 6. Обновление статуса черновика + // 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)) + } else { + found := false + for _, invoice := range invoices { + if invoice.DocumentNumber == docNum { + draft.RMSInvoiceID = &invoice.ID + found = true + break + } + } + if !found { + logger.Log.Warn("UUID созданной накладной не найден", zap.String("document_number", docNum), zap.Time("date", *draft.DateIncoming)) + } + } + + // 7. Обновление статуса черновика draft.Status = drafts.StatusCompleted s.draftRepo.Update(draft) @@ -471,6 +491,8 @@ type UnifiedInvoiceDTO struct { ItemsCount int `json:"items_count"` CreatedAt time.Time `json:"created_at"` IsAppCreated bool `json:"is_app_created"` + PhotoURL string `json:"photo_url"` + ItemsPreview string `json:"items_preview"` } func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) { @@ -491,6 +513,12 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie return nil, err } + // 3. Получаем мапу rms_invoice_id -> sender_photo_url + photoMap, err := s.draftRepo.GetRMSInvoiceIDToPhotoURLMap(server.ID) + if err != nil { + return nil, err + } + result := make([]UnifiedInvoiceDTO, 0, len(draftsList)+len(invoicesList)) // Маппим черновики @@ -510,6 +538,19 @@ 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) + for i, it := range d.Items { + if i >= 3 { + break + } + names = append(names, it.RawName) + } + itemsPreview = strings.Join(names, ", ") + } + result = append(result, UnifiedInvoiceDTO{ ID: d.ID, Type: "DRAFT", @@ -522,6 +563,8 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie ItemsCount: len(d.Items), CreatedAt: d.CreatedAt, IsAppCreated: true, + PhotoURL: d.SenderPhotoURL, + ItemsPreview: itemsPreview, }) } @@ -533,7 +576,29 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie } val, _ := sum.Float64() - isOurs := strings.Contains(strings.ToUpper(inv.Comment), "RMSER") + // Определяем IsAppCreated и PhotoURL через мапу + isAppCreated := false + photoURL := "" + if url, exists := photoMap[inv.ID]; exists { + isAppCreated = true + photoURL = url + } + + // Формируем ItemsPreview для синхронизированных накладных + var itemsPreview string + if len(inv.Items) > 0 { + names := make([]string, 0, 3) + for i, it := range inv.Items { + if i >= 3 { + break + } + // Предполагаем, что Product подгружен, иначе нужно добавить Preload + if it.Product.Name != "" { + names = append(names, it.Product.Name) + } + } + itemsPreview = strings.Join(names, ", ") + } result = append(result, UnifiedInvoiceDTO{ ID: inv.ID, @@ -545,7 +610,9 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie TotalSum: val, ItemsCount: len(inv.Items), CreatedAt: inv.CreatedAt, - IsAppCreated: isOurs, + IsAppCreated: isAppCreated, + PhotoURL: photoURL, + ItemsPreview: itemsPreview, }) } @@ -553,3 +620,34 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie // (Здесь можно добавить библиотеку 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("нет активного сервера") + } + + if inv.RMSServerID != server.ID { + return nil, "", errors.New("накладная не принадлежит активному серверу") + } + + // Попытаться найти черновик по rms_invoice_id + draft, err := s.draftRepo.GetByRMSInvoiceID(invoiceID) + if err != nil { + return nil, "", err + } + + photoURL := "" + if draft != nil { + photoURL = draft.SenderPhotoURL + } + + return inv, photoURL, nil +} diff --git a/internal/services/invoices/service.go b/internal/services/invoices/service.go index 8d5cbe2..b1dd549 100644 --- a/internal/services/invoices/service.go +++ b/internal/services/invoices/service.go @@ -8,20 +8,28 @@ import ( "github.com/shopspring/decimal" "go.uber.org/zap" - "rmser/internal/domain/invoices" + "rmser/internal/domain/drafts" + invDomain "rmser/internal/domain/invoices" + "rmser/internal/domain/suppliers" "rmser/internal/infrastructure/rms" "rmser/pkg/logger" ) type Service struct { - rmsClient rms.ClientI + repo invDomain.Repository + draftsRepo drafts.Repository + supplierRepo suppliers.Repository + rmsFactory *rms.Factory // Здесь можно добавить репозитории каталога и контрагентов для валидации, // но для краткости пока опустим глубокую валидацию. } -func NewService(rmsClient rms.ClientI) *Service { +func NewService(repo invDomain.Repository, draftsRepo drafts.Repository, supplierRepo suppliers.Repository, rmsFactory *rms.Factory) *Service { return &Service{ - rmsClient: rmsClient, + repo: repo, + draftsRepo: draftsRepo, + supplierRepo: supplierRepo, + rmsFactory: rmsFactory, } } @@ -39,7 +47,7 @@ type CreateRequestDTO struct { } // SendInvoiceToRMS валидирует DTO, собирает доменную модель и отправляет в RMS -func (s *Service) SendInvoiceToRMS(req CreateRequestDTO) (string, error) { +func (s *Service) SendInvoiceToRMS(req CreateRequestDTO, userID uuid.UUID) (string, error) { // 1. Базовая валидация if len(req.Items) == 0 { return "", fmt.Errorf("список товаров пуст") @@ -51,20 +59,20 @@ func (s *Service) SendInvoiceToRMS(req CreateRequestDTO) (string, error) { } // 2. Сборка доменной модели - inv := invoices.Invoice{ + inv := invDomain.Invoice{ ID: uuid.Nil, // Новый документ DocumentNumber: req.DocumentNumber, DateIncoming: dateInc, SupplierID: req.SupplierID, DefaultStoreID: req.StoreID, Status: "NEW", - Items: make([]invoices.InvoiceItem, 0, len(req.Items)), + Items: make([]invDomain.InvoiceItem, 0, len(req.Items)), } for _, itemDTO := range req.Items { sum := itemDTO.Amount.Mul(itemDTO.Price) // Пересчитываем сумму - inv.Items = append(inv.Items, invoices.InvoiceItem{ + inv.Items = append(inv.Items, invDomain.InvoiceItem{ ProductID: itemDTO.ProductID, Amount: itemDTO.Amount, Price: itemDTO.Price, @@ -72,15 +80,89 @@ func (s *Service) SendInvoiceToRMS(req CreateRequestDTO) (string, error) { }) } - // 3. Отправка через клиент + // 3. Получение клиента RMS + client, err := s.rmsFactory.GetClientForUser(userID) + if err != nil { + return "", fmt.Errorf("ошибка получения клиента RMS: %w", err) + } + + // 4. Отправка через клиент logger.Log.Info("Отправка накладной в RMS", zap.String("supplier", req.SupplierID.String()), zap.Int("items_count", len(inv.Items))) - docNum, err := s.rmsClient.CreateIncomingInvoice(inv) + docNum, err := client.CreateIncomingInvoice(inv) if err != nil { return "", err } return docNum, nil } + +// InvoiceDetailsDTO - DTO для ответа на запрос деталей накладной +type InvoiceDetailsDTO struct { + ID uuid.UUID `json:"id"` + Number string `json:"number"` + Date string `json:"date"` + Status string `json:"status"` + Supplier struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + } `json:"supplier"` + Items []struct { + Name string `json:"name"` + Quantity float64 `json:"quantity"` + Price float64 `json:"price"` + Total float64 `json:"total"` + } `json:"items"` + PhotoURL *string `json:"photo_url"` +} + +// GetInvoice возвращает детали синхронизированной накладной по ID +func (s *Service) GetInvoice(id uuid.UUID) (*InvoiceDetailsDTO, error) { + // 1. Получить накладную из репозитория + inv, err := s.repo.GetByID(id) + if err != nil { + return nil, fmt.Errorf("ошибка получения накладной: %w", err) + } + + // 2. Получить поставщика + supplier, err := s.supplierRepo.GetByID(inv.SupplierID) + if err != nil { + return nil, fmt.Errorf("ошибка получения поставщика: %w", err) + } + + // 3. Проверить, есть ли draft с photo_url + var photoURL *string + draft, err := s.draftsRepo.GetByRMSInvoiceID(inv.ID) + if err == nil && draft != nil { + photoURL = &draft.SenderPhotoURL + } + + // 4. Собрать DTO + dto := &InvoiceDetailsDTO{ + ID: inv.ID, + Number: inv.DocumentNumber, + Date: inv.DateIncoming.Format("2006-01-02"), + Status: "COMPLETED", // Для синхронизированных накладных статус всегда COMPLETED + Items: make([]struct { + Name string `json:"name"` + Quantity float64 `json:"quantity"` + Price float64 `json:"price"` + Total float64 `json:"total"` + }, len(inv.Items)), + PhotoURL: photoURL, + } + + dto.Supplier.ID = supplier.ID + dto.Supplier.Name = supplier.Name + + for i, item := range inv.Items { + dto.Items[i].Name = item.Product.Name + dto.Items[i].Quantity, _ = item.Amount.Float64() + dto.Items[i].Price, _ = item.Price.Float64() + dto.Items[i].Total, _ = item.Sum.Float64() + } + + return dto, nil +} diff --git a/internal/services/ocr/service.go b/internal/services/ocr/service.go index 0070d1d..8f99dc4 100644 --- a/internal/services/ocr/service.go +++ b/internal/services/ocr/service.go @@ -249,3 +249,14 @@ func (s *Service) GetUnmatchedItems(userID uuid.UUID) ([]ocr.UnmatchedItem, erro } return s.ocrRepo.GetTopUnmatched(server.ID, 50) } + +func (s *Service) DiscardUnmatched(userID uuid.UUID, rawName string) error { + server, err := s.accountRepo.GetActiveServer(userID) + if err != nil || server == nil { + return fmt.Errorf("no server") + } + if err := s.checkWriteAccess(userID, server.ID); err != nil { + return err + } + return s.ocrRepo.DeleteUnmatched(server.ID, rawName) +} diff --git a/internal/transport/http/handlers/drafts.go b/internal/transport/http/handlers/drafts.go index 5ade21b..ff858b8 100644 --- a/internal/transport/http/handlers/drafts.go +++ b/internal/transport/http/handlers/drafts.go @@ -160,10 +160,11 @@ func (h *DraftsHandler) UpdateItem(c *gin.Context) { } 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"` + DateIncoming string `json:"date_incoming"` // YYYY-MM-DD + StoreID string `json:"store_id"` + SupplierID string `json:"supplier_id"` + Comment string `json:"comment"` + IncomingDocNum string `json:"incoming_doc_num"` } func (h *DraftsHandler) CommitDraft(c *gin.Context) { @@ -196,7 +197,7 @@ func (h *DraftsHandler) CommitDraft(c *gin.Context) { return } - if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment); err != nil { + if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment, req.IncomingDocNum); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()}) return } diff --git a/internal/transport/http/handlers/invoices.go b/internal/transport/http/handlers/invoices.go index 15bbd7a..caf3cf6 100644 --- a/internal/transport/http/handlers/invoices.go +++ b/internal/transport/http/handlers/invoices.go @@ -4,14 +4,17 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/google/uuid" "go.uber.org/zap" + "rmser/internal/services/drafts" invService "rmser/internal/services/invoices" "rmser/pkg/logger" ) type InvoiceHandler struct { - service *invService.Service + service *invService.Service + draftsService *drafts.Service } func NewInvoiceHandler(service *invService.Service) *InvoiceHandler { @@ -35,7 +38,8 @@ func (h *InvoiceHandler) SendInvoice(c *gin.Context) { return } - docNum, err := h.service.SendInvoiceToRMS(req) + userID := c.MustGet("userID").(uuid.UUID) + docNum, err := h.service.SendInvoiceToRMS(req, userID) if err != nil { logger.Log.Error("Ошибка отправки накладной", zap.Error(err)) // Возвращаем 502 Bad Gateway, т.к. ошибка скорее всего на стороне RMS @@ -48,3 +52,36 @@ func (h *InvoiceHandler) SendInvoice(c *gin.Context) { "created_number": docNum, }) } + +// GetInvoice godoc +// @Summary Получить детали накладной +// @Description Возвращает детали синхронизированной накладной по ID +// @Tags invoices +// @Produce json +// @Param id path string true "Invoice ID" +// @Success 200 {object} invService.InvoiceDetailsDTO +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Failure 500 {object} map[string]string +func (h *InvoiceHandler) GetInvoice(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID накладной обязателен"}) + return + } + + uuidID, err := uuid.Parse(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Неверный формат ID"}) + return + } + + dto, err := h.service.GetInvoice(uuidID) + if err != nil { + logger.Log.Error("Ошибка получения накладной", zap.Error(err), zap.String("id", id)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка получения накладной"}) + return + } + + c.JSON(http.StatusOK, dto) +} diff --git a/internal/transport/http/handlers/ocr.go b/internal/transport/http/handlers/ocr.go index d1dc86d..f292c5e 100644 --- a/internal/transport/http/handlers/ocr.go +++ b/internal/transport/http/handlers/ocr.go @@ -132,3 +132,20 @@ func (h *OCRHandler) GetUnmatched(c *gin.Context) { } c.JSON(http.StatusOK, items) } + +func (h *OCRHandler) DiscardUnmatched(c *gin.Context) { + userID := c.MustGet("userID").(uuid.UUID) + rawName := c.Query("raw_name") + + if rawName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "raw_name is required"}) + return + } + + if err := h.service.DiscardUnmatched(userID, rawName); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "discarded"}) +} diff --git a/rmser-view/index.html b/rmser-view/index.html index febc0f1..f2d0a2e 100644 --- a/rmser-view/index.html +++ b/rmser-view/index.html @@ -5,12 +5,10 @@