фикс создания фасовки

This commit is contained in:
2025-12-29 12:34:52 +03:00
parent 310a64e3ba
commit d6703e1a4b
8 changed files with 128 additions and 50 deletions

View File

@@ -102,7 +102,7 @@ func main() {
ocrHandler := handlers.NewOCRHandler(ocrService) ocrHandler := handlers.NewOCRHandler(ocrService)
recommendHandler := handlers.NewRecommendationsHandler(recService) recommendHandler := handlers.NewRecommendationsHandler(recService)
settingsHandler := handlers.NewSettingsHandler(accountRepo, catalogRepo) settingsHandler := handlers.NewSettingsHandler(accountRepo, catalogRepo)
invoicesHandler := handlers.NewInvoiceHandler(invoicesService) invoicesHandler := handlers.NewInvoiceHandler(invoicesService, syncService)
// 8. Telegram Bot (Передаем syncService) // 8. Telegram Bot (Передаем syncService)
if cfg.Telegram.Token != "" { if cfg.Telegram.Token != "" {
@@ -176,6 +176,7 @@ func main() {
// Invoices // Invoices
api.GET("/invoices/:id", invoicesHandler.GetInvoice) api.GET("/invoices/:id", invoicesHandler.GetInvoice)
api.POST("/invoices/sync", invoicesHandler.SyncInvoices)
// Manual Sync Trigger // Manual Sync Trigger
api.POST("/sync/all", func(c *gin.Context) { api.POST("/sync/all", func(c *gin.Context) {

View File

@@ -30,5 +30,5 @@ telegram:
web_app_url: "https://rmser.serty.top" web_app_url: "https://rmser.serty.top"
yookassa: yookassa:
shop_id: "1236145" shop_id: "1234397"
secret_key: "test_HxUkDTirAycj7xooYcu_-gURsHMETbE_onIJYXGkj5Y" secret_key: "live_bRlT9tJRi1hvP7_-C6xjdmzNHpaz9rIs9G0gzv6OPA0"

View File

@@ -106,6 +106,31 @@ func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
return nil return nil
} }
// SyncInvoicesOnly запускает синхронизацию только накладных для конкретного пользователя
func (s *Service) SyncInvoicesOnly(userID uuid.UUID) error {
logger.Log.Info("Запуск синхронизации накладных", zap.String("user_id", userID.String()))
// Получаем клиент и инфо о сервере
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return err
}
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("active server not found for user %s", userID)
}
serverID := server.ID
// Синхронизация накладных
if err := s.syncInvoices(client, serverID, false); err != nil {
logger.Log.Error("Sync Invoices failed", zap.Error(err))
return err
}
logger.Log.Info("Синхронизация накладных завершена", zap.String("user_id", userID.String()))
return nil
}
func (s *Service) syncSuppliers(c rms.ClientI, serverID uuid.UUID) error { func (s *Service) syncSuppliers(c rms.ClientI, serverID uuid.UUID) error {
list, err := c.FetchSuppliers() list, err := c.FetchSuppliers()
if err != nil { if err != nil {

View File

@@ -9,16 +9,18 @@ import (
"rmser/internal/services/drafts" "rmser/internal/services/drafts"
invService "rmser/internal/services/invoices" invService "rmser/internal/services/invoices"
"rmser/internal/services/sync"
"rmser/pkg/logger" "rmser/pkg/logger"
) )
type InvoiceHandler struct { type InvoiceHandler struct {
service *invService.Service service *invService.Service
draftsService *drafts.Service draftsService *drafts.Service
syncService *sync.Service
} }
func NewInvoiceHandler(service *invService.Service) *InvoiceHandler { func NewInvoiceHandler(service *invService.Service, syncService *sync.Service) *InvoiceHandler {
return &InvoiceHandler{service: service} return &InvoiceHandler{service: service, syncService: syncService}
} }
// SendInvoice godoc // SendInvoice godoc
@@ -85,3 +87,26 @@ func (h *InvoiceHandler) GetInvoice(c *gin.Context) {
c.JSON(http.StatusOK, dto) c.JSON(http.StatusOK, dto)
} }
// SyncInvoices godoc
// @Summary Запустить синхронизацию накладных
// @Description Запускает синхронизацию накладных для пользователя
// @Tags invoices
// @Produce json
// @Success 200 {object} map[string]string
// @Failure 500 {object} map[string]string
func (h *InvoiceHandler) SyncInvoices(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
err := h.syncService.SyncInvoicesOnly(userID)
if err != nil {
logger.Log.Error("Ошибка синхронизации накладных", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка синхронизации"})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"message": "Синхронизация запущена",
})
}

View File

@@ -2,11 +2,19 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { List, Typography, Tag, Spin, Empty, DatePicker, Flex } from "antd"; import {
List,
Typography,
Tag,
Spin,
Empty,
DatePicker,
Flex,
Button,
} from "antd";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
ArrowRightOutlined, ArrowRightOutlined,
HistoryOutlined,
CheckCircleOutlined, CheckCircleOutlined,
DeleteOutlined, DeleteOutlined,
PlusOutlined, PlusOutlined,
@@ -14,53 +22,56 @@ import {
LoadingOutlined, LoadingOutlined,
CloseCircleOutlined, CloseCircleOutlined,
StopOutlined, StopOutlined,
SyncOutlined,
CloudServerOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { api } from "../services/api"; import { api } from "../services/api";
import type { UnifiedInvoice } from "../services/types"; 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();
// Состояние фильтра дат: по умолчанию последние 7 дней // Состояние фильтра дат: по умолчанию последние 7 дней
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ const [startDate, setStartDate] = useState<Dayjs>(dayjs().subtract(7, "day"));
dayjs().subtract(7, "day"), const [endDate, setEndDate] = useState<Dayjs>(dayjs());
dayjs(), const [syncLoading, setSyncLoading] = useState(false);
]);
// Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос) // Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос)
const { const {
data: invoices, data: invoices,
isLoading, isLoading,
isError, isError,
refetch,
} = useQuery({ } = useQuery({
queryKey: [ queryKey: [
"drafts", "drafts",
dateRange[0].format("YYYY-MM-DD"), startDate.format("YYYY-MM-DD"),
dateRange[1].format("YYYY-MM-DD"), endDate.format("YYYY-MM-DD"),
], ],
queryFn: () => queryFn: () =>
api.getDrafts( api.getDrafts(
dateRange[0].format("YYYY-MM-DD"), startDate.format("YYYY-MM-DD"),
dateRange[1].format("YYYY-MM-DD") endDate.format("YYYY-MM-DD")
), ),
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}); });
const getStatusTag = (item: UnifiedInvoice) => { const handleSync = async () => {
if (item.type === "SYNCED") { setSyncLoading(true);
return ( try {
<Tag icon={<HistoryOutlined />} color="success"> await api.syncInvoices();
Синхронизировано refetch();
</Tag> } finally {
); setSyncLoading(false);
} }
};
const getStatusTag = (item: UnifiedInvoice) => {
switch (item.status) { switch (item.status) {
case "PROCESSING": case "PROCESSING":
return ( return (
@@ -95,19 +106,19 @@ export const DraftsList: React.FC = () => {
case "NEW": case "NEW":
return ( return (
<Tag icon={<PlusOutlined />} color="blue"> <Tag icon={<PlusOutlined />} color="blue">
Новый Новая
</Tag> </Tag>
); );
case "PROCESSED": case "PROCESSED":
return ( return (
<Tag icon={<CheckCircleOutlined />} color="green"> <Tag icon={<CheckCircleOutlined />} color="green">
Обработан Проведена
</Tag> </Tag>
); );
case "DELETED": case "DELETED":
return ( return (
<Tag icon={<DeleteOutlined />} color="red"> <Tag icon={<DeleteOutlined />} color="red">
Удален Удалена
</Tag> </Tag>
); );
default: default:
@@ -133,9 +144,16 @@ export const DraftsList: React.FC = () => {
return ( return (
<div style={{ padding: "0 4px 20px" }}> <div style={{ padding: "0 4px 20px" }}>
<Title level={4} style={{ marginTop: 16, marginBottom: 16 }}> <Flex align="center" gap={8} style={{ marginTop: 16, marginBottom: 16 }}>
<Title level={4} style={{ margin: 0 }}>
Накладные Накладные
</Title> </Title>
<Button
icon={<SyncOutlined />}
loading={syncLoading}
onClick={handleSync}
/>
</Flex>
{/* Фильтр дат */} {/* Фильтр дат */}
<div <div
@@ -152,13 +170,24 @@ export const DraftsList: React.FC = () => {
> >
Период загрузки: Период загрузки:
</Text> </Text>
<RangePicker <Flex gap={8}>
value={dateRange} <DatePicker
onChange={(dates) => dates && setDateRange([dates[0]!, dates[1]!])} value={startDate}
style={{ width: "100%" }} onChange={(date) => date && setStartDate(date)}
allowClear={false} style={{ flex: 1 }}
placeholder="Начало"
format="DD.MM.YYYY" format="DD.MM.YYYY"
allowClear={false}
/> />
<DatePicker
value={endDate}
onChange={(date) => date && setEndDate(date)}
style={{ flex: 1 }}
placeholder="Конец"
format="DD.MM.YYYY"
allowClear={false}
/>
</Flex>
</div> </div>
{isLoading ? ( {isLoading ? (
@@ -194,6 +223,9 @@ export const DraftsList: React.FC = () => {
<Text strong style={{ fontSize: 16 }}> <Text strong style={{ fontSize: 16 }}>
{item.document_number || "Без номера"} {item.document_number || "Без номера"}
</Text> </Text>
{item.type === "SYNCED" && (
<CloudServerOutlined style={{ color: "gray" }} />
)}
{item.is_app_created && ( {item.is_app_created && (
<span title="Создано в RMSer">📱</span> <span title="Создано в RMSer">📱</span>
)} )}

View File

@@ -127,6 +127,7 @@ export const InvoiceDraftPage: React.FC = () => {
onSuccess: (data) => { onSuccess: (data) => {
message.success(`Накладная ${data.document_number} создана!`); message.success(`Накладная ${data.document_number} создана!`);
navigate("/invoices"); navigate("/invoices");
queryClient.invalidateQueries({ queryKey: ["drafts"] });
}, },
onError: () => { onError: () => {
message.error("Ошибка при создании накладной"); message.error("Ошибка при создании накладной");
@@ -499,7 +500,8 @@ export const InvoiceDraftPage: React.FC = () => {
{totalSum.toLocaleString("ru-RU", { {totalSum.toLocaleString("ru-RU", {
style: "currency", style: "currency",
currency: "RUB", currency: "RUB",
maximumFractionDigits: 0, minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} })}
</span> </span>
</div> </div>

View File

@@ -78,17 +78,6 @@ export const InvoiceViewPage: React.FC = () => {
key: "quantity", key: "quantity",
align: "right" as const, align: "right" as const,
}, },
{
title: "Цена",
dataIndex: "price",
key: "price",
align: "right" as const,
render: (price: number) =>
price.toLocaleString("ru-RU", {
style: "currency",
currency: "RUB",
}),
},
{ {
title: "Сумма", title: "Сумма",
dataIndex: "total", dataIndex: "total",
@@ -187,10 +176,10 @@ export const InvoiceViewPage: React.FC = () => {
size="small" size="small"
summary={() => ( summary={() => (
<Table.Summary.Row> <Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={3}> <Table.Summary.Cell index={0} colSpan={2}>
<Text strong>Итого:</Text> <Text strong>Итого:</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={3} align="right"> <Table.Summary.Cell index={2} align="right">
<Text strong> <Text strong>
{totalSum.toLocaleString("ru-RU", { {totalSum.toLocaleString("ru-RU", {
style: "currency", style: "currency",

View File

@@ -251,4 +251,8 @@ export const api = {
const { data } = await apiClient.get<InvoiceDetails>(`/invoices/${id}`); const { data } = await apiClient.get<InvoiceDetails>(`/invoices/${id}`);
return data; return data;
}, },
syncInvoices: async (): Promise<void> => {
await apiClient.post('/invoices/sync');
},
}; };