# NACEF × Odoo POS — Addons

Odoo 17 add-ons that turn the native Point of Sale into a Tunisian NACEF-compliant
fiscal cash register (caisse enregistreuse fiscale), packaged for a multi-tenant
SaaS (one database per taxpayer). Requirements traceability lives in
`../planning/traceability_matrix.csv`; the gap analysis in
`../planning/02_gap_analysis.md`.

## Modules

| Module | Layer | Status |
|---|---|---|
| `nacef_smdf_client` | Pure-Python library wrapping the 5 S-MDF web services (models, enums, error catalog, `NacefClient` interface + real HTTP + mock). Endpoints/envelope aligned to the **official OpenAPI spec** (`document/nacef-smdf-api-1.2.0.json`, developers.nacef.tn): Agent on `http://localhost:10006`, paths under `/sic/external`, integer `errorCode`. | **Built & tested** — 21 tests green (standalone) |
| `nacef_fiscal_core` | POS-agnostic fiscal backbone: IMDF config, S-MDF state mirror, append-only hash-chained audit trail, gapless fiscal counter, agent-by-MAC registry, fiscal closings, archives/exports, purge, fleet-health beacon push | **Built & tested** — 33 tests green in Odoo 17 |
| `nacef_pos` | POS coupling: server-side sign-or-block guard, fiscal reference, audit, per-order inalterability chain, closed-period guard, **A3 ticket JSON** (real `Ticket` schema 1.1.4, jsonschema-validated), archive/purge, **receipt fiscal id + QR**, **ticket mentions** (Ticket copie/formation/remboursement), **refund auto-detect**, **TRAINING mode toggle**, **DUPLICATE + reprint lockdown** (E0505) | **Built & tested** — 21 tests green in Odoo 17; OWL bundle verified |
| `nacef_control_plane` | **SaaS superadmin console — runs in its own master DB.** Tenant registry + provisioning lifecycle, **business type** (restaurant/store/café…) → POS preset, per-tenant IMDF/agent(MAC) registries, fleet-health beacons, NACEF supplier back-office (E0102/E0103). Depends only on `base`. | **Built & tested** — 14 tests green in Odoo 17 |
| `nacef_theme` | Modern branded look: teal palette replaces the Odoo purple (navbar, buttons, forms), branded login page, product logo (`static/img/logo.png`). | **Built** — compiled & served (verified) |
| `nacef_cockpit` | Curated company experience: a branded **KPI home dashboard** as the welcome screen; **Cashier / Manager roles**; hides Discuss; makes POS the first app. Cashiers sell only (no fiscal-admin menu, no sensitive KPIs); managers get fiscal administration + full KPIs. | **Built & tested** — 4 tests green in Odoo 17 |

Dependency order (tenant DB): `nacef_smdf_client` → `nacef_fiscal_core` → `nacef_pos`.
The `nacef_control_plane` is independent and installed in a **separate master
database** — it never imports the tenant fiscal modules (operator isolation).

## Verified in a real Odoo 17 (Docker)

```bash
# 1. Postgres + network
docker network create nacef-net
docker run -d --name nacef-db --network nacef-net \
  -e POSTGRES_USER=odoo -e POSTGRES_PASSWORD=odoo -e POSTGRES_DB=postgres postgres:16

# 2. install + run the whole NACEF test suite
docker run --rm --network nacef-net -e HOST=nacef-db -e USER=odoo -e PASSWORD=odoo \
  -v "$PWD/addons":/mnt/extra-addons odoo:17 \
  odoo -d testdb -i nacef_pos \
  --test-enable --test-tags /nacef_fiscal_core,/nacef_pos \
  --stop-after-init --no-http --log-level=test

# teardown
docker rm -f nacef-db && docker network rm nacef-net
```

> **IMPORTANT (filestore):** every Odoo container — server, `-u`/`-i` updates,
> and `shell` — MUST mount the same filestore volume
> `-v nacef-filestore:/var/lib/odoo`. Running an update in a throwaway container
> without it writes the compiled JS/CSS into a discarded filestore, leaving the
> DB pointing at missing asset files → **500 on assets → white screen**. Create
> it once with `docker volume create nacef-filestore`.

Latest result: `nacef_fiscal_core: 33 tests` + `nacef_pos: 16 tests`,
`0 failed, 0 error(s)`. Control plane: `nacef_control_plane: 13 tests`.

## Fleet-health beacon wiring (tenant DB → control plane)

