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

План проекту ESWF / DOP

Архітектура, tech stack, структура модулів — у ../CLAUDE.md та dop/spec.md. Цей файл — лише активний backlog. Виконане → done.md. Відкладене з тригерами → BACKLOG.md.

Як читати backlog

Класифікація задач для autonomous mode (деталі — ../CLAUDE.md § Autonomous task execution): - 🤖 auto — Claude виконує самостійно від плану до коміту, дрібні розвилки → ADR. Береться у batch-режимі. - 🧭 review — Claude виконує, але стоп після коміту. Скіпається у batch. - 👤 decision — Claude НЕ пише код, готує ADR-пропозицію з варіантами. Скіпається у batch. - без маркера — лише в інтерактивній сесії, не запускати автономно.

Класифікація задач по треках (memory three-tracks-discipline, three-tracks-definition): - [ESWF] — framework: core/, form-shell, common/fields/list/toolbar, skills, AFP framework - [ESS] — Essentials horizontal ERP: essentials/, accounting/, crm/, hrm/, production/, plugins - [SHC] — ShipCore vertical: fleet/, logistic/, shipcore/, mobile/, compliance - (+TAG) — spillover, додатково зачеплений трек

Пріоритет (memory feedback_priority_criteria — порядок: архітектурна правильність → рівномірний розвиток треків → відповідність західним ERP стандартам): - P0 — блокує architectural completeness або працюючий pattern зламано. Беремо першим. - P1 — завершує нещодавно landed pattern, заповнює відомий gap у коді, тех-борг від MVP. Активна черга. - P2 — completeness/uniformity без архітектурного blocker'а. YAGNI поки не зробимо shape. Беремо коли P0/P1 спорожніли.

НЕ використовуємо як тригер активації (memory feedback_no_client_triggers, feedback_priority_criteria): - ❌ «коли клієнт X підпише», «коли pilot активує», «коли operator-side запит» - ❌ «pilot signal на конкретний document type / dashboard / report» - ❌ «при потребі демо для pilot client»

Валідні тригери реактивації: - ✅ «хочу зараз» / «потім» від користувача - ✅ Architectural prerequisite landed (e.g. «Phase 4 refactor closed → Phase 7 pricing concept unblocked») - ✅ Real external dependency resolved (e.g. Maersk creds in hand, УЗ B2B-договір підписаний) - ✅ Regulatory deadline approaching (eTTN 2027-01-01, eFTI 2027-07-09) - ✅ Data-driven threshold crossed (e.g. «коли unmapped Group MDM entries > 1000»)

При першому «далі»/«продовжимо»/«наступна» у сесії — Claude робить git lookback по треках за 14 днів і пропонує сплячий трек.

Backlog-grooming gap — частково закрито 2026-06-06. Нотатка 2026-06-04 фіксувала що P1+P2 не має 🤖-actionable айтемів. Grooming-сесія 2026-06-06 виявила, що причина була не брак роботи, а stale-беклог + promotion lag: (1) кілька ESS [shaped] пунктів насправді вже зроблені (subtable-міграція, drilldown — як LocalizedTextField 2026-06-05), вони створювали ілюзію «є робота, але вся 👤»; (2) Document approval Phase 2 уже був shaped «ready 🤖» у long-term-ess.md § 3, але ніколи не потрапив у чергу — тепер промоутнутий у P1 (вище). Урок: перед «черга порожня → грумити нові ідеї» спершу audit-first звірити наявні [shaped] з кодом — половина вже done. Залишковий gap: після Phase 2 черга знову проріджається — наступний refill з тих самих long-term доків.

Re-audit 2026-06-09 (обидва кандидати закрито): (1) bulk-operations — зроблено 2026-06-09 (ESWF kernel primitive + поле bulk-edit, 2 commits, P2 вище). (2) ПКО/РКО print-формивже були реалізовані у cash-bank S6a (commit 8d2126e2): IncomingPaymentContract/OutgoingPaymentContract рендерять КО-1/КО-2 legal-форми (3-гілковий шаблон UA+cash→КО-1/КО-2 з відривною квитанцією + кореспонденцією, UA+bank→0410001, else→generic), зареєстровані в essentials/apps.py, runtime-резолв ✅. Черговий доказ grooming-уроку: stale-кандидат, давно done. Наступний refill — лише через свіжий read long-term-eswf/ess + audit-first, не з цього (вичерпаного) списку кандидатів.


🟢 P1 — активна черга

[SHC] Logistic — оптимайзер: редагування джерел/транспорту прямо в калькуляторі (Фаза 2) 🧭

Повний промт для нової сесії: planning/grainflow-optimizer-editing-phase2-2026-07-14.md. Модель обрана користувачем 2026-07-14: A — обмеження (pins) + переоптимізація (переви користовує «замки»). Фаза 1 (видимість залишків джерел) ✅ зроблена 2026-07-14sources_summary (всього/використано/залишок) + тултіп + таблиця в калькуляторі; Хмельницький 20k вільних як тест-кандидат. ✅ Фаза 2 виконана 2026-07-14 (порядок 2b→2c за вибором користувача; pins→is_locked на save; панель обмежень: так). 2a бекенд: calculate_grainflow(pins=)_parse_pins матеріалізує transient-pins через price_pinned_row у locked-like псевдорядки, _collect_matrix(pin_rows_by_plan, excludes_by_plan) знімає supply/demand як замки / робить exclude заборону (cost=None); save=true → pins стають is_locked рядками (_apply, lock_reason «закріплено в оптимайзері»); хелпер optimizer.price_leg + ендпоінт POST /logistic/lot-plans/price-leg/; sources_summary дістав lat/lon. 2b фронт: колонка «Обмеження» в таблиці рішень (мода-Select→pin / «виключити»→exclude / «зняти»), RedirectPopover на рядках залишків (план+мода з ₴/т-підказкою+обсяг), панель активних обмежень зі «зняти» + «Перерахувати з обмеженнями» (pinsSig/appliedSig staleness). 2c drag-drop: FlowMap дістав opt-in editMode/extraSources/onLink (SVG pointer-drag, screen→viewBox через getScreenCTM, hit-test споживача, ghost-лінія, dashed pool-0 вузли невикористаних джерел), кнопка «Редагувати на карті» + DropLegModal (мода+обсяг). Verify: control savings 6 085 509 ₴ незмінний; pin/exclude/save/price-leg через сервіс+view; 5 нових pytest (test_optimizer); tsc 0; check чисто. Bundle: ai/domains/logistic/grainflow.md.

[SHC] Logistic — розклад виконання лота проти пропускної здатності портового елеватора 👤

