Entity Model — Shared Foundation¶
Status: Implemented — Phase 1 complete (2026-04-13) Audience: Finance, Cap Table, Legal/HR, Engineering Scope: Canonical entity taxonomy, ownership structure, consolidation methodology, and data model for the Groupe Suffren multi-entity reporting platform inside Aletheia.
This document is the single source of truth for how the group's legal entities, ownership, and consolidation scopes are represented in Aletheia. Two roadmaps build on it:
docs/finance-roadmap.md— dashboards, consolidation, cost control, cash flow, revenue analyticsdocs/captable-roadmap.md— individual holder tracking, PV d'AG / ODM generation, shareholder movement workflows
Neither roadmap may redefine the concepts in this document. Changes here propagate to both; downstream roadmaps cite section numbers and refuse to diverge.
Phase 1 implementation status¶
All models proposed in this document have been implemented. The shared foundation is live.
| Table | Implemented in | Status |
|---|---|---|
entities |
apps/entities/models.Entity |
Complete |
ownership_links |
apps/entities/models.OwnershipLink |
Complete |
holders |
apps/entities/models.Holder |
Complete |
ownership_events |
apps/entities/models.OwnershipEvent |
Complete |
aggregation_scopes |
apps/entities/models.AggregationScope |
Complete |
intercompany_elimination_rules |
apps/entities/models.IntercompanyEliminationRule |
Complete |
fiscal_integration_groups |
apps/entities/models.FiscalIntegrationGroup |
Complete |
fiscal_integration_memberships |
apps/entities/models.FiscalIntegrationMembership |
Complete |
period_close |
apps/entities/models.PeriodClose |
Complete |
share_classes |
apps/captable/models.ShareClass |
Complete (cap table roadmap) |
share_register |
apps/captable/models.ShareRegisterEntry |
Complete (cap table roadmap) |
bond_register |
apps/captable/models.BondRegisterEntry |
Complete (cap table roadmap) |
| Practice ↔ Entity FK | apps/practices/models.Practice.entity |
Complete |
Seed data: manage.py seed_group_entities seeds all 11 entities, 10 ownership links, 11 aggregation scopes, 7 intercompany elimination rules, and 1 fiscal integration group. Idempotent.
Test coverage: 33 unit tests + 12 factory classes in apps/entities/tests/.
Deviation from plan: The holders.individual_person field (described as "ForeignKey or TextField" in §4) was implemented as two CharFields: first_name and last_name. This is simpler and avoids a premature FK to the Dentist model.
1. Share class vocabulary¶
The group uses several instrument types. Each must be represented in the data model as its own share_class row, not as a string or enum — we expect to add custom classes in the future (e.g., new regulatory regimes, new foreign jurisdictions).
| Code | Name | Nature today | Capital/Voting Rights (CVR) | Financial Rights (FR) | Notes |
|---|---|---|---|---|---|
| OS | Ordinary Shares | Equity | Yes (pro rata) | Yes (pro rata) | The default instrument. CVR and FR move together. |
| CSa | Class A shares | Equity | Yes | Reduced / split | Used in FR SELAS structures to cap practicing-dentist control. |
| CSb | Class B shares | Equity | Reduced / none | Enhanced | Pairs with CSa to move financial rights to the group. |
| CSc | Class C shares | Equity | Variable | Variable | Minority-holder class (e.g. practitioner at THS holding 20% FR). |
| CL | Convertible Loan | Debt today | None | 100% economic FR of borrower via "prêt participatif" | Interest flows; tax deductible at borrower. Converts to equity on trigger. |
| CB | Convertible Bond | Debt today | None | 100% economic FR of borrower via "prêt participatif" | Same concept as CL, different legal form. Fiscal integration preserved. |
CVR vs FR — two independent cascades¶
This is the most important modelling rule in the document. Capital/Voting Rights (who controls the entity legally) and Financial Rights (who is entitled to the cash: dividends, interest, liquidation proceeds) do not move together in this group's structure.
- CVR cascade answers: "Who votes? Who can legally block a resolution?"
- FR cascade answers: "Whose bank account does the cash end up in?"
They diverge substantially. Example: SDT holds 49% CVR in operating practices but ~100% FR in them. A report that collapses these into a single "% ownership" column will produce wrong numbers.
Any report computing ownership must specify which dimension it means. The UI must show the dimension explicitly; the data model must expose both.
Practicing-dentist pattern¶
French law requires that dentists practicing at a clinic hold at least some shares. Every operating practice (THS, CDA, PDS, VSM) has a bucket of practicing dentists holding ~1% OS cumulatively (typically 1 share each). Legal/HR manages this with frequent small movements (new hires → issue 1 share; exits → buy back 1 share).
This pattern is consistent enough to be a first-class rule, not an exception. The data model treats "practicing dentist" as a holder category, and the cap table roadmap will automate the standard 6-document onboarding / 3-document exit packs off it.
2. Legal ownership tree¶
Reproduced verbatim from the source of truth in temp/pennylane_reusable/ROADMAP_PROMPT.md. Do not rewrite this in downstream documents; link back here.
ASCII tree¶
CHC ─────── FR top holding (new — governance + new FR shareholders)
│ Also acts as service co on occasion (dual-role)
│ Pennylane: soon
│
│ 100% OS
▼
SUI ─────── LU intermediate holding (former top co)
│ Pennylane: no — Excel GL import
│
│ CL → 100% FR of AST (economic, via prêt participatif today)
▼
AST ─────── BE central
│ Pennylane: no — Excel GL import
│ CVR: 100% partner dentists
│ FR : 100% CHC — indirect today via SUI CL;
│ future: also direct CHC→AST CL pro rata for new clinic acquisitions
│
│ CSa + CSb → 49% CVR / 100% FR
▼
SDT ─────── BE central
│ Pennylane: no — Excel GL import
│ 51% CVR : partner dentists
│
├── 100% OS ──────► SDV (FR service company) — 100% CVR / 100% FR
│
├── CSa + CSb ────► THS (FR dental practice) — 49% CVR / 80% FR
│ 20% FR via CSc → practicing dentist (to be acquired by group)
│ 51% CVR → practicing dentists
│
├── CSa + CSb ────► CDA (FR dental practice) — 49% CVR / 99% FR
│ 51% CVR + ~1% OS → practicing dentists
│
├── CSa + CSb ────► PDSh (FR SPFPL) — 49% CVR / 84% FR
│ │ 16% FR via CSc → practicing dentist (to be acquired by group)
│ │ 51% CVR → practicing dentists
│ │
│ └── 99% OS ──► PDS (FR dental practice)
│ ~1% OS → practicing dentists at PDS
│
└── CB ───────────► VSMh (FR SPFPL) — 0% CVR / 100% FR (economic)
│ 100% CVR (OS) → practicing dentist
│ [CB today = debt; fiscal integration VSM-VSMh; interest tax deductible]
│ [On conversion → equity / dividends; loses interest deductibility]
│
└── 99% OS ──► VSM (FR dental practice)
~1% OS → practicing dentists at VSM
Future flow (not yet in place):
CHC ──── direct CL ────► AST (pro rata, for new clinic acquisitions;
combined with indirect SUI CL, CHC still ends
at 100% FR of AST)
Canonical entity table¶
| Code | Country | Type | Pennylane | Direct parent | Parent CVR % | Parent FR % | Instrument(s) | Other stakeholders | Consolidation |
|---|---|---|---|---|---|---|---|---|---|
| CHC | FR | Top holding (+ occasional service co) | Soon | — (ultimate parent) | — | — | — | — | N/A |
| SUI | LU | Intermediate holding | No (Excel) | CHC | 100% | 100% | OS | — | Full |
| AST | BE | Central holding | No (Excel) | CHC (via SUI CL today; future direct CL + indirect) | 0% | 100% (economic, via CL) | CL / prêt participatif | 100% CVR partner dentists | Full |
| SDT | BE | Central holding | No (Excel) | AST | 49% | 100% | CSa + CSb | 51% CVR partner dentists | Full |
| SDV | FR | Service company | Yes | SDT | 100% | 100% | OS | — | Full |
| THS | FR | Dental practice | Yes | SDT | 49% | 80% | CSa + CSb | 51% CVR practicing dentists; 20% FR via CSc (to acquire) | Full |
| CDA | FR | Dental practice | Yes | SDT | 49% | 99% | CSa + CSb | 51% CVR + ~1% OS practicing dentists | Full |
| PDSh | FR | SPFPL holding | Yes | SDT | 49% | 84% | CSa + CSb | 51% CVR practicing dentists; 16% FR via CSc (to acquire) | Full |
| PDS | FR | Dental practice | Yes | PDSh | 99% | 99% | OS | ~1% practicing dentists at PDS | Full (via PDSh) |
| VSMh | FR | SPFPL holding | Yes | SDT | 0% | 100% (economic, via CB) | CB / prêt participatif | 100% CVR practicing dentist (via OS) | Full |
| VSM | FR | Dental practice | Yes | VSMh | 99% | 99% | OS | ~1% practicing dentists at VSM | Full (via VSMh) |
Total as of 2026-04: 11 legal entities. Expected to grow as new clinic acquisitions bring new SPFPL+operating-practice pairs under AST via the direct-CL flow.
3. Consolidation methodology¶
Scope and method¶
- All 11 entities are full-consolidated, including the 49%-CVR operating practices. Full consolidation is justified by shareholder agreements that give the group substantive control (board appointment rights, veto rights, reserved matters) despite the voting minority. This is the standard French dental-group structure — the "non-practicing holder majority CVR / operator majority FR" pattern is specific to the regulatory constraint that only practicing dentists may hold voting control of a clinic.
- No equity method, no proportional consolidation. The data model does not need to support them today, but should not preclude them (a
consolidation_methodcolumn on the entity row keeps the door open).
Minority interests¶
- Minority interests are computed from the FR side, not CVR.
- Partner dentists' CVR majority (e.g., the 51% voting at THS) does not dilute the group's consolidated net income. Only their FR share does.
- Example: for CDA, minority interest on net income = 1% of CDA net income (the ~1% OS held by practicing dentists), not 51%.
- For THS, minority interest = 20% (the CSc bucket held by a practitioner the group intends to acquire), not 51%.
The consolidation engine computes minority interest as (1 - group_FR_pct) × entity_net_income, walking up the FR cascade.
Practicing-dentist bucket¶
- Every operating practice has a
~1% OSbucket for practicing dentists. Legal/HR maintains the exact headcount and share count via the onboarding / exit document packs. - Individual holders are populated from day one — both roadmaps ship Phase 1 together. The cap table workstream populates each practicing dentist as an individual
holdersrow; finance views aggregate them per entity at query time (showing "practicing dentists ~1%" as a grouped line in P&L minority-interest calculations). - The bucket is always modelled as a minority interest at the operating practice level — even though FR is only ~1%, it should roll up through the FR cascade like any other minority.
CL / CB instruments¶
This is the trickiest part of the model. The consolidation engine must get this right.
- Convertible Loans (CL) and Convertible Bonds (CB) here have a "prêt participatif" clause that captures 100% of the economic rights (cash flows) of the underlying entity.
- Today, legally, they are debt. On the borrower's books (AST, VSMh): interest expense flowing up. On the lender's books (SUI, SDT): interest income.
- This arrangement preserves two benefits that would be lost on conversion:
- Fiscal integration (intégration fiscale) — e.g., VSMh-VSM form an integrated tax group in France
- Interest deductibility — interest on the CL/CB is tax-deductible at the paying entity, whereas dividends post-conversion would not be
Consolidation rule: attribute 100% FR to the lender regardless of legal form.
A naive shareholder cascade (% OS × % OS × ...) will produce 0% FR for the group at AST and VSMh, because the group holds no equity there. That is wrong by the economics. The model must encode an explicit "economic FR claim via debt instrument" on the ownership_links row, independent of the equity percentage.
- Interest cash flows today and dividend cash flows post-conversion must both route to the same economic owner (the CL/CB lender).
- Post-conversion, the same
ownership_linksrow flips fromconvertible_debttoequityand the economics stay consistent (but tax treatment changes — a fiscal integration group may break; see §4fiscal_integration_groups).
Conversion events¶
The data model supports conversion as a time-versioned instrument change on an existing ownership_links row, not as a delete-and-recreate. This is important for historical reports: a P&L for FY25 with VSMh's CB still in place must continue to show interest expense at VSMh even after the conversion happens in, say, FY27.
ownership_links gains an effective_from / effective_to pair. Conversion is implemented as:
- Close the existing row at the conversion date (
effective_to = <date>) - Open a new row for the same parent/child with
instrument_type='OS',effective_from = <date>, matching CVR/FR percentages - Log an
ownership_eventsrow withevent_type='conversion', referencing both the old and new links
The UI should preview the consolidated impact (P&L: interest → 0, dividends → X; BS: debt → 0, equity → Y; tax: integration group member change) before executing the event.
4. Data model proposal¶
Proposed Django tables. Names are suggestions; the finance roadmap Phase 1 ticket will finalize them. All tables live in a new apps/entities/ module (not mixed into apps/practices/).
entities¶
One row per legal entity in the group (including future entities).
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
code |
CharField(10) unique | "CHC", "SUI", "AST", "SDT", "SDV", "THS", "CDA", "PDS", "PDSh", "VSM", "VSMh" |
name |
CharField(200) | Full legal name ("Suffren Dental Trust SRL") |
country |
CharField(2) | ISO alpha-2 ("FR", "BE", "LU") |
entity_type |
CharField(choices) | top_holding, intermediate_holding, spfpl_holding, central_holding, operating_practice, service_company |
legal_form |
CharField(50) | SELAS, SELARL, SPFPL, BV, SARL, SA... |
siren |
CharField(9) blank | For FR entities |
siret |
CharField(14) blank | For FR entities (headquarters SIRET) |
vat_number |
CharField(20) blank | For cross-border invoicing |
fiscal_year_end_month |
PositiveSmallInteger | Default 9 (Sept). Per-entity because legal FY may differ. |
fiscal_year_end_day |
PositiveSmallInteger | Default 30. |
pennylane_status |
CharField(choices) | not_applicable, pending, active — drives which ingestion path is used |
pennylane_company_id |
Integer null | Links to Pennylane companies.id when active |
consolidation_method |
CharField(choices) | full, equity, proportional (only full used today; future-proof) |
lifecycle_status |
CharField(choices) | active, in_creation, dormant, dissolved |
created_on |
DateField | Legal creation date |
dissolved_on |
DateField null | Populated when lifecycle_status=dissolved |
acquisition_date |
Date null | Date the entity was first consolidated into the group. Null for founding entities. |
acquisition_fy |
CharField(4) null | Fiscal year of acquisition (e.g., "FY26"). Derived from acquisition_date. Used for vintage grouping and new/existing classification. |
consolidation_start_date |
Date null | Date from which financials are included in consolidated reports. May differ from acquisition_date (e.g., stub period). |
notes |
TextField blank | |
+ AuditModel fields |
Inherits created_at, updated_at, created_by, updated_by |
Seed data: one row per entity in §2's canonical table. CHC starts with pennylane_status='pending'; SUI/AST/SDT start with pennylane_status='not_applicable'; FR operating entities start with pennylane_status='active'.
Share class and register tables — cap table domain¶
The share class registry (share_classes), per-entity class rights (share_register / registre des titres), and convertible instrument tracking (bond_register / registre des obligataires) are owned by the cap table roadmap. Their schemas are defined there, not here. The shared foundation defines only the derived aggregate that finance reads: ownership_links below.
ownership_links¶
The derived/cached finance view. One row per parent→child entity-to-entity ownership relationship, time-versioned. Both CVR and FR percentages are stored; they are independent.
cvr_pct and fr_pct are computed/cached fields, not independently maintained:
- For equity instruments (OS, CSa, CSb, CSc): derived from the share register — (shares_held / shares_outstanding) × class_rights_weight, summed across all classes the parent holds at the child entity.
- For convertible debt instruments (CL, CB): stated from the bond register's contractual_fr_pct / contractual_cvr_pct.
- Recomputed on any cap table event affecting the child entity (share issuance, transfer, buyback, conversion, class rights amendment, new bond issuance, bond conversion) via a service recompute_ownership_percentages(entity).
- A nightly reconciliation job verifies cached values match computed values and alerts on drift.
- Finance reads these cached values directly — it never queries the share or bond registers.
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
parent_entity |
FK entities | The owner (another group entity) |
child_entity |
FK entities | The owned |
instrument_nature |
CharField(choices) | equity or convertible_debt — replaces the old share_class FK. Finance needs to know debt vs equity for elimination rules and conversion tracking; the specific class breakdown is cap table domain. |
cvr_pct |
Decimal(7,4) | 0.0000–100.0000. Computed/cached — see derivation model above. |
fr_pct |
Decimal(7,4) | 0.0000–100.0000. Computed/cached — see derivation model above. |
economic_claim_pct |
Decimal(7,4) null | For convertible_debt: the "prêt participatif" economic share of the borrower (e.g., 100%). Lets FR reports differentiate "FR via equity" from "FR via debt instrument". |
effective_from |
Date | |
effective_to |
Date null | Null = currently in force |
source_event |
FK ownership_events null | Points at the event that created this link |
notes |
TextField blank | |
+ AuditModel fields |
Seed data:
| Parent | Child | Nature | CVR % | FR % | Notes |
|---|---|---|---|---|---|
| CHC | SUI | equity | 100 | 100 | |
| SUI | AST | convertible_debt | 0 | 0 | economic_claim_pct=100 (100% FR via prêt participatif) |
| AST | SDT | equity | 49 | 100 | |
| SDT | SDV | equity | 100 | 100 | |
| SDT | THS | equity | 49 | 80 | |
| SDT | CDA | equity | 49 | 99 | |
| SDT | PDSh | equity | 49 | 84 | |
| SDT | VSMh | convertible_debt | 0 | 0 | economic_claim_pct=100 (100% FR via prêt participatif) |
| PDSh | PDS | equity | 99 | 99 | |
| VSMh | VSM | equity | 99 | 99 |
Individual holders (practicing dentists, partner dentists) are populated from day one via the holders table — see below. Finance views aggregate them per entity to show "practicing dentists ~1%" as a grouped line.
holders¶
Stores individual holder data. Populated by the cap table workstream from day one — both roadmaps ship Phase 1 together, so individual holders are available from the start.
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
holder_type |
CharField(choices) | entity (internal group entity), individual (natural person) |
entity |
FK entities null | Set when holder_type=entity |
individual_person |
ForeignKey or TextField | For individual holders (dentists, partners). Cap table roadmap defines the detail. |
notes |
TextField blank | |
+ AuditModel fields |
Individual holders own shares and bonds tracked in the cap table registers (share register, bond register). The entity-to-entity ownership_links table is the derived aggregate — its CVR/FR percentages are recomputed from the underlying individual-level data. Finance reads ownership_links only; the consolidation engine sums all individual holder FR% per entity at query time to compute aggregate minority percentages (e.g., "practicing dentists at VSM ~1%").
ownership_events¶
Time-versioned audit trail of everything that changes ownership. Drives the cap table roadmap's document automation and the finance roadmap's historical-report consistency.
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
event_type |
CharField(choices) | issuance, transfer, buyback, conversion, csc_acquisition, capital_increase, capital_reduction, dissolution |
entity |
FK entities | The entity whose cap structure changed |
event_date |
Date | |
description |
TextField | |
affected_links |
M2M ownership_links | Links closed or opened by this event |
pv_document |
FileField blank | Optional — signed PV d'AG / DUA |
odm_document |
FileField blank | Optional — signed ODM |
+ AuditModel fields |
created_by is who recorded the event |
Rule: ownership_links rows are never updated in place. All changes go through an ownership_events row that opens new links and closes old ones. This makes every historical report reproducible.
aggregation_scopes¶
Separate from ownership. Models the management reporting tree in §5. Scope membership is computed from entity attributes (country, entity_type, acquisition_date, SPFPL→practice relationship) via rules — not from manually-maintained membership tables. Adding a new entity with the right attributes automatically places it in the correct scopes.
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
code |
CharField(50) unique | e.g., PDS_GROUP, FR_PRACTICES, GROUP_EXCL_BE, VINTAGE_FY26 |
name |
CharField(100) | Display name |
scope_type |
CharField(choices) | practice_group, subtotal, group_total, vintage |
parent_scope |
FK aggregation_scopes null | For tree structure |
membership_rule |
TextField blank | Human-readable rule (e.g., "all FR operating practices where acquisition_fy < current_fy"). Computed membership engine reads this or equivalent programmatic config. |
| + standard fields |
intercompany_elimination_rules¶
Declarative elimination rules. The consolidation engine reads this table; no rule is hardcoded in Python.
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
code |
CharField(30) unique | e.g., SDV_PRACTICE_SERVICES, SUI_AST_LOAN_INTEREST |
description |
TextField | |
from_entity |
FK entities | The "booking" side of the intercompany pair |
to_entity |
FK entities | The counterparty |
p_and_l_lines |
JSONField | List of report lines to eliminate in pairs (e.g. ["Other Incomes", "Legal and Professional"] for a service-co invoice) |
balance_sheet_lines |
JSONField | Typically receivable/payable pair (["Trade receivables", "Trade payables"]) |
effective_from / effective_to |
Date | Rule may be seasonal / start at a future date |
fires_at_scope |
CharField(50) | Scope label at which this rule fires (e.g., "Group excl. BE", "PDS practice group"). Rule fires at this scope and any scope that contains it. |
notes |
TextField |
Seed rules:
- SDV_PRACTICE_SERVICES — SDV revenue ↔ practice cost of service, fires at "Group excl. BE"
- SUI_AST_CL_INTEREST — SUI financial income ↔ AST financial expense on the CL, fires at "Belgium entities total"
- SDT_VSMh_CB_INTEREST — SDT financial income ↔ VSMh financial expense on the CB, fires at "FR practices total"
- AST_SDT_SHARE_CAPITAL — AST investments in SDT ↔ SDT share capital, fires at "Belgium entities total"
- Future: CHC_SERVICE_CO_FEES when CHC starts invoicing in its service-co role
fiscal_integration_groups + fiscal_integration_memberships¶
Tracks French fiscal integration groups (intégration fiscale). This is a declared fact, not mechanically derivable from ownership or instruments — it's a fiscal election with technical conditions that can change independently. The system records the fact; it does NOT try to auto-compute whether integration holds.
fiscal_integration_groups
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
name |
CharField(100) | e.g., "VSMh-VSM integration" |
notes |
TextField blank | |
+ AuditModel fields |
fiscal_integration_memberships
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
group |
FK fiscal_integration_groups | |
entity |
FK entities | |
effective_from |
Date | |
effective_to |
Date null | |
+ AuditModel fields |
Seed data: one group "VSMh-VSM" with two memberships (VSMh, VSM), effective_from = group creation date. Finance team maintains this manually. ~2-3 groups today, growing with acquisitions. The conversion preview feature warns "this entity is in a fiscal integration group — converting the CB may break it."
period_close¶
Per-entity monthly close status. Drives the "as-reported" vs "current truth" dual-view pattern (see §4a).
| Column | Type | Notes |
|---|---|---|
id |
BigAutoField | |
entity |
FK entities | |
period |
CharField(7) | YYYY-MM format |
status |
CharField(choices) | open, in_close, closed |
closed_at |
DateTimeField null | When status moved to closed |
closed_by |
FK User null | Who closed it |
reopened_at |
DateTimeField null | If re-opened |
reopened_by |
FK User null | |
reopen_reason |
TextField blank | Audit trail for re-opens |
+ AuditModel fields |
Unique constraint on (entity, period). Closed months are immutable unless explicitly re-opened (requires CFO-role approval). Re-opening creates an audit trail entry.
Historical report guarantee¶
Every report query must take a as_of_date parameter and filter ownership_links / aggregation_scope_members / intercompany_elimination_rules by effective_from <= as_of_date AND (effective_to IS NULL OR effective_to > as_of_date). This is how a FY25 report stays correct in FY27 even if the ownership structure has changed.
Convention: if a report asks for "the state at the close of period P", as_of_date = last day of P. If a report is live/ongoing, as_of_date = today.
4a. Monthly close workflow and dual-view reporting¶
The finance team operates a monthly close calendar: - ~6th of month+1: first GL extract pulled (preliminary) - 6th–15th: chase missing invoices, correct entries, accountant Q&A - ~20th: final closed accounts
The close status is tracked per (entity, period) in the period_close table (see above). Status progresses: open → in_close → closed.
Late-booking handling. An invoice dated January can arrive in March after January is closed. The GL model handles this with two dates per entry:
- accounting_date — the economic date (January)
- booking_date — when the entry was recorded in the system (March)
Dual-view reporting pattern. Every report in the system supports two views:
- "As-reported" (default) — shows the numbers as they were when the period was closed. Uses the GL mapping version in force at the close date, and only entries that had been booked at or before the close date. Numbers never change. This is the audit-grade view.
- "Restated" (toggle) — shows what the numbers would look like with current knowledge. Uses the current/latest GL mapping (applied retroactively) and all entries by
accounting_date, including late-arriving ones.
The delta between the two views is surfaced explicitly so finance can investigate what changed since close.
This pattern applies uniformly to P&L / BS / CF at any scope, per-entity reports, consolidated reports, and historical comparisons.
Design rule: the close snapshot is preserved as a fact (not re-derived from current data). Reports query the snapshot for "as-reported" and query live GL with accounting_date filter for "restated."
5. Aggregation hierarchy (management reporting tree)¶
The legal ownership tree in §2 answers "who owns what". The management reporting tree answers "what do we want to see on a dashboard". They are different trees, both first-class, neither derived from the other.
Detail level¶
- Practice groups (each is an operating entity + its SPFPL rolled up): THS, CDA, PDS+PDSh, VSM+VSMh. Future acquisitions expand this list (5-10 new practices/year expected).
- Central entities (shown individually): SDV, SUI, CHC. Future: AST-holding part, SDT-holding part when dimension split is implemented.
- Belgium entities (shown individually or combined): AST (full), SDT (full). Future: split clinic/holding via the LOCATION/PROJECT dimension.
Subtotals (dynamic)¶
| Subtotal | Members | Notes |
|---|---|---|
| Existing FR practices | FR practice groups where acquisition_fy < current_fy |
|
| New FR practices | FR practice groups where acquisition_fy == current_fy |
Graduate to "existing" at next Oct 1. |
| Vintage cohorts | FR practice groups grouped by acquisition_fy |
Permanent tag for cohort analysis (FY26 vintage, FY27 vintage, ...). |
| FR practices total | Existing + New | |
| Central costs total | SDV + SUI + CHC | Future: + AST/SDT holding parts. |
| Belgium entities total | Full AST + SDT | Future: clinic/holding/combined split. |
Group totals¶
| Total | Members | Notes |
|---|---|---|
| Group excl. BE | FR practices total + Central costs total | The "France-core" view. A permanent management preference, not a workaround. |
| Group incl. BE | Group excl. BE + Belgium total | The "Groupe Suffren" total — matches the Conso. sheet in the current Excel. |
Design rules¶
- "New" is derived, not tagged. Computed from
entity.acquisition_datevs current FY start (Oct 1). No manual maintenance. - Vintage is permanent.
entity.acquisition_fynever changes — it's the FY in which the entity was first consolidated. Used for cohort views. - The entity list grows. 5-10 new practice acquisitions per year. Each new acquisition auto-creates a new practice group scope. Adding a new practice must NOT require manual scope configuration beyond creating the entity row.
- Belgium is quarantined from the France view because AST/SDT mix clinic and holding activity. Long-term, once the LOCATION/PROJECT dimension split is implemented, Belgium clinics could optionally join "Practices total" and Belgium holding could join "Central total." But "Group excl. BE" remains a permanent view regardless.
- Country is a filtering dimension. "Excl. BE" is not a workaround — it's a permanent management preference for viewing France-core performance without Belgian mix effects.
- Scopes are self-expanding. The aggregation engine computes scope membership from entity attributes (
country,entity_type,acquisition_date, SPFPL→practice relationship) — not from manually-maintained membership tables. Adding a new entity with the right attributes automatically places it in the correct scopes. - Practice group is the default view. The roll-up of PDS+PDSh (or VSM+VSMh) into one line is the default for practice managers. Finance users get a toggle to see per-entity detail.
Implications for aggregation_scopes table¶
Consider replacing the manual aggregation_scope_members M2M with computed scope membership based on entity attributes + rules. This eliminates the maintenance burden of manually adding every new acquisition to multiple scope membership rows. The scope definitions become rules (e.g., "all entities where country=FR AND entity_type=operating_practice AND acquisition_fy < current_fy") rather than enumerated member lists.
If keeping explicit membership tables, require that entity creation auto-populates all relevant scope memberships via a signal/hook.
Relationship to the legal tree¶
The aggregation tree has no direct relationship to the legal tree. Two examples:
- PDSh is the legal parent of PDS (§2), but in the practice group view they're siblings rolled into one line. The management view flattens the legal nesting.
- SDT is the legal parent of SDV, THS, CDA, PDSh, VSMh, but SDT belongs to "Belgium entities total" while its children are in "FR practices total" or "Central costs total."
Any implementation that tries to express one tree as a transform of the other will fail on the first edge case. Keep them separate.
6. Intercompany elimination rules¶
Eliminations are declarative, stored in intercompany_elimination_rules (see §4). The consolidation engine applies the rules that match the requested scope level at query time.
Current known rules¶
| Rule code | Fires at scope ≥ | What eliminates | Why |
|---|---|---|---|
SDV_PRACTICE_SERVICES |
Group excl. BE | SDV revenue from "central costs reinvoicing" ↔ practice "Central costs reinvoicing" expense; corresponding receivable/payable pair on BS | SDV invoices the practices for shared services. |
SDT_VSMh_CB_INTEREST |
FR practices total (contains both parties) | SDT "Financial incomes" (interest income on CB) ↔ VSMh "Financial expenses" (interest expense on CB) | VSMh CB interest flows up to SDT. Eliminated at any scope containing both parties. |
SUI_AST_CL_INTEREST |
Belgium entities total | SUI "Financial incomes" (interest on SUI→AST CL) ↔ AST "Financial expenses" | CL interest flows up. Eliminated at any scope containing SUI and AST. |
AST_SDT_INVESTMENT |
Belgium entities total | AST "Gross Financial fixed assets" (titres de participation SDT) ↔ SDT "Share Capital" + reserves | Equity-method elimination for the 49%+51% CSa/CSb structure. |
PDSh_PDS_INVESTMENT |
PDS practice group | PDSh "Gross Financial fixed assets" (titres de participation PDS) ↔ PDS "Share Capital" + reserves | Same as above for PDSh→PDS. |
VSMh_VSM_INVESTMENT |
VSM practice group | VSMh "Gross Financial fixed assets" ↔ VSM "Share Capital" | |
SDT_OP_PRACTICE_INVESTMENTS |
FR practices total | SDT investments in THS/CDA/PDSh/VSMh ↔ their equity | Top-level equity-method elimination for the Belgian holding's stakes. |
Future rules¶
CHC_SERVICE_FEES— CHC's dual service-co role (to be activated when CHC starts invoicing SDV / practices)CHC_AST_DIRECT_CL— new CL flow from CHC direct to AST, parallel to SUI→AST- Per-conversion-event rules — when CL/CB flip to equity, the existing
*_INTERESTrules close on the conversion date and new dividend / equity rules open
Who maintains these¶
Rules are config, not code. The finance team edits them via an admin UI (or YAML file, see docs/finance-roadmap.md §Decisions needed). Adding a new rule does not require a Django migration, only a database row.
The consolidation engine validates that every elimination is balanced (the two sides net to zero at the requested scope) and reports unbalanced eliminations as warnings on the consolidation run.
7. Resolved questions¶
All questions from the initial draft have been resolved.
-
CHC role modelling — RESOLVED. One entity row. The dual role (top holding + occasional service co) is expressed via aggregation scope membership, GL account / report line attribution (service fee income on specific P&L lines; holding activity on others), and separate intercompany elimination rules for each role.
-
Post-conversion tax treatment — RESOLVED. A lightweight
fiscal_integration_groupconcept has been added to §4. It is a declared fact (not mechanically derivable), tracked witheffective_from/effective_to. Current group: VSMh+VSM. The conversion preview feature warns when a conversion event would remove an entity from its fiscal integration group. -
Individual holders for finance — RESOLVED. Individual holders are populated from day one — both roadmaps ship Phase 1 together. No "aggregated bucket" rows in the schema. Finance views aggregate individual holders per entity at query time. The
holderstable stores individual person data; theownership_linkstable stores the derived entity-to-entity aggregate. See updated §4. -
CSc acquisitions — RESOLVED. The system supports any post-acquisition outcome — hold the CSc as-is, merge into existing class, transform to another class, or create new classes. A CSc acquisition is modelled as one or more
ownership_eventsthat close old ownership_links and open new ones. Finance consolidation just sums FR% across all links for a given parent→child pair (doesn't care about the share class breakdown). Cap table tracks the full class-level detail. -
Fiscal year alignment — RESOLVED. Management reporting FY is Oct 1 – Sept 30 across all entities and countries. The "FYE 31/12" label in the current Excel is a stale typo — ignore it. Legal FY also aligns to Oct-Sept for all current entities. New acquisitions transition to Sept 30 close via shortened/extended transitional year. The
entitiestable includesacquisition_dateandconsolidation_start_dateto handle transitional periods. 5-10 new acquisitions expected per year. -
Practice ↔ Entity mapping — RESOLVED. Single nullable FK on Practice pointing to Entity (
practice.entity_id). One Practice per operating entity (1:1 for operating practices). SPFPL holdings, service co, centrals have NO linked Practice. The SPFPL→operating relationship is already captured inownership_links. -
Minority bucket granularity — RESOLVED. No aggregated bucket rows in the schema. Individual holders populated from day one (see Q3). Finance views GROUP BY entity to show aggregate minority percentages. The "~1% practicing dentists" line in a P&L minority-interest calculation is computed by summing all individual holder FR% for that entity at query time.
-
Chart-of-accounts harmonization — RESOLVED. One
gl_mapping_ruletable with acountrycolumn — not separate per-country tables. All countries map to the same 47 report-line target catalog. Mapping is time-versioned (effective_from/effective_to). Both "as-reported" (mapping version active at close) and "restated" (current mapping retroactively) views are supported. See §4a for the dual-view pattern. -
Monthly vs daily granularity — RESOLVED. Daily granularity is available across the board — both Pennylane (GL entries have dates) and non-Pennylane entities (full journal entries with dates). The unified GL model stores entries at date-level (
accounting_dateper entry, plusbooking_datefor when it was recorded). Monthly views are query-time aggregation. See §4a for the close workflow and dual-date model.
8. Implementation notes¶
All items below are implemented. This section documents where things landed.
apps/entities/owns all 9 tables listed in this document. Share class and register tables (share_classes,share_register,bond_register) are inapps/captable/as planned.apps/practices/models.Practicehas a nullableentityFK pointing toentities.Entity.- All entity-model tables inherit from
apps/core/models.AuditModel(created_at,updated_at,created_by,updated_by). - Seed data is loaded via
manage.py seed_group_entities(idempotent, committed to git). Seeds 11 entities, 10 ownership links, 11 aggregation scopes, 7 elimination rules, 1 fiscal integration group with 2 memberships. - All 9 models are registered in Django admin with custom list displays, filters, search, and inline editors.
- Test suite: 33 tests in
apps/entities/tests/test_models.py+ 12 factory classes inapps/entities/tests/factories.py.
Appendix A: Relationship to existing Aletheia concepts¶
| Aletheia concept | Role in finance model |
|---|---|
apps.practices.Practice |
Operational unit — stays as-is. Gains a nullable entity FK. Not all entities have a Practice (SDV, holdings have none). |
apps.dentists.Dentist + DentistContract |
Source for revenue-per-dentist and associate-fees-per-dentist joins. Matched to Pennylane RETRO <NAME> accounts by name (with manual override UI for mismatches). |
apps.budgets.BudgetVersion |
Per-practice today. In Phase 2 gains an entity FK; BudgetVersion can live at Practice or Entity level (the latter is used for service cos, holdings). |
apps.core.AuditModel |
Base for all new entity-model tables. |
apps.core.permissions.feature_required |
New feature strings (finance_view, finance_manage, consolidation_view, cap_table_manage, etc.) map to existing Finance Manager, CFO, Legal/HR Manager Django groups. |
apps.annuaire.AnnuaireOrganization |
Reference data, not internal entity registry. Has SIRET/SIREN/forme juridique for every FR-registered dental organization (via RPPS/FHIR). Can be used to auto-populate entities.siren/entities.siret for FR entities by lookup, but is not the authoritative source for our legal structure. |
apps.imports.AbstractImporter |
Reusable base for the Excel GL import path (see finance roadmap §Multi-source financial data ingestion). |
End of document. Changes to this file require review by both the finance roadmap owner and the cap table roadmap owner. Downstream roadmaps reference section numbers — avoid renumbering without a deprecation note.