The tenant DB pushes S-MDF health to the control plane via a cron
(`nacef.smdf.state.cron_push_beacons`, every 15 min) that POSTs a metadata-only
payload to `POST /nacef/control/beacon`. Configure on the tenant DB:

- `nacef.control_plane_url` — base URL of the control-plane instance
- `nacef.beacon_token` — shared secret; must equal the control plane's
  `nacef.beacon_token`

The payload carries status / connectivity / offline budget / cert-days / audit
integrity only — never encaissement content (operator isolation).

## Running the tests

Standalone (no Odoo) — the S-MDF client library, including the full 21-step
NACEF-PROCTEST-02 certification scenario against the mock:

```bash
cd nacef_smdf_client && python3 -m unittest tests.test_api -v
```

Integration (needs Odoo 17) — the fiscal-core ORM models:

```bash
odoo -d <db> -i nacef_fiscal_core --test-enable --stop-after-init
```

## What each fiscal-core model covers

- `nacef.smdf.state` — persisted `SMDFManifest` mirror; drives the sale guard and
  the 30-day certificate-expiry warning (E0606).
- `nacef.audit.log` — piste d'audit; `write`/`unlink` disabled (E0902), SHA-256
  hash chain per company+IMDF, `verify_chain()` tamper detection (E1102),
  `export_ascii()` (E0904). Fields per E0903.
- `nacef.fiscal.sequence` — row-locked, gapless, per-IMDF fiscal counter (E0402).
- `nacef.fiscal.closing` — daily/monthly/annual closings; period + perpetual
  totals (E1204/E1205), immutable once closed, hash-chained; turnover computed
  by a hook overridden in `nacef_pos`. Closed-period recording is blocked (E1202).
- `nacef.fiscal.archive` — open-format (JSON / E0904 ASCII) archive of
  encaissement + audit + closings for a period (≤ 1 year, E1301), SHA-256
  integrity seal independent of storage (E1303), downloadable file for the
  fiscal administration to copy to USB (E1702). Encaissement rows come from a
  hook overridden in `nacef_pos`.
- `nacef.fiscal.purge` — archive-first purge (E1401): generates + verifies a full
  archive before removing encaissement, and asserts the perpetual counters and the
  audit trail survive (E1402). Deletion of sealed orders happens only through this
  sanctioned path (`nacef_purge` context).
- `nacef.smdf.agent` — MAC-registered agent per till (SMDF-10), MAC-format check.
- `res.company` / `pos.config` — IMDF, matricule, store id, CE serial, agent URL.

## What `nacef_pos` adds on top of the guard

- Per-order **inalterability chain** (E1101/E1102): each signed order is sealed
  with a SHA-256 hash chained per company+IMDF; protected encaissement fields
  cannot be modified or the order deleted; `nacef_verify_inalterability()`
  detects storage-layer tampering.
- **DUPLICATE / "Ticket copie"** (E0505): `action_nacef_create_duplicate()` — a
  copy is a distinct recorded transaction with a new fiscal number referencing
  the origin, never a free reprint. (Disabling the raw POS reprint is part of
  the pending OWL layer.)

## Decisions still open (block the `nacef_pos` OWL frontend only)

The server-side sign-or-block guard is built and tested. These two decisions
gate the browser layer and the per-order inalterability approach:

1. **Browser → local S-MDF Agent contract** (CORS / localhost TLS / custom scheme).
   Determines how the OWL frontend calls the Ministry Agent (production signing
   path: sign before print, on the till). Until then, signing runs server-side
   via `nacef.client_mode` = `mock` (default) or `agent`.
2. **Odoo edition** (Community vs Enterprise). Enterprise's `l10n_fr_pos_cert`
   hash-chain would be adapted for per-order inalterability (E1101); on Community
   we build it on the same pattern already used in `nacef.audit.log`.

## Defaults adopted (easily changed)

- Odoo 17 **Community** baseline.
- **Currency: TND** (Tunisian Dinar, 3 decimals = millimes, E0507/E0508). `nacef_fiscal_core`
  activates TND on install. A production tenant DB should install the Tunisia
  localization (`l10n_tn`) so TND is the company currency natively; forcing the
  currency on a non-Tunisian chart (as done in the local demo) leaves the
  accounting chart inconsistent and is a demo-only shortcut.
- **stdlib dataclasses** (no Pydantic dependency inside Odoo).
- `MockNacefClient` is the default client for dev/CI until the Ministry S-MDF
  binaries + a `homologation.nacef.tn` test certificate are available.
