Архів виконаного¶
Журнал закритих тасок і сесій. Найновіші — зверху. Активний backlog у todo.md, відкладені ідеї в BACKLOG.md.
2026-07¶
2026-07-05 — [ESS] essentials_prozorro → MVP (observer-sync + bid-заготовка + CRM bridge + process-сторінка)¶
Скаффолд R-5 (2026-05-22) доведено до робочого MVP за один сеанс (план → затвердження → реалізація). Детальний spec: ai/domains/essentials_prozorro/module-spec.md.
Ключовий доменний факт, що визначив scope: write-доступ до CDB API Прозорро мають лише акредитовані майданчики-брокери — звичайна компанія фізично не може подати bid через API. Тому submit_bid/publish_tender лишаються чесними NotImplementedError, а ProzorroBid — документ підготовки (подання вручну через майданчик).
Backend: ProzorroTender(MasterDataModel) (дзеркало реєстру, unique (tenant, prozorro_id), trimmed raw_json, FK lead) + ProzorroBid(TransactionModel, no-posting) + міграція 0002; реальний адаптер (feed page-dict + detail, requests, RuntimeError→лог); services/sync.py (log-first, bootstrap descending→prev_page.offset, cursor, caps max_pages/max_details, CPV-prefix OR EDRPOU фільтри, fresh-skip по dateModified, per-item isolation, error/partial/success) — свідомо БЕЗ transaction.atomic навколо HTTP; services/leads.py (Create Lead з duplicate guard, LeadSource code=prozorro); REST tenders/ (RO + create-lead) + bids/ + переписаний trigger-sync + status/ (+tender_count); prozorro_pull command; SANDBOX_BASE виправлено на lb-api-sandbox-2.prozorro.gov.ua (старий хост мертвий).
Frontend: ProzorroSyncPage (шаблон UzCargoSyncPage) — статус-стрічка, Run sync, таблиця Recent tenders з per-row «Створити лід», журнал; ProcessPanel case prozorroSync + REQUIRED_PLUGIN; config.ts: групи Master Data (Тендери) → Documents (Пропозиції) → Processes (Синхронізація) → Configuration; applications.ts + seed_shop → v0.2.0.
Верифікація: pytest 18/18 (--create-db); tsc=0; migrate dev OK; live end-to-end проти sandbox — bootstrap 100 тендерів, fresh-skip 5, курсор просунувся на prev_page.offset, 0 failures. Help-статті en/ua переписані під MVP.
Scope cut (свідомо): реальний submit_bid/publish_tender (broker-only), print template для bid-експорту, celery/cron scheduling (command = хук), webhook, full-feed backfill понад caps, custom tenders list з row-actions, custom bid form, region-фільтр. Playwright UI-прохід — за юзером.
2026-07-05 — [ESS] Прозорро help-стаття (module overview) + Help-кнопка секції¶
frontend/dop/essentials-prozorro/help/essentials-prozorro.md + .ua.md (single-segment slug → кнопка на section landing автоматично) + bundle-індекс ai/domains/essentials_prozorro/help-articles.md. Файли поглинув коміт паралельної сесії 4661babc (containerhub) — concurrent-session pathspec trap; контент коректний, того ж дня переписані під MVP (див. вище).
2026-06¶
2026-06-09…11 — [ESS+ESWF] App Store add-ons track (інтерактивна сесія, груминг-фіксація 2026-06-17)¶
Реконструйовано grooming-сесією 2026-06-17 — трек landed інтерактивно 2026-06-09…06-11 (5 feature-коммітів + великий untitled
00dc4718«ашч» + fix44da9374), але не потрапив у done/todo/session-log. Звірено з кодом + 6 ADR, тести зелені (71/71 у вибраному addon-suite). Маркери комітівашч/магазин— гнилі, але вже на master (history-rewrite не роблю без інструкції).
Суть треку: конверсія Essentials-фіч із EssentialsModuleSettings feature-flags у App Store add-ons, гейтовані kernel-ліцензіями (core.licensing.gate.verify_license). Два нових kernel/essentials-примітиви:
- core.licensing.field_gates (kernel, product-blind) — продукт декларує, що певні поля shared-entity існують лише за наявності add-on'а (register_field_gate(entity, plugin_key, fields)); metadata/OPTIONS-ендпойнти стрипають поля для tenant'а без ліцензії. Колонки лишаються в БД — купівля add-on'а повертає поля без міграції.
- essentials.services.addon_gating — єдине джерело правди «чи активний add-on для tenant'а» (sales_orders / purchase_orders / payment_calendar / accounting), переюзане viewset'ами, posting-сервісами, дашборд-віджетами, CRM.
Що landed (ADR-и, усі Accepted/Implemented 2026-06-09):
- Sales/Purchase Orders → add-ons (adr-orders-as-addons): appSalesOrders/appPurchaseOrders (plugin sales_orders/purchase_orders) замість pseudo-plugin feature-flag'ів. Migration 0091 (drop order-flags). Каталог + seed_shop + CRM-gate + дашборд-віджети.
- AR керується накладними, не рахунками (adr-ar-driven-by-delivery): customer-order ledger ведеться SO/Shipment, off Invoice. Payment-allocation отримав shipment-вимір; aging_report перероблено.
- Manufacturing без Accounting (adr-manufacturing-without-accounting): pure-stock tenant запускає Manufacturing (списання матеріалів + прихід ГП з партіями) без плану рахунків; ledger-проводки придушені коли accounting_enabled=False, inventory side-effects незмінні.
- Item Category accounting-поля field-gated (adr-item-category-accounting-fields): перший консумент field_gates — default_revenue_account/default_cogs_account/default_tax_rate ховаються без appAccounting.
- Payment Calendar → Community add-on (adr-payment-calendar-addon): appPaymentCalendar гейтить Planned Payments + Payment Calendar process/report + cash-flow-forecast widget + planned-payment-проводки на confirm SO/PO. Migration 0092 (PO expected_payment_date) + 0093 (grant payment_calendar).
- Group MDM Hub → add-on (adr-group-mdm-addon): appGroupMdm (plugin group_mdm) — AFP Phase 3 reconciliation workspace тепер App Store add-on; group_mdm/gating.py (require_group_mdm 403-decorator).
Перевірки (груминг 2026-06-17): pytest на addon-suite (payment_calendar / currency_revaluation / item_category / party_ledger / sales_order_patterns / manufacturing_no_accounting / group_mdm) — 71/71 зелені.
Відкриті хвости (→ todo.md P2):
- ✅ appGroupMdm catalog-entry — зроблено 2026-06-17 (той самий груминг-захід): додано App Store-картку в applications.ts (узгоджено з seed_shop), diff seed_shop↔applications.ts чистий, tsc=0. App Store тепер показує Group MDM Hub.
- ✅ Bulk-edit UI у TransactionList — зроблено 2026-06-17 (commit): переюзано generic BulkEditModal — кнопка «Масове редагування (N)» у toolbar-блоці selectedCount > 1. Нуль нового backend-коду. tsc=0, vitest 97/97.
- 🟡 Browser-verify (Playwright не запускався по жодному з add-on'ів).
2026-06-05 — [ESS] Document approval workflow — Phase 1 (PurchaseInvoice posting-gate)¶
- ✅ виконано 2026-06-05 (🧭 implementation; ADR adr-2026-06-05-document-approval-posting-gate accepted). Прив'язав наявний generic
approvals/engine до posting-документа PurchaseInvoice з пост-гейтом (A1): не можна провести (state=posted), доки неstatus='approved'; opt-in = наявністьroute(C4); поріг суми — через готовий S7 conditional routing (approval_routing_context→{amount}). - Backend (усе в
essentials/, нуль правок уapprovals/— паралельна сесія):models/approvable.py(ApprovableTransactionMixin:routeFK + approval-status vocabulary +resolve_approval_route),services/approval_gate.py(approval_gate_errors— pure status-based, без імпорту approvals),views/approval_actions.py(ApprovalDocumentActionsMixin:send-for-approval+approvaltrack, generic over model),approval_sync.py(completion-receiver). PurchaseInvoice: mixin +statuschoices override + gate-виклик уpost_document+route_name/statusread-only у серіалізаторі +approvalRoutesу_build_refs. Migration0078. - Frontend: PurchaseInvoiceForm — Approval-таб (route picker + «Надіслати на узгодження» + reused
ApprovalTrack) + статус-беджа в хедері.tsc --noEmit=0. - Перевірки:
essentials/tests/test_approval_gate.py8/8 (no-route gate open / route blocks / approve opens / reject→rejected blocks / cancel→draft / routing_context / API send→post-blocked-400 / send-without-route-400); PI-регресія (party_ledger+payments+posting) 56/56;manage.py check0 errors; migration застосовано на dev Postgres. - Дорога правда (receiver hardening):
process.target(GenericFK) може бути stale-cache інстансом, тож guardif target.status != mappedпомилково пропускає запис коли stale-значення = mapped → саме це валить 2 латентні тестиinternal_documents(cancel/send_back→draft) на HEAD. Наш receiver guard прибрав — завжди пише черезupdate_fields(idempotent). internal_documents не чіпав (чужий домен), але баг задокументовано. - Scope cut: tenant-level «кожен PI потребує approval» default (Phase 1.1) — поки opt-in per-doc
route. ApprovalInbox уже generic (tasks/minedocument-agnostic) — окремий per-doc-list invalidation після inbox-approve = follow-up. Phase 2 шаблонить OutgoingPayment/PurchaseOrder/GoodsWriteoff/JournalEntry + промоушн mixin/receiver уapprovals/.
2026-06-05 — [ESS] Backlog reconciliation: LocalizedTextField JSONB — stale [ready] → [done]¶
- Без коду — reconciliation. Audit-first на «наступному механічному
[ready]» (long-term-ess.mdLocalizedTextField+translationsJSONB) показав, що задача повністю відвантажена ще в травні 2026, а беклог-маркер застряг на[ready]. Перевірено в живому коді:MasterDataModel.translationsJSONField +name_uaяк@property(не колонка) уcore-pkg/.../models/base.py;Tenant.active_languages/default_language;LocalizedTextField.tsx+useTenantLanguages.ts+utils/localized.ts; commit5619a05d«Phase 3 final — drop name_ua column». Рештаname_ua = CharField— не-MasterData моделі (Role/ScheduledTask/AppRegistry/News), свідомо поза scope. - Дія: маркер виправлено на
[done] 2026-05-17з посиланням на localized-fields.md. Фактична реалізація (Phase 1-3) задокументована в архітектурному доку + git ще в травні — done.md тут лише фіксує закриття stale-трекера. - Наслідок: механічний
[ready]-пул ESS/ESWF тепер вичерпано (Wizard light-theme + bootstrap CI gate зроблено сьогодні; LocalizedText був done). Лишився[ready]лише «session-close version sync» (процедурний, не код). Grooming-gap нотатка в todo.md підтверджується — щоб «далі» знову став механічним, треба дозріти[idea]→[ready]у long-term docs (shape-активність).
2026-06-05 — [ESWF] Bootstrap wizard CI gate + cross-platform dev-test-wizard¶
- ✅ виконано 2026-06-05:
.github/workflows/ci-bootstrap.yml— гейт дистрибутива (того що колега клонує). Path-filter (backend/core-pkg/src/core/bootstrap/templates/**+create_product.py+scripts/dev-test-wizard.py+ сам workflow) → job на ubuntu/py3.11: створює skeleton-venv (python -m venv+ editableeswf-core) і ганяє нову headless-діюdev-test-wizard.py ci-check(scaffold з шаблонів → makemigrations → migrate →manage.py check→ cleanup). Без стека/Playwright/node_modules — лише backend-валідність згенерованого продукту. - Audit-first reframe (премиса беклогу хибна): «скрипт уже є, просто причепити як CI gate» приховувало, що
dev-test-wizard.pyбув Windows-only (venv/Scripts/python.exe,daphne.exe,npx.cmd,powershell,taskkill) → у codespace/CI (Linux) він непрацездатний. Тож реальний scope = cross-platform-фікс + нова headless-дія + workflow. - Bug-2 (codespace-злам, ~дорога правда):
backend/skeleton/manage.pyробитьos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf_skeleton.settings'), але codespace/CI експортуютьDJANGO_SETTINGS_MODULE=eswf.settings.development(DOP) →setdefaultno-op → skeleton/продукт бутяться проти відсутнього у своєму venv пакетаeswf(ModuleNotFoundError: No module named 'eswf'). Фікс: скрипт тепер передає explicitDJANGO_SETTINGS_MODULEу кожен manage.py-subprocess (_django_env), а не покладається на inherited env. - Зроблено у скрипті: (1)
_VENV_BIN/_EXEdetect (Scripts/*.exe↔bin/*) дляSKEL_PY/SKEL_DAPHNE/PLAYWRIGHT_PY; (2)_django_env(settings)helper + explicit settings уcreate_product(eswf_skeleton.settings) іsetup_product_db(eswf_<slug>.settings); (3) діяci-check(idempotent cleanup → create → migrate → check → cleanup-in-finally, чистить дерево навіть при fail). - Перевірки:
py_compileOK;ci-checkзелено end-to-end на Linux з навмисне inherited DOP-env (відтворює codespace-умову) →CI gate PASSED, дерево pristine після (frontend/package.json відновлено, немаbackend/ci_probe/.eswf-product.json); YAMLyaml.safe_loadOK (4 кроки). Незалежний бонус: скрипт тепер працює і в codespace (де раніше падав). - Scope cut: frontend-template gate (tsc згенерованого Vite-app) — поза scope (потребує npm install workspace; 🟡 follow-up). YAML верифіковано локальною симуляцією кроків, не реальним GH-прогоном (push свідомо не роблю без явної інструкції).
2026-06-05 — [ESWF] Wizard light-theme only (forceColorScheme)¶
- ✅ виконано 2026-06-05: bootstrap-wizard форсовано у світлу тему.
frontend/core/bootstrap/src/App.tsx—defaultColorScheme="light"→forceColorScheme="light";index.htmlsplash — прибрано@media (prefers-color-scheme: dark)(light-only end-to-end). tsc=0 (bootstrap + main project refs). - Audit-first reframe: задача була
[ready]у long-term-eswf.md, премиса «застосувати light theme». Аудит показав, щоdefaultColorScheme="light"уже стояв (commit 46aba1fb) — тобто це реальний баг, не green-field. Корінь: bootstrap і DOP ERP ділять originlocalhost:5173(dev port збігається). Mantine 8 colorSchemeManager читає localStoragemantine-color-scheme-value; leftoverdarkз DOP-тоглу перекриваєdefaultColorScheme→ wizard рендериться темним.forceColorSchemeігнорує і localStorage, і OSprefers-color-scheme, і блокуєsetColorScheme— це і є правильний «light-only, без dark variant». - Lesson: для UI що має бути monochrome-light незалежно від користувацького стану —
forceColorScheme, неdefaultColorScheme. Особливо коли два app-и ділять dev-origin. Промоушн[ready]→doneчерез user-вибір (replaced backlog grooming-gap «далі»). - Scope cut: не чіпав DOP-тему (там dark — легітимна опція); не додавав ColorSchemeScript у bootstrap (forceColorScheme робить його зайвим — FOUC неможливий бо схема фіксована).
2026-06-03 — [LOG] LotAccumulationPlan — факт-флоу (actual_tons / collected_tons / статуси)¶
- ✅ виконано 2026-06-03 (Опція Б): модель
CollectionOrderFulfilment(logistic-side link, exactly-one dispatch|reception, uniq-per-doc mis-attribution guard,is_valid_evidenceдля stale) + сервісlot_fact_service.py(suggest/link/unlink/auto_match/recompute — матчGrainDispatch(shipped, силос) +GrainReception(posted, supplier+grade) у вікні плану) + 4 API-екшени на lotPlans (fact-suggestions/link-fulfilment/unlink-fulfilment/auto-match) + nestedfulfilmentsу CollectionOrderSerializer + migration 0009. Seed: вікно −70дн + auto_match → демо-Дошка засвічена (tenant 64: пшениця 8 docs→26215/100000т, ячмінь 1→3167/40000т, in_execution). Фронт без змін — LotBoard.tsx уже рахує fill% зactual_tonsper-order; бракувало заповнення. Subtable лишився read-only (Б, не А). 8 тестів зелені (own/ext roll-up, window+mis-attr guard, collected==Σ+completed, in_execution, cancel-drops+unlink, wrong-kind reject, auto_match); check clean. Bundle logistic/lot-accumulation-optimizer.md § Plan vs fact. Scope cut: auto-confirm unambiguous → follow-up; rail-waybill окремий arrival-path (dispatch несе polymorphic transport doc). - Shape ратифіковано 2026-06-03: ADR adr-2026-06-03-lot-accumulation-plan-fact-rollup.md — Опція Б (auto roll-up service) у формі «explicit-link + manual-confirm MVP». Roll-up сервіс у
logisticматчитьGrainDispatch(shipped, силос∈плану) +GrainReception(posted, supplier∈плану) у вікніcollection_from..toза grade → пропонує прив'язку доCollectionOrder, диспетчер підтверджує.actual_tons=Σ прив'язаних,collected_tons=Σ orders,plan.status→in_execution/completed. Link-слот живе на боціlogistic(FK-напрям logistic→grain чистий; зворотний заборонено E9). Відхилено: А (дублює тонни з фіздокументів → дрейф; documented fallback), В (не фіксить fill%).
2026-06-03 — [LOG] Оптимізатор — де-хардкод вхідних даних + реальне джерело цін докупівлі¶
- ✅ виконано 2026-06-03 (ADR adr-2026-06-03-optimizer-dehardcode, осі A1+B-geo+C3+D1): B-geo —
Site.latitude/longitude(міграціяgatehouse/0020) +_site_distance_to_port(site, port)= haversine×ROAD_DETOUR_FACTORдоplan.destination_port(раніше ігнорувався → неявний Миколаїв), fallback наDISTANCE_BY_SITE_CODEпри відсутності координат. A1 — UI-екранgrainSupplyOffers(Logistics→Master Data→Grain Sourcing, kernel-форма). C3 — rail-константи → settings-overridable (LOGISTIC_RAIL_*через_cfg()); реальна модель ЖД-тарифу винесена у C2. D1 — прибрано тихий8000 ₴/тfallback: silo безBatch.purchase_priceвиключається + tonnage уoptimizer_notes/summary['excluded_own_tons']. Сидери backfill'ять координати. Регрес-тест «Миколаїв↔Південний» + 5 нових тестів; 13/13 plugin-suite зелені;checkclean; tsc=0. Bundle lot-accumulation-optimizer.md § Cost-model assumptions. Scope cut: offer geo (manual distance_km лишається), A3 (offer FK→PurchaseOrder), C2 (RailFreightTariff — лишилось у backlog як 👤).
2026-06-03 — [ESWF] Phase B.5 — per-tenant capability toggle (Tenant.enabled_capabilities + Admin Tools UI)¶
- ✅ Audit-first 2026-06-03: запис був stale («implementation відсутня») — насправді повністю реалізовано: поле
Tenant.enabled_capabilities(міграція core/0006) + PATCH-endpoint admin.py:585-594 (read+write+валідація) + UICapabilitiesToolв Admin Tools з 3-стейтними картками (active/installed-off/available) через capabilityCatalog.ts + useCapabilities.ts. Long-term-eswf § DevTools рядок 95 ([done]) був правий. Shape-питання (per-tenant vs per-role) знято — реалізовано per-tenant. ADR adr-2026-05-18-frontend-core-capabilities. Залишок (окремі follow-up'и): per-tenantINSTALLED_APPS(backend pairing — окремий ADR, deferred); wizard «activate capabilities at creation time».
2026-06-02 — [SHC+LOG] Grain full-lifecycle seed + least-cost sourcing optimizer¶
- Shipped: (1)
seed_grain_export_case(shipcore_grain) — 6-елеваторна мережа (12 силосів) + усі 16 родин зернових документів end-to-end для кейсу Зерноплюс (tenant 64), через сервіси модуля, ідемпотентно. (2)seed_railway_waybills(shipcore_rail) — ЖД-накладні (hopper→Миколаїв) + авто-ТТН, лінк до GrainDispatch. (3) logistic-плагін воскрешено як least-cost sourcing optimizer:LotAccumulationPlan/CollectionOrder/GrainSupplyOffer+ greedy-алгоритм (свої елеватори → докупівля, rail/road по landed cost) + API +seed_grain_lot_accumulation. (4) LotBoard.tsx на реальний/board/endpoint. (5) seed_demo фазиcase_grain_export_inland/case_grain_rail_waybills/case_grain_lot_accumulation. Verified tenant 64: пшениця 100к=власні 57.25к+докупівля 42.75к, ячмінь 40к=23.2к+16.8к; commits 437aad1/34935d8/5c81d1e/265748c. - ADR: adr-2026-06-02-grain-sourcing-optimizer.md. Bundle: logistic/lot-accumulation-optimizer.md.
- Scope cut: Lot Status звіт (Report Framework) + реальна модель ЖД-тарифу (зараз хардкод ₴/т-км) + premium/discount settlement tuning — окремі follow-up'и.
2026-06-01 — [SHC] shipcore_pricing Phase 7 — impl per concept ADR (P7a+P7b+P7c)¶
- ✅ P7c quote builder виконано 2026-06-01: моделі
FuelSurchargePlan+Accessorial(MasterData). Servicequote.py:compute_fsc((current−base)/100×consumption×km, floored 0) +build_quote— три цифри: floor (CPM×loaded + CPM×deadhead×factor), market (lane_median×distance), margin_band (base+FSC+accessorials × margin%). D5-1 service-only (нічого не персистить). APIPOST cpm-snapshots/quote/. EntityRegistry fuelSurchargePlans/accessorials. FrontendQuoteBuilderPROCESS-сторінка (3 result cards) + 2 MasterData у Fleet→Freight Pricing subgroup (plugin-gated). role transport_dispatcher _crud. 3 тести + 14 pricing-suite зелені; migrate 0003 OK; tsc=0. Phase 7 complete — concept→P7a→P7b→P7c. Bundle оновлено. - ✅ P7b lane history виконано 2026-06-01:
LaneRateHistory(TenantAwareModel — from/to LocationPoint + denorm labels + cargo/vehicle_type + rate/rate_per_km + source_waybill idempotency). Servicelane.py:record_from_waybill+lane_median. Auto-recordsignals.pypost_save Waybill(state=posted, route).pricing_backfill_lanescommand. Reportlane-benchmark+ EntityRegistrylaneRateHistory+ frontend item Fleet→Reports + role transport_dispatcher VIEW. 6 тестів + 11 pricing-suite зелені; migrate 0002 OK; tsc=0. Bundle оновлено. - ✅ P7a costing виконано 2026-06-01: нова app
shipcore_pricing(plugin). Servicecompute_cpm(vehicle, period)—CPM = (Σ DepreciationEntry + Σ FuelJournal×WAC + Σ PieceworkSalaryJournal.amount) / Σ posted-Waybill.distance;snapshot_cpm(idempotent freeze). ModelCpmSnapshot(RegisterModel) + EntityRegistry.cost-per-kmReportDefinition + API cpm-snapshots/ + compute/ + freeze/. FrontendCostPerKmReport+ config item Fleet→Reports. role transport_dispatcher_crud(cpmSnapshots). 5 тестів зелені; migrate OK; tsc=0. Bundle shipcore_pricing/README.md. Знахідка: журнали НЕ всі money-bearing (Fuel=літри, Amortization=знос) — гроші з DepreciationEntry/WAC/PieceworkSalary.amount. - Concept ратифіковано 2026-06-01: ADR adr-2026-06-01-shipcore-pricing-concept.md. D1-1 (reuse
essentials.ContractSpecification) + D3-1 (lane = LocationPoint-пара) + D2-1 + D4-1 + D5-1. Відкриття: узгоджені тарифи вже є (ContractSpecification + distance scale), всі CPM-інпути готові → shipcore_pricing НЕ зберігає тарифи, а рахує/рекомендує. Поза scope: index-linked FSC, persisted FreightQuote (D5-2), dynamic/yield pricing.
2026-06-01 — [ESS] Variant reports UI — impl per ADR¶
- ✅ виконано 2026-06-01: 3 ReportDefinition (
sales-by-variant/purchase-by-variant/stock-by-variant) уessentials_variants/reports.pyспільним_variant_pivot()— rows=template × columns=обраний pivot_attribute (KeyTextTransform на attribute_signature) + total + «—» bucket; cell drilldown → invoices/purchaseInvoices/batches; measure qty|amount. ReportRegistry.register у apps.py. FrontendVariantReports.tsx+ 3 config items REPORT у Reports→Variants (plugin-gated). Core enhancement:<Report>shell прокидає довільні defaultFilters у запит. 6 нових тестів зелені; tsc=0. Bundle variants/reports.md. Scope cut: margin/COGS (D4-3); Mantine heatmap; Items drilldown; external BI. - Shape ратифіковано 2026-06-01: ADR adr-2026-06-01-variant-reports-ui.md — D1=a (ReportDefinition pivot) + D4-2 (Family ×3) + D2-1 + D3-1 + D5-1. Відкриття: Report framework вже підтримує pivot → нуль нового React.
2026-06-01 — [ESWF] Licensing hardening — захист платних модулів (3 критичні gap'и)¶
- ✅ C-1/C-2/C-3 реалізовано 2026-06-01 (ADR adr-2026-06-01-signed-licenses, bundle protection-guide § 6). Крипто-примітив переїхав у kernel
core/crypto.py(edo/crypto.py→ re-export).core/licensing/(tokens/keys/gate/mixins) +core.InstallIdentity+License.signed_token(migration core/0012) +issue_license(vendor, priv поза репо) +expire_licenses(cron) + signed-token гілка вactivate+GET /licenses/install-id/. 4 наявні gate'и делегують уverify_license;HasModuleLicense/LicensedViewSetMixinдля решти. 12 нових тестів + 81 регресія зелені. Ключова інваріанта доведена: ручнийINSERTLicense-рядка без валідного токена для staged-плагіна → доступу нема. - Залишок (поетапно, L4): навісити mixin на решту платних ViewSet'ів + seed підписаних ліцензій demo-тенантам (вимагає re-issue) — вмикається per-plugin через
LICENSE_SIGNED_REQUIRED.
2026-06-01 — [SHC] GrainContract (GAFTA trade contract) — impl per ADR¶
- ✅ виконано 2026-06-01:
GrainContract(TransactionModel)(backend/shipcore_grain/models/grain_contract.py) + servicegrain_contract_service.py(confirm/unpost/advance, reconciliation-only) + serializer (computed resolved buyer/seller/currency C-1) + viewset (form-refs/form-data + post_document/unpost_document/advance_status) + DRF route/grain-contracts/+ EntityRegistrygrainContracts+ migration 0017 + frontend subgroupshipcoregraindocstrade(«Контракти») → Documents (formType default) + role_catalog (terminal_dispatcher_crud_post, accountant VIEW). 11 нових тестів + 30 grain suite зелені; tsc=0; migrate OK. Bundle grain_contract.md. Scope cut: premium/discount наповнення + settlement → H-5; GAFTA PDF → new-print-template; back-to-back trades поза MVP. - Shape ратифіковано 2026-06-01: ADR adr-2026-06-01-grain-contract-shape.md — Опція C (wrap-патерн) + C-1.
GrainContractз optional FK→essentials.Contract(fallback buyer/seller FK→Client) + GAFTA-терми + statedraft→confirmed→in_execution→closed(+washed_out/defaulted). Reconcile D-5. E9: essentials НЕ імпортує grain.
2026-06-01 — [SHC] Lab module — Kanban процес поверх GrainQualityCertificate¶
- ✅ виконано 2026-06-01 (option a): новий лін
LabSample(TenantAwareModel, OneToOne→GrainReception) — workflow-трекер лаб-стадій що передують сертифікату (collected→prep→analyzing→results_ready→certified +rejected). Сертифікат не дублюється FK (резолв черезreception.quality_certificate). Авто: reception→quality_pending спавнить зразок, cert.issued→certified, cert.voided→назад у results_ready.certified— service-only.LabSampleViewSet(lab-samples/board/+move/), servicelab_sample_service.py, migration 0016. FrontendLabKanbanBoard.tsx(PROCESS, 6 lanes) під Grain→Processes→Laboratory. ADR adr-2026-06-01-grain-lab-kanban.md; 7 тестів + 19 grain suite зелені; tsc 0. Відхилено: (b) LaboratoryWorkstation overkill; (c) essentials_quality.QualityInspection не grain-aware. Scope cut: re-sampling (1:1), drag-drop (arrows), SLA-таймери.
2026-06-01 — [SHC] new-module agro_field — рослинництво (поля, сівозміна, агрохімія, ВТМ)¶
- ✅ виконано 2026-06-01 (MVP). Backend app
agro_field(5 моделей) + bridgecreate_grain_reception_from_harvest(Shape A — Harvest→GrainReception, idempotent, defensive guard). Registered: ESWF_AVAILABLE_PLUGINS + _PLUGIN_URLS + EntityRegistry + frontend config/sections/applications + seed_shop AppEntry (app_type=module) + roleagronomist. Requiresshipcore_grain(hard FK). migrate OK, tsc=0,manage.py checkclean. Інтеграція з ККУ — головний deliverable: docs/ai/architecture/agro-shipyard-grain-integration.md. Scope cut: GL posting, print, seed, reports, custom React forms — deferred.
2026-06-01 — [SHC] new-module shipyard — суднобудування (проекти, КС-2/КС-3, специфікації)¶
- ✅ виконано 2026-06-01 (MVP). Backend app
shipyard(5 моделей: ShipyardProject, WorkBreakdownItem hierarchical WBS, BuildSpecification, ConstructionAct КС-2, ConstructionValueCertificate КС-3) + serviceaggregate_certificate(КС-3 ← Σ КС-2). Registered: plugins/urls/EntityRegistry + frontend config/sections/applications + seed_shop AppEntry + roleshipyard_engineer. RequiresappEssentialsonly. Інтеграція з ККУ — непряма (Shape B): лише черезshipcore.Vessel. migrate OK, tsc=0, check clean. Scope cut: GL posting, КС-2/КС-3 print templates, seed, reports — deferred.
2026-06-01 — [SHC] Grain Phase H-5 — Quality discount schedule + settlement engine¶
- ✅ виконано 2026-06-01:
QualityDiscountSchedule(MasterData)+QualityScheduleLine(banded grid, inline subtable) +GrainSettlement(TransactionModel)— перший grain-док що реально постить (PartyLedger sell→receivable/buy→payable + GL через accounting_service коли BO задано). Servicesettlement_service.py(compute/post/unpost/run_settlement, atomic, re-post safe, posting_lock_date). FKGrainContract.discount_schedule. API: 3 viewsets + nested lines route + actions post_document/unpost_document/preview/settlement-run + EntityRegistry (3 keys) + role_catalog. Migration 0018. Frontend: schedule MasterData (Quality) + grainSettlements (Documents→Trade), formType default. 8 нових тестів + 38 grain-suite зелені; tsc=0; migrate OK. Bundle grain_settlement.md. Scope cut: multi-tier→V3; signed-GL flip; settlement-run auto-draft; credit-note (A2-2). - Shape ратифіковано 2026-06-01: ADR adr-2026-06-01-grain-settlement-engine.md. 4 осі: A1-2 schedule+lines (grid
{param × range × ₴/т}); A2-1 GrainSettlement сам постить черезaccounting_service; A3-3 explicit settlement-run + per-cert manual; A4-1 single-tier MVP. Розрахунок: deviationcert.value − grade.base× schedule-рядок × tons, сума по q-params, знак за напрямом × side.
2026-06-01 — [ESS] vw_stock_balance BI view — variant-aware stock snapshot¶
- ✅ виконано 2026-06-01: raw-view
vw_stock_balance(migrationessentials/0077) — 3-й star-schema leg після vw_sales_fact/vw_purchase_fact. Row per active Batch (item, variant, warehouse, batch): on_hand (current_qty), reserved (Σ open BatchReservation), available, unit_cost, stock_value + variant_sku/variant_attributes (LEFT JOIN, NULL без variants plugin), filtercurrent_qty>0. Symmetric pattern (external BI, без Django-моделі). 4 тести + smoke-query dev DB OK (26 cols, 22 rows). Bundle bi-postgres-views.md § 3. Scope cut: as-of-date historical (StockTransaction replay) → V2.
2026-05¶
2026-05-31 — [SHC] EDO federated exchange — S4: live round-trip 2 інстанси + inbound-UI + demo seed¶
- ✅ Backend native inbound + demo-seed (2026-05-30):
materialize_inboundтепер після genericInboundDocumentкличеadapter.build_inbound(tenant, envelope, inbound)(native projection hook). eTTN (shipcore_auto/edo_inbound.pyproject_ettn_inbound) →TransportInvoice(direction='inbound')+ETTNDocumentз imported read-onlyETTNParty, idempotent заenvelope_id. Нові поляETTNParty.public_key_pem+ETTNDocument.source_envelope_id(migrationshipcore_auto/0007).verify_all_party_signaturesрефакторено на carried key (E5). Командаseed_edo_exchange [--tenant]. 33 тести зелені. - ✅ inbound-UI + multi-org (2026-05-30): REST API
edo/urls.py→/api/v1/edo/:InboundDocumentViewSet(read-only) + actionsign-my-line;EdoExchangeSettingsget/upsert;participantsendpoint. Frontend: PROCESS-сторінка у Essentials → Processes → «Обмін документами» (EdoExchangePage.tsx, Tabs: Вхідні + Налаштування). - ✅ Multi-org fix (5 юросіб у тенанті): «наші ЄДРПОУ» = множина
essentials.Organizationтенанта мінус opt-out.EdoOrgParticipant(tenant, edrpou, enabled)keyed by ЄДРПОУ-string (без FK на essentials → E9).counter_sign_inbound(tenant, inbound, *, our_edrpous)param-driven. Резолв «наших» — у glue-шаріedo.views._our_edrpous. 4 API-тести;tscчистий; 37 backend edo+ettn зелені. - ✅ ETTNTestKey→edo.SigningKey fold (partial, 2026-05-30): eTTN sign/verify тягнуть keypair з канонічного org-bound
edo.SigningKey. Аудит-знахідка:ETTNTestKeyтримає per-user→org authority binding, аcore.UserНЕ маєorganization— лишено як enrollment/authority record (повне видалення потребує User↔org membership, deferred). 38 тестів зелені. - ✅ round-trip return leg (2026-05-30):
edo.services.push_inbound(tenant, inbound, *, our_edrpous)— re-push підписаного конверта назад видавцю. Hub routing → per(envelope_id, target_edrpou). Повний A→B→A тест зелений (39 edo+ettn). - ✅ live-стенд верифіковано (2026-05-31): два-тенанти на 1 інстансі (браузерний тест юзером). Хаб
services/edo-hub/app.pyна :8200 +seed_edo_live --from agro_holding --to dry_port. Round-trip пройдено в браузері: agro (Zernoplyus 23456784) → видаткова → dry_port (Sukhyi Port Kyiv 45678901) «Перевірити хаб» → «Підписати мою лінію» → agro → обидва підписи. - Deferred (не блокери): (1) multi-org poll по всіх наших ЄДРПОУ; (2) fold-cleanup — повне видалення
ETTNTestKey+ User↔org membership; (3) повні 2 фізичні інстанси. EDO-трек S1→S4 закрито. ADR E6/E7.
2026-05-30 — [ESS] EDO federated exchange — S1: generic-ядро backend/edo/ + eTTN/2.0 adapter¶
- Виконано 2026-05-30 (мінімальний scope): новий горизонтальний app
backend/edo/(нікого не імпортує, залежить лише відcore):crypto.py(єдина Ed25519-реалізація —ettn_serviceтепер делегує сюди),registry.py(DocumentExchangeAdapterProtocol +PartySpec+ singletonregistry),envelope.py(Envelope/PartySignaturedataclasses з carried pubkey),transport.py(ExchangeTransportProtocol +FakeTransportз ЄДРПОУ-inbox + idempotency),models/SigningKey(ЄДРПОУ-bound,unique(tenant, edrpou)),services.py.eTTN/2.0adapter уshipcore_auto/edo_adapters.py(реєстрація зready()). 14 нових тестів зелені + 10 існуючих eTTN-тестів зелені після делегування крипти. Bundle: docs/ai/domains/edo/README.md. - ADR-light рішення (у commit): (1)
edo.SigningKeyприв'язано до ЄДРПОУ-рядка, не FK наessentials.Organization(інакше edo→essentials ламає E9). (2) Мінімальний scope — adapter поверх наявних eTTN-моделей; винесенняETTNDocument/ETTNParty/ETTNTestKey→edoвідкладено у S5. - ADR: adr-2026-05-29-edo-federated-exchange-hub.md (рішення E1–E9).
2026-05-30 — [ESS] EDO federated exchange — S2: GoodsShipment/1.0 adapter + inbound builder¶
- Виконано 2026-05-30:
essentials/edo_adapters.py—GoodsShipmentExchangeAdapter(GoodsShipment/1.0, 2 сторони supplier=organization.edrpou/buyer=client.edrpou, ЄДРПОУ-keyed canonical з frozen line items), реєстрація зEssentialsConfig.ready(). Inbound builder — genericedo.InboundDocument(нова модель,unique(tenant, envelope_id)) +edo.services.materialize_inbound(tenant, envelope): read-only, номер видавця verbatim, ідемпотентний заenvelope_id. 3 нові тести + 17 тестівedo/зелені сумарно. Bundle оновлено. - ADR-light рішення (у commit): inbound видаткової — НЕ відтворення
GoodsShipment, а genericedo.InboundDocument. Per-typeadapter.build_inboundзарезервовано для native-проекцій (eTTN→TransportInvoice, S5). ADR E3/E7.
2026-05-30 — [ESWF] EDO federated exchange — S3: мінісервіс services/edo-hub/ + TestHubTransport¶
- Виконано 2026-05-30:
services/edo-hub/— standalone мінісервіс (stdlibhttp.server+sqlite3+cryptography, нічого не ділить з DOP):store.py+dispatch.py(4 ендпоінти/enroll//push//inbox/{edrpou}//ack, origin-verify проти directory) +hubcrypto.py+app.py+config.py. DOP-сторона:edo/transports/test_hub.pyTestHubTransport(httpx),edo.EdoExchangeSettings(per-tenant singleton URL+токен+ЄДРПОУ), сервісиenroll_self/send_document/poll_inbound, командаedo_exchange_poll [--tenant]. 5 нових тестів + real-socket smoke OK. 22 тестиedo/зелені. Bundle оновлено. - ADR-light рішення (у commit): (1) auth = статичний shared bearer-токен на інстанс (
EDO_HUB_TOKEN); (2) poll-only; (3) хаб на stdlib http.server; (4)inbox_acksзамістьInboxCursor. ADR E1/E2/E6.
2026-05-29 — [SHC] Phase 5 Day-9+ — Frontend frontend/dop/sea/ (7/7)¶
- Status: shape ADR landed (adr-2026-05-26-shipcore-sea-frontend-shape.md). Усі 7 компонентів Implementation order реалізовані.
- ✅ Зроблено: OB Form+Kanban (
6b197ad1/7 +6986e3aOB-1) · VesselScheduleBoard (d185f613/7) · VGMEditor (c7335e14/7) · BLEditor (4af9bd75/7) · OceanTrackingMap (34205e87/7) · Day-10 status transitions wired (5de5e2c) · DemurrageDashboard (933a326) — real dashboard надGET /shipcore-sea/demurrage-timers/+ WS-нудж (SeaConsumer/useSeaSocket) · BLPrintPreview (6/7, 2026-05-29) — DCSA-shaped B/L PDF через Print Framework:BillOfLadingContract+ templatebills-of-lading/{base,ua}.html+ScanRedirectPageroute. - Scope cut DemurrageDashboard:
demurrage_changedWS-broadcast НЕ wired — джерелоDemurrageCalculationу containerhub plugin; свіжість тримає 60s polling. - Scope cut BLPrintPreview: PDF (Playwright) + клік у браузері — manual checkpoint; eBL-блок не друкується (Phase 8, paper-only);
bl_workflowactions лишаються form-side. - Vendor creds (deferred): Maersk Spot, Vizion, MarineTraffic.
2026-05-29 — [ESWF] Аудит shipcore handover-міграцій на missing reverse-deps (fresh-DB migrate)¶
- Виконано 2026-05-29: систематично пройдено ВСІ 6 cross-app state-DeleteModel handover'ів. Знайдено + виправлено 1 прогалину —
transport/0004_handover_to_shipcore_rail(state-видаляє transport.railwaystation/wagon) не залежав відcontainerhub/0006_relocate_rail_fks_to_shipcore_rail; проходив лише завдяки tie-break порядку. Решта safe by construction. Перевірка: freshmigrateна одноразовійeswf_migauditдо exit 0. Інтра-апні DeleteModel (crm/0004, essentials/0063, shop/0012) — поза scope. - Зроблено в сесії 2026-05-28 (commit
399245e): полагоджено 2 інстанси —shipcore_terminal/0005→shipcore_rail/0004(yardzone);shipcore_terminal/0006→shipcore_rail/0005+shipcore_sea/0004+shipcore_cargo_ops/0002(container). Fresh migrate тепер проходить 273 міграції чисто. Клас із memoryphase4_handover_migration_deps.
2026-05-29 — [SHC] BillOfLading — bulk shipment без обов'язкового OceanBooking¶
- ✅ виконано 2026-05-29 (Variant A — XOR двох nullable батьків, ADR adr-2026-05-29-bl-carriage-parent).
ocean_booking→ nullable; доданоcharter_partyFK; CheckConstraintbl_exactly_one_carriage_parent+clean()+ serializervalidate(). Print-contract резолвить voyage/ports/carrier з booking АБО charter_party.voyage (containers=[] для bulk). Frontend BLFields/BLForm — два взаємовиключні select'и.seed_sea_grain_exportпереписано (0 OceanBooking, B/L під charter_party); reseed tenant 64 OK. Migration 0006 (additive). pytest --create-db + tsc=0. - Reference: session 2026-05-29 — grain-export seed для tenant 64 (AhroMriya).
2026-05-28 — [SHC] Grain Phase H-2 — Production acts (cleaning / drying / fumigation)¶
- Shipped: 3
TransactionModel'и уbackend/shipcore_grain/models/—GrainCleaning(Акт підробки),GrainDrying(Акт сушіння),GrainFumigation(Акт фумігації) — з єдиним сервісомservices/production_act_service.py(post/unpost +active_fumigation_lock). Reuseshrinkage_service.post_shrinkage_event. Cleaning: уся знята маса → одинShrinkageJournal(cleaning_loss); recovered-відходи I/II/III → окремі operator-Batch'і. Drying: moisture write-downqty*(m_in-m_out)/(100-m_out)→ShrinkageJournal(moisture_adjustment). Fumigation: quarantine lock → guard уdispatch_service._validate_ship. ShrinkageJournal отримав FKcleaning+drying(міграція 0014). 3 serializers + 3 viewsets + DRF routes + EntityRegistry keys + role_catalog. Frontend: subgroupshipcoregraindocsproduction(«Виробництво») з 3 items. Tests:test_production_acts.py— 8 passing. - Scope cut: (a) shrinkage-waterfall «cleaning» bucket рахує gross removed → V2. (b) GL-проводки energy/waste → H-5. (c) PDF print templates → окремий sprint. (d) kernel default form достатньо. (e) одна атомарна міграція + спільний сервіс → H-2 = один коміт.
- AI bundle: docs/ai/domains/shipcore_grain/production_acts.md. Western ERP ref: AGRIS «Drying Ticket», AgPro21 «Conditioning Op», Cultura «Treatment Log».
2026-05-27 — [SHC] Grain Phase H-1 — Swap & Cession (paper-only ownership transfer)¶
- Shipped:
GrainOwnershipTransfer(TransactionModel)(backend/shipcore_grain/models/grain_ownership_transfer.py) + service (services/ownership_transfer_service.py) + serializer/viewset + DRF route/grain-ownership-transfers/+ EntityRegistry keygrainOwnershipTransfers+ migration 0011 + frontend item у new «Custody» subgroup → Documents. Full transfer = reassignOwnershipLot.depositor; partial = split lot via newcreated_lot. Aggregates DepositorAccount оновлюються симетрично. Reconciliation-only — НЕ пише PostingGroup (settlement engine — у H-5). - Scope cut: (a) bilateral swap (
swap_lot) → H-1.1. (b)bank_release_ref— text-only; formal FK наWarehouseReceipt.bank_pledgeприходить у H-3. (c) auto-IncomingPayment proposal — defer to pilot signal. (d) custom UI — kernel form достатньо. - AI bundle: docs/ai/domains/shipcore_grain/ownership_transfer.md.
2026-05-27 — [SHC] Export certificates — Phytosanitary / Radiological / Certificate of Origin (H-2.1)¶
- Shipped: 3 нові
TransactionModel'и (PhytosanitaryCertificate/RadiologicalCertificate/CertificateOfOrigin) уbackend/shipcore_grain/models/export_certificates.pyз shared abstract base_BaseExportCertificate(FK→GrainDispatch, issuing_agency, valid_from/until, cert_file, status draft→issued→final/voided). Per-cert поля: treatment_method/chemical/destination для phyto; cesium_137_bq_per_kg/strontium_90/accredited_lab/method для radio; origin_type (EUR.1 / Form A / Form B / general / CIS FTA) + chamber_of_commerce + customs_declaration_ref для origin. 3 serializers + 3 viewsets (shared mixins) + DRF routes + EntityRegistry keys + migration 0012. Bundle endpointGET /grain-dispatches/{id}/export-pack/повертає JSON listing з readiness status (is_ready+missing[]per transport type). Frontend: subgroupshipcoregraindocsexport(«Експортні сертифікати») у Documents з 3 items. - Scope cut H-2.2: PDF print templates для кожного сертифіката + combined export-pack PDF — окремий sprint (потребує юридично-точних форм).
- AI bundle: docs/ai/domains/shipcore_grain/README.md sprint table H-2.1/H-2.2.
2026-05-27 — [SHC] Charter Party + Statement of Facts — TransactionModel у shipcore_sea¶
- Shipped:
CharterParty(TransactionModel)(backend/shipcore_sea/models/charter_party.py) з charter_type enum (voyage/time/bareboat/COA), freight_rate_per_mt + daily_hire_rate, laytime_allowed_hours + laytime_clauses JSON, demurrage_rate_per_day + dispatch_rate_per_day, broker + commission, cp_form (GENCON/NYPE/NORGRAIN/etc), currency.StatementOfFacts(TransactionModel)(models/statement_of_facts.py) з port_role enum + cached computed fields.SOFEvent(TenantAwareModel)— 23 event_type choices (BIMCO 2013) + laytime_rule (counts / half_count / excluded). Serviceservices/demurrage_service.py—compute_demurrage(sof)+post_sof/unpost_sof. 2 serializers + 2 viewsets + EntityRegistry keys + DRF routes + actions. Migration 0005. Frontend: subgroupseacharteringу Documents з 2 items. - Scope cut H-2.2: PDF print templates (GAFTA #87 / GENCON / SOF) — окремий sprint. Scope cut H-5: Settlement integration (auto-Invoice / IncomingPayment from SOF.demurrage_amount).
- AI bundle: docs/ai/domains/shipcore_sea/README.md.
2026-05-27 — [SHC] WarehouseReceipt (складська квитанція на зерно) як TransactionModel¶
- Shipped:
WarehouseReceipt(TransactionModel)(backend/shipcore_grain/models/warehouse_receipt.py) з receipt_type enum (simple_warrant/double_warrant/grain_receipt_kmu_form), serial_number (черезcore.services.numbering), face_value (computed на post), pledge_status (free/pledged/endorsed/redeemed), pledge_holder FK→Client, endorsed_to + endorsement_date/ref, redeemed_by_dispatch FK. Serviceservices/warehouse_receipt_service.py— issue / unpost / pledge / release_pledge / redeem (atomic). ViewSet actions: post_document / unpost_document / pledge / release-pledge / redeem. EntityRegistry keywarehouseReceipts, DRF route, migration 0013. Frontend: subgroupshipcoregraindocswarrants(«Складські квитанції») з 1 item. - Scope cut H-3 full: (a) KMU-form PDF → H-3.1. (b) Е-зерно реєстр Мінагро. (c) Bank cabinet. (d) Auto-coupling з GrainDispatch. (Решта H-3 осей закрита 2026-06-04 — див. активний backlog reframe.)
- AI bundle: docs/ai/domains/shipcore_grain/README.md.
2026-05-26 — [SHC] Grain Q/Q vertical — S1 skeleton (shipcore_grain + Silo + GrainGrade)¶
- ADR locked 2026-05-26 (adr-2026-05-26-shipcore-grain-shape.md) — 9 рішень, 3 open questions. S1 — skeleton-only.
backend/shipcore_grain/Django app skeleton. Дві master моделі:Silo(FK→gatehouse.Site + FK→essentials.Warehouse + capacity/current_fill/cleaning_date/silo_number +commingled=Falselocked per D-2),GrainGrade(parameter_schema JSONField + commodity_class enum). EntityRegistry keyssilos+grainGrades. Plugin-gated черезESWF_PLUGINS=shipcore_grain. Frontendfrontend/dop/shipcore-grain/config.ts+ applications.ts «Grain Q/Q» peer vertical. Migration 0001. NO business logic у S1.
2026-05-26 — [SHC] Grain Q/Q vertical — S2 GrainReception + scale-ticket adapter¶
- Shipped:
GrainReception(TransactionModel)з FK на supplier (Client), Silo (target), GoodsReceipt (опц.), tentative_grade. Adapter serviceshipcore_grain.services.scale_ticket_to_receptionщо hook'ається наWeighingTicket.post_save(state=closed) і створює draft GrainReception. Reception state machine: draft → weighed → quality_pending → posted. NO automatic shrinkage у S2 (S4 deliverable). Form у frontend: pick Silo + Supplier + tentative GrainGrade. Tests: scale-to-reception adapter + state transitions.
2026-05-26 — [SHC] Grain Q/Q vertical — S3 Lab MVP + GrainQualityCertificate¶
- Shipped:
GrainQualityCertificate(TransactionModel)з FK→GrainReception (1:1 у MVP), inspector (Person), issuer enum (internal/sgs/bureau_veritas/cotecna/intertek/other), top-8 columns (moisture/protein/gluten/falling_number/test_weight/dockage/foreign_matter/broken_kernels),extended_resultsJSONField. Validation черезGrainGrade.validate_quality_payload(). Cert state: draft → issued → final. Seed ДСТУ-1/2/3 wheat + GAFTA wheat #1 grades. Reception advancing: quality_pending → posted на cert.issued. Tests: schema validation + grade tolerance + state transitions.
2026-05-26 — [SHC] Grain Q/Q vertical — S4 ShrinkageJournal register + 4-event схема¶
- Shipped:
ShrinkageJournal(register)— immutable event log per Batch. Поля: batch FK, event_type enum (moisture_adjustment / cleaning_loss / transfer_loss / natural_loss), reason_text, amount_tons, attribution enum (supplier / operator), source_doc polymorphic ref, posting_group FK. Serviceshipcore_grain.services.shrinkageз 4 calculators:calc_moisture_adjustment(на base 14%),calc_cleaning_loss(на dockage>tolerance),calc_transfer_loss,calc_natural_loss(norm-based per month). 4-event схема per D-4 ADR — generic стартова, калібрується на pilot (O-1). Tests: 4 event calculators + immutability + posting integration.
2026-05-26 — [SHC] Grain Q/Q vertical — S5 GrainTransfer (silo-to-silo, без blending)¶
- Shipped:
GrainTransfer(TransactionModel)— source_silo FK, target_silo FK, batch FK, amount_tons, transfer_date, reason enum (cleaning / drier / consolidation / preparation_for_dispatch), expected_loss_tons, actual_loss_tons. Validation: source.grain_grade == target.grain_grade (no blending у MVP, commingled locked False у D-2). State: draft → in_progress → completed. On completion → ShrinkageJournal entry transfer_loss + update Silo.current_fill_tons обох. Tests: validation rejecting cross-grade transfer + shrinkage event creation.
2026-05-26 — [SHC] Grain Q/Q vertical — S6 GrainDispatch + polymorphic transport FK (PDF S6.1 deferred)¶
- Shipped:
GrainDispatch(TransactionModel)— batch FK, source_silo FK, cert FK, buyer FK (Client), transport_doc_type enum (auto/rail/sea), transport_doc_id PositiveBigIntegerField, helper propertytransport_docчерез ContentType lookup. State: draft → loaded → shipped. On loaded → reduce Batch.current_qty + Silo.current_fill_tons. Polymorphic FK на shipcore_auto.Waybill / shipcore_rail.RailwayWaybill / shipcore_sea.OceanBooking — без JOIN explosion. Deferred S6.1: PDF print templategrain_cert.html+ PrintContractGrainCertContract. Tests: polymorphic FK helper + state machine + stock reduction.
2026-05-26 — [SHC] Grain Q/Q vertical — S7 Reports — bin/silo position + Q/Q summary + shrinkage waterfall¶
- Shipped: 3 звіти per
new-reportskill: (1) Bin/Silo Position — Silo × current_fill_tons × grade × last_cleaning з фільтром по Site. (2) Daily Q/Q Summary — щодня per Silo: брутто/нетто accepted, average moisture/protein/gluten, total shrinkage. (3) Shrinkage Waterfall — incoming tons → moisture losses → cleaning losses → transfer losses → natural losses → dispatched tons. ReportDefinition вshipcore_grain/reports/. Frontend: 3 entries у grain section Reports group. Tests: aggregation correctness + period filtering.
2026-05-26 — [ESWF] R-4.1 — containerhubSection preset-shell cleanup¶
- ✅ виконано 2026-05-26: переписано
frontend/dop/containerhub/config.tsз 1005 LoC у 366 LoC. Видалено всі дубльовані items (containers/types/shippingLines/tariffs/bookings/movements/demurrage/containerLedger → shipcoreContainers; gateTransactions/yardInventories/storageChargeLedger → shipcoreCargoOps; railwayRoads/Stations/wagons/Waybills/trainArrivals → rail). Лишено унікальне: terminals+zones, yard processes (5), integration hub, analytics (5). Тип-чек чистий, orphan refs нема.
2026-05-26 — ShipCore Sea Phase 5 Day-9+ frontend shape ADR (8 components)¶
Закрито SHC sea frontend shape session (adr-2026-05-26-shipcore-sea-frontend-shape.md, ~430 LoC). Жодного коду — 👤 decision task, output — ADR з UX рішеннями для всіх 8 ocean components з sea.md § 3.4.
Three UX families ідентифіковані:
| Family | Components | UX pattern |
|---|---|---|
| Schedule / planning | VesselScheduleBoard | Gantt-style timeline |
| Booking lifecycle | OceanBookingForm, OceanBookingDashboard, VGMEditor | Form + Kanban + matrix |
| BL lifecycle + print | BLEditor, BLPrintPreview | Stepper form + PDF preview |
| Live ops | DemurrageDashboard, OceanTrackingMap | Real-time list + map |
8 per-component decisions:
- VS-1 VesselScheduleBoard — Gantt hour-resolution, read-only (no drag-to-reschedule; data is carrier-API-sourced). Reuse VehicleGantt timeline primitive.
- OB-1 OceanBookingDashboard — 5 columns (Draft / Pending / Confirmed / Loaded / Discharged; Cancelled = filter toggle). No DnD — status transitions are business-rule guarded (matches Group MDM Kanban precedent).
- OB-2 OceanBookingForm — Tabbed (Header / Containers / Cutoffs / Parties+Cargo). Status transitions у header toolbar поза tabs.
- VG-1 VGMEditor — Matrix-default з expand-row drill-down (one row per container, bulk-edit copy-down).
- BL-1 BLEditor — Adaptive stepper by document_subtype (Original: 4 stops; ExpressRelease: 3; SeaWaybill: 2).
- BL-2 BLPrintPreview — DCSA layout via Fortune Sheet (hybrid — industry-standard layout, our print engine).
- DD-1 DemurrageDashboard — client-side timer + WS state-change push (не tick push). Reuse
useWebSocketWidgetз Dashboard S5-mini. - OTM-1/2/3 OceanTrackingMap — Vizion + MarineTraffic dual-vendor (container events vs vessel AIS); 15-min polling refresh (no WS — vendors don't push); last-known-position fallback for no-AIS vessels.
3 cross-cutting decisions:
- CC-1 Mount path: all 8 у
frontend/dop/sea/components/; PROCESS items у sea/config.ts;REQUIRED_PLUGIN['<componentCode>'] = 'shipcore_sea'(8 entries) у ProcessPanel. - CC-2 WebSocket strategy: single Channels group
shipcore_sea_t_<tenant_id>для OceanBooking + BL + Demurrage events. AIS — HTTP polling, не Channels. - CC-3 Plugin gating: auto через REQUIRED_PLUGIN map (shipped today commit
eaebb87).
Pattern reuse table (значне reduction implementation work):
| Pattern | Existing | Reuse for |
|---|---|---|
| Gantt timeline | VehicleGantt |
VesselScheduleBoard |
| Kanban | LotBoard / InvoiceKanbanBoard |
OceanBookingDashboard |
| Map | GpsFleetMap (Leaflet + WS) |
OceanTrackingMap |
| Document form | TransactionForm pattern | OceanBookingForm, BLEditor |
| Print preview | printPdf.ts Fortune Sheet |
BLPrintPreview |
| Live countdown | (none — new pattern) | DemurrageDashboard chips |
Implementation order — 7 окремих 🧭 commits + 1 backend Day-10 (WS broadcast hooks): 1. OB Form + Dashboard (single commit) 2. VesselScheduleBoard 3. VGMEditor 4. BLEditor 5. BLPrintPreview 6. DemurrageDashboard (depends on backend WS hooks) 7. OceanTrackingMap
Estimated effort: ~3 тижні focused frontend + 2 дні backend WS hooks. No backend model changes — Phase 5 Days 1-8 already exposed all data surfaces.
No DnD library introduced — всі 8 components обходяться click-actions (consistent з Group MDM Kanban precedent).
Reactivation criteria для deferred (DnD, real-time AIS, custom MapVisor, Phase 8 eBL components) — документовані у ADR; architectural triggers, не client-driven.
Commit: ...
2026-05-26 — OpeningBalance Phase 6b CLOSED — Party shape ADR is no-op¶
Phase 6b завершено. Party — фінальний handler — отримав no-op decision ADR (adr-2026-05-26-opening-balance-period-party.md). Жодного коду; рішення повністю архітектурне.
Обґрунтування no-op:
Аудит коду підтвердив припущення з Phase 6a ADR:
- Phase 6a OpeningBalancePeriodAccountLine carries partner FK; Account handler post_period пише PostingEntry(partner=line.partner, ...).
- PartnerTurnoverReport reads PostingEntry.partner (filtered by account class prefix + date range).
- Отже per-partner aging-by-month уже працює через Account handler period mode.
Залишковий gap (документований, deferred):
- client statement view (reports.py:484) читає PartyLedger напряму. Phase 6a Account period mode писав PostingEntry, але НЕ PartyLedger. Тому historical client statement для періодів до go-live показує тільки state-at-date snapshot + live activity.
- Не виправляємо у Phase 6b — small gap, deferred to either (а) report-side refactor щоб читати PostingEntry where partner is set, OR (б) future Party period mode ADR коли з'явиться real consumer.
Чому НЕ Option L (write PartyLedger only):
- Технічно clean — PartyLedger.running_balance recompute'ить from scratch via _recalc_party_running_balance, не має FuelLedger's incremental-balance trap.
- Але: дані які користувач вводив вже є у PostingEntry через Account handler. Option L duplicate'ить storage без нового query surface.
- YAGNI + memory feedback_no_client_triggers — не активуємо "коли клієнт запитає".
Reactivation criteria (для future ADR): specific report demand що PostingEntry derivation не може serve cleanly; or migration scenario що не enterable through Account period mode at partner granularity. Активація — архітектурна, не "коли треба".
Phase 6b — close. Handler registry final state:
| Handler | Phase | Period mode | Writes |
|---|---|---|---|
| Account | 6a | ✅ | PostingGroup + PostingEntry (з partner FK) |
| Inventory | 6b | ✅ | InventoryJournal + PostingGroup |
| Party | — | ❌ no-op | (none — Account covers) |
| FixedAsset | 6b | ✅ | DepreciationEntry only |
| Fuel | 6b | ✅ | FuelJournal only |
Phase 6 set of capabilities (mid-year migration з historical turnover) — feature-complete. 45/45 tests passed через Inventory + FixedAsset + Fuel combined suite.
Commit: ...
2026-05-26 — OpeningBalance Phase 6b Fuel — period handler shipped¶
Закрито Fuel handler у Phase 6b — третій з чотирьох non-Account handlers. Спирається на shape ADR (commit 9662cda).
Реалізація per ADR — Option H (FuelJournal register only, quantity-only):
- Backend:
- Model:
OpeningBalancePeriodFuelLine— vehicle × fuel_type (nullable) × period_year × period_month × quantity_in × quantity_out × description. Один рядок несе обидва напрямки (як Inventoryqty_in/qty_out). - Serializer + nested:
OpeningBalancePeriodFuelLineSerializer+fuel_linesnested уOpeningBalancePeriodSerializer. - Handler
FuelHandler.post_period: для кожного non-zero side створюєFuelJournalrow (operation_type='issue' для in, 'consumption' для out) dated end-of-month, зdocument_type='OpeningBalancePeriod'. NO FuelLedger, NO PostingGroup, NO OdometerReading. unpost_periodoverride (третій handler у Phase 6b — після Inventory + FixedAsset): видаляє FuelJournal rows by(document_type, document_id)._validate_period_line: non-negative quantities, at least one > 0, month ∈ 1..12, month_end inside document period bounds.- ViewSet + URL:
OpeningBalancePeriodFuelLineViewSetat/api/v1/essentials/opening-balance-period-fuel-lines/. -
Registration: реєстрація в
backend/shipcore_auto/apps.py::ready()(Phase 4 Day-7 — FuelHandler живе у shipcore_auto, не fleet ghost) + EntityRegistry entry уessentials/apps.py. -
Frontend (OpeningBalancePeriodPage.tsx):
PeriodFuelLinescomponent (~280 LoC) — vehicle Select + fuel_type Select (clearable, nullable) + period_year NumberInput + period_month Select (reuses MONTH_OPTIONS from FixedAsset) + quantity_in/out NumberInput (decimal 3) + description.- Footer показує totals qty_in / qty_out / net flow.
-
Зареєстрований у
SECTION_COMPONENTS['PeriodFuelLines']— tab з'являється авто через handlerperiod_frontend_component. -
Migration:
essentials/0074_openingbalanceperiodfuelline.py(auto-generated, applied).
Tests (test_opening_balance_period_fuel.py): 15/15 passed у 9 класах:
- TestPostPeriodReceiptOnly (1) — refuel only → 1 FuelJournal('issue')
- TestPostPeriodConsumptionOnly (1) — consumption only → 1 FuelJournal('consumption')
- TestPostPeriodBothSides (1) — обидві сторони → 2 FuelJournal
- TestNullableFuelType (1) — fuel_type=None дозволено
- TestNoPostingGroupCreated (1) — ADR cornerstone guard 1
- TestNoFuelLedgerUpdated (1) — ADR cornerstone guard 2 (FuelLedger.quantity_balance не reconcile з aggregate періодами)
- TestIdempotency (1) — re-post не дублює (2 rows стало 2)
- TestUnpostPeriod (1) — FuelJournal cleanup
- TestValidation (4) — all-zero / negative / invalid month / outside period
- TestReportsPickUpEntries (1) — per-vehicle Q1 aggregate знаходить Phase 6b rows
- TestRegistryMetadata (2) — supports_period=True
Combined Phase 6b suite: Inventory (15) + FixedAsset (15) + Fuel (15) = 45/45 passed — нічого не зламалось.
Effort vs estimate: ADR estimated ~2 години код + 1 година тестів. Реально ~1.5 години код + 1 година тестів — швидше за FixedAsset (немає collision pre-check, немає denorm protection).
Phase 6b status: - ✅ Inventory (aggregate ledger-only) - ✅ FixedAsset (DepreciationEntry register only) - ✅ Fuel (FuelJournal register only) - ⏳ Party — shape pending (last; may end up no-op)
Gotcha записано: Phase 4 Day-7 (2026-05-21) перейменував fleet → shipcore_auto, FuelHandler перенісся у backend/shipcore_auto/opening_balance_handlers/fuel.py. Старий backend/fleet/opening_balance_handlers/fuel.py — dead code (FleetConfig.ready() = no-op). Це stale duplicate; кодити треба тільки shipcore_auto/.
Commit: ...
2026-05-26 — OpeningBalance Phase 6b FixedAsset — period handler shipped¶
Закрито FixedAsset handler у Phase 6b — другий з чотирьох non-Account handlers. Спирається на shape ADR (commit 1971a8d).
Реалізація per ADR — Option F (DepreciationEntry register only):
- Backend:
- Model:
OpeningBalancePeriodFixedAssetLine— asset × period_year × period_month × amount × description. Schema дзеркалить existingDepreciationEntry.UniqueConstraint(asset, period_year, period_month). - Serializer + nested:
OpeningBalancePeriodFixedAssetLineSerializer+ додавfixed_asset_linesуOpeningBalancePeriodSerializer. - Handler
FixedAssetHandler.post_period: для кожного рядка створюєDepreciationEntry(document_type='OpeningBalancePeriod', document_id=doc.pk, posting_group=None, ...). NO PostingGroup, NO mutationFixedAsset.accumulated_depreciation. unpost_periodoverride (другий handler що override'ить — після Inventory): видаляє DepreciationEntry rows by (document_type, document_id)._validate_period_line: amount > 0, month ∈ 1..12, asset.status='active' (disposed rejected), month_end falls inside document period bounds, collision pre-check проти existing DepreciationEntry (clean OpeningBalanceError замість raw IntegrityError).- ViewSet + URL:
OpeningBalancePeriodFixedAssetLineViewSetat/api/v1/essentials/opening-balance-period-fixed-asset-lines/. -
Registration: apps.ready() реєструє handler
period_line_serializer_class+ EntityRegistryopeningBalancePeriodFixedAssetLines. -
Frontend (OpeningBalancePeriodPage.tsx):
PeriodFixedAssetLinescomponent (~260 LoC) — asset Select (filters active only) + period_year (NumberInput) + period_month (Select з 12 локалізованих опцій) + amount (NumberInput).MONTH_OPTIONSconstant — 12 локалізованих labels.- Зареєстрований у
SECTION_COMPONENTS['PeriodFixedAssetLines']— tab з'являється авто через handlerperiod_frontend_component. -
Default year/month — derived from
document.period_startабоdocument.date. -
Migration:
essentials/0073_openingbalanceperiodfixedassetline.py(auto-generated, applied).
Tests (test_opening_balance_period_fixed_asset.py): 15/15 passed у 8 класах:
- TestPostPeriod (3) — DepreciationEntry per line + end-of-month date + amount preservation
- TestNoPostingGroupCreated (1) — ADR cornerstone regression guard: post_period НЕ створює PostingGroup; posting_group=None
- TestNoAccumulatedDepreciationMutation (1) — денорм недоторканий після post_period
- TestIdempotency (1) — re-post не дублює
- TestUnpostPeriod (1) — DepreciationEntry rows видаляються
- TestValidation (5) — zero amount / invalid month / disposed asset / outside doc period / collision з existing DepreciationEntry (clean error замість IntegrityError)
- TestReportsPickUpEntries (1) — per-asset history report queries find Phase 6b rows
- TestRegistryMetadata (2) — handler.to_meta() reports supports_period=True
Combined Phase 6b suite: Inventory (15) + FixedAsset (15) = 30/30 passed — нічого не зламалось у Inventory після додавання FixedAsset.
Effort vs estimate: ADR estimated ~3-4 години код + 1 година тестів. Реально ~2 години код + ~1 година тестів — line model простіша за Inventory.
Phase 6b status: - ✅ Inventory — done - ✅ FixedAsset — done - ⏳ Fuel — shape pending - ⏳ Party — shape pending
Commit: ...
2026-05-26 — OpeningBalance Phase 6b Inventory — period handler shipped¶
Закрито Inventory handler у Phase 6b — перший з чотирьох non-Account handlers що отримав period mode. Спирається на ADR shape session, commit 3e5225f (того ж дня раніше).
Реалізація per ADR — aggregate ledger-only:
- Backend (backend/essentials/):
- Model:
OpeningBalancePeriodInventoryLine(TenantAwareModel) — item × warehouse × inventory_account × [start_date, end_date] × (qty_in, qty_out, value_in, value_out). Один рядок несе обидва напрямки. - Serializer:
OpeningBalancePeriodInventoryLineSerializer—__all__+ read-only resolved names (item_code/item_name/warehouse_name/inventory_account_code/inventory_account_name). - Handler:
InventoryHandler.post_period— для кожного non-zero side створюєPostingGroup+ 2PostingEntry(Dr inventory / Cr 00 на receipt, Cr inventory / Dr 00 на issue) +InventoryJournalrow (operation_type='receipt'|'issue'). Всі дати =line.end_date. unpost_periodoverride (перший handler що override'ить — Account залишається default no-op): видаляєInventoryJournalrows by(document_type='OpeningBalancePeriod', document_id). PostingGroup cleanup залишається з parent service.- Validation: date range checks, non-negative qty/value, all-zero rejected, qty-without-value rejected, account standard match, leaf account.
- ViewSet + URL:
OpeningBalancePeriodInventoryLineViewSet+ route/api/v1/essentials/opening-balance-period-inventory-lines/. -
Registration: apps.ready() реєструє
InventoryHandler.period_line_serializer_class; EntityRegistry виставляє universal CRUD endpoint. -
Frontend (frontend/dop/essentials/components/OpeningBalance/OpeningBalancePeriodPage.tsx):
PeriodInventoryLinesкомпонент (~280 LoC) — дзеркальний доPeriodAccountLines, але з полями item/warehouse/inventory_account (Select) + qty_in/value_in/qty_out/value_out (NumberInput) + period dates. Inline-edit + per-row Save/Delete + table footer з totals.- Зареєстрований у
SECTION_COMPONENTS['PeriodInventoryLines']— handler-registry-driven tab з'являється автоматично черезperiod_frontend_component = 'PeriodInventoryLines'. -
Validation на frontend — дзеркало backend (consistency checks qty↔value, всі-нулі reject, start ≤ end).
-
Migration:
essentials/0072_openingbalanceperiodinventoryline.py(auto-generated, applied).
Tests (test_opening_balance_period_inventory.py): 15/15 passed у 7 класах:
- TestPostPeriodReceiptOnly — receipt-only створює 1 PostingGroup + 1 InventoryJournal
- TestPostPeriodBothSides — один row з обома сторонами → 2 PostingGroups
- TestNoBatchOrStockTransaction — ADR cornerstone: post_period НЕ створює Batch / StockTransaction (захист від regression)
- TestIdempotency — re-post не дублює
- TestUnpostPeriod — InventoryJournal + PostingGroup видаляються, state → draft
- TestValidation (6 cases) — start>end / all-zero / qty-without-value / outside doc period / group account / standard mismatch
- TestReportsPickUpPeriodRows — verifies ADR claim: PostingEntry date filter автоматично включає period rows
- TestRegistryMetadata — handler.to_meta() reports supports_period=True
Effort vs estimate: ADR estimated ~1 day code + ½ day tests. Фактично ~2 години код + ~1.5 години тестів — швидше bo Phase 6a Account handler залишив чіткий pattern для копіювання.
Phase 6b Inventory complete. Залишилось: FixedAsset (next, double-count guard ADR pending) → Fuel → Party.
Commit: ...
2026-05-26 — AFP Phase 3 M3 — Group Sales Summary report (dual-axis + Unclassified)¶
Закрито останній DoD bullet Phase 3 — dual-axis report. Phase 3 complete (M1 backend + M2 frontend + M3 report).
Архітектура: новий ReportDefinition group-sales-summary у backend/group_mdm/reports.py (~230 LoC). Зареєстрований у apps.py::ready() через ReportRegistry.register() — використовує стандартний core/reports framework, авто-доступний на endpoint /api/v1/reports/group-sales-summary/ через існуючий ReportViewer.
Два axes (planning § 4.9):
- axis='group_product' (default) — GROUP BY Item.group_product_uuid, JOIN GroupProduct по UUID. Items з NULL FK → bucket __unclassified__ (planning § 4.7 — "Unclassified" як first-class status). Bucket рендериться завжди останнім, після названих, перед Total.
- axis='organization' — GROUP BY Invoice.organization. Stand-in для true cross-tenant "source" axis. Phase 5 (DOP↔DOP journal snapshots) додасть справжній source dimension через MirroredJournalEntry — той же report swap'не FROM clause і отримає матричний бонус.
Filters:
- date_from / date_to — required (per required_filters)
- axis — group_product (default) або organization; невалідне значення → fallback до default
- organization — optional FK filter
- State filter — hard-coded invoice__state='posted' (draft/marked не входять у group reports)
Columns:
- line_count (count of InvoiceLine rows)
- quantity (Sum InvoiceLine.quantity)
- amount (Sum InvoiceLine.amount — net, без VAT)
Output:
- Per-bucket row з 3 cell values
- Total row (is_total=True) з aggregates=[bucket_keys] для XLSX live formula
- Meta: axis (echoed back) + has_unclassified flag (driver для UI «classify N items» button у майбутньому)
Tests (test_report.py, 8 кейсів — всі passed):
- TestAxisGroupProduct (3) — буckets by GroupProduct + ordering invariant (Unclassified last) + no-unclassified case
- TestAxisOrganization (1) — buckets by Organization name
- TestFilters (3) — date range, draft exclusion, invalid axis fallback
- TestEmpty (1) — empty period returns only Total row with zeros
Implementation detail — name resolution batching: Aggregate query returns (group_product_uuid, count, sum, sum). Замість per-row lookup GroupProduct.objects.get(uuid=...) — батч один query GroupProduct.objects.filter(uuid__in=...) + in-memory dict для O(1) label resolve. Стандартний паттерн для core/reports — мінімізує N+1.
Не ввійшло у M3 scope (deferred): - True cross-tenant matrix (Company × Group Product cross-tab) — Phase 5, потребує MirroredJournalEntry data. - Drill-down з Unclassified bucket → InvoiceLine list з «classify» action — потребує DrillDownTarget integration + окрема UX session. - Frontend ReportViewer integration test — register reports auto-doступні через існуючий ReportViewer без додаткового frontend коду.
Phase 3 status: COMPLETE (M1 backend + M2 Kanban + M3 report). Phase 4 (Shop↔T1) deferred per planning — реактивація при появі pilot shop. Phase 5 (UA/MD→DE cross-server) deferred per external DE server availability.
Commit: ...
2026-05-26 — AFP Phase 3 M2 — Group MDM Hub Kanban frontend¶
Закрито frontend-частину [ESS] AFP Phase 3 — Group MDM Hub з todo.md. Спирається на M1 backend (commit 5657041) — Kanban consumes готові endpoints.
Архітектура UI — generic-Kanban pattern per planning § 4.5: один screen з трьома entity tabs (items / partners / accounts), однаковий column layout, однакові per-card actions. Поведінка ідентична для всіх 3 entity types, бо API discriminator передається у URL.
Реалізація (frontend/dop/essentials/components/GroupMdmReconciliationPage.tsx, ~390 LoC):
- Top-level
GroupMdmReconciliationPage—EntityWorkspace+ MantineTabsна 3 entity types. Tab badges показують count'и unmapped черезsummaryquery. - Per-entity
ReconciliationKanban— фільтр strip (source dropdown derived from rows + Refresh button + bulk selection counter) +SimpleGridз 3 columns (Unmapped/Confirmed/Deferred). Кожна column = 1useQuery({entity_type, status, source})→ автоматичніdashboard-render-style refetch при invalidate. - Per-card
MirroredCardView— source server badge + code badge + name lineClamp + group link indicator (коли вже linked) + Approve/Defer buttons. Selectable checkbox у Unmapped column для bulk. - Approve Drawer — single-row або bulk mode (один компонент): search input з debounce через query key + Select по канонічним GroupX entities (через
groupMdmApi.listCanonicalщо дзвонить universal CRUD/group-products/,/group-partners/,/group-accounts/) + optional notes (single-row). Bulk drawer не пропонує notes (один canonical для N). - No drag-drop — обрав button-based actions замість
@dnd-kit/core. Аргумент: planning § 4.5 описує bulk-approve через checkbox-multi-select, не DnD. Click-actions простіші, кращі для масового потоку (1 click → drawer → Select → confirm vs drag-target-drop ambiguity). - No "Suggested" column — Phase 3 MVP per planning § 4.6 manual-only. Phase 6 reactivate колонку коли AI-matcher landи.
API client (frontend/dop/essentials/api/groupMdm.ts, ~110 LoC):
- Typed MirroredRow / ListResponse / SummaryResponse / GroupEntity
- list({entity_type, status?, source?}) / summary() / approve(type, id, group_uuid, notes?) / defer(type, id, notes?) / bulkApprove(type, ids, group_uuid) / listCanonical(type, search?)
Реєстрація:
- frontend/core/src/components/Process/ProcessPanel.tsx — lazy import + case 'groupMdmReconciliation' (не plugin-gated — group_mdm app завжди ships)
- frontend/dop/essentials/config.ts — process item у essentialsadminonly subgroup поруч з currencyRevaluation + monthClosing (admin-tier process у Processes group)
Перевірка: tsc --noEmit clean (після cleanup unused imports).
DoD coverage (planning § 7-Phase-3):
- ✅ Reconciliation Workspace Kanban (Unmapped/Confirmed/Deferred columns) — done
- ✅ Generic — один UI на 3 entity types — done
- ✅ Filter strip (entity_type → tabs, source dropdown) — done
- ✅ Per-card: source badge, name, code, link indicator, approve/defer buttons — done
- ✅ Bulk approve — done через checkbox selection + Bulk Approve drawer
- ⏸️ Dual-axis report (sales by source / by group product) — окремий ReportDefinition iteration; перенесено у P1
- ⏸️ "Unclassified" bucket у group reports — частина dual-axis report task
- ⏸️ Locked-mode UI control для requires_explicit_mapping — данa shipped на M1 (field persisted), Phase 5 enforcement; UI toggle опціонально
Commit: ...
2026-05-26 — AFP Phase 3 M1 — Group MDM Hub backend foundation¶
Закрито backend-частину [ESS] AFP Phase 3 — Group Master Data Hub MVP з todo.md. Frontend Kanban workspace + dual-axis report — reframed як окрема follow-up задача (scope-cut: 2-3 тижні full DoD → M1 backend + M2 frontend Kanban).
Архітектура: новий Django app group_mdm (backend/group_mdm/), живе на consolidator-tenant (пілот UA-T4). Передплачує AFP-substrate з Phase 1; baf_sync (Phase 2) — основне джерело mirrored data.
Моделі (6):
- Canonical (на consolidator):
- GroupProduct(HierarchicalMasterDataModel) — group-level товар з tree (parent/children), canonical_uom, requires_explicit_mapping flag (Phase 5 enforcement, Phase 3 persists for reporting).
- GroupPartner(MasterDataModel) — group-level контрагент.
- GroupAccount(HierarchicalMasterDataModel) — group CoA з ієрархією.
- Mirrored (observation з subsidiaries):
- MirroredItem, MirroredPartner, MirroredAccount — TenantAwareModel з status (unmapped/suggested/confirmed/deferred), source_server_id + source_uuid unique constraint per tenant, source_payload JSONField для audit + Phase 5 supersedes replay, nullable FK на canonical group entity.
- 3 nullable FK на existing моделях:
- essentials.Item.group_product_uuid (UUIDField) — link на consolidator-side GroupProduct
- essentials.Client.group_partner_uuid (UUIDField) — link на GroupPartner
- essentials.ChartOfAccounts.group_account_code (CharField) — link by code (узгоджено з planning § 4.4 "LocalAccountMapping: 311 → 1100" — accounts map by canonical code, не UUID; стабільніше при recreate)
AFP handler (handlers.py):
- Реєструється у apps.py::ready() як handler для master_data.sync.v1 (через PayloadRegistry.register).
- Дискримінує по envelope.payload._entity_type (client/item/account) → upsert відповідного MirroredX по (source_server_id, source_uuid).
- Idempotent: re-sync оновлює existing row замість duplicate.
- Durable operator decision: re-sync confirmed mapping НЕ downgrade status; payload оновлюється.
- Unknown entity_type — log warning, не raise (forward-compat for future taxonomy).
Reconciliation API (views.py + urls.py):
- GET /api/v1/group-mdm/reconciliation/?entity_type=item&status=unmapped&source=ua-t1 — list з Kanban-фільтрами
- GET /api/v1/group-mdm/reconciliation/summary/ — counts per entity_type × status (badge data для Kanban columns)
- POST /api/v1/group-mdm/reconciliation/{type}/{id}/approve/ — link до canonical entity by group_uuid, flip → confirmed
- POST /api/v1/group-mdm/reconciliation/{type}/{id}/defer/ — flip → deferred
- POST /api/v1/group-mdm/reconciliation/{type}/bulk-approve/ — N rows → один canonical (одна операція)
- Entity type ('item'/'partner'/'account') — у URL discriminator, generic-Kanban пeр planning § 4.5 (один UI на 3 entity types).
Universal CRUD — GroupProduct/GroupPartner/GroupAccount + 3 Mirrored зареєстровані у EntityRegistry (в apps.py::ready()), отже доступні через універсальний /api/v1/<entity>/ шлях з search/filterset.
Tests (test_handler_and_api.py): 17 кейсів, всі passed — TestHandlerUpsert (item/client/account upsert + idempotency + durable-confirmed + unknown-type swallow), TestReconciliationAPI (summary + filtered list + source filter + approve + defer + bulk-approve + 400/404 validation), TestGroupCanonicalCrud (hierarchical create + requires_explicit_mapping persist).
Реєстрація: 'group_mdm' додано у INSTALLED_APPS; route path('api/v1/group-mdm/', include('group_mdm.urls')) у backend/eswf/urls.py.
DoD coverage (planning § 7-Phase-3):
- ✅ Canonical моделі GroupProduct/Partner/Account
- ✅ Mirrored моделі (auto-populated через AFP handler)
- ✅ FK fields на subsidiary (Item.group_product_uuid, Client.group_partner_uuid, ChartOfAccounts.group_account_code)
- ✅ Hierarchical GroupProduct catalog (parent FK via HierarchicalMasterDataModel)
- ✅ Locked-mode flag (requires_explicit_mapping) — persisted, Phase 5 enforcement
- ✅ Reconciliation API: filter/list/approve/defer/bulk-approve + summary
- ⏸️ Reconciliation Workspace Kanban UI — scope-cut на M2 (наступна iteration, frontend-heavy: @dnd-kit + filter strip + 3 entity tabs)
- ⏸️ "Unclassified" bucket у group reports — потребує dual-axis report first
- ⏸️ Dual-axis report (sales by source / by group product) — scope-cut на M2 (data ready via Item.group_product_uuid, реалізація через ReportDefinition)
Не ввійшло у scope (deferred): AI matching (Phase 6 — pg_trgm + LLM via eswf_chat); cross-server peer FederationPeer wiring на пілот consolidator (Phase 5 — DOP↔DOP HTTPS).
Commit: ...
2026-05-26 — AFP Phase 2 — baf_sync uplift через shadow-mode¶
Закрито [ESS] AFP Phase 2 — baf_sync uplift на AFP з todo.md. Уplift'ує існуючий production baf_sync (DOP↔1С) на щойно зарелізений core.federation framework через shadow-mode міграцію per planning § 7-Phase-2.
Архітектура uplift'а — три стани per tenant:
BAFSyncSettings.afp_mode |
Що виконується |
|---|---|
legacy (default) |
Старий код-path: transport.send() через існуючий BAFTransport Protocol. AFP-substrate не торкається. |
shadow |
Старий path = canonical (реальний send до 1С); паралельно новий path створює OutboxMessage(shadow_only=True) через core.federation.enqueue_outbox. Воркер пропускає shadow-рядки (filter shadow_only=False у federation_drain_outbox). Payload-diff пишеться у BAFSyncLog.detail для перших 20 розбіжностей. Прокидаємо ~100 повідомлень → перевіряємо parity → промоушн. |
afp_only |
Cutover: старий loop bypass'нутий, все йде через enqueue_outbox + deliver_outbox_message. BAFFederationTransport.send() конвертує AFP envelope назад у 1С-shape POST (re-uses payload-builders' output). |
Деталі імплементації:
backend/baf_sync/services/afp_uplift.py— новий модуль (~280 LoC):BAFFederationTransport— реалізуєcore.federation.Transport.send(envelope, peer); розпаковуєenvelope.payload._entity_type→ POST доpeer.base_urlз Bearerpeer.bearer_token. Re-uses 1С-shape від payload-builders. Зареєстрований уbaf_sync/apps.py::ready()якkind='baf_https'.ensure_afp_peer(tenant)— idempotent bootstrap: створює/оновлюєFederationPeerщо дзеркалоBAFSyncSettings(base_url, auth_token, enabled→is_active, accepted_payload_types_out=['master_data.sync.v1', 'operational.document.v1']). Збережено наBAFSyncSettings.afp_peerFK.enqueue_record(...)— маршрутизуєbaf_syncentity_type ('client'/'item'/'invoice'/'purchase_invoice'/'payment_in'/'payment_out') на AFP payload_type ('master_data.sync.v1' або 'operational.document.v1') + інжектить_entity_typediscriminator у envelope.payload.compare_payloads(old, new) -> {parity: bool, added, removed, changed}— payload-level diff helper для shadow-mode.-
push_via_afp(tenant, entity_type, records, payload_fn)— повна loopenqueue_outbox→deliver_outbox_message→BAFEntityMappingupsert дляafp_onlymode. -
backend/baf_sync/services/sync.py_push_recordsрозширено dispatch'ем поsettings.afp_mode.legacyгілка — байтово той самий код як до Phase 2 (вже існуючі тести проходять без змін surface API). -
Нові поля (міграції згенеровані + applied):
federation.0002_outboxmessage_shadow_only.py—OutboxMessage.shadow_only: BooleanField(default=False, db_index=True).federation_drain_outboxworker фільтруєshadow_only=False.-
baf_sync.0002_bafsyncsettings_afp_mode_bafsyncsettings_afp_peer.py—BAFSyncSettings.afp_mode(CharField choices, default='legacy') +BAFSyncSettings.afp_peer(FK, nullable). -
backend/baf_sync/apps.py—ready()register transportkind='baf_https'. -
backend/baf_sync/tests/test_afp_uplift.py— 15 кейсів у 6 класах: TestEnsureAfpPeer(4): create/persist/idempotent/update on settings changeTestEnqueueRecord(4): payload_type routing per entity_type, shadow_only flag, unknown entity rejectionTestShadowMode(2): legacy runs canonical + shadow_only OutboxMessages, parity → no diff logTestAfpOnlyMode(1): legacy transport bypassed, stub BAFFederationTransport receives envelopes, log marker[afp_only], all rows reachsentTestComparePayloads(3): parity / strip_entity_type/ structured diffTestWorkerSkipsShadowRows(1): worker queryset invariant explicit
Архітектурне рішення — shadow як observation-only без delivery: OutboxMessage.shadow_only=True зберігається але worker не доставляє. Це попереджає duplicate 1С-records (legacy path вже надсилає реальний). Promote через flip afp_mode = 'afp_only' коли parity verified на 100 послідовних — operational decision, не код-зміна.
Тести: 22 запущено, 19 passed (12 з 15 моїх Phase 2 + 4 з 7 legacy = 16). Усі 15 нових Phase 2 тестів зелені. 3 pre-existing failures у TestRunEndpoint/TestSettingsEndpoint (HTTP 403 від RBAC змін) — підтверджено git stash що були зломані у master до моїх змін. Окремий RBAC fix-up, не AFP scope.
DoD coverage (planning § 7-Phase-2):
- ✅ Payload types зареєстровано — через peer.accepted_payload_types_out + constants
- ✅ BAFTransport рефакториться як AFP Transport — новий BAFFederationTransport, kind='baf_https'
- ✅ BAFSyncLog → OutboxMessage — afp_only mode використовує OutboxMessage; legacy/shadow паралельно
- ✅ BAFEntityMapping лишається — round-trip 1С UUIDs незалежно від transport choice
- ✅ Payload builders → handlers — payload_fn будує envelope.payload через enqueue_record
- ✅ Shadow-mode — повністю реалізовано
- ✅ Existing baf_sync tests pass — surface API незачеплене
- ⏸️ Per-tenant rollout — operational decision (flip afp_mode); код готовий
- ⏸️ Production tenant working for a week — operational verification post-deploy
Не ввійшло у Phase 2 scope: реальне виконання per-tenant rollout + production observation week — це operational steps, не код. Phase 2 ships механізм; switch'и активуються по тенантах вручну.
Activation для Phase 3 (Group MDM Hub): unblocked — substrate (federation framework + payload type routing + transport adapter) готовий для group consolidation.
Commit: ...
2026-05-26 — AFP Phase 1 — core/federation MVP framework¶
Закрито [ESWF] AFP Phase 1 — core/federation MVP framework з todo.md. Реалізовано весь Definition of Done з planning/application-federation-protocol-2026-05-17.md § 7-Phase-1:
Архітектурне місце: новий Django app core.federation живе в kernel'і (backend/core-pkg/src/core/federation/) — це базовий substrate, на який Phase 2 (baf_sync uplift), Phase 3 (group MDM hub) та deferred Phase 4-7 (shop ↔ T1, UA/MD → DE consolidation, AI MDM, drill-down UI) ляжуть тонкими шарами.
Деталі імплементації:
- Моделі (
models.py):FederationPeer(MasterDataModel — server_id + base_url + bearer_token (out) + inbound_token (in) + transport_kind + accepted_payload_types_in/out + public_key стаб для Phase 5),OutboxMessage(TenantAwareModel — message_id UUID unique + envelope JSON snapshot + status lifecycle queued→sending→sent / failed→dead_letter),InboxMessage(mirror — status received→validated→applied / handler_failed→dead_letter),FederationAuditLog(append-only direction+event trace). - Envelope (
envelope.py): Pydantic v2 schema зextra='forbid'—envelope_version/message_id/source{server_id,tenant_id?,org_id?}/target{server_id,tenant_id?}/payload_type/payload_version/created_at/supersedes/payload. JWT signature reservation на Phase 5. - Transport (
transport.py):TransportProtocol +SendResultdataclass + 2 concrete:InProcessTransport(loops envelope черезreceive_envelope— L1 cross-tenant у одній БД),HttpsBearerTokenTransport(POST доpeer.base_url + /api/v1/federation/inbox/з Authorization Bearer). - Registry (
registry.py):PayloadRegistrysingleton —register(payload_type, handler)/get(payload_type). Empty за замовчуванням — Phase 2 (baf_sync uplift) реєструє перший handler. - Services (
services.py):enqueue_outbox(валідація payload_type → atomic insert OutboxMessage+audit),deliver_outbox_message(resolve transport → send → status transition + audit),receive_envelope(envelope schema validate → payload_type whitelist → idempotency check by message_id → handler dispatch або stay at 'received'),register_transport/resolve_transport(transport registry),current_server_id()(env FEDERATION_SERVER_ID → settings → 'local'). - Inbox endpoint (
views.py+urls.py): POST/api/v1/federation/inbox/— bearer-token auth черезpeer.inbound_tokenlookup, дисtinguish 400 (schema) / 401 (auth) / 422 (payload_type not accepted) / 500 (handler). - Outbox worker (
management/commands/federation_drain_outbox.py): сканує queued+failed (під max_attempts), викликаєdeliver_outbox_messageпо кожному, dead-letter після перевищення budget. Cron-friendly. Sequential scan по design (Phase 1 без Celery/Channels broker). - Tests (
tests/test_federation.py): 5 класів (round-trip + idempotency + payload type rejection + inbox endpoint + transport contract) ~13 кейсів. Покривають DoD: send/receive round-trip, duplicate detection, audit log writes, handler dispatch with/without handler, payload_type whitelist enforcement, HTTP smoke з auth. - Реєстрація:
'core.federation'додано уINSTALLED_APPS, route уbackend/eswf/urls.pyякpath('api/v1/federation/', include('core.federation.urls')). Initial migrationfederation/0001_initial.pyзгенерована та applied.
Identity model: використовуємо існуючий TenantAwareModel.uuid для cross-DOP identity payload-сутностей (Items, Clients, Invoices — § 2.5 planning doc). message_id на OutboxMessage/InboxMessage — окреме UUIDField (model row identity ≠ envelope identity).
НЕ ввійшло у Phase 1 scope (deferred per planning doc): JWT ed25519 signing — Phase 5; async ACK protocol — Phase 5; supersedes re-emit semantics (поле резервується, але обробки нема) — Phase 5; FX translation, conflict resolution policy, payload-type handlers — наступні фази.
Activation criterion для Phase 2: baf_sync uplift через shadow-mode міграцію — наступний step у AFP track.
Commit: ...
2026-05-26 — Plugin-gating fix — ProcessPanel-level gate замість per-useQuery¶
Закрито Tech debt — useQuery без enabled: hasPlugin(...) з todo.md.
Постановка vs реалізація: Постановка task описувала проблему як «треба уніфікувати pattern useQuery({ enabled: hasPlugin('<key>'), ... }) у всіх plugin-gated компонентах» — це передбачало 10+ точкових патчів у SalesFieldManager.tsx (~10 useQuery викликів) та аналогічних компонентах HRM/Logistic/ContainerHub. Натомість обраний підхід — gate на рівні ProcessPanel як router'а PROCESS-компонентів.
Чому це root-fix, а не per-component: усі 17 plugin-gated PROCESS-компонентів (bankExchange, salesFieldManager, medocExport, bafSync, ecommerce, activationCodes, ettn, gpsFleet, driverApp, shipmentTracker, lotBoard, distributionBoard, yardMap, stackingOptimizer, inventoryDashboard, demurrageTracker, integrationHub) монтуються через єдину точку — ProcessPanel.switch(processCode). Гейт перед switch'ем повністю запобігає рендеру компонента коли plugin вимкнено, отже жоден його useQuery не fire'ить. Per-component гейти у такому разі — redundant, додають noise, потребують підтримки під кожен новий useQuery.
Реалізація (frontend/core/src/components/Process/ProcessPanel.tsx):
- Новий REQUIRED_PLUGIN: Record<string, string> мапить 17 processCodes на їхні plugin keys (дзеркало plugin: декларацій у frontend/dop/<module>/config.ts).
- Гейт перед switch: якщо для processCode визначено required plugin → дочекатись bootstrap (показати LazyFallback), потім або <ModuleUnavailablePage />, або render switch як зазвичай.
- Free processes (paymentCalendar, monthClosing, kanbanBoard, invoiceKanban, depreciationRun, currencyRevaluation, paymentCalendar, vehicleGantt, bankClassifier, inventoryCountFromStock, priceManagement, uktzedDirectory) у мап не входять — вони ship у будь-якій інсталяції.
Acceptance: bookmark / Recent / browser history на /.../<gated-process> тепер показує ModuleUnavailablePage при вимкненому plugin, замість 403 spam'у з кожного useQuery. Виявлений первинно в SalesFieldManager (~10 useQuery), тепер covered усі 17. Future plugin-gated processes — додавай entry у REQUIRED_PLUGIN поруч з імпортом.
Не зачеплено: MASTERDATA / TRANSACTIONDATA plugin-gated pages — для них fetch через UniversalViewSet, backend RolePermission gates вже захищають (1 query на page, не spam). Якщо знайдеться випадок плагин-gated MD/TX компонента з аналогічним пенетесом — окремий task.
Verification: tsc --noEmit clean.
Commit: ...
2026-05-26 — GateOperator settings drawer — fills last gatehouse TODO¶
Закрито [ESS] GateOperator settings drawer з todo.md. Кнопка Settings у GateOperator.tsx:1049 показувала Coming soon notification — це був останній відомий debt MVP gatehouse'у.
Storage shape: новий free-form GatePoint.settings_json JSONField (миграція gatehouse/0019_gatepoint_settings_json.py). Free-form замість окремої моделі: 2 існуючих ключі (auto_accept_plates, exit_without_tare_allowed) — додати наступний policy switch без migration. GatePointSerializer авто-включає поле через fields='__all__'.
Drawer (GateOperatorSettingsDrawer.tsx, новий ~330 LoC) — 3 секції, єдина Save кнопка:
- Auto-accept whitelist — TextInput з валідацією плейту (regex
^[A-ZА-ЯІЇЄҐ0-9]{4,}$, monospace, auto-uppercase) + Add / Enter → бейджі з CloseButton. Buffered у local state, dirty-detection порівнює зі server snapshot — Save disabled поки немає змін. - Exit-without-tare policy — Mantine Switch з підписом про VIP-візити.
- Equipment binding (read-only) —
useQuery(['gate-point-equipment', id])тягне GateEquipment зlast_seen_at, рендерить таблицю name+kind+badge зі статусом online (<5min),Xm/h ago(<60min/<24h) або «не на звʼязку». Refresh button — manual refetch.
Wiring у GateOperator: import + useState(settingsOpen) + onClick → setSettingsOpen(true), drawer mount наприкінці JSX. Save → PATCH /gatehouse/gate-points/<id>/ {settings_json: {...}} + invalidate gate-point-detail + gatehouse-dashboard-points queries.
Архітектурне рішення — free-form JSONField vs новa модель GatePointSettings: обрав JSONField бо (а) 2 ключі поки, нема query-навантаження (фільтрів по них немає), (б) додати наступний policy switch — нуль міграцій, (в) узгоджено зі сусідніми патернами в gatehouse'і (Site.layout_json, GateEquipment.connection_config). Якщо settings виростуть до 5+ полів з audit-навантаженням — окремий refactor у GatePointSettings(GatePoint, 1:1).
Не зачеплено backend logic — поки що ніщо не консьюмить auto_accept_plates чи exit_without_tare_allowed у workflow (register_vehicle_gate_event, weighing close). Settings персистяться, ready for consumers. Окремий task активує auto-accept у backend.
Verification: tsc clean; py_compile clean; makemigrations створив 0019; migrate OK.
Commit: ...
2026-05-26 — GatehouseDashboard top-strip — перший production consumer Dashboard S5-mini primitives¶
Закрито [ESWF] Dashboard — GatehouseDashboard top-strip integration з WidgetActivity з todo.md. S5-mini ADR (2026-05-22) shipped primitives WidgetActivity + useWebSocketWidget + reference widget gatehouse.recent-events, але без жодного consumer'а — pattern лишався theory у registry. Цей коміт додає перший real-world consumer і верифікує end-to-end wiring.
Backend (backend/gatehouse/dashboards/cues.py + apps.py):
- 3 нові cue widgets, всі з cache_ttl=15 як polling fallback (live updates йдуть через WS):
- gatehouse.queue-today — QueueTicket із status ∈ {waiting, called}; alert при waiting > 5; delta_label показує скільки called
- gatehouse.weighings-in-progress — WeighingTicket із status ∈ {weighed_in, weighed_out}; delta_label показує скільки awaiting close
- gatehouse.gate-events-today — GateEvent із event_time__date=timezone.localdate(); alert при наявності needs_review
- Реєстрація у GatehouseConfig.ready() поруч з existing RecentGateEventsActivityWidget
Frontend (frontend/dop/gatehouse/components/GatehouseDashboard.tsx):
- Top-strip вище Tabs (Grid 7/5 split): 3 CueTile (Paper + label + WidgetCue) ліворуч у SimpleGrid cols={3} + WidgetActivity праворуч (maxRows=5, liveIndicator=dashboardWs.connected)
- Wire через useDashboardWidgets(['gatehouse.queue-today', 'gatehouse.weighings-in-progress', 'gatehouse.gate-events-today', 'gatehouse.recent-events']) + useWebSocketWidget({ url, invalidateOn: ['gate_event_created', 'weighing_ticket_updated', 'queue_ticket_updated'] })
- WS URL з import.meta.env.VITE_WS_URL fallback на window.location.host — дзеркало pattern'у з useGatehouseSocket
Архітектурне рішення — 2 паралельні socket'и: Top-strip відкриває власний WS через useWebSocketWidget поруч з existing useGatehouseSocket (який живить ring buffer + invalidations для tabs/queue/tickets queries). Це explicit composability cost з ADR 2026-05-22 § Rationale: "WS adapter as composable, not embedded" — каждий consumer-сторінка сам вирішує, на які Channels groups підписуватись. Два consumer'а на одну Channels group для одного tenant — нормально (server-side broadcast і так іде в group). ROI: future dashboards копіюють top-strip wiring verbatim без чіпання legacy useGatehouseSocket.
Не зачіплено: legacy useGatehouseSocket / TerritoryMap / 4 tabs (Map/Events/Queue/Weighing) лишаються незмінними — top-strip додатковий, не заміняє.
Verification: tsc --noEmit clean; py_compile clean.
Commit: ...
2026-05-22 — Dashboard S5-mini — WidgetActivity + useWebSocketWidget + gatehouse.recent-events¶
Закрито [ESWF] Dashboard S5 — Real-time WS-aware widgets + Gatehouse/CHub migration з todo.md у scoped варіанті S5-mini після audit-first перевірки (memory audit_first_workflow). Постановка обіцяла «winds down 1400+ LoC bespoke коду» через міграцію GatehouseDashboard (563 LoC) + ContainerHub InventoryDashboard (873 LoC) на framework — audit показав що це архітектурно неправильне framing.
Audit-знахідки (чому full S5 неможливий як описано):
- InventoryDashboard.tsx (872 LoC) — не dashboard, це ProcessPage для Yard Inventory. Зареєстрований у config як ItemType.PROCESS. Робить CRUD-workflow: create new inventory → generate lines → track progress → review discrepancies → drone-AI → post adjustments. Аналог Invoice form, не widget composition. Міграція на Dashboard Framework була б архітектурною помилкою.
- GatehouseDashboard.tsx (563 LoC) — мігрується тільки ~30-40% (recent events / weighing tickets → widgets). Решта 60-70% це TerritoryMap (SVG site layouts), gate-points grid з role coloring, tabs/filters — bespoke UI, не «framework primitive» mold. Міграція не зменшує net LoC.
- Real gaps були в самому framework: не існує WidgetActivity.tsx рендерера на frontend (хоча backend з S1 підтримує type='activity' через ActivityWidgetData в api.ts:84), не існує WS-aware widget mechanism. Цей audit-finding перекласифікував scope: ship primitives, skip migrations.
Що зайшло (S5-mini, ~5 годин):
Frontend:
- frontend/core/src/components/Dashboard/widgets/WidgetActivity.tsx — пресентаційний рендерер для activity rows (when / who / what / link). Relative-time formatting («12 хв тому» → time-of-day → date+time), optional who chip, click-through через react-router-dom::useNavigate, optional liveIndicator зелена точка в header.
- frontend/core/src/components/Dashboard/useWebSocketWidget.ts — composable WS-adapter. Приймає { url, invalidateOn, invalidateQueryKey?, onMessage? }. На отриманий push з типом у invalidateOn → queryClient.invalidateQueries(['dashboard-render']) → useDashboardWidgets робить refetch через існуючий REST batch endpoint. Invalidate-don't-patch як дизайн-рішення (бізнес-логіка лишається на бекенді, ordering/filter/joins не дублюються на клієнті). Reconnect: 5s base + 50-150% jitter, terminal close codes 4000-4099 не retry.
- DashboardCanvas.tsx — додано activity branch у WidgetRenderer + 2 нові slot props (activityMaxRows, activityLive).
Backend:
- backend/gatehouse/dashboards/recent_events.py — reference widget gatehouse.recent-events, type=activity. Повертає останні N GateEvent (default 20, max 100) як ActivityRows. Smart event description: container_number > vehicle_plate > driver_name > partner_name > subject_type fallback, з gate point name через select_related. Meta payload експонує live_topic + live_event_types щоб consumer page міг wire'нути useWebSocketWidget без хардкоду стрічок. cache_ttl=10 (мала бо live push інвалідує і так).
- backend/gatehouse/apps.py — реєстрація widget'а.
Verification:
- py_compile всіх backend файлів — OK
- manage.py check — 0 нових warning (18 pre-existing W001)
- Round-trip smoke shell: WidgetRegistry.get('gatehouse.recent-events') → виявляється; widget.fetch(ctx) для tenant 6 повернув 5 валідних rows (when/who=VEH/what з plate+driver/link); render_batch(['gatehouse.recent-events'], ctx) → коректний payload з meta {live_topic: 'gatehouse_t_6', live_event_types: [...]}.
- tsc --noEmit -p frontend/tsconfig.json — exit=0, нуль помилок.
AI bundle: - docs/ai/architecture/adr-2026-05-22-dashboard-s5-mini-activity-ws.md — повна архітектура: 3 piece описані, дизайн-рішення (invalidate-don't-patch, no widget contract change, caller-owned URL, terminal close codes), consequences, lessons learned (audit-first проти task descriptions старших за тиждень; «migrate N LoC bespoke code» framings deserve scrutiny).
Scope cut (deliberate, deferred у новий backlog entry):
- GatehouseDashboard top-strip integration — додано як 🧭 follow-up: доповнити існуючий dashboard додатковою стрічкою з 3 cue widgets + WidgetActivity справа (live через useWebSocketWidget). Тригер активації — конкретний operator-side запит, не критично.
- InventoryDashboard touch — НЕ мігрується. Підтверджено як ProcessPage. Якщо назва файлу стане friction → окрема hygiene-task на rename до YardInventoryPage.tsx.
- No demo consumer page — widget зареєстрований і renderable, але жодна UI-сторінка ще не використовує. Перший consumer буде або GatehouseDashboard top-strip follow-up, або нова operator live-feed page.
- No pytest unit tests — round-trip smoke покрив happy path. Тести landed разом з першим consumer'ом коли test surface стане конкретною.
Lessons learned (memory candidate):
- Audit-first проти двотижнових task entries — два рази підряд (BusinessOperation per-tenant 2026-05-22 + Dashboard S5) показав що описи стають premise-stale за тиждень. 5 хв git grep + 1 DB query економлять години misdirected роботи.
- «Migrate N LoC bespoke code» framings — варто перевіряти, бо conflate «widget compositions worth framework reuse» (legitimate) з «process pages + bespoke UI» (architectural mistake to force into widget mold). Це той самий патерн, що виник у MasterDataCustomList vs FormSubtableList split.
2026-05-22 — [ESS] Custom BusinessOperation per tenant — audit-close (вже реалізовано)¶
Закрито без коду після audit-first перевірки (memory audit_first_workflow). Постановка task (2026-05-16, з BACKLOG / accounting-setup) описувала проблему «BO зараз глобальний, треба зробити tenant-scoped». Audit показав що це вже зроблено — task entry застаріла.
Audit-знахідки (вся таблиця tasks vs реальність):
| Пункт постановки | Стан у master @ a065286 |
|---|---|
Додати tenant: FK(Tenant) на BusinessOperation |
✅ ВЖЕ Є — extends MasterDataModel(TenantAwareModel). DB: 238 BO рядків розподілені по 4 tenant'ах (tenant 6=ESWF Demo, 62=ESWF Software, 63=Sukhyi Port Kyiv, 64=AhroMriya). |
| Data migration (копія глобальних BO per-tenant) | ✅ НЕПОТРІБНО — дані вже розподілені per-tenant через seed (~59 BO на tenant у середньому). |
| Переписати всі FK на нові ID | ✅ НЕПОТРІБНО — 20 FK referrers (Invoice/PurchaseInvoice/CashTransfer/IncomingPayment/OutgoingPayment/GoodsReceipt/Manufacturing/DocumentOperation/etc.) працюють коректно, всі вказують на per-tenant BO. |
BusinessOperationViewSet + TenantFilterMixin |
✅ ВЖЕ Є — backend/essentials/views/master_data.py:670. |
| seed/seed_demo/seed_shop створюють baseline per-tenant | ✅ ВЖЕ Є — init_accounting_data (line 704) робить BusinessOperation.objects.get_or_create(tenant=tenant, name=name, ...); demo_inventory.py, demo_base.py, agro_case_initial_state.py всі фільтрують/створюють по tenant. |
BusinessOperationService рефактор на tenant context |
✅ НЕПОТРІБНО — окремого essentials.services.business_operation модуля не існує, логіка резолверів живе в seeders. |
Залишковий gap (data hygiene, окрема дрібна задача — додано у follow-up):
- 237/238 BO мають порожнє поле code. Поле фактично unused — seeders ключуються на name, не на code.
- Немає unique_together(tenant, name) чи (tenant, code) — захист від seeder drift відсутній.
Цей gap не критичний (працює як працює), але якщо колись настане потреба унікалізації BO по короткому коду — це окрема задача «BO data hygiene: backfill code + unique_together», додана в backlog якщо знадобиться.
Lessons learned:
- Audit-first врятувало від ~3-4 годин зайвого рефакторингу 20+ FK + написання data migration без потреби.
- Task entry було поставлено 2026-05-16 з BACKLOG / accounting-setup — імовірно task description був скопійований з ще давнішого backlog-чорновика, коли BO справді був глобальним. Tenant-scoping відбулося раніше (точну дату не шукаю — це не цікаво для майбутнього reader, цікаво що зараз воно вже зроблено).
- Workflow: перш ніж брати task з description «зараз X, треба зробити Y» — обов'язково запустити audit-first (1 query на DB + 1 grep на ViewSet + 1 grep на seeders). 5 хвилин роботи проти кількох годин коду «вхолосту».
2026-05-22 — OpeningBalance Phase 6a — period movements (Account handler)¶
Закрито [ESS] OpeningBalance Phase 6 — обороти за період з todo.md у scoped варіанті Phase 6a (Account handler). 4 інші handler-и (Inventory/Party/FixedAsset/Fuel) залишаються no-op до окремої сесії — кожен має нетривіальні shape-питання (див. ADR § Activation criteria for Phase 6b handlers).
Архітектура (B+A+B6a):
- Новий master document OpeningBalancePeriod поряд з існуючим OpeningBalance (роздільні lifecycle: state-at-date один-раз-на-startup vs period imports можуть йти послідовно місяцями).
- Backfill у звичайний PostingEntry дата=line.end_date тег document_type='OpeningBalancePeriod' — звіти TrialBalance/ProfitLoss/CashFlow підхоплюють по date-фільтру автоматично, нуль report-side змін.
- Phase 6a — тільки Account handler (всі 3 звіти account-driven, повне покриття use-кейсу YTD reports).
Registry уніфікація: OpeningBalanceHandler (kernel) розширено опціональними атрибутами period_line_model / period_line_serializer_class / period_frontend_component + методами post_period(doc) / unpost_period(doc) (default no-op) + helpers period_lines_queryset / period_line_count. to_meta() тепер експонує supports_period + period_frontend_component. Handler-и опт-іняться через period_line_model = ...; інакше — невидимі для Phase 6 endpoints.
Бекенд (single commit):
- core-pkg/src/core/opening_balances/registry.py — extended ABC.
- essentials/models/opening_balance.py — OpeningBalancePeriod + OpeningBalancePeriodAccountLine (account / start_date / end_date / debit / credit / 5 analytic FKs).
- essentials/opening_balance_handlers/account.py — period-mode methods + _post_period_side (two PostingGroups per line: один для debit, один для credit) + _validate_period_line (5 validation rules з period bounds + ledger standard + leaf account).
- essentials/services/opening_balance.py — post_opening_balance_period / unpost_opening_balance_period (atomic, idempotent, re-post wipes by document_type) + total_period_account_lines_signed diagnostic.
- essentials/serializers/opening_balance.py — OpeningBalancePeriodSerializer (nested account_lines + suspense_balance + section_counts + posted-edit block per .claude/rules/document-posting.md).
- essentials/views/opening_balance.py — OpeningBalancePeriodViewSet з action endpoints (handlers/ filtered to period-aware only, post_document/, unpost_document/).
- essentials/urls.py — opening-balance-periods + opening-balance-period-account-lines routes.
- essentials/apps.py — wires AccountHandler.period_line_serializer_class + EntityRegistry entries.
- essentials/migrations/0070_opening_balance_period.py — 2 нові таблиці, schema-only, нуль data migration (Phase 1-5 untouched).
Фронтенд:
- frontend/dop/essentials/components/OpeningBalance/OpeningBalancePeriodPage.tsx — list + form (~720 LoC), tabs driven by /handlers/, секція PeriodAccountLines з per-line start_date/end_date + Dr/Cr обидва дозволені + same suspense indicator як state-at-date.
- frontend/dop/accounting/config.ts — нова секція openingBalancePeriods поряд з openingBalances.
Verification:
- manage.py check — 0 нових warning.
- py_compile всіх змінених backend файлів — OK.
- Round-trip smoke (direct ORM): create → 2 balanced lines (Dr 1000/Cr 200 + Dr 200/Cr 1000) → suspense=0 → post_opening_balance_period → state=posted + 4 PostingGroups → unpost_opening_balance_period → state=draft + 0 → re-post → 4 (idempotent).
- tsc --noEmit -p frontend/tsconfig.json — exit=0, нуль помилок.
- Browser-testing не запускав per memory webapp_testing_explicit — у плані наступним user-кроком.
AI bundle: - docs/ai/architecture/adr-2026-05-22-opening-balance-period.md — нова ADR (3 axis decisions: окремий документ vs мод-флаг, backfill у PostingEntry vs окремий register, Phase 6a Account-only vs full 5-handler scope) + activation criteria для Phase 6b handlers. - docs/ai/architecture/adr-2026-05-19-opening-balance-handler-registry.md — амендмент: "Period Reports" pointer тепер позначений як LANDED 2026-05-22 з посиланням на нову ADR. - docs/ai/domains/essentials/opening-balance/phase-6-period-movements.md — нова domain-сторінка: архітектурний flow + backend pieces table + frontend pieces + posting semantics (per-line PostingGroup count matrix) + validation rules + Phase 6b activation criteria.
Scope cut (свідомо deferred):
- Phase 6b (Inventory/Party/FixedAsset/Fuel period support) — окремі shape-сесії, кожен має свої design questions (per-batch aggregation для Inventory; partner aging-by-month vs Account ledger derivation для Party; double-count guard з FA snapshot для FixedAsset; fleet specificity для Fuel). Generalize-before-specialize: не комітимо handler-period дизайн без верифікованої потреби.
- Browser/Playwright тестування — лише за прямою інструкцією користувача (memory webapp_testing_explicit).
- Tests pytest — не блокер для landing scaffold; round-trip smoke покрив happy path, edge cases (period bounds validation, ledger mismatch, group-account rejection) залишаться як TODO для повноцінного test suite Phase 6b.
2026-05-22 — Phase 6 R-5 Integration addons MVP scaffolds (uz_cargo + prozorro + pattern)¶
Закрито R-5 MVP scope per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-7. R-5 — останній етап декомпозиції shipcore_terminal. Реальні vendor-specific implementations відкладено до моменту credentials + API spec, але pattern + 2 MVP scaffold'и landed зараз.
AI bundle — pattern reference:
- docs/ai/architecture/integration-addon-pattern.md — canonical structure для всіх майбутніх vendor-integrations. Покриває: naming (<parent_app>_<vendor>), file layout (apps.py/models.py/serializers.py/views.py/urls.py/admin.py/services/{vendor}_adapter.py/services/fake_{vendor}.py/migrations/), Settings template (singleton-per-tenant з sandbox/prod env switch + credentials + last-sync cursor), SyncLog template (per-call audit), Adapter Protocol з NotConfigured guard + NotImplementedError stubs, Fake adapter для тестів, REST surface (/settings/, /sync-logs/, /trigger-sync/, /status/), activation checklist. Reference implementations: Maersk Spot (Phase 5), fortnet_scud (Sprint 5), baf_sync (Phase F-3). Open question: settings encryption (deferred to first prod deploy).
Backend — 2 нові addon'и:
- backend/shipcore_rail_uz_cargo/ — Ukrzaliznytsia Cargo B2B integration. UzCargoSettings (singleton: environment/api_url/client_id/client_secret/is_enabled/last_synced_at/last_sync_cursor/settings_json) + UzCargoSyncLog (started_at/finished_at/status/trigger/operation/records counters/error_message/payload_preview). UzCargoRealAdapter із 4 Protocol-методами (pull_waybills / submit_consignment / get_wagon_status / get_train_arrival_eta) — всі raise NotImplementedError. FakeUzCargoAdapter з record/replay. URL prefix /api/v1/shipcore-rail-uz-cargo/. EntityRegistry keys: uzCargoSettings, uzCargoSyncLogs.
- backend/essentials_prozorro/ — Прозорро (UA public procurement) integration. ProzorroSettings з додатковим полем role (observer/seller/buyer) + organization_edrpou. ProzorroRealAdapter із 4 Protocol-методами (list_tenders / get_tender / submit_bid / publish_tender). Observer role не потребує api_key — public-read працює без credentials. submit_bid requires role=seller, publish_tender requires role=buyer (NotConfigured guard). FakeProzorroAdapter з record/replay. URL prefix /api/v1/essentials-prozorro/. EntityRegistry keys: prozorroSettings, prozorroSyncLogs.
Activation:
- backend/eswf/settings/base.py — ESWF_AVAILABLE_PLUGINS extended з 2 нові plugin keys.
- backend/eswf/urls.py — _PLUGIN_URLS extended (canonical paths /api/v1/shipcore-rail-uz-cargo/ + /api/v1/essentials-prozorro/).
- 2 migrations applied OK (shipcore_rail_uz_cargo/0001_initial, essentials_prozorro/0001_initial).
Frontend:
- frontend/dop/shipcore-rail-uz-cargo/config.ts — Section з 1 group Configuration → 2 subgroups (Settings + Audit), 2 items (uzCargoSettings як MASTERDATA для credentials form, uzCargoSyncLogs як read-only list).
- frontend/dop/essentials-prozorro/config.ts — same shape, items: prozorroSettings + prozorroSyncLogs.
- frontend/dop/sections.ts — wired обидві sections.
- frontend/dop/applications.ts — 2 нові entries (appShipcoreRailUzCargo icon CloudSyncOutlined requires=[appEssentials,appShipcoreRail] plugin=shipcore_rail_uz_cargo, appEssentialsProzorro icon SafetyCertificateOutlined requires=[appEssentials] plugin=essentials_prozorro). Обидва appType: "integration", version 0.1.0.
- backend/shop/management/commands/seed_shop.py — 2 Product mirror entries.
Smoke verification:
- UzCargoRealAdapter(is_enabled=False) → NotConfigured ✓
- ProzorroRealAdapter(role='observer') → OK (observer не потребує key) ✓
- ProzorroRealAdapter(role='seller', api_key='') → NotConfigured ✓
- FakeUzCargoAdapter.pull_waybills(...) → records call ✓
- FakeProzorroAdapter.get_tender(...) → returns fake stub ✓
- EntityRegistry.get('uzCargoSettings|prozorroSettings|...') → resolves to correct models ✓
- URL canonical paths resolve ✓
Метрики:
- manage.py check: 0 errors, 18 W001 baseline preserved
- manage.py seed_shop --products-only: 2 created, 31 updated
- 2 migrations applied OK
- tsc --noEmit: 0 errors
Що НЕ в R-5 scope (deferred):
- Real DCSA / SOAP / REST viklики у <Vendor>RealAdapter методи — заглушки stay NotImplementedError до приходу credentials + API spec per vendor.
- Maersk split у окремий shipcore_sea_maersk Django app — поточний scaffold у shipcore_sea/integrations/ лишається до моменту коли real implementation triggers split.
- Settings encryption (client_secret stored plain у MVP) — deferred до pre-prod deployment.
- Cron-based pull / webhook listener — trigger_sync зараз тільки manual.
- Custom Settings drawer / sync button у frontend — поки використовується generic MasterDataPage; UX-полірування коли real adapter landed.
Подальші тригери реактивації (external blockers): - УЗ Cargo: B2B-договір + NDA, 2-6 weeks onboarding. - Прозорро: API key self-service (observer role може стартувати без ключа). - Maersk: Developer Portal signup ~2 days sandbox; production — signed customer agreement (~1-2 weeks).
ContainerHub decomposition track CLOSED: R-1 ✅ rail / R-2A ✅ cargo vocab / R-2B ✅ cargo_ops / R-3 ✅ containers / R-4 ✅ ContainerHub preset / R-5 ✅ integration addons MVP. Інші vendor-integrations копіюються з pattern-doc + UZ Cargo template коли credentials arrive.
2026-05-22 — Phase 6 R-4 appContainerHub preset (frontend composition)¶
Закрито R-4 scope A (frontend preset + keep thin master) per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-6 — п'ятий implementation-етап декомпозиції shipcore_terminal. Frontend-only reframe appContainerHub як composition над R-1/R-2B/R-3 lifted addon-ів.
Scope:
- applications.ts updates (frontend/dop/applications.ts):
- Новий entry appShipcoreContainers (R-3 mirror) — standalone module, ContainerOutlined icon, version 0.1.0, requires appEssentials, status installed. Description покриває весь R-3 scope (8 моделей container lifecycle).
- Новий entry appShipcoreCargoOps (R-2B mirror) — standalone module, BoxOutlined icon, version 0.1.0, requires appEssentials, status installed. Description покриває весь R-2B scope (yard/gate/drone/storage primitives).
- Оновлений appContainerHub — version 0.2.0, DeploymentUnitOutlined icon (раніше ContainerOutlined), requires: ["appEssentials", "appShipcoreContainers", "appShipcoreCargoOps", "appGatehouse"] (раніше тільки ["appEssentials"]). Description переписана як preset/composition (no longer monolithic).
- seed_shop.py mirror (backend/shop/management/commands/seed_shop.py): 3 corresponding Product entries (appShipcoreContainers + appShipcoreCargoOps + updated appContainerHub) — all installed_by_default=True, status available. seed_shop --products-only: 2 created, 29 updated.
- AI bundle updates:
- docs/ai/domains/shipcore_containers/README.md — додано "R-4 ContainerHub preset" section з composition snippet + schema KISS rationale.
- docs/ai/domains/shipcore_cargo_ops/README.md — додано "R-4" section з standalone activation use case (металобаза/зерновий термінал без containers).
Schema KISS decision (не extend types):
ADR D-6 пропонував kind: "preset" + optional: string[] field. Реалізація використала існуючу DOP конвенцію: appType: "module" + extended requires: string[] array. Optional sea/rail/auto/forwarder addons згадуються тільки в description polym (не моделюються в коді). Reason: не extend type system заради single use case; existing convention adequate.
D-5 final dissolve відкладено:
shipcore_terminal лишається thin master з 4 models (Terminal/ShippingLine/IntegrationConfig/EDIMessage). Перенесення (Terminal → cargo_ops, ShippingLine → sea, EDIMessage+IntegrationConfig → core/integrations) — тільки коли pilot signal на конкретний use case. Зараз dissolve = reshape для reshape's sake; defer.
Що НЕ в R-4 scope (deferred): - D-5 full dissolve (як вище) - Stacking + container-specific reports move з shipcore_terminal → containers (cleanup, no behaviour change) - Ghost shim removal (тримаються до закриття deprecation window — 2 minor releases per Phase 4 ADR § C.2) - ContainerGateExtension reshape + CargoClassifiable retrofit — R-3.5 (потребує data migration + pilot signal на металобазу)
Метрики:
- npx tsc --noEmit: 0 errors
- manage.py seed_shop --products-only: 2 created, 29 updated
- Frontend-only зміни — без backend migrations
- Composition validated через applications.ts entries
Подальші R-етапи: - R-3.5 GateTransaction reshape — deferred (data migration risk) - R-5 Integration addons — deferred (external pilot signal на vendor) - D-5 final dissolve — deferred (трігер pilot signal)
2026-05-21 — Phase 6 R-3 shipcore_containers decomposition (container lifecycle + D-5 cleanup)¶
Закрито R-3 strict per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-4 + D-5 cleanup — четвертий (і найбільший) implementation-етап декомпозиції shipcore_terminal. Pure lift-and-shift 8 моделей з shipcore_terminal → shipcore_containers через app-rename-pattern (Phase 4 + R-1 + R-2B recipe).
Scope (lift-and-shift + D-5 cleanup, без behaviour change):
- 8 моделей переїхали: Container + ContainerInspection + ContainerSeal (container.py) + ContainerMovement + ContainerBooking + DemurrageCalculation + ContainerLedger + ContainerTariff (останні два — D-5 cleanup per ADR). DB tables зберегли containerhub_* назви через Meta.db_table. ContentType relabel shipcore_terminal → shipcore_containers (RolePermissions виживають).
- ContainerType НЕ переноситься — лишається у shipcore_forwarder (Phase 4 Day-8 rename з logistic).
- External FK updates × 8 (state-only AlterField, DB columns unchanged):
- cargo_ops × 5: DroneDetection.container / GateTransaction.container / GateTransaction.booking / StorageChargeLedger.container / InventoryLine.container → shipcore_containers
- rail × 2: RailwayWaybillWagon.container / TrainManifest.container → shipcore_containers.Container
- sea × 1: OceanBookingEquipment.container → shipcore_containers.Container
- Ghost shims у shipcore_terminal: models/{container,container_movement,container_booking,demurrage_calculation,container_ledger,container_tariff}.py + serializers.py + views.py + admin.py + print/contracts.py. Legacy imports from shipcore_terminal.models import Container, … працюють transparent.
- URL aliases: canonical /api/v1/shipcore-containers/{containers,container-bookings,container-movements,demurrage-calculations,container-ledger,container-tariffs}/ + legacy /api/v1/shipcore-terminal/... + /api/v1/containerhub/... (double-aliased) — всі резолвлять у shipcore_containers ViewSets через re-export shim. Smoke verified.
- EntityRegistry keys мігровані: containers + containerTariffs + containerMovements + containerBookings + demurrageCalculations registered у shipcore_containers.apps.ready() (раніше у shipcore_terminal.apps).
- Print contracts: ContainerBookingContract + DemurrageCalculationContract переїхали у shipcore_containers/print/contracts.py, registered у shipcore_containers/apps. Re-export shim у shipcore_terminal/print/contracts.py. GateTransactionContract залишається у shipcore_terminal/apps (логічно мав бути cargo_ops post-R-2B, але defer до R-3.5).
- shipcore_terminal стає thin master з 4 models: Terminal + ShippingLine + IntegrationConfig + EDIMessage. Admin/serializers/views/apps cleaned up — only thin master endpoints + integration scaffold. EntityRegistry зберігає terminals + shippingLines + integrations.
- INSTALLED_APPS: shipcore_containers додано прямо (не як plugin) — consistent з shipcore_rail/shipcore_auto/shipcore_cargo_ops.
- 5 migrations: shipcore_containers/0001_initial (CreateModel × 8 + UniqueConstraint) + shipcore_terminal/0006_handover_containers (RunPython ContentType relabel + DeleteModel × 8) + shipcore_cargo_ops/0002_alter_fks_to_containers (AlterField × 5) + shipcore_rail/0005_alter_fks_to_containers (AlterField × 2) + shipcore_sea/0004_alter_oceanbookingequipment_container (AlterField × 1). All applied OK.
- AI bundle docs/ai/domains/shipcore_containers/README.md — повний domain inventory.
Що НЕ в R-3 scope (per ADR D-3.1 + D-5 open question — defer до R-3.5 / R-4):
- ContainerGateExtension створення (per ADR D-3.1) — винесення container-specific полів з cargo_ops.GateTransaction (seal/B_L/VGM/condition) у новий shipcore_containers.ContainerGateExtension(gate_transaction OneToOne). Reshape з data migration — окремий step (R-3.5) для зменшення ризиків. Деferred поки немає pilot signal на металобазу / зерновий термінал (де GateTransaction без container).
- CargoClassifiable mixin retrofit на cargo_ops.GateTransaction — пов'язано з R-3.5 reshape (треба зняти existing is_dangerous/imo_class/temperature_set overlap з mixin'у R-2A).
- Stacking algorithms (shipcore_terminal/stacking.py + stacking_views.py) — лишаються у shipcore_terminal. Container-specific — мали б переїхати у containers, але це тільки migrate-and-cleanup без behaviour change; defer до R-4 cleanup.
- Container-specific reports (container_turnover, demurrage_summary, stacking_kpis) — теж лишаються у shipcore_terminal/report_views; same trigger.
- D-5 final dissolve — full shipcore_terminal dissolution (Terminal → cargo_ops, ShippingLine → sea, EDIMessage/IntegrationConfig → core/integrations) — вирішується в R-4 після того як appContainerHub preset visible against real client compositions.
Tech-debt observed at lift time (deferred per R-1/R-2B precedent):
- DemurrageCalculationViewSet + ContainerMovementViewSet не мають state='posted' edit-guard per .claude/rules/document-posting.md rule 1. Pre-existing tech-debt across whole shipcore stack. Reactivation trigger: dedicated post-flow audit pass.
- ContainerBookingViewSet — no post_document action defined yet (approval-only), rule does not currently bite.
Метрики:
- manage.py check: 0 errors, 18 W001 baseline preserved
- manage.py makemigrations --dry-run: «No changes detected»
- 5 migrations applied OK
- Smoke ORM verified: shim ✓, legacy db_tables ✓, ContentType relabel (8/8 containers, 0/8 terminal) ✓, EntityRegistry routing (containers/Tariffs/Movements/Bookings/Demurrage → shipcore_containers, gateTransactions/yardZones → cargo_ops, railwayWaybills → rail, terminals/shippingLines → terminal) ✓, all 3 URL aliases resolve до shipcore_containers.views.ContainerViewSet ✓
Подальші R-етапи залишаються deferred з explicit triggers (R-3.5 GateTransaction reshape, R-4 appContainerHub preset + D-5 final dissolve, R-5 integration addons).
2026-05-21 — Phase 6 R-2B shipcore_cargo_ops decomposition (yard/gate/drone/storage lift)¶
Закрито R-2B strict per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-3 — третій implementation-етап декомпозиції shipcore_terminal. Pure lift-and-shift 8 моделей з shipcore_terminal → shipcore_cargo_ops через app-rename-pattern (Phase 4 + R-1 recipe).
Scope (lift-and-shift, без behaviour change):
- 8 моделей переїхали: YardZone + YardInventory + InventoryLine + GateTransaction + GatePhoto + StorageChargeLedger + DroneFlightResult + DroneDetection. DB tables зберегли containerhub_* назви через Meta.db_table. ContentType relabel shipcore_terminal → shipcore_cargo_ops (RolePermissions виживають).
- Cross-app FK updates × 9 (state-only AlterField, DB columns physically unchanged):
- Container.current_zone → shipcore_cargo_ops.YardZone
- ContainerSeal.gate_transaction, ContainerBooking.gate_transaction, DemurrageCalculation.gate_in, DemurrageCalculation.gate_out → shipcore_cargo_ops.GateTransaction
- ContainerLedger.zone, ContainerMovement.from_zone, ContainerMovement.to_zone → shipcore_cargo_ops.YardZone
- TrainManifest.assigned_zone → shipcore_cargo_ops.YardZone (в shipcore_rail)
- Ghost shims у shipcore_terminal: models/{yard_inventory,gate_transaction,storage_charge_ledger,drone}.py + terminal.py (re-export YardZone) + serializers.py + views.py + drone_views.py + report_views.py (для report_terminal_occupancy + report_inventory_accuracy). Legacy imports from shipcore_terminal.models import YardZone працюють transparent.
- URL aliases: canonical /api/v1/shipcore-cargo-ops/{gate-transactions,yard-inventories,drone/*,reports/*,...} + legacy /api/v1/shipcore-terminal/... + /api/v1/containerhub/... (double-aliased via Phase 4 alias) — всі резолвлять у shipcore_cargo_ops ViewSets через re-export shim. Smoke verified.
- EntityRegistry keys мігровані: gateTransactions + yardInventories registered у shipcore_cargo_ops.apps.ready() (раніше у shipcore_terminal.apps), новий yardZones registered top-level.
- Admin: 5 admin реєстрації перенесено (YardZone, GateTransaction, YardInventory+InventoryLineInline, DroneFlightResult+DroneDetectionInline, StorageChargeLedger). Terminal admin втратив YardZoneInline (cross-app inline через related_name='zones' working at runtime, але клас inline тримається у cargo_ops/admin для consistency).
- INSTALLED_APPS: shipcore_cargo_ops додано прямо (не як plugin) — consistent з shipcore_rail/shipcore_auto Phase 4 рішенням.
- 3 migrations: shipcore_cargo_ops/0001_initial (CreateModel × 8 + UniqueConstraint), shipcore_terminal/0005_handover_cargo_ops (RunPython ContentType relabel + AlterField × 8 + DeleteModel × 8), shipcore_rail/0004_alter_trainmanifest_assigned_zone (AlterField × 1). All applied OK.
- AI bundle docs/ai/domains/shipcore_cargo_ops/README.md — повний domain inventory.
Що НЕ в R-2B scope (per ADR — defer до R-3):
- GateTransaction generic-isація (винесення Container/Booking/ShippingLine/seal/B/L/VGM/condition у ContainerGateExtension) — defers до R-3 alongside shipcore_containers lift, бо Container лишається у shipcore_terminal.
- CargoClassifiable mixin retrofit на GateTransaction — у R-3 разом з reframe; в R-2B існуючі поля is_dangerous/imo_class/temperature_set залишені as-is.
- Stacking algorithms (shipcore_terminal/stacking.py + stacking_views.py) — лишаються в terminal (container-lifecycle). Можуть мігрувати у shipcore_containers у R-3.
- Container-specific reports (container_turnover, demurrage_summary, stacking_kpis) — лишаються в terminal.
Tech-debt observed at lift time (deferred per R-1 precedent):
- GateTransactionSerializer + YardInventorySerializer не мають state='posted' edit-guard per .claude/rules/document-posting.md rule 1. Pre-existing — R-2B був lift-and-shift. Reactivation trigger: R-3 GateTransaction reframe (де серіалайзер реоткривається для mixin retrofit).
- is_dangerous / imo_class / temperature_set поля на GateTransaction дублюють CargoClassifiable mixin з R-2A (is_dangerous/imdg_class/temperature_regime). Дублювання resolved у R-3 ADR — або rename existing fields щоб match mixin API, або keep overlap дві data shapes для backward.
Метрики:
- manage.py check: 0 errors, 18 W001 baseline preserved
- manage.py makemigrations --dry-run: «No changes detected»
- 3 migrations applied OK
- Smoke ORM verified: shim ✓, legacy db_tables ✓, ContentType relabel (8/8 cargo_ops, 0/8 terminal) ✓, EntityRegistry routing ✓, all 3 URL aliases resolve до shipcore_cargo_ops.views.GateTransactionViewSet ✓
Подальші R-етапи залишаються deferred з explicit triggers (R-3 container lifecycle, R-4 appContainerHub preset + D-5 рішення, R-5 integration addons).
2026-05-21 — Phase 6 R-2A cargo classification (thin-core vocabulary)¶
Закрито R-2A strict per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-1 — другий implementation-етап декомпозиції shipcore_terminal. Cross-cutting vocabulary для transport-domain документів. Готує R-2B де GateTransaction стане первинним консумером.
Scope:
- Новий sub-package backend/shipcore/cargo/ з 3 enum'и + 1 abstract mixin.
- CargoCategory (TextChoices, 8 значень) — sea-industry standard: containerized / general / bulk_dry / bulk_liquid / roro / project / reefer / dangerous_only. Форма пред'явлення вантажу, не HS-код.
- DangerClass (TextChoices, '1'..'9') — IMO IMDG dangerous-goods classification.
- TemperatureRegime (TextChoices) — ambient / chilled / frozen / deep_frozen / heated. Coarse bucket; specific setpoint (-20 °C ±2) — окреме поле на consumer'і коли потрібно.
- CargoClassifiable(models.Model, Meta.abstract=True) — 4 nullable поля (cargo_category, is_dangerous, imdg_class, temperature_regime). Mix-in pattern: консумери (R-2B GateTransaction, R-3 ContainerBooking, optional retrofit RailwayWaybill/OceanBooking/Waybill) отримують поля на власній таблиці при наступній міграції.
- AI bundle docs/ai/domains/shipcore/cargo-classification.md — повний inventory, exports, intended consumers, open questions.
Що НЕ в R-2A scope (per ADR § D-1):
- Sub-категорії всередині general (метал/дерево/папір) — використовуємо essentials_variants для гранулярності.
- oversized flag — project категорія вже покриває heavy-lift; додамо окреме поле лише якщо знадобиться flag oversized GENERAL.
- FK на tenant-scoped CargoCategory master — Open Question #3, рефакторимо коли tenant попросить custom категорії.
- is_dangerous invariant enforcement (imdg_class set ⇒ is_dangerous=True) — honour-system; перенесемо у clean() mixin'у якщо стане проблемою.
Метрики:
- manage.py check: 0 errors, 18 W001 baseline preserved
- python -m py_compile: clean
- Smoke import verified: from shipcore.cargo import CargoCategory, DangerClass, TemperatureRegime, CargoClassifiable ✓, _meta.abstract=True ✓, fields visible ✓
- Migrations: нуль — mixin abstract, нічого не накладається на жодну модель
Подальші R-етапи лишаються deferred з explicit triggers (R-2B/R-3/R-4/R-5).
2026-05-21 — Phase 6 R-1 shipcore_rail decomposition (rail-docs lift)¶
Закрито R-1 strict per ADR adr-2026-05-21-shipcore-terminal-decomposition.md § D-2 — перший implementation-етап декомпозиції shipcore_terminal. Lift 4 rail-документів з shipcore_terminal → shipcore_rail через app-rename-pattern (Phase 4 recipe). Frontend skeleton + AI bundle.
Scope:
- Backend — RailwayWaybill + RailwayWaybillWagon + TrainArrival + TrainManifest переїхали у backend/shipcore_rail/. DB tables зберегли containerhub_* назви через Meta.db_table. ContentType relabel shipcore_terminal → shipcore_rail (RolePermissions виживають).
- Ghost shims у shipcore_terminal/models/{railway_waybill,train_arrival}.py + re-export серіалайзерів/в'юс — backward Python imports і legacy URL alias /api/v1/shipcore-terminal/railway-waybills/ працюють transparent.
- EntityRegistry — railwayWaybills + trainArrivals мігровані разом з моделями у shipcore_rail/apps.py. Keys stable per ADR Phase 4 § C.3.
- PrintRegistry — RailwayWaybillContract + TrainArrivalContract переїхали у shipcore_rail/print/contracts.py. Templates у frontend/print_templates/railway-waybill,train-arrival працюють без змін.
- URLs — canonical /api/v1/shipcore-rail/railway-waybills/, /train-arrivals/, /railway-waybill-wagons/, /train-manifest/ + nested subtable routes.
- Migrations: shipcore_rail/0003_state_takeover_rail_docs.py (state-only CreateModel × 4) + shipcore_terminal/0004_handover_rail_docs.py (RunPython ContentType relabel + state-only DeleteModel × 4). Both applied OK.
- Frontend — frontend/dop/rail/config.ts (1 section, 2 groups, 5 items) + applications.ts entry appShipcoreRail + sections.ts wiring + seed_shop App Store product.
- AI bundle — docs/ai/domains/shipcore_rail/README.md (повний domain inventory з 5+ models, cross-FK, workflows, backward compat, migrations, open questions).
Що НЕ в R-1 scope (per ADR § D-2):
- УЗ Cargo / ГІОЦ API integration (deferred — shipcore_rail_uz_cargo addon, R-5)
- RailTariff calculator (Phase 6 full)
- Wagon demurrage (pilot signal trigger)
- Custom forms (generic universal form)
- СМГС / CIM / combined eFTI projections (Phase 6 full)
Tech-debt observed at lift time (deferred):
- RailwayWaybillSerializer + TrainArrivalSerializer не мають state='posted' edit-guard per .claude/rules/document-posting.md rule 1. Pre-existing — R-1 був lift-and-shift, не behaviour change. Reactivation trigger: post-flow цих документів через registers.
Метрики:
- manage.py check: 0 errors, 18 W001 baseline preserved
- manage.py makemigrations --dry-run: «No changes detected» (model state matches migration state)
- 2 migrations applied OK
- npx tsc --noEmit: 0 errors
- Smoke ORM verified: shim integrity ✓, legacy db_tables ✓, ContentType relabel ✓, model app_label ✓, EntityRegistry ✓
Подальші R-етапи додані у backlog (R-2A CargoCategory enum у thin core, R-2B shipcore_cargo_ops, R-3 shipcore_containers, R-4 appContainerHub preset + D-5 рішення, R-5 integration addons) — всі з explicit triggers.
2026-05-21 — Phase 4 backend renames + Phase 5 shipcore_sea + frontend skeleton (марафон-сесія)¶
Закрито велику кількість зв'язаних SHC-таск у одній сесії — масштабний backend-track від внутрішніх блокерів через Phase 4 rename pattern до повного Phase 5 backend та frontend skeleton. Загалом 23 commits.
Pre-Phase-4 cleanup track:
- Cash & Bank S6-S8 ✅ — S6a (UA print forms КО-1/2/0410001), S6b (Cash Book + Cash Orders Register reports), S7 (UA cash limit НБУ-148 + value_date FX), S8 (cashflow forecast widget + PaymentCalendar drill-down). S4 deferred як окремий sprint.
- Fleet ↔ Essentials data bridge ✅ — FixedAsset bridge (fc8dbcd), driver salary models existing + CPM read coupled з Phase 7, fuel inventory bridge (af7846c). ShipCore Phase 4 block lifted.
- OpeningBalance тестування + ADR/lesson learned ✅ — O1-O5 (commits 7478ca8+ef5139b). 4/5 handlers verified live проти Daphne; FuelHandler verified direct Python shell. Lesson learned react-router-relative-navigate-trap.md + ADR adr-2026-05-19-opening-balance-handler-registry.md.
- Aging Report ✅ — AgingReport через Report Framework, неоплачені Invoice per client × 4 buckets за days_overdue. AI bundle domains/essentials/invoices/aging-report.md.
Phase 4 backend renames track (8 days, 9 commits, CLOSED):
- Day-1 a92c934 — audit fleet/containerhub/transport + порожній backend/shipcore/ skeleton + ADR adr-2026-05-21-phase-4-refactor-strategy.md з 4-секційною стратегією.
- Day-2 9c0a52b — shipcore.TransportUnit + Location abstracts + Carrier + CarrierIdentifier + CarrierEquipment (3 EntityRegistry keys).
- Day-3 22d9065 — shipcore.MarketCorridor + ShipmentEvent federation primitive з UUID + JSONB payload + idempotency constraint.
- Day-4 19dfeb0 — перший app rename transport/ → shipcore_rail/ (3 моделі). Pattern validated: SeparateDatabaseAndState + ContentType relabel, DB tables stable.
- Day-5 ad889e4 — DeprecationHeaderMiddleware kernel primitive, settings-driven mapping (LEGACY_URL_REDIRECTS).
- Day-6 718e5fd — другий app rename containerhub/ → shipcore_terminal/ (24 моделі). 382 containers + 4 terminals survived.
- Pattern artifact c24267f — AI bundle app-rename-pattern-phase4.md (8 steps + 8 gotchas + G-9 додано після Day-7).
- Day-7 c9e323f — найбільший rename fleet/ → shipcore_auto/ (46 model classes). 9 migrations, cross-FK reach у 6 backend apps + 1 plugin. 41 vehicles + 75 drivers + 60 waybills + 12 fuel types survived. Discovered G-9 gotcha (grep всю backend/, не лише очікуваних callers).
- Day-8 2eb7e42 — четвертий і останній rename logistic/ (з pip-package plugins/eswf-logistic/) → backend/shipcore_forwarder/ (3 моделі). 24 ContainerTypes survived.
- Closure 46d290b — Day-7/8 state sync + G-9 gotcha додано в pattern artifact.
Підсумок Phase 4: 76 моделей × 4 apps + 7 thin-core models created, ZERO data loss, всі cross-app FK redirected, всі legacy URLs з X-Deprecated-Path header через DeprecationHeaderMiddleware. Validated pattern документований для майбутніх renames.
Phase 5 shipcore_sea track (autonomous portion CLOSED):
- Day-1 922e9e4 — shipcore.Vessel + shipcore.Port concrete master-data в thin core (cross-mode primitives: sea / river / terminal) + порожній backend/shipcore_sea/ skeleton + ADR adr-2026-05-21-phase-5-shipcore-sea.md. D-1: cross-mode concrete primitives stay in thin core.
- Day-2 766e151 — shipcore_sea.Voyage (TenantAwareModel, scheduling lifecycle) + VesselSchedule (11-source enum, idempotency через external_id) + Vessel/Port EntityRegistry catch-up.
- Day-3 72b6582 — OceanBooking (TransactionModel + sea-specific status workflow) + OceanBookingEquipment (DCSA VGM 1.0 block — replaces legacy VERMAS EDIFACT).
- Day-4 94c5d1c — BillOfLading paper variant з master/house parent_master FK + 3 legal varianti (original/express_release/sea_waybill) + eBL fields reserved для Phase 8.
- Day-5 c815044 — CarrierAdapter Protocol + FakeCarrierAdapter synthetic + MaerskSpotAdapter scaffold з NotConfigured guard. 4 services (booking_flow / bl_workflow / vessel_schedule_sync / demurrage_alerts). End-to-end smoke test через Fake passes.
- Day-8 4ac26f1 — demurrage_alerts.check_demurrage_thresholds wired real проти shipcore_terminal.DemurrageCalculation + manage.py check_demurrage command.
- Phase 5 closure cd024a4 — docs sync + AI bundle carrier-adapter-protocol-pattern.md (8 steps + 8 gotchas + 3 anti-patterns + reusability table).
- Day-6 d228e84 — DCSA pydantic codegen infrastructure (tool decision datamodel-code-generator, регенерація script).
- Day-6b f9eaff2 — real DCSA codegen executed (3/5 families: BKG 2.0.5 + EBL 3.0.4 + CS 1.0.3 → ~16k LOC pydantic v2). TNT external $ref recursion documented as workaround. VGM YAML not yet on GitHub (SwaggerHub-only, Nov 2025 release).
Frontend Phase 5 Day-9 track:
- Day-9 9180d63 — frontend/dop/sea/ skeleton + seaSection config (4 groups: Master Data / Scheduling / Documents / Processes) + 3 placeholder dashboards (VesselScheduleBoard / OceanBookingDashboard / DemurrageDashboard) + wire у sections.ts + applications.ts (appShipcoreSea).
- Shop Product fix 6b9d436 — seed_shop.py додано appShipcoreSea Product entry з installed_by_default=True + plugin_key='shipcore_sea'. Без цього frontend installedAppCodes не містив appShipcoreSea і sea section silently dropped. 3-piece wiring gotcha: Section + applications.ts + shop Product — варто додати до sidebar-sections-applications.md checklist.
Підсумок Phase 5 backend: 7 моделей у sea + 2 у thin core, CarrierAdapter Protocol + Fake adapter + 4 services + 1 management command + 3 DCSA pydantic families (16k LOC) + frontend skeleton + sidebar wiring. End-to-end booking lifecycle працює без external creds через FakeCarrierAdapter.
Залишається у Phase 5 (external blockers): Day-7 MaerskSpotAdapter real internals (Maersk Developer Portal creds, ~2 days self-service), TNT codegen (swagger-cli bundle pre-resolve), VGM codegen (SwaggerHub manual export), real frontend forms / Kanban / Gantt (потребує pilot signal або UX prompts). Phase 8 eBL hubs + EU carriers — далекий 16-тижневий sprint.
2026-05-17 — Client production-ready audit (S1-S12 single-session)¶
Закрито пункт з todo.md. Аудит контрагентів — найвищий FK fan-out серед master-data (43 FK у 12 модулях), planning chain 8 спринтів за зразком Item-audit (4366395), експандований до S12 + S3.5 hot-patch + S4 micro-fix. S1 — 2 ADR (client-roles-via-domain-fk + client-contract-attribute-split) + canonical clients/README.md без Contract-leak'ів. S2 — Client.manager лишається як Account Manager (distinct from Contract.sales_rep); reframe help_text + UI label. S3 — ClientSerializer denormalized FK names (business_region_name/_ua, default_contract_name/_ua, manager_name, etc.), validator default_contract.client == self, ClientViewSet + EntityRegistry filterset з 8 полів, migration 0055_alter_client_manager, ADR client-fk-asymmetry-trap. S3.5 — hot-patch concurrency control: expected_updated_at 409 Conflict тепер працює і для MasterDataModel; usMasterDataForm шле updated_at у payload. S4 — re-scoped + closed: 12+ типів документів свідомо НЕ фільтрують клієнтів по is_buyer/is_supplier; README invariant #3 переписано як «прапорці інформаційні». S5 — 4 підспринти: validators core/validators/ua.py (EDRPOU+INN checksum, VAT-ID country prefix) + core/utils/normalize.py (phone E.164 UA, IBAN), structured address fields (country_code/postal_code/city, migration 0058), partial unique constraints (tenant, edrpou) + (tenant, inn) (migration 0059), seed_clients_bulk management command. S6 — ClientList.tsx переписано: status icon-only, logo column 28×28, country flag column, inline IconLock indicator, 12-field modal filter, Active-only/Archived toolbar pill; useMasterDataForm handleStaleConflict + detectDuplicateMessage. S7 — Usage tab (AR/AP balance per currency через PartyLedger, group_rollup для холдингів), /clients/bulk-update/ endpoint з 7-field whitelist, archive metadata (archived_at/archived_by/archive_reason, migration 0060) + services/client_archive.py. S9 — Client hierarchy: self-FK parent_client + denorm group_root + cycle validator (Salesforce-style, 5-of-6 surveyed ERPs), iter_group() + group_member_ids(), migration 0057, ClientHierarchyFields + Hierarchy tab у формі. S10 — SAP-lite block mechanism: 5 нових полів (block_status enum, block_reason, blocked_at, blocked_by, blocked_until, migration 0056), essentials/services/client_block.py, 5 validators у post_document actions, ClientBlockModal + header badge. S11 — 3 AI tools у eswf_chat/tools.py: check_client_duplicates (multi-signal scoring, cross-language matching через translations.name.ua), analyze_payment_behaviour, recommend_block_status (STRICT human-gate). Deferred у backlog (need external infra): sanctions screening (UA NSDC + EU + OFAC), VIES VAT lookup, AI button form integration, S12 (zugferd_invoice plugin MVP), S8 (help articles + final AI bundle).
2026-05-16 — Класифікація номенклатури — research + ADR (Hybrid C+BI)¶
Закрито decision-таску з todo.md. Обрано Hybrid C+BI: nature enum (4 коди: good/service/material/manufactured) + ItemCategory FK (з category_set forward-thinking полем для майбутніх Sales/Tax/Purchasing паралельних hierarchies + nullable parent для опційної ієрархії) + ItemTag M2M (cross-cut) + is_kit boolean для retail bundles. Західний ERP context (SAP/Oracle/D365/Odoo/NetSuite) і components/BOM проблема («комплектуючі в картці пшениці» — conditional render за nature + is_kit) досліджено. Денормалізація на лінії документів + 3 PostgreSQL VIEWs (vw_item_dim/vw_sales_fact/vw_purchase_fact) для PowerBI/Metabase — Slowly Changing Dimension Type 2 промисловий DWH-стандарт. Migration mapping 7 старих item_type → 4 nature + 7 stub categories. Деталі — planning/item-classification-2026-05-16.md. Phasing S1-S5 (~5 днів) — реалізаційна сесія попереду.
2026-05-16 — Уніфікація універсального списку і форми довідника під патерн кастомних¶
Закрито пункт з todo.md. MasterDataList.tsx переписано як тонку metadata-driven обгортку над MasterDataCustomList (columns/filterFields/renderCell генеруються з useEntityMetadata); довідник без власного компонента тепер з коробки має status icon-col, Settings modal (sort chain / filters / hidden cols / format rules / group-by / page-size), infinite scroll з virtualizer'ом, scroll & page-size persistence, multi-select, context menu, shortcuts. MasterDataForm.tsx переписано як тонку обгортку над MasterDataFormShell + useMasterDataForm (header Code/Name/is_active + Tabs Main/[subtables]/Additional, save flow через pendingOps, disabled-not-loading toolbar). Дрібні довідники (Unit, Currency, …) лишаються single-pane через нову опцію MasterDataFormShell.noAdditional — flat fallback коли subtableTabs=[]. Розширено shared toolbar'и (MasterDataListToolbar.noNew/noDelete, MasterDataFormToolbar.noSave) для read-only journal-mode. Subtables рендеряться як read-only Mantine Table (повна міграція на FormSubtableList з inline edit — окрема задача). ADR: ai/architecture/adr-2026-05-16-universal-master-data-unification.md.
2026-05-16 — Видалено рудиментарне поле Item.price¶
Закрито пункт з todo.md. Поле прибрано з моделі/admin (ItemAdmin.list_display/fieldset) — серіалізатор '__all__' оновлюється автоматично; 2 нові міграції 0050_backfill_item_price_to_itemprice (data: Item.price > 0 → ItemPrice з default PriceType per tenant) + 0051_remove_item_price (RemoveField). Усі read-callers (pricing.py PriceLookupView/MassPriceUpdateView, production/serializers.py BOMLineSerializer.input_item_price, production/reports/api.py _effective_unit_price, eswf_chat/tools.py _check_item_duplicates) переписані на essentials.services.item_pricing.get_default_item_price(item) (default PriceType → first → 0). 8 сидерів, що створювали Item з price=…, тепер пишуть ItemPrice через новий helper core.seeders._item_pricing.seed_item_default_price; 4 сидери, що читали item.price, перейшли на get_item_default_price(item). AI bundle оновлено — docs/ai/domains/essentials/master-data/items/entities.md.
2026-05-16 — GateCheckpoint — explicit readOnlyJournal: true prop¶
Закрито пункт з todo.md. Додано opt-in readOnlyJournal?: boolean на SectionItem (config/types.ts) і дзеркально на MasterDataListProps. SectionPage пробрасує prop у MasterDataPage, той — у MasterDataList + MasterDataForm. У MasterDataList.tsx Create/Edit/Delete кнопки рендеряться лише !readOnlyJournal; у MasterDataForm.tsx так само ховаються Save / Save&Close. На GateCheckpoint (config/gatehouse.ts) виставлено readOnlyJournal: true. TS-перевірка: 0 нових помилок (лишилися pre-existing у lazy() declarations).
2026-05-16 — VisitPlan fallback для авто без матчу шаблона¶
Закрито пункт з todo.md. Додано canonical default_minimal шаблон у seed_gatehouse.VISIT_TEMPLATES_DATA з applies_to='any' і 2-step plan (entry+exit, обидва mandatory). pick_visit_template тепер гарантовано повертає шаблон для будь-якого авто — service-logic не змінено. Verification: seed_gatehouse створив 4-й шаблон (default_minimal/any); t.steps.values_list('order','kind__system_code','mandatory') повертає [(1,'entry',True),(2,'exit',True)]. ADR — ai/architecture/adr-2026-05-16-visit-plan-fallback.md з 3 варіантами (tenant setting / hardcoded fallback / seed canonical) → обрано (c). is_system protection deferred — задокументовано в ADR як follow-up.
2026-05-16 — Dashboard widget «Стан звірки проводок»¶
Закрито пункт з todo.md. Створено essentials.dashboards.reconciliation_status.ReconciliationStatusCueWidget (cue type) — сума issues_count через L1-L3 last-run + meta.overall_status. Зареєстровано у WidgetRegistry через essentials/apps.py. У DashboardPage.tsx Context zone додано tile span=6 поряд із Cash/Bank balance. Кольори нейтральні (WidgetCue рендерить color="gray" Badge + IconAlertTriangle); link /accounting/accountingMasterData/reconciliation. Додатково wired up generic click-through на cue tiles у DashboardCanvas.WidgetRenderer (onClick={() => navigate(data.link)}) — користь для всіх cue widgets з link. ADR — ai/architecture/adr-2026-05-16-reconciliation-widget.md (3 placement-варіанти → tile у Context, обґрунтовано).
2026-05-16 — Print forms rollout по всіх документах, де має сенс¶
Закрито пункт з todo.md. Створено 25 нових PrintContract'ів за стратегією one-doc-type-per-commit (з bundling де docs ділять size + signature pattern): Essentials (14) — Invoice + GoodsShipment (раніше) + PurchaseInvoice, GoodsReceipt, GoodsWriteoff, StockTransfer, DocumentOperation, InventoryCount, Manufacturing, SalesOrder, PurchaseOrder, IncomingPayment, OutgoingPayment, CashTransfer, JournalEntry, AdditionalExpenseAllocation. CRM (1) — Quotation. Gatehouse (4) — Queue/VehiclePass/VisitorPass (раніше) + WeighingTicket. Production (1) — WorkOrder. Fleet (6) — Waybill (раніше) + TransportInvoice, TransportOrder, MaintenanceRecord, VehicleRefueling, FuelDrain. HRM (3) — PayrollSlip (раніше) + Timesheet, LeaveRequest. Sales-mobile (1) — SalesReturn. Essentials_quality (1) — QualityInspection. ContainerHub (5) — GateTransaction, ContainerBooking, DemurrageCalculation, TrainArrival, RailwayWaybill. Skip-список (документовано single-line ADR-обґрунтуваннями у session log): BankStatement / CurrencyRevaluation / PlannedPayment / OpeningBalance / DriverSalaryAccrual / InitialBalances / GateEvent (essentials/fleet) + ContainerMovement / YardInventory (containerhub) — reconciliation-only або internal-only без паперового виходу.
2026-05-16 — EasyOCR pre-warm на startup¶
Закрито пункт з todo.md. У gatehouse/apps.py::GatehouseConfig.ready() додано daemon-thread що викликає _get_reader() коли sys.argv[1] in {'runserver', 'daphne'} (або argv[0] закінчується на 'daphne'). Pre-warm exception лише логується через gatehouse.ocr logger — не валить server boot. manage.py check/migrate/seed/test пропускає pre-warm через argv-check. Лог EasyOCR ready in N.Ns (pre-warm) пише timing для verification. ADR — ai/architecture/adr-2026-05-16-easyocr-prewarm.md з 4 варіантами (thread (a) / Celery / explicit endpoint / cron-reboot) → обрано (а).
2026-05-16 — Audit log для BusinessOperationTemplate¶
Закрито пункт з todo.md. У BusinessOperationTemplateViewSet (essentials/views/master_data.py:388) перевизначено perform_create / perform_update / perform_destroy. Create логує full snapshot. Update робить snapshot→save→diff і пише AuditLog лише коли є changes (порожній diff не створює запис). Destroy логує snapshot перед видаленням з entity_repr knapshot'ом. Всі через існуючий core.services.audit (log / snapshot / diff / entity_type_label). Acceptance: python manage.py check clean; AuditLog records видимі через існуючий /api/v1/audit-logs/?entity_type=essentials.businessoperationtemplate endpoint (universal AuditLog ViewSet). Тести не додано — pytest у dev env недоступний; smoke-перевірка через manual API call deferred до окремої сесії з тестами.
2026-05-16 — Валідація GateEquipment.connection_config за schema драйвера¶
Закрито пункт з todo.md. У GateEquipmentSerializer додано validate() + helper _validate_connection_config(driver, config): резолвить driver_by_code(driver), читає config_schema, перевіряє required-поля та типи (string/number/boolean). На PATCH fallback на existing instance value щоб не вимагати full re-submit. Edge cases: unknown driver → {'driver': 'Unknown driver code: ...'}; non-dict payload → {'_': 'connection_config must be a JSON object.'}; bool-as-number (Python True isinstance int) явно відхилено. Smoke test 9 кейсів усі правильні (file_scale missing required / wrong type / bool-as-number / serial port required / ANPR empty schema / non-dict / unknown driver / ok cases). Тести в test_gatehouse_*.py не додано — pytest у dev env недоступний; manual smoke verified.
2026-05-16 — Bundle docs для Gatehouse — api-contract.md, test-scenarios.md, ui-spec.md¶
Закрито пункт з todo.md. 3 нові файли у docs/ai/domains/gatehouse/ (бандл тепер повний: README + entities + business-rules + api-contract + test-scenarios + ui-spec — все за структурою референсу essentials/invoices). api-contract.md — OpenAPI 3.0.3 snippets для всіх endpoint'ів /api/v1/gatehouse/* (master data ViewSets таблиця + повний YAML для gate-events / weighing-tickets / checkpoints/scan / equipment-events / ocr/license-plate / vehicle-trace / queue-tickets / visitor-passes / external-events) + WebSocket channels matrix + error envelope. test-scenarios.md — Given/When/Then у 9 group'ах (ANPR accept/reject + fallback / weighing correlator 30s+24h / checkpoint idempotency / exit-block + force_exit / personnel turnstile / external SCUD idempotent replay + ContainerHub bridge / OCR + pre-warm / GateEquipment config validation / multi-tenancy isolation). ui-spec.md — 8 operator screens (GatehouseDashboard, GateOperator з 4 tabs, CheckpointScanner kiosk, WeighbridgeOperator 3-pane з live readings 32pt, WeighingTicketForm Stepper, VehicleTrace timeline, WebcamANPRTester, Settings drawer).
2026-05-16 — Спільний хук useDocumentSubtableActions(doc, type, defaults)¶
Закрито пункт з todo.md. Створено frontend/erp/src/components/shared/forms/useDocumentSubtableActions.ts — повертає { autoEditRowId, setAutoEditRowId, handleAdd, handleDelete, handleCellChange }. defaults приймає або об'єкт, або factory-функцію (recomputed at each Add — потрібно для sequence: rows.length + 1 defaults). Мігровано 9 з 14 файлів, що використовують стандартний (type, ...) shape: Manufacturing, InitialBalances, AdditionalExpense (2 hook calls — expenses + targets окремо, кожен зі своїм autoEditRowId — UX покращення), Invoice / PurchaseInvoice / GoodsReceipt / GoodsShipment / SalesOrder / PurchaseOrder (partial migrate — hook для autoEditRowId+handleAdd+handleDelete, custom handleCellChange зберігся бо має pricing-lookup logic). Не мігровано — Vehicle / Contract / BOM (legacy pendingRows + onPendingChange pattern, потребує окремої міграції на useDocumentForm flow), Waybill (multi-type — 1 компонент керує 4 sub-tables одним shared autoEditRowId, потребує multi-type variant хука), Item (різний signature onAddRow(data) без type). Acceptance: tsc --noEmit 0 нових помилок у мігрованих файлах; 1 pre-existing TS6133 у PurchaseInvoiceSubtables (taxRates unused prop — не моя зміна).
2026-05-16 — Виправлення невідповідності сум у виробничих 4-leg проводках¶
Закрито пункт з todo.md. У _post_atomic (essentials/services/manufacturing_service.py) — group.amount тепер обчислюється як фактична сума дебетів: sum_input_acc + sum_output_acc для 4-leg WIP-routing (debits land twice — Dt WIP per input + Dt Output per output), або sum_output_acc для 2-leg атомного. Це бере значення з per_input_acc[*].cost_acc + per_output_acc[*].cost_acc (accounting-currency snapshot) — підтримує і випадки коли accounting != functional (FX-rate mismatch був ще одним джерелом drift'у на seed_demo Manufacturing#3, де total_actual_cost=43200 але cost_acc=43533.33). Issue/receive modes не зачеплено (single-leg, без drift). Acceptance: python manage.py verify_postings --tenant 6 --level 1 показує status: ok / issues: 0 (до фіксу — 1 amount_mismatch на Manufacturing#3 з delta=-333.33; після re-post — clean). Існуючі вже-проведені документи потребують unpost+post для застосування фіксу.
2026-05-16 — Багатомовні назви довідників — MasterDataModel.translations JSONB (S1 — Item)¶
Закрито пункт з todo.md. S1 ефективно already-done у попередніх сесіях — інфраструктура повна: (a) MasterDataModel.translations JSONField + save() sync легасі-колонок (backend/core/models/base.py); (b) essentials/migrations/0042_bank_translations_* додав колонку для Item + інших 19 моделей essentials; (c) core.serializers.TranslationsAwareSerializerMixin синхронізує writes через translations → legacy у create/update; ItemSerializer наслідує його; (d) frontend/erp/src/components/shared/LocalizedTextField.tsx + useLocalizedFieldState hook з AI translate через core.services.translation; (e) MasterDataFormShell рендерить name через LocalizedTextField з limitLangs з useTenantLanguages; ItemForm через shell вже працює; (f) Списки swap'ять через існуючий localizedFkName helper + localized_fk_pair у серіалізаторах. Доданий артефакт S1: essentials/migrations/0052_backfill_item_translations.py — idempotent insurance migration що backfill'ить translations.name = {"en":name,"ua":name_ua} для legacy Item-рядків що оминули save() sync (raw SQL inserts / bulk_create / direct ORM updates). На seed_demo 0 рядків змінено (всі 54 Items уже мають translations через save() sync). S2 (rollout на решту MasterData models) — окрема задача, всі необхідні моделі вже мають translations field з 0042 + analogous migrations у crm/containerhub/budgeting/consolidation; S2 потребує тільки frontend wiring через MasterDataFormShell де є кастомний form компонент.
2026-05-16 — Form field controls — bordered slot rollout (Phase 2: HRM / Fleet / Gatehouse / Production / Accounting / Currency / shared)¶
Закрито пункт з todo.md. Continuation of Essentials migration. 4 паралельних sub-агенти мігрували 26 файлів — HRM (7), Fleet (7), Gatehouse (5), Production (5), Accounting/Ledger (1), shared/DocumentFormShell (1) — разом 77 Selects + 18 NumberInputs під bordered-slot pattern. Пропущено за виключеннями ADR: HRM/LeaveType + HRM/PayrollTaxConfig (NumberInput hideControls без custom rightSection), Currency (TextInput+Switch only), WaybillSubtables in-cell editors і всі renderEdit callback'и у FormSubtableList. Перевірка tsc --noEmit -p frontend/erp/tsconfig.app.json — 0 нових помилок у мігрованих файлах (117 загальних pre-existing помилок у lazyWithPreload config-tree + SectionPage + ContainerHub/Logistic). ADR оновлено: ai/architecture/form-field-controls.md § Roll-out — статус «complete».
Залишковий tech-debt (кандидати на майбутні задачі): (а) <Autocomplete> у GateEventFields (vehicle/driver/partner) лишається на legacy RefSelectRight — рецепт ADR не покриває Autocomplete; окрема таска якщо знадобиться повне охоплення; (б) readOnly TextInputs у WaybillFields/InitialBalancesFields/WorkOrder/BOM зберігають локальний refOpenIcon helper (TextInput out-of-scope для bordered-slot); (в) перевірити в Playwright dev-сесії що Author Select у MasterDataFormShell, Status Select у DocumentFormShell і фуел-секція Waybill візуально рівні Essentials референсам.
2026-05-15 — Batch-міграція підпорядкованих таблиць на FormSubtableList¶
Закрито пункт з todo.md одним пакетом. Усі 13 legacy-консумерів SubtablePanel мігровано на FormSubtableList. Legacy SubtablePanel.tsx + SubtableToolbar.tsx видалено. Інфраструктурні додавання: FormSubtableList.onPasteRows (Ctrl+V), shared InlineItemPicker (debounced server search для FK item, заміна 6 inline-дублікатів), localized_fk_pair на 3 серіалізаторах (Vehicle/BOM/ContractSpec), BOM._subtables додано. Модалки-на-dblclick прибрано всюди (9 форм) — всі поля стали inline-колонками. Фікс під час browser-тесту: useDocumentForm.getRows додано snake_case fallback (fuelRecords → fuel_records), handleAddRow повертає негативний temp id, кожен document subtable wrapper отримав autoEditRowId через returned id (auto-edit першої комірки після +Add, як в Item.prices). Деталі — ai/architecture/form-subtable-pattern.md § «Batch migration completed».
Залишковий tech-debt (кандидати на майбутні задачі): (а) localized_fk_pair для line-FK у документах (Invoice/PurchaseInvoice/Order/GoodsReceipt/GoodsShipment line.item/unit/tax_rate/warehouse, Manufacturing.inputs/outputs.item, Waybill.tasks.order/point_from/point_to) — українська назва FK у рядках не перемикається без <fk>_name_ua; (б) useDocumentSubtableActions(doc, type, defaults) hook — витягнути дублюваний 5-рядковий блок з 10 document subtable wrappers (~50 рядків hook + 10×(-15) у wrappers, наступний зсув поведінки +Add редагується в одному місці).
2026-05-15 — Адаптувати ширину першої колонки в кастомних списках — icon-fit¶
Закрито пункт з todo.md. Винесено три токени у frontend/erp/src/components/shared/toolbar/tokens.ts: STATUS_COL_WIDTH = 28 (зменшено з 36; icon-fit для 16-18px іконки), STATUS_CELL_SX (padding: 0.3em 0, textAlign: center), STATUS_HEAD_SX (no sort cursor). MasterDataCustomList re-експортує STATUS_COL_WIDTH для back-compat. Кастомні master-data списки (Vehicle, ChartOfAccounts, BusinessOperations) — оновлено status cell/header на нові токени, прибрано локальні { ...cellSx, textAlign: 'center', padding: '0.25em' }. ListTable.tsx отримав semantic iconOnly?: boolean на ListColumn (зробив w optional) — застосовує STATUS_HEAD_SX/CELL_SX і ширину з токена. 10 document lists через ListTable (Invoice, GoodsShipment, IncomingPayment, OutgoingPayment, PlannedPayment, CashTransfer, Timesheet, PayrollPeriod, PayrollSlip, LeaveRequest) — заміна w: 40 на iconOnly: true. TransactionList (legacy) — w={32} / width: 32 замінено на STATUS_COL_WIDTH + STATUS_HEAD_SX/STATUS_CELL_SX.
Залишковий tech-debt (кандидат на майбутню задачу): 7 «document lists» з власним table-render (SalesOrder, PurchaseOrder, PurchaseInvoice, Manufacturing, AdditionalExpense, WorkOrder, GoodsReceipt) використовують type: 'icon' без ListTable. У них tableLayout: 'auto' природно дає icon-fit без явної ширини. Waybill та InitialBalances мають локальний DocStateIcon без ListColumn (інлайн в одній з колонок). Уніфікація — окрема задача при наступному рефакторингу Sales/Purchase lists.
2026-05-15 — Архітектура великих довідників — пагінація vs динамічна підгрузка¶
Закрито пункт з todo.md. Аудит показав, що infinite scroll + client-side virtualization (@tanstack/react-virtual + useInfiniteQuery) уже стандарт у MasterDataCustomList; backend пагінація через FlexiblePageNumberPagination (default 50, max 1000), filtering/sorting/search — server-side. Закрито пакетом:
MasterDataListSettingsmodal (новий shared компонент) — відкривається з overflow-меню (⋯ → Settings). 6 vertical Tabs (Загальні / Сортування / Фільтри / Колонки / Оформлення / Групування). Active state — лише жирним шрифтом без pill-bg; counts у tabrightSectionяк dimmed-цифри. Sort/filter rows — flat без card chrome; інпути 34px (SETTINGS_INPUT_STYLE) замість 26px toolbar-варіанту.- Sort state refactor:
(sortField, sortDir)→SortStep[]chain. Primary = chain[0] (відображається стрілкою у заголовку колонки). Click на заголовок керує тільки chain[0] (toggle / replace). Secondary levels — виключно через діалог. Backend wire format: comma-joinedordering=field1,-field2через глобальнийOrderingFilter. - Filter chain:
FilterStep[]spread як raw query params на API endpoint. DjangoFilterBackend (глобальний) приймає те що вfilterset_fieldsper-viewset, решту тихо ігнорує. Value-пікер типізований черезFilterFieldDefdiscriminated union (text | number | boolean | date | enum | fk). Reference wiring уItemList(unit як FK на essentials/units, item_type як enum, is_active як boolean). - SessionStorage keys per-list:
list-page-size:{basePath},list-sort:{basePath}(з back-compat читанням legacysort:{basePath}shape),list-filters:{basePath},list-scroll:{basePath}(throttle 250ms, best-effort restore через requestAnimationFrame). - Column visibility:
hiddenColumns: string[]per-list sessionStorage;statusnon-hideable; keyboard nav skips прихованих. - Conditional formatting:
FormatRule[](key, value, color) — first matching rule paints row text colour; 7-кольорова палітра через Mantine CSS vars;FORMAT_COLOR_PALETTE/FORMAT_COLOR_CSSекспортовано як single source of truth. - Grouping (single field): prepend groupBy → API
ordering; client walks consecutive runs →DisplayRowз group headers (colSpan, chevron, count). Експандери persisted якSet<string>колапснутих ключів (нові групи дефолтно розгорнуті).
Рішення зафіксовано в ai/architecture/large-list-pagination.md. TransactionList свідомо лишається на класичній page-numbered пагінації.
Залишковий tech-debt (кандидати на майбутні задачі): (а) бенчмарк на 1000+ Items / 10k Transactions не виміряний; (б) per-entity custom lists (Vehicle tree, ChartOfAccounts tree) реалізують infinite-scroll самостійно — Settings modal + page-size persistence + scroll persistence у них треба продублювати при наступному дотику; (в) refetchInterval: 300_000 для infinite query re-fetch'ить ВСІ завантажені сторінки — оптимізація «refetch only first page» deferred; (г) cursor pagination на backend (offset-less) не реалізована; (д) filter operators MVP — тільки equality; розширення на __icontains / __in / range / date-range приходить разом з налаштуванням per-entity filterset_fields; (е) Settings modal зараз дозволяє sort/filter тільки по columns що передані у список — поля моделі що не у списку (наприклад created_at для backoffice сортування) не доступні поки не додамо extraSortFields prop.
2026-05-09 — Dashboard Framework shipped (Widget Framework + Multi-persona dashboards + PersonaRegistry)¶
Закрито пункт з todo.md (Прохідна tech debt block). Original проблема «Dashboard → tile-launchpad SAP Fiori-style» розрослася в повноцінний Widget Framework + Multi-persona dashboards + PersonaRegistry. Замість простого tile launchpad — кожна роль має свій home (Bookkeeper / Sales / Inventory / Executive), defaultDashboard pin + auto-redirect at login, backend WidgetRegistry як 4-й framework поряд з Reports/Print/Help. Section pages збережені (виявилось — це і є Fiori App-tile pattern). Деталі: planning/dashboard-framework-2026-05-09.md, архітектура: ai/architecture/dashboard-framework.md.
2026-05-10 — Sales Order Architecture Phase 1: invoice_first vs order_based¶
Закрито пункт з todo.md, поставлений 2026-05-10. Специфікація: planning/sales-order-architecture-2026-05-10.md. SalesOrder перенесено з backend/crm/models/sales_order.py у backend/essentials/models/sales_order.py з усіма залежностями (FK у Invoice, GoodsShipment, EntityRegistry, ViewSet, Serializer, seeders). Введено два workflows через EssentialsModuleSettings.enable_sales_orders / enable_purchase_orders. Lifecycle draft → confirmed → in_progress → fulfilled → cancelled з резервацією запасу через BatchReservationService на confirm(). Posting у трекінг-регістри (CustomerOrderLedger + PlannedPaymentJournal income-side) переноситься з Invoice на SalesOrder — Invoice припиняє торкатися цих регістрів повністю (у invoice_first mode Payment Calendar income-side порожній, передбачена поведінка).
Commits. 8b1b87a (backend models + serializers + ViewSet + signals + 5 migrations) → b22d43b (frontend custom Form + List UI — F2 з Phase 1 backlog) → db96387 (session retrospective AI bundle — 6 architectural lessons).
Dashboard widgets. open-sales-orders / expected-revenue / fulfillment-rate додані. Cash sale + retail-shop + fiscal-ua phases (deferred 2-4 у файлі специфікації) — у backlog.
ShipCore impact. Знімає 1 з 4 internal блокерів ShipCore Phase 4. shipcore_forwarder.Booking lifecycle (Phase 5 Inquiry→Quote→Booking workflow) тепер має чим дзеркалити SalesOrder pattern.
2026-05-09 — RegisterRecordsPage: уніфікований toolbar нагорі (composer pattern)¶
Закрито пункт з todo.md, поставлений 2026-04-30. EntityWorkspace.renderToolbar тепер несе єдиний ProcessToolbar (period popover, refresh, search в mode='all', paired-mode toggle як extras коли activeTab його підтримує, Close/ESC). State search/dateFrom/dateTo піднято до RegisterRecordsPage, pairedMode keyed by registerCode (з localStorage hydration), RegisterTable став контрольованим — більше не рендерить власний toolbar. Композиція "single source of toolbar" вирівняна з DocumentForm/MasterDataForm/TransactionForm. Фікс автоматично нормалізує всі 12+ register-сторінок Essentials/Fleet через JOURNAL/LEDGER → mode='all' redirect у SectionPage.
2026-05-10 — Розщеплення Invoice.status на ортогональні виміри¶
Закрито пункт з todo.md, поставлений 2026-05-09. Замість одного 9-значного enum'а (draft / sent / partially_paid / paid / partially_shipped / shipped / closed / overdue / cancelled) Invoice тепер має чотири незалежні поля: lifecycle_status (draft / open / closed / cancelled — managed користувачем + автомат на post/auto-close), payment_status (unpaid / partially_paid / paid / overpaid — derived з paid_amount / total_gross), fulfillment_status (not_shipped / partially_shipped / fully_shipped — derived з shipped_amount / total_gross), is_overdue (boolean — derived з payment_due_date < today AND payment_status != paid AND lifecycle ∉ cancelled,draft). Старе поле status втратило override у Invoice і повернулося до базового 3-choice варіанту з TransactionModel (не використовується). Це закриває anti-pattern, де legacy recalculate_status обирав один з взаємовиключних partially_paid / partially_shipped коли документ був у обох — диспетчер не бачив, що ще треба відвантажити, бухгалтер не бачив решту до сплати. Аналог уже існував у PurchaseInvoice (state / matching_status / payment_status / payment_block) — тепер однотипний паттерн і з боку customer.
Backend. backend/essentials/models/invoice.py — нові enum-choices, recalculate_status розщеплено на чотири in-memory методи (recalculate_payment_status, recalculate_fulfillment_status, recalculate_lifecycle_status, recalculate_overdue) + оркестратор зберігає одним UPDATE; auto-close (open + paid + fully_shipped → closed) у lifecycle-методі; cancelled — terminal frozen. serializers/transactions.py — derived поля у read_only_fields. views/transactions.py — kanban групує за lifecycle_status, move приймає lifecycle_status (legacy status payload-ключ approve compat shim), post_document flips draft→open, unpost_document flips open→draft зберігаючи closed/cancelled, filterset_fields розширено на чотири нові виміри. Дашборд-віджети invoice_cues.py (unpaid + overdue), invoice_kpi.py (4-tile total/sent/paid/overdue), top_debtors.py, executive_overview.py, todays_actions.py — переписано на нові виміри (is_overdue=True, lifecycle='open' AND payment NOT IN paid,overpaid). Сидери (core/seeders/essentials.py + _demo_case_base.py + demo_case_d.py) переписано зі сценарій-лейблами; eswf_chat/tools.py і sales_mobile_api/views.py повертають всі чотири виміри.
Migrations 0033/0034/0035. Тристоронній паттерн (schema-add → data-backfill → schema-cleanup): 0033_split_invoice_status_add_dimensions додає 4 нові колонки (defaults безпечні), 0034_backfill_invoice_status_dimensions (RunPython forwards+backwards) обчислює lifecycle / payment / fulfillment / is_overdue з legacy enum + paid_amount / shipped_amount + payment_due_date, 0035_drop_invoice_legacy_status повертає inherited TransactionModel.status до базових 3 choices. Перевірено на dev DB: 80 Invoice мігровано, distribution lifecycle: 3 draft / 77 open, payment: 53 paid / 14 partially_paid / 13 unpaid, overdue: 8 true. Backwards-міграція реалізована — pickає найінформативніший legacy value (overdue > paid/overpaid > partial_paid > shipped > partial_shipped > sent).
Frontend. InvoiceKanbanBoard.tsx — 4 lifecycle-колонки замість 9; у картці payment + fulfillment progress bars з кольоровим кодом, бейджі trio (lifecycle/payment/fulfillment) і Overdue red filled badge; фільтр-панель отримала окремі секції на кожен вимір + checkbox "Overdue only"; persisted kanbanStatusFilter санітизується на 4-value enum щоб не залишити панель у unreachable state. InvoiceList.tsx — колонка Status тепер рендерить три бейджі поряд + Overdue. i18n en/ua — нові ключі invoice.lifecycle.*, invoice.payment.*, invoice.fulfillment.*, invoice.overdue.
ADR. docs/ai/architecture/multidimensional-document-status.md — генералізує паттерн: коли застосовувати (independent dimensions колапсуються у 1-D enum), коли НЕ застосовувати (single-actor lifecycle типу JournalEntry, або справжній sequenced state machine типу Quotation), recipe міграцій 3 кроки, recompute-order (payment → fulfillment → lifecycle → overdue). Зареєстровано в architecture/README.md. Наступний на черзі — SalesOrder / Quotation / PurchaseOrder.
Перевірки: python manage.py check essentials 0 errors (3 pre-existing tenant warnings); py_compile усіх backend-файлів clean; npx tsc --noEmit 0 помилок; en.json + ua.json валідні; smoke shell-test показав коректний backfill distribution.
2026-05-07 — Gatehouse tech-debt audit + i18n рефактор для gatehouse.ts¶
Закрито дві задачі з todo.md: (1) пункт «Прохідна — командний центр з реквізитом «Підрозділ»» (поставлений 2026-04-29) та (2) P3-пункт «i18n рефактор для gatehouse.ts» з блоку tech debt після MVP. Решта tech debt P1/P2/P3 (8 пунктів) перенесена в docs/dop/modules/horizontal/gatehouse/README.md § Deferred / Ideas — звідти автогенерується BACKLOG.md через python docs/_build_backlog.py. Аудит виявив, що командний центр та два P1/P2 пункти (Vehicle auto-creation на ANPR забруднює Fleet, частково FortNet UI editing) уже були закриті в коммітах gate (43b2ec7 / 9400336) 30 квіт – 1 трав. без оновлення todo.md/done.md — задокументовано окремими записами нижче. i18n рефактор: усі name_ua / description_ua / description для секції / груп / підгруп / 16 items винесені в нові файли frontend/erp/src/i18n/locales/gatehouse.en.json + gatehouse.ua.json (24 пари ключів, dot-notation section.name / groups.{x}.{name|description} / subgroups.{x}.{name|description} / items.{code}.{name|description}). У config/gatehouse.ts додано tx(key) helper, що повертає 4 поля одночасно — у 24 місцях замість inline ставиться ...tx('items.sites'). Тип Section/Group/Subgroup/SectionItem не змінений (контракт той самий, рендер у Sidebar/useBreadcrumbs/SectionDashboardPage теж). Інші 12 config-файлів (essentials, fleet, hrm, accounting, …) залишаються inline — їх рефактор окремою задачею. npx tsc --noEmit EXIT=0. Документація оновлена: додана згадка Gatehouse в dop/overview.md (Horizontal-таблиця) + dop/spec.md (домен B пункт 8); README модуля отримав category: horizontal у frontmatter (раніше було uncategorized) + повноцінну ## 🔮 Deferred / Ideas секцію з 11 пунктами. Це відразу закриває P3-пункт «docs/dop/spec.md + overview.md не згадують Gatehouse» і «docs/BACKLOG.md не має згадки про Gatehouse» — після python docs/_build_backlog.py backlog тепер містить категорію 🧱 Horizontal-модулі з 11 deferred-пунктами Gatehouse.
2026-04-29 — Gatehouse «Командний центр з реквізитом «Підрозділ»» (виявлено 2026-05-07 під час аудиту)¶
Закрито пункт todo.md, поставлений 2026-04-29. Замість оригінально передбачуваного subdivision обрана сильніша абстракція — Site (physical production unit: elevator / oil-mill / pier / warehouse complex) як новий MasterDataModel у backend/gatehouse/models.py (Site не привʼязаний до однієї Organization — один Site може містити обʼєкти різних юр.осіб холдингу, тому це pure physical topology, окреме від HR/cost-center). Зміни: міграція 0007_site_gatepoint_site додає Site + GatePoint.site FK; Site.layout_json зберігає SVG zone-coordinates per-Site → TerritoryMap.tsx рендерить різну карту залежно від обраного Site; GatehouseDashboard.tsx отримав site-selector у toolbar (persistent через localStorage gatehouse-dashboard-selected-site-id) + 4 окремі tabs Map / Recent events / Queue / Weighing tickets (замість 2 запланованих); фільтрація events / queue / tickets по gate_point.site = selectedSiteId; новий Site довідник з formType:'custom' (SiteForm.tsx, SiteLayoutEditor.tsx, SiteList.tsx). Окремих weighbridge-tickets-tab у dashboard не зроблено — оператор користується Weighbridge Operator процес-екраном з S4 (там ізольовані live-readings + open/closed lists), ця задача дублювала б його. Змін зафіксовано в коміті 43b2ec7 gate 30 квіт.
2026-04-30 — Gatehouse Sprint post-MVP (Site / VisitTemplate / QueueTicket / VisitorPass / Personnel / GatePointRole)¶
Велика серія коммітів 43b2ec7 gate (30 квіт) + 9400336 gate (1 трав) shipping-ready, але done.md не був оновлений — записано тут ретроспективно під час аудиту 2026-05-07. Що додалось поверх 5 sprints MVP:
- Site (вище) як phys-topology root + GatePoint.site FK + per-Site SVG layout.
- VisitTemplate / VisitTemplateStep / VisitPlan / VisitPlanStep — шаблони візитів з впорядкованими кроками; pick_visit_template 5-priority resolver (queue_ticket.template → vehicle.gate_is_representative='representative' → vehicle in fleet='internal_vehicle' → external truck='external_vehicle' → applies_to='any' fallback); materialize_visit_plan(event, template) snapshot'ить шаблон у plan (services.py:1554-1590, materialize_visit_plan нижче); 3 канонічні seed-шаблони (internal_overnight, external_full_cycle, representative_free).
- VisitStepKind довідник із system_code (entry / weigh_in / weigh_out / qc / unloading / loading / parking / exit) — anchor cross-module advancement; tenant може реєструвати custom kinds (dosimetry / customs / vet) без зміни коду.
- GatePointRole довідник — user-defined ролі прохідних замість hard-coded enum-у (turnstile / vehicle / weighing / checkpoint / mixed). Tenant додає «Quality control» / «Customs post» через UI.
- QueueTicket з kiosk_token + driver self-service kiosk + Kanban оператора черги (QueueOperator.tsx) + management expire_queue_tickets для cron + PrintContract queue-ticket.
- VisitorPass — guest registration з PDF print-form (print_templates/visitor-pass + GuestRegistrationPanel.tsx).
- Personnel tab у GateOperator (PersonnelTab + SelectedSubjectCard + types.ts) — оператор бачить employees + guests на одній прохідній.
- Vehicle auto-creation на ANPR — рефактор з варіанту «створювати draft Vehicle» (забруднював Fleet) на «не створювати взагалі»: plate зберігається лише як snapshot string у event.vehicle_plate, FK заповнюється тільки якщо існуючий fleet.Vehicle знайдено (services.py:360-369). Закриває P1-пункт з MVP tech debt.
- GateOperator масивно розширений (+1719 рядків): live ANPR-feed, accept/reject UI, manual-override, Personnel tab, Print PDF.
- backfill_gate_event_numbers + backfill_gate_snapshots management commands для існуючих tenants.
- AI bundle оновлений: docs/ai/domains/gatehouse/business-rules.md + entity-доки (gate-events, visitor-passes generated.md), eswf/architecture/app-shell.md отримав абзац про single-pane-навігацію контекст. Коментар: 5 спринтів MVP давали роботу від ANPR до GoodsReceipt; post-MVP додав «формалізацію візиту» (план/чекпойнти/ролі) і прибрав забруднення Fleet.
2026-05-03 — tabsStore cleanup: 43 forms cleared + store deleted¶
Закрито TODO «Cleanup legacy tabsStore посилань» з todo.md, яке з'явилось після Stage 8 merge V2 single-pane shell (single-pane-navigation.md). 43 файли (всі custom master-data forms, custom transaction forms, fleet/gatehouse/hrm/production/accounting forms, plus ChatFAB / ListPrintV2 / TransportRequestToolbar / GatehouseDashboard / TerritoryMap) вичищено від legacy useTabsStore викликів. Заміна паттернів: closeTab локальний callback → useFormClose(doc.basePath) хук (navigate на список); openTab({...}); navigate(path) для refs → просто navigate(path); updateTabTitle subscribe + sync useEffect → видалено (заголовок у HeaderBreadcrumbPath); replaceTabPath дзвінки → видалено (navigate({replace:true}) у useDocumentForm робить URL update самостійно); useTabsStore((s) => s.tabs.length > 0) у ChatFAB → false. Видалено сам src/store/tabsStore.ts (no-op заглушка більше непотрібна). Особливі випадки: useUIStore збережено у Item/GatePoint/GateEquipment/GateEvent forms (там setChatDrawerOpen для AI chat); Vehicle/Waybill forms — 4+1 окремих openTab callbacks замінені на navigate, бізнес-гард if (!doc.isNew || !doc.dirty) у Waybill збережено; GatehouseDashboard + TerritoryMap — 4+4 wrapper-функцій спрощено до прямих navigate; ListPrintV2: getState().activeTabId → useLocation().pathname. Комміт 33576f0 (45 files changed, +112/-820). tsc EXIT=0. Жодних UI/функціональних змін очікується — мертвий код видалено, зовнішня поведінка та сама.
2026-05-03 — Task-centric navigation: experiment closed, V2 single-pane shell merged (Stage 8)¶
Закрито 8-етапний експеримент task-centric-navigation.md, який почався 2026-05-03 з гіпотези «1С-style internal tabs — інерція мислення, single-pane + drafts + command palette закривають 95% реальних задач». Результат — Варіант 1 (повний merge V2 в master): AppLayout (V1 shell), KeepAliveOutlet (174 LOC UNSAFE React Router internals), TabBar (252 LOC), useShellVersion, NavigationSettings видалено; tabsStore перетворено на no-op заглушку (~50 forms ще містять legacy useTabsStore.getState().X виклики — окрема cleanup задача в todo.md). AppShellV2.tsx перейменовано на AppShell.tsx. Серія 5 коммітів cleanup: 2870a71 (form guards), a1a7870 (self-registration), f22fb4e (flag drop), f92cda4 (V1 layout drop). Реалізована архітектура — у docs/ai/architecture/single-pane-navigation.md (новий ADR), tab-based-navigation.md переведений у статус superseded. Економія: 96px → 36px chrome на ноутбуці (12% screen height на 768p), -700 LOC технічного боргу, концептуальна чесність позиціонування DOP як «task-centric платформа», не «1С на вебі». Drafts API (Stage 2-3, drafts-api-memo.md) лишається як єдине джерело persistence — TabBar persist invariant ніколи не був реалізований у коді (tabsStore без persist middleware, F5 чистив усі вкладки). Stages: S1 inventory (task-centric-navigation-inventory.md) → S2 backend core.Draft model + /api/v1/drafts/ ViewSet → S3 frontend DraftsPage + useDraft hook (POC на GoodsReceiptForm) → S4 AppShellV2 + useShellVersion feature-flag + NavigationSettings toggle → S5 Command Palette розширено (auto-generated SEARCHABLE_ENTITIES → whitelist 14 entities після виявлення 1/641 API calls regression, Quick Create секція, Recent items dropdown через Zustand persist) → S6 Split View (deferred) → S7 self-test чек-ліст (task-centric-navigation-self-test.md, 26 сценаріїв у 5 категоріях) → S8 рішення merge. Lessons learned (#1-7 у single-pane-navigation.md § Lessons learned): React StrictMode dev double-effect race, hydration race з useState defaults, DRF DEFAULT_PAGINATION_CLASS на bounded resources, list-page stale data після back-nav, plugin gate ≠ navigation gate (виявлено через 403 спам на sales-field), SEARCHABLE_ENTITIES автогенерація — bad default, custom form hooks потребують централізованого tracking. Backlog поповнено: cleanup ~50 forms (видалити tabsStore посилання), plugin-gate в useQuery усіх plugin-gated компонентів.
2026-04¶
2026-04-28 — Driver App — audit і fix модуля¶
Закрито пункт з todo.md. Сторінка Driver App (DriverAppPage.tsx) та /api/v1/mobile/ endpoints — помилки при відкритті усунуто, functional parity з Sales Field Manager / mobile-sales підтримана.
2026-04-28 — Gatehouse Sprint 5 (personnel turnstile + FortNet integration + cross-module bridges)¶
Закрито Sprint 5 з planning/gatehouse-plugin.md — фінальний sprint Gatehouse, plugin переходить у status: sprint-5-shipped. Backend HRM: нова модель AttendanceLog(tenant, employee, date, in_at, out_at, source ∈ {gatehouse, fortnet, manual}, is_open) з UniqueConstraint(tenant, employee, date), ViewSet/serializer/EntityRegistry; Employee отримав поля external_scud_id/card_uid/access_level (1-4); services.attendance.upsert_attendance_log_from_gate_event(gate_event) (idempotent earliest-in/latest-out), recompute_days_worked(period, employee) (MVP правило: пара in/out = 1 день) + endpoint /payroll-slips/{id}/recompute-days-worked/. Backend Gatehouse: GateEvent.source + external_event_id + UniqueConstraint(tenant, source, external_event_id) для idempotency replay-ів; services.register_personnel_gate_event(card_uid|external_scud_id, ...) з Employee resolution + silent drop on miss + dedup; services.bridge_to_containerhub_if_installed(event) — defensive bridge у containerhub.GateTransaction коли ANPR несе container_number і ContainerHub plugin встановлено; endpoint POST /external-events/ як універсальний для будь-якого external SCUD pull; equipment_event_ingest для ANPR з container_number тепер тригерить ContainerHub bridge. Новий Django app integrations/fortnet_scud/: моделі FortnetIntegrationSettings/FortnetSyncLog, services.pull_events_from_fortnet(tenant, trigger) (CSV reader → register_personnel_gate_event per row + FortnetSyncLog audit), management fortnet_pull для cron, REST endpoints /integrations/fortnet/sync/ + /fortnet/status/ + ViewSet'и для settings/logs. Seed CSV seed_data/access_log_2026_04.csv (50 рядків, 5 employees × 5 days × 2 events). Frontend: новий FortnetIntegrationPage.tsx (settings + status + manual sync кнопка + історія pull-ів), gatehouse.ts має нову subgroup Integrations з пунктом fortnetIntegration (gated by plugin: 'fortnet_scud'); applications.ts + seed_shop мають appFortnetSCUD (appType: 'integration', requires=['appEssentials','appGatehouse','appManager'], installed_by_default=True). seed_gatehouse v5 додає SCUD-поля 5 employees (EMP001-005, AABBCC01-05, access_level 1-4) + FortnetIntegrationSettings(csv_path=<seed CSV>, gate_mapping={'GATE-MAIN':'gate_personnel'}). Перевірки: manage.py check ✅, tsc --noEmit ✅, dependencies.test 24/24 ✅, makemigrations згенерувала 3 міграції (gatehouse 0002, hrm 0003, fortnet_scud 0001) — migrate зачекає на Postgres. AI bundle оновлено секцією Sprint 5: rationale за «fail loud vs fail quiet on missing employees», external SCUD pattern (per-vendor isolated app + stable external-events/ contract), bridge_to_containerhub defensive try/except. Concurrent з S5 — Plugin Gatehouse повний end-to-end: vehicle gate, weighbridge, turnstile, vehicle trace, FortNet sync — без жодного фізичного hardware.
2026-04-28 — Gatehouse Sprint 4 (weighing scenario — ANPR+scale correlator + GoodsReceipt bridge)¶
Закрито Sprint 4 з planning/gatehouse-plugin.md. End-to-end від webcam-фото зі смартфона до проведеного GR без ручного вводу ваги. Backend: stateful event correlator у services.py — per-process in-memory ring buffer keyed by (tenant_id, gate_point_id) з 30s window, thread-safe _recent_lock, match-and-remove semantics щоб запобігти подвійному створенню тикетів; _open_or_close_ticket decides 1-ше зважування (open weighed_in) vs 2-ге (close + auto-derived direction_pattern); 24h lookback на open ticket lookup; needs_review при weight<=0 чи vehicle mismatch. services.create_goods_receipt_from_weighing(ticket) — idempotent bridge у essentials.GoodsReceipt з одним GoodsReceiptLine(item, quantity=net_weight, unit=kg) (з конверсією kg→t якщо Item.base_unit у тонах); створює GR як state='draft' (operator посту окремо), оновлює ticket.goods_receipt + status='posted'. Endpoint POST /weighing-tickets/{id}/create-goods-receipt/ з WS broadcast. equipment_event_ingest для purpose ∈ (weighing_station, mixed) тепер маршрутизує kind ∈ (anpr_camera, scale) через correlator перш — fallback до S3 register_vehicle_gate_event тільки коли correlator не утворив пари. Frontend: WeighingTicketForm розширено Mantine Stepper (Чернетка → Брутто → Тара → Закрито → Прихід з кольорами orange для needs_review, teal для posted) і кнопкою «Створити прихід (GoodsReceipt)» (з Tooltip-поясненням коли disabled). Новий WeighbridgeOperator.tsx — лайв-екран із live scale + ANPR readings, списком відкритих талонів, окремим блоком «Готові до приходу». gatehouse.ts має новий пункт weighbridgeOperator у Processes/Operator. seed_gatehouse v4: 3 closed S4-тикети (з event_in + event_out wired до scale GateEvents), 2 з GR-link через bridge, 1 open weighed_in, 1 needs_review. Перевірки: manage.py check ✅, tsc --noEmit ✅, dependencies.test 24/24 ✅. AI bundle оновлений секцією Sprint 4 з correlator-rationale (per-process buffer, match-and-remove, 30s/24h windows), direction_pattern auto-derivation, GR-bridge constraints. Sprint 5 (turnstile + FortNet + cross-module bridges) — далі.
2026-04-28 — Gatehouse Sprint 3 (vehicle gate scenario — ANPR + QR labels + checkpoint timeline)¶
Закрито Sprint 3 з planning/gatehouse-plugin.md. End-to-end: ANPR → auto-Vehicle → QR-наклейка → mobile-сканер на постах → timeline візиту. Backend service-layer розширено: register_vehicle_gate_event (lookup fleet.Vehicle by license_plate__iexact → auto-create draft на miss → відкрити GateEvent з qr_label_uid=secrets.token_urlsafe(8) для direction=in, або закрити open visit для direction=out з defensive needs_review якщо exit без entry); accept_gate_event (operator one-click draft → registered); record_checkpoint (lookup by qr_label_uid + 30-секундна idempotency-window). Endpoints /gate-events/{id}/accept/, /checkpoints/scan/. equipment_event_ingest для kind=anpr_camera тепер маршрутизує через нову service (auto-Vehicle + QR + direction-aware). PrintContract vehicle-gate-pass (A6 landscape з plate + QR-кодом qr_label_uid як inline-SVG; template backend/gatehouse/print_templates/vehicle-gate-pass/base.html); frontend mapping vehicleGatePass → vehicle-gate-pass додано до lib/printPdf.ts (бо deriveDocType зрізає trailing s у Pass). Frontend: 3 нові custom-компоненти — GateOperator.tsx (оператор з live ANPR-feed, accept/reject, однокліковий PDF, manual override, на території list), CheckpointScanner.tsx (mobile-first сканер QR через html5-qrcode з 5-сек дедуп на клієнті), VehicleTrace.tsx (Mantine Timeline візиту з time deltas + open/closed бейджем). У gatehouse.ts нова subgroup Yard posts під Processes з checkpointScanner + vehicleTrace. seed_gatehouse v3: 5 closed visits з 3-4 GateCheckpoints кожен + 1 currently-open visit. Перевірки: manage.py check ✅, tsc --noEmit ✅, dependencies.test 24/24 ✅. AI bundle (docs/ai/domains/gatehouse/) оновлений секцією Sprint 3 additions з vehicle-gate scenario rationale, idempotency rationale, PrintContract gotcha. Sprint 4 (event correlator + GoodsReceipt bridge) — далі.
2026-04-28 — Gatehouse Sprint 2 (hardware bridge layer)¶
Закрито Sprint 2 з planning/gatehouse-plugin.md. ADR: eswf/architecture/hardware-integration.md — driver-as-plugin registry, standalone Python device-bridge, REST + WebSocket transport, SQLite outbox для backpressure, EasyOCR на бекенді для webcam-driven ANPR. Backend: POST /api/v1/gatehouse/ocr/license-plate/ (lazy-singleton EasyOCR + UA regex post-process з Cyrillic→Latin normalisation, 503 fallback), GET /api/v1/gatehouse/equipment-drivers/ (метадата 6 кодів), POST /api/v1/gatehouse/equipment-events/ (REST ingest з tenant check + WS broadcast + auto GateEvent для ANPR/card). WebSocket consumer /ws/gatehouse/{tenant_id}/[gate_point_id]/?token=<JWT> з 4 close codes для tenant isolation (4001 auth/4002 no tenant/4003 mismatch/4004 wrong gate-point), broadcaster helpers push_equipment_event/gate_event_created/weighing_ticket_updated. device-bridge/ — окремий Python-сервіс з 4 драйверами (file_scale_generic, serial_scale_ascii, serial_scale_cas_ci, card_reader_emulator), 3 emulator-CLI (scale + card + anpr), config через TOML, async runtime з outbox-drain і retention-loop. Frontend: WebcamANPRTester.tsx (getUserMedia + canvas overlay + bbox), useGatehouseSocket.ts (auto-reconnect WS hook), GatehouseDashboard оновлено з WS-індикатором + live-equipment секція (12 events ring buffer); webcamAnprTester роут у Processes → Operator. seed_gatehouse v2 додає 7 GateEquipment з connection_config готовою для bridge. requirements.txt отримав easyocr>=1.7,<2.0 з коментарем про heavy dep. Docs: README модуля + AI bundle оновлені секцією Sprint 2 deliverables. Перевірки: manage.py check, dependencies.test (24/24), tsc --noEmit чисто. Sprint 3 (vehicle gate ANPR + QR labels + checkpoints) — далі.
2026-04-28 — Gatehouse Sprint 1 (universal core, manual input)¶
Реалізовано Sprint 1 з planning/gatehouse-plugin.md. Новий Django app backend/gatehouse/ з 5 моделями: GatePoint, GateEquipment, GateEvent, GateCheckpoint, WeighingTicket. REST endpoints /api/v1/gatehouse/* через TenantFilterMixin + EntityRegistry.register. Service layer (record_weight_in/out, close/reopen_weighing_ticket, register/reject_gate_event) проводить state machine; _PostedStateGuardMixin блокує тихі редагування state='posted' згідно .claude/rules/document-posting.md — preshape для Sprint 4 GoodsReceipt-bridge без retrofit. Frontend: frontend/erp/src/config/gatehouse.ts (3 групи: Operations, Documents, Master Data) + кастомні GatehouseDashboard.tsx (operator overview зі статичним polling) і WeighingTicketForm.tsx (дві ваги + кнопки Брутто/Тара/Закрити/Перевідкрити). applications.ts: appGatehouse як plugin (forModules=['essentials'], requires=['appEssentials'], installed_by_default=True). seed_shop.PRODUCTS дзеркалить frontend. Management seed_gatehouse v1: 4 GatePoint (mixed/personnel/weighing/yard), 5 авто/водіїв/працівників, 10 історичних events, 2 closed WeighingTicket з net 13.4 / 13.65 т. AI bundle: docs/ai/domains/gatehouse/ — README + entities (5 моделей JSON Schema) + business-rules (pseudo-code service layer). Глосарій оновлено новими термінами. Перевірки: manage.py check, dependencies.test (24 passed), tsc --noEmit чистий. Roadmap S2-S5 — далі.
2026-04-28 — Three-layer docs — заповнено user/production/work-orders.md¶
Закрито TODO-маркери в user/production/work-orders.md — відповіді власника на питання про life-cycle WO (статуси, обовʼязкові поля, запуск, закриття). Документ переведено з draft-state з маркерами в стабільний reference для AI-bundle і нових розробників.
2026-04-28 — Module versioning + dependencies¶
Реалізовано в planning/module-versioning-and-dependencies.md. version/channel поля у Product + SectionItem, ендпоінт /api/v1/system/version/, buildDependencyTree/validateDependencies/topoSort/getAppDescendants/getAppAncestors у config/dependencies.ts, 17 Vitest-кейсів зелені, гепи appChat/appMED/appBAFSync закриті. Готує дерево залежностей для accordion-сайдбара.
2026-04-28 — Accordion sidebar — модулі та плагіни як акордеон¶
Реалізовано accordion-структуру бокової панелі — planning/accordion-sidebar.md. appType: 'addon' → 'plugin', 6 horizontal-функцій (Accounting/CRM/HRM/Production/Budgeting/Consolidation) → 'plugin'. Нові поля sidebarHidden/sidebarParent у config/types.ts. Backend: shop migrations 0009 (choices) + 0010 (data backfill stub) + seed_shop тепер пише app_type (DB normalised: 14 plugin / 4 integration / 3 module / 1 core). buildSidebarTree(items) поряд з buildDependencyTree у config/dependencies.ts — 23 Vitest-кейсів (з них 6 нових для accordion). UI: Mantine Accordion у Sidebar.tsx з persisted open-state у uiStore.sidebarAccordionOpen.
2026-04-27 — ADR Update Delivery — архітектура доставки оновлень DOP клієнтам¶
Зафіксовано архітектурне рішення доставки оновлень — eswf/infrastructure/update-delivery.md. Без коду; перший крок до auto-update після desktop-installer.md. Ключові рішення:
- 3 канали релізів:
stable(default, ~раз/місяць),beta(opt-in, ~раз/2 тижні),dev(per-merge, внутрішнє). Перемикач — у admin tools per-tenant. - SemVer 2.0 для core, незалежний SemVer для плагінів з
core_min_version/core_max_versionяк полямиProduct(новий gap у plugin-instruction.md). - Manifest на well-known URL (
updates.eswf.dev/<channel>.json) — джерело правди про «остання версія для каналу X», містить image digest, migration_level, plugin_compat, kill_switch, signature. Хоститься як статика (CF Pages / GitHub Releases). - Розповсюдження гібридом: Phase 1 — manifest + tarball (без registry); Phase 2 — pull з ghcr.io за digest; Phase 3 — приватний registry для платних редакцій.
- Міграції БД forward-only + обовʼязковий pre-flight
migrate --checkу новому контейнері до зупинки старого. - Rollback через snapshot-restore, не через migration revert. Snapshot = volume tar + DB dump (через існуючий backup-recovery.md) + previous image digest. Тригери: failed migrate, health-check timeout, ручне натискання.
- Auto-update agent у двох форм-факторах: Electron tray-модуль у
launcher-gui/для desktop (manual mode default),dop-updatersystemd-сервіс для self-hosted (scheduled mode default), side-car / external CI для Compose/K8s. - Plugin compat hard-block: агент перевіряє діапазони перед
migrate, при несумісності апгрейд core блокується з UI-підказкою «оновити плагін → ретрай». - Phased rollout через manifest-промоушен
beta → stable, kill-switch через поле в манифесті (warning-нотифікація, не примусове блокування).
Roadmap у 7 фаз від «manual upgrade via tarball» (Phase 1) до «K8s operator» (Deferred). Перший крок реалізації — manage.py upgrade_to <version> команда + GitHub Release manifest. Trigger — перший зовнішній desktop-клієнт. Перехресне посилання у desktop-installer.md (Частина 2 deferred) і запис у DOCS.md під Infrastructure.
2026-04-27 — Seed_demo друга хвиля (друга група документів)¶
11 нових типів артефактів у демо-snapshot, всі — у posted-state з proper postings:
- GoodsWriteoff × 1 (0.5 t spoiled wheat, FIFO consumption через
deduct_writeoff+ InventoryJournal). Inline post уdemo_inventory.py::_seed_goods_writeoff. - AdditionalExpenseAllocation × 1 (5 000 UAH freight на 30 t wheat GR через
post_additional_expense— WAC re-pricing + COGS adjustment). Створює 2 BusinessOperation на льоту. Використовує European PCG (3700/4000/6070) — UA-NSBO ще не seeded в inventory phase. - WorkOrder closure → Manufacturing × 1 (через
complete_work_orderсервіс — wheat + corn → finished feed product). BOM перебудовано: input items = wheat + corn (з batches), output_qty=2 щоб не вичерпати залишки. - VehicleRefueling × 5, TransportInvoice × 3 (IV→ZT парні до waybills), DriverSalaryAccrual × 1 (Іваненко, березень 2026 фікс. оклад) — у
demo_case_b.py::_seed_fleet_documents. - CRM Quotation × 2 + SalesOrder × 2 (на базі deals, conversion flow Q → SO) — у
crm.py::seed_quotations+seed_sales_orders. - QualityInspection × 1 (29 t accepted / 1 t rejected — partial outcome, через
post_quality_inspection→ BlockedStockLedger). - SalesReturn × 1 (часткове повернення 1 шт по case_d invoice).
- EDIMessages × 29 (через
call_command('seed_edi_messages', tenant=N, count=30)у extras phase).
Implementation lessons: (1) European PCG (4-значні: 3700/4000/6070) seeded init_accounting_data для всіх tenant'ів; UA-NSBO (3-значні: 281/631/903) seeded ПІСЛЯ inventory phase, в extras. Тому код залежний від рахунків треба mapping'ити на правильний ledger відповідно до того, в якій phase він виконується. (2) Driver не має last_name напряму — це через person FK; пошук — через name__icontains. (3) complete_work_order потребує stock на input items, тому BOM перебудовано на wheat+corn (мають batches з demo_inventory).
Sanity-check після seed_demo --reset — всі 11 артефактів posted. Підвищує покриття UI-сценаріїв demo-tenant'у з ~70% до ~95% (всі основні TransactionModel-документи мають seed-приклад).
2026-04-27 — B-P0.4d (вечір): silence remaining 72 core.W001 з reason-tagged whitelist¶
W001 cleanup tail після B-P0.4b denormalization. 41 авто-правила (32 CHAINED_FILTER, 3 DOC_TENANT_ID, 6 SCOPED_VIA_FK) пройшло через backend/scripts/silence_w001.py. 31 NEEDS_REVIEW protriaged вручну і застосовано через backend/scripts/silence_w001_manual.py з 4 чіткими reason-категоріями: tenant in **kwargs × 7, scoped via parent FK × 14, PKs tenant-bound globally unique × 6, chained filter below × 4. python manage.py check зараз чистий (0 W001). Деталі — w001-audit.md.
2026-04-27 — B-P0.4b (вечір): Subtable tenant_id denormalization migration (19 subtables)¶
19 моделей × (AddField nullable → RunPython backfill з parent → AlterField NOT NULL → composite index). 6 коммітів, по одному на app: shop, sales_mobile_api, essentials_quality, fleet, eswf_chat, essentials. Auto-fill backstop: pre_save signal в essentials/apps.py (12 моделей) + Model.save() override у eswf_chat (Message, RoomMembership). ItemComponent — виключено зі скоупу (BOM M:N self-reference, debate scope vs global). Follow-up commit enable_rls: cover plain models with denormalized tenant FK — RLS policies охопили 19 нових denormalized FK. Tests: test_tenant_isolation.py 65 passed, 71 skipped (skips незмінені). Деталі — subtable-tenant-audit.md § Lessons learned.
2026-04-27 — A4-frontend завершальний штрих: аудит tenant-management поверхонь¶
Пройшовся по frontend/erp/src/config/*.ts + pages/AdminToolsPage.tsx шукаючи кандидатів на requiresMultitenant: true понад уже промарковані organizations + activeUsers admin tool. Нових поверхонь не знайдено — departments/persons/businessDirections/contracts/clients це внутрішня структура одного бізнесу (валідні в single-tenant); journals/ledgers/reports/processes операційні; cleanup/repost/scheduledTasks/debugPanel системні; екрани users-management / tenant-invite / multi-org admin у коді ще не існують. TODO-пункт із todo.md знято: повернути увагу при додаванні нового екрана керування тенантами/користувачами.
2026-04-27 — B-P0.3b: Decimal precision drift fix (79→0)¶
Класифікатор + матриця (backend/core/management/commands/check_decimal_precision.py). Додано 11 нових концептів — money_rate, money_rate_hp, money_functional, gps_coord, confidence_score, physical_weight, physical_volume, distance_km, mileage, work_time, fuel_norm. Для concept-залежних від моделі полів (наприклад, rate у DriverSalaryRate — money_rate; у ExchangeRate — FX rate; у TaxRate — vat_rate) — окремий MODEL_FIELD_OVERRIDES з підтримкою __skip__. Класифікатор-як-source-of-truth: --strict exit 1 якщо який-небудь DecimalField не вписується.
Owner-confirmed рішення:
1. PartyLedger.{debit,credit,running_balance} = (18,2) — функціональні гроші для multi-currency settlement, НЕ drift. CashJournal/PlannedPaymentJournal functional_amount піднято з (15,2) до (18,2) для узгодження.
2. medoc_exchange.TaxInvoice.{total_net,total_vat,total_gross} — UA tax invoices у грн, тому (18,2) → (15,2).
3. production.{BOM.standard_cost_per_unit, BOMLine.standard_input_cost} — feed WAC, тому (15,4) → (15,6) cost_price.
4. fleet.FuelModifier.value + WaybillFuelRecord.calc_coefficient_sum — це коеф-fuel, не money; reclassified як percent. (6,2)/(8,2) → (5,2).
5. ContractSpecification.{price_per_km, price_per_ton_km} + WorkCenter.cost_per_hour — high-precision money_rate (15,4). Окремий концепт money_rate_hp.
11 міграцій × 79 полів (всі AlterField): consolidation 0002, containerhub 0003, essentials 0016, essentials_quality 0002, fleet 0003, hrm 0002, medoc_exchange 0002, production 0005, registers 0002, sales_mobile_api 0002, shop 0002. Усі застосовано migrate на dev SQLite — OK. python manage.py check_decimal_precision --strict зараз exit 0 (DRIFT=0).
Snapshot: OK 121 → 324, DRIFT 79 → 0, UNCATEGORIZED 125 → 1 (containerhub.GateTransaction.temperature_set — out-of-matrix °C, marked __skip__). Audit doc/JSON оновлено: docs/ai/result/r4/decimal-precision-audit.md.
CI gate --strict буде увімкнено разом з решти CI (CI deferred — див. memory).
2026-04-27 — Sweep dev сесія: B-P0.4c + A4-frontend + seed_demo Multi-CoA активація¶
Закрито 3 derivative-задачі з ранкового списку:
B-P0.4c — 14 LIKELY_LEAK fix'ів у service-шарі. Додано tenant_id= kwarg у 14 lookup-сайтах (reference_id/source_document_id без tenant) у essentials/services/{additional_expense_service,batch_service,goods_receipt_service,manufacturing_service}.py. Розширено сигнатури reverse_batch_receipt(*, tenant_id) та reverse_batch_deductions(*, tenant_id) (раніше cross-tenant ID overlap міг видалити чужі StockTransaction/Batch); оновлено 7 call-sites: 6 в essentials/views/transactions.py (post/unpost для GoodsShipment + GoodsWriteoff), 1 у goods_receipt_service.py. Новий test_service_tenant_isolation.py — 4 цілеспрямовані тести з cross-tenant ID-collision (shared SHARED_ID між tenant=acme та tenant=globex); всі passed. manage.py check тепер показує 0 W001 LIKELY_LEAK у service-файлах (раніше було 14). 26/26 існуючих manufacturing+posting тестів passed.
A4-frontend — DOP_MULTITENANT conditional rendering у React shell. Backend endpoint /api/v1/instance/info/ уже існував з 2026-04-27 ранкової сесії. Frontend bind: новий хук useInstanceInfo.ts (60-хв staleTime, 24-год gcTime через React Query — endpoint cacheable). Розширено useFilteredSections.ts — фільтрує items з прапорцем requiresMultitenant: true коли multitenant=false. Додано поле requiresMultitenant?: boolean у SectionItem (config/types.ts). Розмічено organizations (config/essentials.ts). У AdminToolsPage.tsx activeUsers tool (force-logout/block) сховано в single-tenant режимі. Тест useFilteredSections.test.tsx — 2/2 passed (multitenant=true → видно, multitenant=false → приховано).
Seed_demo: Multi-CoA активація + новий inventory phase + base фікси. Інвентар Postgres БД після ранкового переїзду показав 27 порожніх типів документів (включно з GoodsReceipt/Shipment/Manufacturing про які питав власник) і 9 порожніх ключових master-data. Після обстеження виявлено: Multi-CoA інфраструктура (Ledger × 2, core.reports.standards, BusinessOperationTemplate модель) була реалізована, але seed_demo не викликав seed_ua_nsbo — тому 0 templates і UA-NSBO posting'и скіпались через missing_template_policy='skip'. Зміни:
baseфаза фікс БАГ-ів (core/seeders/demo_base.py): доданоseed_departments(5 шт),seed_contracts(5 шт по парах ES↔partner з EDRPOU lookup),seed_exchange_rates(112 синтетичних UAH/EUR за Jan-Apr 2026 — sinusoidal walk 45.2 ± 0.8). Методологія в seed-methodology.md §2 обіцяла ці три, але код мовчки скіпав.- Нова фаза
inventory(core/seeders/demo_inventory.py, 273 рядки): 1 grain farmer Client (ФГ "Колосок"), 2 GoodsReceipt (ZT отримує 30t wheat + 20t corn — посту черезpost_goods_receipt→ реальні Batches + StockTransactions + InventoryJournal + PostingGroup), 2 GoodsShipment (ZT→AE 12+8t wheat — FIFO consumption + COGS черезdeduct_fifo), 1 PurchaseOrder, 1 CashTransfer (з новою BO Bank-to-Cash 5310/5120 створеною на льоту), 1 PlannedPayment з 3 lines (income/expense для Payment Calendar). Підключена вseed_demo.PHASESі запускається передextras. extrasфаза розширена: новий_seed_ua_nsbo_templatesвикликаєcall_command('seed_ua_nsbo', activate=True)— створює 81 UA-NSBO ChartOfAccounts + 54 BusinessOperationTemplate + активує Ledger ua_nsbo. Тепер кожен пост документа породжуєPostingGroupв обидвох контурах (pcg_eur + ua_nsbo).
Кінцевий стан demo tenant після seed_demo --reset: 18 типів документів з даними (раніше 13, тепер +GoodsReceipt/GoodsShipment/CashTransfer/PurchaseOrder/PlannedPayment), Department(5)/Contract(5)/ExchangeRate(112)/Client(6 з grain farmer)/BusinessOperationTemplate(54)/ChartOfAccounts(201=120 PCG+81 UA). Ідемпотентність перевірена — повторний запуск нічого не дублює.
Lessons learned:
- Концептуальні моделі != Django models. Модель Standard не існує — стандарт це enum-код у Ledger.code + статичний реєстр core.reports.standards.STANDARDS. Шукав модель буквально → LookupError → мав хибний висновок «Multi-CoA не реалізовано». Урок: при LookupError робити ширший grep по концептуальних термінах (standard|ledger|parallel) перш ніж заявляти про відсутність.
- Конфіг seed-методології != фактичний код seed-ів. seed-methodology.md §2 описує що base сидить tax_rates/exchange_rates/departments — реальний demo_base.py мовчки скіпав. Тільки інвентар через прямий запит до БД виявив. Треба бути впевненим що документація — це pre-condition, а не post-fact.
- Скоп користувача "робимо все" треба перетвоювати у Definition-of-Done з оцінкою. Із початкового списку 14 типів документів реалізовано 7 базових. Решта (Manufacturing closing, AdditionalExpenseAllocation, Fleet, CRM Quotation/SalesOrder, QualityInspection, SalesReturn, EDIMessages) занесено в backlog як друга хвиля. Краще відхилитися від "все" з пояснюванням ніж засипатись на 8/14 і нічого не довести до working.
2026-04-27 — Multi-tenancy hardening + R4 P0 closures (10 задач)¶
Закрито дві паралельні лінії з активної сесії: Лінія А (Postgres dev + multi-tenancy hardening) і Лінія Б (R4 P0 improvements). Підсумок — 10 задач, ~17 нових/змінених файлів, всі core тести зелені (92 passed / 71 skipped).
A1 — Локальний dev на Postgres. docker-compose.dev.yml (Postgres 16-alpine, host port 5433, named volume eswf_pgdata), settings/development.py переключено на Postgres за замовчуванням з USE_SQLITE=1 як аварійним fallback, scripts/init_db.sh, launcher-dev.js авто-піднімає контейнер якщо не healthy. 212 таблиць створено, seed_demo пройшов end-to-end (3 invoices, 30 waybills, 11 vehicles).
A2a — Tenant filter system check. backend/core/checks.py — Django system check (id core.W001) AST-сканує <app>/views/** + <app>/services/**, ловить <TenantAwareModel>.objects.filter|all|get|create|bulk_create(...) без tenant=/tenant_id= kwarg. Whitelist через inline # tenant-ok: <reason>. Exempt context: queryset = … class attr + def get_queryset(self): body (TenantFilterMixin handle їх runtime). Перший запуск — 87 попереджень.
A2b — Auto-isolation tests. backend/core/tests/test_tenant_isolation.py — pytest.mark.parametrize по EntityRegistry.get_all_codes(), для кожної entity: smoke-create row у tenant A через generic _smoke_value() introspection → assert GET /api/v1/data/<code>/<id>/ з tenant B повертає 404 + GET /api/v1/data/<code>/ не містить id. Reusable decorator assert_tenant_isolated(Model) для ad-hoc тестів. Прогон: 65 passed / 71 skipped (FK потребують factory) / 0 failed — жодного tenant leak.
A2c — W001 audit та класифікатор. backend/scripts/classify_w001.py — script читає 87 попереджень, для кожного дивиться ±5 рядків контексту і класифікує: LIKELY_LEAK (lookup за source_document_id/reference_id без tenant — реальна ризик колізії між тенантами), CHAINED_FILTER, DOC_TENANT_ID, SCOPED_VIA_FK, NEEDS_REVIEW. Результат: 14 LIKELY_LEAK (справжні дірки в batch_service.py/additional_expense_service.py/manufacturing_service.py), 32 CHAINED_FILTER, 4 DOC_TENANT_ID, 6 SCOPED_VIA_FK, 31 NEEDS_REVIEW. Audit doc: docs/ai/result/r4/w001-audit.md.
A3 — Postgres RLS (defense in depth). manage.py enable_rls — ALTER TABLE … ENABLE ROW LEVEL SECURITY + FORCE + CREATE POLICY tenant_isolation … USING (tenant_id = NULLIF(current_setting('app.tenant_id', TRUE), '')::bigint) WITH CHECK (...) на 186 tenant-aware таблицях. Subcommands: --check (state report + warns про bypass roles), --drop (test cleanup), --create-app-role <name> (provisions NOSUPERUSER NOBYPASSRLS ролей). Middleware core/middleware/tenant.py — apply_postgres_rls_setting() на lazy resolve + reset_postgres_rls_setting() у finally (запобігає leak між requestами через connection pool). End-to-end тест через eswf_app ролей: без app.tenant_id→0 рядків, =1→3 рядки, =999→0 рядків. Доповнено multi-tenancy.md розділом про RLS implementation + critical caveat про BYPASSRLS.
A4 — DOP_MULTITENANT=false build flag. Backend portion: env var у settings/base.py, middleware single-tenant fallback (Tenant.objects.get(pk=SINGLE_TENANT_ID)), TenantFilterMixin.get_queryset no-op за false, enable_rls skip за false. Public endpoint /api/v1/instance/info/ повертає {multitenant, active_plugins} для frontend conditional rendering. Frontend UI hide — окрема задача.
B-P0.1 — N+1 reports audit (negative result). Прогон по backend/{essentials,production,fleet,containerhub}/reports/ + containerhub/report_views.py — жодного for X: aggregate() патерну. Всі звіти використовують серверний .values(...).annotate(Sum, Sum) GROUP BY з самого початку. R4 P0.1 був defensive prediction про bundle pseudo-code, який реально не переїхав у DOP. Audit: report-n-plus-1-audit.md. Висновок: перепріоритезувати — P0.1 більше не топ-ROI.
B-P0.2 — PartyLedger.direction ADR. docs/ai/architecture/party-ledger.md — convention: direction='receivable' = counterparty owes us (asset), direction='payable' = we owe counterparty (liability). Convention from company's perspective, не counterparty type. Anti-patterns documented: HRM payroll → payable (не employee), Fleet driver salary → payable. Reference implementation essentials/services/posting.py для всіх 4 потоків (Invoice + IncomingPayment + PurchaseInvoice + OutgoingPayment).
B-P0.3a — Decimal precision audit tool + owner-confirmed standard. manage.py check_decimal_precision — AST scan всіх DecimalField проти DOP матриці. Owner-підтверджено 2026-04-27: money/price/amount=(15,2), qty=(15,3), rate=(18,6), vat_rate=(6,4), percent=(5,2), + cost_price=(15,6) (WAC exception), money_functional=(18,2) (multi-currency exception). Прогон: 121 OK, 79 DRIFT, 125 UNCATEGORIZED. Drift здебільшого (19,4) legacy у Invoice/InvoiceLine/PurchaseInvoiceLine/SalesReturnLine/ItemPrice — залишки R4 lock'у який реально не відповідав DOP стандарту. Critical fix: posting-model.md invariant #6 переписаний на правду — раніше документ казав (19,4)/(19,2) як обов'язкове, реально DOP завжди жив на (15,2). Audit: decimal-precision-audit.md + JSON snapshot.
B-P0.4a — Subtable tenant_id decision + audit. Standard chosen: denormalize tenant_id на кожному subtable. Аудит: 18 моделей-розбіжностей (12 essentials, 1 essentials_quality, 2 eswf_chat, 2 fleet, 1 sales_mobile_api, 1 shop). Bundle Gap #11 виявився неточним — описував InvoiceLine як denormalized, а реально тільки PostingEntry (через TenantAwareModel спадкування). Audit doc: subtable-tenant-audit.md з повним migration shape per subtable.
Methodology lessons:
- Defensive R4 predictions ≠ DOP reality. Дві P0 задачі (P0.1 N+1, P0.4 subtable inconsistency) виявилися неточними — bundle описав ситуацію або amplified, або з помилкою. Реальний DOP кращий ніж його описували в R4 closeout. Висновок: завжди валідувати prediction emp-fact'ом перед прийняттям рішень.
- Owner-confirmation > documented standard. posting-model.md invariant #6 претендував на (19,4)/(19,2), реально DOP жив на (15,2) весь час. Без owner-підтвердження я б продовжив сповіщати про "drift" до правильного стандарту як про bug. Урок: при суперечці doc vs code — питати власника, не вірити доку автоматично.
- Hook-блокованих міграцій не уникнути архітектурно — розбити на decision phase + apply phase. Три задачі (B-P0.3b, B-P0.4b, A2c LIKELY_LEAK fix'и) винесено як окремі сесії з командою «розблокувати міграції».
2026-04-27 — Production день (Phase G + H)¶
Production addon UI (Phase G):
- Кастомні форми для всіх 5 сутностей: WorkOrder, BOM, WorkCenter, Operation, BOMRouting.
- Кастомні списки з правильними колонками (capacity, times, sequence, linked mfg number).
- ProductionSettings перенесено у загальний /settings?section=production hub.
- Shortcut-посилання з Essentials (Items, Warehouses, Accounts, Orgs, Departments, Manufacturing) у Production sidebar.
- Quick-jump pill на WorkOrder → Manufacturing form.
- Гвинтик проводок → redirect на /essentials/.../manufacturing/<id>/postings + новий POSTING_CONFIG entry для manufacturing і workOrders.
5 аналітичних звітів (Phase H):
- Warehouse utilization — SVG полиці/силоси/склади-будівлі; real fill % коли задано capacity_weight_kg.
- Order readiness — RingProgress картки з shortfall breakdown.
- Production queue — swimlane WC × час; dept filter; монохром + red only для late.
- Planned cost — Excel-подібний drill-down з BOM клікабельним.
- Actual cost — агрегат Manufacturing за період, drill-down до документів; materials FIFO + labor plan.
- Stock balances — табличний item × warehouse з фільтрами.
Економічна логіка:
- Per-line PostingEntry у Manufacturing (з quantity + product FK) замість агрегованих. 7 inputs + 1 output → 16 entries у WIP-потоці.
- 3-рівневий пріоритет планової ціни: BOMLine.standard_input_cost > ItemPrice[PLAN_COST] > Item.price. Новий PriceType з кодом PLAN_COST + seeded 15 матеріалів.
- Warehouse.capacity_weight_kg + capacity_volume_m3 + real fill % у звіті (fallback на relative коли не задано).
- Production seeder з 2 сценаріями (bakery short-cycle + steel long-cycle), 2 GoodsReceipts для FIFO stock, idempotent (детерміновані WO-номери + cleanup legacy + repost-on-account-change).
- Bug fixes у seeder: 304 Manufacturing зі старим неправильним inventory-account (2010 Intangibles замість 3000 Raw Materials) автоматично repair + repost з коректними рахунками.
Документація: - production-bom/README.md — повне оновлення: Phases A-H, технічний борг, P1-P4 пріоритети.
2026-04-26 — Spec-as-you-go workflow + Phase 3 generators¶
Контекст: після трьох POC-ітерацій порту Invoice → .NET 9 виявилось що ітерації перетворились на археологію — кожна знаходила insights які давно живуть у DOP-коді але не записані в docs/ai/. Висновок: змінювати процес, а не bundle. Деталі рішення в planning/three-layer-docs-strategy.md § Фаза 2.5 + 5.
Аналіз round 3:
- Прочитано RESULT.md і BUNDLE_GAPS.md round 3 (docs/ai/result/).
- Метрики R2 → R3: bundle gaps 8 → 13 (7 spec + 6 UI-only), build/runtime errors 0/0 → 0/0, всі 5 hard requirements виконані, 0 user questions під час порту.
- Спостереження: 6 з 13 gap'ів — UI-only, не ловляться dotnet test (render-mode boundary, SQLite ORDER BY decimal, middleware ordering, Postgres fallback, env-gate visibility, SSR+Interactive double-render). Внесено як lesson learned у planning/three-layer-docs-strategy.md § Фаза 2.
- Phase 2 закрито у strategy-doc з фінальною метрикою-таблицею. R4 не запускати (правило «POC раз на квартал, не як driver»).
Запровадження Spec-as-you-go (Three-layer docs Фаза 5):
- Розширено skills .claude/skills/{custom-form,new-print-template,new-report,new-module}/SKILL.md — додано обов'язковий фінальний checkpoint 📚 Обов'язкове оновлення AI bundle адаптований під специфіку кожного skill (custom-form має таблицю DOP-зміна → bundle-файл; new-print-template акцентує PrintContract / Jinja gotchas; new-report — DrillDownTarget / multi-standard; new-module — повний набір domains/{module}/ + glossary).
- Створено skill update-ai-bundle — для випадків коли завершена робота не покрита іншими skills (баг-фікс >1 год, ADR, lessons learned з POC). 5 кроків: категоризація → формат запису (ADR / business-rule / entity / target gotcha) → консистентність сусідніх артефактів → quality bar → connect to backlog.
- Додано фразу в кореневий CLAUDE.md — нова секція «Working discipline» з двома правилами: «Lessons learned → AI bundle» (>1 год debug → запис) і «Spec-as-you-go для DOP-модулів» (Definition of Done). Список skills у Claude Code Setup оновлено (тепер 5 skills видно).
Auto-generation для bundle (Three-layer docs Фаза 3):
- Generator: python manage.py export_entities — Django model introspection → JSON Schema. Працює з _subtables (dict і str форми), FK з правильною типізацією через target.pk, decimal precision, choices як enum, readOnly через editable=False. 136 сутностей всіх модулів exported, 0 крашів. (backend/core/management/commands/export_entities.py)
- Generator: OpenAPI export з drf-spectacular — python manage.py export_openapi. Програмно викликає SchemaGenerator, фільтрує paths за prefix /<module>/<entity>/, транзитивне закриття компонентів через $ref-walk, dump в bundle-стилі yaml у markdown. 122 entities з OpenAPI paths + повна схема 2.4 MB у _generated/openapi.yaml. (backend/core/management/commands/export_openapi.py)
- Скрипт npm run ai:bundle у docs/package.json — Node.js mjs-скрипт (без зайвих deps), мірорить docs/ai/ → dist/dop-port-bundle/ai-bundle/, виключає result/ (POC feedback) і _generated/ (raw dump 2.4 MB). 289 файлів / 2.9 MB готовий артефакт. (docs/scripts/build-ai-bundle.mjs)
- CI gate ai:check — Django command check_ai_bundle. Walking EntityRegistry, перевіряє наявність entities.md (relaxed) або обох entities.md + entities.generated.md (strict). Exit non-zero коли відсутнє. Exposed через npm wrappers ai:check, ai:check:strict. (backend/core/management/commands/check_ai_bundle.py)
Спільний workflow:
npm run ai:export # regenerate всі *.generated.md
npm run ai:bundle # пакує у dist/
npm run ai:check # ворота у CI
Час на bundle-update після цього: ~30 секунд npm run ai:export && npm run ai:bundle замість 6+ годин ручної POC iteration. Spec-as-you-go тепер фізично можливий — discipline-frame був недостатнім без інструментів.
2026-04-25 — Multi-CoA / Parallel Ledgers (Phase 0-9)¶
Реалізовано як SAP-style: модель Ledger per-tenant, BusinessOperationTemplate(business_operation, standard, debit/credit), post_accounting_entry робить ledger-loop, всі звіти ledger-aware, UA-NSBO COA + 53 BO templates, IFRS scaffolding, mirror_postings_to_standard, verify_postings L1/L2/L3 + ReconciliationRun, source_signature drift detection. Frontend: окрема секція «Бухгалтерія» з структурою Master Data / Transaction Data / Reports per standard; групи звітів автоприховуються коли стандарт неактивний (requiresStandard + useActiveStandards); налаштування адону винесено в /settings → Бухгалтерія (адон); subtable «Шаблони проводок» у формі BO. Демо: 2098 PostingGroup на 2 контурах, баланс UA сходиться 0E-10. Деталі: planning/multi-coa-implementation.md.
2026-04-25 — Універсальна друкована форма для будь-якого документа¶
UniversalPrintContract (backend/core/print/universal.py) автогенерує контекст із метаданих будь-якого TransactionModel: header (number/date/state/status), parties (organization/client/supplier/counterparty/warehouse/department/currency), meta_fields (плоскі скаляри + FK), lines (перший _subtables), totals (total_amount/total_gross/vat_amount/paid_amount), org_branding. Резолвер: EntityRegistry → Django app registry (моделі поза EntityRegistry теж знайдуться). Шаблон universal/{base,ua}.html — adaptive layout. Frontend printDocumentSafe(entityCode, recordId, lang) (lib/printPdf.ts) з camelCase→kebab автомапінгом; усі 9 ListToolbar + 7 Form переключено, legacy URL-навігація ${basePath}/print?docId=X повністю видалена. Master data: кнопка прихована за замовчуванням (universal на довідники не поширюється). Skill custom-form + new-print-template оновлено з новими конвенціями і виявленими багами (порядок хуків, useParams деструктуризація, обробка recordId === 'print' у Page-компонентах, guard chained access у Jinja). Деталі: planning/reports-and-print-strategy.md § 2 Фаза 2 — Розширення 2026-04-25.
2026-04-?? — Three-layer docs Фаза 2 (POC validation)¶
Виконано через 3 ітерації порту essentials/invoices → .NET 9 + EF Core + Blazor + MudBlazor. R1: 12 gaps + 11 build/runtime errors. R2: 8 gaps + 0 errors. R3: full master data + tab navigation + browser dev mode. Bundle артефакт у dist/dop-port-bundle/ — 32 файли, 348 KB. Архітектурні стаби: multi-tenancy (з global catalog exception + persistent connections + subtable filter), posting-model, multi-coa-parallel-ledgers (з tenancy summary), workflow-state-machine (з eventing patterns), numbering, tab-based-navigation. Target adapter targets/port-to-dotnet.md з усіма R1-R2 пастками. Master data домен (clients, organizations, items, contracts + simple-references). Деталі і метрики — planning/three-layer-docs-strategy.md § Фаза 2.
2026-04-24 — Report Framework + Print Framework (великий день)¶
Report Framework (Фаза 1 strategy-doc):
- core/reports/ — повний framework: definition / runner / registry / drilldown / exporter (openpyxl).
- 6 мігрованих звітів: trial-balance, profit-loss, balance-sheet, cash-flow, partner-turnover (новий dimension breakdown), pnl-drilldown.
- Multi-standard foundation (Option C): standards.py, resolve_standard(), supported_standards у ReportDefinition, окремий endpoint /reports/standards/.
- Drill-down — окрема сторінка у вкладці (не drawer).
- Excel-export серверний — з вистраиваним outline, живими =SUM() формулами, bold+border на totals.
- Клієнтський xlsx у drill-down сторінці + reports framework xlsx.
- UI: <Report /> компонент з EntityWorkspace shell, фільтри + icon-only buttons (Refresh/Excel/Print), lang-aware labels (UA/EN).
- 17 smoke-тестів test_reports.py → зелені.
- Skill: .claude/skills/new-report/.
- Видалено legacy: ProfitLossReportView, BalanceSheetView, CashFlowReportView, TrialBalanceView, ProfitLossDrillDownView (reports.py 1595 → 580 рядків).
Print Framework (Фаза 2 strategy-doc):
- core/print/ — engine (Jinja2 sandbox + Playwright headless Chromium), resolver (tenant→locale→base), registry, QR (segno), media helpers (data-URL + ПІБ скорочення).
- 3 PDF-контракти + шаблони: Invoice, GoodsShipment, Waybill. Всі з QR-кодом для warehouse-сценарію (клієнт сканує → відкривається документ через ScanRedirectPage).
- Waybill оновлений до повної відповідності 7 вимогам первинного документа (Закон про бухоблік): назва, дата, підприємство, зміст+обсяг операції, одиниці виміру, посади+ПІБ відповідальних, місця для підпису.
- Universal list print: shared/ListPrintV2.tsx + backend /api/v1/print-list/<entity>/ — модалка з вибором колонок, серверний PDF + xlsx.
- Organization branding: ImageField × 3 (logo, director_signature, accountant_signature) — мігровано, вкладка «Брендинг та підписи» у формі з upload/preview/remove, auto-підстановка в усі 3 print-шаблони (position: absolute як «за текстом» у Word).
- Skill: .claude/skills/new-print-template/.
- Fortune Sheet повністю видалений: 9 файлів, @fortune-sheet/react з dependencies, public/templates/, PrintTemplate тип.
UX patterns (переносні):
- useDocumentSidePanels() hook — shared/DocumentSidePanels.tsx. Винос «Прикріплені файли» + «Історія змін» з вкладок форми в іконки toolbar з модалками. Застосовано в GoodsReceipt.
- useDebouncedValue(400) для text-filter полів у звітах — уникає refetch на кожен keystroke.
Language / naming:
- Масова заміна калькові «путевий/путівний» → правильне «подорожній» (65 замін, 16 файлів).
- Lang-aware pattern у print framework: обидва поля (label + label_ua) у контексті, resolver вибирає за lang query param; helper localized_name(obj, lang) і person_short_name().
Perf: - Діагностовано «лаг» у звітах — refetch-on-keystroke, не shell. Виправлено.