Each cash register stores its own encrypted NP API key. Status polling uses register keys and binds an order to the register whose key sees the TTN as its own (PhoneSender present); ETTN receipts are created from that register. Migration 0008 moves the old NOVA_POSHTA_API_KEY into the default register. Closes #3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
3.6 KiB
Python
64 lines
3.6 KiB
Python
"""Локальная копия заказов, синхронизированная из CRM.
|
|
|
|
Живёт отдельно от live-проксирования (`app/services/crm/`), чтобы кассир мог
|
|
скрыть ненужный заказ (soft-delete) и он не появился снова при следующей
|
|
синхронизации, и чтобы отличать заказы без чека от уже пробитых.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, 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))
|
|
|
|
# Заказ правили вручную в карточке — синхронизация из CRM его больше не
|
|
# перезаписывает, иначе правки кассира затирались бы при следующем открытии.
|
|
edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Заполняется будущей интеграцией с Checkbox — сейчас всегда NULL.
|
|
receipt_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
|
|
|
# Статус ТТН Nova Poshta — опрашивается ARQ worker'ом раз в минуту для
|
|
# заказов без чека, у которых есть номер ТТН (см. app/worker.py).
|
|
np_status: Mapped[str | None] = mapped_column(String(255))
|
|
np_status_code: Mapped[str | None] = mapped_column(String(16))
|
|
np_cod_amount_kopecks: Mapped[int | None] = mapped_column(BigInteger)
|
|
np_payment_status: Mapped[str | None] = mapped_column(String(32))
|
|
|
|
# Касса, чей ключ НП видит ТТН как свою (определяется при опросе статусов,
|
|
# см. `services/orders.sync_np_statuses`). От неё создаётся ЕТТН-чек.
|
|
cash_register_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
ForeignKey("cash_registers.id", ondelete="SET NULL"), index=True
|
|
)
|