feat(inventory): list-based ChangeInventoryPage + mobile-first UX + return ledger controls

- All ChangeInventoryPage sections (withdrawal/damage/waste/loss/transfer/
  stock-count/project-transfer) become list-based like the withdrawal
  section: pick a warehouse, see its stock rows, material name is only an
  optional filter; manual material/brand/unit entry removed everywhere
  except PURCHASE.
- RETURN redesigned: day-range presets (today/yesterday/7d/30d + Jalali
  custom range) with server-side filters, immutable-ledger returnable
  remainder per withdrawal (REF:[tx-id] note tag + FIFO attribution for
  legacy returns, round4 float-safe), destination warehouse selector.
- PROJECT_TRANSFER: row-based list with one destination project per batch;
  sequential submits keep failed rows inline with errors and drop
  succeeded rows; history dashboard untouched.
- PURCHASE: unit locked to catalog (search-only Combobox), wizard-style
  '+ register new material' option with yellow warning, optional Key-Value
  specs editor (replaces mandatory text chips), continuous-entry UX
  (warehouse/date kept, auto-focus, session log, duplicate warning).
- Mobile-first: responsive table/cards core (StockTable), numeric keyboards
  (inputMode+pattern), sticky bottom submit bar with dynamic counter above
  MobileTabBar incl. safe-area inset, horizontally scrollable touch chips.
- Backend fix (prereq of RETURN): GET /inventory/transactions date filters
  crashed with 500 under asyncpg (string vs timestamptz); parse to aware
  UTC datetimes (DB runs Etc/UTC).
- docs/inventory-ledger-gaps.md: ledger gap report (server-side return cap,
  standalone TRANSFER_IN, transfer row fallback, audit coverage, legacy
  REF backfill, UTC day semantics, pagination gaps).
This commit is contained in:
2026-10-03 21:56:46 +03:30
parent 1bf55ba8a6
commit 3931baadd5
14 changed files with 1926 additions and 703 deletions
+12 -2
View File
@@ -12,6 +12,7 @@ import base64
import uuid
from contextlib import nullcontext
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from fastapi import APIRouter, Depends, Query, Request
@@ -398,9 +399,18 @@ async def list_transactions(
if transaction_type is not None:
stmt = stmt.where(InventoryTransaction.transaction_type == transaction_type)
if date_from is not None:
stmt = stmt.where(InventoryTransaction.transaction_date >= date_from)
# asyncpg تایپ سخت‌گیرانه دارد: رشته باید به datetime آگاه تبدیل شود.
# دیتابیس با TZ=UTC اجرا می‌شود، پس «YYYY-MM-DD» یعنی نیمه‌شب UTC
# (همان معناشناسی کست قدیمیِ رشته در PostgreSQL).
stmt = stmt.where(
InventoryTransaction.transaction_date
>= datetime.fromisoformat(date_from).replace(tzinfo=timezone.utc)
)
if date_to is not None:
stmt = stmt.where(InventoryTransaction.transaction_date < date_to + " 24:00:00")
stmt = stmt.where(
InventoryTransaction.transaction_date
< datetime.fromisoformat(date_to).replace(tzinfo=timezone.utc) + timedelta(days=1)
)
stmt = sort_query(stmt, InventoryTransaction, sort, default="-transaction_date")
items, meta = await paginate(stmt, page, page_size, db)