Skip to content

Odoo Mapping Notes (Product Marketing)

Status

This document is retained as historical mapping evidence and provider-design context.

It is not the current normative contract source for marketing-domain modular BFF design.

Current normative sources are:

If this page conflicts with those chapters, treat this page as out of date and in need of later reconciliation.

This document maps the Product Marketing contract to Odoo models and extensions. It is the system of record for product data in DIRAC FED.

Related: - Current frontend repo direction: dirac-uxi-isr - Historical consumer contract artifact: legacy BFF-Contracts-ISR.md - Historical sample payload: legacy ProductMarketingDetail.json

Live Site Validation: This contract reproduces existing production pages: - Category: Shift to Electric (product series grouping) - Detail: ovEgo E-3 PLUS (image gallery, tabs, specs)

Developers can extract real product data from these pages as additional mock data sources.

Scope

  • Products (with BoM, BoP, media, markdown descriptions)
  • Articles (marketing content)
  • Media (images, videos)
  • Bill of Materials (BoM) — 2-level product composition
  • Bill of Properties (BoP) — technical properties with ISO/IEC references
  • Historical Items model (BoM line item references)
  • Properties (BoP property definitions)

Product Data Contract (UXI Consumer View)

ProductMarketingDetail Schema

type ProductMarketingDetail = {
  productKey: string;           // Stable UUID
  slug: string;                 // URL-friendly identifier
  displayName: string;
  tagline: string;
  series: string;               // "LEV_BATTERY", "FLEET_CHARGER"
  category: string[];           // ["Energy Storage", "Urban Mobility"]
  publishState: string;         // "draft" | "published" | "archived"

  // Markdown Technical Description
  generalDescription: string;   // Markdown text with headings, lists, links

  // Media
  mainMedia: MediaAsset;        // Primary hero (image or video)
  mediaList: MediaAsset[];      // Gallery (3-5 images + 1-2 videos)

  // B2B Technical Data
  bom: BillOfMaterials;         // Component list (qty + item ID)
  bop: BillOfProperties;        // Technical properties with ISO/IEC refs

  // Marketing Content
  overview: ContentBlock[];
  features: FeatureBlock[];
  specifications: SpecSection[];

  // Relationships
  relatedProducts: ProductMarketingCard[];
  compatibleAccessories: ProductMarketingCard[];
};

// BoM Structure
type BillOfMaterials = {
  productKey: string;
  lineItems: Array<{
    qty: number;
    item: string;               // Item ID
  }>;
};

// BoP Structure
type BillOfProperties = {
  productKey: string;
  propertyLines: Array<{
    value: number | string;     // Numeric OR "use" | "refer" | "IP67"
    prop: string;               // "Nominal Voltage in V", "Gross Weight in kg"
    referenceUrl?: string;      // ISO/IEC/OVES docs link for tooltips
  }>;
};

// Media Asset
type MediaAsset = {
  type: "image" | "video";
  url: string;
  alt: string;
  width?: number;
  height?: number;
  thumbnail?: string;           // For videos
  duration?: number;            // For videos (seconds)
};

Odoo Model Mapping (Provider Implementation)

Products

Odoo Base Model: product.template
Extensions Required: Custom module oves_product_marketing

Field Mapping:

Contract Field Odoo Field Type Notes
productKey x_product_key Char Custom field, unique, indexed
slug x_slug Char Custom field, unique, indexed
displayName name Char Standard field
tagline x_tagline Char Custom field
series categ_id Many2one(product.category) Map series to product category
category x_category_tags Many2many Custom tag model
publishState x_publish_state Selection [('draft','Draft'),('published','Published'),('archived','Archived')]
generalDescription x_general_description_md Text Markdown text field
mainMedia x_main_media_id Many2one(ir.attachment) Link to attachment with metadata
mediaList x_media_ids One2many(product.media) Custom media relation model
bom bom_ids One2many(mrp.bom) Standard Odoo BoM model
bop x_bop_ids One2many(product.property.line) Custom BoP model
overview description_sale Html Or custom JSON field
features x_features Text (JSON) Serialized JSON
specifications x_specifications Text (JSON) Serialized JSON

Required Extensions: 1. x_product_key (Char, unique, indexed) 2. x_slug (Char, unique, indexed) 3. x_general_description_md (Text) — Markdown field 4. x_main_media_id (Many2one → ir.attachment) 5. x_media_ids (One2many → product.media) 6. x_bop_ids (One2many → product.property.line)


Bill of Materials (BoM)

Odoo Model: mrp.bom (Standard Manufacturing Module)
No custom extensions needed — native Odoo BoM handles 2-level structure.

Field Mapping:

