Перейти до змісту

Архів виконаного

Журнал закритих тасок і сесій. Найновіші — зверху. Активний backlog у todo.md, відкладені ідеї в BACKLOG.md.

2026-05

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 з типом у invalidateOnqueryClient.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.pyOpeningBalancePeriod + 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.pypost_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.pyOpeningBalancePeriodSerializer (nested account_lines + suspense_balance + section_counts + posted-edit block per .claude/rules/document-posting.md). - essentials/views/opening_balance.pyOpeningBalancePeriodViewSet з action endpoints (handlers/ filtered to period-aware only, post_document/, unpost_document/). - essentials/urls.pyopening-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.pyESWF_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_terminalshipcore_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_terminalshipcore_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.containershipcore_containers - rail × 2: RailwayWaybillWagon.container / TrainManifest.containershipcore_containers.Container - sea × 1: OceanBookingEquipment.containershipcore_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_terminalshipcore_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_terminalshipcore_cargo_ops (RolePermissions виживають). - Cross-app FK updates × 9 (state-only AlterField, DB columns physically unchanged): - Container.current_zoneshipcore_cargo_ops.YardZone - ContainerSeal.gate_transaction, ContainerBooking.gate_transaction, DemurrageCalculation.gate_in, DemurrageCalculation.gate_outshipcore_cargo_ops.GateTransaction - ContainerLedger.zone, ContainerMovement.from_zone, ContainerMovement.to_zoneshipcore_cargo_ops.YardZone - TrainManifest.assigned_zoneshipcore_cargo_ops.YardZoneshipcore_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_terminalshipcore_rail через app-rename-pattern (Phase 4 recipe). Frontend skeleton + AI bundle.

Scope: - BackendRailwayWaybill + RailwayWaybillWagon + TrainArrival + TrainManifest переїхали у backend/shipcore_rail/. DB tables зберегли containerhub_* назви через Meta.db_table. ContentType relabel shipcore_terminalshipcore_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. - EntityRegistryrailwayWaybills + trainArrivals мігровані разом з моделями у shipcore_rail/apps.py. Keys stable per ADR Phase 4 § C.3. - PrintRegistryRailwayWaybillContract + 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. - Frontendfrontend/dop/rail/config.ts (1 section, 2 groups, 5 items) + applications.ts entry appShipcoreRail + sections.ts wiring + seed_shop App Store product. - AI bundledocs/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 9c0a52bshipcore.TransportUnit + Location abstracts + Carrier + CarrierIdentifier + CarrierEquipment (3 EntityRegistry keys). - Day-3 22d9065shipcore.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 ad889e4DeprecationHeaderMiddleware 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 922e9e4shipcore.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 766e151shipcore_sea.Voyage (TenantAwareModel, scheduling lifecycle) + VesselSchedule (11-source enum, idempotency через external_id) + Vessel/Port EntityRegistry catch-up. - Day-3 72b6582OceanBooking (TransactionModel + sea-specific status workflow) + OceanBookingEquipment (DCSA VGM 1.0 block — replaces legacy VERMAS EDIFACT). - Day-4 94c5d1cBillOfLading paper variant з master/house parent_master FK + 3 legal varianti (original/express_release/sea_waybill) + eBL fields reserved для Phase 8. - Day-5 c815044CarrierAdapter 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 4ac26f1demurrage_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 9180d63frontend/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 6b9d436seed_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'ів. S2Client.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. S6ClientList.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 > 0ItemPrice з 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 (fuelRecordsfuel_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. Закрито пакетом:

  • MasterDataListSettings modal (новий shared компонент) — відкривається з overflow-меню (⋯ → Settings). 6 vertical Tabs (Загальні / Сортування / Фільтри / Колонки / Оформлення / Групування). Active state — лише жирним шрифтом без pill-bg; counts у tab rightSection як 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-joined ordering=field1,-field2 через глобальний OrderingFilter.
  • Filter chain: FilterStep[] spread як raw query params на API endpoint. DjangoFilterBackend (глобальний) приймає те що в filterset_fields per-viewset, решту тихо ігнорує. Value-пікер типізований через FilterFieldDef discriminated 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 читанням legacy sort:{basePath} shape), list-filters:{basePath}, list-scroll:{basePath} (throttle 250ms, best-effort restore через requestAnimationFrame).
  • Column visibility: hiddenColumns: string[] per-list sessionStorage; status non-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.pykanban групує за 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().activeTabIduseLocation().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-updater systemd-сервіс для 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.pypytest.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_rlsALTER 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.pyapply_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. Виправлено.