Files
lux_fiscal/backend/app/schemas/orders.py
T
lauadminandClaude Opus 5.5 518a99197f Add Checkbox ETTN receipts for Nova Poshta COD waybills
Cashier creates an ETTN receipt in Checkbox bound to the TTN with payment
control; Checkbox fiscalizes it itself when the parcel is paid for.

- cash_registers (Fernet-encrypted license key / PIN) and receipts tables
- Checkbox HTTP client + stub (ETTN does not work on test registers)
- two-phase create via ARQ job, timeout reconciliation, cron status polling
- /receipts and /cash-registers API, audit records
- dashboard: per-order and bulk create, prepayment, cancel; cash registers page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:39:00 +03:00

120 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Схемы заказов CRM.
Поля валидируются напрямую из "сырых" PascalCase-ключей ответа CRM через
`Field(alias=...)`, а наружу (в JSON фронту) отдаются как чистые snake_case
имена. Денежные значения остаются строками "как есть" от CRM — конвенция
integer-kopecks из CLAUDE.md относится к будущим персистентным моделям
`Order`/`Receipt`, а не к этому read-only проксирующему эндпоинту.
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
if TYPE_CHECKING:
from app.db.models.order import Order
from app.db.models.receipt import Receipt
class OrderGoodOut(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str = Field(alias="ID")
sku: str = Field(alias="SKU")
name: str = Field(alias="Name")
price: str = Field(alias="Price")
quantity: str = Field(alias="Quantity")
discount_amount: str | None = Field(default=None, alias="DiscountAmount")
discount_percent: str | None = Field(default=None, alias="DiscountPercent")
amount: str = Field(alias="Amount")
class OrderTotalOut(BaseModel):
model_config = ConfigDict(populate_by_name=True)
cost: str = Field(alias="Cost")
quantity: str = Field(alias="Quantity")
weight: str = Field(alias="Weight")
discount_amount: str = Field(alias="DiscountAmount")
discount_percent: str = Field(alias="DiscountPercent")
amount: str = Field(alias="Amount")
# CRM возвращает Quantity/Weight то числом, то строкой в зависимости от
# заказа — приводим к строке единообразно для фронта.
@field_validator("quantity", "weight", mode="before")
@classmethod
def _stringify(cls, value: Any) -> Any:
return str(value) if value is not None else value
class OrderOut(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str = Field(alias="ID")
create_date_time: str = Field(alias="CreateDateTime")
recipient_name: str | None = Field(default=None, alias="RecipientDName")
recipient_phone: str | None = Field(default=None, alias="RecipientPhone")
recipient_email: str | None = Field(default=None, alias="RecipientEmail")
waybill_number: str | None = Field(default=None, alias="Waybill_Number")
notes: str | None = Field(default=None, alias="Notes")
total: OrderTotalOut = Field(alias="Total")
goods: list[OrderGoodOut] = Field(default_factory=list, alias="Goods")
class OrderRowOut(BaseModel):
"""Ответ `GET /orders` — строится из локальной таблицы `orders`, не из CRM напрямую."""
model_config = ConfigDict(from_attributes=True)
id: str
create_date_time: datetime
recipient_name: str | None
recipient_phone: str | None
recipient_email: str | None
waybill_number: str | None
notes: str | None
total_amount: str
goods: list[OrderGoodOut]
has_receipt: bool
np_status: str | None
np_status_code: str | None
np_cod_amount: str | None
np_payment_status: str | None
# Последний ЕТТН-чек по заказу (в т.ч. отменённый/неудачный — для показа причины).
receipt_id: str | None = None
receipt_status: str | None = None
receipt_error: str | None = None
receipt_prepayment: str | None = None
@classmethod
def from_order(cls, order: Order, receipt: Receipt | None = None) -> OrderRowOut:
return cls(
id=order.id,
create_date_time=order.create_date_time,
recipient_name=order.recipient_name,
recipient_phone=order.recipient_phone,
recipient_email=order.recipient_email,
waybill_number=order.waybill_number,
notes=order.notes,
total_amount=f"{order.total_amount_kopecks / 100:.2f}",
goods=[OrderGoodOut.model_validate(good) for good in order.goods],
has_receipt=order.receipt_created_at is not None,
np_status=order.np_status,
np_status_code=order.np_status_code,
np_cod_amount=(
f"{order.np_cod_amount_kopecks / 100:.2f}"
if order.np_cod_amount_kopecks is not None
else None
),
np_payment_status=order.np_payment_status,
receipt_id=str(receipt.id) if receipt else None,
receipt_status=receipt.status.value if receipt else None,
receipt_error=receipt.error if receipt else None,
receipt_prepayment=(
f"{receipt.prepayment_kopecks / 100:.2f}" if receipt else None
),
)