Contract Field Odoo Field Type Notes
productKey product_tmpl_id Many2one(product.template) Parent product
lineItems bom_line_ids One2many(mrp.bom.line) Line items
lineItems[].qty product_qty Float Quantity
lineItems[].item product_id Many2one(product.product) Item reference

Historical usage: - Create mrp.bom record per product - Add mrp.bom.line entries for each component - Query via product_tmpl_id.bom_ids.bom_line_ids

Important: this section reflects the older product-versus-item split and should not be read as the corrected public BFF contract. The corrected intent is one canonical Product object with BoM lines referencing component products.


Bill of Properties (BoP)

Odoo Model: NEW product.property.line (Custom Model)
Module: oves_product_marketing

Model Definition (models/product_property_line.py):

from odoo import models, fields

class ProductPropertyLine(models.Model):
    _name = 'product.property.line'
    _description = 'Product Property Line (BoP)'

    product_tmpl_id = fields.Many2one(
        'product.template', 
        string='Product', 
        required=True, 
        ondelete='cascade',
        index=True
    )

    prop = fields.Char(
        string='Property Description', 
        required=True,
        help='Property with inline unit/standard: "Nominal Voltage in V", "Gross Weight in kg"'
    )

    value = fields.Char(
        string='Value', 
        required=True,
        help='Numeric value (48, 7.2) OR descriptor ("use", "refer", "IP67")'
    )

    reference_url = fields.Char(
        string='Reference URL',
        help='Link to ISO/IEC standard or OVES docs for tooltip'
    )

    sequence = fields.Integer(default=10, help='Display order')

Field Mapping:

Contract Field Odoo Field Type Notes
productKey product_tmpl_id Many2one Parent product
propertyLines[].prop prop Char "Nominal Voltage in V"
propertyLines[].value value Char "48" or "use" or "IP67"
propertyLines[].referenceUrl reference_url Char ISO/IEC URL

Media Assets

Odoo Model: NEW product.media (Custom Model)
Module: oves_product_marketing

Model Definition (models/product_media.py):

from odoo import models, fields

class ProductMedia(models.Model):
    _name = 'product.media'
    _description = 'Product Media Asset'
    _order = 'sequence, id'

    product_tmpl_id = fields.Many2one(
        'product.template', 
        string='Product', 
        required=True, 
        ondelete='cascade'
    )

    media_type = fields.Selection(
        [('image', 'Image'), ('video', 'Video')],
        string='Type',
        required=True
    )

    url = fields.Char(string='URL', required=True)
    alt_text = fields.Char(string='Alt Text', required=True)

    # Image-specific
    width = fields.Integer(string='Width (px)')
    height = fields.Integer(string='Height (px)')

    # Video-specific
    thumbnail_url = fields.Char(string='Thumbnail URL')
    duration = fields.Integer(string='Duration (seconds)')

    sequence = fields.Integer(default=10, help='Display order')

Products

  • Odoo model(s):
  • Key fields:
  • Required extensions:
  • Notes:

Historical Items (BoM References)

Odoo Model: product.product (Standard Product Variant)
No custom extensions needed

Notes: - BoM lineItems[].item references product.product.id - This reflects the earlier split between Product and Item and is now considered historical design debt rather than target public contract. - Items can be products, raw materials, or subassemblies - Query via product_id.name, product_id.default_code (SKU)


Properties (BoP Property Definitions)

Odoo Model: Handled inline via product.property.line.prop field
No separate property registry model needed

Notes: - Properties are not normalized to a separate table - Each product defines its own property lines - Property naming convention: "<Property Name> in <Unit>" - Example: "Nominal Voltage in V" - Example: "Gross Weight in kg" - Example: "IEC 62619 Safety Standard" (no unit) - For reusable property definitions (tooltip text, ISO links), use OVES docs site: - https://docs.oves.com/properties/<property-slug> - Example: https://docs.oves.com/properties/nominal-voltage


GraphQL Resolver Implementation (BFF Layer)

Query: productDetail(slug: String!)

import { OdooClient } from './odoo-client';

