mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
фикс создания фасовки
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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": "Синхронизация запущена",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
format="DD.MM.YYYY"
|
placeholder="Начало"
|
||||||
/>
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user