Add COD-in-transit summary and received tab to the dashboard
- GET /orders/summary: count and sum of cash-on-delivery for parcels not yet picked up (excludes received, refused, deleted/unknown TTNs, paid COD); shown as a card above the tabs. - New «Полученные» tab: orders with NP received codes (9, 10, 11, 106) move there automatically, regardless of receipt; 106 is now a final status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ from datetime import UTC, datetime
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models.order import Order
|
||||
@@ -25,9 +25,15 @@ _NP_BATCH_SIZE = 100
|
||||
# посылку, заказ уходит во вкладку «Отказы».
|
||||
NP_REFUSAL_STATUS_CODES = ("102", "103", "105", "108")
|
||||
|
||||
# Коды NP "відправлення отримано" и отказы — после них статус ТТН больше не
|
||||
# опрашивается.
|
||||
_NP_FINAL_STATUS_CODES = ("9", "10", "11", *NP_REFUSAL_STATUS_CODES)
|
||||
# Коды NP "відправлення отримано" (106 — отримано і створено ЄН зворотньої
|
||||
# доставки) — заказ уходит во вкладку «Полученные».
|
||||
NP_RECEIVED_STATUS_CODES = ("9", "10", "11", "106")
|
||||
|
||||
# Получено или отказ — после этого статус ТТН больше не опрашивается.
|
||||
_NP_FINAL_STATUS_CODES = (*NP_RECEIVED_STATUS_CODES, *NP_REFUSAL_STATUS_CODES)
|
||||
|
||||
# "Видалено" / "Номер не знайдено" — денег по такой ТТН не будет.
|
||||
_NP_DEAD_STATUS_CODES = ("2", "3")
|
||||
|
||||
# Поля, которые кассир может править в карточке заказа (кроме goods/total).
|
||||
_EDITABLE_FIELDS = (
|
||||
@@ -43,6 +49,7 @@ class OrderTab(enum.StrEnum):
|
||||
NO_RECEIPT = "no_receipt"
|
||||
HAS_RECEIPT = "has_receipt"
|
||||
REFUSED = "refused"
|
||||
RECEIVED = "received"
|
||||
|
||||
|
||||
class OrderEditError(Exception):
|
||||
@@ -96,14 +103,19 @@ async def sync_orders_from_crm(session: AsyncSession, crm: CrmClient) -> None:
|
||||
|
||||
|
||||
async def list_orders(session: AsyncSession, *, tab: OrderTab) -> list[Order]:
|
||||
"""Заказы вкладки. Отказ клиента важнее наличия чека: такой заказ показывается
|
||||
только в «Отказах», даже если по нему уже создан ЕТТН-чек."""
|
||||
"""Заказы вкладки. Статус посылки важнее наличия чека: полученный заказ
|
||||
показывается только в «Полученных», отказ — только в «Отказах», даже если по
|
||||
нему уже создан ЕТТН-чек."""
|
||||
refused = Order.np_status_code.in_(NP_REFUSAL_STATUS_CODES)
|
||||
not_refused = Order.np_status_code.is_(None) | ~refused
|
||||
received = Order.np_status_code.in_(NP_RECEIVED_STATUS_CODES)
|
||||
in_transit = Order.np_status_code.is_(None) | Order.np_status_code.not_in(
|
||||
_NP_FINAL_STATUS_CODES
|
||||
)
|
||||
tab_filter = {
|
||||
OrderTab.REFUSED: refused,
|
||||
OrderTab.NO_RECEIPT: Order.receipt_created_at.is_(None) & not_refused,
|
||||
OrderTab.HAS_RECEIPT: Order.receipt_created_at.is_not(None) & not_refused,
|
||||
OrderTab.RECEIVED: received,
|
||||
OrderTab.NO_RECEIPT: Order.receipt_created_at.is_(None) & in_transit,
|
||||
OrderTab.HAS_RECEIPT: Order.receipt_created_at.is_not(None) & in_transit,
|
||||
}[tab]
|
||||
result = await session.scalars(
|
||||
select(Order)
|
||||
@@ -114,6 +126,23 @@ async def list_orders(session: AsyncSession, *, tab: OrderTab) -> list[Order]:
|
||||
return list(result)
|
||||
|
||||
|
||||
async def cod_in_transit(session: AsyncSession) -> tuple[int, int]:
|
||||
"""Наложка в пути: (кол-во посылок, сумма в копейках).
|
||||
|
||||
Учитываются только посылки, которые ещё не забрали и по которым нет отказа:
|
||||
финальный статус NP ("отримано"/отказ) или оплата наложки их исключают.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(func.count(), func.coalesce(func.sum(Order.np_cod_amount_kopecks), 0))
|
||||
.where(Order.is_deleted.is_(False))
|
||||
.where(Order.np_cod_amount_kopecks.is_not(None))
|
||||
.where(Order.np_status_code.not_in((*_NP_FINAL_STATUS_CODES, *_NP_DEAD_STATUS_CODES)))
|
||||
.where(Order.np_payment_status.is_distinct_from("Payed"))
|
||||
)
|
||||
count, total = result.one()
|
||||
return count, total
|
||||
|
||||
|
||||
async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None:
|
||||
"""Обновляет статус ТТН, сумму и статус оплаты наложки по заказам в пути.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user