const resolvers = {
  Query: {
    productDetail: async (_, { slug }, { odoo }: { odoo: OdooClient }) => {
      // 1. Fetch product template by slug
      const products = await odoo.search_read('product.template', [
        ['x_slug', '=', slug],
        ['x_publish_state', '=', 'published']
      ], {
        fields: [
          'id', 'x_product_key', 'x_slug', 'name', 'x_tagline',
          'x_general_description_md', 'x_main_media_id', 'description_sale',
          'x_features', 'x_specifications'
        ]
      });

      if (!products.length) return null;
      const product = products[0];

      // 2. Fetch BoM
      const boms = await odoo.search_read('mrp.bom', [
        ['product_tmpl_id', '=', product.id]
      ], { fields: ['id'] });

      let bomData = null;
      if (boms.length) {
        const bomLines = await odoo.search_read('mrp.bom.line', [
          ['bom_id', '=', boms[0].id]
        ], { fields: ['product_qty', 'product_id'] });

        bomData = {
          productKey: product.x_product_key,
          lineItems: bomLines.map(line => ({
            qty: line.product_qty,
            item: line.product_id[1] // [id, name] tuple
          }))
        };
      }

      // 3. Fetch BoP
      const bopLines = await odoo.search_read('product.property.line', [
        ['product_tmpl_id', '=', product.id]
      ], {
        fields: ['prop', 'value', 'reference_url', 'sequence'],
        order: 'sequence ASC'
      });

      const bopData = {
        productKey: product.x_product_key,
        propertyLines: bopLines.map(line => ({
          prop: line.prop,
          value: isNaN(parseFloat(line.value)) ? line.value : parseFloat(line.value),
          referenceUrl: line.reference_url || undefined
        }))
      };

      // 4. Fetch Media List
      const mediaList = await odoo.search_read('product.media', [
        ['product_tmpl_id', '=', product.id]
      ], {
        fields: ['media_type', 'url', 'alt_text', 'width', 'height', 'thumbnail_url', 'duration', 'sequence'],
        order: 'sequence ASC'
      });

      const mediaAssets = mediaList.map(media => ({
        type: media.media_type,
        url: media.url,
        alt: media.alt_text,
        width: media.width || undefined,
        height: media.height || undefined,
        thumbnail: media.thumbnail_url || undefined,
        duration: media.duration || undefined
      }));

      // 5. Assemble ProductMarketingDetail DTO
      return {
        productKey: product.x_product_key,
        slug: product.x_slug,
        displayName: product.name,
        tagline: product.x_tagline,
        generalDescription: product.x_general_description_md,
        mainMedia: mediaAssets[0] || null, // Assume first is mainMedia
        mediaList: mediaAssets,
        bom: bomData,
        bop: bopData,
        // ... map remaining fields
      };
    }
  }
};

Implementation Checklist

Odoo Module Setup

  • [ ] Create module: oves_product_marketing
  • [ ] Define model: product.property.line (BoP)
  • [ ] Define model: product.media
  • [ ] Extend product.template with custom fields:
  • [ ] x_product_key (Char, unique, indexed)
  • [ ] x_slug (Char, unique, indexed)
  • [ ] x_general_description_md (Text) — Markdown
  • [ ] x_main_media_id (Many2one → ir.attachment)
  • [ ] x_publish_state (Selection)
  • [ ] x_tagline (Char)
  • [ ] Add BoM support (use standard mrp.bom module)
  • [ ] Create views: form, tree, kanban for property lines and media
  • [ ] Add security rules (ACL)

BFF GraphQL Schema

  • [ ] Define ProductMarketingDetail type in SDL
  • [ ] Define BillOfMaterials type
  • [ ] Define BillOfProperties type
  • [ ] Define MediaAsset type
  • [ ] Implement resolver: productDetail(slug: String!)
  • [ ] Implement resolver: productList(series: String, category: String)

Frontend (UXI)

  • [ ] Render markdown: generalDescription with react-markdown or marked
  • [ ] Display BoM table: Qty + Item ID
  • [ ] Display BoP table: Property + Value + Tooltip (referenceUrl)
  • [ ] Media gallery: Image carousel + Video player
  • [ ] Test with sample data: /test-samples

Sample Data Reference

File: legacy dirac-uxi/samples/ProductMarketingDetail.json
Product: LEV Battery 48V 20Ah
BoM Line Items: 8 (battery pack, BMS, enclosure, connectors, handle, brackets, cable, label)
BoP Property Lines: 25 (voltage, capacity, weight, dimensions, standards, chemistry)


Notes

  1. Markdown vs. HTML:
  2. generalDescription is stored as Markdown in Odoo
  3. BFF returns markdown string
  4. Frontend renders with markdown parser
  5. Rationale: Cleaner editing, version control friendly

  6. Property referenceUrl Usage:

  7. Displayed as tooltip on hover
  8. Links to authoritative sources (ISO, IEC, OVES docs)
  9. Example UX: Hover "Nominal Voltage in V" → Shows popup with ISO standard definition

  10. Media mainMedia vs. mediaList:

  11. mainMedia: Primary hero (typically first in mediaList)
  12. mediaList: Full gallery (3-5 images + 1-2 videos)
  13. Videos include thumbnail for player preview

  14. BoM Use Case:

  15. B2B transparency: "What components make up this battery?"
  16. Spare parts ordering
  17. Regulatory compliance (material declarations)

  18. BoP Use Case:

  19. Technical specifications with authoritative references
  20. Tooltips explain property meanings (non-technical users)
  21. ISO/IEC links establish credibility (engineering teams)