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
Itemsmodel (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.templatewith 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.bommodule) - [ ] Create views: form, tree, kanban for property lines and media
- [ ] Add security rules (ACL)
BFF GraphQL Schema¶
- [ ] Define
ProductMarketingDetailtype in SDL - [ ] Define
BillOfMaterialstype - [ ] Define
BillOfPropertiestype - [ ] Define
MediaAssettype - [ ] Implement resolver:
productDetail(slug: String!) - [ ] Implement resolver:
productList(series: String, category: String)
Frontend (UXI)¶
- [ ] Render markdown:
generalDescriptionwithreact-markdownormarked - [ ] 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¶
- Markdown vs. HTML:
generalDescriptionis stored as Markdown in Odoo- BFF returns markdown string
- Frontend renders with markdown parser
-
Rationale: Cleaner editing, version control friendly
-
Property
referenceUrlUsage: - Displayed as tooltip on hover
- Links to authoritative sources (ISO, IEC, OVES docs)
-
Example UX: Hover "Nominal Voltage in V" → Shows popup with ISO standard definition
-
Media
mainMediavs.mediaList: mainMedia: Primary hero (typically first inmediaList)mediaList: Full gallery (3-5 images + 1-2 videos)-
Videos include
thumbnailfor player preview -
BoM Use Case:
- B2B transparency: "What components make up this battery?"
- Spare parts ordering
-
Regulatory compliance (material declarations)
-
BoP Use Case:
- Technical specifications with authoritative references
- Tooltips explain property meanings (non-technical users)
- ISO/IEC links establish credibility (engineering teams)