Demo Seed Methodology¶
Призначення: єдиний підхід до наповнення Postgres demo-знімком, який пакується в Docker-образ і відсилається колегам / клієнтам для оцінки. Цей документ — specification, з якої виростає код у backend/core/management/commands/seed_demo.py і похідних. Якщо ми додаємо новий tenant, нову Organization, новий бізнес-кейс або новий модуль — спочатку оновлюємо цей документ, потім код.
Quick start — як зайти в кожен tenant¶
Демо-знімок створює три ізольовані tenants з повноцінною бізнес-моделлю — і по одному superuser-логіну на tenant. У DOP немає in-app tenant switcher: request.tenant резолвиться напряму з User.tenant (див. core/middleware/tenant.py), тому щоб побачити інший бізнес — треба вийти і увійти під іншим логіном.
| Tenant | Логін | Пароль | Що побачиш |
|---|---|---|---|
| Agro Holding "АгроМрія" | admin |
admin |
4 organizations (Зерноплюс / ОлійПром / АгроТранс / ФОП Пекар), повний цикл «зерно → переробка → випічка», ConsolidationGroup з intercompany elimination, 17 employees, 38 invoices |
| Sukhyi Port Kyiv | port |
admin |
Контейнерний термінал, ContainerHub-довідники (12 типів, 10 shipping lines, 182 контейнери), 12 export-сервіс інвойсів агрохолдингу, 8 employees |
| ESWF Software | eswf |
admin |
Розробник DOP, 7 модулів-ліцензій, 16 SalesInvoices зовнішнім клієнтам (4 Agro orgs + DryPort), 4 employees |
Як перемкнутись: топ-меню → Logout → Login з іншою парою.
Чи ізольовані модулі/плагіни між tenants? Так. У DOP три шари: код Django apps глобальний (плагіни завантажені в процес для всіх), каталог shop.Product глобальний (App Store показує однаковий перелік), а активація через shop.License — per-tenant (FK на Tenant). Frontend useActivePlugins фільтрує навігацію саме по License поточного tenant'а — деактивуєш модуль в одному → у другого все лишається. Phase 0.5 у seed_demo викликає seed_shop і створює 24 active License × 3 tenants = 72 рядки. Деталі — §3-bis.
Один запуск:
Виведе:
=== seed_demo: 3 tenants ready. Logins: admin/admin (agro_holding), eswf/admin (eswf), port/admin (dry_port) ===
[shop] catalogue + default Licenses synced for all tenants
Далі → відкрити http://localhost:5173/ → залогінитись.
1. Базові принципи¶
| Принцип | Що означає на практиці |
|---|---|
| Ідемпотентність | python manage.py seed_demo можна викликати повторно — дані не дублюються. Кожен seeder використовує get_or_create за стабільним code / number у межах свого tenant. |
| Композиція з малих частин | Жоден seeder не «знає всю картину». Є низькорівневі seeders (EssentialsSeeder, FleetSeeder, CrmSeeder, seed_containerhub, seed_shop, init_accounting_data) — і orchestrator seed_demo викликає їх у правильному порядку per tenant + додає cross-organization і cross-tenant транзакції. |
| Реалістичний знімок, не випадковий шум | Дані мають утворювати сюжет: «Зерноплюс вирощує пшеницю → ОлійПром меле борошно → ФОП-пекарня випікає → роздріб купує», «АгроТранс возить між організаціями холдингу», «Сухий Порт переваленовує контейнери на експорт», «ESWF продає DOP-модулі холдингу і порту». Кожна цифра має історію. |
| Три tenants, multi-organization | Замість одного Tenant: ESWF Demo з 6 Organization — три окремі tenants з власними бізнес-моделями: 1. agro_holding — 4 organizations (виробник зерна, переробник/масляний термінал, перевізник, ФОП-пекарня) під спільним керівництвом холдингу. 2. dry_port — 1 organization (контейнерний термінал з Z/Z-під'їзною колією). 3. eswf — 1 organization (розробник DOP). Контрагентом у документах є Client-mirror, що відтворює іншу Organization у тому ж tenant (внутрішньотенантний потік) або Organization з іншого tenant (cross-tenant потік). Між-tenant документи дублюються в обох tenants з протилежних сторін (sale у одного = purchase у іншого). |
| Detuning через прапорці | Хочеш мінімум? seed_demo --skip=extras. Хочеш тільки один tenant? seed_demo --only-tenant=agro_holding. Хочеш скинути? seed_demo --reset. Хочеш конкретні фази? seed_demo --only=case_processor,case_carrier. |
| Версіонування знімку | Кожен запуск пише Tenant.name = "<Tenant Name> (snapshot YYYY-MM-DD)". У Docker-образ зашиваємо дату білду, щоб по логам розрізняти версії. |
| Завжди через класифікатори | Довідники з готовим класифікатором (валюти, банки, одиниці виміру, УКТЗЕД), наповнюються виключно через load_*_classifier команди — окремо в кожен tenant — щоб структура (numeric_code, name_ua, ієрархія) збереглась. Див. §2-bis. |
| Стандартизована нумерація | Усі довідники: code формату 000000001 (9 цифр з ведучими нулями), послідовно у межах сутності у межах одного tenant. Усі документи: <XX-prefix><sequence> де XX — Organization.code_prefix (2 символи), всього 9 символів. Див. §2-ter. Реалізовано в core/services/numbering.py. |
| Локалізація: name vs name_ua | Майже всюди є парні поля name (латиниця) + name_ua (українська). Загальні номенклатурні значення (товари, послуги, ролі, статуси) — перекладаємо (Sunflower oil / Олія соняшникова). Власні назви (юр. назви компаній, ПІБ, торгові марки) — транслітеруємо в name, оригінал у name_ua. Див. §2-quater. |
| Один superuser на tenant | DOP не має in-app tenant switcher: request.tenant резолвиться напряму з User.tenant (див. core/middleware/tenant.py). Тому seed_demo створює по одному superuser на кожен tenant — admin/admin (agro_holding), eswf/admin (eswf), port/admin (dry_port). Перемикання між бізнесами — через logout/login. Це свідомий компроміс: зробити tenant switcher у UI можна, але для демо простіші окремі логіни наочніше показують ізоляцію. |
2. Архітектурна ієрархія команд¶
python manage.py seed_demo ← top-level orchestrator
├── Phase 0: створити 3 Tenants + 3 superuser-логіни
│ └── tenant_eswf, tenant_dry_port, tenant_agro_holding
│ └── admin/admin (agro), eswf/admin (eswf), port/admin (dry_port)
├── Phase 0.5: seed_shop — App Store catalogue + License per tenant
│ └── 24 Products × 3 tenants = 72 active Licenses
│ └── гарантує що нав-меню в усіх 3 tenants має модулі без runtime-магії
│
├── tenant_eswf: [Tenant code='eswf']
│ ├── base_eswf: EswfBaseSeeder
│ │ ├── load_currency_classifier --codes UAH,EUR,USD
│ │ ├── load_unit_classifier
│ │ ├── load_bank_classifier
│ │ ├── exchange_rates UAH↔EUR, UAH↔USD ← НБУ за останні 4 міс
│ │ ├── tax_rates (ПДВ 20/7/0)
│ │ ├── chart_of_accounts ← делегується в init_accounting_data
│ │ ├── multi_ledger (pcg_eur + ua_nsbo, активні з 2026-01-01) ← див. §2-quinquies
│ │ ├── 1 Organization: TOV ESWF Software (prefix='ES')
│ │ ├── Clients (mirror усіх 5 organizations з інших 2 tenants — як зовнішніх покупців модулів)
│ │ ├── persons + departments (R&D, Sales, Support, Admin)
│ │ ├── settlement_account + cashbox
│ │ ├── warehouses (license_stock)
│ │ ├── price_types + items (модулі ESWF — perpetual/subscription/community)
│ │ └── essentials_module_settings (UAH primary, EUR presentation)
│ │
│ ├── case_es_modules: EswfModulesCaseSeeder ← StoreManager + cross-tenant продажі
│ │ ├── delegates → seed_shop (22 Products, всі pricing='community' у dev, див. §3-bis)
│ │ ├── _activate_all_available() ← safety-net License активація
│ │ ├── ActivationCodes (3× appFleetTrack, 2× appPortal)
│ │ ├── 6 SalesInvoices модулів зовнішнім клієнтам:
│ │ │ ├── ES → Zernoplyus (Essentials + CRM) [cross-tenant: agro_holding]
│ │ │ ├── ES → OliyProm (Essentials + Production + Quality) [cross-tenant: agro_holding]
│ │ │ ├── ES → AhroTrans (Essentials + Fleet + Driver Mobile) [cross-tenant: agro_holding]
│ │ │ ├── ES → FOP Pekar (Essentials community) [cross-tenant: agro_holding]
│ │ │ ├── ES → DryPort (Essentials + ContainerHub) [cross-tenant: dry_port]
│ │ │ └── 1 Subscription billing recurring (12 рахунків × 5 клієнтів = annual)
│ │ ├── IncomingPayments (повна / часткова оплата по 4 з 5)
│ │ ├── 1 CustomerProfile (admin ↔ ES org)
│ │ ├── 1 EmailSmtpSettings (фіктивна smtp.eswf.dev)
│ │ ├── EmailLog (~5 записів — activation code emails до клієнтів)
│ │ └── 1 ShopOrder + activated License для appPortal (flow redeem → license)
│ │
│ └── extras_eswf: EswfExtrasSeeder
│ ├── _seed_crm → 12 Leads з агро/порт-ринку, 8 Deals, 5 Contacts, 20 Activities
│ ├── _seed_hr_payroll → 8 Positions, 8 Employees (devs/sales/support), 1 PayrollPeriod
│ ├── _seed_budgeting → R&D budget 2026 (12 міс × 6 рахунків)
│ ├── _seed_fixed_assets → 5 ПК + 2 сервери + 1 ноут
│ ├── _seed_chat → #general, #dev, #support
│ ├── _seed_baf_sync → BAFSyncSettings (dry_run)
│ └── _seed_manual_journal_entry → 1 збалансована JE (50 000 UAH cash ↔ bank)
│
├── tenant_dry_port: [Tenant code='dry_port']
│ ├── base_dry_port: DryPortBaseSeeder
│ │ ├── load_currency_classifier --codes UAH,EUR,USD
│ │ ├── load_unit_classifier (+ TEU, FEU контейнерні)
│ │ ├── load_bank_classifier
│ │ ├── exchange_rates
│ │ ├── tax_rates + chart_of_accounts
│ │ ├── multi_ledger (pcg_eur + ua_nsbo, активні з 2026-01-01)
│ │ ├── 1 Organization: TOV Sukhyi Port Kyiv (prefix='SP')
│ │ ├── Clients:
│ │ │ ├── mirror Zernoplyus, OliyProm, AhroTrans (як замовники транзиту/перевалки)
│ │ │ ├── mirror ESWF (як постачальник DOP-модулів)
│ │ │ └── 5 external (shipping lines: MSC, Maersk, COSCO, ZIM, ONE)
│ │ ├── persons + departments (operations, gate, RW, customs, accounting)
│ │ ├── settlement_account UAH + USD + cashbox
│ │ ├── warehouses (yard_main, customs_zone, RW_siding, transit_grain)
│ │ └── essentials_module_settings (UAH primary, USD presentation — експортна валюта)
│ │
│ ├── case_terminal_ops: DryPortTerminalCaseSeeder ← Базові операції терміналу
│ │ ├── delegates → seed_containerhub (12 types, 10 shipping lines, 2 terminals,
│ │ │ 20 zones, 30 tariffs, 6 integrations,
│ │ │ 182 Containers)
│ │ ├── 30 GateTransactions (in/out за 4 міс)
│ │ ├── 12 Bookings
│ │ ├── 8 RailwayWaybills (ВПС/ВВС → морські порти)
│ │ ├── 15 StorageCharges
│ │ └── 5 DemurrageCalculations
│ │
│ ├── case_grain_export: DryPortGrainExportCaseSeeder ← Cross-tenant: експорт зерна для agro
│ │ ├── 5 Bookings зерна Зерноплюс → морський порт через DryPort
│ │ ├── 5 ServiceInvoices → Zernoplyus (RW switching, storage, customs clearance)
│ │ ├── 5 ServiceInvoices → OliyProm (експорт олії наливом — flexitank)
│ │ ├── 8 GateTransactions з зерновозами AhroTrans (вантажі агрохолдингу)
│ │ └── 3 IncomingPayments від agro_holding клієнтів
│ │
│ └── extras_dry_port: DryPortExtrasSeeder
│ ├── _seed_hr_payroll → 12 Employees: gate operators, RW dispatchers, customs, crane
│ ├── _seed_quality → 5 DefectReason (зерно: вологість, домішки) + 4 StorageLocation
│ ├── _seed_fixed_assets → 2 крана-перевантажувача, 3 yard tractor, 1 ваги
│ ├── _seed_chat → #operations, #gate, #rw-dispatch
│ ├── _seed_baf_sync → BAFSyncSettings (dry_run)
│ └── _seed_gps → yard tractors GPS snapshots
│
└── tenant_agro_holding: [Tenant code='agro_holding']
├── base_agro: AgroBaseSeeder
│ ├── load_currency_classifier --codes UAH,EUR,USD
│ ├── load_unit_classifier (+ ц/га, т/год, кг/буханка)
│ ├── load_bank_classifier
│ ├── exchange_rates
│ ├── tax_rates + chart_of_accounts (consolidated CoA для 4 organizations холдингу)
│ ├── multi_ledger (pcg_eur + ua_nsbo, активні з 2026-01-01)
│ ├── 4 Organizations:
│ │ ├── TOV Zernoplyus (prefix='ZP') — grower (ПДВ 20)
│ │ ├── TOV OliyProm (prefix='OP') — processor / масляний термінал (ПДВ 20)
│ │ ├── TOV AhroTrans (prefix='AT') — внутрішній перевізник (ПДВ 20)
│ │ └── FOP Pekar (prefix='PK') — пекарня (ФОП, single-tax, без ПДВ)
│ ├── 12 Clients:
│ │ ├── 6 internal mirrors (всі 4 internal orgs дзеркалять одне одного попарно для
│ │ │ внутрішніх sale/purchase — Zernoplyus↔OliyProm, OliyProm↔FOP, AhroTrans↔інші 3)
│ │ ├── 2 cross-tenant mirrors (DryPort, ESWF як external suppliers/clients)
│ │ └── 4 external (роздрібні мережі — Сільпо, АТБ, Novus, Varus як покупці пекарні
│ │ та експортери зерна як покупці Zernoplyus)
│ ├── persons + departments per organization
│ ├── settlement_accounts (4 — один на org) + cashboxes (4)
│ ├── warehouses (зерносховище ZP, маслоналив OP-tank, склад готової продукції OP-store,
│ │ автодепо AT, склад сировини PK-raw, склад готової випічки PK-bread)
│ ├── price_types + items:
│ │ ├── Сировина: пшениця 2 кл, пшениця 3 кл, соняшник, кукурудза
│ │ ├── Готова продукція переробки: олія соняшникова рафінована, шрот соняшниковий,
│ │ │ борошно пшеничне в/с, висівки пшеничні
│ │ ├── Хлібобулочні вироби: хліб «Український» 700г, батон «Нарізний» 500г,
│ │ │ булочки з маком 80г, рулет з маком 400г
│ │ ├── Послуги: транспортування за кілометр, оренда зерносховища,
│ │ │ логістика-послуги (АТ → внутрішні + зовнішні)
│ │ └── Модулі ESWF (з view-only license records, sourced from tenant_eswf invoices)
│ └── essentials_module_settings (UAH primary, EUR presentation, FIFO, per-org defaults)
│
├── case_grower: AgroGrowerCaseSeeder ← Зерноплюс: вирощування і продаж зерна
│ ├── 8 SalesInvoices → OliyProm (4× пшениця, 4× соняшник на переробку)
│ ├── 3 SalesInvoices → External (експорт пшениці через DryPort + посередник)
│ ├── 2 SalesInvoices → FOP Pekar (пшениця 2 кл напряму без переробки — невелика партія)
│ ├── 5 PurchaseInvoices ← External (насіння, добрива, ЗЗР, паливо)
│ ├── Production: 3 «WorkOrder»-аналоги для збору врожаю (умовний BOM поле→бункер)
│ └── 8 IncomingPayments (mix повна/часткова, по 4 покупцях)
│
├── case_processor: AgroProcessorCaseSeeder ← ОлійПром: масляний термінал
│ ├── 8 PurchaseInvoices ← Zernoplyus (вхід сировини)
│ ├── BOM "Sunflower → Oil + Meal": 1 т соняшник → 0.40 т олія + 0.55 т шрот + 0.05 т втрати
│ ├── BOM "Wheat → Flour + Bran": 1 т пшениця → 0.75 т борошно + 0.20 т висівки + 0.05 т втрати
│ ├── 12 WorkOrders (8 олії + 4 борошна, з GoodsReceipt продукції на склад)
│ ├── 6 SalesInvoices → FOP Pekar (борошно + олія для випічки)
│ ├── 4 SalesInvoices → External (олія наливом експорт через DryPort flexitank,
│ │ шрот як корм — комбікормові заводи)
│ ├── 2 SalesInvoices → External (відходи: висівки на корм)
│ └── 6 OutgoingPayments + 6 IncomingPayments
│
├── case_carrier: AgroCarrierCaseSeeder ← АгроТранс: внутрішня логістика
│ ├── delegates → FleetSeeder (8 vehicles, 6 drivers, fuel types, routes)
│ ├── 30 Waybills (внутрішньохолдингові рейси: ZP→OP, OP→PK, ZP→DryPort, OP→DryPort)
│ ├── 30 WaybillTasks + WaybillWorkResults
│ ├── 30 WaybillFuelRecords + 5 VehicleRefueling
│ ├── 12 TransportInvoices → Zernoplyus / OliyProm / FOP Pekar (внутрішні замовники)
│ ├── 4 TransportInvoices → DryPort (зовнішні рейси з зерном/олією на термінал)
│ ├── 8 MaintenanceRecords
│ ├── 4 DriverSalaryAccruals (нарахування ЗП водіям за місяць)
│ └── 6 IncomingPayments (від internal та DryPort)
│
├── case_bakery: AgroBakeryCaseSeeder ← ФОП Пекар: випікання і ритейл
│ ├── 6 PurchaseInvoices ← OliyProm (борошно + олія — основні інгредієнти)
│ ├── 4 PurchaseInvoices ← External (дріжджі, сіль, цукор, дрібна сировина)
│ ├── BOM "Хліб Український": 0.45 кг борошно + 0.005 кг дріжджі + сіль/цукор/вода → 1 буханка 700г
│ ├── BOM "Батон Нарізний": 0.30 кг борошно + 0.02 л олія + дріжджі/сіль/цукор → 1 батон 500г
│ ├── BOM "Булочки з маком": 0.06 кг борошно + 0.005 л олія + мак/цукор → 1 булочка 80г
│ ├── 30 WorkOrders (щоденні зміни випікання, по 3 типи виробів × 10 робочих днів)
│ ├── 16 SalesInvoices → External (4 ритейл-мережі × 4 щотижневі поставки)
│ ├── 60 cash IncomingPayments (щоденна виручка з фірмового магазину при пекарні)
│ └── 4 OutgoingPayments → OliyProm (оплата за борошно/олію)
│
├── case_external: AgroExternalCaseSeeder ← Cross-tenant покупки агрохолдингу
│ ├── 5 PurchaseInvoices ← ESWF (модулі DOP — мирорять SalesInvoices у tenant_eswf)
│ ├── 5 OutgoingPayments → ESWF
│ ├── 5 PurchaseInvoices ← DryPort (експорт-послуги — мирорять у tenant_dry_port)
│ ├── 3 OutgoingPayments → DryPort (часткові, з відстроченням 30 днів)
│ └── 12 monthly subscription PurchaseInvoices ← ESWF (recurring billing)
│
├── case_agro_crm: AgroCrmSeeder ← Наскрізна CRM-воронка по холдингу
│ ├── 4 додаткові LeadSources (AgroExpo / NIBULON-referral / Retail outreach / DryPort-referral)
│ ├── 12 Leads — потенційні агро-клієнти: Kernel/Glencore/Cargill/Bunge (експорт),
│ │ ЕКО-Маркет/Фора/Метро/Ашан (ритейл), 2 transport partners
│ ├── 7 Contacts — primary contacts на Сільпо/АТБ/Novus/Varus/NIBULON/Lallemand
│ ├── 24 Deals у всіх 6 стадіях (3 New + 4 Qual + 5 Prop + 3 Neg + 6 Won + 3 Lost)
│ │ з assigned_to 4 demo-менеджерам (admin/bilous/oliynyk/bulkina)
│ ├── 50 PipelineHistoryJournal entries — replay переходів через `record_stage_change`
│ │ (drives Stage Duration list widget)
│ ├── 95 Activities × 4 managers через `record_activity` → ActivityLedger
│ │ (drives Manager Activity widget)
│ ├── 6 overdue activities (due_date<now, completed_at IS NULL) → Sales Actions counter
│ ├── 4 stuck deals (updated_at backdated >35д через .update()) → Deals at Risk top-5
│ ├── 5 Quotations + 3 SalesOrders для advanced/won deals
│ └── self-bootstrap: викликає `seed_roles_agro_holding` ідемпотентно — фаза runnable
│ standalone через `--only=case_agro_crm` навіть на свіжому tenant'і.
│
└── extras_agro: AgroExtrasSeeder
├── _seed_hr_payroll → ~50 Employees розподілено по 4 orgs:
│ ZP (15 — agronomists, combine operators),
│ OP (20 — production line, lab, accounting),
│ AT (10 — drivers, mechanics, dispatcher),
│ PK (5 — bakers, sales)
├── _seed_budgeting → consolidated 2026 budget (cross-org, по холдингу)
├── _seed_consolidation → 1 ConsolidationGroup "AhroMriya Holding" з 4 org members
│ + IntercompanyMap (ZP-OP, OP-PK, AT-всі) для elimination
├── _seed_production → BOMs з case_processor + case_bakery vivified як reference
├── _seed_fixed_assets → комбайни (3 ZP), преси і олійні лінії (4 OP),
│ ваги і сепаратори (2 OP), вантажівки і причепи (8 AT),
│ пекарські печі і тістозмішувачі (3 PK)
├── _seed_quality → стандарти приймання зерна (вологість, засміченість, протеїн)
├── _seed_chat → #agro-ops, #bakery, #drivers, #processing
├── _seed_sales_field → мобільні агенти ритейл (FOP Pekar): 1 DayPlan + 5 visits
├── _seed_baf_sync → BAFSyncSettings (dry_run)
├── _seed_manual_journal_entry → 1 збалансована JE в OP (амортизація лінії)
└── _seed_gps → комбайни ZP + Fleet vehicles AT (last 7 days)
Ключове: seed_demo — це тонкий шар оркестрації. Уся «розумна» бізнес-логіка лишається в існуючих seeders / management commands. Якщо хочеш «50 рандомних waybills» — викликаєш python manage.py seed waybills --tenant=agro_holding --count=50, а не торкаєшся seed_demo.
Фаза extras_<tenant> як сміттєзбирач: усі модулі, у яких нема окремого бізнес-кейсу (HR, Budgeting, Production extras, Quality, Chat тощо), але які треба «показати» в UI з мінімумом даних — сідаються в extras_<tenant>. Нові модулі з простою моделлю даних додаються туди ж новим _seed_<module> методом, щоб не плодити phases. Для модуля з окремим сюжетом (як case_grain_export для DryPort) — окрема фаза в межах того tenant.
Cross-tenant зв'язки реалізуються парою фаз: case_es_modules у tenant_eswf створює SalesInvoices → 5 client-mirrors; case_external у tenant_agro_holding (і відповідна логіка в tenant_dry_port) створює дзеркальні PurchaseInvoices з тими ж номерами/датами/сумами. Ідентифікатор зв'язку — поле Document.external_reference = number документа з іншого tenant.
2-bis. Класифікатори — джерело правди для довідників¶
DOP має чотири готові класифікатори (JSON-файли) і чотири команди, що з них наповнюють довідники. Seed-методика забороняє створювати ці довідники в обхід класифікаторів — інакше втрачається структура (numeric_code, ієрархія, локалізовані назви). Важливо: класифікатори завантажуються окремо в кожен tenant — це isolation-by-design (RLS-готовність).
| Довідник | Класифікатор | Команда завантаження | Default scope у seed_demo |
|---|---|---|---|
Currency |
backend/essentials/data/currency_classifier.json | python manage.py load_currency_classifier --codes UAH,EUR,USD --tenant <id> |
UAH + EUR + USD для всіх трьох tenants (USD потрібен Dry Port для shipping lines і Agro Holding для зернового експорту). |
Unit |
backend/essentials/data/unit_classifier.json | python manage.py load_unit_classifier --tenant <id> |
Усі (pcs, kg, t, l, m, m², m³, hr, day, TEU, FEU…) |
Bank |
backend/essentials/data/bank_classifier.json | python manage.py load_bank_classifier --tenant <id> |
Усі (МФО код + name + name_ua) |
UKTZEDDirectory |
backend/uktzed.json | python manage.py load_uktzed --tenant <id> |
Усі (для майбутньої M.E.Doc інтеграції). Особливо важливо для tenant_agro_holding — там реальний рух зерна по УКТЗЕД-кодах 1001/1206/1512. |
base_<tenant> фази викликають ці команди в правильному порядку перед створенням Organizations / Items.
Правило: якщо ми додаємо новий довідник, який має зовнішнє джерело істини (КОАТУУ, ДКВЕД, ISO-3166 країни, …) — спочатку готуємо JSON-класифікатор у backend/<app>/data/, потім команду load_<name>_classifier, і тільки тоді додаємо це у відповідну base_<tenant> фазу.
2-ter. Нумерація: довідники vs документи¶
Довідники (master data)¶
Поле MasterDataModel.code (CharField(max_length=50)) заповнюється завжди у форматі 9 цифр з ведучими нулями (константа MASTERDATA_CODE_LENGTH = 9 у core/services/numbering.py), послідовно в межах сутності в межах одного tenant:
Item.code → '000000001', '000000002', '000000003', …
Client.code → '000000001', '000000002', …
Vehicle.code → '000000001', '000000002', …
Driver.code → '000000001', '000000002', …
Organization.code → '000000001', '000000002', … (плюс окремий code_prefix — див. нижче)
Унікалізація — в межах (tenant, entity_type). Послідовність рестартує з 000000001 для кожного tenant: у tenant_eswf Organization.code='000000001' (ESWF Software), у tenant_dry_port — теж '000000001' (Sukhyi Port Kyiv), у tenant_agro_holding — '000000001'..'000000004' (Zernoplyus, OliyProm, AhroTrans, FOP Pekar). Колізій нема, бо ключ (tenant_id, code).
Хелпер get_next_masterdata_code(model_class, tenant) повертає наступний доступний номер у межах tenant.
Виняток — довідники з зовнішнього класифікатора (Currency, Bank, Unit, UKTZEDDirectory): їхній code приходить з класифікатора (numeric_code для валют — '980' для UAH, '978' для EUR; МФО для банків — '305299'; …). Це особливе джерело істини.
Документи (transactions)¶
Поле <DocumentModel>.number заповнюється у форматі 9 символів (константа TRANSACTION_NUMBER_LENGTH = 9):
де:
- XX — Organization.code_prefix (2 символи, ASCII літери), задається при створенні Organization;
- <sequence> — послідовний номер у межах (tenant, organization, document_type) з ведучими нулями, 7 цифр (= 9 - 2). Якщо префікс порожній — лічильник 9 цифр.
Хелпер get_next_transaction_number(model_class, tenant, prefix) повертає наступний доступний номер у форматі XX1234567.
Приклади (всі в межах 9 символів):
Tenant 'eswf':
Organization TOV ESWF Software (prefix='ES'):
Invoice.number → 'ES0000001', 'ES0000002', …
ShopOrder.number → 'ES0000001', 'ES0000002', … (своя послідовність на тип документа)
Tenant 'dry_port':
Organization TOV Sukhyi Port Kyiv (prefix='SP'):
GateTransaction.number → 'SP0000001', 'SP0000002', …
RailwayWaybill.number → 'SP0000001', 'SP0000002', …
ServiceInvoice.number → 'SP0000001', 'SP0000002', …
Tenant 'agro_holding':
Organization TOV Zernoplyus (prefix='ZP'):
Invoice.number → 'ZP0000001', 'ZP0000002', …
Organization TOV OliyProm (prefix='OP'):
Invoice.number → 'OP0000001', …
PurchaseInvoice.number → 'OP0000001', … (інша послідовність)
WorkOrder.number → 'OP0000001', …
Organization TOV AhroTrans (prefix='AT'):
Waybill.number → 'AT0000001', 'AT0000002', …
TransportInvoice.number → 'AT0000001', 'AT0000002', …
DriverSalaryAccrual.number → 'AT0000001', 'AT0000002', …
Organization FOP Pekar (prefix='PK'):
Invoice.number → 'PK0000001', 'PK0000002', …
WorkOrder.number → 'PK0000001', 'PK0000002', …
Лічильник окремий на кожен тип документа в межах кожної organization — тому Invoice.number = 'ZP0000001' і Waybill.number = 'AT0000001' мирно співіснують. Природний ключ — (tenant, model_class, number).
Префікси для 6 demo-organizations across 3 tenants¶
| Tenant | Code | code_prefix |
name (latin) |
name_ua (ua) |
|---|---|---|---|---|
eswf |
org_es |
ES |
TOV ESWF Software |
ТОВ "ESWF Software" |
dry_port |
org_sp |
SP |
TOV Sukhyi Port Kyiv |
ТОВ "Сухий Порт Київ" |
agro_holding |
org_zp |
ZP |
TOV Zernoplyus |
ТОВ "Зерноплюс" |
agro_holding |
org_op |
OP |
TOV OliyProm |
ТОВ "ОлійПром" |
agro_holding |
org_at |
AT |
TOV AhroTrans |
ТОВ "АгроТранс" |
agro_holding |
org_pk |
PK |
FOP Pekar P.P. |
ФОП Пекар П.П. |
Налаштування валют обліку¶
Після створення Currency-довідника в кожному tenant, відповідний base_<tenant> встановлює в EssentialsModuleSettings:
| Tenant | accounting_currency |
presentation_currency |
Пояснення |
|---|---|---|---|
eswf |
UAH | EUR | Розробник з європейським презентаційним поглядом |
dry_port |
UAH | USD | Експорт через міжнародні shipping lines, USD домінантна валюта |
agro_holding |
UAH | EUR | Холдинг з європейським expansion-планом |
Інші settings (per tenant):
- inventory_method = 'fifo' (всюди)
- default_warehouse → перший Warehouse що створюється у відповідному tenant (license_stock / yard_main / зерносховище ZP)
- default_sales_operation, default_incoming_payment_operation, default_purchase_operation, default_outgoing_payment_operation → відповідні BusinessOperation з init_accounting_data (для синхронізації PartyLedger ↔ chart-of-accounts, див. audit §15-bis)
В tenant_agro_holding settings — per organization, бо 4 organization в одному tenant мають різні бізнес-моделі: у FOP Pekar ПДВ вимкнений (vat_default = 0), у решти — 20%.
Multi-ledger: pcg_eur + ua_nsbo з 2026-01-01¶
Кожен demo-tenant — українська компанія, яка одночасно веде два паралельні облікові контури (Ledger):
| Code | Назва | Призначення | Стандарт COA |
|---|---|---|---|
pcg_eur |
European PCG (by Nature) | Управлінський / групповий звіт у європейському шаблоні | "By Nature" 4–7 (1000–7900) |
ua_nsbo |
Ukrainian NSBO | Юридична відповідність — наказ Мінфіну №291 | UA-NSBO 1–9 (10, 20, 30, 40, 6411…) |
Обидва контури активуються з 2026-01-01 — це початок demo-знімка, від якого народжуються всі PostingGroup. Кожен пост документа породжує одну PostingGroup на кожен активний Ledger через відповідний BusinessOperationTemplate. Деталі архітектури — у essentials/models/ledger.py і тестах essentials/tests/test_multi_ledger.py.
Що робить базовий seeder для кожного tenant'а (метод seed_chart_and_operations у _demo_shared.py):
init_accounting_data→ ~120 рахунків PCG + ~55 BusinessOperation з парами Dt/Ct (legacy поля).init_multi_ledger --activated-from 2026-01-01→ створюєLedger(code='pcg_eur', is_active=True, activated_from=2026-01-01)+BusinessOperationTemplate(standard='pcg_eur')для кожного BO.seed_ua_nsbo --activate --activated-from 2026-01-01→ створює ~70 UA-NSBO рахунків (ChartOfAccounts(standard='ua_nsbo')), мапить кожен BO уBusinessOperationTemplate(standard='ua_nsbo')за таблицеюPCG_TO_UA_MAP+ special-case overrides для writeoff/inventory-adjust, і активуєLedger(code='ua_nsbo', is_active=True, activated_from=2026-01-01).
Усі три кроки ідемпотентні. Опція --activated-from YYYY-MM-DD в обох командах перепиняє існуючу дату активації, щоб ребіл бази або повторний seed повертав до канонічної 2026-01-01.
IFRS (Ledger(code='ifrs')) — scaffolding-only: команда seed_ifrs створює caption-style COA (A.1, L.1, R.1…), але не активує контур і не створює BusinessOperationTemplate'и. Реальний IFRS-маппінг — окрема задача методолога.
2-quater. Локалізація назв: коли перекладати, коли транслітерувати¶
У більшості наших моделей (MasterDataModel і похідних) є парні поля:
Це дві проєкції одного й того самого об'єкта, а не різні переклади під різні аудиторії. Тому правило вибору між перекладом і транслітерацією залежить від того, що саме записано в полі.
Правило 1 — загальні номенклатурні значення → перекладаємо¶
Для смислових сутностей, де name фактично є описом / категорією / типом — використовуємо семантичний переклад:
name (en) |
name_ua (ua) |
Контекст |
|---|---|---|
Sunflower oil refined |
Олія соняшникова рафінована |
Item.name — товар (OP) |
Wheat flour, premium grade |
Борошно пшеничне в/с |
Item.name — товар (OP) |
Sunflower meal |
Шрот соняшниковий |
Item.name — товар (OP) |
Wheat 2nd class |
Пшениця 2 класу |
Item.name — товар (ZP) |
Bread "Ukrainian" 700g |
Хліб «Український» 700г |
Item.name — готова продукція (PK) |
Transportation services per km |
Транспортні послуги за км |
Item.name — послуга (AT) |
Container handling, TEU |
Перевалка контейнера, TEU |
Item.name — послуга (SP) |
Cash desk Kyiv |
Каса Київ |
Cashbox.name |
Grain silo |
Зерносховище |
Warehouse.name (ZP) |
Flexitank loading station |
Станція наливу олії |
Warehouse.name (OP) |
Sales Manager |
Менеджер з продажу |
Person.position |
Posted |
Проведено |
BusinessOperation.name (стани) |
Sales Invoice |
Видаткова накладна |
BusinessOperation.name |
Загальне правило: якщо хтось англомовний відкриє довідник з name-колонкою — він має зрозуміти, що це таке.
Правило 2 — власні назви → транслітеруємо в name, оригінал у name_ua¶
Для юр. назв організацій, ПІБ, торгових марок, географічних власних назв — не перекладаємо, а транслітеруємо латиницею (≈ ISO 9 / паспортна транслітерація):
name (en, транслітерація) |
name_ua (ua, оригінал) |
Контекст |
|---|---|---|
TOV ESWF Software |
ТОВ "ESWF Software" |
Organization.name |
TOV Sukhyi Port Kyiv |
ТОВ "Сухий Порт Київ" |
Organization.name |
TOV Zernoplyus |
ТОВ "Зерноплюс" |
Organization.name |
TOV OliyProm |
ТОВ "ОлійПром" |
Organization.name |
TOV AhroTrans |
ТОВ "АгроТранс" |
Organization.name |
FOP Pekar P.P. |
ФОП Пекар П.П. |
Organization.name |
Pekar Petro Petrovych |
Пекар Петро Петрович |
Driver.full_name / Person.full_name |
Kyiv |
Київ |
LocationPoint.name |
Odesa |
Одеса |
LocationPoint.name |
❌ Заборонено: LLC "Grain Plus", LLC ESWF Software (переклад організаційно-правової форми), Sunflower Industries Ltd (вигаданий англ. еквівалент).
✅ Правильно: TOV Zernoplyus, TOV ESWF Software, TOV OliyProm.
Правило 3 — англомовні бренди / технічні коди → дублюємо as-is¶
Якщо вихідне значення — англомовний бренд або технічний код (модулі ESWF, торгові марки), пишемо однаково в обох полях:
name |
name_ua |
Контекст |
|---|---|---|
ESWF Essentials |
ESWF Essentials |
Item.name (модуль) |
ESWF Fleet |
ESWF Fleet |
Item.name (модуль) |
ESWF ContainerHub |
ESWF ContainerHub |
Item.name (модуль) |
Volvo FH16 |
Volvo FH16 |
Vehicle.brand_model |
Bühler MDDK |
Bühler MDDK |
FixedAsset.brand (млин в OP) |
Як перевіряти у seed-коді¶
Зручно зробити thin helper у core/seeders/_naming.py:
def names(en: str, ua: str) -> dict:
"""Парний name + name_ua. Завжди обидва поля одночасно."""
return {'name': en, 'name_ua': ua}
def proper(latin: str, original_ua: str) -> dict:
"""Власна назва — транслітерація + оригінал. Окремий helper щоб у diff було видно намір."""
return {'name': latin, 'name_ua': original_ua}
Тоді в seed-коді намір читається з виклику:
Item.objects.get_or_create(tenant=t, code='000000001',
defaults={**names('Sunflower oil refined', 'Олія соняшникова рафінована'),
'unit': liter, ...})
Organization.objects.get_or_create(tenant=t_agro, code='000000002',
defaults={**proper('TOV OliyProm', 'ТОВ "ОлійПром"'),
'code_prefix': 'OP', 'edrpou': '23456784', ...})
2-quinquies. Описи об'єктів: бізнес-контекст як частина seed¶
У кожної сутності, яку ми сіємо, є поле description (або аналогічне — note, comment, legal_address тощо). Ми заповнюємо його осмислено, а не залишаємо порожнім — щоб демо-база була самодокументованою.
Що писати в description¶
Опис кожного об'єкта має відповідати на 3 питання:
- Що це — коротка характеристика сутності (тип, роль, категорія)
- Навіщо створено — яку demo-історію / бізнес-кейс обслуговує
- Як взаємодіє з іншими — які сусідні сутності очікують цей об'єкт, у яких документах / регістрах він з'явиться
"OliyProm — масляний термінал агрохолдингу: купує сировину у Зерноплюс, переробляє в олію + борошно, продає ФОП-пекарні і на експорт через DryPort"
"Grain owner — entry point для зерна холдингу; продає сировину OliyProm (внутрішньо) і експортерам (через DryPort)"
"Internal carrier — обслуговує перевезення між ZP/OP/PK + рейси на DryPort. Не продає послуги назовні."
Приклади за типами сутностей¶
| Тип | description |
|---|---|
Organization (tenant agro_holding) |
"OliyProm — переробник агрохолдингу. Купує пшеницю/соняшник у ZP, продає борошно/олію FOP Pekar (внутрішньо) + експорт через DryPort. ПДВ 20%, accounting UAH, presentation EUR." |
Client (cross-tenant mirror) |
"Mirror of TOV Sukhyi Port Kyiv (tenant=dry_port) — used when agro_holding orgs pay for grain export services. ServiceInvoices з порту з'являються тут як PurchaseInvoices." |
Item (raw material) |
"Sunflower seeds, 8% moisture max. Sold by Zernoplyus to OliyProm; appears as input in BOM 'Sunflower → Oil + Meal' (case_processor)." |
Item (finished product) |
"Refined sunflower oil, food grade. OliyProm output of pressing+refining BOM. Sold to FOP Pekar (internal) і експорт через DryPort flexitank." |
Item (module license) |
"ESWF Essentials perpetual license. Sold by tenant_eswf to all 5 external orgs in case_es_modules; mirrored as PurchaseInvoice у tenant_agro_holding (case_external) і tenant_dry_port." |
Warehouse |
"Grain silo Zernoplyus — capacity 10000 t. Default for case_grower harvest receipts. Linked from EssentialsModuleSettings.default_warehouse for org_zp." |
Cashbox |
"Cash desk of FOP Pekar (org_pk) — UAH only. Receives daily retail bread sales (60+ cash IncomingPayments in case_bakery)." |
BusinessOperation |
"Customer Sale — Revenue: Dr 36 (AR) / Cr 70 (Revenue). Default for Invoice posting (set in EssentialsModuleSettings.default_invoice_operation)." |
Invoice (demo doc) |
"case_processor: OliyProm sells flour + oil to FOP Pekar — internal cross-org transaction. Full payment via IncomingPayment same period — PartyLedger balance must settle to 0." |
Waybill |
"AhroTrans rейс ZP→OP — Driver Pekarchuk + DAF XF105 hauls 25t wheat. Internal carrier flow part of case_carrier." |
Правило двомовності: опис завжди EN + UA¶
Описи завжди пишемо двома мовами. Технічно — два режими, залежно від моделі:
Режим A — модель має парні поля description + description_ua (shop.Product, medoc_exchange.UKTZEDDirectory, core.ScheduledTask).
Режим B — модель має лише description (TextField) (усі MasterDataModel / TransactionModel нащадки): UA-параграф, порожній рядок, EN: <англ>:
ОлійПром — масляний термінал агрохолдингу. Купує сировину у Зерноплюс, переробляє в олію та борошно, продає ФОП-пекарні і на експорт через Сухий Порт.
EN: OliyProm — agroholding's processing terminal. Buys raw materials from Zernoplyus, processes into oil and flour, sells to FOP-bakery and exports via Dry Port.
Код: використовуй хелпер describe()¶
У core/seeders/_naming.py є функція describe(en, ua, has_ua_field=False) + шорткат self.describe(...) на BaseSeeder. Вона сама вибирає потрібний режим. Деталі — як раніше (self.describe(...) йде після self.master_fields(...) у розпакуванні **).
Як застосовувати¶
- У довідниках (MasterDataModel): описи живуть у
description— постійні, редагуються як частина довідника. Правило двомовності обов'язкове. - У документах (TransactionModel):
description— короткий ярлик документа. Для demo-документів використовуємо label сценарію українською + EN-переклад ("OP→PK продаж борошна+олії, повна оплата\n\nEN: OP→PK flour+oil sale, full payment"). - У cross-tenant дзеркалах: явно прописуємо source:
"Mirror of ES0000001 (tenant=eswf) — see source for original terms".
Паттерн для нових seed-ерів (чек-ліст)¶
- Модель має
description_ua? Грепни:grep -n "description_ua" backend/<app>/models.py. - Обидва рядки непорожні?
- Описи унікальні?
- Опис читається з БД, а не лише з коду?
❌ Що НЕ писати¶
- Технічні ID і FK (
"tenant_id=7") - Переклади назв (опис — про роль, не про
name_ua) - Порожні рядки чи
"-"/"TBD" - Однією мовою
3. Розподіл 6 Organizations across 3 Tenants¶
Демо-знімок — це три ізольовані бізнеси під одним adminом, з реалістичними між-tenant зв'язками. Усі організації — Organization-записи у відповідному tenant. Internal mirrors через Client — для виставлення документів між організаціями того ж tenant. Cross-tenant mirrors через Client — для документів від однієї tenant-organization до іншої tenant-organization (узгоджені пари document_pairs у seed-логу).
Іменування — Правило 2 (§2-quater): name транслітерація, name_ua оригінал.
Tenant 1: agro_holding — Агрохолдинг "АгроМрія"¶
Бізнес-модель: вертикально-інтегрований холдинг з повним циклом «зерно → переробка → випічка», з власним перевізником.
| Code | code_prefix |
name (latin) |
name_ua (ua) |
Role | EDRPOU (фейк) | Tax | VAT |
|---|---|---|---|---|---|---|---|
org_zp |
ZP |
TOV Zernoplyus |
ТОВ "Зерноплюс" |
Виробник зерна — вирощує пшеницю, соняшник, кукурудзу | 23456784 | corporate_tax | ✅ |
org_op |
OP |
TOV OliyProm |
ТОВ "ОлійПром" |
Переробник / масляний термінал — купує зерно, переробляє в олію + борошно, продає | 11223344 | corporate_tax | ✅ |
org_at |
AT |
TOV AhroTrans |
ТОВ "АгроТранс" |
Внутрішній перевізник — обслуговує лише організації холдингу + рейси на DryPort | 55667788 | corporate_tax | ✅ |
org_pk |
PK |
FOP Pekar P.P. |
ФОП Пекар П.П. |
Пекарня — купує сировину у OP, випікає хліб, продає в роздріб | 1234567890 | single_tax | ❌ |
Документи кожної організації:
org_zpZernoplyus:- Inbound: PurchaseInvoices (насіння, добрива, ЗЗР, паливо)
- Outbound: SalesInvoices → OliyProm (внутрішньо), External (експорт)
- Special: harvest WorkOrder-аналоги, Combine GPS snapshots
org_opOliyProm:- Inbound: PurchaseInvoices ← Zernoplyus
- Production: WorkOrder з BOM (sunflower→oil+meal, wheat→flour+bran), GoodsReceipt готової продукції
- Outbound: SalesInvoices → FOP Pekar, External (експорт олії, шрот на корм)
org_atAhroTrans:- Operational: Waybill, WaybillTask, WaybillFuelRecord, VehicleRefueling
- Outbound: TransportInvoice → ZP/OP/PK/DryPort
- HR: DriverSalaryAccrual
- Maintenance: MaintenanceRecord
org_pkFOP Pekar:- Inbound: PurchaseInvoices ← OliyProm + External (дріжджі/сіль/цукор)
- Production: WorkOrder з BOM (хліб, батон, булочки)
- Outbound: SalesInvoices → ритейл-мережі + кешеві IncomingPayments з фірмового магазину
Внутрішньо-холдингові потоки (для analytics і consolidation): - ZP → OP: пшениця/соняшник (велика частина обороту) - OP → PK: борошно + олія (внутрішня поставка) - ZP → PK: пшениця напряму (мала партія) - AT → ZP/OP/PK: транспортні послуги (всі три як замовники) - AT → DryPort: вивіз на експорт (зовнішній клієнт)
ConsolidationGroup "AhroMriya Holding" (з _seed_consolidation) включає всі 4 organizations + IntercompanyMap для elimination → consolidated P&L.
Tenant 2: dry_port — Сухий Порт Київ¶
Бізнес-модель: контейнерний термінал з залізничним під'їздом, обслуговує контейнерний транзит і експорт зерна/олії від агрохолдингу.
| Code | code_prefix |
name (latin) |
name_ua (ua) |
Role | EDRPOU (фейк) | Tax | VAT |
|---|---|---|---|---|---|---|---|
org_sp |
SP |
TOV Sukhyi Port Kyiv |
ТОВ "Сухий Порт Київ" |
Контейнерний термінал — перевалка, транзит, RW, customs | 45678901 | corporate_tax | ✅ |
Документи: - Operations: GateTransaction, Booking, ContainerMovement, ContainerInspection, Seal - Rail: RailwayWaybill, TrainArrival, TrainManifest - Storage: StorageCharge, ContainerLedger, DemurrageCalculation - Sales (services): ServiceInvoice → Zernoplyus / OliyProm / AhroTrans / shipping lines (international) - Inbound: PurchaseInvoice ← External vendors (паливо для cranes), ESWF (модулі), AhroTrans (transportation)
Cross-tenant потоки: - DryPort → Zernoplyus: експорт-послуги для зерна (case_grain_export, mirror у case_external агро) - DryPort → OliyProm: експорт-послуги для олії наливом (flexitank) - AhroTrans → DryPort: вивіз вантажів агрохолдингу на термінал (mirror у case_carrier) - ESWF → DryPort: модулі DOP (case_es_modules, mirror тут як PurchaseInvoice)
Tenant 3: eswf — ESWF Software¶
Бізнес-модель: розробник DOP, продає модулі/ліцензії/підписки агрохолдингу і сухому порту.
| Code | code_prefix |
name (latin) |
name_ua (ua) |
Role | EDRPOU (фейк) | Tax | VAT |
|---|---|---|---|---|---|---|---|
org_es |
ES |
TOV ESWF Software |
ТОВ "ESWF Software" |
Розробник DOP | 12345672 | corporate_tax | ✅ |
Документи: - Products: Product (модулі), ActivationCode, License (через seed_shop) - Sales: SalesInvoice → 5 external clients (4× agro_holding + 1× dry_port), ShopOrder, IncomingPayments - Recurring: Monthly subscription billing (12 рахунків × N клієнтів) - Support: EmailLog, CustomerProfile, SmtpSettings - Inbound: PurchaseInvoice ← External (cloud, hosting), AhroTrans (occasional courier)
Cross-tenant потоки (виходять): - ES → 4× agro_holding orgs: DOP-модулі (case_es_modules → mirror у case_external агро) - ES → dry_port: DOP-модулі + ContainerHub plugin (case_es_modules → mirror у tenant dry_port)
Зведена матриця між-tenant зв'язків¶
| Source | Target | Документ у source | Mirror у target | Phase у source | Phase у target |
|---|---|---|---|---|---|
| ESWF | Zernoplyus | SalesInvoice ES000000X | PurchaseInvoice ZP-mirror | case_es_modules | case_external |
| ESWF | OliyProm | SalesInvoice ES000000X | PurchaseInvoice OP-mirror | case_es_modules | case_external |
| ESWF | AhroTrans | SalesInvoice ES000000X | PurchaseInvoice AT-mirror | case_es_modules | case_external |
| ESWF | FOP Pekar | SalesInvoice ES000000X | PurchaseInvoice PK-mirror | case_es_modules | case_external |
| ESWF | DryPort | SalesInvoice ES000000X | PurchaseInvoice SP-mirror | case_es_modules | case_terminal_ops |
| DryPort | Zernoplyus | ServiceInvoice SP000000X | PurchaseInvoice ZP-mirror | case_grain_export | case_external |
| DryPort | OliyProm | ServiceInvoice SP000000X | PurchaseInvoice OP-mirror | case_grain_export | case_external |
| AhroTrans | DryPort | TransportInvoice AT000000X | PurchaseInvoice SP-mirror | case_carrier | case_terminal_ops |
Wow-ефект для звітів: - P&L кожного tenant — повноцінні дві сторони обороту, реальні марджі - Cash Flow з реальним рухом грошей (кешові продажі пекарні дають ритм) - Trial Balance як знімок per organization + consolidated для агрохолдингу - Payment Calendar з відстроченими платежами між tenants - PartyLedger running balance для всіх внутрішньо- і між-tenant клієнтів - ConsolidationGroup elimination — реальні intercompany продажі ZP↔OP↔PK затиркаються
3-bis. App Store seed: політика ціноутворення та автоактивації¶
case_es_modules (через seed_shop + _activate_all_available) наповнює App Store у tenant_eswf — це ката́лог модулів. License-записи створюються:
- У tenant_eswf — як «продані ліцензії», прив'язані до Order
- У tenant_dry_port і tenant_agro_holding — як «активовані ліцензії», прив'язані до тих модулів, які customer купив (matching через License.product_code)
Ціноутворення: всі модулі — community у dev¶
| Поле Product | Значення в dev seed | Пояснення |
|---|---|---|
pricing |
'community' для всіх модулів |
Активація без коду. Платні тарифи (perpetual / subscription) — тільки на проді. |
price, price_monthly, price_annual |
0 |
Каталог у ERP показує "безкоштовно". |
status |
'available' / 'coming_soon' |
coming_soon = у розробці — не активується автоматично. |
installed_by_default |
True для 21 модуля, False для appConsolidator |
Визначає, чи створювати License одразу при першому GET /licenses/ від нового tenant. |
Чому всі community в dev: demo-образ має показати весь функціонал одразу для всіх трьох tenants, без квестів «введи код активації». Реальна платна логіка (paid/subscription) перевіряється окремо через unit-тести LicenseViewSet.activate, не в seed.
Автоактивація: три шари захисту (спрацьовують в кожному tenant окремо)¶
- Phase 0.5 у
seed_demoвикликаєseed_shop(без--tenantпараметра). Він перебирає всіх Tenant у БД і створює License для кожногоinstalled_by_default=TrueProduct у кожному tenant. Поточний стан післяseed_demo --reset: 24 active Licenses × 3 tenants = 72 рядка License. Product.installed_by_default=True— використовуєтьсяseed_shopяк критерій масової активації. Усі продукти з цим прапорцем автоактивуються в кожному tenant.LicenseViewSet._ensure_default_licenses(tenant)у runtime — при першому GET/licenses/від tenant автоматично створює відсутні License. Останній рубіж захисту (наприклад, якщо tenant з'явився позаseed_demo).
Чітке логування для всіх tenants одразу:
--- Licenses ---
[+] License: ESWF Software ← appEssentials
[+] License: ESWF Software ← appFleet
... (24 Products × 3 tenants)
Licenses: 72 created, 0 reactivated, 24 already active.
[shop] catalogue + default Licenses synced for all tenants
Продукти, що потребують ручної активації¶
Якщо Product має installed_by_default=False, він не активується в Phase 0.5 і потребує вручної логіки в extras_<tenant>. Поточний приклад — appBudgeting (installed_by_default=False за станом 2026-05-06; перевірити в seed_shop.py). У такому випадку:
# extras_<tenant>.py — захищена через _safe(...)
def _activate_<product>_license(self):
from shop.models import License, Product
product = Product.objects.filter(code="<appCode>").first()
if not product:
self.log("Product not in catalogue — skipped")
return
lic, created = License.objects.get_or_create(
tenant=self.tenant, product=product,
defaults={"is_active": True},
)
Реальний приклад — _activate_consolidator_license у agro_extras.py (страховка, бо appConsolidation зараз installed_by_default=True, але цей прапорець може мінятися).
Синхронізація з frontend-конфігом¶
backend/shop/management/commands/seed_shop.py і frontend/erp/src/config/applications.ts — два джерела істини, які треба тримати в sync.
3-ter. Ролі та demo-юзери з призначеннями¶
Окрім трьох superuser-логінів (Phase 0), кожна extras_<tenant> фаза підтягує системний каталог ролей у тенант і створює per-role demo-юзерів. Каталог — спільний для всіх тенантів, призначення — специфічні.
Що робить orchestrator (Phase 0)¶
- Створює 3 tenants
- Створює 3 superuser'и (
admin/eswf/port) — коженis_tenant_admin=True - Оновлення (2026-05-06): одразу після superuser'ів викликає
apply_system_roles(tenant)для кожного тенанта → у БД з'являються 14 системних ролей (drift-free copy ofrole_catalog.py). Це гарантує щоRoleє завжди, навіть якщо хтось запуститьseed_demo --skip=extras_*.
Що роблять extras_<tenant> фази¶
Кожна фаза в кінці викликає seed_roles_<tenant> (з backend/core/seeders/_roles_<tenant>.py):
- Re-apply каталогу (idempotent — нічого не змінює якщо Phase 0 уже застосував).
- Призначити
tenant_adminbound superuser'у явним записом уUserRole. Це дублює auto-allow зis_tenant_admin=True, але робить роль видимою в UI як explicit assignment. - Створити per-role demo-юзерів:
- eswf:
melnyk,tkachenko,bondarenko(по одному на роль accountant+sales/support/engineer) - dry_port:
honchar,lytvyn(Person-linked) +gate,weigher,dispatcher,yard(standalone — не Persons, а демо-логіни для специфічних робочих місць) - agro_holding: 8 логінів по юрособах + 2 standalone (
gate_agro,weigher_agro— ворота на сило́сах і пекарні) - Прив'язати кожного юзера до його ролі/-ей через
assign_role(user, code).
Усі demo-юзери мають is_tenant_admin=False (крім тих 3 superuser'ів) — це навмисно, щоб через них тестувати реальну роботу HasRolePermission (короткозамикання адмінських прав не активне).
Структура файлів¶
backend/core/seeders/
├── _roles_eswf.py ← seed_roles_eswf(tenant)
├── _roles_dry_port.py ← seed_roles_dry_port(tenant)
└── _roles_agro_holding.py ← seed_roles_agro_holding(tenant)
Кожен файл містить:
- список PERSON_ROLE_USERS — (username, last_name, role_codes, position) для юзерів, прив'язаних до існуючих Person
- список STANDALONE_USERS — для логінів-без-Person (наприклад gate operators)
- функцію seed_roles_<tenant>(tenant, log=print) — idempotent
Повний список логінів¶
Усі — пароль admin. Деталі (роль → user → що бачить) — у docs/eswf/permissions/demo-users.md.
| Tenant | Superuser | + Demo-юзери |
|---|---|---|
eswf |
eswf (Ніколаєнко) |
melnyk, tkachenko, bondarenko |
dry_port |
port (Бойко) |
honchar, lytvyn, gate, weigher, dispatcher, yard |
agro_holding |
admin (Петренко) |
koval, sydorenko, bilous, havryliuk, kravets, oliynyk, bulkina, gate_agro, weigher_agro |
Зв'язок із системою прав¶
- Каталог ролей у коді —
backend/core/services/role_catalog.py - Документація прав — docs/eswf/permissions/
- Як перевірити що залогінений юзер бачить — UI: Admin Tools → Roles Matrix; CLI:
python manage.py shell+ JWT черезRefreshToken.for_user(u) - Як змінити права через UI — Admin Tools → Roles Matrix (працює per tenant, не редагує Python-каталог)
- Як відкотити правки до коду — там же, "Скинути до каталогу" (викликає
apply_system_roles(tenant))
Чек-ліст для нової extras_<tenant> фази (наприклад майбутній 4-й тенант)¶
При додаванні нового тенанта:
1. Створити _roles_<tenant>.py за патерном існуючих
2. У <tenant>_extras.py додати self._safe("roles", self._seed_roles) як остання дія run() (після hr_payroll, fixed_assets, journal_entry)
3. Завести _seed_roles метод що імпортує і викликає seed_roles_<tenant>
4. Тест-залогінитись новим юзером, переконатись що useFilteredSections показує очікуваний sidebar
Якщо не потрібно роздавати ролі (мінімальний кейс)¶
Якщо новий тенант — суто адмін-демо (тільки superuser, без додаткових юзерів):
- В extras_<tenant> пропустити _seed_roles (або викликати з STANDALONE_USERS=[], PERSON_ROLE_USERS=[])
- Phase 0 все одно застосує каталог → 14 ролей будуть у БД, готові до призначення через UI
4. Часовий діапазон даних¶
- Opening balances: 2025-12-31 (делеговано в
init_accounting_data._seed_document_operationsper tenant). - Робочі транзакції: 2026-01-01 → 2026-05-06 (поточна дата).
- GPS snapshots: останні 7 діб до поточної дати (комбайни ZP + Fleet AT + yard tractors SP).
- Period для майбутніх запусків:
seed_demoприймає--end-date YYYY-MM-DD(default = today).
Це дає достатньо даних для:
- P&L per tenant за 4+ повних місяці
- Consolidated P&L agro_holding за той самий період
- Cash Flow з реальним рухом per tenant
- Trial Balance як знімок per tenant
- Payment Calendar з планованими платежами на травень-червень
- PartyLedger running balance для всіх клієнтів (internal + cross-tenant + external)
5. Контракт ідемпотентності¶
Кожен sub-seeder зобов'язаний:
- Шукати по природному ключу перед створенням:
Tenant→code('eswf','dry_port','agro_holding')Organization→(tenant, code)(деcode— формат000000001, рестартує per tenant)Client→(tenant, code)(формат000000001)Item→(tenant, code)(формат000000001)Invoice/Waybill/ShopOrder→(tenant, model_class, number)у форматі<XX-prefix><7-digits>(див. §2-ter)ChartOfAccounts→(tenant, code)(PCG номер)BusinessOperation→(tenant, name)-
Currency/Bank/Unit→ не створюються вручну, тільки черезload_*_classifier(§2-bis) -
Якщо знайдено — оновити поля з patch-логікою (не перетирати ID, FK, історію):
-
Не торкатись transactional state при повторному запуску (статуси документів, проводки в журналі, GPS snapshots) — окрім випадку
--reset-transactions, який чистить документи + переграє автопроводки. Reset діє per tenant (не torkає інших). -
Логувати кожне створення / пропуск через
self.stdout.writeз префіксами[tenant=<code>][created],[tenant=<code>][updated],[tenant=<code>][skip]. -
Cross-tenant зв'язки: при створенні mirror-документа в target tenant обов'язково проставити
external_reference= number документа з source tenant. Це дозволяє ідемпотентний replay і readable diagnostics.
6. Прапорці CLI¶
Поточний реєстр фаз (seed_demo.PHASES у core/management/commands/seed_demo.py):
| Tenant | Ключ | Статус | Що робить |
|---|---|---|---|
eswf |
base_eswf |
✓ implemented | 1 org TOV ESWF Software, 7 модулів, 5 cross-tenant mirrors |
eswf |
case_es_modules |
✓ implemented | 16 SalesInvoices модулів 5 зовнішнім клієнтам + payments |
eswf |
extras_eswf |
✓ implemented | Chat, BAF Sync, HR/Payroll (4 employees), FixedAssets (4), JE |
dry_port |
base_dry_port |
✓ implemented | 1 org Sukhyi Port, 6 terminal-services items, 8 clients |
dry_port |
case_terminal_ops |
✓ implemented | seed_containerhub delegate (12 types, 10 lines, 182 containers) |
dry_port |
case_grain_export |
✓ implemented | 12 ServiceInvoices до ZP/OP/AT (cross-tenant flow) |
dry_port |
extras_dry_port |
✓ implemented | Chat, BAF Sync, HR/Payroll (8), FixedAssets (5), Quality, JE |
agro_holding |
base_agro |
✓ implemented | 4 orgs (ZP/OP/AT/PK), 14 items, 12 clients (6 internal + cross + external) |
agro_holding |
case_grower |
✓ implemented | Зерноплюс — 9 SalesInvoices зерна + 1 PurchaseInvoice |
agro_holding |
case_processor |
✓ implemented | ОлійПром — 8 SalesInvoices олії/борошна/шроту/висівок |
agro_holding |
case_carrier |
✓ implemented | АгроТранс — 13 TransportInvoices до ZP/OP/PK + DryPort |
agro_holding |
case_bakery |
✓ implemented | ФОП Пекар — 8 sales до ритейл-мереж + 4 purchases |
agro_holding |
case_external |
✓ implemented | 5 inline external items + 12 ESWF mirrors + 3 DryPort mirrors |
agro_holding |
case_agro_crm |
✓ implemented | Cross-org CRM funnel — 12 leads, 7 contacts, 24 deals (Won/Lost/Open mix), 50 pipeline-history entries, 95 activities (4 managers), 5 quotations, 3 sales orders. Drives all CRM dashboards (Funnel/Win-Loss KPI/Stage Duration/Manager Activity/Sales Actions/Deals at Risk). Self-bootstraps demo manager users. |
agro_holding |
extras_agro |
✓ implemented | Chat, HR/Payroll (17), FixedAssets (10), Quality, Budget, Consolidation, JE |
# Найчастіші сценарії
python manage.py seed_demo # повний знімок (всі 3 tenants, всі implemented фази)
python manage.py seed_demo --reset # знести 3 demo tenants і створити з нуля
python manage.py seed_demo --only-tenant=agro_holding # тільки агрохолдинг (всі його фази)
python manage.py seed_demo --only-tenant=eswf,dry_port # без агрохолдингу
python manage.py seed_demo --only=base_agro,case_grower # тільки конкретні фази
python manage.py seed_demo --skip=extras_agro,extras_eswf # без extras для двох tenants
python manage.py seed_demo --reset-tenant=dry_port # перерахувати лише один tenant
# Допоміжні
python manage.py seed_demo --list # перелік фаз з маркером ✓/·, груповано по tenant
python manage.py seed_demo --list --tenant=agro_holding # фази тільки для одного tenant
Default users: три superuser-и, по одному на tenant — admin / admin (agro_holding), eswf / admin (eswf), port / admin (dry_port). Перемикання між tenants — через logout + login з іншою парою (in-app switcher не реалізовано — User.tenant рулить). Логіка у _ensure_demo_users — якщо користувач вже існує, пароль перепризначається; is_superuser=True, is_staff=True, is_tenant_admin=True форсуються щоразу.
Важливо про --reset: чистить усі три demo tenants (eswf, dry_port, agro_holding) через _purge_tenant_data. Інші tenants (наприклад, ваш робочий my_company) не торкаються. Для скидання лише одного — --reset-tenant=<code>.
7. Як додати новий бізнес-кейс (нову компанію / новий модуль / новий tenant)¶
Чек-ліст:
- Оновити цей документ — визначити, до якого tenant додаємо (чи створюємо новий tenant). Додати рядок у §2-ter (нова Organization з префіксом), §3 (новий блок або рядок у блоці tenant), фразу в §2 (новий метод чи Phase у дереві).
- Якщо новий tenant — описати в §3 окремий блок з business-model, organizations, document flow, cross-tenant relations.
- Вибрати місце для sub-seeder:
- Модуль має власний сюжет → нова фаза: sub-seeder
core/seeders/<tenant>_case_<name>.pyз класом<Tenant><Name>CaseSeeder(BaseSeeder), зареєстрована уPHASESвseed_demo.pyпід відповідним tenant. - Модуль не має окремого сюжету, але потрібен мінімум даних → метод у
<Tenant>ExtrasSeeder:_seed_<module>у core/seeders/_case_extras.py . - Модуль вже має власну
seed_*management-команду (seed_containerhub,seed_gps_data) →call_command("seed_<name>", tenant=tenant.id, verbosity=0)у_seed_<module>методі. - Зареєструвати:
- Нову фазу — додати рядок у
PHASESуseed_demo.py(зtenant_key) + гілку в_run_phase(). - Новий метод у
<Tenant>ExtrasSeeder— додати викликself._safe("<label>", self._seed_<module>)уrun(). - Гарантувати ідемпотентність (див. §5). У
<Tenant>ExtrasSeederкожен метод вже обгорнутий у_safe()→ власнуatomictransaction → якщо щось падає, інші модулі не ламаються. - Дотриматись локалізаційного правила (§2-quater) — для кожної нової сутності з парою
name/name_uaявно визначити, чи це загальна номенклатура (перекладаємо), чи власна назва (транслітеруємо). 6-bis. Двомовнийdescription(§2-quinquies) — черезself.describe(en, ua). Перевірити що обидва рядки непорожні та унікальні. - Прописати cross-org / cross-tenant зв'язки — в коментарях вказати, як цей кейс взаємодіє з вже існуючими (хто кому виставляє рахунки, хто чий клієнт, mirrors у яких tenants).
- Перебудувати Docker образ (див. §8).
Правило приналежності: окрема фаза чи метод в extras?¶
| Критерій | Окрема фаза (case_X) | Метод у <Tenant>ExtrasSeeder |
|---|---|---|
| Сюжет/сценарій | Так (виробництво → продажі → оплата → consolidation) | Ні — просто «заповнити щоб показати» |
| Обсяг даних | 30+ записів з різними статусами | 3-10 записів, по 1-2 статуси |
| Cross-org / cross-tenant зв'язки | Задіюються 2+ Organizations або 2+ Tenants | Переважно на одній org |
| Потребує власного orchestration | Так (як case_processor — purchase → BOM → workorder → goods → sale) |
Ні — одна-дві моделі |
Правило: окремий tenant чи нова organization у наявному?¶
| Критерій | Новий tenant | Нова organization у наявному tenant |
|---|---|---|
| Контекст бізнесу | Інший власник, інший CoA, окремі звіти | Той самий власник (як холдинг) |
| Доступ користувачів | Доступ ізольований (різні юзери з різних компаній) | Спільний admin/staff |
| Потреба у consolidation | Ні (справді окремі бізнеси) | Так (intercompany elimination) |
| Шкала | Цілий ринок/галузь | 4-10 organizations максимум на один tenant |
Приклад — чому FOP Pekar і Zernoplyus в одному tenant, а ESWF — в окремому: пекарня і виробник зерна належать одному холдингу (consolidated reporting, спільний admin); ESWF — це постачальник, не частина холдингу.
8. Як перебудувати Docker-образ після зміни seed¶
Один скрипт-команда — scripts/build-demo-image.js:
node scripts/build-demo-image.js
# або з тегом версії:
node scripts/build-demo-image.js --tag=v2026.05.06
Що відбувається всередині:
- Чиста БД: drop & create database.
- Migrate:
python manage.py migrate --noinput. - Seed:
python manage.py seed_demo --reset(всі 3 tenants). - Build ERP:
cd frontend/erp && npx vite build. - Docker build:
docker build -f Dockerfile.demo -t eswf-demo:<tag> .. - Export:
docker save eswf-demo:<tag> | gzip > dist/eswf-demo-<tag>.tar.gz. - Прибирання:
docker system prune -fдля звільнення локального диску.
Розмір очікуваний: 250-400MB у .tar.gz (через 3 tenants з повним seed). Файл готовий до відправки.
9. Як колега запускає образ¶
Тільки 3 команди (з README.colleague.md що йде разом з .tar):
docker load -i eswf-demo-<tag>.tar.gz
docker run -d -p 8080:80 --name eswf-demo eswf-demo:<tag>
# відкрити http://localhost:8080 і залогінитись:
# admin / admin → Agro Holding "АгроМрія" (4 organizations)
# eswf / admin → ESWF Software (DOP vendor)
# port / admin → Sukhyi Port Kyiv (container terminal)
# Перехід між tenants — через logout + login (in-app switcher не реалізовано).
Зупинити: docker stop eswf-demo. Видалити: docker rm eswf-demo && docker rmi eswf-demo:<tag>.
RAM під час роботи: ~300-400MB (Daphne + nginx + supervisord + Postgres з 3 tenants). Вписується у 1GB вільного.
10. Дорожня карта seed-методики¶
| Хвиля | Що додаємо | Статус |
|---|---|---|
| Wave 1 (legacy) | Один tenant main з 6 organizations: base, case_a, case_b, case_d, inventory, extras. Legacy code лишається в репо як reference (demo_base.py, demo_case_*.py), але з PHASES виключено. |
✓ done (історичний) |
| Wave 2 — Multi-tenant migration | 14 фаз через 3 tenants з повноцінними посталеними документами і Consolidation: • Phase 0 — _ensure_tenants + _ensure_demo_users (3 superuser логіни) • EswfBaseSeeder / DryPortBaseSeeder / AgroBaseSeeder • 5 case-фаз внутрішніх потоків (case_es_modules, case_grower/processor/carrier/bakery) • 3 cross-tenant case-фази (case_external, case_grain_export, case_terminal_ops) • 3 extras-фази (chat, baf, hr/payroll, fixed_assets, quality, budget, consolidation, JE) • --only-tenant, --reset-tenant, --list --tenant=<code> CLI прапорці • Перевірено повний seed_demo --reset + ідемпотентний re-run |
✓ done (2026-05-06) |
| Wave 3 | case_vbs (eTTN B→B→C тристороння схема — на tenant_agro_holding як натуральна площадка) + case_e (Logistic plugin multimodal forwarding — на tenant_dry_port) + Docker pack автоматизація. |
⏳ planned |
Майбутнє покращення (backlog):
- 🔮 Snapshot-режим: експорт поточного стану БД → reusable fixture per tenant, замість повного re-seed щоразу.
- 🔮 N-tenant scale тест: seed_demo --duplicate-tenant=agro_holding --copies=10 → симулювати 10 агрохолдингів для нагрузочного тесту RLS і tenant isolation.
- 🔮 Random-noise добавки: опційне --noise=10x --tenant=<code> — генерує додаткові «шумові» транзакції для UI-тестування з великим обсягом.
- 🔮 Локалізовані варіанти: seed_demo --locale=ua додає документи з українськими описами, --locale=en — англійськими (поверх двомовного обов'язкового мінімуму з §2-quinquies).
- 🔮 Часові ряди для дашбордів: окремий seed_demo_timeseries що генерує погодинні / денні snapshot-и для перевірки real-time графіків.
- 🔮 Real-RLS smoke-test: автоматичний pytest що під 3 окремих юзерів (по одному на tenant) перевіряє відсутність витоку даних між tenants.
Пов'язане¶
- audit-2026-04-21.md — аудит з якого виросла ідея демо-образу
- audit-2026-04-22.md — Tenant switching у UI (для admin/admin доступу до 3 tenants)
- build.md — як збираються frontend-и (ERP build → nginx)
- backend.md — структура Django apps
- plugin-instruction.md — як підключати paid plugins (ContainerHub у dry_port, Logistic у Wave 3)
- testing.md — pytest на бекенді (consistency-тести seed-у per tenant)
- docs/dop/modules/horizontal/consolidation/README.md — Consolidation для агрохолдингу