Add CRM order queue: live sync, view modal, receipt tabs, delete
Wires the CRM (exoCRM GetOrders) into the dashboard as a locally persisted order queue instead of the previous static mockup: - CrmClient Protocol + ExoCrmClient/StubCrmClient for the CRM's signed JSON-RPC API - Order model + migration, synced from CRM on each queue view; soft-deleted orders stay hidden across re-syncs - GET/DELETE /api/v1/orders with "no receipt"/"receipt issued" tabs (the latter is empty until Checkbox fiscalization lands) - Dashboard: real order list, item-detail modal, tab switcher, one-click delete Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,14 @@ Alembic автогенерирует миграции по `Base.metadata`, по
|
||||
|
||||
from app.db.base import Base
|
||||
from app.db.models.audit import AuditAction, AuditLog
|
||||
from app.db.models.order import Order
|
||||
from app.db.models.user import RefreshToken, User, UserRole
|
||||
|
||||
__all__ = [
|
||||
"AuditAction",
|
||||
"AuditLog",
|
||||
"Base",
|
||||
"Order",
|
||||
"RefreshToken",
|
||||
"User",
|
||||
"UserRole",
|
||||
|
||||
@@ -31,6 +31,7 @@ class AuditAction(str):
|
||||
USER_CREATED = "user.created"
|
||||
USER_UPDATED = "user.updated"
|
||||
USER_DEACTIVATED = "user.deactivated"
|
||||
ORDER_DELETED = "order.deleted"
|
||||
|
||||
|
||||
class AuditLog(UUIDPrimaryKeyMixin, Base):
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Локальная копия заказов, синхронизированная из CRM.
|
||||
|
||||
Живёт отдельно от live-проксирования (`app/services/crm/`), чтобы кассир мог
|
||||
скрыть ненужный заказ (soft-delete) и он не появился снова при следующей
|
||||
синхронизации, и чтобы отличать заказы без чека от уже пробитых.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import BigInteger, Boolean, DateTime, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Order(TimestampMixin, Base):
|
||||
__tablename__ = "orders"
|
||||
|
||||
# PK — собственный ID заказа в CRM, а не сгенерированный UUID: синхронизация
|
||||
# делает upsert по нему.
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
|
||||
create_date_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
recipient_name: Mapped[str | None] = mapped_column(String(255))
|
||||
recipient_phone: Mapped[str | None] = mapped_column(String(32))
|
||||
recipient_email: Mapped[str | None] = mapped_column(String(320))
|
||||
waybill_number: Mapped[str | None] = mapped_column(String(64))
|
||||
notes: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# Деньги — integer копейки (конвенция проекта), а не строка/float.
|
||||
total_amount_kopecks: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
|
||||
# Товарные позиции — снимок из CRM для отображения в модалке. Построчно в
|
||||
# копейки не переводится: не участвует в денежных расчётах, только в показе.
|
||||
goods: Mapped[list[dict[str, Any]]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
|
||||
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
# Заполняется будущей интеграцией с Checkbox — сейчас всегда NULL.
|
||||
receipt_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||
Reference in New Issue
Block a user