ADR 0009: Company Product Profile Framework¶
Status¶
Proposed
Date¶
2026-07-18
Motivation: Why This Matters to Every Operations Team¶
Imagine a platform with twelve operating companies across Africa and Asia, each selling electric vehicles, batteries, spare parts, and energy services. Now imagine the current workflow when Company Kenya wants to add a product to their catalog:
- Someone searches the corporate product list and finds the battery
BAT-LFP-7250. - They cannot sell it directly — the product has no local name, no local description, no local price, and no inventory routing.
- So they duplicate it. Create
BAT-LFP-7250-KE. Give it a local name. Set a price. Configure the warehouse. - Next month, Company Togo wants the same battery. Duplicate again.
BAT-LFP-7250-TG. French description. Local price. - Company Philippines. Duplicate.
BAT-LFP-7250-PH. Subscription pricing. Rental terms.
One battery is now four database records, each with its own identity, its own inventory, its own procurement history, and its own bills of materials.
The Duplication Death Spiral¶
At small scale (2-3 companies, 50 products), duplication looks harmless. The spreadsheet still balances.
But this platform is not small. With 4,581 products, 10+ operating companies, and a product catalog that grows by the month, the duplication approach leads to consequences that compound geometrically:
| Problem | With 3 companies | With 10 companies |
|---|---|---|
| Duplicate products per real SKU | 3 | 10 |
| Divergent BOMs | 3 BOMs, 3 sources of truth | 10 BOMs, engineering cannot guarantee consistency |
| Fragmented reporting | Must union 3 tables | Must union 10 tables, each with different naming |
| API ambiguity | Which product ID is "the battery"? | Every integration breaks when the canonical ID is unclear |
| Inventory reconciliation | 3 separate stock pools | 10 separate stock pools, inter-company transfers = manual Excel |
| Regulatory updates (e.g., battery certification) | Update 3 records | Miss 2 of 10 — two countries sell uncertified product |
| AI/ML ready | Possible but messy | Impossible without a unified data model |
| Maintenance staff needed | 1 person, part-time | Dedicated team of 3-5, full-time, just to synchronize |
| Data integrity | Acceptable | Catastrophic — you can no longer trust any single product as "the truth" |
This is not a theoretical risk. It is the inevitable destination of any multi-company ERP that allows per-company product duplication without a systematic alternative. Every company that has walked this path — from automotive parts distributors to pharmaceutical wholesalers — reaches the same breaking point: the cost of maintaining duplicated records exceeds the cost of building the correct architecture from the beginning.
The Systematic Alternative: Products and Profiles¶
This ADR proposes a different path. Instead of duplicating the product, we give each product named profiles — overlay objects that customize how the product appears and behaves in a specific context.
What Is a Skin?¶
"Skins" (or "themes") are a well-established concept in UI/UX engineering, dating back at least to Winamp (1997) and formalized widely by CSS Zen Garden (2003). The industry consensus is consistent across domains:
| Domain | Body | Skin | Principle |
|---|---|---|---|
| Winamp (1997) | Audio engine, playlist, codecs | .wsz skin: bitmaps, colors, layout |
Same player. Infinite looks. Zero engine changes. |
| CSS Zen Garden (2003) | One HTML document (never modified) | 200+ CSS stylesheets submitted by designers worldwide | Same content. Same structure. Radically different visual identities. |
| WordPress | Core CMS, database, content | Themes: templates, stylesheets, widgets | Same posts, same data. Theme changes the presentation layer only. |
| Video games (Fortnite, CS, etc.) | Character model, hitbox, mechanics | Cosmetic skin: texture, color, accessory | Same gameplay. Different appearance. Cosmetic only. |
| Odoo Website Builder | Page structure, blocks, data | Website themes: colors, fonts, layouts | Same pages. Different look per website. |
The common thread: a skin changes perception and configuration without altering the underlying engine. The body does the work. The skin controls how it looks, what it's called, and how it's configured for a given audience — but it never duplicates, replaces, or owns the engine.
This is exactly the pattern we apply to products. The body (product.template) is the engineering engine — identity, specs, BOM, tracking. A profile (ov.product_profile) is a named overlay — local name, description, and promotion. The SA chooses which profile to use. The body never changes.
Conceptually, profiles are skins: they overlay the body without changing it. Unlike UI-only skins, a product profile carries business content (localization, regulatory text, promotion eligibility) — not operational configuration (pricing, cost, inventory — those belong to the company layer). For this reason we use the broader term profile throughout this ADR and the platform.
Products and Profiles¶
A product has one identity, one BOM, one set of engineering specs. A profile changes nothing about the product — it just configures how the product is presented and operated in a particular context.
Instead of:
Product "BAT-LFP-7250" → duplicate → "BAT-LFP-7250-KE" → duplicate → "BAT-LFP-7250-TG"
We build:
Product "BAT-LFP-7250" (one body)
├── profile: "Kenya B2C Battery" (local name, Swahili description, promotions)
├── profile: "Togo Standard" (French name, local regulatory text)
├── profile: "Philippines Rental" (local commercial name, marketing description)
└── profile: "East Africa Shared" (used by Tanzania + Uganda — one profile, multiple SAs)
Each profile is a named, standalone database record (ov.product_profile). A profile references its parent body (product.template) because it IS a profile of that specific product. A profile carries content — local name, description, marketing text, regulatory notes, and promotion eligibility. It never owns inventory, serial numbers, BOMs, engineering data, pricing, cost, or warehouse configuration. That all belongs to the body and the company layer.
The SA chooses which profile to use. When OVES Kenya sells this battery, its SA picks the "Kenya B2C Battery" profile. When OVES Tanzania sells it, its SA picks "East Africa Shared." Both SAs could share the same profile. The body — BAT-LFP-7250 — never changes. Only the profile changes.
The 100× Advantage¶
This approach is not marginally better than duplication. It is two orders of magnitude better across the dimensions that actually matter for a growing multi-company platform:
| Dimension | Duplication Approach | Central Object + Edge | Improvement |
|---|---|---|---|
| Products to maintain | N companies × M products | 1 × M products + N × M profiles | ~N× fewer product records |
| BOM consistency | Cannot be guaranteed | Guaranteed (one BOM) | Infinite — from impossible to automatic |
| Adding a new company | Duplicate all products + reconfigure all pricing/inventory | Create company profiles (batch) | Days → hours |
| Product recall / regulatory change | Find and update every duplicate | Update one product; profiles auto-inherit | Hours → minutes |
| Cross-company reporting | UNION queries across tables | Single table, GROUP BY company | Complex → trivial |
| API integration | Ambiguous product identity | One canonical product ID | Fragile → reliable |
| AI agent reasoning | Must reconcile multiple identities | One identity, predictable | Unreliable → deterministic |
| Odoo upgrade safety | High risk — duplicate records break during migrations | Low risk — profiles are independent custom models | Fragile → stable |
For a platform that will add companies, products, and business models for years to come, the choice is clear. Build the edge model once. Every future company benefits. Every future product benefits. Every future integration benefits.
This ADR defines how.
Context¶
dirac-odoo already defines:
- V3 association model (ADR 0003): Explicit edge objects (
ov.sa_x,ov.actor_sa_x) as the default for SA-governed visibility scope. No direct stamping on native Odoo models. - Serviced Account model (
ov.serviced_account): The entity representing governed organizational units (internal companies and external clients), with hierarchy, membership, and document branding. - SA-level product assignment (
ov.sa_product): Links a Serviced Account to aproduct.templatewith governance kind (access/assignment/binding). Deployed and active. - Central Object + Contextual Edge principle (
docs/concepts/company-profiles.md): Architectural separation of immutable Corporate Objects from contextual Edge Objects that carry company-specific, SA-specific, channel-specific, or region-specific behavior. - 62
ov.*models deployed in the live Odoo 18 Enterprise instance atenterprise.omnivoltaic.com, with a consistent two-tier SA→actor association pattern covering 32 business object domains.
The gap: the current ov.sa_product model answers "which SA has access to this product?" (governance/visibility). It does not answer "how does this company localize this product?" — local name, description, marketing text, regulatory notes, and promotion eligibility.
This ADR fills that gap.
Decision¶
Adopt a Product Profile framework that extends the V3 association-model pattern with a clear three-layer separation.
Three-Layer Model¶
The architecture separates three concerns that should never be conflated:
| Layer | Model Class | Role | Purpose | Owned By |
|---|---|---|---|---|
| 1 — Native Odoo | product.template |
The Body | Immutable engineering truth: identity, specs, BOM, tracking. One physical record. | Engineering |
| 2 — Extended Logic (Foundation) | ov.product_profile |
The Profile | Named custom overlay: local name, description, promotion. A standalone, indexed, persisted content model. Each product can have many profiles. Carries no company reference. | Operations |
| 3 — Governance (Association) | ov.sa_product + ov.sa_product_profile |
Who uses which profile | SA association edges: which SA can access the product body, and which SA uses which profile. Governs visibility and configuration binding. | SA Manager |
Key Design Rules¶
ov.product_profileis a standalone foundation object. It is not a duplicate product. It does not carry acompany_id. It is a pure content model — indexed, persisted, reusable across any number of company boundaries.- SA governs the profile binding. A Serviced Account (SA) represents a company's governance boundary.
ov.sa_product_profilelinks an SA to one or more product profiles for a given Corporate Product. The SA determines "which profile does this context use?" — not the profile itself. - Financial, Inventory, and Pricing are Company-aligned — NOT modified by SA. An SA is set up to associate with a company (
res.company). Once that company is associated, all financial, inventory, and pricing references are derived fromres.company— not from the SA. Odoo's native pricelist, warehouse, currency, supplier, and cost management is intact and in use. The profile does not carry any pricing, cost, or inventory fields. - Profiles cover two content domains: Localization (name, description, marketing, regulatory) and Promotion (eligibility, tags). Pricing, Cost, Inventory, and Drop Sales are NOT profile domains — they are resolved from
res.companyat runtime using native Odoo mechanisms. - Transactional documents reference the Corporate Product. The profile is resolved via:
document.company_id → SA → ov.sa_product_profile. Financial/inventory/pricing defaults are resolved via:document.company_id → res.company. Two parallel, independent paths. - The pattern generalizes. Product is the first domain. The same three-layer approach applies to other extended logic models as future ADRs.
1. The Two-Layer Product Architecture¶
Layer 1: Corporate Product (product.template)¶
The immutable, corporate-owned source of truth. Owned by Engineering. Never duplicated merely because different companies describe or configure it differently.
| Field Group | Examples |
|---|---|
| Identity | Global Part Number, Engineering Name, Internal Reference |
| Technical | Specifications, Drawings, Product Family, Category |
| Manufacturing | Bill of Materials, Routing, Tracking Method, Serial Number Policy |
| Compatibility | Warranty Compatibility, ABS Compatibility |
| Lifecycle | Lifecycle Status, Discontinuation Date |
One record per real-world product. The platform's 4,581 products map to exactly 4,581 product.template records.
Layer 2: ov.product_profile — The Profile¶
A first-class content object. Each profile row is a named overlay for one product. Profiles are reusable across any number of company boundaries.
| Domain | Fields in Profile | Notes |
|---|---|---|
| Localization | local_name, local_description, marketing_description, regulatory_notes, local_category, sale_enabled, purchase_enabled |
Core profile content. This IS the profile. |
| Promotion | promotion_eligible, promotion_tag_ids, promotion_exclusion_reason |
Shared content. |
Key constraint: No field in this profile owns inventory, stock levels, serial numbers, BOMs, procurement history, pricing, cost, or warehouse configuration. All transactional documents (sale.order.line, purchase.order.line, stock.move, account.move.line) continue to reference the Corporate Product (product.product / product.template). Pricing, cost, inventory, and drop-sales defaults are resolved from res.company on the transactional document — not from the profile. The SA does not mediate them.
How It Fits Together¶
┌──────────────────────────────────────────────────────────────┐
│ LAYER 1: THE BODY │
│ │
│ PRODUCT.TEMPLATE (immutable engineering truth) │
│ id, name, part_number, BOM, tracking, specs... │
│ ^ One body. All transactions reference this. Never dup'd. │
└──────────┬───────────────────────────────────────┬───────────┘
│ │
│ (Layer 3: access governance) │ (Layer 2 → 3: profiles)
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────────────┐
│ LAYER 3: ov.sa_product │ │ LAYER 2: ov.product_profile │
│ (access governance) │ │ (THE PROFILES — named overlays) │
│ │ │ │
│ account_id → SA │ │ name → "Kenya B2C Battery" │
│ product_tmpl_id → Body │ │ product_tmpl_id → Body (profile of │
│ association_kind │ │ this specific product) │
│ state │ │ local_name, description... │
│ │ │ promotion │
│ Answers: │ │ │
│ "Can SA X see this body?" │ │ NO company/SA ref — pure content │
└──────────────────────────┘ └──────────────┬───────────────────────┘
│
│ (Layer 3: which profile to use)
▼
┌──────────────────────────────────────┐
│ LAYER 3: ov.sa_product_profile │
│ (SA chooses which profile to use) │
│ │
│ sa_id → ov.serviced_account │
│ profile_id → ov.product_profile │
│ product_tmpl_id → product.template │
│ is_default → boolean │
│ state → active | inactive │
│ │
│ Answers: │
│ "Which profile does SA X use │
│ for product Y?" │
└──────────────────────────────────────┘
ov.product_profile is a standalone content model. It carries NO company reference. The SA association (ov.sa_product_profile) determines which company (via its SA) uses which profile. Multiple SAs — representing different companies, or different contexts within the same company (B2B vs B2C) — can share the same profile, each with their own default flag.
2. Motivation Section — Embedded in the Model¶
A key design decision for this ADR is that the motivational and educational content — the "why this matters" narrative — is embedded directly in the Product Profile model.
Why Embed Motivation in the Data Model?¶
Operators, company managers, and new staff will encounter product profiles through the Odoo UI, not through ADR documents. When they open a profile form, they should immediately understand why the record exists, what it represents, and why it is not a duplicate product.
Implementation: ov.product_profile as a Transparent Learning Surface¶
| Profile Field | Example Value | What It Teaches |
|---|---|---|
corporate_product_display |
BAT-LFP-7250 — 72V 50Ah LFP Battery Pack |
"This is the one real product. You are configuring a custom profile for it." |
local_name |
Battery Pack 72V 50Ah (Kenya) |
"You name it for your target context without breaking the engineering identity." |
duplication_warning (computed text) |
"This profile configures the same battery referenced by 12 other SA associations across OVES Togo, OVES Philippines, and 7 other companies. Changes to engineering specs come from the corporate product — they propagate to ALL contexts. Contact Engineering (not your local admin) to change technical data." |
Cross-context awareness at the point of configuration. |
related_sa_count (computed) |
Used by 4 Serviced Accounts (OVES Kenya B2C, OVES Kenya B2B, OVES Tanzania, OVES Uganda) |
Visibility into which company boundary SAs share this profile. |
Form View Design Principle¶
The profile form should make the Corporate → Profile → SA relationship visually obvious:
┌─────────────────────────────────────────────────────────┐
│ 🏢 CORPORATE PRODUCT │
│ BAT-LFP-7250 — 72V 50Ah LFP Battery Pack │
│ Part#: BAT-LFP-7250 │ Category: EV Batteries │
│ Engineering: LiFePO4, 72V nominal, 50Ah capacity │
│ │
│ ⚠ This is the one source of truth. 12 SAs use it. │
├─────────────────────────────────────────────────────────┤
│ 📋 PRODUCT PROFILE: "Kenya B2C Battery" │
│ (ov.product_profile — standalone content model) │
│ │
│ [LOCALIZATION] [PROMOTION] │
│ │
│ 📎 Associated SAs: OVES Kenya B2C (default), ... │
│ (managed via ov.sa_product_profile) │
│ │
│ You are configuring localization and promotion │
│ content for this product. You are NOT creating a │
│ new product. This profile can be shared across │
│ multiple SAs. Pricing, cost, and inventory are │
│ managed at the company level (res.company). │
└─────────────────────────────────────────────────────────┘
This design is not cosmetic. It is the primary mechanism by which the "Central Object + Contextual Edge" principle becomes an everyday operational reality rather than an abstract ADR concept.
3. Concrete Model Design¶
ov.product_profile — The Profile (Layer 2 Foundation Content Model)¶
A profile is a named, standalone database record that customizes how a specific Corporate Product appears. Each profile belongs to exactly one body (product.template). Each body can have many profiles.
A profile does NOT know who uses it. That knowledge lives in Layer 3. A profile is pure content — reusable, shareable, independently versioned.
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | ✅ | Human-readable profile identifier (e.g., "Kenya B2C Battery", "East Africa Standard"). |
product_tmpl_id |
Many2one → product.template |
✅ | The body this profile belongs to. A profile IS a profile of this specific product. |
active |
Boolean (default True) | ✅ | Archive without deleting. |
state |
Selection: draft, active, suspended, retired |
✅ | Lifecycle of this profile. |
sequence |
Integer | — | Ordering within profile lists. |
Localization¶
| Field | Type | Description |
|---|---|---|
local_name |
Char | Local product display name (e.g., "Betri ya 72V 50Ah"). |
local_description |
Text | Local product description in the relevant language. |
marketing_description |
Html | Marketing copy for the profile's target context. |
regulatory_notes |
Text | Local regulatory requirements, certifications, restrictions. |
local_category_id |
Many2one → product.category |
Optional override to group products differently per context. |
sale_enabled |
Boolean | Can this product be sold in this profile's context? |
purchase_enabled |
Boolean | Can this product be purchased in this profile's context? |
Promotion¶
| Field | Type | Description |
|---|---|---|
promotion_eligible |
Boolean | Is this product eligible for promotions in this context? |
promotion_tag_ids |
Many2many → ov.promotion_tag (future model) |
Tags linking this profile to active promotion campaigns. |
promotion_exclusion_reason |
Text | If not eligible, why? (e.g., "margin below threshold", "regulatory restriction"). |
What Is NOT on the Profile¶
The profile carries only content: name, description, promotion. Everything below belongs to the company layer (res.company) — resolved natively at runtime, not mediated by the SA:
- Pricelist →
res.companydefault pricelist - Currency, tax rules →
res.companyaccounting defaults - Warehouse, safety stock, reorder rules → native Odoo (already company-scoped)
- Supplier, cost method, landed cost →
res.companydefaults - Fulfillment strategy, inter-company transfers →
res.company/ warehouse configuration - Drop sales configuration, delivery zones, lead times →
res.company/ website configuration
Uniqueness Constraint¶
UNIQUE (name, product_tmpl_id)
One profile name per body. "Kenya B2C Battery" and "Kenya B2B Battery" are distinct profiles for the same body.
ov.sa_product_profile — Which Profile to Use (Layer 3 SA Association Edge)¶
This model answers: "Which profile does SA X use for product Y?"
| Field | Type | Required | Description |
|---|---|---|---|
sa_id |
Many2one → ov.serviced_account |
✅ | The SA (company boundary) using this profile. |
profile_id |
Many2one → ov.product_profile |
✅ | The profile. |
product_tmpl_id |
Many2one → product.template |
✅ | Redundant FK for efficient lookup (derivable from profile, stored for query speed). |
is_default |
Boolean (default False) | — | Is this the default profile for this SA + product combination? |
state |
Selection: active, inactive |
✅ | Association lifecycle. |
date_from |
DateTime | — | When this SA started using this profile. |
date_to |
DateTime | — | When this SA stops using this profile (if applicable). |
Uniqueness constraint: UNIQUE (sa_id, profile_id)
Usage pattern: An SA can use multiple profiles for the same product body (e.g., B2C and B2B profiles). The is_default flag selects which profile is applied when the context is unambiguous.
Relationship to Existing Models¶
| Model | Layer | Role | Purpose | Binding Target |
|---|---|---|---|---|
product.template |
1 — Native Odoo | The Body | Immutable engineering truth | — |
ov.product_profile |
2 — Extended Logic | The Profile | Custom content (local name, description, marketing, regulatory, promotion) | product.template (profile of this body) |
ov.sa_product |
3 — Governance | Can SA see the body? | Access control | ov.serviced_account → product.template |
ov.sa_product_profile |
3 — Governance | Which profile does SA use? | Configuration binding | ov.serviced_account → ov.product_profile |
4. Beyond Products: Framework Generalization¶
The "Central Object + Contextual Edge" pattern, with the three-layer separation, applies across the platform. Each domain follows the same template:
| Domain | Layer 1 (Native Odoo) | Layer 2 (Extended Logic / Foundation) | Layer 3 (Governance Association) | Status |
|---|---|---|---|---|
| Product | product.template |
ov.product_profile |
ov.sa_product_profile |
This ADR |
| Service | product.template (service type) |
ov.service_profile |
ov.sa_service_profile |
Future ADR |
| Asset | ov.asset |
ov.asset_profile |
ov.sa_asset_profile |
Future ADR |
| Fleet | ov.fleet |
ov.fleet_profile |
ov.sa_fleet_profile |
Future ADR |
| Warranty | ov.warranty_template (future) |
ov.warranty_profile |
ov.sa_warranty_profile |
Future ADR |
Each future ADR follows the same structure: 1. Identify the Corporate Object and its immutable fields 2. Define the Company Edge and its configurable fields 3. Resolve the governance (SA) vs. configuration (Company) separation 4. Define uniqueness, lifecycle, and transactional resolution rules
5. Transactional Resolution: How the System Uses Profiles¶
The Company Product Profile is not stored on transactional documents. This is deliberate.
Resolution at Runtime¶
When a user in Company X creates a sales order, two parallel resolution paths run:
Path A — Financial/Inventory/Pricing (Company-aligned, SA not involved):
document.company_id→res.company→ default pricelist, warehouse, currency, tax rules, supplier, cost method.- These are native Odoo mechanisms. The SA does not mediate, override, or route them.
Path B — Profile/Content (SA-mediated):
document.company_id→ SA →ov.sa_product_profilefor(sa_id=SA, product_tmpl_id=body, state=active).- If found with
is_default=True→ loads the associated profile (ov.product_profile). - Applies profile content:
local_name,local_description,marketing_description,regulatory_notes. - If multiple active profiles exist but none is default → presents a selection or applies a priority rule (TBD).
- If NOT found → uses bare body display name. Logs: "Product Y has no active profile for SA X. Using body defaults."
What the Transaction Stores¶
| Document | Stored Reference | What Comes from Profile |
|---|---|---|
sale.order.line |
product_id (Corporate Product) |
local_name, marketing_description |
purchase.order.line |
product_id (Corporate Product) |
— |
stock.move |
product_id (Corporate Product) |
— |
account.move.line |
product_id (Corporate Product) |
— |
The profile is a runtime content resolver, not a data stamp. If the profile changes later, existing orders reflect the profile at the time of order creation (the profile ID is captured, not its overridable fields). Financial, inventory, and pricing values come from res.company at creation time and are stored on the transaction.
6. Migration Path¶
Phase 1: Create the Models (Week 1)¶
- Deploy
ov.product_profilemodel to the Odoo 18 instance. - Deploy
ov.sa_product_profilemodel as the SA association edge. - Add form views, tree views, search views with the motivational UI design described in Section 2.
- Define access rights: SA Managers (write SA associations), Operations (write profiles), Company Users (read).
- Add
UNIQUE(name, product_tmpl_id)on profiles andUNIQUE(sa_id, profile_id)on associations.
Phase 2: Pilot with HM Company (Week 2)¶
- HM Company's SA maps to its
res.company. Create the initial profile set. - Batch-create profiles from existing
product.templatefields (name → local_name). - Create
ov.sa_product_profileassociations linking HM's SA to each profile. - Set
is_default=Truefor each association. - Test: verify that sales orders resolve profiles correctly via the SA → profile lookup path.
Phase 3: Roll Out to Second SA (Week 3)¶
- Select the next operating company (e.g., OVES Kenya). Identify or create its SA.
- Create profiles for their product subset — or reuse existing profiles if the same configuration applies.
- Create
ov.sa_product_profileassociations. - Populate localization and promotion fields.
- Test: verify that each SA resolves its own profiles (or shared profiles) correctly.
Phase 4: Deprecate Per-Company Duplicates (Week 4+)¶
- Identify any products that were duplicated per company before this framework existed.
- Map duplicates back to the Corporate Product.
- Migrate company-specific data from duplicates into
ov.product_profilerecords. - Set duplicates to inactive/archived.
- This phase is optional — companies that never duplicated products skip it.
Phase 5: Generalize (Ongoing)¶
- Apply the same three-layer pattern to Assets, Fleets, and other domains as separate ADRs.
- Each domain reuses the same structural template: Layer 1 (native) → Layer 2 (foundation profile) → Layer 3 (SA association).
7. Relationship to Existing ADRs¶
| ADR | Relationship |
|---|---|
| ADR 0003 (V3 Association Model) | This ADR extends the association/edge pattern from governance (SA→entity access) to configuration (Company→entity profile). Uses the same "don't stamp native models" principle. |
ADR 0002 (ov.* Namespace) |
ov.product_profile follows the ov.* namespace convention. |
| ADR 0005 (SA-POS Governance) | SA governs retail transactions. Company profiles govern how products are configured for retail across companies. Complementary — SA provides access, Company provides configuration. |
| ADR 0004 (Fleet Governance) | Future generalization target. Fleet profiles follow the same pattern: one Fleet (corporate), many Company Fleet Profiles. |
8. Open Questions for Future ADRs¶
- Customer Profiles: When a customer belongs to multiple companies (e.g., a pan-African distributor), does the profile belong to the customer or to the company-customer relationship?
- Supplier Profiles: Should supplier profiles carry company-specific payment terms, lead times, and quality ratings?
- Promotion Model: What does
ov.promotion_taglook like? Should promotions be company-scoped, region-scoped, or channel-scoped? - Delivery Zone Model: How are geographic delivery zones defined, and how do they interact with warehouse routing and drop-sales eligibility?
- Profile Inheritance: Can a child company inherit a parent company's profile, with overrides? (e.g., OVES Kenya → OVES Kenya Coast Region.)
- Profile Audit Trail: Should profile changes be logged for compliance? (Who changed the local name? When? Which SA reassigned a profile?)
Cross-References¶
- Concept document:
docs/concepts/company-profiles.md— "Central Object + Contextual Edge" architectural principle (Layer 2 foundation objects + Layer 3 SA governance). - ADR 0003:
docs/adr/0003-v3-association-model-migration-pattern.md— V3 association model (Layer 3 pattern:ov.sa_x+ov.actor_sa_x). - ADR 0002:
docs/adr/0002-ov-model-namespace-and-semantic-independence.md—ov.*namespace. - Implementation Scorecard:
docs/reference/implementation-scorecard.md— live Odoo model inventory (62ov.*models). - Odoo Extension Architecture:
docs/concepts/odoo-extension-architecture.md— graph model (nodes + edges).
Decision Summary¶
| # | Decision | Resolution |
|---|---|---|
| D1 | Model class | ov.product_profile — a standalone Layer 2 foundation object. Each profile belongs to exactly one body (product.template), which it MUST reference because it IS a profile of that specific product. A body can have many named profiles. |
| D2 | Binding mechanism | Not via a company_id field on the profile. Binding is through Layer 3: ov.sa_product_profile records which SA (company boundary) uses which profile. Multiple SAs can use the same profile. One SA can use multiple profiles for the same body (with is_default). |
| D3 | Relationship to ov.sa_product |
Independent but coexisting in Layer 3. ov.sa_product = "can SA X see this body?" (access). ov.sa_product_profile = "which profile does SA X use for this body?" (configuration). Both follow the ADR 0003 association edge pattern. |
| D4 | Context granularity | The ov.product_profile is context-agnostic — it can serve company-level, channel-level, or region-level use. Context is determined by which SA associates with it. |
| D5 | Transaction reference | Transactions store only the Corporate Product. Profile is resolved via Path B (document.company_id → SA → ov.sa_product_profile). Financial/inventory/pricing defaults are resolved via Path A (document.company_id → res.company). Two parallel, independent paths. Profile changes do not retroactively affect existing transactions. |
| D6 | Template vs. variant | Profiles bind to product.template. Variant-level profiles are a future consideration (most OVES products are not heavily variant-dependent). |
| D7 | Non-product adoption order | Asset → Fleet → Service → Warranty. Each follows the same three-layer template. |
| D8 | Pricing/Cost/Inventory: off the profile | The profile carries NO pricing, cost, or inventory fields — not even overrides. These domains are resolved directly from res.company at runtime, using native Odoo mechanisms. The profile is a pure content model: name, description, promotion. |
| D9 | Inventory: warehouse and stock | Warehouse, safety stock, reorder rules, and fulfillment strategy are managed by native Odoo at the company level. The profile does not carry any inventory configuration. |
| D10 | Promotion model | Profile carries promotion_eligible (boolean) and promotion_tag_ids (M2M to future ov.promotion_tag). The promotion campaign model itself is a separate future ADR. |
| D11 | Drop Sales routing | Drop sales configuration (delivery zones, lead times, fulfillment strategy) belongs to res.company / website configuration, not to the profile. The actual routing engine is a separate future ADR. |
| D12 | Profile sharing across SAs | Multiple SAs can associate with the same ov.product_profile via separate ov.sa_product_profile records. This allows "East Africa Standard" to be shared by OVES Kenya, OVES Tanzania, and OVES Uganda without data duplication. |
| D13 | Multiple profiles per SA | One SA can have multiple ov.sa_product_profile associations for the same product — e.g., B2C and B2B profiles. The is_default flag selects which one applies when the context is ambiguous. |