Прохідна та вагова (Gatehouse)¶
Layer: Horizontal plugin поверх Essentials. Code:
gatehouse(Django app),appGatehouse(License + sidebar pillar). Status: Усі 5 sprints shipped 2026-04-28 — universal core + hardware bridge + vehicle gate scenario + weighing correlator + personnel turnstile + FortNet integration + ContainerHub bridge. Source of truth: docs/planning/gatehouse-plugin.md (5 спринтів, ADR-рішення §0). Hardware ADR — docs/eswf/architecture/hardware-integration.md.Інші документи цієї теки: - testing-guide.md — детальна QA/dev інструкція по перевірці кожного спринта (DoD checklists + troubleshooting). - user-guide.md — інструкція оператора (без термінів
JWT/WS/migrate).
Призначення¶
Універсальна точка контролю на межі підприємства, що масштабується на всі вертикалі (ContainerHub, Logistic, Fleet, GrainElevator). Замість трьох незалежних модулів — один плагін з спільною моделлю події, класифікацією прохідних і шаром equipment-драйверів. Доменні модулі (HRM, ContainerHub, Essentials) споживають ці події через адаптери.
Три типові сценарії:
| Кейс | Прохідна | Документ |
|---|---|---|
| Турнікет з картою | personnel_turnstile |
hrm.AttendanceLog (Sprint 5) |
| Прохідна авто з ANPR | vehicle_gate |
containerhub.GateTransaction / logistic.ContainerVisit (Sprint 5) |
| Промислова вагова | weighing_station |
essentials.GoodsReceipt через WeighingTicket (Sprint 4) |
Моделі (Sprint 1)¶
| Модель | Базовий клас | Призначення |
|---|---|---|
GatePoint |
MasterDataModel |
Фізична точка контролю + purpose (turnstile / vehicle / weighing / mixed). |
GateEquipment |
MasterDataModel |
Обладнання на точці (ваги, ANPR, картрідер, etc.) — у Sprint 1 лише config-рядок, у Sprint 2 — реальний driver через device-bridge. |
GateEvent |
TransactionModel |
Immutable подія перетину. У Sprint 1 створюється оператором руками. |
GateCheckpoint |
TenantAwareModel |
Проміжкові чекпойнти території між гейт-в'їздом і виїздом. Sprint 3 будує timeline. |
WeighingTicket |
TransactionModel |
Бізнес-документ зважування (брутто + тара) з власним state machine. У Sprint 1 — ручний ввід; Sprint 4 додає ANPR+scale correlator. |
GateEvent ↔ WeighingTicket — той самий патерн, що Payment ↔ Invoice у бухгалтерії: подія immutable, документ має lifecycle.
Endpoints (REST)¶
/api/v1/gatehouse/gate-points/
/api/v1/gatehouse/gate-equipment/
/api/v1/gatehouse/gate-events/
/api/v1/gatehouse/gate-events/{id}/register/
/api/v1/gatehouse/gate-events/{id}/reject/
/api/v1/gatehouse/gate-checkpoints/
/api/v1/gatehouse/weighing-tickets/
/api/v1/gatehouse/weighing-tickets/{id}/record-weight-in/
/api/v1/gatehouse/weighing-tickets/{id}/record-weight-out/
/api/v1/gatehouse/weighing-tickets/{id}/close/
/api/v1/gatehouse/weighing-tickets/{id}/reopen/
Service layer¶
backend/gatehouse/services.py:
record_weight_in(ticket, weight, when, user)— фіксує брутто, переводить ticket уweighed_in.record_weight_out(ticket, weight, when, user)— фіксує тару, переводить уweighed_out.close_weighing_ticket(ticket, user)— закриває (вимагає обидва weighings, валідує, що дляin_loaded_out_emptyбрутто > тара).reopen_weighing_ticket(ticket, user)— відкат ізclosedназад уweighed_out/weighed_in.register_gate_event(event, user)—draft→registered.reject_gate_event(event, reason, user)— будь-який статус →rejectedз нотаткою уnote.
Frontend¶
Section composition тримається канонічного порядку груп Master Data → Documents → Reports → Processes (live-dashboards живуть у Processes, не в Reports). Sprint 1 пропускає групу Reports — вона з'являється у S3 (vehicle trace report) і S4-S5 (weighing statistics, shift attendance).
| Маршрут | Компонент |
|---|---|
/gatehouse |
SectionDashboardPage (стандарт) |
/gatehouse/gatehousemasterdata/gatePoints + gateEquipment |
стандартний MasterDataPage |
/gatehouse/gatehousetransactiondata/weighingTickets |
WeighingTicketForm — кастомна форма з кнопками Брутто / Тара / Закрити / Перевідкрити |
/gatehouse/gatehousetransactiondata/gateEvents |
стандартний TransactionList для gateEvents |
/gatehouse/gatehousetransactiondata/gateCheckpoints |
стандартний MasterDataPage (Sprint 3 додає timeline-view) |
/gatehouse/gatehouseprocesses/gatehouseDashboard |
GatehouseDashboard — operator overview (Sprint 2 додасть live WebSocket) |
Sidebar: під Essentials → Plugins → Прохідна та вагова (бо forModules: ["essentials"], appType: "plugin").
Seed¶
python manage.py seed_gatehouse [--clear] (інкрементальний):
- 4
GatePoint(gate_main/gate_personnel/gate_weighbridge/checkpoint_yard_1) - 5
Vehicle(top-up — фактично fleet master data) - 5
Driver+ 5Employee - 10 історичних
GateEventза минулий тиждень (mix personnel / vehicle / anonymous) - 2 закритих
WeighingTicketз реальним net (13.4 / 13.65 т)
Sprint 2 розширить seed equipment-драйверами; Sprint 3 — vehicle gate events з QR-наклейками; Sprint 4 — weighing tickets з ANPR+scale correlator; Sprint 5 — FortNet-CSV-emulator + AttendanceLog історія.
Roadmap (5 послідовних спринтів)¶
| Sprint | Зона | Статус |
|---|---|---|
| S1 | Universal core, manual input, seed v1 | ✅ shipped 2026-04-28 |
| S2 | Hardware bridge layer, EasyOCR ANPR через webcam, 4 драйвери + 3 emulator-CLI, WebSocket live-feed | ✅ shipped 2026-04-28 |
| S3 | Vehicle gate scenario — auto-Vehicle, qr_label_uid, PDF gate-pass, mobile QR scanner, vehicle trace timeline | ✅ shipped 2026-04-28 |
| S4 | Weighing scenario — ANPR+scale correlator у вікні 30с, автоматичні WeighingTicket, bridge у GoodsReceipt одним кліком | ✅ shipped 2026-04-28 |
| S5 | Personnel turnstile + FortNet CSV reference integration + cross-module bridges | ✅ shipped 2026-04-28 |
Sprint 2 deliverables (2026-04-28)¶
Backend¶
POST /api/v1/gatehouse/ocr/license-plate/— EasyOCR lazy-singleton, UA regex post-process, повертає{plate, plate_display, confidence, bbox, all_candidates}. 503 fallback якщоeasyocrне встановлено.GET /api/v1/gatehouse/equipment-drivers/— каталог 6 драйверів з config-schema.POST /api/v1/gatehouse/equipment-events/— REST ingest від device-bridge / webcam-tester. Tenant ownership check, broadcast у WS, опційне auto-створенняGateEvent, оновленняGateEquipment.last_seen_at.- WebSocket
/ws/gatehouse/{tenant_id}/[gate_point_id]/?token=<JWT>— multi-tenant ізоляція (4 close codes: 4001 auth, 4002 no tenant, 4003 mismatch, 4004 gate-point not in tenant). Patten за GPSConsumer. gatehouse/broadcaster.py— sync helperspush_equipment_event/push_gate_event_created/push_weighing_ticket_updated.
Device Bridge (device-bridge/)¶
Окремий Python-сервіс, що працює на ПК оператора. Драйвер-as-plugin registry, asyncio-таски, SQLite outbox для backpressure.
| Driver code | Kind | Транспорт |
|---|---|---|
file_scale_generic |
scale | watch файла |
serial_scale_ascii |
scale | pyserial-asyncio + regex |
serial_scale_cas_ci |
scale | pyserial-asyncio (CAS CI протокол) |
card_reader_emulator |
card_reader | watch файла |
ANPR драйвери (anpr_emulator_http, anpr_webcam_browser) живуть поза device-bridge — перший — CLI emulator, другий — frontend-driven через WebcamANPRTester.
3 emulator-CLI: scale_emulator.py (file pattern, in_loaded_out_empty cycle), card_emulator.py, anpr_cli.py.
Frontend¶
WebcamANPRTester.tsx—getUserMedia+ frame capture + canvas overlay з bbox + manual/auto modes.useGatehouseSocket.ts— auto-reconnect WS hook з ring-buffer 50 events.GatehouseDashboard.tsx— додано WS-індикатор + live-equipment секцію (12 останніх подій).- Sidebar:
Processes → Operator → Webcam ANPR Tester.
Seed v2¶
- 7
GateEquipment(1-3 на кожну прохідну) з прив'язаними driver-codes іconnection_configготовою дляdevice-bridge.toml.
Sprint 3 deliverables (2026-04-28)¶
Backend¶
services.register_vehicle_gate_event(plate, direction, ...)— ANPR plate → lookupfleet.Vehiclebylicense_plate(case-insensitive, dash/space-stripped) → auto-create draft Vehicle if missing → open newGateEvent(subject_type=vehicle, qr_label_uid=...)fordirection=in, OR close the matching open visit fordirection=out. Defensiveneeds_reviewif exit ANPR has no matching entry.services.accept_gate_event(event)— operator one-click promotiondraft → registered. Issuesqr_label_uidif missing.services.record_checkpoint(qr_label_uid, gate_point)— yard QR-scan handler. Looks up the parent visit by UID, createsGateCheckpoint. 30-second idempotency window — re-scanning the same QR at the same post within 30s returns the existing checkpoint instead of duplicating.- Endpoints:
POST /api/v1/gatehouse/gate-events/{id}/accept/— operator accept (auto-issues QR + WS broadcast).POST /api/v1/gatehouse/checkpoints/scan/—{qr_label_uid, gate_point_id}→ 200 with checkpoint payload + WS broadcast; 404 if QR unknown.equipment_event_ingestextended: ANPRkind=anpr_cameranow goes throughregister_vehicle_gate_event(not rawGateEvent.create) — auto-Vehicle, QR generation, direction-aware open/close.- PrintContract
vehicle-gate-pass(backend/gatehouse/print/contracts.py) — A6 landscape PDF with plate, vehicle metadata, gate point, entry timestamp, and an inline-SVG QR encodingqr_label_uid. Template inbackend/gatehouse/print_templates/vehicle-gate-pass/base.html. Frontend mappingvehicleGatePass → vehicle-gate-passadded tolib/printPdf.ts.
Frontend¶
GateOperator.tsx— operator screen at a vehicle gate. Subscribes to per-gate-point WS. Cards for each pending draft event:[Accept + print]/[Reject]. Accept → POST/accept/→ auto-open PDF in new tab. Manual override field for cases when ANPR misses (POST to/equipment-events/withdriver=manual_operator_input). «Currently on territory» list with reprint + trace shortcuts.CheckpointScanner.tsx— mobile-first QR scanner. Useshtml5-qrcode(~50KB) for the rear camera. Decoded UID → POST/checkpoints/scan/. 5-second per-UID dedup на клієнті щоб не флудити при decode'і кадру кадрі. Recent-scans sidebar з trace-shortcuts.VehicleTrace.tsx— MantineTimelineодного візиту:gate-in → checkpoints (chronological) → gate-out. Time deltas between consecutive points. Open-visit (no exit yet) показаний якНА ТЕРИТОРІЇбейдж з біжучим total.- Sidebar (
gatehouse.ts): нова subgroupYard postsпідProcessesзcheckpointScanner+vehicleTrace. Нова itemgateOperatorприєднується доOperatorsubgroup.
Seed v3¶
- 5 closed історичних vehicle visits за минулий тиждень (кожен з
qr_label_uid, 3-4GateCheckpoint15-45 хв apart, парний exit event, ANPR-photo evidence stub). - 1 currently-open visit (авто заїхало ~1h тому, 1 yard checkpoint, без exit) — для дашборда «на території».
User testing flow (DoD)¶
seed_gatehousev3 створює 5 closed visits + 1 open.- Open
/gatehouse/gatehouseprocesses/gateOperator— обратиgate_main. WS-онлайн. - Webcam → photo of plate
АА1234ВВ(з seed) —WebcamANPRTesterпушить ANPR-event → у Operator screen картка з [Прийняти + друк] / [Відхилити]. - Click
Прийняти + друк→ PDF відкривається у новій вкладці (A6 landscape з QR). - На іншій вкладці (як планшет посту) →
/gatehouse/gatehouseprocesses/checkpointScanner/checkpoint_yard_1→ старт сканера → камера на надрукованому QR (PDF на смартфоні) → toast «Чекпойнт записано». - На третій вкладці →
/gatehouse/gatehouseprocesses/vehicleTrace/<event_id>→ Timeline:Заїзд → Чекпойнт → .... - Exit ANPR (через webcam-tester з direction=out) — Timeline закривається
Виїзд, статус →ЗАВЕРШЕНО.
Sprint 4 deliverables (2026-04-28)¶
Backend¶
- Event correlator (
services.correlate_weighing_event). Stateful — per-process in-memory ring buffer ({(tenant_id, gate_point_id) → deque}) з 30-сек expiry. Кожен ANPR + scale event наweighing_station/mixedgate point проходить через корелятор: - якщо у буфері є counterpart-event у вікні 30с → пара утворює
WeighingTicket(open / close залежно від наявності ticket'а для цього vehicle у останніх 24h); - якщо ні → event буферизується, чекаємо counterpart;
- при матчі обидва events видаляються з буфера → одна пара = один тикет, без подвійного створення.
- Open / close decision tree (
_open_or_close_ticket): - 1-ше зважування → новий
WeighingTicket(weight_in, status=weighed_in); - 2-ге зважування → знайти open ticket для того ж vehicle на тому ж gate point у останніх 24h → закрити
weight_out, обчислитиdirection_pattern(weight_in > weight_out→ приймання); - конфлікт (нульова вага, vehicle mismatch) →
status='needs_review'. services.create_goods_receipt_from_weighing(ticket)— bridge уessentials.GoodsReceipt. Створює GR з однимGoodsReceiptLine(item=cargo_item, quantity=net_weight, unit=kg). ЯкщоItem.base_unitу тонах → конверсіяkg → t. Idempotent: повторний виклик повертає існуючий GR замість дубля. Налаштовуєticket.goods_receipt = gr,ticket.status = 'posted'.- Endpoint:
POST /api/v1/gatehouse/weighing-tickets/{id}/create-goods-receipt/— повертає{goods_receipt_id, goods_receipt_number, ticket}+ WS broadcastweighing_ticket_updated. equipment_event_ingestextended: дляgate_point.purpose in (weighing_station, mixed)усі ANPR + scale events ідуть через correlator перш. Тільки коли correlator не утворив пари (буферизується) — fallback до Sprint 3register_vehicle_gate_event(для ANPR на mixed-gate'і).
Frontend¶
WeighingTicketForm.tsxenhancement:- Mantine
Stepper(Чернетка → Брутто → Тара → Закрито → Прихід) — кольори:blueнормально,orangeдляneeds_review,tealдляposted. Описи кроків наповнені фактичними часами. - Кнопка
Створити прихід (GoodsReceipt)(teal) — активна якщоstatus=closed AND direction_pattern=in_loaded_out_empty AND !goods_receipt AND cargo_item AND partner. Tooltip пояснює чому disabled. - Якщо GR уже створений — замість кнопки показуємо
Alert(teal)з посиланням на GR. needs_review-статус показуєAlert(orange)зticket.note.WeighbridgeOperator.tsx— новий screen/gatehouse/gatehouseprocesses/weighbridgeOperator. Картки live-зчитувань (last scale + last ANPR), список відкритих талонів (status weighed_in/weighed_out/needs_review) з підсвіткою needs_review, окремий блок «Готові до приходу» з teal-net-weight.- Sidebar (
gatehouse.ts): додано пунктweighbridgeOperatorуProcesses → Operator.
Seed v4¶
- 3 closed
WeighingTicket(event_in + event_out wired до scale GateEvents наgate_weighbridge); 2 з них уже мають FK у GoodsReceipt черезcreate_goods_receipt_from_weighingпід час seed'у. - 1 open ticket у статусі
weighed_in(авто на території ~35 хв, тара ще не зафіксована). - 1 ticket у
needs_review— симуляція correlator-конфлікту зnote='Suspicious weight 0 kg ...'.
User testing flow (DoD Sprint 4)¶
- Backend + frontend dev + bridge запущені, scale-emulator на
C:/weights/weighbridge.txt(вагова), ANPR через webcam. - Відкрив
/gatehouse/gatehouseprocesses/weighbridgeOperator→ обравgate_weighbridge. - Запустив scale-emulator з
--value 25000→ у Live readings бачиш25000 kg. - У webcam-tester (інша вкладка) → фото номера
AA1234BB→ ANPR пушить event наgate_weighbridge. - Correlator зв'язує ANPR + scale у вікні 30с → автоматично відкривається
WeighingTicket(weight_in=25000)→ з'являється у списку «Відкриті талони». - Перемкнув scale-emulator на
--value 12000(порожнє авто) → знову webcam того ж номера → correlator знаходить open ticket, пишеweight_out=12000, статус →closed,net=13000 kg. - Кліцнув на ticket → відкрилася форма з Stepper'ом на
Закрито+ кнопкаСтворити прихід. - Кнопка
Створити прихід→ новийGoodsReceiptвідкривається у новій вкладці з рядком 13 т зерна, FKweighing_ticketназад.
End-to-end від webcam-фото зі смартфона до проведеного GR без жодного ручного вводу ваги.
Sprint 5 deliverables (2026-04-28)¶
Backend¶
- HRM AttendanceLog model (backend/hrm/models/attendance.py) — daily roll-up з
(in_at, out_at),source ∈ {gatehouse, fortnet, manual},is_openflag для «currently in the building» індикатора. ConstraintUniqueConstraint(tenant, employee, date). Endpointattendance-logs/через стандартнийTenantFilterMixinviewset. - HRM Employee SCUD fields:
external_scud_id,card_uid,access_level (1-4). Indexed для швидкого matching на pull. hrm.services.attendance.upsert_attendance_log_from_gate_event— приймаєGateEventзsubject_type=employee, відповідно оновлюєin_at(earliest) /out_at(latest). Source — derived зevidence(fortnetякщо{source: 'fortnet'}payload є у списку, inakshegatehouse).hrm.services.attendance.recompute_days_worked(period, employee=None)— re-derivesPayrollSlip.days_worked. MVP правило: будь-яка пара(in, out)на дату = 1 робочий день. Тонкощі (нічні, переривчасті) — Deferred. Endpoint action/payroll-slips/{id}/recompute-days-worked/.- GateEvent.source + external_event_id +
UniqueConstraint(tenant, source, external_event_id)— idempotency для replays зовнішнього SCUD pull-у. gatehouse.services.register_personnel_gate_event— Employee resolution byexternal_scud_id→card_uidfallback → silent drop якщо нічого не знайшлось (FortNet seed з невідомими employee'ами не падає halt'ом). Forwards у HRM adapter.gatehouse.services.bridge_to_containerhub_if_installed— якщо ANPR-event несеcontainer_numberі ContainerHub plugin встановлено → створюєcontainerhub.GateTransaction(container, vehicle, transaction_time, transaction_type, evidence). Defensive try/except: bridge non-fatal — Gatehouse продовжує працювати незалежно від ContainerHub schema mismatch.POST /api/v1/gatehouse/external-events/endpoint — універсальний для зовнішніх СКД pull-сервісів. Payload{source, external_event_id, employee_external_id або card_uid, gate_external_id або gate_point_id, event_type IN/OUT, event_time, raw_data}. Idempotent черезregister_personnel_gate_event.integrations/fortnet_scud/Django app зFortnetIntegrationSettings(singleton per tenant: csv_path/db_dsn,last_synced_event_idcursor,pull_interval_sec,gate_mappingJSON),FortnetSyncLog(per-pull audit),pull_events_from_fortnetservice (CSV reader →register_personnel_gate_eventper row),fortnet_pullmanagement command для cron, REST endpoints/integrations/fortnet/sync/+/fortnet/status/.- Seed CSV: backend/integrations/fortnet_scud/seed_data/access_log_2026_04.csv — 50 рядків (5 employees × 5 days × 2 events per day = 25 IN + 25 OUT, з різними access_level).
equipment_event_ingestextended: ANPR зcontainer_numberу payload → післяregister_vehicle_gate_eventвикликаєтьсяbridge_to_containerhub_if_installed.
Frontend¶
FortnetIntegrationPage.tsx(frontend/erp/src/components/Gatehouse/FortnetIntegrationPage.tsx) — admin сторінка: налаштування CSV path / pull interval / enabled, поточний cursor, останній sync log, manual «Sync now» кнопка, історія pull-ів з status badges + counts (+/replay/fail).- gatehouse.ts: новий subgroup
IntegrationsпідProcessesзfortnetIntegrationпунктом (gated byplugin: "fortnet_scud"). - applications.ts + seed_shop:
appFortnetSCUDякappType: "integration",requires: ['appEssentials', 'appGatehouse', 'appManager'],forModules: ['essentials'],installed_by_default=True.
Seed v5¶
- Top-up SCUD-полів на 5 seeded employees (EMP001–EMP005, AABBCC01-05, access_level 1-4).
FortnetIntegrationSettings(csv_path=<seed CSV>, pull_interval_sec=60, is_enabled=True, gate_mapping={'GATE-MAIN': 'gate_personnel'}).gate_mappingмапує FortNet'sGATE-MAINна DOPGatePoint.code='gate_personnel'(бо у нашій схемі gate_main — mixed, а turnstile-only — це gate_personnel).- Реальна історія
AttendanceLogбудується через першийpython manage.py fortnet_pull --tenant <id>після seed-у — це за патерном "seed без дублювання даних, які може створити сервіс".
User testing flow (DoD Sprint 5)¶
- Backend + frontend запущені, seed v5 виконаний.
- Власний канал: запусти card-emulator через лаунчер з
--card AABBCC01→ bridge зловить →card_reader_emulatordriver → POST/equipment-events/→ AttendanceLog дляEMP001зsource='gatehouse'. - FortNet канал: відкрий
/gatehouse/gatehouseprocesses/fortnetIntegration→ бачиш «cursor: пусто, last sync: ніколи». Клацни «Sync now» → ~50 GateEvent створено → 25 AttendanceLog зsource='fortnet'→ видно у/hrm/operations/attendanceLogs/. - Інкрементальний pull: додай вручну рядок у CSV (
event_id=1051,EMP001,...,IN,2026-04-26 09:00:00) → клацни «Sync now» → з'явиться 1 event (replays=49, created=1, failed=0) — cursor рухається уперед. - Recompute days_worked: на
PayrollSlipдляEMP001за квітень → actionrecompute-days-worked→days_worked=5(5 робочих днів з парами IN/OUT у CSV). - ContainerHub bridge: на ContainerHub-tenant'і (де є Containers + GateTransaction model) → ANPR з
payload={plate, container_number}→ у ContainerHub з'являється GateTransaction з тим жеvehicle + container + evidence.
Усі 5 спринтів послідовно дають повний демо всіх сценаріїв через webcam, emulator'и і CSV-FortNet без жодного фізичного hardware.
User testing flow (DoD)¶
python manage.py seed_gatehouseстворює 4 GatePoint + 7 Equipment.cd device-bridge && python -m venv venv && pip install -r requirements.txt.- Заповнити
device-bridge.tomlз JWT bridge-користувача +gate_point_id=<gate_main id>. python device-bridge/main.py— bridge запускається з потрібними драйверами.python device-bridge/emulators/scale_emulator.py --file C:/weights/current.txt --pattern in_loaded_out_empty— генерує ваги.- У браузері
/gatehouse/gatehouseprocesses/gatehouseDashboard—WS онлайн, у Live-feed з'являються події (вага кожні 0.5с). /gatehouse/gatehouseprocesses/webcamAnprTester— дозволити webcam, показати фото номера зі смартфона → bbox з UA-номером + confidence → «Підтвердити» → подія у Live-feed → автоматичнийGateEventу списку.
Status¶
MVP shipped (Sprint 1, 2026-04-28). Production-ready тільки після Sprint 2+ (без обладнання — лише ручний flow).
🔮 Deferred / Ideas¶
Tech debt і розширення після MVP (всі 5 sprints + post-MVP polish 2026-04-30 — 2026-05-07). Закриті пункти переїхали в done.md. Тригер реактивації нижче — або реальний customer-flow, або суміжний модуль, що залежить від цього debt'у.
P1 — production / horizontal-scaling blockers¶
- [ ] Лабораторія якості (
QualityInspection). QC-крок плану візиту тригериться черезgate_point.role.step_kind_system_code='qc'+record_checkpoint(див. business-rules.md § QC checkpoint mechanism). Substitute доти, доки немає реального документа з параметрами огляду / fail-pass / прив'язкою лабораторного протоколу. ПолеVisitPlanStep.quality_inspection_id(loose-FKPositiveBigIntegerField) уже зарезервовано. Локація:essentials_qualityplugin (вже існує — додати документ) або новий core документ. Trigger: реальний customer з вимогами до лабораторії якості. - [ ] ANPR + scale correlator: Redis-backing для multi-worker. Сьогодні
services._recent: dict[(tenant_id, gate_point_id), deque]— in-memory per-process. Один Daphne worker — OK. Горизонтальне масштабування ламається: подія летить у worker А, парна — у worker B, кореляція не відбувається. Public API (correlate_event(payload) → ticket | None) лишається той самий. Див. hardware-integration.md § 7. Trigger: реальний прокід > 1 RPS на гейт або deploy multi-worker. - [ ] VisitPlan materialization edge case — entry без template. Якщо
pick_visit_templateповернувNone(профіль авто не відповідає жодномуapplies_to), візит реєструється, але без плану → exit-block check не спрацьовує, іforce_exitне потрібен. Fallback: за-tenant налаштовуваний «default minimal plan» (тількиentry → exit) щоб шар exit-control завжди працював. Локація:services.py:1594.
P2 — UX / operational ergonomics¶
- [ ] EasyOCR pre-warm на startup.
/api/v1/gatehouse/ocr/license-plate/блокує перший запит ~1–2 хв (download model + warmup). Користувач натиснув «Розпізнати» наWebcamANPRTesterі думає що зависло. Варіанти: (а) async warmup при старті Daphne; (б) окремий celery / RQ worker з ready-стейтом + 503 поки не готово; (в) explicit endpointPOST /ocr/warmup/що запускається cron-ом раз на день. Локація:backend/gatehouse/ocr.py. - [ ] GateOperator settings drawer. TODO-заглушка показує «Coming soon» notification:
frontend/erp/src/components/Gatehouse/GateOperator.tsx. Має бути drawer з: auto-accept whitelist (vehicle-profiles бо template зauto_accept_event=True), exit-without-tare policy (force-exit-без-зважування для VIP), equipment binding mapping (live видно scale/ANPRlast_seen_at). - [ ]
GateEquipment.connection_configschema-validation. Зараз JSON Textarea без runtime-валідації → invalid configs зберігаються → backend driver падає в runtime, error невиразний. Drivers вже виставляють required поля уgatehouse/driver_registry.py. Зробити explicit validator на serializer-рівні:validate_connection_config(value, driver)шукає схему driver'а, перевіряє required + types. - [ ] FortNet integration — повний UI для settings. csv_path / pull_interval_sec / is_enabled уже редаговані; не реалізовано:
db_dsn(production-режим),gate_mapping(zip externalGateID → DOPGatePoint). Без них production-перемикач =manage.py shellдля tenant_admin. Локація:frontend/erp/src/components/Gatehouse/FortnetIntegrationPage.tsx. - [ ] Status
marked≠ delete. Документи Gatehouse мають universalstate ∈ {draft, posted, marked}, але UI не показує marked-стан окремо — він тільки фільтрується з default списку. На GateEvent / WeighingTicket / QueueTicket / VisitorPass треба чіткий індикатор «помічено для видалення» + права на unmark.
P3 — codebase hygiene / docs-debt¶
- [ ]
QueueKiosk+WebcamANPRTester— статус не ясний.QueueKioskуже не вconfig/gatehouse.ts(фактично прихований), але файл живе.WebcamANPRTester— dev-tool без production-use-case. Вирішити: документувати як kiosk-only public або deprecated → видалити. Якщо лишаємо — додати в README.md як reference dev-screen. - [ ]
GateCheckpointitemType=MASTERDATA — misleading. Вgatehouse.tsзаписаний як MASTERDATA, але це read-only journal (immutable child of GateEvent). ІснуючийJOURNAL/LEDGERitemType не повністю підходить. Варіанти: (а) новий itemTypeJOURNAL_READONLY; (б) explicit propreadOnlyJournal: trueна MasterData item; (в) лишити як є + явний коментар (showInMenu:falseуже мітигує). - [ ] Bundle docs не дотягують до
essentials/invoices/reference. Уdocs/ai/domains/gatehouse/єREADME.md,entities.md,business-rules.md. Відсутні:api-contract.md(OpenAPI snippets),test-scenarios.md(Given/When/Then),ui-spec.md(стек-нейтральний UX опис). Заповнити коли почнеться POC-порт Gatehouse в інший стек, або як планове доповнення.