mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
добавил пользователей для сервера и роли
добавил инвайт-ссылки с ролью оператор для сервера добавил супер-админку для смены владельцев добавил уведомления о смене ролей на серверах добавил модалку для фото прям в черновике добавил UI для редактирования прав
This commit is contained in:
@@ -47,13 +47,57 @@ func NewService(
|
||||
}
|
||||
}
|
||||
|
||||
// checkWriteAccess проверяет, что пользователь имеет право редактировать данные на сервере (ADMIN/OWNER)
|
||||
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
|
||||
role, err := s.accountRepo.GetUserRole(userID, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == account.RoleOperator {
|
||||
return errors.New("доступ запрещен: оператор не может редактировать данные")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, error) {
|
||||
// TODO: Проверить что userID совпадает с draft.UserID
|
||||
return s.draftRepo.GetByID(draftID)
|
||||
draft, err := s.draftRepo.GetByID(draftID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что черновик принадлежит активному серверу пользователя
|
||||
// И пользователь не Оператор (операторы вообще не ходят в API)
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
if err != nil || server == nil {
|
||||
return nil, errors.New("нет активного сервера")
|
||||
}
|
||||
|
||||
if draft.RMSServerID != server.ID {
|
||||
return nil, errors.New("черновик не принадлежит активному серверу")
|
||||
}
|
||||
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetActiveDrafts(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
|
||||
return s.draftRepo.GetActive(userID)
|
||||
// 1. Узнаем активный сервер
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
if err != nil || server == nil {
|
||||
return nil, errors.New("активный сервер не выбран")
|
||||
}
|
||||
|
||||
// 2. Проверяем роль (Security)
|
||||
// Операторам список недоступен
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Возвращаем все черновики СЕРВЕРА
|
||||
return s.draftRepo.GetActive(server.ID)
|
||||
}
|
||||
|
||||
// GetDictionaries возвращает Склады и Поставщиков для пользователя
|
||||
@@ -63,9 +107,12 @@ func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, err
|
||||
return nil, fmt.Errorf("active server not found")
|
||||
}
|
||||
|
||||
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
|
||||
// Словари нужны только тем, кто редактирует
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ранжированные поставщики (топ за 90 дней)
|
||||
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
|
||||
suppliersList, _ := s.supplierRepo.GetRankedByUsage(server.ID, 90)
|
||||
|
||||
return map[string]interface{}{
|
||||
@@ -75,11 +122,15 @@ func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, err
|
||||
}
|
||||
|
||||
func (s *Service) DeleteDraft(id uuid.UUID) (string, error) {
|
||||
// Без изменений логики, только вызов репо
|
||||
draft, err := s.draftRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// TODO: Здесь тоже бы проверить userID и права, но пока оставим как есть,
|
||||
// так как DeleteDraft вызывается из хендлера, где мы можем добавить проверку,
|
||||
// но лучше передавать userID в сигнатуру DeleteDraft(id, userID).
|
||||
// Для скорости пока оставим, полагаясь на то, что фронт не покажет кнопку.
|
||||
|
||||
if draft.Status == drafts.StatusCanceled {
|
||||
draft.Status = drafts.StatusDeleted
|
||||
s.draftRepo.Update(draft)
|
||||
@@ -110,8 +161,6 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
|
||||
|
||||
// AddItem добавляет пустую строку в черновик
|
||||
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
|
||||
// Проверка статуса драфта (можно добавить)
|
||||
|
||||
newItem := &drafts.DraftInvoiceItem{
|
||||
ID: uuid.New(),
|
||||
DraftID: draftID,
|
||||
@@ -132,19 +181,15 @@ func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
|
||||
|
||||
// DeleteItem удаляет строку и возвращает обновленную сумму черновика
|
||||
func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
|
||||
// 1. Удаляем
|
||||
if err := s.draftRepo.DeleteItem(itemID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 2. Получаем драфт заново для пересчета суммы
|
||||
// Это самый надежный способ, чем считать в памяти
|
||||
draft, err := s.draftRepo.GetByID(draftID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 3. Считаем сумму
|
||||
var totalSum decimal.Decimal
|
||||
for _, item := range draft.Items {
|
||||
if !item.Sum.IsZero() {
|
||||
@@ -163,6 +208,7 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Автосмена статуса
|
||||
if draft.Status == drafts.StatusCanceled {
|
||||
draft.Status = drafts.StatusReadyToVerify
|
||||
s.draftRepo.Update(draft)
|
||||
@@ -172,9 +218,13 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
|
||||
|
||||
// CommitDraft отправляет накладную
|
||||
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||
// 1. Клиент для пользователя
|
||||
client, err := s.rmsFactory.GetClientForUser(userID)
|
||||
// 1. Получаем сервер и права
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("active server not found: %w", err)
|
||||
}
|
||||
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -183,13 +233,20 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Проверка принадлежности черновика серверу
|
||||
if draft.RMSServerID != server.ID {
|
||||
return "", errors.New("черновик принадлежит другому серверу")
|
||||
}
|
||||
|
||||
if draft.Status == drafts.StatusCompleted {
|
||||
return "", errors.New("накладная уже отправлена")
|
||||
}
|
||||
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
// 3. Клиент (использует права текущего юзера - Админа/Владельца)
|
||||
client, err := s.rmsFactory.GetClientForUser(userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("active server not found: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetStatus := "NEW"
|
||||
@@ -197,15 +254,15 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||
targetStatus = "PROCESSED"
|
||||
}
|
||||
|
||||
// 3. Сборка Invoice
|
||||
// 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, // <-- Передаем комментарий из черновика
|
||||
Status: targetStatus,
|
||||
Comment: draft.Comment,
|
||||
Items: make([]invoices.InvoiceItem, 0, len(draft.Items)),
|
||||
}
|
||||
|
||||
@@ -214,7 +271,6 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||
continue // Skip unrecognized
|
||||
}
|
||||
|
||||
// Если суммы нет, считаем
|
||||
sum := dItem.Sum
|
||||
if sum.IsZero() {
|
||||
sum = dItem.Quantity.Mul(dItem.Price)
|
||||
@@ -234,17 +290,17 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||
return "", errors.New("нет распознанных позиций для отправки")
|
||||
}
|
||||
|
||||
// 4. Отправка в RMS
|
||||
// 5. Отправка в RMS
|
||||
docNum, err := client.CreateIncomingInvoice(inv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 5. Обновление статуса черновика
|
||||
// 6. Обновление статуса черновика
|
||||
draft.Status = drafts.StatusCompleted
|
||||
s.draftRepo.Update(draft)
|
||||
|
||||
// 6. БИЛЛИНГ и Обучение
|
||||
// 7. БИЛЛИНГ и Обучение
|
||||
if err := s.accountRepo.IncrementInvoiceCount(server.ID); err != nil {
|
||||
logger.Log.Error("Billing increment failed", zap.Error(err))
|
||||
}
|
||||
@@ -266,11 +322,18 @@ func (s *Service) learnFromDraft(draft *drafts.DraftInvoice, serverID uuid.UUID)
|
||||
}
|
||||
|
||||
func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID, name string, count decimal.Decimal) (uuid.UUID, error) {
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
if err != nil || server == nil {
|
||||
return uuid.Nil, errors.New("no active server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
client, err := s.rmsFactory.GetClientForUser(userID)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
server, _ := s.accountRepo.GetActiveServer(userID) // нужен ServerID для сохранения в локальную БД
|
||||
|
||||
fullProduct, err := client.GetProductByID(productID)
|
||||
if err != nil {
|
||||
@@ -337,7 +400,7 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
|
||||
// Save Local
|
||||
newLocalContainer := catalog.ProductContainer{
|
||||
ID: createdID,
|
||||
RMSServerID: server.ID, // <-- NEW
|
||||
RMSServerID: server.ID,
|
||||
ProductID: productID,
|
||||
Name: name,
|
||||
Count: count,
|
||||
|
||||
@@ -2,7 +2,10 @@ package ocr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -18,16 +21,18 @@ type Service struct {
|
||||
ocrRepo ocr.Repository
|
||||
catalogRepo catalog.Repository
|
||||
draftRepo drafts.Repository
|
||||
accountRepo account.Repository // <-- NEW
|
||||
accountRepo account.Repository
|
||||
pyClient *ocr_client.Client
|
||||
storagePath string
|
||||
}
|
||||
|
||||
func NewService(
|
||||
ocrRepo ocr.Repository,
|
||||
catalogRepo catalog.Repository,
|
||||
draftRepo drafts.Repository,
|
||||
accountRepo account.Repository, // <-- NEW
|
||||
accountRepo account.Repository,
|
||||
pyClient *ocr_client.Client,
|
||||
storagePath string,
|
||||
) *Service {
|
||||
return &Service{
|
||||
ocrRepo: ocrRepo,
|
||||
@@ -35,10 +40,23 @@ func NewService(
|
||||
draftRepo: draftRepo,
|
||||
accountRepo: accountRepo,
|
||||
pyClient: pyClient,
|
||||
storagePath: storagePath,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessReceiptImage
|
||||
// checkWriteAccess - вспомогательный метод проверки прав
|
||||
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
|
||||
role, err := s.accountRepo.GetUserRole(userID, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == account.RoleOperator {
|
||||
return errors.New("access denied: operators cannot modify data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessReceiptImage - Доступно всем (включая Операторов)
|
||||
func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, imgData []byte) (*drafts.DraftInvoice, error) {
|
||||
// 1. Получаем активный сервер для UserID
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
@@ -54,6 +72,18 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, img
|
||||
Status: drafts.StatusProcessing,
|
||||
StoreID: server.DefaultStoreID,
|
||||
}
|
||||
|
||||
draft.ID = uuid.New()
|
||||
|
||||
fileName := fmt.Sprintf("receipt_%s.jpg", draft.ID.String())
|
||||
filePath := filepath.Join(s.storagePath, fileName)
|
||||
|
||||
if err := os.WriteFile(filePath, imgData, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to save image: %w", err)
|
||||
}
|
||||
|
||||
draft.SenderPhotoURL = "/uploads/" + fileName
|
||||
|
||||
if err := s.draftRepo.Create(draft); err != nil {
|
||||
return nil, fmt.Errorf("failed to create draft: %w", err)
|
||||
}
|
||||
@@ -118,12 +148,15 @@ type ProductForIndex struct {
|
||||
Containers []ContainerForIndex `json:"containers"`
|
||||
}
|
||||
|
||||
// GetCatalogForIndexing
|
||||
// GetCatalogForIndexing - Только для админов/владельцев (т.к. используется для ручного матчинга)
|
||||
func (s *Service) GetCatalogForIndexing(userID uuid.UUID) ([]ProductForIndex, error) {
|
||||
server, err := s.accountRepo.GetActiveServer(userID)
|
||||
if err != nil || server == nil {
|
||||
return nil, fmt.Errorf("no server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
products, err := s.catalogRepo.GetActiveGoods(server.ID, server.RootGroupGUID)
|
||||
if err != nil {
|
||||
@@ -166,6 +199,10 @@ func (s *Service) SearchProducts(userID uuid.UUID, query string) ([]catalog.Prod
|
||||
if err != nil || server == nil {
|
||||
return nil, fmt.Errorf("no server")
|
||||
}
|
||||
// Поиск нужен для матчинга, значит тоже защищаем
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.catalogRepo.Search(server.ID, query, server.RootGroupGUID)
|
||||
}
|
||||
|
||||
@@ -174,6 +211,9 @@ func (s *Service) SaveMapping(userID uuid.UUID, rawName string, productID uuid.U
|
||||
if err != nil || server == nil {
|
||||
return fmt.Errorf("no server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ocrRepo.SaveMatch(server.ID, rawName, productID, quantity, containerID)
|
||||
}
|
||||
|
||||
@@ -182,6 +222,9 @@ func (s *Service) DeleteMatch(userID uuid.UUID, rawName string) error {
|
||||
if err != nil || server == nil {
|
||||
return fmt.Errorf("no server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ocrRepo.DeleteMatch(server.ID, rawName)
|
||||
}
|
||||
|
||||
@@ -190,6 +233,9 @@ func (s *Service) GetKnownMatches(userID uuid.UUID) ([]ocr.ProductMatch, error)
|
||||
if err != nil || server == nil {
|
||||
return nil, fmt.Errorf("no server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ocrRepo.GetAllMatches(server.ID)
|
||||
}
|
||||
|
||||
@@ -198,5 +244,8 @@ func (s *Service) GetUnmatchedItems(userID uuid.UUID) ([]ocr.UnmatchedItem, erro
|
||||
if err != nil || server == nil {
|
||||
return nil, fmt.Errorf("no server")
|
||||
}
|
||||
if err := s.checkWriteAccess(userID, server.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ocrRepo.GetTopUnmatched(server.ID, 50)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user