Перевел на multi-tenant

Добавил поставщиков
Накладные успешно создаются из фронта
This commit is contained in:
2025-12-18 03:56:21 +03:00
parent 47ec8094e5
commit 542beafe0e
38 changed files with 1942 additions and 977 deletions

View File

@@ -0,0 +1,63 @@
package account
import (
"time"
"github.com/google/uuid"
)
// User - Пользователь системы (Telegram аккаунт)
type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
TelegramID int64 `gorm:"uniqueIndex;not null" json:"telegram_id"`
Username string `gorm:"type:varchar(100)" json:"username"`
FirstName string `gorm:"type:varchar(100)" json:"first_name"`
LastName string `gorm:"type:varchar(100)" json:"last_name"`
PhotoURL string `gorm:"type:text" json:"photo_url"`
IsAdmin bool `gorm:"default:false" json:"is_admin"`
// Связь с серверами
Servers []RMSServer `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"servers,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// RMSServer - Настройки подключения к iikoRMS
type RMSServer struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
Name string `gorm:"type:varchar(100);not null" json:"name"` // Название (напр. "Ресторан на Ленина")
// Credentials
BaseURL string `gorm:"type:varchar(255);not null" json:"base_url"`
Login string `gorm:"type:varchar(100);not null" json:"login"`
EncryptedPassword string `gorm:"type:text;not null" json:"-"` // Пароль храним зашифрованным
// Billing / Stats
InvoiceCount int `gorm:"default:0" json:"invoice_count"` // Счетчик успешно отправленных накладных
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Repository интерфейс управления аккаунтами
type Repository interface {
// Users
GetOrCreateUser(telegramID int64, username, first, last string) (*User, error)
GetUserByTelegramID(telegramID int64) (*User, error)
// Servers
SaveServer(server *RMSServer) error
SetActiveServer(userID, serverID uuid.UUID) error
GetActiveServer(userID uuid.UUID) (*RMSServer, error) // Получить активный (первый попавшийся или помеченный)
GetAllServers(userID uuid.UUID) ([]RMSServer, error)
DeleteServer(serverID uuid.UUID) error
// Billing
IncrementInvoiceCount(serverID uuid.UUID) error
}

View File

@@ -9,22 +9,25 @@ import (
// MeasureUnit - Единица измерения (kg, l, pcs)
type MeasureUnit struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Code string `gorm:"type:varchar(50)" json:"code"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"-"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Code string `gorm:"type:varchar(50)" json:"code"`
}
// ProductContainer - Фасовка (упаковка) товара
type ProductContainer struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ProductID uuid.UUID `gorm:"type:uuid;index;not null" json:"product_id"`
Name string `gorm:"type:varchar(100);not null" json:"name"`
Count decimal.Decimal `gorm:"type:numeric(19,4);not null" json:"count"` // Коэфф. пересчета
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"-"`
ProductID uuid.UUID `gorm:"type:uuid;index;not null" json:"product_id"`
Name string `gorm:"type:varchar(100);not null" json:"name"`
Count decimal.Decimal `gorm:"type:numeric(19,4);not null" json:"count"` // Коэфф. пересчета
}
// Product - Номенклатура
type Product struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"-"`
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
Type string `gorm:"type:varchar(50);index" json:"type"` // GOODS, DISH, PREPARED
@@ -53,11 +56,14 @@ type Product struct {
type Repository interface {
SaveMeasureUnits(units []MeasureUnit) error
SaveProducts(products []Product) error
SaveContainer(container ProductContainer) error // Добавление фасовки
Search(query string) ([]Product, error)
GetAll() ([]Product, error)
GetActiveGoods() ([]Product, error)
// --- Stores ---
SaveContainer(container ProductContainer) error
Search(serverID uuid.UUID, query string) ([]Product, error)
GetActiveGoods(serverID uuid.UUID) ([]Product, error)
SaveStores(stores []Store) error
GetActiveStores() ([]Store, error)
GetActiveStores(serverID uuid.UUID) ([]Store, error)
CountGoods(serverID uuid.UUID) (int64, error)
CountStores(serverID uuid.UUID) (int64, error)
}

View File

@@ -6,11 +6,13 @@ import (
"github.com/google/uuid"
)
// Store - Склад (в терминологии iiko: Entity с типом Account и подтипом INVENTORY_ASSETS)
// Store - Склад
type Store struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"-"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ParentCorporateID uuid.UUID `gorm:"type:uuid;index" json:"parent_corporate_id"` // ID юр.лица/торгового предприятия
ParentCorporateID uuid.UUID `gorm:"type:uuid;index" json:"parent_corporate_id"`
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
CreatedAt time.Time `json:"created_at"`

View File

@@ -9,31 +9,31 @@ import (
"github.com/shopspring/decimal"
)
// Статусы черновика
const (
StatusProcessing = "PROCESSING" // OCR в процессе
StatusReadyToVerify = "READY_TO_VERIFY" // Распознано, ждет проверки пользователем
StatusCompleted = "COMPLETED" // Отправлено в RMS
StatusError = "ERROR" // Ошибка обработки
StatusCanceled = "CANCELED" // Пользователь отменил
StatusDeleted = "DELETED" // Пользователь удалил
StatusProcessing = "PROCESSING"
StatusReadyToVerify = "READY_TO_VERIFY"
StatusCompleted = "COMPLETED"
StatusError = "ERROR"
StatusCanceled = "CANCELED"
StatusDeleted = "DELETED"
)
// DraftInvoice - Черновик накладной
type DraftInvoice struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
ChatID int64 `gorm:"index" json:"chat_id"` // ID чата в Telegram (кто прислал)
SenderPhotoURL string `gorm:"type:text" json:"photo_url"` // Ссылка на фото
Status string `gorm:"type:varchar(50);default:'PROCESSING'" json:"status"`
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
// Привязка к аккаунту и серверу
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"rms_server_id"`
SenderPhotoURL string `gorm:"type:text" json:"photo_url"`
Status string `gorm:"type:varchar(50);default:'PROCESSING'" json:"status"`
// Данные для отправки в RMS
DocumentNumber string `gorm:"type:varchar(100)" json:"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"`
// Связь со складом для Preload
Store *catalog.Store `gorm:"foreignKey:StoreID" json:"store,omitempty"`
StoreID *uuid.UUID `gorm:"type:uuid" json:"store_id"`
Store *catalog.Store `gorm:"foreignKey:StoreID" json:"store,omitempty"`
Comment string `gorm:"type:text" json:"comment"`
RMSInvoiceID *uuid.UUID `gorm:"type:uuid" json:"rms_invoice_id"`
@@ -44,38 +44,32 @@ type DraftInvoice struct {
UpdatedAt time.Time `json:"updated_at"`
}
// DraftInvoiceItem - Позиция черновика
type DraftInvoiceItem struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
DraftID uuid.UUID `gorm:"type:uuid;not null;index" json:"draft_id"`
// --- Результаты OCR (Исходные данные) ---
RawName string `gorm:"type:varchar(255);not null" json:"raw_name"` // Текст с чека
RawAmount decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_amount"` // Кол-во, которое увидел OCR
RawPrice decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_price"` // Цена, которую увидел OCR
RawName string `gorm:"type:varchar(255);not null" json:"raw_name"`
RawAmount decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_amount"`
RawPrice decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_price"`
// --- Результат Матчинга и Выбора пользователя ---
ProductID *uuid.UUID `gorm:"type:uuid;index" json:"product_id"`
Product *catalog.Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
ContainerID *uuid.UUID `gorm:"type:uuid;index" json:"container_id"`
Container *catalog.ProductContainer `gorm:"foreignKey:ContainerID" json:"container,omitempty"`
// Финальные цифры, которые пойдут в накладную
Quantity decimal.Decimal `gorm:"type:numeric(19,4);default:0" json:"quantity"`
Price decimal.Decimal `gorm:"type:numeric(19,4);default:0" json:"price"`
Sum decimal.Decimal `gorm:"type:numeric(19,4);default:0" json:"sum"`
IsMatched bool `gorm:"default:false" json:"is_matched"` // Удалось ли системе найти пару автоматически
IsMatched bool `gorm:"default:false" json:"is_matched"`
}
// Repository интерфейс
type Repository interface {
Create(draft *DraftInvoice) error
GetByID(id uuid.UUID) (*DraftInvoice, error)
Update(draft *DraftInvoice) error
CreateItems(items []DraftInvoiceItem) error
// UpdateItem обновляет конкретную строку (например, при ручном выборе товара)
UpdateItem(itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error
Delete(id uuid.UUID) error
GetActive() ([]DraftInvoice, error)
GetActive(userID uuid.UUID) ([]DraftInvoice, error)
}

View File

@@ -12,6 +12,7 @@ import (
// Invoice - Приходная накладная
type Invoice struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
DocumentNumber string `gorm:"type:varchar(100);index"`
DateIncoming time.Time `gorm:"index"`
SupplierID uuid.UUID `gorm:"type:uuid;index"`
@@ -39,6 +40,7 @@ type InvoiceItem struct {
}
type Repository interface {
GetLastInvoiceDate() (*time.Time, error)
GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error)
SaveInvoices(invoices []Invoice) error
CountRecent(serverID uuid.UUID, days int) (int64, error)
}

View File

@@ -9,38 +9,45 @@ import (
"github.com/shopspring/decimal"
)
// ProductMatch связывает текст из чека с конкретным товаром в iiko
// ProductMatch
type ProductMatch struct {
RawName string `gorm:"type:varchar(255);primary_key" json:"raw_name"`
// RawName больше не PrimaryKey, так как в разных серверах один текст может значить разное
// Делаем составной ключ или суррогатный ID.
// Для простоты GORM: ID - uuid, а уникальность через индекс (RawName + ServerID)
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index:idx_raw_server,unique"`
RawName string `gorm:"type:varchar(255);not null;index:idx_raw_server,unique" json:"raw_name"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
Product catalog.Product `gorm:"foreignKey:ProductID" json:"product"`
// Количество и фасовки
Quantity decimal.Decimal `gorm:"type:numeric(19,4);default:1" json:"quantity"`
ContainerID *uuid.UUID `gorm:"type:uuid;index" json:"container_id"`
// Для подгрузки данных о фасовке при чтении
Container *catalog.ProductContainer `gorm:"foreignKey:ContainerID" json:"container,omitempty"`
Quantity decimal.Decimal `gorm:"type:numeric(19,4);default:1" json:"quantity"`
ContainerID *uuid.UUID `gorm:"type:uuid;index" json:"container_id"`
Container *catalog.ProductContainer `gorm:"foreignKey:ContainerID" json:"container,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// UnmatchedItem хранит строки, которые не удалось распознать, для подсказок
// UnmatchedItem тоже стоит делить, чтобы подсказывать пользователю только его нераспознанные,
// хотя глобальная база unmatched может быть полезна для аналитики.
// Сделаем раздельной для чистоты SaaS.
type UnmatchedItem struct {
RawName string `gorm:"type:varchar(255);primary_key" json:"raw_name"`
Count int `gorm:"default:1" json:"count"` // Сколько раз встречалось
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index:idx_unm_raw_server,unique"`
RawName string `gorm:"type:varchar(255);not null;index:idx_unm_raw_server,unique" json:"raw_name"`
Count int `gorm:"default:1" json:"count"`
LastSeen time.Time `json:"last_seen"`
}
type Repository interface {
// SaveMatch теперь принимает quantity и containerID
SaveMatch(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error
DeleteMatch(rawName string) error
FindMatch(rawName string) (*ProductMatch, error) // Возвращаем полную структуру, чтобы получить qty
GetAllMatches() ([]ProductMatch, error)
SaveMatch(serverID uuid.UUID, rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error
DeleteMatch(serverID uuid.UUID, rawName string) error
FindMatch(serverID uuid.UUID, rawName string) (*ProductMatch, error)
GetAllMatches(serverID uuid.UUID) ([]ProductMatch, error)
UpsertUnmatched(rawName string) error
GetTopUnmatched(limit int) ([]UnmatchedItem, error)
DeleteUnmatched(rawName string) error
UpsertUnmatched(serverID uuid.UUID, rawName string) error
GetTopUnmatched(serverID uuid.UUID, limit int) ([]UnmatchedItem, error)
DeleteUnmatched(serverID uuid.UUID, rawName string) error
}

View File

@@ -19,8 +19,9 @@ const (
// StoreOperation - запись из складского отчета
type StoreOperation struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
// Наш внутренний, "очищенный" тип операции
OpType OperationType `gorm:"type:varchar(50);index"`
@@ -44,5 +45,5 @@ type StoreOperation struct {
}
type Repository interface {
SaveOperations(ops []StoreOperation, opType OperationType, dateFrom, dateTo time.Time) error
SaveOperations(ops []StoreOperation, serverID uuid.UUID, opType OperationType, dateFrom, dateTo time.Time) error
}

View File

@@ -11,10 +11,11 @@ import (
// Recipe - Технологическая карта
type Recipe struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
DateFrom time.Time `gorm:"index"`
DateTo *time.Time
ID uuid.UUID `gorm:"type:uuid;primary_key;"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
DateFrom time.Time `gorm:"index"`
DateTo *time.Time
Product catalog.Product `gorm:"foreignKey:ProductID"`
Items []RecipeItem `gorm:"foreignKey:RecipeID;constraint:OnDelete:CASCADE"`
@@ -22,11 +23,12 @@ type Recipe struct {
// RecipeItem - Ингредиент
type RecipeItem struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
RecipeID uuid.UUID `gorm:"type:uuid;not null;index"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
AmountIn decimal.Decimal `gorm:"type:numeric(19,4);not null"`
AmountOut decimal.Decimal `gorm:"type:numeric(19,4);not null"`
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
RecipeID uuid.UUID `gorm:"type:uuid;not null;index"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
AmountIn decimal.Decimal `gorm:"type:numeric(19,4);not null"`
AmountOut decimal.Decimal `gorm:"type:numeric(19,4);not null"`
Product catalog.Product `gorm:"foreignKey:ProductID"`
}

View File

@@ -0,0 +1,29 @@
package suppliers
import (
"time"
"github.com/google/uuid"
)
// Supplier - Поставщик (Контрагент)
type Supplier struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
Code string `gorm:"type:varchar(50)" json:"code"`
INN string `gorm:"type:varchar(20)" json:"inn"` // taxpayerIdNumber
// Привязка к конкретному серверу iiko (Multi-tenant)
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index" json:"-"`
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
UpdatedAt time.Time `json:"updated_at"`
}
type Repository interface {
SaveBatch(suppliers []Supplier) error
// GetRankedByUsage возвращает поставщиков, отсортированных по частоте использования в накладных за N дней
GetRankedByUsage(serverID uuid.UUID, daysLookBack int) ([]Supplier, error)
Count(serverID uuid.UUID) (int64, error)
}