Skip to content

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:

  1. Someone searches the corporate product list and finds the battery BAT-LFP-7250.
  2. They cannot sell it directly — the product has no local name, no local description, no local price, and no inventory routing.
  3. So they duplicate it. Create BAT-LFP-7250-KE. Give it a local name. Set a price. Configure the warehouse.
  4. Next month, Company Togo wants the same battery. Duplicate again. BAT-LFP-7250-TG. French description. Local price.
  5. 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:

  1. 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.
  2. Serviced Account model (ov.serviced_account): The entity representing governed organizational units (internal companies and external clients), with hierarchy, membership, and document branding.
  3. SA-level product assignment (ov.sa_product): Links a Serviced Account to a product.template with governance kind (access/assignment/binding). Deployed and active.
  4. 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.
  5. 62 ov.* models deployed in the live Odoo 18 Enterprise instance at enterprise.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_profile is a standalone foundation object. It is not a duplicate product. It does not carry a company_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_profile links 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 from res.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.company at 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.company default pricelist
  • Currency, tax rules → res.company accounting defaults
  • Warehouse, safety stock, reorder rules → native Odoo (already company-scoped)
  • Supplier, cost method, landed cost → res.company defaults
  • 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_accountproduct.template
ov.sa_product_profile 3 — Governance Which profile does SA use? Configuration binding ov.serviced_accountov.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):

  1. document.company_idres.company → default pricelist, warehouse, currency, tax rules, supplier, cost method.
  2. These are native Odoo mechanisms. The SA does not mediate, override, or route them.

Path B — Profile/Content (SA-mediated):

  1. document.company_id → SA → ov.sa_product_profile for (sa_id=SA, product_tmpl_id=body, state=active).
  2. If found with is_default=True → loads the associated profile (ov.product_profile).
  3. Applies profile content: local_name, local_description, marketing_description, regulatory_notes.
  4. If multiple active profiles exist but none is default → presents a selection or applies a priority rule (TBD).
  5. 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_profile model to the Odoo 18 instance.
  • Deploy ov.sa_product_profile model 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 and UNIQUE(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.template fields (name → local_name).
  • Create ov.sa_product_profile associations linking HM's SA to each profile.
  • Set is_default=True for 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_profile associations.
  • 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_profile records.
  • 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

  1. 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?
  2. Supplier Profiles: Should supplier profiles carry company-specific payment terms, lead times, and quality ratings?
  3. Promotion Model: What does ov.promotion_tag look like? Should promotions be company-scoped, region-scoped, or channel-scoped?
  4. Delivery Zone Model: How are geographic delivery zones defined, and how do they interact with warehouse routing and drop-sales eligibility?
  5. Profile Inheritance: Can a child company inherit a parent company's profile, with overrides? (e.g., OVES Kenya → OVES Kenya Coast Region.)
  6. 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.mdov.* namespace.
  • Implementation Scorecard: docs/reference/implementation-scorecard.md — live Odoo model inventory (62 ov.* 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.