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:
@@ -0,0 +1,82 @@
|
||||
"""Синхронизация локальной очереди заказов с CRM и работа с ней."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models.order import Order
|
||||
from app.services.crm.client import CrmClient
|
||||
|
||||
_CRM_STATUS = "APPROVED"
|
||||
|
||||
|
||||
def _to_kopecks(amount: str) -> int:
|
||||
return int((Decimal(amount) * 100).to_integral_value())
|
||||
|
||||
|
||||
def _parse_crm_datetime(value: str) -> datetime:
|
||||
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
async def sync_orders_from_crm(session: AsyncSession, crm: CrmClient) -> None:
|
||||
"""Подтягивает заказы CRM (статус APPROVED) в локальную таблицу.
|
||||
|
||||
Уже скрытые (`is_deleted`) заказы не восстанавливаются и не перезаписываются
|
||||
— иначе кнопка «Удалить» переставала бы работать при следующем открытии
|
||||
дашборда, т.к. CRM продолжает возвращать эти заказы как есть.
|
||||
"""
|
||||
crm_orders = await crm.get_orders(status=_CRM_STATUS)
|
||||
if not crm_orders:
|
||||
return
|
||||
|
||||
existing = await session.scalars(
|
||||
select(Order).where(Order.id.in_(order.id for order in crm_orders))
|
||||
)
|
||||
existing_by_id = {order.id: order for order in existing}
|
||||
|
||||
for crm_order in crm_orders:
|
||||
local = existing_by_id.get(crm_order.id)
|
||||
if local is not None:
|
||||
if local.is_deleted:
|
||||
continue
|
||||
else:
|
||||
local = Order(id=crm_order.id)
|
||||
session.add(local)
|
||||
|
||||
local.create_date_time = _parse_crm_datetime(crm_order.create_date_time)
|
||||
local.recipient_name = crm_order.recipient_name
|
||||
local.recipient_phone = crm_order.recipient_phone
|
||||
local.recipient_email = crm_order.recipient_email
|
||||
local.waybill_number = crm_order.waybill_number
|
||||
local.notes = crm_order.notes
|
||||
local.total_amount_kopecks = _to_kopecks(crm_order.total.amount)
|
||||
local.goods = [good.model_dump(by_alias=False) for good in crm_order.goods]
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def list_orders(session: AsyncSession, *, has_receipt: bool) -> list[Order]:
|
||||
receipt_filter = (
|
||||
Order.receipt_created_at.is_not(None) if has_receipt else Order.receipt_created_at.is_(None)
|
||||
)
|
||||
result = await session.scalars(
|
||||
select(Order)
|
||||
.where(Order.is_deleted.is_(False))
|
||||
.where(receipt_filter)
|
||||
.order_by(Order.create_date_time.desc())
|
||||
)
|
||||
return list(result)
|
||||
|
||||
|
||||
async def delete_order(session: AsyncSession, order_id: str) -> Order | None:
|
||||
order = await session.get(Order, order_id)
|
||||
if order is None or order.is_deleted:
|
||||
return None
|
||||
|
||||
order.is_deleted = True
|
||||
order.deleted_at = datetime.now(UTC)
|
||||
return order
|
||||
Reference in New Issue
Block a user