ADR-пропозиція: ai/architecture/adr-2026-07-15-lot-execution-scheduling.md (status proposed — чекає відповідей клієнта). Походження: 7-ме зауваження клієнта (2026-07-15) — «алгоритм має враховувати, чи готовий отримувач прийняти всю партію». Первинне тлумачення «обмежити план закупок ємністю елеватора» — ВІДХИЛЕНО (судно порожнє, готове прийняти все; вартість sourcing'у від пропускної здатності не залежить; GrainFlow LP не чіпати). Правильна постановка: зерно не завжди перевалюється напряму вагон/авто→трюм — частина потребує підробки в портовому елеваторі (підсушка/очистка/фумігація), і саме там є пропускна здатність (скільки й КОЛИ приймає). Це задача розкладу/послідовності (задача-3 класу ADR ядра), execution-шар, НЕ transportation-LP. Модель: портовий елеватор = конвеєр із вузьким місцем — прийом (t/добу) → підробка (t/добу + dwell, фумігація тримає партію N діб) → буфер (вільна ємність) → навантаження на судно (t/год, обмежене laycan+demurrage). Що є: документи ФАКТУ (GrainDrying/Cleaning/Fumigation, ShrinkageJournal, Silo.capacity/fill, SOF/laytime/demurrage). Чого немає: планового шару ТЕМПІВ і ЧАСУ (rates, dwell, графік прибуття ТЗ, laycan-як-обмеження, зарезервована ємність під лот). Прапорці клієнта переосмислені: «дробити партію чи ні» = цілісність технологічної партії (фумігована опломбована на dwell); «немає сенсу везти 200 т» = мінімальна партія на операцію. Обидва — processing-рівень, не sourcing. Рекомендація: MVP = перевірка здійсненності темпу + таймлайн (Gantt), людина рулить (патерн transport_planner own/hire+bottleneck); повний оптимізатор розкладу (MILP/CP-SAT) — окремий трек пізніше. ГЕЙТ — 3 питання клієнту перед кодом: (1) елеватор наш чи третьої сторони? (2) головна мета — не платити demurrage / не переповнити елеватор / вкластися в laycan? (3) підробка для частини зерна чи для всього лота?

[SHC] GrainFlow Stage 2 — наймане авто / вагонний движок у ЖД / еТТН / бухгалтерія (грумінг 2026-07-12)

Контекст: після передачі тенанта grainflow логісту користувач замовив 4 треки. Повний план з розвідкою кодової бази: planning/grainflow-stage2-plan-2026-07-12.md. Рекомендований порядок: 5 → 1 → 4 → 3 → 2 (сумарно ~8 дн). Маркери (🤖/🧭/👤) не розставлені — класифікація за користувачем.

  • [x] Етап 5 — Бухгалтерія + P&L по контракту ✅ 2026-07-12 — appAccounting активний; управлінський = перейменований pcg_eur («Управлінський облік (PCG)»); фільтр contract у profit_loss + drill-down + вимір contract у pnl_drilldown + фронт-пікер (ReportFilterKind 'contract' на обох P&L-обгортках). 2 БАГФІКСИ: подвійна FX-конвертація (functional-сума + валюта документа у post_accounting_entry — USD-інвойс GASC роздувався до 37.97 млрд) і JE ledger=None (ручні проводки невидимі для standard-звітів — тепер стемп за стандартом рахунків). JournalEntryLine.contract (migr 0101); voyage-pack: Contract-дзеркала + invoice.contract + по-плановий COGS з факт-собівартості fulfilments в ОБИДВА контури; export-finance JE-дзеркала per-standard. Контроль: контури копійка-в-копійку 182,978,352; P&L по EXP-ZP-C02 = 312М − 278.45М = 33.55М валового.
  • [x] Етап 1 — Наймане авто ✅ 2026-07-12 — ТТН для hired-легів (перевізник з наряду, авто = hired-картка round-robin через _next_hired_vehicle, cleanup за ttn_hired); БАГФІКС plan_transport знищував тендерні закріплення при переплануванні (перенос за natural key mode+origin_name); гард путьових проти hired у серіалізаторі; сід 2 авто/перевізника + пост-тендерний стан; колонка «Належність» у списку авто. 17 ТТН всі з перевізником і авто.
  • [x] Етап 4 — еТТН ✅ 2026-07-12 — appeTTN у whitelist; seed_grain_ettn (gf_ettn): реєстрація + справжні Ed25519-підписи через edo.SigningKey per-ЄДРПОУ — 6 confirmed (виконані) + 7 carrier_signed (у дорозі) + 4 submitted, verify всі valid; пункт е-ТТН у grain-trader § Виконання.
  • [x] Етап 3 — Вагонний движок у ЖД ✅ 2026-07-12 — tenders-style cutover: моделі → shipcore_rail (нові таблиці, task_ref soft-ref, logistic/0027 + rail/0011); НОВА WagonDemandLine (ручна потреба станція×вагони×дедлайн = самостійна задача ЖД, adapter-лінії матеріалізуються compute'ом); registry wagon_demand_registry + logistic TransportPlanWagonDemandAdapter (guarded); планер у rail з soft-bridge до RouteContext; фронт → @dop/rail + пункт у railSection (сабтейбли Потреба/Наряди) + editable WagonDemandLines; grain-trader/logistic запозичують. 21/21 test_optimizer; WD 78/185 + 11 demand-ліній на свіжому тенанті.
  • [x] Етап 2 — Акт перевізника (carrier-side MVP ТЕД) ✅ 2026-07-12 — ExpeditionAct/Line (logistic/0028): рядки = виконані наймані плечі (ТТН, тонни, ставка vs договірна тендерна); пост → 3-way match → PurchaseInvoice перевізнику (SRV-FREIGHT-ROAD service-item, без стоку) → кредиторка; build-lines/post/unpost екшни + меню (logistic + grain-trader); сід gf_carrier_acts: АП0000001/2 (15,000 т + 28,534.92 т = 16.1М ₴, matched). Горизонтальна фаза 1a ТЕД свідомо не зачеплена — повний ТЕД за своїм треком.

Виконано повністю 2026-07-12 (7 комітів, тенант пересіяний з нуля pk=8, 21 фаза, 11 ліцензій). Session log: ai/sessions/2026-07-12-autonomous-log.md § Stage 2.

[SHC] Logistic — зауваження клієнта з тестової демонстрації 2026-07-10 🧭

Контекст: тестова демонстрація калькулятора зернотрейдера клієнту → 6 зауважень. Зведення для узгодження з клієнтом: planning/demo-remarks-client-2026-07-10.md. Порядок затверджено користувачем у сесії 2026-07-10. Кожен пункт = 1 коміт, стоп після коміту (🧭).

  • [x] 🧭 1. «Замки» на рядках плану (зауваж. 2+3: фіксація обсягу/джерела/судна + примусова мода) ✅ 2026-07-10 — CollectionOrder.is_locked/lock_reason (міграція logistic/0021), препроцесинг у greedy + GrainFlow (тонни замків знімаються з demand/supply ДО розв'язання, солвер optimize.flows незмінний), price_pinned_row (заморозка вартості на момент фіксації, примусова мода через mode_costs), екшни pin-candidates/lock-order/add-pinned-order (guard на posted), фронт: колонка-замок + CollectionOrderLockModal + «Додати фіксацію» + помаранчева підсвітка, 🔒 у GrainFlow-таблиці. Перевірено: 101 млн ₴ контроль незмінний (59.69 пшениця + 41.32 кукурудза), 17/17 test_optimizer, tsc 0.
  • [x] 🧭 2. Довідник вивірених маршрутів (зауваж. 4) ✅ 2026-07-10 — VerifiedRoute (міграція logistic/0022): координатні кінці + мітки, mode road/rail (rail = тарифні відстані УЗ → у RailFreightTariff замість авто-апроксимації), effective dating (кейс моста: зміна дороги = новий рядок з періодом дії), source manual/osrm/gps_track. Драбина: VerifiedRoute (чинний на дату плану) → haversine×detour → legacy; матчинг за близькістю кінців (5 км, симетрично A↔B), RouteContext-кеш на прогін оптимізатора. OSRM свідомо НЕ в рантаймі (повільно/недетерміновано) — наповнює довідник офлайн командою fill_verified_routes --tenant. Довідник у меню логістики (default MASTERDATA форма). Перевірено: кейс моста наскрізно (geo 11.7 → verified 92 → об'їзд 147 на дату плану), 101 млн контроль незмінний, 24/24 тестів, tsc 0.
  • [x] 🧭 3a. Вхідний тендер TradeTender (зауваж. 1, хоп 0) ✅ 2026-07-10 — TradeTender + TradeTenderLot (міграція logistic/0023, номери TN…, статуси draft→submitted→won/lost, БЕЗ облікової природи — state інертний). What-if прайсинг: лоти → draft-плани (носії параметрів TN…-L#) → calculate_grainflow(save=False) ОДНІЄЮ задачею (лоти конкурують за джерела), нічого не матеріалізується; маржа чесно на покритий обсяг (дефіцит = прапор, не маржа). Подання вимагає розрахунку. Won → чернетки GrainContract (side=sell) по лотах + contract_ref на план/лот; lost → порожні draft-плани прибираються. Повний custom-form стек (Page/Form/Fields/Lots+модалка/List/Toolbar за UI-стандартом TransportOrder), група «Тендери» в конфізі, ролі forwarder/_crud + auditor/_readonly (apply_system_roles на tenant 3). Перевірено: 16/16 тестів (4 нових), живий HTTP-цикл create→lots→price→submit→won (маржа 119.9 млн ₴, 29.4%, 2 контракти) з rollback, 101 млн контроль незмінний, tsc 0.
  • [x] 🧭 3b. Вихідний тендер FreightTender на найм авто (зауваж. 6) ✅ 2026-07-10 — FreightTender+Lot+Bid + TransportTask.carrier/awarded_rate_per_ton (міграція logistic/0024, номери FT…, статуси draft→collecting→awarded, без облікової природи). Лоти ідемпотентно з hired road-легів плану розвозу (стеля = планова ставка × км; перезбирання з пропозиціями заборонене). Пропозиції = FK FreightContractor (role=carrier). Частковий авард по машинах (лот ділиться між перевізниками); фіналізація: покриті тонни за середньозваженою договірною ставкою, недобір чесно за стелею; write-back: TransportTask.carrier + awarded_rate + план.hire_cost_estimate. Кнопка «Тендер на найм» на плані розвозу (активна при trucks_hired>0) → створює і відкриває тендер. Повний UI-стек (лоти/пропозиції/авард-модалка/List). Перевірено: 19/19 тестів (3 нових), живий цикл на плані 1 tenant 3 (2 лоти × 9 500 т: план 1 700 500 ₴ → договірна 1 496 440 ₴, економія 204 060 ₴, write-back ✓) з rollback, 101 млн контроль незмінний, tsc 0. Scope cut: ТТН для найманих легів (авто перевізника невідомі), вагонна оренда (mode закладений), P1 ТЕД 3-way match.
  • [x] 🧭 4. Вагонний Канторович — подача порожняка (зауваж. 5 + часове уточнення клієнта) ✅ 2026-07-10 — WagonDispatchPlan+Order (міграція logistic/0025, номери WD…, без облікової природи). Часова модель: парк = порожні на станціях + порожні у поверненні (готові після дороги) + завантажені (після дороги + нормативу вигрузки, потрібен direction_to); пара допустима лише якщо прибуття ≤ window_to плану — хто не встигає, заборонена пара → чесний дефіцит. Нормативи на плані (швидкість УЗ км/добу, вигрузка діб; дефолти LOGISTIC_). Км порожняка: verified rail → haversine×1.2. Demand: rail-леги активних планів (станція: silo→site.nearest_rail_station → fallback ім'я Site → чесний пропуск зовнішніх). Бенчмарк — наївне пер-легове призначення (Decimal-квантування як в оптимуму — float-епсилон ловився тестом). seed_wagon_dislocation --tenant розкидає порожні по станціях. Повний UI-стек + пункт «Подача вагонів» у планах вантажопотоків. Перевірено: 21/21 тестів (2 нових: часовий пул, дедлайн-гейт), живий план WD0000001 на tenant 3: 90/118 ваг (дефіцит 28 чесний), 8 534 ваг·км оптимум проти 16 911 наївних (−49.5%)*, 101 млн контроль незмінний, tsc 0. Фаза 2 (deferred): конкретні номери вагонів + заявки/ЗН на подачу.
  • [x] 🧭 4c. Митна декларація (МД / ЕК-10) ✅ 2026-07-12 — питання клієнта «документи відходу судна». Audit-first: export-pack родина ВЖЕ існувала (фіто/радіо/походження на GrainDispatch + readiness endpoint) — МД доданий як її четвертий член, але standalone (якір = GrainContract + опційні voyage/BoL, статуси митні: draft → submitted → cleared/rejected → resubmit; voided). CustomsDeclaration (міграція shipcore_grain/0021, номери МД…, БЕЗ облікової природи — 0% ПДВ експорт + вивізного мита немає, чекпоінт A.7 зафіксовано в docstring). Guards: подача вимагає контракту+нетто, пропуск — № МД митниці; дати штампуються. Повний стек: serializer edit-lock (rejected редагований для resubmit) + viewset (form-data/refs, post=подати, unpost=анулювати, advance_status=рішення митниці) + EntityRegistry + ролі terminal_dispatcher + фронт на grain-doc двигуні (Config/Form/Page, кнопки «Пропуск»/«Відмова» у submitted) + сід voyage-pack (3 cleared + 2 submitted, purge за маркером). Перевірено: 57/57 grain-тестів (7 нових), tsc 0, контроль voyage-pack інвойсів 931М (315+312+304), C02 312М posted. ⚠️ Контроль P&L оновлено: 166,870,431.44 обидва контури (старі 182,978,352 були виміряні ДО посту актів перевізників: −16,107,920.40 AP = точна дельта; моя зміна проводок не додає). Scope cuts: АСМО/M.E.Doc подання (немає API), multi-item позиції МД, розширення export-pack readiness endpoint на МД (він dispatch-anchored), кастомний print-бланк; AI УКТЗЕД-підбір — розглянуто, відкладено.
  • [x] 🧭 4b. Вагонний трек Phase 2 — конкретні номери + ЗН подачі ✅ 2026-07-12 — (а) наряд подачі отримує поіменний зріз групи дислокації (WagonDispatchOrder.assigned_wagons JSON, патерн TransportTask.assigned_units); пул чесно виключає закріплені деінде вагони (вихідні draft-ЗН — семантика complete_wagons — + наряди інших планів in_execution), нотатка «закріплені деінде: N ваг» у planner_notes; (б) generate_dispatch_waybills — draft-ЗН порожнього рейсу на кожен наряд (нове поле RailwayWaybill.is_empty_run; пост рухає дислокацію у empty_in_transit до станції навантаження, не loaded — гілка у wagon_tracking), consignor_org → direction='out' → draft сам резервує свої вагони від complete_wagons інших документів; ідемпотентна регенерація за FK+маркером [auto:подача]; проведена ЗН = факт: skip при регенерації, перерахунок блокується («переплан не знищує факт»); (в) екшн generate-waybills + кнопка «Сформувати ЗН» + колонки «Номери вагонів»/«ЗН подачі» у нарядах; (г) міграція shipcore_rail/0012. Перевірено: 44/44 logistic+rail (4 нових: номери/генерація/резервації/факт-заморозка), GrainFlow WD0000001 контроль 78/185 точний (резервацій на тенанті 0 — пул не зсунувся), 78 поіменних унікальних, live-генерація 28 ЗН з rollback, tsc 0. Scope cuts: подача-ЗН не сідуються (демо показує кнопкою); WagonOperationLog (unpost ЗН стирає сідовану дислокацію) — і далі відкладений; заявка ГУ-12 окремим документом не моделювалась (draft-ЗН покриває MVP).

Після зауважень — повернення до P0 постинг виконання (рух зерна по залишках + факт-собівартість, див. memory grain-calculator-readiness).

[SHC] Logistic — калькулятор зернотрейдера: вихід у продукт (грумінг 2026-07-11, ПЕРШОЧЕРГОВО — вище ТЕД)

Контекст: після закриття хопа 8 користувач вніс 5 першочергових задач (сесія 2026-07-11). Три архітектурні розвилки узгоджені: (1) тендерний engine витягається ЗАРАЗ з міграцією TradeTender/FreightTender на нього (дані лише demo — міграція дешева, поки без клієнтського легасі); (2) зовнішні біди у магазині — тільки зареєстровані CustomerProfile; (3) дозьє контракту — варіант А: read-only агрегатор по наявних refs. ADR: ai/architecture/adr-2026-07-11-tenders-engine-extraction.md. Порядок: швидкі видимі виграші (секція + звіти + дозьє) → структурний engine. Кожен пункт = 1 коміт.

  • [x] 🤖 1. Секція «Калькулятор зернотрейдера» у бічному меню ✅ 2026-07-11 — showcase-секція grainTrader за preset-рецептом (3-й доведений випадок після Stevedoring/ContainerHub): frontend/dop/grain-trader/config.ts — 6 груп (Тендери / Планування з GrainFlow-процесом / Зерно / Вагони з дислокацією і тарифом УЗ / Виконання ВЗ+ЗН / Звіти), custom-component items імпортовані напряму (lazy для крос-модульних), default-form items з module: override (logistic/shipcoreGrain/shipcoreRail); app-карта appGrainTrader (requires logistic+grain+rail, zero own models, іконка WheatOutlined); seed_shop PRODUCTS запис + AppEntry синхронізовано + License активована на tenant 3; section-landing help grain-trader.md+.ua.md (ланцюг угоди 6 кроків). tsc 0. Звіти задач 2-3 додаються у групу «Звіти» цієї секції їхніми комітами.
  • [x] 🤖 2. Звіт «Доступне зерно для тендерів» ✅ 2026-07-11 (skill new-report) — logistic/reports/available_grain.py (code available-grain): секції по commodity_class, рядки = силос×власність (відкриті OwnershipLot через grain_bridge, депоненти згорнуті) + чинні GrainSupplyOffer; станція silo→site→nearest_rail_station; ₴/т = зважена batch.purchase_price / price_per_ton; два grand-totals — «Разом в обох пулах» і «Доступно для тендерів» (custody/consignment показані, але чесно виключені — оперувати не можна); фільтр commodity_class на API-рівні (UI — групування по класах). Меню: logistic Analytics + grainTrader Звіти. Verify: live tenant 3 — 32 рядки (45.2К barley + 115К corn + 182.2К wheat = 342 382.993 т), фільтр wheat ok, permission smoke 200/403 по ролях, py_compile + tsc 0. Bundle: reports.md § available-grain. Scope cut: drill-down немає (джерела не ledger-регістри).
  • [x] 🤖 3. Звіт «Дислокація вагонів» ✅ 2026-07-11 — audit-first: звіт УЖЕ існував (shipcore_rail/reports/wagon_dislocation.py, list+map+stale-days) — розширено замість написання з нуля: (а) колонка «Прогноз прибуття» для рейсових (норма 250 км/добу; відстань verified rail через soft bridge до logistic → haversine×1.2; ETA у минулому = overdue-сигнал + лічильник); (б) колонка «Зайнятість» — бронь на вихідному draft-ЗН (семантика complete_wagons; прив'язка до наряду подачі свідомо ні — Phase 2 конкретних номерів deferred) / «Виконує перевезення» / «Повернення порожняком»; (в) багфікс «невидимий порожняк»: фільтр as_of викидав рядки з last_event_at=NULL — 40/90 вагонів (УВЕСЬ вільний парк) були відсутні у звіті. Фронт без змін (колонки динамічні). Verify: rollback-сценарій на tenant 3 — 90 рядків (було 60), reserved_count 14, ETA+overdue 10; pytest rail 7 passed. Bundle: shipcore_rail/wagon_dislocation/README.md § Розширення. Доповнення (фідбек «прочерки у колонках»): seed_wagon_dislocation переписано у повний снапшот трекера (source='csv_import'): статуси/дати/напрямки/власники детерміновано від pk — 104/104 рядки з датами й власниками, 46 рейсових з ETA (3 overdue навмисно), простій 1–45 діб за статусом; планер подачі на новій картині −49.7% порожнього пробігу.
  • [x] 🤖 4. Дозьє «Справа контракту» ✅ 2026-07-11 (узгоджений варіант А, нуль нових моделей) — logistic/services/contract_dossier.py + GET /logistic/contract-dossier/[{ref}/]: збирає по наявних ключах — лот-плани (contract_ref) → плани розвозу → ВЗ (маркер + fulfilment.dispatch_ref) / ЗН / ТТН (маркер) + прийомки (fulfilment.reception_ref, вхідні) + GrainSettlement (FK) + тендер-джерело (TradeTenderLot.contract_ref); totals target/collected/shipped; секції вихідні/вхідні, статуси, навігаційні refs. Фронт ContractDossierPage (пікер + KPI + 2 таблиці, клік → документ), зареєстрована у grainTrader § Виконання + logistic § shipment_execution; iconMap += FolderOpenOutlined. Verify: tenant 3 — пікер 8 контрактів, справа №1 = 22 док. (14 ВЗ + 3 ЗН + 4 ТТН + розрахунок), shipped 26 202.37 т; tsc 0, py_compile чисто. Bundle: ai/domains/logistic/contract-dossier.md. Scope cuts: рахунки — після P1 ТЕД (немає ключа essentials.Contract↔GrainContract), оплата (allocations) — там само; ручний attach — P2 binder-добавка.
  • [~] 🧭 5. Тендерний engine — horizontal plugin tenders (узгоджено: витягати зараз; патерн = approvals: engine + registry + консументи; skill new-module Checkpoint 0 → plugin, forModules=['essentials']). T1+T2 ✅ 2026-07-11: app backend/tenders/ (Tender state-інертний + TenderLot soft-ref/payload + TenderBid internal/portal + TenderAward частковий; migr 0001), registry register_tender_adapter (Protocol: build_lots/write_back, finalize викликає), API /api/v1/tenders/ (nested lots/bids + advance-status/award-bid/finalize-award/adapters), ролі (crud у forwarder-блоці, readonly auditor), картка appTenders (plugin на essentials) + seed_shop + License t3, секція «Тендери» на УНІВЕРСАЛЬНИХ формах (нуль кастомних компонентів = T2), демо-сід TD0000001 (закупівля дизпалива, 3 біди, частковий авард verified 30000 л @ 52.80) + TD0000002 (вхідний service). Verify: API-цикл + posted-guard 400 + permission 403 + tsc 0 + vitest 24/24. Bundle: ai/domains/tenders/README.md. T1b-freight ✅ 2026-07-11: адаптер logistic.freight_hire (build_lots з найманих легів + write_back зваженої ставки у TransportTask/план через мапінг Client→FreightContractor), кнопка «Тендер на найм» створює документ движка, демо FT змігровано (FT0000001→TD0000003, старі рядки видалені), менюпункти freightTenders → universal item; повний цикл verified у rollback (write-back перевізник+370.91 ₴/т+кошторис), контроль 101 009 460.88 ₴ точно, pytest 29, tsc 0. T1b-trade ✅ 2026-07-11 — cutover завершено: адаптер logistic.trade_sale (what-if прайсинг через adapter-action + хуки on_status: won→контракти, lost→cleanup, submit-guard), Tender.payload (migr tenders/0002), СТАРІ моделі Trade/FreightTender видалені (logistic migr 0026, сервіси/views/urls/ролі прибрані), кастомний UI трейдера портовано на движок (payload-mapper, refs-ендпоінт), FreightTenderHire UI видалено, 7 тестів портовано (21/21, test-БД перебудована --create-db). Verify: живий цикл price(маржа 16.8М, 29.26%)→submit→won→GrainContract у rollback; контроль 101 009 460.88 ₴ точно; suite 21+19 passed; tsc 0; vitest 24/24. Лишились фази:
  • T1 backend engine + міграція logistic: моделі Tender (direction in/out, kind purchase/sale/freight/service, state інертний — тендери НЕ проводяться), TenderLot (предмет = soft-ref <base>_ref+_label), TenderBid (учасник Client FK), частковий авард; registry register_tenderable(model, adapter) — адаптер дає стелю ціни, побудову лотів, write-back аварду. TradeTender/FreightTender → консументи engine (data-migration demo-tenant 3; кастомні UI логістики лишаються поверх). Контроль 101 009 460.88 ₴ після міграції обовʼязковий.
  • T2 генеричний UI тендерів — custom-form стек для «просто тендера» без домен-адаптера (інші предметні області без коду).
  • T3 shop-публікація вихідних тендерів — Client Portal: сторінка «Тендери», біди ТІЛЬКИ від зареєстрованих CustomerProfile (патерн TransportRequest: user FK + статуси), Kanban бідів для менеджера у commerce (патерн E-Commerce Manager). Публікація = явна дія «Опублікувати в портал» (не автоматична).

[SHC] Logistic — ТЕД фінансовий шар: розділення експедиторської винагороди 🧭

Промоутнуто 2026-07-08 — закриває gap, свідомо відкладений у ADR adr-2026-07-07-logistic-documents § «Що НЕ ввійшло у scope» («якщо колись знадобиться нарахування фрахтових витрат — окремий ADR»). Модуль планувався як мультимодальна логістика + експедирування; мультимодальність є, економіка експедирування (транзитні послуги сторонніх перевізників через облік компанії з виділенням винагороди у первинці) — відсутня повністю.

Аналіз + поетапний план: planning/logistic-forwarding-remuneration-2026-07-08.md. Рішення (ратифіковані користувачем): ADR adr-2026-07-08-forwarding-remuneration — (A) обидві облікові моделі через settlement_mode; (B) новий ExpeditionAct у logistic генерує essentials Invoice+PurchaseInvoice; (C) generic InvoiceLine.line_kind (торкає горизонталь → regression обов'язковий); (D) TaxRate «0% міжнар.» + mirror; (E) remuneration_type на контракті.

Чому 🧭 (стоп після коміту): фаза 1a/3 змінюють горизонтальний posting-контракт essentials (InvoiceLine.line_kind → маршрутизація проводки) — blast-radius на звичайні інвойси, потрібне ревʼю. Скіпається у batch; береться явним «зроби Logistic ТЕД» / «продовж фазу N».

  • [x] Фаза 0 — ADR ✅ 2026-07-08 (adr-2026-07-08-forwarding-remuneration, accepted; обґрунтування горизонтального торкання окремим підрозділом).
  • [ ] 🧭 Фаза 1a — горизонталь: InvoiceLine.line_kind ∈ {revenue, reimbursable, remuneration} (nullable, default revenue) + serializer + frontend line-editor + regression на звичайні інвойси + міграція essentials.
  • [ ] 🧭 Фаза 1b — домен logistic: VAT-поля FreightContractor; remuneration_type/value/basis на FreightContract; ShipmentLeg.tax_rate; сід TaxRate «0% міжнар.»; міграція плагіна.
  • [ ] 🧭 Фаза 2 — ExpeditionAct (skill custom-form): model+lines (settlement_mode, line_kind), serializer (+PostedGuard), viewset, urls, EntityRegistry, ролі, plugin-urls.
  • [ ] 🧭 Фаза 3 — проводки (обидві моделі): posting-routing за line_kind; BusinessOperations транзит/дохід/AP; post/unpost_expedition_act генерує Invoice+PurchaseInvoice; intermediary+gross гілки + ПДВ split; PartyLedger. (rule: document-posting).
  • [ ] 🧭 Фаза 4 — frontend (skill custom-form): ExpeditionAct Page/Form/Fields/Lines/List; поле «винагорода» у TransportOrder; KPI «Винагорода/Маржа» у футерах Shipment+TransportOrder; config.
  • [ ] 🧭 Фаза 5 — print (skill new-print-template): PrintContract Акт ТЕП + Рахунок + Звіт експедитора (net-new — logistic print-базису немає).
  • [ ] 🧭 Фаза 6 — reports (skill new-report): «Реєстр винагороди» + «Транзит vs винагорода» (замінити comingSoon freightCostAnalysis).
  • [ ] 🧭 Фаза 7: розширити seed_logistic_documents; тести ПДВ-розділення+проводок; bundle docs/ai/domains/logistic/.

MVP-цінність (розділення винагороди у первинці + посередницькі проводки) — наприкінці фази 4. Оцінка ~6–6.5 дн.

[SHC] Logistic — план закупівель → план розвозу (execution hand-off) 🧭

Контекст 2026-07-10 — readiness-аудит калькулятора зернотрейдера (перед демо клієнту) виявив головний розрив: оптимізатор давав лише плановий набір закупівель, а виконуваного плану розвозу з призначенням конкретного транспорту не було. Артефакт оцінки готовності — окрема сторінка (chain / demo-стратегія / roadmap).

  • [x] Крок 1 — призначення ресурсів ✅ 2026-07-10. Нова дія POST /logistic/lot-plans/{id}/build-delivery-plan/ + сервіс dispatch_planner.build_delivery_plan: надбудова над plan_transport, закріплює поіменно власні авто (держ-номери) і вагони (номери) за кожним плечем, overflow → найм/оренда, план → in_execution. Поля TransportTask.assigned_units/dispatch_from/dispatch_to + TransportPlan.dispatch_ready/dispatch_summary (міграція logistic/0018). Фронт: кнопка «Сформувати план розвозу» на формі LotAccumulationPlan → навігація на TransportPlan + колонка «Призначено» у нарядах. Перевірено наскрізно (HTTP + tsc 0).
  • [~] Крок 2 — документи виконання (P0): ✅ 2026-07-10 (майже) — build_delivery_plan драфтить fleet.Waybill (ТТН, 1/власне авто, з водієм — авто-driver або round-robin fallback) + shipcore_rail.RailwayWaybill (ЗН, 1/ЖД-плече, wagon_count+gross), state=draft, ідемпотентно (delete-and-recreate за doc_ref), plugin-safe. Окрема дія generate-documents + кнопка «Згенерувати ТТН/ЗН» на самому Плані перевезень (generate_documents_only — регенерація з поточного призначення). Doc-номери в колонці «Призначено» + нотифікації. station FK-резолв для ЗН ✅; GrainDispatch ✅ 2026-07-10 — draft ВЗ (відвантаження зерна) на кожне власне плече (batch з силоса, buyer з контракту, transport_doc_type rail/auto), state/status='draft'петлі план↔факт немає (auto_match шукає shipped, чернетки ігнорує; стають фактом після відвантаження). Ідемпотентно за маркером [auto:відвантаження], у savepoint (збій ВЗ не валить план розвозу). Крок 2 закрито повністю.
  • [x] Крок 3 — реальні дані ЖД (P0): ✅ 2026-07-10. Команда seed_rail_tariff --tenant — 5 hopper distance-band рядків RailFreightTariff (регресивна ₴/т·км × УЗ-індекс 1.10) + проставляє Site.nearest_rail_station (6 елеваторів) / Port.nearest_rail_station (7 портів) за найближчою RailwayStation (haversine). Фаза case_rail_tariff у seed_demo перед case_grain_lot_accumulation. Оптимізатор _rail_linehaul тепер резолвить реальний УЗ line-haul для власних силосів (зовнішні пропозиції — константи, немає station-link). GrainFlow-числа стабільні (тариф чіпає лише власні силоси; значення близькі до констант: 464 vs 420, 705 vs 692) — економія лишилась ~101 млн ₴.
  • [~] Крок 4 — вартісний own-vs-hire (P1): ✅ 2026-07-10 (частково) — крос-планове бронювання: _units_committed_elsewhere виключає з пулу власні авто/вагони, вже закріплені за ІНШИМИ планами розвозу → одна одиниця ≤1 плану, 0 подвійних бронювань (перевірено на 8 планах, 40 вагонів + 7 авто). де-хардкод норм ✅ 2026-07-10plan_transport бере середній payload авто / ємність вагона з реального флоту (available_road_units.avg_payload_t / avg_capacity_t), fallback на константу коли флот без даних; cycle/turnaround лишились константами (потрібні маршрути+графіки). Лишилось (не баг): явне порівняння opex власне-vs-найм — зараз fill-own-first, що вже cost-optimal (власне завжди дешевше за найм). Крок 4 практично закрито.
  • [x] Крок 6 — звіт план-факт виконання плану закупівель (P1): ✅ 2026-07-10. Перший звіт логістики на Report Framework — purchase-plan-fact (logistic/reports/purchase_plan_fact.py + реєстрація в apps.py + фронт-обгортка PurchasePlanFactReport + пункт purchasePlanFact з component: замість мок-ReportViewer). План (CollectionOrder) vs факт (нові поля CollectionOrderFulfilment.actual_*_cost_per_ton, міграція 0019, заповнюються в lot_fact_service). Показує зерно/перевезення/landed ₴/т план vs факт + Δ перевезення (реальний ЖД-тариф вище планового: rail ×1.08–1.14) + перевитрату ₴. Усе з реальних полів, не мок. Endpoint /api/v1/reports/purchase-plan-fact/ (+xlsx). Лишилось: факт-транспорт із реального акта фрахту (не варіаційна модель) — трек shipcore_rail_uz_cargo.
  • [x] Крок 5 — прибрати мок-fallback дошок (P1): ✅ 2026-07-10. LotBoard/DistributionBoard/ShipmentTracker — мок-дані гейтнуто у import.meta.env.DEV: прод на порожньому/помилковому тенанті показує чесний порожній стан (не вигадані дані), у dev — мок з помітним filled-бейджем «ДЕМО-ДАНІ — не реальні операції». На засіяному тенанті дошки й далі показують реальні дані.
  • [x] Крок 7 — постинг виконання, хоп 8 (P0): ✅ 2026-07-11. Audit-first показав: усі 3 постинг-механізми ВЖЕ існували (ВЗ ship_dispatch рухає стік+FIFO лоти; ЗН post_railway_waybill рухає дислокацію; ПЛ post_document пише fleet-регістри) — бракувало постабельності драфтів і UI. Зроблено: clamp тоннажу ВЗ до партії; complete_wagons_from_dislocation (конкретні вільні вагони 3-tier за станцією, анти-double-booking по вихідних драфтах, чесний shortfall) + кнопка «Укомплектувати вагони» на ЗН; валідація посту ЗН — train/terminal лише для вхідних (per model help_text) + consignor_org у планері (direction='out'); WaybillTask у драфтах ПЛ; self-healing purge сиріт по маркеру (було 20 на tenant 3); station-fallback по origin_name (ре-оптимізація рве FK SET_NULL); фронт-таб «Факт виконання» на LotAccumulationPlan (auto-match + fulfilments + unlink) — backend-петля існувала, UI не викликав. Verify: 22/22 E2E чеки rollback (ship 6/8 — 2 guards правомірні: фумігація+застава; стік/відкат точні; auto-match 6 док., 65 491.95 т), контроль 101 009 460.88 ₴ точно, pytest 40 passed, tsc 0. Bundle: ai/domains/logistic/execution-posting.md. Scope cuts: unpost ЗН стирає сідовану дислокацію (потрібен WagonOperationLog, Phase 2); факт-тариф варіаційний (P1 акт фрахту); фін-проводки — P1 ТЕД.

[ESS] Reports drilldown — ✅ cash_flow зроблено 2026-06-09; aging_report re-scoped (deferred)

Промоутнуто refill-грумінгом 2026-06-09 з long-term-ess.md § 5. При взятті — другий audit-first reframe: фронтова ReportDrillDownPage register-орієнтована (REGISTER_ENDPOINTS = лише ledger-регістри postingEntries/cashJournal/…, рендерить Дт/Кт-таблицю). Тож drilldown лягає тільки на register-sourced звіти.

  • cash_flow зроблено 2026-06-09 (commit): джерело PostingEntry (клас-5) → ідеально лягає. Per-month flow-cells (inflows/outflows/net_cash) дістали DrillDownTarget(register='postingEntries', filters={ledger, account__code__startswith:5, group__date range, org}) — ті самі filter-ключі, що й pnl_drilldown (proven проти /data/postingEntries/). closing_balance свідомо без drilldown (cumulative). Helper _month_bounds. +1 тест (18/18 у test_reports.py), нуль нового frontend UI.
  • ⏸️ aging_report — deferred (НЕ механічно): джерело = Invoice (документи, не ledger). Register-drilldown сторінка ledger-shaped (Дт/Кт) → Invoice-рядки не вписуються. Потрібен окремий documents-drilldown register (новий REGISTER_ENDPOINTS-entry + documents-shaped drilldown view) — це capability, не one-liner. Реактивація: коли захочемо documents-level drilldown (теж покриє majority-документні звіти).
  • Решта 7/9 звітів drilldown уже мали (profit_loss, balance_sheet, partner_turnover, trial_balance, cash_book, cash_orders_register, inventory_reports). Після cash_flow — 8/9, лишилось тільки aging (за окремим capability).

[ESS] Document approval Phase 2 — ✅✅ FULLY DONE 2026-06-08 (7 доків + generic UI + промоушн + tenant-default + admin-UI)

Promoted з long-term-ess.md § 3 під час backlog-grooming 2026-06-06 — пункт уже був shaped «ready 🤖» у long-term доку, але ніколи не потрапив у чергу (promotion lag, а не брак роботи). Перший actionable ESS-айтем у P1 — rebalance треків (критерій пріоритету #2).

OutgoingPayment зроблено 2026-06-06 (1/4, commit): model+viewset+serializer+receiver+migration 0079 + frontend ApprovalTrack tab. Gate у outgoing_payment_service.validate_before_post (B2 — не inline у viewset, бо post іде через сервіс). 11/11 нових тестів + 7/7 PI регрес, tsc=0.

PurchaseOrder зроблено 2026-06-08 (2/4, commit): ApprovableTransactionMixin + status(approval choices) + approval_routing_context на моделі; ApprovalDocumentActionsMixin + gate inline у PurchaseOrderViewSet.post_document (A1 — PO без posting-service); route_name+status read-only у serializer; PurchaseOrder додано у _APPROVABLE_MODELS; migration 0087; frontend ApprovalTrack tab + header badge. Reconcile: прибрано вестигіальне дзеркало status='posted'/'draft' з post/unpost (PO тепер як PurchaseInvoice — status = чисте approval-поле, фронт-бейджі читають state; ніхто не читав PO.status='posted'). 10/10 нових + 18/18 PI+OP регрес, tsc=0.

GoodsWriteoff backend зроблено 2026-06-08 (3/4, commit): mixin + status(approval choices) + approval_routing_context; gate у goods_writeoff_service.validate_before_post (B2 — post іде через сервіс, як OutgoingPayment); ApprovalDocumentActionsMixin на viewset; route_name+status read-only у serializer; GoodsWriteoff у _APPROVABLE_MODELS; migration 0088. 10/10 нових + 38/38 повний approval-suite + 6/6 існуючі writeoff-тести (gate = no-op без route). ✅ Generic approval-таб у kernel + InventoryCount + StockTransfer зроблено 2026-06-08 (commit): фреймворк-таб core/TransactionData/TransactionForm/ApprovalSection.tsx (тягне /approvals/routes/, route-picker + send-кнопка + ApprovalTrack + badge) вмонтований у TransactionForm — світиться для будь-якого generic-form документа з route-полем (детект 'route' in record), нуль per-doc frontend. ApprovalTrack перенесено dop/internal-documents → core/components/shared (domain-neutral kernel-primitive; dop-файл = re-export shim → нульова churn для PI/OP/PO/Contract). route виключено з header-гріда + save-payload (таб володіє через прямий PATCH). InventoryCount + StockTransfer backend (mixin+gate B2+serializer+receiver+migration 0089) — щоб таб реально покрив усі три generic-form доки. 14/14 нових IC+ST + 69/69 повний approval+posting регрес, tsc=0, vitest 4/4, no core→dop leak. ✅ JournalEntry зроблено 2026-06-08 (4/4 цільових, commit): mixin + status(approval choices) + routing-context ({amount: total_debit()}); gate inline у JournalEntryViewSet.post_document (власний viewset у accounting_extra.py, post через post_journal_entry без validate_before_post → gate перед balance-перевіркою); ApprovalDocumentActionsMixin; serializer route_name/status read-only; receiver += JournalEntry; migration 0090. Frontend: кастомний JournalEntryPage переюзає той самий core ApprovalSection (через новий опційний extraInvalidateKeys для його власних query-ключів journal-entry/journal-entries). 8/8 нових + 97/97 повний approval+posting регрес, tsc=0, vitest 4/4.

Підсумок rollout: усі 4 цільові документи (OutgoingPayment, PurchaseOrder, GoodsWriteoff, JournalEntry) + бонус generic-form (InventoryCount, StockTransfer) = 7 документів (з пілотом PurchaseInvoice) мають posting-gate + approval UI.

Промоушн essentials → approvals/ зроблено 2026-06-08 (commit): ApprovableTransactionMixinapprovals/mixins.py (essentials shim re-export → 7 моделей незмінні); completion-sync receiver → generic registry-keyed approvals/receivers.py (конект у ApprovalsConfig.ready()); новий approvals/registry.py (register_approvable(Model, status_map) + DEFAULT_STATUS_MAP); essentials/approval_sync.py тепер лише реєструє 7 доків. Міграційно-нейтрально (abstract mixin не має таблиці — makemigrations --check = no changes). Seam reusable будь-яким app поза essentials. 97/97 approval + 53/53 internal_documents+approvals регрес, check exit=0. Consumer-contract bundle оновлено. internal_documents лишив власний bespoke receiver (pre-promotion консумент, кандидат на міграцію в registry — не чіпав). ✅ Phase 1.1 tenant-default route зроблено 2026-06-08 (commit): нова модель approvals.ApprovalDefault(tenant, content_type, route, is_active) — unique per (tenant, content_type); ApprovableTransactionMixin.resolve_approval_route тепер консультує її (explicit route > tenant default > None); serializer + EntityRegistry approvalDefaults (universal CRUD) + endpoint GET /api/v1/approvals/approvable-models/ (фідить picker списком зареєстрованих доків з registry); serializer-guard відхиляє незареєстровані моделі; migration 0005. 8/8 нових + 61/61 approvals+internal_documents регрес, check=0. Реалізує C4 opt-in на рівні tenant — «кожен документ типу X вимагає узгодження» без ручного route.

Phase 2 — повністю завершено (7 доків gate+UI + промоушн у engine + tenant-default).

internal_documents → registry зроблено 2026-06-08 (commit): InternalDocument+ContractDocument тепер реєструються через register_approvable у InternalDocumentsConfig.ready(); bespoke internal_documents/signals.py видалено. Усі консументи (9 моделей) на одному generic engine-receiver, нуль per-consumer receiver'ів. 61/61 регрес, runtime-assert: registry=9 / receivers=1.

ApprovalDefault admin-UI зроблено 2026-06-08 (commit): кастомна сторінка internal-documents/components/ApprovalDefaultsPage.tsx (PROCESS-item «Типові маршрути» поряд з «Налаштування узгодження») — ContentType-picker (фідиться /approvals/approvable-models/, ховає вже сконфігуровані типи) + route-select + per-row route/active edit (PATCH) + delete + add. Зареєстровано в internal-documents/config.ts. tsc=0. Admin-доступ (як sibling RouteDesigner — requiresEntities + isWildcard bypass). Не browser-verified (Playwright не запускався).

Phase 2 ПОВНІСТЮ ЗАКРИТО. Опційний хвіст: розширити requiresEntities-доступ на не-admin ролі через role-catalog (за потребою; зараз admin-only, що коректно для tenant-config).

  • Арх.бенефіт: завершує нещодавно landed pattern. Phase 1 shipped 2026-06-05 (PurchaseInvoice pilot + seam, 8/8 tests). Generic approvals/ engine (S1–S7) + ApprovableTransactionMixin/ApprovalActionsMixin/receiver + frontend ApprovalTrack/inbox + mobile-director + push — усе вже є. Phase 2 = застосувати той самий перевірений posting-gate seam до наступних документів.
  • Audit-first 2026-06-06 (premise свіжий): ApprovableTransactionMixin спожитий лише essentials/models/purchase_invoice.py (Phase 1); цілі OutgoingPayment / PurchaseOrder / GoodsWriteoff / JournalEntry існують у essentials/models/, але mixin ще не підключено. Робота механічна (copy PurchaseInvoice wiring), не green-field.
  • Scope (ADR adr-2026-06-05-document-approval-posting-gate F1, accepted): на кожен з 4 документів — (1) ApprovableTransactionMixin на моделі + lifecycle status='on_approval' (D1); (2) ApprovalActionsMixin + send-for-approval action на viewset; (3) posting_gate.approval_gate_errors() у validate_before_post (A1/B2); (4) signal-receiver completion-sync; (5) frontend ApprovalTrack tab + send-кнопка. + Phase 1.1 tenant-default route (C4 opt-in). + промоушн mixin/receiver essentials → approvals/ (reuse поза essentials).
  • Перевірки: py_compile + точкові approval-тести на кожен документ (eталон — Phase 1 8/8), tsc --noEmit. Skill: прямого немає; custom-form posting+AI checkpoint'и дотичні.
  • Тригер: ready зараз — pattern проявлено, ADR accepted, 4 цілі існують. Береться у batch як 🤖. Розбивка можлива по 1 документу = 1 commit (OutgoingPayment першим — найближчий до PurchaseInvoice posting-shape).

[SHC] Grain Phase H-3 — Warehouse Receipt full (Е-зерно / dispatch coupling / bank cabinet) — reframe 2026-06-04

  • Audit-first reframe 2026-06-04 (ADR adr-2026-06-04-grain-warehouse-receipt-h3): базовий WarehouseReceipt(TransactionModel) уже відвантажено 2026-05-27 (entity + lifecycle + pledge/endorse/redeem + dispatch-FK + route + frontend + role_catalog). Backlog-питання (a) [single-table vs окремі моделі] — МЕРТВЕ (вже single-table + receipt_type enum). Решта осей ратифіковано й розщеплено по маркерах:
  • D — GrainDispatch coupling (D1) виконано 2026-06-04: collateral-lock у dispatch_service._validate_ship (pledged/endorsed WR на лотах, що підуть у consume → ship blocked) + auto-redeem вільних WR коли covered-лот повністю спожитий (ship_dispatch) + exact reverse на cancel_dispatch через нове поле GrainDispatch.redeemed_receipts_log (migration 0019). 5 тестів + 12 регрес зелені. Реальний інваріант: не можна відвантажити зерно в заставі.
  • B — Е-зерно Мінагро submission seam (B1) виконано 2026-06-04: shipcore_grain/integrations/ezerno_transport.py (EzernoTransport Protocol + FakeEzernoTransport + EzernoRegistryAdapter NotConfigured-bait + factory) + моделі EzernoSettings/EzernoSubmissionLog (migration 0020) + WarehouseReceipt.ezerno_registry_id/ezerno_status + service submit_receipt/submit_if_configured + command ezerno_submit. 6 тестів зелені. External-blocker: real Мінагро API-контракт + КЕП-провайдер (Вчасно.КЕП / дія.підпис) — реактивація коли API-контракт у руках.
  • ⏸️ C — Bank cabinet defer (C4) — окремий sub-продукт; арх-тригер = AFP Phase 1 outbox/inbox primitive landed (не client-trigger). Шляхи: C1 filtered-view-scoped, C2 external portal, C3 federation.
  • 🟡 E — Double-warrant ПДСС — E1 (single-entity моделює ~80%) достатньо для MVP; E2 фізичний split складська/варрант частин (незалежний обіг половин) → V2.
  • ⏸️ H-3.1 KMU-form PDF — окремий new-print-template sprint (двосторонній warrant + заставний талон).
  • Western ERP ref: Bushel «Warehouse Receipt Manager», AgVantage «Negotiable Warehouse Receipt», Cargill Cultura «Pledge Tracker» — life-cycle issued → pledged → endorsed → redeemed.
  • Reference: session research 2026-05-27 ТОП-5 § B («ЗУ Про зерно та ринок зерна» ст. 37-43, Реєстр Мінагро Е-зерно). Bundle: domains/shipcore_grain/warehouse_receipt.md.

[SHC] Grain Phase H-4 — EUDR compliance: thin scaffold shipped 2026-06-04, real internals deferred

  • Thin scaffold відвантажено 2026-06-04 (ратифіковано «роби каркас»). ADR adr-2026-06-04-eudr-compliance (accepted). У agro_field: моделі EudrSettings/DueDiligenceStatement/EudrSubmissionLog (migration 0002) + 2 NotConfigured-bait seam (integrations/eudr_transport.py: verification + EU DDS) + eudr_service.py (collect_geolocations з Field.geometry, run_verification, submit_dds, submit_if_configured, export-clearance gate) + command eudr_submit_dds + API (DDS actions from-harvest/run-verification/submit-dds; settings current) + frontend DDS entity (group EUDR Compliance). 10 тестів зелені, tsc=0, migration applied.
  • ⏸️ Залишилось (deferred, external dep): real satellite provider + EU Information System API internals (зараз NotImplementedError за NotConfigured); wiring export-clearance gate у живий dispatch/export flow (зараз ready-функція, не викликається); custom DDS-форма з action-кнопками; settings UI + submission-log surfacing. Реактивація: реальний соєвий EU-export pilot + стабільний post-simplification EU DDS API.
  • Audit-first reframe (2 виправлення фактів + 2 «мертві» осі):
  • ⚠️ Дедлайн виправлено: Reg (EU) 2025/2650 (груд. 2025) переніс — large+medium → 30.12.2026, micro+small → 30.06.2027. Старе «30.06.2026 для малих/середніх» — stale. NIBULON-архетип = large → 30.12.2026 (~6 міс). Режим ще спрощується (DDS IT-система у русі).
  • ⚠️ Scope виправлено: в EUDR Annex I зернових — лише соя. Соняшник/пшениця/кукурудза — НЕ в scope (кукурудза under review). Gate соя-scoped, не «всі зернові».
  • (b) геометрія — МЕРТВА: agro_field.Field.geometry вже JSONField/GeoJSON (покриває Art.9: >4 га polygon, ≤4 га point). Spatial-overlap робить provider, не ми → без PostGIS.
  • (d) Field ownership — вирішено для MVP: Field вже own master data в agro_field. AFP-federation = майбутній AFP Phase 4+.
  • 🔧 (c) DDS submission — обмежено: queue infra немає → синхронний best-effort hook + management command (як ezerno_service), «queued» не опція без нової інфри.
  • 🔧 (a) Satellite provider — provider-agnostic bait: НЕ обирати вендора зараз; Protocol+Fake+NotConfigured (як Maersk/Е-зерно). Реальний (Satelligence vs DIY JRC Forest 2020 overlay) = runtime config, deferred.
  • Реальні рішення (звужено): DDS data-model (EudrSettings/DueDiligenceStatement/EudrSubmissionLog) + commodity+destination-scoped gate на export-cert issuance + 2 bait-seam (verification + EU DDS API). Foundation (Field→Harvest→GrainReception→OwnershipLot.source_reception traceability) вже є.
  • Рекомендація: thin-seam-now (model + bait + gate scaffold), real-internals deferred до реального соєвого EU-export pilot + стабілізації EU API.
  • Western ERP ref: Satelligence, Whisker, AgroVISTA, Meridia (EU Information System / TRACES NT DDS); Trase.earth / EU JRC Global Forest Cover 2020 (cut-off 31.12.2020).
  • Тригер реактивації (не client-trigger): реальний соєвий EU-export pilot АБО стабільний EU DDS API post-simplification. Поки — proposed-ADR readiness.

[SHC] GrainReception — intent-only vs UA-form intake (Form 47 alignment) — A-lite shipped 2026-06-04, B deferred

  • A-lite відвантажено 2026-06-04 (ратифіковано). ADR adr-2026-06-04-grain-reception-intake (accepted). Архітектура лишилась intent-only. Сторінка /postings приймання тепер показує чесний intent-only-напис + deep-link на проводки сертифіката (cert-anchored, вже працюють) — через generic INTENT_ONLY_POSTINGS map + RegisterRecordsPage empty-state + read-only поля quality_certificate_id/number на GrainReceptionSerializer (без міграції). tsc=0, chain-тест зелений. A-full відкинуто (показ cert-проводок на прийманні = leak grain-фільтрів в essentials + вдавання «приймання запостило»).
  • ⏸️ B (intake-anchored редизайн) — deferred за реальним regulatory/pilot forcing function (не client-trigger, memory feedback_no_client_triggers). Архітектурний вибір intake-vs-cert лишається intent-only, доки не спрацює тригер B.
  • Питання: поточна архітектура — Reception нічого не пише в регістри, всі рухи (Batch + StockTransaction + Silo fill + Shrinkage) створюються в одній атомарній транзакції cert.issue. Це конфліктує з UA agro practice — Положення про облік зерна очікує що Прибуткова накладна (Форма 47 / ВЗСГ-3) формує облікову позицію на момент фізичного приймання, а cert.issue лише корегує grade/weight.
  • Симптом: Reception's /postings page порожня (truthful empty state); operator бачить status='Проведено' але регістри прив'язані до cert. Доводиться навігувати через cert для бачення Batch + StockTransaction.
  • Варіанти (не вирішено в S1-S7):
  • A (UI-only ~30 хв): backend filter cert__reception=<id> для ShrinkageJournal + custom action на Reception що повертає cert-derived register entries; POSTING_CONFIG entry для grainReceptions. Архітектура та сама.
  • B (архітектурний редизайн ~3-4 год): Reception → 'weighed' пише StockTransaction(RECEIPT) з tentative grade + створює Batch. Cert.issue робить StockTransaction(ADJUSTMENT) за різницю замість initial entry. Складніший unwind на cert.void, але operator бачить інвентар одразу після зважування.
  • Тригер на decision: pilot client відкриває Form-47 і питає «де це у balance sheet?» → реальний UA-compliance тиск форсує вибір.
  • Reference: docs/ai/domains/shipcore_grain/grain_reception/postings.md § Open question.

🟡 P2 — completeness / без архітектурного blocker'а

[SHC] Logistic — FlowMap: реальна гео-підкладка (GeoJSON контур України) 👤

Контекст (сесія 2026-07-14): карта потоків (frontend/core/src/components/shared/FlowMap/FlowMap.tsx) ставить вузли за реальними lat/lon, але без мапи-підкладки — лише спроєктовані точки на чистому фоні. Спроба домалювати контур мальованим полігоном відхилена користувачем: калічити кордони країни неприйнятно (відкочено того ж дня). Потрібен точний контур з реальних даних. - Що треба (3 складові): (1) дані — Natural Earth admin-0 Ukraine (public domain, де-юре кордони, Крим = Україна), спрощений mapshaper/topojson-simplify до ~1–3 КБ, збережений як ukraine.geo.json у репо; (2) проєкціяd3-geo (geoConicConformal/geoMercator + fitSize/fitExtent) проєктує контур і вузли однією трансформацією → збігаються; або cos(lat)-корекція руками; (3) рендерgeoPath → SVG <path> (тонкий stroke + ледь помітна заливка), той самий projection для вузлів. - Рішення до старту (чому 👤): (а) джерело/ліцензія (рекомендація — Natural Earth PD); (б) залежність d3-geo (~30 КБ) чи руками; (в) масштаб — вся Україна (південні вузли скупчуються) vs fitExtent на робочий регіон; (г) тип: тонкий SVG-контур vs повноцінна Leaflet-мапа (Leaflet+react-leaflet уже в стеку для GPS/yard) з тайлами+GeoJSON-шаром — «жива» мапа з зумом, але важча й потребує тайл-провайдера. - Прапор-перемикач «показати мапу» на карті (контур усієї країни знову скупчить південь → або зум, або toggle). - Нюанс середовища: GeoJSON треба завантажити (мережа обмежена) — або файл у репо, або WebFetch спрощеного контуру з публічного джерела. - Тригер: бажання географічного контексту на картах потоків. Косметика, не блокер.

[ESS+ACC] Повноцінний двовалютний GL (Variant B) — presentation-сума у проводках + звітність у двох валютах 👤

Виявлено аудитом 2026-06-19 (інтерактивна сесія, currency-change guard). Двовалютність доведена лише до складського шару: currency_service.convert_to_dual + DualAmount матеріалізують *_presentation у Batch/StockTransaction/InventoryJournal (+ KPI/дашборди). GL одновалютнийPostingGroup/PostingEntry зберігають суми лише в обліковій (функціональній) валюті, presentation-поля немає. Усі фінзвіти (profit_loss, balance_sheet, trial_balance, partner_turnover, cash_book, cash_orders_register) читають PostingEntry і хардкодять валюту (буквально meta={"currency":"UAH"}) → P&L/баланс у валюті презентацій отримати неможливо. Користувач обрав Variant B (двовалютний GL) над A (трансляція на момент звіту) — консистентно з уже наявним складським патерном.

  • Чому 👤 (старт з ADR): варіант (B) обрано, але лишаються рішення з реальними наслідками — рівень суворості трансляції: спрощений (один presentation-leg за курсом на дату проведення/документа) vs строгий IAS 21 (середній курс за період для P&L, closing-курс для балансу, курсові різниці / CTA окремим рахунком). Також: дата курсу для presentation-leg; стратегія backfill наявних проводок (повний repost vs інкремент). Це ADR docs/ai/architecture/adr-YYYY-MM-DD-dual-currency-gl.md перед кодом.
  • Scope (B), орієнтовна розбивка 1 фаза = 1 commit:
  • Схема: PostingEntry.debit_presentation/credit_presentation + PostingGroup.presentation_currency/presentation_rate snapshot; migration.
  • Проведення: матеріалізувати presentation-leg через convert_to_dual у accounting_service.post_accounting_entry (еталон — інвентарні сервіси, що вже так роблять); покрити всі posting-шляхи (services/posting.py, party-ledger).
  • Звіти: параметр валюти (accounting|presentation) у build() для P&L/balance/trial_balance/partner_turnover/cash_ — читати відповідну колонку; прибрати хардкод "UAH"* → реальний код з settings.accounting_currency/presentation_currency (це і окремий дрібний баг — звіти мислейблять валюту, можна закрити раніше).
  • Repost-рушій: оркестратор перепроведення (reverse+post) для backfill presentation-колонок наявних документів — те, що свідомо не будували у guard-задачі 2026-06-19.
  • Currency-change wiring: підключити repost до підтвердження зміни валюти — зараз модалка лише попереджає (backend 409 currency_change_confirmation_required у essentials/views/essentials_settings.py, frontend модалка у AccountingSettings.tsx), рушія перерахунку немає.
  • Наявне (не green-field): convert_to_dual/DualAmount/get_settings готові; складський шар — робочий еталон двовалютної матеріалізації; currency-change guard уже блокує+попереджає (2026-06-19); verify_postings L5 (InventoryJournal↔PostingEntry) — інструмент перевірки консистентності до/після repost.
  • Перевірки: posting-тести (presentation-leg = convert_to_dual(...).presentation), reports-тести (P&L у двох валютах сходиться за курсом), verify_postings, manage.py check + migration, tsc --noEmit. Skill: new-report (для валютного параметра) + правило document-posting.md (repost інваріанти).
  • Тригер: потреба мультивалютної фінзвітності (P&L/баланс у presentation). Зараз presentation = лише складська управлінська валюта + KPI — свідома часткова реалізація, не баг.

[ESS] Одиниці виміру номенклатури — таблична частина з коефіцієнтами + одиниця за замовчуванням 👤

Виявлено інтерактивно 2026-06-27 (ввід приходної на пальне: 10,234 т). Зараз Item має ОДНУ одиницю (Item.unit), Unit — плоский довідник без коефіцієнтів, перерахунку одиниць немає ніде (grep: 0 Conversion/factor/base_unit/density). Кількість у рядках — Decimal(15,3), тож сам ввід 10,234 т працює (точність до кг); маску «3 знаки» у полі кількості вже виправлено 2026-06-27 (fixedDecimalScale + фікс-відображення у GoodsReceipt/Invoice/GoodsShipment/PurchaseInvoice). Відкритим лишається кілька одиниць на номенклатуру (пальне: тонни на закупівлю ↔ літри на відпуск).

  • Рішення користувача (явне): НЕ «друга одиниця» (flat alternative UoM), а таблична частина одиниць виміру на номенклатурі — список одиниць з коефіцієнтами перерахунку до базової + одна за замовчуванням (патерн 1С «Одиниці виміру»).
  • Дизайн + відкриті питання — в ADR adr-2026-06-27-item-units-subtable (Proposed): нова ТЧ ItemUnit(item, unit, factor, is_default); база = Item.unit (factor=1); document-рядок обирає одиницю з ТЧ, перерахунок у базову при проведенні. 👤 ратифікувати перед кодом: (1) де база (Item.unit vs is_base-прапор); (2) зберігати чи рахувати quantity_base для Batch/StockTransaction/FIFO; (3) ціна за обрану чи базову одиницю; (4) точність factor; (5) міграція наявних Item → авто base-рядок; (6) температурна корекція (VCF) для пального — поза MVP.
  • Скоуп (фази): (1) модель ItemUnit+міграція+авто base-рядок; (2) ТЧ «Одиниці виміру» в картці номенклатури через FormSubtableList (skill form-subtable, еталон Item.prices); (3) picker одиниці в документах + дефолт-підстановка + перерахунок у базову при проведенні; (4) звіти/друкформи (обрана + базова одиниця).
  • Ризик: усі inventory/posting-сервіси мають враховувати перерахунок у базову одиницю (FIFO/собівартість/звіти) — найбільший вузол консистентності.
  • Тригер: потреба кількох одиниць (пальне т↔л; пакування шт/ящик/палета). Поки — одна одиниця на номенклатуру.

[ESS+ESWF] App Store add-ons track — хвости (груминг 2026-06-17)

Трек App Store add-ons landed інтерактивно 2026-06-09…06-11 (6 ADR: orders-as-addons, ar-driven-by-delivery, manufacturing-without-accounting, item-category-accounting-fields, payment-calendar-addon, group-mdm-addon), зафіксовано у done.md. Нові kernel/essentials-примітиви: core.licensing.field_gates + essentials.services.addon_gating. Тести 71/71 зелені. Нижче — виявлені відкриті хвости.

  • appGroupMdm catalog-entry зроблено 2026-06-17 (commit): додано App Store-картку в frontend/dop/applications.ts (поля узгоджені з seed_shop-записом: plugin:group_mdm, requires:[appEssentials], forModules:[essentials], price:community, icon DeploymentUnitOutlined, status:available бо not-installed-by-default). Десктопний App Store тепер показує/дає встановити Group MDM Hub. Diff seed_shop↔applications.ts чистий (0 розбіжностей), tsc --noEmit=0. Реальний install-стан backend-driven (installedSet з /active-plugins/), статичний status впливає лише на dev-фільтр — тож available коректний без хибного лейбла.
  • Bulk-edit UI у TransactionList зроблено 2026-06-17 (commit): переюзано generic BulkEditModal (metadata-driven field-picker + type-aware input через bulk-update endpoint) — кнопка «Масове редагування (N)» у toolbar-блоці selectedCount > 1 поряд з bulk Post/Unpost. Нуль нового backend-коду (endpoint generic з bulk-ops треку). tsc=0, vitest 97/97.
  • 🟡 Browser-verify add-on-гейтів — Playwright не запускався по жодному з add-on'ів (orders/payment-calendar/group-mdm/manufacturing-no-accounting). За явним запитом — webapp-testing.

[SHC] NIBULON-archetype gap pack — фіксація після research-сесії 2026-05-27

Research-сесія за архетипом НІБУЛОНа (commit master) виявила що agro_holding tenant покриває full cycle gatehouse → consolidation (24/26 етапів зеленим), але 6 документів і 2 vertical-модулі відсутні для повної реалістичності експорт-орієнтованого агрохолдингу.

Більшість пунктів пакета закрито (травень–червень 2026, → done.md): grain seed + sourcing optimizer, export-сертифікати, Charter Party + SOF, GrainContract, WarehouseReceipt, Lab Kanban, agro_field, shipyard, settlement engine. Відкритим лишився RailFreightTariff (нижче).

[SHC] RailFreightTariff — модель ЖД-фрахту у shipcore_rail (C2 з optimizer de-hardcode) ✅ shipped 2026-06-06

  • Відвантажено 2026-06-06 — ADR adr-2026-06-04-rail-freight-tariff (Implemented). Усі осі ратифіковані тоді ж і реалізовані на явний запит користувача (gate overridden). (c) C-railRailFreightTariff(MasterDataModel) у shipcore_rail біля station/wagon master data; shipcore_pricing лишається observed/commercial шаром. (a) A3 — band-рядки [from,to) × wagon_type + optional station-pair override. (b) B1+B3effective_from + index_coefficient field (re-index = одна колонка). (d) D1 — nullable Site.nearest_rail_station + Port.nearest_rail_station (migrations gatehouse/0021 + shipcore/0004); optimizer _rail_linehaul soft-bridge (apps.is_installed + try/except) → tariff коли обидва кінці + рядок резолвляться, інакше graceful fallback на LOGISTIC_RAIL_*; external offers завжди константи (нема station-link). Resolution services/rail_tariff.resolve_rail_linehaul. EntityRegistry railFreightTariffs + frontend Rail → Tariffs (kernel-default form). 16 тестів зелені (8 resolution + 4 optimizer wiring + 4 pre-existing), manage.py check 0 errors, tsc --noEmit 0. Bundle: shipcore_rail/README § Rail freight tariff + logistic/lot-accumulation-optimizer.
  • Audit-first reframe (2 правки фактів): (1) беклог-вісь (c) «shipcore_rail vs core» застаріла — після цього запису приземлився shipcore_pricing (P7, 2026-06-01) як окремий rating-движок → стала тривимірною; (2) УЗ-тариф ≠ західні lane-tables (регульована дистанційно-брекетна матриця), тож band-granularity чесніша за station-pair lane table (відкинутий A1).
  • Арх.бенефіт: оптимізатор закупівлі зерна (logistic) і майбутні rail-документи рахують ЖД-плече на хардкод-константах (LOGISTIC_RAIL_*, settings-overridable per ADR adr-2026-06-03-optimizer-dehardcode вісь C3). Реальний UA ЖД-тариф — це матриця (станція-пара × рід вагона × відстань × держ-індексація УЗ), її не можна чесно звести до одного ContractSpecification(per_ton_km) рядка (відкинутий варіант C1 = фейкова точність).
  • Western ERP ref: TMS rail rating engines (MercuryGate / BluJay) — station-pair lane tables + fuel/index surcharge (caveat: моделюють негоційовані комерційні lane'и, не регульований УЗ тариф).
  • Залишені shape-питання (a/b/d): ратифікувати рекомендації або звузити (band-only A2 / model+admin-first D3) — на момент implementation.
  • Тригер: реальні rail-перевезення в demo/pilot або потреба точного landed-cost для rail-плеча. Поки — константи (свідомий demo borrow, не баг).

[ESS] Variants V4b extension — wire VariantPicker у решта line types 🟡 частково 2026-05-28

  • Арх.бенефіт: uniformity completion — V4b 2026-05-21 закрило 4 high-traffic line types (Invoice/PurchaseInvoice/GoodsReceipt/GoodsShipment). Pattern уже встановлений у V4b commit.
  • Shipped 2026-05-28: backend serializer denorm (item_is_template + variant_sku + variant_attribute_signature) для 7 LineSerializer'ів (PurchaseOrder, GoodsWriteoff, InventoryCount, StockTransfer, ManufacturingInput, ManufacturingOutput, SalesOrder). Frontend variant-колонка (hasPlugin('essentials_variants') gate + item_is_template gate + VariantPicker, onChange→onUpdateRow) для 3 документів з кастомними subtables: PurchaseOrderSubtables, SalesOrderSubtables, ManufacturingSubtables (inputs+outputs, onChange→kind).
  • Audit-first reframe (2026-05-28): «8 решта» виявилось гетерогенним — GoodsWriteoff/InventoryCount/StockTransfer мають formType:'default'; SalesReturn живе в sales_mobile_api (frontend = RN mobile-app). Тому backend denorm зроблено для всіх з FK variant, а frontend — лише для 3, що мають кастомні subtables.
  • (a) виконано 2026-06-04 — kernel-generic форми (GoodsWriteoff/InventoryCount/StockTransfer). Audit-first reframe #2: ці доки взагалі не рендерили рядків (відсутні в LINES_CONFIG, без custom component:) — premise «generic inline-renderer» був хибний. Повний scope-B: (1) зареєстровано у LINES_CONFIG + LINE_REFERENCE_CONFIG → kernel LinesTable рендерить рядки (закрито latent gap InventoryCountFromStock); (2) framework line-cell renderer registry (core/.../lineCellRegistry.ts) дозволяє @dop/essentials інжектити VariantPicker у generic LineCell без core→dop імпорту; dop-бік Variants/registerVariantCell.tsx (side-effect import у main.tsx). ADR adr-2026-06-04-line-cell-registry. tsc=0, 4 тести зелені.
  • Залишилось (🟡, mobile-only): (b) SalesReturn mobile — variant у RN-схемі (mobile schema bump, явно відкладено у моделі). (c) SalesReturnLineSerializer backend denorm — пропущено разом з (b), бо змінює mobile-sync схему. Решта desktop-форм покрита.
  • Western ERP ref: NetSuite Item Matrix uniformly applies to all transaction subforms.

[SHC] Product Variants Phase B — ShipCore грузообработка adapter (+ESS) ⏸️ 👤

  • Арх.бенефіт: generalize-before-specialize (memory feedback_generalize_before_specialize). V1-V5 universal landed без прокат-bias 2026-05-21. Phase B = специфічний adapter поверх universal: прокат-attributes (Метал/Профіль/Діаметр/ГОСТ/Партія), Variant UoM overrides (для прокату вага variant'а різна), прокат-specific forms (steel-mark selector, ГОСТ-номер lookup), інтеграція з ShipCore cargo_handling domain.
  • Western ERP ref: SAP Variant Configuration з industry-specific add-ons (Steel/Chemical/Pharma), Microsoft Dynamics Industry Solutions — convergent pattern «universal core + vertical adapters».
  • Деталі: залежить від внутрішнього prerequisite — ShipCore Phase 6 (rail/road) повинен принести concrete cargo_handling domain. Зараз domain поки шкаралупа. До тих пір — premature.

[ESWF] Dashboard S4c — Personal Launchpad customisation (Variant C) ✅ shipped 2026-06-04

  • Shipped 2026-06-04 (frontend-only + 1 backend allowlist рядок, tsc --noEmit=0). Per-user override розкладки дашборда: reorder (native drag + up/down) / resize (¼/½/full) / add (palette над GET /dashboard/widgets/) / remove + Save/Cancel/Reset + stale-default banner. Персистенс — User.ui_preferences.dashboardLayouts (keyed per layout) через наявний PATCH /auth/me/. Підключено Home (layoutKey="home") + config persona-дашборди (section:<itemCode>). Файли: editableLayout.ts, EditableDashboard.tsx, WidgetPalette.tsx, uiStore.ts, DashboardPage.tsx, SectionPage.tsx, auth.py. Bundle: dashboard-framework.md § S4c.
  • Shaped + ratified 2026-06-04 — ADR adr-2026-06-04-dashboard-s4c-personal-launchpad (Implemented). Осі: A3 hybrid (реалізовано нативним HTML5-drag + always-on up/down замість dnd-kit — той самий контракт без нової залежності); C1 explicit edit-mode + Save/Cancel; D1 baseHash + non-destructive stale-banner; E (team presets) deferred — лише per-user override у S4c.
  • Audit-first reframe (2 хибні premise беклогу): (1) персистенс — це User.ui_preferences JSONField з уже наявним PATCH-ендпойнтом + allowlist (ALLOWED_PREFERENCE_KEYS), а НЕ нова UserPreferences.dashboard_layout модель → бекенд = +1 рядок, без моделі/міграції; (2) палітра «додати віджет» вже має ендпойнт GET /dashboard/widgets/ (manifest) + dashboardApi.getWidgets(). Тобто scope — фронт-only на інфрі ~90% готовій; оцінка «~2-3 тижні» спиралась на неіснуючу модель.
  • Арх.бенефіт: завершує Dashboard Framework S4 vision (кастомізація Home). S1-S4b shipped 2026-05-09, S5-mini shipped 2026-05-22. S4c = remaining gap до feature-complete Personal Launchpad.
  • Western ERP ref: SAP Fiori Launchpad Edit Mode / NetSuite Dashboard Personalize / Dynamics Saved Views / Salesforce Home Page Layouts.
  • Scope cut: team-wide/admin-editable role-default presets (вісь E), міграція bespoke Gatehouse/CHub дашбордів, per-widget param-кастомізація — поза S4c.

[ESWF] Kernel import hygiene — інвертувати lazy product-імпорти в core-pkg ✅ shipped 2026-06-04 (request-path + seed-relocation)

  • Request-path інверсія зроблено 2026-06-04 (commit). Єдиний справжній runtime-leak — views/auth.py ×2 (from shop.models import CustomerProfile у Google-login + register) — інвертовано через новий core.signals.user_registered + shop/receivers.py (@receiver під'єднано в ShopConfig.ready()). Kernel auth більше не імпортує shop; на kernel-only send — no-op (раніше → ImportError). Перевірка: py_compile + django.setup() (усі ready() чисті, сигнал має 1 receiver) + leak-grep (auth.py зник зі списку).
  • Audit-first reframe «7 імпортів»: print/registry.py:10docstring, не імпорт; views/reports.py:56уже graceful (Phase D L-C4: lazy+try/except→[] на kernel-only), не leak. Тобто runtime-leak був лише auth.py ×2 (закрито).
  • Seed-relocation зроблено 2026-06-04 (commit). core-pkg/src/core/seeders/ (~16.6K LoC / 45 файлів) + команди seed/seed_demo → новий flat product-app backend/demo_seed/ (model-less, без міграцій, НЕ в skeleton/bootstrap INSTALLED_APPS). Усі core.seedersdemo_seed.seeders (нуль stragglers). Refinement vs план: kernel seed-hook не потрібен — wholesale-перенос команд на product-side чистіший за registry. Заодно закрило 3 seed_demo product-імпорти (тепер легітимні). Перевірка на dev Postgres: import-all 44 модулі + 2 команди, manage.py check (0 errors), seed --list, реальний seed currencies units (5/12) у rolled-back транзакції. Пакет eswf-core тепер несе нуль DOP-коду.
  • Чому НЕ folder-split: косметична backend/dop/-симетрія відкинута (ADR adr-2026-06-04-backend-core-dop-symmetry O1) — кордон тримає пакет; реальна робота — import-гігієна.

[ESWF] Bulk operations — kernel primitive ✅ shipped 2026-06-09

  • Shipped 2026-06-09 (interactive, ратифіковано scope «Delete + post/unpost»). ADR adr-2026-06-09-bulk-operations (Implemented). Backend: BulkActionsMixin (core/views/bulk.py) вмонтований у TenantFilterMixin → кожен per-module entity-viewset дістає bulk-delete / bulk-deletion-mark (MasterData-only) / bulk-post / bulk-unpost (guard на post_document) безкоштовно, нуль per-viewset churn. bulk-post re-invoke'ить наявний detail-action per id → той самий posting-gate (lock-date, approval) reused verbatim. Best-effort per-item {success, errors}, MAX_BULK_IDS=500, tenant-scoped. Frontend: спільний useBulkAction хук; MasterDataCustomList fan-out N→1 (delete+deletion-mark); TransactionList нові Post/Unpost toolbar-кнопки (при selectedCount>1) + multi-delete. 9/9 нових + 8/8 approval regression, check=0, tsc=0, vitest 97/97.
  • Audit-first reframe: premise «green-field» наполовину хибний — useListSelection (multi-row) + entitiesApi.bulkAction стаб + типізований {success,errors} контракт уже існували; MasterDataCustomList уже робив bulk через N-запитовий fan-out. Реальна робота — generic backend-mixin + фронт N→1, не «з нуля».
  • Continuation — поле bulk-edit зроблено 2026-06-09 (2-й commit): новий bulk-update action (body {field, value}, значення через серіалізатор partial=True → type-coercion + FK + choices + posted-doc guard; _BULK_EDIT_FORBIDDEN={number,code,deletion_mark} + read-only/unknown → 400 наперед). Frontend BulkEditModal (metadata-driven field-picker + type-aware input) через новий overflow-item «Масове редагування» у MasterDataListToolbar. +6 тестів (15/15 total), tsc=0, vitest 97/97. Bulk-edit у TransactionList — deferred (endpoint generic, UI тривіально додається).
  • Scope cut: per-bulk-action permission granularity не вводили (успадкування viewset-перм); не browser-verified (Playwright не запускався).

🌐 Application Federation Protocol (AFP) track — deferred Phase 4-7

Concept session 2026-05-17 закрито. Повна архітектура у planning/application-federation-protocol-2026-05-17.md: 7 фаз, 10 ADR. Phase 1-3 — у P1 черзі вище. Phase 4-7 deferred з legitimate non-client тригерами.

  • [ ] AFP Phase 4 Shop↔T1 — реактивація коли архітектурно знадобиться separation frontend/shop у окремий tenant від T1. Зараз shop живе в тому ж tenant — sync не потрібна.
  • [ ] AFP Phase 5 UA/MD→DE cross-server consolidation — реактивація коли DE-сервер фізично доступний (real external dependency).
  • [ ] AFP Phase 6 MDM AI matching — реактивація коли обсяг unmapped entries у Group MDM перевищить ~1000 (data-driven тригер на основі Phase 3 metrics).
  • [ ] AFP Phase 7 Drill-down UI на DE — архітектурно після Phase 5.

🚢 ShipCore implementation track

ShipCore concept session закрито 2026-05-12 (21 файл / ~85K слів / 25 ADR у dop/modules/vertical/shipcore/). Phase 4 backend renames + Phase 5 shipcore_sea backend + R-1..R-5 terminal decomposition + Variants V1-V5 CLOSED 2026-05-21. Phase 7 pricing concept + Phase 5 Day-9+ frontend shape — у P1 черзі вище.

Phase 5 ShipCore Sea — deferred deliverables (external/internal blockers)

  • [ ] ⏸️ Phase 5 Day-7 — MaerskSpotAdapter real internalsDEFERRED, external dependency. Maersk Developer Portal sandbox credentials (~2 days self-service signup + customer-agreement step для production keys). Bait готовий: scaffold у c815044, NotConfigured guard працює, 5 Protocol-методів raise NotImplementedError. Реактивація: Maersk creds у руках. Імплементація автономна після цього (DCSA Booking 2.0 + T&T 2.2 + VGM 1.0 + BL 3.0 mapping вже типізовано через 3 згенеровані pydantic families).
  • [ ] ⏸️ Phase 5 — TNT codegen — DEFERRED, internal small autonomous fix. TNT_v2.2.0.yaml має external $ref URLs у dcsaorg/DCSA-domain, datamodel-code-generator ламається. Workaround: swagger-cli bundle перед regen. ~30 хв. Беремо коли потрібно T&T-events ingestion (real-time tracking).
  • [ ] ⏸️ Phase 5 — VGM codegen — DEFERRED, external mini-step. DCSA VGM 1.0 опубліковано Nov 2025 але YAML лише на SwaggerHub. Manual SwaggerHub → Export → Download API → YAML Resolved once, drop у yaml/vgm_1.0.yaml. До того часу MaerskSpotAdapter.submit_vgm працює з hand-crafted dict (~10 полів).

External blockers (factual — non-client triggers)

  • [ ] [SHC] Maersk Spot sandbox credentials — outreach + onboarding ~1-2 тижні. Потрібно для Phase 5 deliverable. Sandbox self-service, production вимагає customer agreement.
  • [ ] [SHC] УЗ Cargo / ГІОЦ B2B-договір + NDA — onboarding 2-6 тижнів per shipcore/phase2-ua-integrations.md. Потрібно для Phase 6 shipcore_rail integration через АС Клієнт УЗ + е.Портал.
  • [ ] [SHC] Awery outreach (Tristan Koch CCO) — Phase 9+ avia partnership (Model C bidirectional ONE Record). Можна стартувати у Phase 4-5 calendar window паралельно з основним impl track.
  • [ ] [SHC] Pilot Wave 1 UA road carrier — identify + agreement — outreach через AsMAP membership list / LinkedIn long-list. Узгодити з менеджментом обраного pilot per shipcore/phase1-ua-customers.md § Profile 2. Pilot outreach policy (memory pilot_outreach_policy): не починаємо до коли product не demo-grade.

[SHC] ShipCore Stevedoring — MVP module — класифікація закрита 2026-07-04

  • Класифікація закрита (ADR adr-2026-07-04-shipcore-stevedoring-mvp, на основі паралельного domain-research + code-audit): гібрид, не preset і не повністю ізольований модуль. Новий тонкий shipcore_stevedoring (Gang, TallyRecord, CargoDamageSurvey, StevedoringTariff) + перевикористання без змін gatehouse / YardZone+YardInventory / shipcore_sea (CharterParty+StatementOfFacts+demurrage_service).
  • Знахідка при аудиті: GateTransaction/StorageChargeLedger документально позначені generic (ADR adr-2026-05-21-shipcore-terminal-decomposition.md D-3), але фактично мають обов'язковий (non-nullable) FK на Container — «генеральний термінал без контейнерів» зараз не працює технічно, попри що написано в доках.
  • Арх.бенефіт: перший реальний споживач уже написаних, але не підключених заготовок CargoClassifiable/TransportUnit (thin core); другий консумент shipcore_sea laytime/demurrage машини (підтверджує її cargo-agnostic природу).
  • Тригер: ready зараз — domain-research і code-audit зроблені, класифікація прийнята, скіл new-module/custom-form застосовні напряму без додаткового дослідження.
  • [x] 🤖 D-3 (передумова, окремо від модуля) — generic-ізація GateTransaction/StorageChargeLedger: Container FK → null=True, blank=True, підключити вже написаний CargoClassifiable mixin. Механічно, обережна міграція (не чіпати існуючі рядки). ✅ виконано 2026-07-04: обидві моделі nullable + mixin підключено (is_dangerous на GateTransaction лишився локальним override — та ж колонка, без ALTER); guard у GateTransactionViewSet.post_document/unpost_document на container is None (ContainerLedger лишається container-only); міграція 0003_generic_cargo_classification (7×AddField + 2×AlterField); 4 точкові тести shipcore_cargo_ops/tests/; докстрінги + docs/ai/domains/{shipcore_cargo_ops,shipcore}/ оновлено.
  • [x] 🧭 R-1shipcore_stevedoring MVP: Gang + StevedoringTariff (master data), backend+frontend за skill new-module. ✅ виконано 2026-07-04: новий Django-застосунок (paid plugin module, price: subscription), 2 моделі + serializers/views/urls, роль stevedoring_supervisor, seed_shop PRODUCTS запис, frontend shipcoreStevedoringSection (Master Data group, 2 items) + applications.ts запис + sections.ts реєстрація; 5 точкових тестів; docs/ai/domains/shipcore_stevedoring/README.md створено. tsc --noEmit чистий, dependencies.test.ts 24/24, manage.py check без нових помилок.
  • [x] 🧭 R-2TallyRecord + CargoDamageSurvey (transaction documents) за skill custom-form. ✅ виконано 2026-07-04: обидві моделі TransactionModel+CargoClassifiable, опційний анкор на shipcore_sea.StatementOfFacts (судно) або shipcore_cargo_ops.GateTransaction (суходіл) — без нового проміжного «operation»-заголовка (Open Question 1 закрито); CargoDamageSurvey — окрема модель (Open Question 2 закрито); photos subtable; PostedDocumentGuardMixin у серіалізаторах; post/unpost без бух. проводок (обидва — операційні записи, задокументовано в bundle); frontend — formType: default без кастомного компонента (як GateTransaction); роль розширена; 10 нових тестів (15 разом з R-1). AI-чекпоінт розглянуто, не реалізовано (ідеї занотовано).
  • [x] 🧭 R-3 — App Store preset appShipcoreStevedoring + docs/ai/domains/shipcore_stevedoring/ домен-документація (spec-as-you-go). ✅ виконано 2026-07-04 — audit-closed, без нового коду: обидва deliverables вже зроблені побічно (AppEntry+applications.ts+sections.ts у R-1, домен-документація у R-1+R-2). Перевірено живим запитом AppEntry.objects.get(...) + grep count у трьох файлах. appShipcoreStevedoring — НЕ preset у сенсі appContainerHub (0 власних моделей, лише композиція) — тонкий застосунок з 4 власними моделями і опційними nullable FK на shipcore_sea/shipcore_cargo_ops, requires: [appEssentials] коректний. Звірено з shipcore_grain/shipyard/agro_field — жоден не отримав docs/dop/modules/vertical/shipcore/modules/*.md, стара planning-дерево не є живим спеком з Phase 6. ShipCore Stevedoring MVP (R-1/R-2/R-3) — завершено.
  • [x] 🧭 R-4 (рефакторинг) — злиття shipcore_stevedoringshipcore_cargo_ops після тесту користувачем. ✅ виконано 2026-07-05 (ADR adr-2026-07-05-stevedoring-merge-into-cargo-ops, суперсідить D-1 класифікацію 2026-07-04): усі 4 сутності — generic cargo-handling примітиви, не ніша-специфіка. State-only takeover (фізичні shipcore_stevedoring_* таблиці без змін, дані вижили на dry_port+agro_holding), StevedoringTariffCargoHandlingTariff (entity cargoHandlingTariffs), URL → /api/v1/shipcore-cargo-ops/…, backend-app → migration-ghost, appShipcoreStevedoring → composition preset (requires cargo_ops+sea — заодно чесно задекларовано міграційну залежність від sea, яку «тонкий» модуль мав недокументовано), секція → витрина з module: 'shipcoreCargoOps' overrides, help-статті переїхали в canonical cargo-ops дім. Звіти/роль/сідер переїхали. 28 backend-тестів + 106 frontend + tsc чисто. Пастка спіймана: nested-only viewset без EntityRegistry потребує явний permission_classes=[IsAuthenticated] (HasRolePermission → 403).
  • [x] 🧭 R-5 (D-5 dissolve) — розчинення shipcore_terminal + ContainerHub як повний showcase-preset. ✅ виконано 2026-07-05 (ADR adr-2026-07-05-terminal-dissolve-into-cargo-ops, закриває D-5/R-4.1 з ADR 2026-05-21): Terminal+IntegrationConfig+EDIMessageshipcore_cargo_ops, ShippingLineshipcore_containers (sea її не використовує — FK-аудит; ADR-ескіз «→ sea» відхилено), stacking engine + контейнерні звіти → containers, integration hub + GateTransactionContract + seed-команди → cargo_ops. State-only takeover ×5 міграцій (autodetector сам розставив reverse-deps і зловив 2 пропущені FK — TrainManifest.shipping_line, RailwayWaybill.edi_message), shipcore_terminal і containerhub — migration-ghosts (containerhub тримає load-bearing URL-alias). Frontend: containerhubSection → повна витрина за стиводорним патерном (Master Data / Documents / Registers / Analytics / Processes, module-overrides на shipcoreCargoOps/shipcoreContainers), cargo-ops секція отримала terminals+yard-процеси+integrationHub, containers — stackingOptimizer+demurrageTracker+shippingLines native. appContainerHub 0.3.0. Fresh-DB 30 тестів + 11 суміжних + 106 frontend + tsc чисто. Scope cut: R-3.5 ContainerGateExtension — deferred як і був; core/integrations framework — до другого споживача; ShippingLine↔Carrier уніфікація — [idea] у long-term-shipcore.

Календарні deadlines (regulatory — non-client triggers)

  • 2026-06-01 — ICS2 Release 3 mandatory для PL/RO/SK/HR/LV — пропускаємо свідомо (v1 = UA-only per decisions § C7)
  • 2027-01-01 — eTTN UA mandatory — Phase 4 deliverable, must hit (2 backends Vchasno + M.E.Doc)
  • ⚠️ 2027-07-09 — eFTI mandatory для public authorities (EU Reg 2020/1056) — Phase 8 deliverable, ризик якщо Phase 4 ковзне >2 months
  • 📅 2030 — DCSA eBL 100% adoption (9 carriers commit) — Phase 8 хаби (CargoX + WaveBL)

🎯 Окремі сесії (стартують за явним запитом користувача)

[ESWF] ESWF skeleton experiment — «порожня кімната» 👤

  • Арх.бенефіт: гіпотеза-перевірка чи ESWF справді framework для швидкої розробки бізнес-додатків, чи маркетингове відмежування. Підхід переглянуто 2026-05-17 з sibling-папки на frontend monorepo split після 4 leak'ів сесії. Несуча конструкція для 4 запланованих ініціатив: module versioning, ShipCore Phase 4 refactor, update delivery (community vs enterprise), AFP Phase 1.
  • Прийнята структура: frontend/core/ + frontend/dop/ (22 модулі) + frontend/blank_demo/ (другий продукт-консумер, sibling до dop/) + portal/+news/+shop/ (окремі веб-сайти). Цільовий читач blueprint — 1С-ник без Python/JS у термінах довідник/документ/реквізит/табчастина.
  • План фаз A-D + 4 виявлені leak'и + 5 рішень по осяхplanning/skeleton-experiment-2026-05-17.md. Backend (backend/dop/ симетрія?) — окрема сесія, відкладено до завершення frontend Phase A-C. Окрема сесія, не batch.

💡 Ідеї (raw, без пріоритету)

  • AI-агент «Виписати рахунок» — Phase 1 (chat-trigger) ✅ shipped 2026-05-11. Деталі: dop/features/ai-invoice-agent.md. Phase 2 (email-trigger через IMAP+HMAC) — у backlog, deferred до спільної інфри з Agent Inbox.
  • Agent Inbox — автономна розробка через AI-агента — лист клієнта з багом/фічею → AI-агент тріажить → Draft PR на worktree → Dev preview → approval власника → prod deploy. Реактивація: (а) період недоступності власника (army — real external constraint), або (б) природне продовження core/federation Phase 1 — коли outbox/inbox primitive дасть baseline, Agent Inbox стає архітектурним extension цього паттерну.