редактирование и удаление сопоставлений

список накладных с позициями
This commit is contained in:
2025-12-29 10:46:05 +03:00
parent c2d382cb6a
commit 310a64e3ba
30 changed files with 1250 additions and 8173 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}