maptara_san_provider_base reworkTake the 4 658-line "vendor-neutral" module that contains 106 lines of one vendor's vocabulary and one vendor's cron, and turn it into an actual base — by moving the eKV domain out of it, not by renaming things until the grep is clean.
Three things changed after the first draft, and together they flip the recommendation.
1. The estimates were padded. They carried buffer for review cycles and migration verification and then presented that buffer as coding time. Re-cut in §5: W1 ≈ 2 h, W2 ≈ 1 d, W6 ≈ 2 h. Phase 1 is 1.5–2 days, not 4. R3 is the only figure in the programme that does not compress — §7 says why.
2. A production-read database is available (confirmed 2026-09-19). The upgrade hazard in §6 is now verifiable instead of theoretical. R1-d is closed.
3. Option B is deferred, possibly permanently. R2 — the driver contract — goes first. The SWOT in §3a settles it in one line: R2 needs no migration at all; B is nothing but migration.
lines, 23 files
class definitions and field definitions
methods, ~182 asserts, 8 files
lines. Not a typo.
Four files carry 84% of the weight:
Red bars are the eKV domain. That is 1 366 of the 2 232 non-test, non-migration Python lines — 61% of the module doing something that has nothing to do with being a provider base.
169 lines match egeko|eGeKo|Egekonummer|optadata|X01|A01|Sonstige; 106 outside tests/, in 10 of the 14 non-test Python files. Only four non-test files are clean: two __init__.py, the confirm wizard, and one migration.
| Kind | Examples | Cost to fix |
|---|---|---|
| Structural — renaming Egeko breaks it | post_init_hook_egeko (__manifest__.py:18, __init__.py:2, hooks.py:11); _ensure_egeko_cron (hooks.py:25); the cron record itself (hooks.py:54); _auto_init re-creating it every -u (sale_order.py:328-330); action_new_egeko_code bound from XML (wizard:498 + views:44) |
Needs a migration — the cron is noupdate |
| Vocabulary — cosmetic but user-visible | Labels 'eGeKo KV-Nr', 'Let eGeKo assign the KV-Nr'; chatter "eKV NOT accepted — no Egekonummer returned" (maptara_provider.py:739); the eGeKo web-viewer footer (:761-763); "sent as X01 (Sonstige)" / "A01 for a prescription" (wizard:177-180); 8 XML column labels |
Cheap, plus i18n/de.po churn |
| Assumptions — no vendor name, all vendor | The poll-then-fetch-documents sequencing is Egeko's three-call protocol, in base, with 17 "Egeko cron:" log lines (sale_order.py:418-502); the activity document_type selection is Egeko's operation list (maptara_provider.py:499-507); _get_ref_family is built around opta data's German refusal string and their base/base-1/base-2 collision scheme (wizard:434-496); EKV_APPENDIX_MIMETYPES is a capability declaration wearing a constant's clothes (:22-24) |
This is the actual work |
And the one that should settle the argument: views/sale_order_views.xml:25 is a comment in base's XML citing maptara_san_provider_egeko/models/maptara_provider.py:211 by file and line. The base documents itself against its own plugin. 🫠
| Model | Fields | Verdict | Reasoning |
|---|---|---|---|
maptara.provider.document.typemaptara_provider.py:64 | 4 | keep in core | "A document type maps to a provider code" is precisely the driver-layer concept, already generic. Zero changes. |
maptara.provider.uommaptara_provider.py:133 | 3 | delete | Defined, ACL'd (ir.model.access.csv:12-13), shown in the form (views:93-97), and uom_ids is read by nothing in either module. A model whose entire job is appearing in a form. Free deletion. |
maptara.service.providermaptara_provider.py:157 | +11 | keep move 2 down split later | Credentials, branches and routing are genuinely generic. Move name and active down into maptara_base — that alone frees 15 of the 22 modules in the closure from caring about this layer. The deeper dual-purpose split is §9. |
maptara.provider.activitymaptara_provider.py:461 | 17 | keep shell move the state machine | "An exchange happened, here is the audit record" is generic and must stay. But document_type (:499) is a five-value eKV state machine, and request_data/response_data are labelled "XML File" (:531, :538) — an SFTP driver ships a fixed-width text file and a REST driver ships JSON. Generalise the labels; move the selection values to the eKV module via selection_add. |
maptara.provider.ref.burnref_burn.py:37 | 5 | rename keep mechanism | An idempotency-token ledger written on its own cursor so a spent token survives a rollback. Excellent, reusable, and its field label is literally 'eGeKo KV-Nr' (:63). Rename the labels, touch nothing else. 23 tests guard the behaviour. |
sale.ordersale_order.py:13 | +6 | move to eKV module | cost_estimate_completed (:27) hardcodes the three-activity Egeko lifecycle at :212-216. The warning centre and the poll cron are eKV, not "provider". |
maptara.send.ekv.wizardwizard:13 | 13 | move to eKV module | 784 lines. It is called send.ekv. There is no reading under which this belongs in a provider base. |
maptara.send.ekv.line.wizardwizard:667 | 10 | move | Actually generic — a two-store file picker with an _exactly_one_source SQL CHECK. Moves with its parent; promote to core only if a second consumer appears. |
maptara.confirm.send.ekv.wizardconfirm:9 | 1 | move fix bug | Moves with its parent. While it is open: action_confirm (:17-20) calls _send_ekv directly, bypassing _materialise_uploads, so a file in upload_attachment_ids on that path silently never becomes a document. |
Two coherent options. I recommend the second, and the argument is not aesthetic.
Rename the strings, rename the hook, de-brand the cron, delete maptara.provider.uom, fix the ACL. One module, one dependency graph, no migration beyond the cron XML-ID.
What you get: a base module whose grep is clean.
What you still have: 1 366 lines of eKV domain inside the "provider base", and azhDirekt inheriting all of it.
Everything in A, plus: maptara_san_provider_base keeps its name and its three dependents and loses its eKV half to a new maptara_san_ekv.
What you get: a base that a §302 billing driver can depend on without inheriting a cost-estimate wizard.
What it costs: an ir_model_data reassignment migration (§6), which is the genuinely fiddly part.
azhDirekt is not an eKV integration. §302 SGB V is billing — claims submitted after delivery, which already has maptara.claim.batch as its home. It never sends a Kostenvoranschlag, never opens the send-eKV wizard, never touches a KV-Nr, and has no use for a 15-minute cost-estimate poll cron.
Under Option A, CR #1197 ships a module that depends on 784 lines of eKV wizard and a warning centre keyed on LEGS positions, neither of which it calls.
Add MIP and ZHP X3 (per CR #1174) and the question stops being "is one more module worth it" and becomes "how many integrations inherit a cost-estimate wizard before someone objects".
It said azh would inherit "a cron it must actively suppress". It would not. Base routes background operations through _log_unroutable (maptara_provider.py:644), which is explicitly the non-raising variant: an azh order with no _get_state_cost_estimate_azh produces a log line, not an error.
So Option A's real penalty is clutter and log noise, not breakage. That makes A materially more defensible than this section originally implied, and it is part of why the recommendation below moved.
Splitting an Odoo module that already has four migration scripts (19.0.0.3, .7, .8, .14) and 112 tests is not free, and the failure mode is not a test going red — it is Odoo deleting records on upgrade because their ir_model_data.module still names the old module. §6 is the whole reason W4 is the expensive package.
"Do MIP and ZHP X3 have committed dates?" is D4 in the programme report and it is still unanswered. If the answer is no, and #1197 slips past this quarter, Option A is not merely defensible — it is correct.
These are not alternatives on the same axis, which is exactly why they kept getting compared badly. B is a filing question — where does the eKV wizard live. R2 is the interface question — how does base call a provider without knowing who it is. You can do either without the other. Only one of them has a deadline.
maptara_san_ekv · 4–6 d| S | Base becomes an honest base: azh depends on ~870 lines, not 4 658. Test suites split along a real seam. The 61%-of-the-module problem simply stops existing. |
|---|---|
| W | 4–6 days for zero behaviour change. Carries the single riskiest migration in the programme. Touches four existing migration scripts. |
| O | Natural moment to also fix the dual-purpose maptara.service.provider. Sets a domain-per-module precedent before MIP and ZHP X3 arrive. |
| T | Stale ir_model_data silently deletes views, menus and the cron on upgrade. A "move-only" diff drifting into a rewrite. If MIP/ZHP never land, it is one extra module serving one consumer. |
| S | ~200 lines. No migration whatsoever — purely additive. The only change that stops provider #5 editing shared code. Builds on an AbstractModel pattern already proven 12× in this repo. |
|---|---|
| W | You are designing an interface against 2 known protocols and 3 unknowns (MIP, ZHP X3, azh in detail). Get it wrong and you version it. |
| O | Makes azh a plugin instead of integration #3. Forces the undocumented context-key smuggling (selected_document_ids, selected_mdm_attachment_ids) into explicit arguments. Makes probe's stage usable by the UI instead of a log line nobody reads. |
| T | A wrong abstraction means churn across every plugin. MIP/ZHP turn out to need something unmodelled. Scope creep into R3. |
R2's worst case is revising an interface nothing outside our own code depends on. B's worst case is a production database missing its cron. That is the entire argument and it does not need a tiebreaker.
The one genuine weakness in R2 — designing against three unknowns — is an argument for keeping it thin, not for delaying it: ship the six operations we can prove plus capability flags, and nothing speculative. No GraphQL driver (no provider in scope uses it), no async, no batch semantics until azh tells us what §302 actually needs.
And the cost asymmetry over time: R2 gets more expensive with every provider added; B costs the same next quarter as it does today. Nobody's pager goes off because a wizard sits in a slightly wrong folder.
| Package | First draft | Actual typing | What the difference was |
|---|---|---|---|
| W1 baseline | 1 d | ~2 h | Running a suite and writing one portal test. Most of it is waiting for Odoo to boot. |
| W2 de-brand | 2 d | ~1 d | Find-and-replace over ~106 lines. 80% of the remaining day is the cron migration. |
| W6 ACL + sweep | 1 d | ~2 h | One CSV line, two groups=, delete a model nothing reads. |
| Phase 1 total | 4 d | 1.5–2 d | The first figure was buffer for review cycles and migration verification, dressed up as coding time. |
| W3, W4, W5 | 6–10 d | unchanged | W4 is dominated by migration verification, which the prod-read DB makes possible but not fast. |
What will not compress, and being fast does not help: the Egeko rewrite in R3. 1 448 lines, one 263-line method mixing ORM reads, VAT arithmetic, MDM downloads and wire vocabulary, with zero tests on payload assembly. Speed is no use when nothing tells you the bytes changed.
Grey bars are deferred or schedule-free. The committed sequence is the three green bars plus R2 — about four days, after which azh is unblocked on everything except the SFTP driver itself.
Run all 112 tests, record the pass count and the wall time, commit the output. Nothing else in this document is safe without a known-good starting line — and the suite is unusually good insurance, because its docstrings cite real incident dates and row counts.
Add the one test that does not exist and that W3 needs: assert maptara_patient_connect_hub's two search() calls on maptara.service.provider still exclude archived rows. Today they inherit that filter by accident.
./odoo test --tags /maptara_san_provider_base green with a recorded count, plus one new red-then-green portal test.Rename post_init_hook_egeko → a neutral name across __manifest__.py:18, __init__.py:2, hooks.py:11; _ensure_egeko_cron → neutral (hooks.py:25); the cron record's display name (hooks.py:54); action_new_egeko_code → neutral in both the wizard (:498) and the XML that binds it (:44). Strip vendor vocabulary from the 8 XML column labels, the chatter block (maptara_provider.py:739-776) and the X01/A01 warning text (wizard:177-180).
The cron is noupdate, so a rename is a migration, not an edit — see §6.
grep -rn 'egeko\|eGeKo\|Egekonummer\|optadata\|X01\|A01' --include='*.py' --include='*.xml' over non-test base files returns 0; suite still green; i18n/de.po updated rather than left pointing at dead msgids.name and active down to maptara_base 1–2 dThe entire Codexa tree — 15 modules — depends on this layer through exactly two column names. Move them and those 15 stop caring which provider module is installed.
maptara_codexa_api installs with maptara_san_provider_base absent.maptara_san_ekv 4–6 d skip for Option AMove, unchanged where possible: both send wizards and the confirm wizard (784 + 19 lines + 173 lines of XML), sale.order's 6 eKV fields and the warning centre (508), the poll cron body (sale_order.py:333-502), and the four eKV-reasoning migrations. The `document_type` selection values move here and re-enter the activity model via selection_add.
Resist rewriting while the files are open. The point of this package is that the diff is moves, so a reviewer can verify it by reading paths rather than logic. Behaviour changes go in R2/R3 where they have their own tests.
Relabel request_data / response_data from "XML File" (maptara_provider.py:531, :538) to something a fixed-width §302 file and a JSON body can also live in. Decide whether url (:518) and data (:520) are used by the plugin or are dead — survey A flagged them as unread by base and did not check the plugin.
Fix security/ir.model.access.csv:9 — base.group_user currently holds 1,1,1,1 on the model storing password and client_key as plain Char. Every internal user can read, edit and delete every ECE credential.
maptara_codexa_api/models/provider_site.py:20 declares a Many2one to maptara.service.provider and is not sudo'd. Strip read from base.group_user and every Codexa user opening a provider-site form gets an access error. The create path at :63-67 is sudo'd, so it would survive — the form would not.
base.group_user | Action |
|---|---|
| read | keep — Codexa's FK needs it |
| write / create / unlink | remove |
password, client_key | add groups= so the fields are invisible regardless of the model ACL |
That removes the credential exposure without touching anyone's foreign key. Field-level groups= is what actually protects the secret; the model ACL just stops casual editing and deletion.
Delete maptara.provider.uom (model, 2 ACL rows, form section). Flip the manifest off baotnp/OPL-1 — though see the programme report's D6 before claiming clean-room authorship.
password or client_key, and can still open a Codexa provider-site form; no model in the module is unreferenced.R2 needs no migration at all. It is purely additive: a new AbstractModel, new methods, and a selection_add registry that already exists. Nothing moves, no ir_model_data is touched, no data is at risk. B is nothing but migration — the code moves are trivial and the entire cost sits in the reassignment.
So the ordering question answers itself. When you do start moving things, go cheapest-and-safest first, so the harness gets debugged on something that cannot hurt you:
| # | Migration | Risk | Verify on the prod copy by |
|---|---|---|---|
| 1 | Delete maptara.provider.uom (W6) | lowest | SELECT count(*) on the table first. Odoo leaves the orphan table rather than dropping it, so the risk is low — but if the count is non-zero, know that you are abandoning rows before you decide, not after. |
| 2 | Cron rename (W2) | contained | Count ir.cron rows matching the poll job, before and after. Must be exactly 1 — not 0, not 2. |
| 3 | name/active → maptara_base (W3) | medium | The portal test from W1, plus maptara_codexa_api installing with provider_base absent. |
| 4 | B — the module extraction (W4) | highest | Row counts on maptara.provider.activity, maptara.provider.ref.burn and the stored warning fields, before and after. |
A production-read database is available (confirmed 2026-09-19), so every row in that last column is actually runnable. Before it existed, none of them were.
When a model or XML record moves between modules, its ir_model_data row still names the old module. On the next upgrade Odoo sees records belonging to maptara_san_provider_base that the new maptara_san_provider_base no longer declares, and does the reasonable thing: deletes them. Views, actions, menus, ACL rows, and — worst — the cron.
A test suite on a fresh database is perfectly happy with this, because a fresh DB has no stale ir_model_data. The failure only appears on an upgrade of an existing database, which is to say: in production.
Mitigation: a pre-migration on the new version that reassigns ir_model_data.module for every moved XML ID before the registry loads. And a test that upgrades a populated database, not just a fresh one.
| Hazard | Why | Handling |
|---|---|---|
The poll cron is noupdate: True |
hooks.py:70-71. Renaming the record does not update installed databases; the old one survives under its old name and the new one is created beside it. Two crons, both firing. |
Migration that renames in place by XML ID, plus a guard against the _auto_init at sale_order.py:328-330 re-creating the old one. |
_auto_init re-creates the cron on every -u |
models/sale_order.py:328-330. Deleting the cron is not a supported state — it comes back. |
Move this with the cron in W4, or the base module keeps resurrecting a job it no longer owns. |
| Four existing migrations reason about Egeko and MIP by name | 19.0.0.3/pre-migrate.py:7,19, 19.0.0.7/post-migrate.py:8, 19.0.0.14/post-migrate.py:20 |
They are historical; do not rewrite them. They move with the eKV module, and any DB that has already passed them stays passed. |
| Manifest version numbers after a rebase | A "keep mine" conflict resolution can land a version lower than the base — and the migration then simply never runs. Silent. | Check the version number is monotonic after every rebase in this programme, not just at the end. |
Field renames on maptara.provider.ref.burn |
Label-only changes are free; renaming the column is not, and the ledger is the one table where losing a row means reissuing a spent KV-Nr. | Rename labels only. Leave column names alone unless someone wants to fund the migration and the risk. |
112 tests, ~182 asserts, all @tagged('post_install', '-at_install'). Every one of them must stay green, and none of them should be edited beyond import paths and module assignment. Survey A's instruction was blunt and correct: do not rewrite the tests as part of the split.
| File | tests | Guards | Lands in |
|---|---|---|---|
test_ekv_internal_ref.py | 23 | KV-Nr never reissued: family scoping not order scoping, numeric not lexicographic suffix sort, burn ledger survives rollback, empty-ref escape hatch | split — ledger to base, allocation to ekv |
test_ekv_media_type.py | 20 | media_type_code returns False not 'X01' when unmapped; per-provider isolation; re-tick on provider change | base |
test_provider_company.py | 18 | Group-wide vs branch-locked credentials, _for_company precedence, both @api.constrains, test_a_removed_branch_stays_removed | base |
test_provider_routing.py | 17 | Unroutable activities raise on foreground paths and log-not-raise in background; ondelete='restrict'; the nine dead stage-test rows | base |
test_ekv_warnings.py | 13 | LEGS warning fires, counts correctly, never blocks, survives empty recordsets | ekv |
test_send_ekv_mdm_source.py | 11 | MDM attachments reach wizard lines without being copied; the _exactly_one_source SQL CHECK | ekv |
test_send_ekv_image_warning.py | 6 | Both empty-appendix states produce distinct banners; the negative case | ekv |
test_send_chatter.py | 4 | Chatter on success, failure and exception; the sent→error downgrade when no external_ref came back | base |
Every one of these runs on a fresh database. Nothing in the suite exercises an upgrade — which is precisely the axis W4 puts at risk (§6). Add one upgrade check against a populated DB and compare row counts on maptara.provider.activity, maptara.provider.ref.burn and the stored warning fields before and after. Real numbers, not a green tick.
| Risk | Blast radius | Who finds out, and when | Mitigation |
|---|---|---|---|
Stale ir_model_data deletes moved records on upgrade |
Views, menus, ACLs and the poll cron vanish on an existing DB | Nobody, until an eKV stops being polled. Feedback loop: days. | Reassignment pre-migration + a populated-DB upgrade test. Non-negotiable for W4. |
| Two crons after the rename | Every order polled twice; duplicate chatter; doubled load on opta data | Fast — the chatter is visible. Unless egeko_state_code dedupe hides it, which it might. |
Rename by XML ID in a migration; move _auto_init with the cron. |
| Archived providers appear in the patient portal after W3 | A public-facing surface lists providers we deliberately retired | Possibly a customer. No error is raised, no test fails today. | The W1 portal test, written before W3 starts. |
| A spent KV-Nr gets reissued | opta data rejects the eKV with the German collision message; at worst two Vorgänge collide | Fast and loud, but customer-visible. | Do not touch ref-burn column names. 23 tests already guard the behaviour; keep them green through every package. |
| Scope drift into R2/R3 | A "move-only" diff becomes a rewrite and stops being reviewable | At review, by which point it is expensive to unwind | W4's exit criterion is explicit: zero edited assertions. If a behaviour change is needed, it belongs to R2 or R3. |
| Option B is built and MIP/ZHP X3 never arrive | One extra module for one consumer | Months | Honestly: survivable. azh alone justifies it. But answer D4 before committing the 4–6 days of W4. |
| Left alone | Why | What breaks if nobody ever does it |
|---|---|---|
Splitting maptara.service.provider's dual purpose — it is both the Leistungserbringer (defined in maptara_base, 25 lines, 3 fields) and the ECE credential holder |
The constraint at maptara_provider.py:311-351 is scoped to "rows carrying a secret" purely to route around this, and its own docstring says "The real fix is to stop the model being dual-purpose". Doing it here would double R1 and touch all 22 modules. |
Every future constraint on this model needs the same "does this row have a secret" dance. Annoying, not dangerous. Revisit when CR #1174's one-config-vs-per-location question is answered. |
The _unique_provider_type constraint (one configuration per provider type, system-wide) |
Flagged by CR #1174, not independently re-verified in this survey. If real, it blocks per-Betriebsstätte credentials. | azh may need one Kundennummer per location. That is an open question in CR #1197, so this gets answered there, not here. |
| The confirm-wizard upload bypass | Real bug, medium confidence, no reproducer traced. It is a behaviour fix, and W4 is a move-only package. | A file attached on the resend-confirm path silently never becomes a document. Fix in R3, or as its own one-line ticket. |
| Rewriting any test | They are the only thing making this refactor verifiable, and their docstrings carry incident history that would be lost. | Nothing. This is the correct call. |
| # | Question | Blocks | Recommendation |
|---|---|---|---|
| R1-a | Option A (scrub) or Option B (split)? | W4 entirely | A now, B deferred — revised 2026-09-19. B is still the right end state; it is simply the only item on the list whose cost does not rise with delay, and it is the only one carrying production-data risk. See §3a. |
| R1-b | Name for the extracted module. maptara_san_ekv follows the existing maptara_san_* convention; maptara_ekv matches the newer tree. | W4 file layout | maptara_san_ekv — consistency with its neighbours beats consistency with the newer naming. Trivial to change now, painful later. |
| R1-c | Does the ACL fix (W6) ship in R1 or wait for a security ticket? | Nothing — it is independent | Ship it in R1. It is a one-line CSV change plus groups=, the file is already open, and "every user can delete every credential" does not improve with age. |
| R1-d | Is there a populated non-production database we can upgrade against? CLOSED | — | Yes — a production-read copy is available (2026-09-19). Every verification step in §6 is now runnable. This was the one hard blocker on the whole migration story. |
Programme: report-provider-driver-refactor.html — R1 is this document; R2 (driver contract), R3 (Egeko rewrite), R4 (SFTP), R5 (REST) and R6 (licence) are still to be written up at this level of detail.
Internal engineering planning document. No code has been modified; this describes work that has not started.