- 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).
9.0 KiB
گزارش گپهای دفتر موجودی (Inventory Ledger Gaps)
بررسی کامل منطق تراکنشهای انبار (بکاند + فرانتاند) در جریان بازطراحی صفحه «تغییر موجودی». این گزارش فقط فهرست گپهاست برای تصمیمگیری معماری — بهجز مورد ۱، هیچکدام در این نسخه اصلاح نشدهاند.
وضعیتها: 🔴 ریسک بالای داده · 🟡 ریسک متوسط · 🔵 بهبود
۱. ✅ اصلاحشده در همین نسخه (پیشنیاز فیچر برگشت)
GET /inventory/transactions?date_from=&date_to= → HTTP 500
(backend/app/routers/inventory.py — list_transactions)
ستون transaction_date (timestamptz) با رشتهی خام مقایسه میشد. درایور asyncpg تایپ سختگیرانه دارد و این مقایسه با خطای operator does not exist: timestamp with time zone >= character varying کرش میکرد. هیچجای UI قدیمی این فیلترها را صدا نمیزد، برای همین مخفی مانده بود.
اصلاح: پارامترهای YYYY-MM-DD به datetime آگاه از UTC تبدیل میشوند (YYYY-MM-DD 00:00 UTC تا +1 روز) — همان معناشناسی کستِ قدیمی رشته در PostgreSQL با timezone = Etc/UTC. ⚠️ نکتهی مرتبط: مورد ۱۲.
۲. 🔴 سقف برگشت و اتصال REF فقط در فرانت است
بکاند RETURN را دقیقاً مثل PURCHASE (افزایش ساده) میپذیرد (inventory_service.py:268-299) — نه به برداشت خاصی وصل است و نه سقفی دارد. با فراخوانی مستقیم API میتوان موجودی یک انبار را بینهایت تورم داد.
UI جدید این را کنترل میکند (تگ [REF:<tx-id>] در note + محاسبهی مانده + سقف ورودی)، اما خود دفتر از سمت سرور محافظت نمیشود.
پیشنهاد برای تصمیمگیری: اعتبارسنجی سمت سرور — برگشت حتماً ref_transaction_id بگیرد و سرویس مانده را با قفل ردیف چک کند؛ یا جدول attribution جداگانه.
۳. 🔴 TRANSFER_IN مستقل بدون جفت پذیرفته میشود
POST /inventory/transactions با نوع TRANSFER_IN بدون هیچ TRANSFER_OUT متناظری پذیرفته میشود (سرویس آن را افزایش عمومی حساب میکند). یعنی از مسیر API میتوان «ورودی انتقال» جعلی ثبت کرد. فقط جفتِ واقعیِ ساختهشده توسط خود سرویس transfer_pair_id دارد.
پیشنهاد: TRANSFER_IN از مسیر عمومی بسته شود (فقط از طریق جفتسازی داخلی سرویس ثبت شود).
۴. 🟡 create انتقال بینکارگاهی «ردیف انبار» نمیگیرد
POST /inventory/project-transfers بهجای inventory_row_id فقط material/brand/unit/specs میگیرد؛ اگر match دقیق پیدا نشود، سرویس با fallback امتیازدهی «بهترین کاندیدای همان متریال» را کسر میکند (project_transfer_service.py:105-151). اگر یک متریال با چند ردیف specs نزدیک به هم باشد، ممکن است از ردیفی غیر از قصد کاربر کسر شود.
پیشنهاد: پذیرش inventory_row_id اختیاری در payload و کسر دقیق از همان ردیف.
۵. 🟡 Audit Trail ناقص است
فقط POST /inventory/transactions رکورد audit مینویسد (inventory.py:491-500). اینها audit ندارند:
- ساخت / ویرایش (PATCH) / لغو (DELETE) انتقال بینکارگاهی
- تأیید تحویل PM (
/pm/receiptsو دریافت انتقال) - CRUD انبارها و تغییرات کاربران
پیشنهاد: گسترش audit_record به این مسیرها (helper موجود آماده است و هرگز raise نمیکند).
۶. 🟡 فیلدهای الزامی کلیدی فقط در UI اعمال میشوند
delivered_to(تحویلگیرنده برداشت) در UI اجباری است؛ API بدون آن 201 میدهد.- ظرفیتسنجی مشابه برای «توضیح» (note) شمارش موجودی و… .
۷. 🟡 رفتار postSeq قدیمی هنگام خطای وسط بچ
در بخشهای لیستی ساده (برداشت/ضایعات/اتلاف/مفقودی/انتقال داخلی) اگر ثبت ردیف kام شکست بخورد، ردیفهای ۱..k-1 موفق ثبتشده در جدول باقی میمانند → retry کاربر = ثبت تکراری. (در انتقال بینکارگاهی و برگشتِ این نسخه، ردیفهای موفق از لیست حذف میشوند — الگوی درست.)
پیشنهاد: انتقال الگوی «حذف ردیف موفق + خطای inline» به postSeq.
۸. 🟡 برگشتهای قدیمی بدون REF قابل انتساب دقیق نیستند
برگشتهای ثبتشده قبل از این نسخه هیچ اشارهای به برداشت مرجع ندارند. UI با تخصیص FIFO بین برداشتهای همردیف (متریال+برند+مشخصات+واحد) جبران میکند، ولی این حدس است و در دو حالت خطا دارد:
- برگشت قدیمی به انباری غیر از انبار مبدأ → قابل انتساب نیست و از مانده کم نمیشود.
- تخصیص FIFO صفحات اولِ واکشی ممکن است برگشتِ متعلق به برداشتِ صفحههای بعد را جابهجا کند.
پیشنهاد (داده): اسکریپت یکبارهی backfill — تطبیق برگشتهای قدیمی با برداشتهای همان ردیف (قدیمیترین برداشتِ باقیمانده) و درج REF در note.
۹. 🔵 معناشناسی تاریخ فیلترها = UTC (نه تهران)
فیلتر date_from/date_to (بعد از اصلاح مورد ۱) مرز روز را در UTC میبرد، در حالی که UI جلالی و ساعت کاربر +03:30 است. برداشت ساعت ۰۰:۰۰–۰۳:۳۰ تهران در «دیروز» UTC میافتد. این معناشناسی قدیمی حفظ شد؛ اگر روزِ تهران ملاک است، باید offset منطقه (یا پارامتر tz از کلاینت) اضافه شود.
۱۰. 🔵 GET /inventory/{id}/transactions enrichment ناقص
پاسخ این endpoint فیلدهای material/brand/specs/unit را ندارد (فقط GET لیست کامل enrich میکند). UI فعلی از آن استفاده نمیکند ولی برای دیباگ/گزارشها گمراهکننده است.
۱۱. 🔵 سقف ۱۰۰ رکوردی واکشیهای لیست
GET /inventory و lookup ها در useInventoryData با page_size=100 واکشی میشوند؛ انبار با بیش از ۱۰۰ رکورد موجودی در UI کامل نمایش داده نمیشود (محدودیت قدیمی، در لیستهای جدید این صفحه هم از همان دیتا میخواند). لیست برگشت و مفقودیها صفحهبندی/واکشی کامل دارند.
۱۲. 🔵 لیست انتقالهای بینکارگاهی بدون صفحهبندی
GET /inventory/project-transfers و /pm/transfers/on-the-way همهی ردیفها را بدون pagination برمیگردانند؛ با رشد داده بار پاسخ زیاد میشود.
چیزهایی که بررسی شد و درست است ✅
- Immutable Ledger در سطح API: برای تراکنشها هیچ endpoint ویرایش/حذفی وجود ندارد (فقط POST و GET). تنها حذفِ موجود، لغو انتقال بینکارگاهی است که تراکنش خروجی خودش را بازمیگرداند و موجودی را ترمیم میکند.
- سقف FOUND در بکاند اعمال میشود: بیش از «Σ|LOSS| − ΣFOUND» همان ردیف → 422 (تست شد).
- STOCK_COUNT فقط روی رکورد موجود و با ثبت اختلاف علامتدار صحیح.
- TRANSFER_OUT جفتی: کسر مبدأ و ایجاد TRANSFER_IN مقصد با
transfer_pair_idمشترک و قفل ردیف؛ موجودی منفی نمیشود. - دریافت انتقال بینکارگاهی: مجموع allocations باید دقیقاً برابر مقدار انتقال باشد و انبارها به پروژه مقصد تعلق داشته باشند.
- هویت ردیف انبار با
specs_hash(SHA256 مرتبشدهی specs) و sentinel برند NULL یکتاست — ریسک ادغام رکوردهای متفاوت وجود ندارد.