mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
пофиксил неправильный пересчет фасовок в накладной
This commit is contained in:
@@ -92,7 +92,7 @@ func main() {
|
|||||||
syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
|
syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
|
||||||
recService := recServicePkg.NewService(recRepo)
|
recService := recServicePkg.NewService(recRepo)
|
||||||
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, pyClient, cfg.App.StoragePath)
|
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, pyClient, cfg.App.StoragePath)
|
||||||
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, rmsFactory, billingService)
|
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, invoicesRepo, rmsFactory, billingService)
|
||||||
|
|
||||||
// 7. Handlers
|
// 7. Handlers
|
||||||
draftsHandler := handlers.NewDraftsHandler(draftsService)
|
draftsHandler := handlers.NewDraftsHandler(draftsService)
|
||||||
@@ -173,9 +173,10 @@ func main() {
|
|||||||
// Manual Sync Trigger
|
// Manual Sync Trigger
|
||||||
api.POST("/sync/all", func(c *gin.Context) {
|
api.POST("/sync/all", func(c *gin.Context) {
|
||||||
userID := c.MustGet("userID").(uuid.UUID)
|
userID := c.MustGet("userID").(uuid.UUID)
|
||||||
|
force := c.Query("force") == "true"
|
||||||
// Запускаем в горутине, чтобы не держать соединение
|
// Запускаем в горутине, чтобы не держать соединение
|
||||||
go func() {
|
go func() {
|
||||||
if err := syncService.SyncAllData(userID); err != nil {
|
if err := syncService.SyncAllData(userID, force); err != nil {
|
||||||
logger.Log.Error("Manual sync failed", zap.Error(err))
|
logger.Log.Error("Manual sync failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ import (
|
|||||||
|
|
||||||
// Invoice - Приходная накладная
|
// Invoice - Приходная накладная
|
||||||
type Invoice struct {
|
type Invoice struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;"`
|
||||||
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
|
RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
|
||||||
DocumentNumber string `gorm:"type:varchar(100);index"`
|
DocumentNumber string `gorm:"type:varchar(100);index"`
|
||||||
DateIncoming time.Time `gorm:"index"`
|
IncomingDocumentNumber string `gorm:"type:varchar(100)"`
|
||||||
SupplierID uuid.UUID `gorm:"type:uuid;index"`
|
DateIncoming time.Time `gorm:"index"`
|
||||||
DefaultStoreID uuid.UUID `gorm:"type:uuid;index"`
|
SupplierID uuid.UUID `gorm:"type:uuid;index"`
|
||||||
Status string `gorm:"type:varchar(50)"`
|
DefaultStoreID uuid.UUID `gorm:"type:uuid;index"`
|
||||||
Comment string `gorm:"type:text"`
|
Status string `gorm:"type:varchar(50)"`
|
||||||
|
Comment string `gorm:"type:text"`
|
||||||
|
|
||||||
Items []InvoiceItem `gorm:"foreignKey:InvoiceID;constraint:OnDelete:CASCADE"`
|
Items []InvoiceItem `gorm:"foreignKey:InvoiceID;constraint:OnDelete:CASCADE"`
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ type InvoiceItem struct {
|
|||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error)
|
GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error)
|
||||||
|
GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]Invoice, error)
|
||||||
SaveInvoices(invoices []Invoice) error
|
SaveInvoices(invoices []Invoice) error
|
||||||
CountRecent(serverID uuid.UUID, days int) (int64, error)
|
CountRecent(serverID uuid.UUID, days int) (int64, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ func (r *pgRepository) GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error
|
|||||||
return &inv.DateIncoming, nil
|
return &inv.DateIncoming, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *pgRepository) GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]invoices.Invoice, error) {
|
||||||
|
var list []invoices.Invoice
|
||||||
|
err := r.db.
|
||||||
|
Preload("Items").
|
||||||
|
Where("rms_server_id = ? AND date_incoming BETWEEN ? AND ?", serverID, from, to).
|
||||||
|
Order("date_incoming DESC").
|
||||||
|
Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *pgRepository) SaveInvoices(list []invoices.Invoice) error {
|
func (r *pgRepository) SaveInvoices(list []invoices.Invoice) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
for _, inv := range list {
|
for _, inv := range list {
|
||||||
|
|||||||
@@ -518,13 +518,14 @@ func (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allinvoices = append(allinvoices, invoices.Invoice{
|
allinvoices = append(allinvoices, invoices.Invoice{
|
||||||
ID: docID,
|
ID: docID,
|
||||||
DocumentNumber: doc.DocumentNumber,
|
DocumentNumber: doc.DocumentNumber,
|
||||||
DateIncoming: dateInc,
|
IncomingDocumentNumber: doc.IncomingDocumentNumber,
|
||||||
SupplierID: supID,
|
DateIncoming: dateInc,
|
||||||
DefaultStoreID: storeID,
|
SupplierID: supID,
|
||||||
Status: doc.Status,
|
DefaultStoreID: storeID,
|
||||||
Items: items,
|
Status: doc.Status,
|
||||||
|
Items: items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,13 +142,14 @@ type IncomingInvoiceListXML struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IncomingInvoiceXML struct {
|
type IncomingInvoiceXML struct {
|
||||||
ID string `xml:"id"`
|
ID string `xml:"id"`
|
||||||
DocumentNumber string `xml:"documentNumber"`
|
DocumentNumber string `xml:"documentNumber"`
|
||||||
DateIncoming string `xml:"dateIncoming"` // Format: yyyy-MM-ddTHH:mm:ss
|
IncomingDocumentNumber string `xml:"incomingDocumentNumber"`
|
||||||
Status string `xml:"status"` // PROCESSED, NEW, DELETED
|
DateIncoming string `xml:"dateIncoming"` // Format: yyyy-MM-ddTHH:mm:ss
|
||||||
Supplier string `xml:"supplier"` // GUID
|
Status string `xml:"status"` // PROCESSED, NEW, DELETED
|
||||||
DefaultStore string `xml:"defaultStore"` // GUID
|
Supplier string `xml:"supplier"` // GUID
|
||||||
Items []InvoiceItemXML `xml:"items>item"`
|
DefaultStore string `xml:"defaultStore"` // GUID
|
||||||
|
Items []InvoiceItemXML `xml:"items>item"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvoiceItemXML struct {
|
type InvoiceItemXML struct {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -27,6 +28,7 @@ type Service struct {
|
|||||||
catalogRepo catalog.Repository
|
catalogRepo catalog.Repository
|
||||||
accountRepo account.Repository
|
accountRepo account.Repository
|
||||||
supplierRepo suppliers.Repository
|
supplierRepo suppliers.Repository
|
||||||
|
invoiceRepo invoices.Repository
|
||||||
rmsFactory *rms.Factory
|
rmsFactory *rms.Factory
|
||||||
billingService *billing.Service
|
billingService *billing.Service
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,7 @@ func NewService(
|
|||||||
catalogRepo catalog.Repository,
|
catalogRepo catalog.Repository,
|
||||||
accountRepo account.Repository,
|
accountRepo account.Repository,
|
||||||
supplierRepo suppliers.Repository,
|
supplierRepo suppliers.Repository,
|
||||||
|
invoiceRepo invoices.Repository,
|
||||||
rmsFactory *rms.Factory,
|
rmsFactory *rms.Factory,
|
||||||
billingService *billing.Service,
|
billingService *billing.Service,
|
||||||
) *Service {
|
) *Service {
|
||||||
@@ -46,6 +49,7 @@ func NewService(
|
|||||||
catalogRepo: catalogRepo,
|
catalogRepo: catalogRepo,
|
||||||
accountRepo: accountRepo,
|
accountRepo: accountRepo,
|
||||||
supplierRepo: supplierRepo,
|
supplierRepo: supplierRepo,
|
||||||
|
invoiceRepo: invoiceRepo,
|
||||||
rmsFactory: rmsFactory,
|
rmsFactory: rmsFactory,
|
||||||
billingService: billingService,
|
billingService: billingService,
|
||||||
}
|
}
|
||||||
@@ -220,6 +224,7 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
|
|||||||
return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price)
|
return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommitDraft отправляет накладную
|
||||||
// CommitDraft отправляет накладную
|
// CommitDraft отправляет накладную
|
||||||
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
||||||
// 1. Получаем сервер и права
|
// 1. Получаем сервер и права
|
||||||
@@ -285,11 +290,39 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
|
|||||||
sum = dItem.Quantity.Mul(dItem.Price)
|
sum = dItem.Quantity.Mul(dItem.Price)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Инициализируем значениями из черновика (по умолчанию для базовых единиц)
|
||||||
|
amountToSend := dItem.Quantity
|
||||||
|
priceToSend := dItem.Price
|
||||||
|
|
||||||
|
// ЛОГИКА ПЕРЕСЧЕТА ДЛЯ ФАСОВОК (СОГЛАСНО ДОКУМЕНТАЦИИ IIKO)
|
||||||
|
// Если указан ContainerID, iiko требует:
|
||||||
|
// <amount> = кол-во упаковок * вес упаковки (итоговое кол-во в базовых единицах)
|
||||||
|
// <price> = цена за упаковку / вес упаковки (цена за базовую единицу)
|
||||||
|
// <containerId> = ID фасовки
|
||||||
|
if dItem.ContainerID != nil && *dItem.ContainerID != uuid.Nil {
|
||||||
|
// Проверяем, что Container загружен (Preload в репозитории)
|
||||||
|
if dItem.Container != nil {
|
||||||
|
if !dItem.Container.Count.IsZero() {
|
||||||
|
// 1. Пересчитываем кол-во: 5 ящиков * 10 кг = 50 кг
|
||||||
|
amountToSend = dItem.Quantity.Mul(dItem.Container.Count)
|
||||||
|
|
||||||
|
// 2. Пересчитываем цену: 1000 руб/ящ / 10 кг = 100 руб/кг
|
||||||
|
priceToSend = dItem.Price.Div(dItem.Container.Count)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если фасовка есть в ID, но не подгрузилась структура - это ошибка данных.
|
||||||
|
// Логируем варнинг, но пробуем отправить как есть (iiko может отвергнуть или посчитать криво)
|
||||||
|
logger.Log.Warn("Container struct is nil for item with ContainerID",
|
||||||
|
zap.String("item_id", dItem.ID.String()),
|
||||||
|
zap.String("container_id", dItem.ContainerID.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
invItem := invoices.InvoiceItem{
|
invItem := invoices.InvoiceItem{
|
||||||
ProductID: *dItem.ProductID,
|
ProductID: *dItem.ProductID,
|
||||||
Amount: dItem.Quantity,
|
Amount: amountToSend, // Отправляем ПЕРЕСЧИТАННЫЙ вес/объем
|
||||||
Price: dItem.Price,
|
Price: priceToSend, // Отправляем ПЕРЕСЧИТАННУЮ цену за базовую ед.
|
||||||
Sum: sum,
|
Sum: sum, // Сумма остается неизменной (Total)
|
||||||
ContainerID: dItem.ContainerID,
|
ContainerID: dItem.ContainerID,
|
||||||
}
|
}
|
||||||
inv.Items = append(inv.Items, invItem)
|
inv.Items = append(inv.Items, invItem)
|
||||||
@@ -424,3 +457,99 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
|
|||||||
|
|
||||||
return createdID, nil
|
return createdID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавим новый DTO для единого списка (Frontend Contract)
|
||||||
|
type UnifiedInvoiceDTO struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Type string `json:"type"` // "DRAFT" или "SYNCED"
|
||||||
|
DocumentNumber string `json:"document_number"`
|
||||||
|
IncomingNumber string `json:"incoming_number"`
|
||||||
|
DateIncoming time.Time `json:"date_incoming"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TotalSum float64 `json:"total_sum"`
|
||||||
|
StoreName string `json:"store_name"`
|
||||||
|
ItemsCount int `json:"items_count"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
IsAppCreated bool `json:"is_app_created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) {
|
||||||
|
server, err := s.accountRepo.GetActiveServer(userID)
|
||||||
|
if err != nil || server == nil {
|
||||||
|
return nil, errors.New("активный сервер не выбран")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Получаем черновики (их обычно немного, берем все активные)
|
||||||
|
draftsList, err := s.draftRepo.GetActive(server.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Получаем синхронизированные накладные за период
|
||||||
|
invoicesList, err := s.invoiceRepo.GetByPeriod(server.ID, from, to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]UnifiedInvoiceDTO, 0, len(draftsList)+len(invoicesList))
|
||||||
|
|
||||||
|
// Маппим черновики
|
||||||
|
for _, d := range draftsList {
|
||||||
|
var sum decimal.Decimal
|
||||||
|
for _, it := range d.Items {
|
||||||
|
if !it.Sum.IsZero() {
|
||||||
|
sum = sum.Add(it.Sum)
|
||||||
|
} else {
|
||||||
|
sum = sum.Add(it.Quantity.Mul(it.Price))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val, _ := sum.Float64()
|
||||||
|
date := time.Now()
|
||||||
|
if d.DateIncoming != nil {
|
||||||
|
date = *d.DateIncoming
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, UnifiedInvoiceDTO{
|
||||||
|
ID: d.ID,
|
||||||
|
Type: "DRAFT",
|
||||||
|
DocumentNumber: d.DocumentNumber,
|
||||||
|
IncomingNumber: "", // В черновиках пока не разделяем
|
||||||
|
DateIncoming: date,
|
||||||
|
Status: d.Status,
|
||||||
|
TotalSum: val,
|
||||||
|
StoreName: "", // Можно подгрузить из d.Store.Name если сделан Preload
|
||||||
|
ItemsCount: len(d.Items),
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
IsAppCreated: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маппим проведенные
|
||||||
|
for _, inv := range invoicesList {
|
||||||
|
var sum decimal.Decimal
|
||||||
|
for _, it := range inv.Items {
|
||||||
|
sum = sum.Add(it.Sum)
|
||||||
|
}
|
||||||
|
val, _ := sum.Float64()
|
||||||
|
|
||||||
|
isOurs := strings.Contains(strings.ToUpper(inv.Comment), "RMSER")
|
||||||
|
|
||||||
|
result = append(result, UnifiedInvoiceDTO{
|
||||||
|
ID: inv.ID,
|
||||||
|
Type: "SYNCED",
|
||||||
|
DocumentNumber: inv.DocumentNumber,
|
||||||
|
IncomingNumber: inv.IncomingDocumentNumber,
|
||||||
|
DateIncoming: inv.DateIncoming,
|
||||||
|
Status: inv.Status,
|
||||||
|
TotalSum: val,
|
||||||
|
ItemsCount: len(inv.Items),
|
||||||
|
CreatedAt: inv.CreatedAt,
|
||||||
|
IsAppCreated: isOurs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортировка по дате накладной (desc)
|
||||||
|
// (Здесь можно добавить библиотеку sort или оставить как есть, если БД уже отсортировала части)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ func NewService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SyncAllData запускает полную синхронизацию для конкретного пользователя
|
// SyncAllData запускает полную синхронизацию для конкретного пользователя
|
||||||
func (s *Service) SyncAllData(userID uuid.UUID) error {
|
func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
|
||||||
logger.Log.Info("Запуск полной синхронизации", zap.String("user_id", userID.String()))
|
logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force))
|
||||||
|
|
||||||
// 1. Получаем клиент и инфо о сервере
|
// 1. Получаем клиент и инфо о сервере
|
||||||
client, err := s.rmsFactory.GetClientForUser(userID)
|
client, err := s.rmsFactory.GetClientForUser(userID)
|
||||||
@@ -92,7 +92,7 @@ func (s *Service) SyncAllData(userID uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. Накладные (история)
|
// 6. Накладные (история)
|
||||||
if err := s.syncInvoices(client, serverID); err != nil {
|
if err := s.syncInvoices(client, serverID, force); err != nil {
|
||||||
logger.Log.Error("Sync Invoices failed", zap.Error(err))
|
logger.Log.Error("Sync Invoices failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +172,24 @@ func (s *Service) syncRecipes(c rms.ClientI, serverID uuid.UUID) error {
|
|||||||
return s.recipeRepo.SaveRecipes(recipesList)
|
return s.recipeRepo.SaveRecipes(recipesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error {
|
func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) error {
|
||||||
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var from time.Time
|
var from time.Time
|
||||||
to := time.Now()
|
to := time.Now()
|
||||||
|
|
||||||
if lastDate != nil {
|
if force {
|
||||||
from = *lastDate
|
// Принудительная перезагрузка за последние 40 дней
|
||||||
|
from = time.Now().AddDate(0, 0, -40)
|
||||||
|
logger.Log.Info("Force sync invoices", zap.String("from", from.Format("2006-01-02")))
|
||||||
} else {
|
} else {
|
||||||
from = time.Now().AddDate(0, 0, -45) // 45 дней по дефолту
|
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if lastDate != nil {
|
||||||
|
from = *lastDate
|
||||||
|
} else {
|
||||||
|
from = time.Now().AddDate(0, 0, -45)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invs, err := c.FetchInvoices(from, to)
|
invs, err := c.FetchInvoices(from, to)
|
||||||
@@ -194,10 +199,10 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error {
|
|||||||
|
|
||||||
for i := range invs {
|
for i := range invs {
|
||||||
invs[i].RMSServerID = serverID
|
invs[i].RMSServerID = serverID
|
||||||
// В Items пока не добавляли ServerID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(invs) > 0 {
|
if len(invs) > 0 {
|
||||||
|
// Репозиторий использует OnConflict(UpdateAll), поэтому существующие записи обновятся
|
||||||
return s.invoiceRepo.SaveInvoices(invs)
|
return s.invoiceRepo.SaveInvoices(invs)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -253,49 +253,28 @@ type DraftListItemDTO struct {
|
|||||||
TotalSum float64 `json:"total_sum"`
|
TotalSum float64 `json:"total_sum"`
|
||||||
StoreName string `json:"store_name"`
|
StoreName string `json:"store_name"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
IsAppCreated bool `json:"is_app_created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *DraftsHandler) GetDrafts(c *gin.Context) {
|
func (h *DraftsHandler) GetDrafts(c *gin.Context) {
|
||||||
userID := c.MustGet("userID").(uuid.UUID)
|
userID := c.MustGet("userID").(uuid.UUID)
|
||||||
list, err := h.service.GetActiveDrafts(userID)
|
|
||||||
|
// Читаем параметры периода из Query (default: 30 days)
|
||||||
|
fromStr := c.DefaultQuery("from", time.Now().AddDate(0, 0, -30).Format("2006-01-02"))
|
||||||
|
toStr := c.DefaultQuery("to", time.Now().Format("2006-01-02"))
|
||||||
|
|
||||||
|
from, _ := time.Parse("2006-01-02", fromStr)
|
||||||
|
to, _ := time.Parse("2006-01-02", toStr)
|
||||||
|
// Устанавливаем конец дня для 'to'
|
||||||
|
to = to.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||||
|
|
||||||
|
list, err := h.service.GetUnifiedList(userID, from, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := make([]DraftListItemDTO, 0, len(list))
|
c.JSON(http.StatusOK, list)
|
||||||
for _, d := range list {
|
|
||||||
var totalSum decimal.Decimal
|
|
||||||
for _, item := range d.Items {
|
|
||||||
if !item.Sum.IsZero() {
|
|
||||||
totalSum = totalSum.Add(item.Sum)
|
|
||||||
} else {
|
|
||||||
totalSum = totalSum.Add(item.Quantity.Mul(item.Price))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sumFloat, _ := totalSum.Float64()
|
|
||||||
|
|
||||||
dateStr := ""
|
|
||||||
if d.DateIncoming != nil {
|
|
||||||
dateStr = d.DateIncoming.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
storeName := ""
|
|
||||||
if d.Store != nil {
|
|
||||||
storeName = d.Store.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
response = append(response, DraftListItemDTO{
|
|
||||||
ID: d.ID.String(),
|
|
||||||
DocumentNumber: d.DocumentNumber,
|
|
||||||
DateIncoming: dateStr,
|
|
||||||
Status: d.Status,
|
|
||||||
ItemsCount: len(d.Items),
|
|
||||||
TotalSum: sumFloat,
|
|
||||||
StoreName: storeName,
|
|
||||||
CreatedAt: d.CreatedAt.Format(time.RFC3339),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
|
func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
|
||||||
|
|||||||
@@ -97,10 +97,12 @@ func (bot *Bot) initMenus() {
|
|||||||
bot.menuServers = &tele.ReplyMarkup{}
|
bot.menuServers = &tele.ReplyMarkup{}
|
||||||
|
|
||||||
bot.menuDicts = &tele.ReplyMarkup{}
|
bot.menuDicts = &tele.ReplyMarkup{}
|
||||||
btnSync := bot.menuDicts.Data("⚡️ Обновить данные", "act_sync")
|
btnSync := bot.menuDicts.Data("⚡️ Быстрое обновление", "act_sync")
|
||||||
|
btnFullSync := bot.menuDicts.Data("♻️ Полная перезагрузка", "act_full_sync")
|
||||||
btnBack := bot.menuDicts.Data("🔙 Назад", "nav_main")
|
btnBack := bot.menuDicts.Data("🔙 Назад", "nav_main")
|
||||||
bot.menuDicts.Inline(
|
bot.menuDicts.Inline(
|
||||||
bot.menuDicts.Row(btnSync),
|
bot.menuDicts.Row(btnSync),
|
||||||
|
bot.menuDicts.Row(btnFullSync),
|
||||||
bot.menuDicts.Row(btnBack),
|
bot.menuDicts.Row(btnBack),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ func (bot *Bot) initHandlers() {
|
|||||||
|
|
||||||
bot.b.Handle(&tele.Btn{Unique: "act_add_server"}, bot.startAddServerFlow)
|
bot.b.Handle(&tele.Btn{Unique: "act_add_server"}, bot.startAddServerFlow)
|
||||||
bot.b.Handle(&tele.Btn{Unique: "act_sync"}, bot.triggerSync)
|
bot.b.Handle(&tele.Btn{Unique: "act_sync"}, bot.triggerSync)
|
||||||
|
bot.b.Handle(&tele.Btn{Unique: "act_full_sync"}, bot.triggerFullSync)
|
||||||
bot.b.Handle(&tele.Btn{Unique: "act_del_server_menu"}, bot.renderDeleteServerMenu)
|
bot.b.Handle(&tele.Btn{Unique: "act_del_server_menu"}, bot.renderDeleteServerMenu)
|
||||||
bot.b.Handle(&tele.Btn{Unique: "confirm_name_yes"}, bot.handleConfirmNameYes)
|
bot.b.Handle(&tele.Btn{Unique: "confirm_name_yes"}, bot.handleConfirmNameYes)
|
||||||
bot.b.Handle(&tele.Btn{Unique: "confirm_name_no"}, bot.handleConfirmNameNo)
|
bot.b.Handle(&tele.Btn{Unique: "confirm_name_no"}, bot.handleConfirmNameNo)
|
||||||
@@ -571,6 +574,26 @@ func (bot *Bot) NotifySuccess(userID uuid.UUID, amount float64, newBalance int,
|
|||||||
bot.b.Send(&tele.User{ID: user.TelegramID}, msg, tele.ModeHTML)
|
bot.b.Send(&tele.User{ID: user.TelegramID}, msg, tele.ModeHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bot *Bot) triggerFullSync(c tele.Context) error {
|
||||||
|
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||||
|
server, _ := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||||
|
if server == nil {
|
||||||
|
return c.Respond(&tele.CallbackResponse{Text: "Нет активного сервера"})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Respond(&tele.CallbackResponse{Text: "Запущена полная перезагрузка данных...", ShowAlert: false})
|
||||||
|
c.Send("⏳ <b>Полная синхронизация</b>\\nОбновляю историю накладных за 60 дней и справочники. Это может занять до 1 минуты.", tele.ModeHTML)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := bot.syncService.SyncAllData(userDB.ID, true); err != nil {
|
||||||
|
bot.b.Send(c.Sender(), "❌ Ошибка при полной синхронизации: "+err.Error())
|
||||||
|
} else {
|
||||||
|
bot.b.Send(c.Sender(), "✅ Все данные успешно обновлены!")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bot *Bot) handleBillingCallbacks(c tele.Context, data string, userDB *account.User) error {
|
func (bot *Bot) handleBillingCallbacks(c tele.Context, data string, userDB *account.User) error {
|
||||||
if data == "bill_topup" {
|
if data == "bill_topup" {
|
||||||
return bot.renderTariffShowcase(c, "")
|
return bot.renderTariffShowcase(c, "")
|
||||||
@@ -670,7 +693,7 @@ func (bot *Bot) triggerSync(c tele.Context) error {
|
|||||||
}
|
}
|
||||||
c.Respond(&tele.CallbackResponse{Text: "Запускаю синхронизацию..."})
|
c.Respond(&tele.CallbackResponse{Text: "Запускаю синхронизацию..."})
|
||||||
go func() {
|
go func() {
|
||||||
if err := bot.syncService.SyncAllData(userDB.ID); err != nil {
|
if err := bot.syncService.SyncAllData(userDB.ID, false); err != nil {
|
||||||
logger.Log.Error("Manual sync failed", zap.Error(err))
|
logger.Log.Error("Manual sync failed", zap.Error(err))
|
||||||
bot.b.Send(c.Sender(), "❌ Ошибка синхронизации. Проверьте настройки сервера.")
|
bot.b.Send(c.Sender(), "❌ Ошибка синхронизации. Проверьте настройки сервера.")
|
||||||
} else {
|
} else {
|
||||||
@@ -867,7 +890,7 @@ func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string)
|
|||||||
successMsg += "Начинаю первичную синхронизацию данных..."
|
successMsg += "Начинаю первичную синхронизацию данных..."
|
||||||
|
|
||||||
c.Send(successMsg, tele.ModeHTML)
|
c.Send(successMsg, tele.ModeHTML)
|
||||||
go bot.syncService.SyncAllData(userDB.ID)
|
go bot.syncService.SyncAllData(userDB.ID, false)
|
||||||
|
|
||||||
return bot.renderMainMenu(c)
|
return bot.renderMainMenu(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,231 @@
|
|||||||
import React from 'react';
|
// src/pages/DraftsList.tsx
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { List, Typography, Tag, Spin, Empty } from 'antd';
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
import {
|
||||||
import { api } from '../services/api';
|
List,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Spin,
|
||||||
|
Empty,
|
||||||
|
DatePicker,
|
||||||
|
Flex,
|
||||||
|
message,
|
||||||
|
} from "antd";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
ArrowRightOutlined,
|
||||||
|
ThunderboltFilled,
|
||||||
|
HistoryOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { api } from "../services/api";
|
||||||
|
import type { UnifiedInvoice } from "../services/types";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
export const DraftsList: React.FC = () => {
|
export const DraftsList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: drafts, isLoading, isError } = useQuery({
|
// Состояние фильтра дат: по умолчанию последние 30 дней
|
||||||
queryKey: ['drafts'],
|
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
|
||||||
queryFn: api.getDrafts,
|
dayjs().subtract(30, "day"),
|
||||||
refetchOnWindowFocus: true
|
dayjs(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос)
|
||||||
|
const {
|
||||||
|
data: invoices,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"drafts",
|
||||||
|
dateRange[0].format("YYYY-MM-DD"),
|
||||||
|
dateRange[1].format("YYYY-MM-DD"),
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
api.getDrafts(
|
||||||
|
dateRange[0].format("YYYY-MM-DD"),
|
||||||
|
dateRange[1].format("YYYY-MM-DD")
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStatusTag = (status: string) => {
|
const getStatusTag = (item: UnifiedInvoice) => {
|
||||||
switch (status) {
|
if (item.type === "SYNCED") {
|
||||||
case 'PROCESSING': return <Tag color="blue">Обработка</Tag>;
|
return (
|
||||||
case 'READY_TO_VERIFY': return <Tag color="orange">Проверка</Tag>;
|
<Tag icon={<HistoryOutlined />} color="success">
|
||||||
case 'COMPLETED': return <Tag color="green">Готово</Tag>;
|
Синхронизировано
|
||||||
case 'ERROR': return <Tag color="red">Ошибка</Tag>;
|
</Tag>
|
||||||
case 'CANCELED': return <Tag color="default" style={{ color: '#999' }}>Отменен</Tag>;
|
);
|
||||||
default: return <Tag>{status}</Tag>;
|
}
|
||||||
|
|
||||||
|
switch (item.status) {
|
||||||
|
case "PROCESSING":
|
||||||
|
return <Tag color="blue">Обработка</Tag>;
|
||||||
|
case "READY_TO_VERIFY":
|
||||||
|
return <Tag color="orange">Проверка</Tag>;
|
||||||
|
case "COMPLETED":
|
||||||
|
return <Tag color="green">Готово</Tag>;
|
||||||
|
case "ERROR":
|
||||||
|
return <Tag color="red">Ошибка</Tag>;
|
||||||
|
case "CANCELED":
|
||||||
|
return <Tag color="default">Отменен</Tag>;
|
||||||
|
default:
|
||||||
|
return <Tag>{item.status}</Tag>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
const handleInvoiceClick = (item: UnifiedInvoice) => {
|
||||||
return <div style={{ textAlign: 'center', padding: 40 }}><Spin size="large" /></div>;
|
if (item.type === "SYNCED") {
|
||||||
}
|
message.info("История доступна только для просмотра");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`/invoice/${item.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return <div style={{ padding: 20, textAlign: 'center' }}>Ошибка загрузки списка</div>;
|
return (
|
||||||
|
<div style={{ padding: 20 }}>
|
||||||
|
<Text type="danger">Ошибка загрузки списка накладных</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 16px 20px' }}>
|
<div style={{ padding: "0 4px 20px" }}>
|
||||||
<Title level={4} style={{ marginTop: 16, marginBottom: 16 }}>Черновики накладных</Title>
|
<Title level={4} style={{ marginTop: 16, marginBottom: 16 }}>
|
||||||
|
Накладные
|
||||||
|
</Title>
|
||||||
|
|
||||||
{(!drafts || drafts.length === 0) ? (
|
{/* Фильтр дат */}
|
||||||
<Empty description="Нет активных черновиков" />
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
background: "#fff",
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ display: "block", marginBottom: 8, fontSize: 12 }}
|
||||||
|
>
|
||||||
|
Период загрузки:
|
||||||
|
</Text>
|
||||||
|
<RangePicker
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => dates && setDateRange([dates[0]!, dates[1]!])}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
allowClear={false}
|
||||||
|
format="DD.MM.YYYY"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div style={{ textAlign: "center", padding: 40 }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : !invoices || invoices.length === 0 ? (
|
||||||
|
<Empty description="Нет данных за выбранный период" />
|
||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
itemLayout="horizontal"
|
dataSource={invoices}
|
||||||
dataSource={drafts}
|
renderItem={(item) => {
|
||||||
renderItem={(item) => (
|
const isSynced = item.type === "SYNCED";
|
||||||
<List.Item
|
|
||||||
style={{
|
|
||||||
background: '#fff',
|
|
||||||
padding: 12,
|
|
||||||
marginBottom: 8,
|
|
||||||
borderRadius: 8,
|
|
||||||
cursor: 'pointer',
|
|
||||||
opacity: item.status === 'CANCELED' ? 0.6 : 1 // Делаем отмененные бледными
|
|
||||||
}}
|
|
||||||
onClick={() => navigate(`/invoice/${item.id}`)}
|
|
||||||
>
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
||||||
<Text strong style={{ fontSize: 16, textDecoration: item.status === 'CANCELED' ? 'line-through' : 'none' }}>
|
|
||||||
{item.document_number || 'Без номера'}
|
|
||||||
</Text>
|
|
||||||
{getStatusTag(item.status)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#888', fontSize: 13 }}>
|
return (
|
||||||
<span>{new Date(item.date_incoming).toLocaleDateString()}</span>
|
<List.Item
|
||||||
<span>{item.items_count} поз.</span>
|
style={{
|
||||||
</div>
|
background: isSynced ? "#fafafa" : "#fff",
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 10,
|
||||||
|
borderRadius: 12,
|
||||||
|
cursor: isSynced ? "default" : "pointer",
|
||||||
|
border: isSynced ? "1px solid #f0f0f0" : "1px solid #e6f7ff",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.02)",
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
onClick={() => handleInvoiceClick(item)}
|
||||||
|
>
|
||||||
|
<Flex vertical gap={4}>
|
||||||
|
<Flex justify="space-between" align="start">
|
||||||
|
<Flex vertical>
|
||||||
|
<Flex align="center" gap={8}>
|
||||||
|
<Text strong style={{ fontSize: 16 }}>
|
||||||
|
{item.document_number || "Без номера"}
|
||||||
|
</Text>
|
||||||
|
{item.is_app_created && (
|
||||||
|
<ThunderboltFilled
|
||||||
|
style={{ color: "#faad14" }}
|
||||||
|
title="Создано в RMSer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{item.incoming_number && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
Вх. № {item.incoming_number}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{getStatusTag(item)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 6, alignItems: 'center' }}>
|
<Flex justify="space-between" style={{ marginTop: 4 }}>
|
||||||
<Text strong>
|
<Flex gap={8} align="center">
|
||||||
{item.total_sum.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' })}
|
<FileTextOutlined style={{ color: "#888" }} />
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
{item.items_count} поз.
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
{dayjs(item.date_incoming).format("DD.MM.YYYY")}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
{item.store_name && (
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
maxWidth: 120,
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.store_name}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
fontSize: 17,
|
||||||
|
color: isSynced ? "#595959" : "#1890ff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.total_sum.toLocaleString("ru-RU", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "RUB",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<ArrowRightOutlined style={{ color: '#1890ff' }} />
|
{!isSynced && (
|
||||||
</div>
|
<ArrowRightOutlined style={{ color: "#1890ff" }} />
|
||||||
</div>
|
)}
|
||||||
</List.Item>
|
</Flex>
|
||||||
)}
|
</Flex>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type {
|
|||||||
AddContainerRequest,
|
AddContainerRequest,
|
||||||
AddContainerResponse,
|
AddContainerResponse,
|
||||||
DictionariesResponse,
|
DictionariesResponse,
|
||||||
DraftSummary,
|
UnifiedInvoice,
|
||||||
ServerUser,
|
ServerUser,
|
||||||
UserRole
|
UserRole
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -159,8 +159,11 @@ export const api = {
|
|||||||
return data.suppliers;
|
return data.suppliers;
|
||||||
},
|
},
|
||||||
|
|
||||||
getDrafts: async (): Promise<DraftSummary[]> => {
|
// Обновленный метод получения списка накладных с фильтрацией
|
||||||
const { data } = await apiClient.get<DraftSummary[]>('/drafts');
|
getDrafts: async (from?: string, to?: string): Promise<UnifiedInvoice[]> => {
|
||||||
|
const { data } = await apiClient.get<UnifiedInvoice[]>('/drafts', {
|
||||||
|
params: { from, to }
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -234,3 +234,19 @@ export interface MainUnit {
|
|||||||
name: string; // "кг"
|
name: string; // "кг"
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InvoiceType = 'DRAFT' | 'SYNCED'; // Тип записи: Черновик или Синхронизировано из iiko
|
||||||
|
|
||||||
|
export interface UnifiedInvoice {
|
||||||
|
id: UUID;
|
||||||
|
type: InvoiceType; // Новый признак типа
|
||||||
|
document_number: string; // Внутренний номер iiko или ID черновика
|
||||||
|
incoming_number: string; // Входящий номер накладной от поставщика
|
||||||
|
date_incoming: string;
|
||||||
|
status: DraftStatus;
|
||||||
|
items_count: number;
|
||||||
|
total_sum: number;
|
||||||
|
store_name?: string;
|
||||||
|
created_at: string;
|
||||||
|
is_app_created: boolean; // Создано ли через наше приложение
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user