005 — Domain Model
High-level only: the models that matter, how they relate, and what was customised on Odoo's
standard models.
Entity Relationship Overview
erDiagram
RES_PARTNER_COMPANY ||--o{ RES_USERS : "portal users"
RES_PARTNER_COMPANY ||--o{ SALE_ORDER : "orders"
RES_PARTNER_COMPANY ||--o{ ECOMMERCE_BANNER : "targeted banners"
RES_PARTNER_COMPANY ||--o{ PORTAL_USER_TAG : "own tags"
RES_PARTNER_COMPANY ||--o{ NOTIFICATION : "activity feed"
RES_PARTNER_COMPANY }o--|| PRODUCT_PRICELIST : "property pricelist"
RES_PARTNER_COMPANY }o--o| RES_PARTNER_GRADE : "customer segment"
RES_PARTNER_COMPANY }o--o| RES_USERS_AM : "account manager"
RES_PARTNER_COMPANY ||--o| DISCUSS_CHANNEL : "company chat channel"
RES_PARTNER_COMPANY ||--o| IR_SEQUENCE : "portal order sequence"
RES_USERS }o--o{ PORTAL_USER_TAG : "tagged with"
SALE_ORDER ||--o{ SALE_ORDER_LINE : "lines"
SALE_ORDER ||--o{ PRODUCT_REQUEST_LINE : "catalog requests"
SALE_ORDER }o--|| PRODUCT_PRICELIST : "priced by"
SALE_ORDER ||--o{ IR_ATTACHMENT : "documents"
SALE_ORDER ||--o{ MAIL_MESSAGE : "chatter"
SALE_ORDER_LINE }o--|| PRODUCT_PRODUCT : "product variant"
PRODUCT_PRODUCT }o--|| PRODUCT_TEMPLATE : "product variant of"
PRODUCT_TEMPLATE }o--o| ECOMMERCE_BRAND : "brand"
PRODUCT_TEMPLATE }o--o{ PRODUCT_PUBLIC_CATEGORY : "public categories"
PRODUCT_TEMPLATE }o--o| ALERT_MESSAGE : "catalog alert"
PRODUCT_PRICELIST ||--o{ PRODUCT_PRICELIST_ITEM : "price rules"
RES_PARTNER_GRADE ||--o{ RES_PARTNER_COMPANY : "categorises"
PRODUCT_REQUEST_LINE }o--o| SALE_ORDER_LINE : "fulfilled by"
PRODUCT_REQUEST_LINE }o--o| PRODUCT_PRODUCT : "created product"
The Tenancy Spine
Everything in the platform hangs off one relationship:
flowchart LR
U["res.users<br/>portal user"] -->|portal_company_partner_id| C["res.partner<br/>is_company = True<br/>is_b2b_portal = True"]
C -->|account_manager_user_id| AM["res.users<br/>Account Manager"]
C -->|property_product_pricelist| PL["product.pricelist"]
C -->|partner_grade_id| G["res.partner.grade<br/>Customer Segment"]
A portal user's company partner determines: which orders they see, which prices they get,
which banners are shown, which chat channel they join, which notifications reach them, and
which Account Manager is alerted on state changes.
Custom Models
| Model |
Purpose |
Notable traits |
ecommerce.brand |
Product brands |
Name & description, manual sequence, active flag, chatter |
ecommerce.banner |
Marketing banners |
Scheduledactive_datetime / deactivate_datetime, optional customer targeting, promotion URL, linked products & categories, obfuscated IDs |
ecommerce.alert_message |
Short catalog alert text attached to products |
Simple label model |
ecommerce.portal.user.tag |
Tags for portal users |
Scoped per customer company |
ecommerce.product.request.line |
Customer request for a product not in the catalog |
Own state machine; can auto-create a sale order line |
ecommerce.notification |
In-app activity notification |
Typed, company-scoped,owner_user_id excludes the actor from their own notifications, obfuscated IDs |
New product request line state machine
stateDiagram-v2
[*] --> submitted: customer requests a product
submitted --> in_progress: account manager picks it up
in_progress --> product_added: product created / matched → order line added
in_progress --> not_found: cannot be sourced
product_added --> [*]
not_found --> [*]
Each transition stamps its own date field. When a request reaches product_added with a linked
order line, that line is created automatically on the customer's order; reverting the link
removes it again.
Extended Odoo Models
res.users
| Added capability |
Fields / behaviour |
| Tenancy |
portal_company_partner_id, computed is_b2b_portal, is_admin_portal_user |
| Dual activation |
activate (controlled by the vendor's account manager) and portal_activate (controlled by the customer's own company admin) — both must be true to log in |
| Identity |
identification_no as an alternative login, portal_user_code |
| Password reset |
otp, otp_expiry_datetime — a one-time code e-mailed and verified during a forgotten-password reset. It never establishes a session |
| Invitation |
Computedinvitation_url and qr_code_image, printable QR report, invitation e-mail |
| Roles |
is_account_manager, portal_user_tags |
| Presence |
Overriddenim_status computed from mail.presence with a staleness window, plus a searchable variant |
| API surface |
The full portal-user CRUD, authentication and password flows live here as api_* methods |
res.partner
| Added capability |
Detail |
| Portal customer flag |
is_b2b_portal marks a company as a B2B customer |
| Account managers |
account_manager_user_id (primary) and account_manager_ids (many-to-many) |
| Per-customer numbering |
portal_order_sequence_id — each customer gets its own order sequence |
| Portal URL |
shop_portal_url, falling back to a global system parameter |
| Chat |
company_channel_id — the discuss channel for company-wide conversation |
| Pricing |
property_product_pricelist with conflict resolution against the customer's grade default |
| Owned collections |
Banners, portal users, portal user tags |
The customer segment (res.partner.grade, reused from the partnership module) carries a
default pricelist. Assigning a category and a conflicting pricelist to the same customer triggers
an explicit resolution that logs the outcome in the chatter.
sale.order / sale.order.line
| Added capability |
Detail |
| Parallel status |
portal_state — 9 states in use, tracked; mapped onto Odoo's native state on every transition |
| Portal identity |
portal_order_no, portal_label (customer's own name for the basket), per-customer sequence |
| Planning |
portal_planned_order_date and a computed portal_order_planning_state (late / on time / upcoming) |
| Attention flags |
portal_visible ("action required"), portal_reviewed + date, portal_pending_submit, portal_print_quotation_date |
| Collaboration |
portal_messages_count, attachment count, account-manager notes (internal) and comment (customer-visible) |
| Catalog requests |
request_product_ids with a count of new requests |
| Line additions |
portal_target_price (the price the customer is asking for), manual flag, need_call related from the product |
| Live refresh |
Inherits the auto-refresh mixin — ERP Back Office lists update without a reload |
product.template / product.product
| Added capability |
Detail |
| Merchandising |
brand_id, public_categ_ids, is_featured_product, alert_message_id, terms (HTML) |
|
|
| Sales behaviour |
need_call — a product that must be quoted by phone |
| Pricing projection |
Computedproduct_pricelist_ids; the catalog API resolves the effective price, list price and discount per pricelist |
| Admin management |
Full product CRUD, media sync, product attribute linking, product variant generation and the JSON import pipeline are implemented as model methods here |
Collaboration models
| Model |
Customisation |
mail.message |
Portal chatter API: read, post, update; attachment upload/delete; access check against the related sale order |
ir.attachment |
Portal provenance (upload_from_portal, portal_create_uid, portal_company_partner_id, auto_created) and an attachment_view visibility selector |
discuss.channel |
Portal-aware unread counting and read pointers; mirrors new messages onto a string bus channel because Odoo.sh does not relay record channels to non-Odoo WebSocket clients |
mail.presence |
Stale-presence cron, transition-only bus broadcasts (so the heartbeat does not spam), and suppression of spurious "user is online" alerts caused by page refreshes |
Abstract Models (Mixins)
| Mixin |
Applied to |
Effect |
ecommerce.base.secure_model |
ecommerce.banner, ecommerce.notification |
Reversible XOR + Base64 ID obfuscation for externally exposed identifiers; controllers resolve incoming external IDs back to database IDs. Toggled by a system parameter, defaults to off. |
ecommerce.base_auto_refresh_model |
sale.order |
Broadcasts a bus event on create/write/unlink so open ERP Back Office views refresh live. |
Removed Models
ecommerce.tier and ecommerce.product_merchant (product ↔ merchant price link) were fully
removed from the codebase — model, views, security rules and manifest entries all deleted — in
commit c26ef8b (2026-08-06). They are not present in any form; do not build on either name.
Customer segmentation is done with res.partner.grade (Customer Segments) instead.
ecommerce.merchant (supplier master data, singular) is still present in code today but is
slated for removal too — see _docs/todos/ecommerce-models-dead-code.md — so it is no longer
documented here.