Populate¶
The Populate module is a synthetic data generation framework for Odoo databases. It follows a declarative Blueprint pattern: you describe what data to create in XML or JSON, and the system generates records at scale, with support for parallel execution, statistical distributions, and dependencies between generated targets.
Typical use cases:
Performance testing – generate thousands of records to stress-test queries, views, and reports.
Demo environments – ship a module with a realistic-looking dataset out of the box.
Development – quickly populate a local database so you can work on features that need existing data.
See also
duplicate - Duplicate Records for a simpler tool that duplicates existing records in bulk.
Installation¶
Install the
populateOdoo module on your database.(Optional) Install the Faker library to unlock the
fake.*generators:$ pip install -r odoo/addons/populate/requirements.txt
Important
After installing a new module that ships blueprints, you must upgrade the populate
module so that its blueprints are discovered and loaded into the database:
$ odoo-bin -d <database> -u populate
CLI command¶
$ odoo-bin populate -d <database> -b <blueprint>
- -d <database>, --database <database>¶
Target database (required).
- -b <blueprint>, --blueprint <blueprint>¶
Blueprint name or full xmlid (required, unless
--resumeis used).
- --seed <seed>¶
Seed for the random number generator. If omitted, a random seed is chosen. Providing the same seed guarantees reproducible results (deterministic generation).
- --scale <factor>¶
Multiply all record counts in the blueprint by this factor. Default:
1.
- -j <workers>, --jobs <workers>¶
Number of parallel worker processes. Use
autoto use all available CPU threads. Default:1.
- --resume [session_id]¶
Resume an interrupted session. Without an argument, resumes the most recent unfinished session. With a session ID, resumes that specific session.
- --profile¶
Save profiler traces for each executable populate job in this run.
Example
# Run a blueprint at 10x scale using all CPU cores
$ odoo-bin populate -d mydb -b project.fake_project_demo --scale 10 -j auto
# Run with a fixed seed for reproducibility
$ odoo-bin populate -d mydb -b my_module.my_blueprint --seed 42
# Resume the last interrupted session
$ odoo-bin populate -d mydb --resume
# Resume a specific session by ID
$ odoo-bin populate -d mydb --resume 7
# Save profiler traces for the generated workload
$ odoo-bin populate -d mydb -b project.fake_project_demo --profile
Profiling populate runs¶
Use --profile to measure the runtime cost of a populate run. The command creates
ir.profile entries grouped under one profiler session named after the populate session and
blueprint.
The option only applies to the current command invocation, so the same interrupted session can be resumed later with or without profiling:
$ odoo-bin populate -d mydb --resume 7 --profile
Profiling works in both single-worker and multi-worker mode. Each executable job creates its own profile entry, including subjobs created when a large job is split for parallel execution. Planner jobs, which only coordinate split subjobs and do not create or update records themselves, are not profiled. Failed executable job attempts also create profile entries, so their traces remain available for failure analysis.
Blueprints¶
A Blueprint is a record of populate.blueprint that declaratively describes what data to
create. Blueprints are typically shipped inside a module’s populate/ data folder and loaded
automatically when the populate module is upgraded.
If a module’s populate/ folder is a valid Python package (contains an __init__.py), its
code is imported, allowing the module to register custom generators.
Blueprints can be defined in XML, JSON, or both. If both definition_xml and
definition_json are set on the same record, the XML definition takes precedence.
Operation blocks¶
A blueprint definition is an ordered list of operation blocks. Use <create/> to create
records, <write/> to update targeted records, and <function/> to call a method on targeted
records. Fields are persisted through ORM create or write calls, while values are local
variables that can be reused by later fields, values, and function arguments in the same block.
Example
<create model="res.partner" count="500" id="my_partners">
<value name="email_domain" eval="'example.com'"/>
<field name="name" generator="fake.company"/>
<field name="email"
eval="name.lower().replace(' ', '-') + '@' + email_domain"/>
<field name="active" eval="True"/>
</create>
<write model="res.partner" ref="my_partners" batched="True">
<value name="suffix" eval="'imported by populate'"/>
<field name="comment" eval="suffix"/>
</write>
<function model="res.partner" name="message_subscribe"
ref="my_partners" batched="True">
<value name="current_partner" eval="env.user.partner_id.id"/>
<arg eval="[current_partner]"/>
</function>
model(required)Odoo model technical name, e.g.
res.partner.name(required forfunction)Name of the method to call.
count(required forcreate)Number of records to create.
idReference tag for a
createblock. Later blocks can target these records usingref.refFor
writeandfunctionblocks: reference to a previously created batch (itsid).domainFor
writeandfunctionblocks: ORM domain selecting the target records. If neitherrefnordomainis provided, all records of the model are targeted.createblocks cannot define a top-leveldomainbecause they create records instead of targeting existing ones.scaleTrue(default) orFalse. Whether the--scalefactor applies to this block’scount.parallelTrue(default) orFalse. Whether this job can be split across parallel workers. Set toFalsewhen the model’s constraints require sequential writes.batchedFor
writeandfunctionblocks only. WithTrue, generate one value set and perform one write or function call per executable job. The default isFalse, which generates values and performs the operation once per target record. See Write jobs and Function jobs.contextA Python dict literal merged into the ORM context for the
create,write, or function calls.
Important
Operation blocks are executed in document order. A block that references another via ref can
only target a block that was defined earlier in the blueprint. Define master data first,
then the records that depend on it:
Example
<!-- 1. Stage definitions (master data) -->
<create model="project.task.type" count="8" id="task_types" scale="False">
<field name="name" generator="fake.bs"/>
</create>
<!-- 2. Projects (reference stages) -->
<create model="project.project" count="120" id="projects">
<field name="type_ids" ref="task_types" count="8"/>
</create>
<!-- 3. Tasks (reference projects and stages) -->
<create model="project.task" count="10000" id="tasks">
<field name="project_id" ref="projects"/>
<field name="stage_id" ref="task_types"/>
</create>
JSON format¶
The JSON format mirrors the XML structure. The mandatory operation key corresponds to the XML
operation element name (create, write, or function). The top-level array maps to the
ordered list of operation blocks:
Example
[
{
"operation": "create",
"model": "res.partner",
"count": 500,
"id": "my_partners",
"values": {
"email_domain": { "eval": "'example.com'" }
},
"fields": {
"name": { "generator": "fake.company", "null_ratio": "0" },
"email": { "eval": "name.lower().replace(' ', '-') + '@' + email_domain" },
"active": { "eval": "True" }
}
},
{
"operation": "write",
"model": "res.partner",
"ref": "my_partners",
"batched": true,
"values": {
"suffix": { "eval": "'imported by populate'" }
},
"fields": {
"comment": { "eval": "suffix" }
}
},
{
"operation": "function",
"model": "res.partner",
"name": "message_subscribe",
"ref": "my_partners",
"batched": true,
"values": {
"current_partner": { "eval": "env.user.partner_id.id" }
},
"args": {
"0": { "eval": "[current_partner]" }
}
}
]
Each object’s optional fields, values, and args keys map target names to their attribute
dictionaries – the same keys you would write as XML attributes. Positional function arguments use
numeric string keys ("0", "1", and so on).
Fields, values, and arguments¶
Each <field/>, <value/>, and <arg/> declaration describes a generated target:
<field/>is valid increateandwriteblocks and is persisted through the ORM.<value/>is valid in every operation block and defines a non-persisted local variable.<arg/>is valid infunctionblocks and defines a generated method argument. An argument with anameis passed as a keyword argument; an unnamed argument is passed positionally in declaration order.
They support the same generation attributes, except that name is optional for positional
<arg/> declarations.
Example
<field name="age" generator="scalar.integer" start="18" end="65"
distribution="normal(mean=35, std=12)"/>
name(required forfieldandvalue)ORM field name, local value name, or keyword argument name.
generatorThe generator to use (see Generators). Mutually exclusive with
eval. For a field, if neither is provided, a default generator is selected based on the field type. Values and arguments have no ORM type from which to select a default.evalA Python expression. Can reference other generated targets by name to produce computed values. Mutually exclusive with
generator.null_ratioProbability (0–1) of generating
Falseinstead of a real value. Default:0. Cannot be combined with required fields or weightedvalues.uniqueTrueto enforce uniqueness. For ORM fields, generated values are checked against both existing database records and previously generated values within the same job. For local values and arguments, uniqueness only applies within the current job because they are not persisted.valuesAn explicit value list or weighted dict. Examples:
"['a', 'b', 'c']"(equal weights) or"{'a': 3, 'b': 1}"(ais 3x more likely thanb).distributionA statistical distribution specification, e.g.
"normal(mean=50, std=10)". See Distributions. Cannot be combined with weightedvalues.domainAn ORM domain to filter related records. Only applies to relational and reference generators. Can contain generated-target references resolved at generation time – see Dynamic domains.
refRestrict relational picks to records created under this reference tag. Supports dot-path traversal – see Ref dot-path navigation.
comodel_nameRequired for relational values or arguments, where the comodel cannot be inferred from an ORM field.
partitionTrueto partition comodel IDs across parallel workers. See Partitioning for parallel execution.
Default generators¶
When neither generator nor eval is specified for a field, a default generator is
automatically selected based on the field type:
Field type |
Default generator |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If a field type is not listed above and no generator or eval is provided, an error is
raised.
Generators¶
Generators are the building blocks that produce fields, local values, and function arguments. Every
generator has a name (used to reference it in blueprints) and a set of compatible target types.
Scalar generators¶
Generate numeric and boolean values.
scalar.booleanGenerates
TrueorFalse. Withvalues, you can weight the probability:values="{'True': 9, 'False': 1}"producesTrue~90% of the time.Compatible targets:
booleanfields and generated values or arguments.scalar.integerGenerates random integers in a range.
Compatible targets:
integerandfloatfields, and generated values or arguments.startLower bound (inclusive). Default:
1.endUpper bound (inclusive). Default:
1000000.
Example
<field name="quantity" generator="scalar.integer" start="1" end="100"/>
scalar.floatGenerates random floating-point numbers in a range.
Compatible targets:
floatfields and generated values or arguments.startLower bound. Default:
1.0.endUpper bound. Default:
1000000.0.
scalar.monetaryGenerates random monetary values in a range. Depends on the model’s currency field – a value for said field must be generated (or eval’d) in the same blueprint block.
Compatible targets:
monetaryfields and generated values or arguments.startLower bound. Default:
1.0.endUpper bound. Default:
1000000.0.
Textual generators¶
Generate random strings.
textual.charGenerates a random string of fixed length from a character set.
Compatible targets:
charandhtmlfields, and generated values or arguments.char_setCharacters to pick from. Default: ASCII letters and digits.
lengthLength of the generated string. Default:
12.
textual.textGenerates a random text block of fixed length.
Compatible targets:
textandhtmlfields, and generated values or arguments.char_setCharacters to pick from. Default: ASCII letters, digits, spaces, and newlines.
lengthLength of the generated text. Default:
50.
Tip
For realistic-looking text (names, emails, addresses), use the fake.* generators instead.
Temporal generators¶
Generate dates and datetimes within a range, using a relative date syntax.
temporal.dateGenerates random dates.
Compatible targets:
dateanddatetimefields, and generated values or arguments.startStart of the range. Default:
None(beginning of time).endEnd of the range. Default:
None(end of time).
temporal.datetimeGenerates random datetimes.
Compatible targets:
datetimefields and generated values or arguments.startStart of the range. Default:
None(beginning of time).endEnd of the range. Default:
None(end of time).
Both generators accept a relative date syntax for start and end:
temporal.dateusestodayas the anchor:"today -6m","today +1y"temporal.datetimeusesnowas the anchor:"now -30d","now +2h"
Supported suffixes: y (years), m (months), w (weeks), d (days), h (hours),
M (minutes), s (seconds).
Example
<field name="date_order" generator="temporal.date" start="today -6m" end="today"/>
<field name="create_date" generator="temporal.datetime" start="now -30d" end="now"/>
Choice generators¶
Pick values from a set.
choice.samplePicks from an explicit
valueslist (required). Supports weighted values.Compatible targets:
integer,float,char,text,html,date,datetime,boolean, andselectionfields, and generated values or arguments.Example
<field name="priority" generator="choice.sample" values="{'high': 1, 'medium': 5, 'low': 4}"/>
choice.selectionPicks from the field’s own selection keys. If
valuesis provided, only those keys are used (with optional weights). Otherwise, all valid selection keys are equally likely.Compatible types:
selection.Example
<!-- All selection values equally likely --> <field name="state" generator="choice.selection"/> <!-- Only these values, with weights --> <field name="state" generator="choice.selection" values="{'draft': 1, 'confirmed': 5, 'done': 3}"/>
Binary generators¶
Generate binary data.
binary.binaryGenerates random binary data.
Compatible targets:
binaryfields and generated values or arguments.sizeSize in bytes. Default:
1024.
binary.imageGenerates a random solid-color image (PNG).
Compatible targets:
binaryfields and generated values or arguments.widthImage width in pixels. Default:
64.heightImage height in pixels. Default:
64.
Relational generators¶
Generate relational targets by picking from existing records.
relation.onePicks a single related record.
Compatible targets:
many2onefields and generated values or arguments.domainORM domain to filter candidates. See Dynamic domains.
refRestrict to records created under this reference tag. See Ref dot-path navigation.
comodel_nameRequired for generated values or arguments, where the comodel cannot be inferred from an ORM field.
partitionPartition comodel IDs across parallel workers. See Partitioning for parallel execution.
relation.manyPicks multiple related records (for
one2manyandmany2manyfields).Compatible targets:
one2manyandmany2manyfields, and generated values or arguments.countAverage number of related records to link.
stdStandard deviation for the count. Default:
0(always exactlycount).groupbyGroup linked records by a field on the comodel.
domain,ref,comodel_name,partitionSame as
relation.one.
Example
<field name="tag_ids" generator="relation.many" count="3" std="2"/>
Dynamic domains¶
The domain parameter on relational generators can contain generated-target references that
are resolved at generation time against the current record’s already-generated values:
Example
<field name="project_id" generator="relation.one"/>
<field name="task_id" generator="relation.one"
domain="[('project_id', '=', project_id)]"/>
project_id in the domain expression is automatically detected as a dependency. At
generation time the expression is evaluated with the actual value produced for project_id,
so every task_id is guaranteed to belong to its sibling project_id.
Partitioning for parallel execution¶
Generators that pick from a comodel (relation.one, relation.many, reference.one,
reference.raw) support a partition parameter. When enabled in parallel jobs, comodel
IDs are distributed across workers using round-robin partitioning:
Example
<field name="user_id" generator="relation.one" partition="True"/>
This avoids conflicts when creating related records in parallel.
Note
Partitioning only takes effect when the job has sibling sub-jobs (i.e., it was split for parallel execution). In single-worker mode, the parameter has no effect.
Partitioning may introduce slight biases when used with non-uniform distributions. The general shape of the distribution is preserved, but the parameters won’t be followed as precisely. For most cases this can be ignored.
Reference generators¶
Generate values for reference-type fields.
reference.onePicks a record for a
many2one_referencefield. Implicitly depends on the field that stores the model name.Compatible types:
many2one_reference.partitionPartition IDs across parallel workers.
reference.rawPicks a record for a
referencefield (stores"model_name,id"string).Compatible types:
reference.res_modelRestrict to a specific model.
res_idRestrict to a specific record ID.
refRestrict to records under this reference tag.
partitionPartition IDs across parallel workers.
Faker generators (fake.*)¶
Wraps the Faker library. Any method
from an allowed provider can be used directly as fake.<method_name>:
Example
<field name="name" generator="fake.name"/>
<field name="email" generator="fake.email" locale="fr_FR"/>
<field name="phone" generator="fake.phone_number"/>
<field name="bio" generator="fake.paragraph" nb_sentences="5"/>
Method-specific keyword arguments (e.g. nb_sentences) are forwarded as-is to the Faker
method.
localeLocale for localized data. Default:
en_US.
Allowed providers: address, automotive, bank, barcode, color,
company, credit_card, currency, emoji, file, geo, internet,
isbn, job, lorem, misc, passport, person, phone_number,
profile, sbn, ssn, user_agent.
Important
Faker must be installed separately. See Installation.
Miscellaneous generators¶
misc.counterGenerates an arithmetic sequence. Wraps around to
startifendis reached.Compatible targets:
integerandfloatfields, and generated values or arguments.startInitial value. Default:
0.stepIncrement per record. Default:
1.endUpper bound (wraps around). Default:
None(no wrap).
Example
<field name="sequence" generator="misc.counter" start="1" step="1"/>
misc.cycleCycles through a
valueslist in order, deterministically. Unlikechoice.sample, this is not random – it repeats the sequence exactly.Compatible targets:
integer,float,char,text,html,date, anddatetimefields, and generated values or arguments.Note
Weighted values are not allowed with
misc.cycle– values are always cycled in order.Example
<field name="day" generator="misc.cycle" values="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"/>
misc.evalEvaluates a Python expression. Can reference other target names to produce computed values.
Compatible types: any.
The evaluation context contains:
env– the Odoo environmentmodel– the model being populatedCommand–odoo.fields.Commandfor building relation commands
Example
<field name="display_name" generator="misc.eval" eval="name + ' (' + str(email) + ')'"/>
Properties generators¶
Generate values for the properties / properties_definition field system.
properties.definitionGenerates a property schema (list of property definitions).
Compatible types:
properties_definition.propsExplicit list of property names.
countNumber of properties to generate (used if
propsis not set).allowed_typesRestrict generated property types to this set.
possible_valuesFor selection-type properties: dict mapping property names to their possible values.
properties.propHelper for defining a single property entry. Used inside
properties.definition.Compatible targets: generated values or arguments.
prop_typeThe property type (e.g.
char,integer,selection).stringThe display label for the property.
possible_valuesFor selection-type: list of possible values.
properties.valueGenerates values for a
propertiesfield, matching the schema defined by its parent’sproperties_definitionfield.Compatible types:
properties.
Distributions¶
By default, generators produce values uniformly at random within their range. Adding a
distribution parameter changes how likely certain parts of the range are sampled.
Example
<field name="age" generator="scalar.integer" start="18" end="90"
distribution="normal(mean=35, std=12)"/>
<field name="delay" generator="scalar.float" start="0" end="100"
distribution="exponential(rate=0.05)"/>
normal(mean, std) – Most values near the center¶
Produces a classic bell curve. Most values land close to mean; the further from it, the
rarer. std (standard deviation) controls the spread – a smaller std packs values
tighter around the mean.
Use when you want a realistic “average with natural variation” pattern.
Example field |
Parameters |
Reason |
|---|---|---|
Employee age |
|
Most employees are around 35, fewer very young or very old |
Product price |
|
Prices cluster around 50, with some cheaper/expensive outliers |
Task duration (hours) |
|
Most tasks take about a day, some shorter or longer |
uniform() – Any value is equally likely¶
A flat distribution – every value in the range has the exact same chance. This is the default
behavior when you omit distribution entirely, so you rarely need to write it out.
exponential(rate) – Lots of small values, rare large ones¶
A steep curve that starts high and drops off. Most generated values are small; large values are
increasingly rare. A higher rate makes it drop off faster.
Use when the data should be skewed toward the low end, with occasional spikes.
Example field |
Parameters |
Reason |
|---|---|---|
Days until deadline |
|
Most deadlines are soon, a few are months away |
Allocated hours |
|
Most tasks are quick, a few are very long |
Time between events |
|
Short gaps are common, long gaps are rare |
beta(alpha, beta) – Values between 0 and 1, shaped how you want¶
Always produces values in [0, 1]. The generator maps this onto your start/end range
automatically. The two parameters shape the curve:
alpha=2, beta=2– bell-shaped, centered at 0.5 (like a bounded normal)alpha=1, beta=3– skewed toward 0 (most values are low)alpha=3, beta=1– skewed toward 1 (most values are high)alpha=0.5, beta=0.5– U-shaped, values cluster near 0 and 1
Use when you are modeling percentages, progress, ratings, or any bounded proportion.
Example field |
Parameters |
Reason |
|---|---|---|
Project progress (%) |
|
Most projects are roughly mid-way, few at 0% or 100% |
Discount rate |
|
Most discounts are small, large discounts are rare |
Satisfaction score |
|
Most scores are high |
poisson(lam) – How many times something happens¶
Produces whole numbers representing a count of occurrences. lam (lambda) is the average
number of occurrences you expect. Values near lam are most likely; values far from it are
rare.
Use when you are generating “how many” – e.g., number of items, events, or attempts.
Example field |
Parameters |
Reason |
|---|---|---|
Number of order lines |
|
Orders average 5 lines, some have 1, rarely 15+ |
Support tickets per day |
|
About 3 per day on average |
Login attempts |
|
Usually 1–3 attempts, occasionally more |
triangular(min, max, mode) – Three-point estimate¶
A simple triangle shape. mode is the peak (most likely value), min and max are the
absolute bounds. Values near mode are most common; probability falls off linearly to the
edges.
Use when you can estimate three points – minimum, maximum, and most likely – but don’t have more detailed data.
Example field |
Parameters |
Reason |
|---|---|---|
Task estimate (days) |
|
Most tasks take ~5 days, never less than 1 or more than 30 |
Shipping cost |
|
Typically around 25, bounded by 5 and 200 |
Quick decision guide¶
You want… |
Use |
|---|---|
Realistic clustering around an average |
|
Everything equally likely |
|
Mostly small values, rare big ones |
|
A percentage / bounded ratio |
|
A count of “how many times” |
|
Three-point estimate (min / likely / max) |
|
Advanced topics¶
Generated values¶
Fields are persisted values passed to ORM create or write. By contrast, <value/>
declarations are local variables that are generated but not persisted. They let you build
intermediate values that multiple fields depend on, avoiding duplication:
Example
<create model="account.move.line" count="1000">
<field name="quantity" generator="scalar.integer" start="1" end="100"/>
<field name="price_unit" generator="scalar.float" start="5" end="500"/>
<value name="subtotal" eval="quantity * price_unit"/>
<field name="discount" eval="subtotal * 0.1 if subtotal > 200 else 0"/>
<field name="price_total" eval="subtotal - discount"/>
</create>
Here subtotal is computed but never written to the database. Both discount and
price_total reference it, so the quantity * price_unit logic lives in one place.
Generated values are also useful for correlating persisted fields:
Example
<create model="res.partner" count="200">
<value name="first_name" generator="fake.first_name"/>
<value name="last_name" generator="fake.last_name"/>
<field name="name" eval="first_name + ' ' + last_name"/>
<field name="email"
eval="first_name.lower() + '.' + last_name.lower() + '@example.com'"/>
</create>
Every record’s name and email stay consistent with each other, without either
intermediate value being stored on its own.
Write jobs¶
Use <write/> to update existing records. The target records can be selected with ref, with
a top-level domain, or with both.
Example
<!-- Create partners and tag them with the "customers" reference -->
<create model="res.partner" count="500" id="customers">
<field name="name" generator="fake.company"/>
<field name="active" values="{'True': 9, 'False': 1}"/>
<field name="customer_rank" start="0" end="5"/>
</create>
<!-- Update all partners created under the "customers" reference -->
<write model="res.partner" ref="customers">
<field name="phone" generator="fake.phone_number"/>
</write>
<!-- Update all active customers, even if they were not created by this blueprint -->
<write model="res.partner"
domain="[('customer_rank', '>', 0), ('active', '=', True)]">
<field name="mobile" generator="fake.phone_number"/>
</write>
<!-- Update only active customers created under the "customers" reference -->
<write model="res.partner" ref="customers"
domain="[('customer_rank', '>', 0), ('active', '=', True)]">
<field name="email" generator="fake.company_email"/>
</write>
The targeting rules are:
Attributes |
Target records |
|---|---|
|
Records created under that populate reference. |
|
Existing records of the job model matching the domain. |
|
Referenced records that also match the domain. |
Neither |
All existing records of the job model. |
Domains on write jobs are evaluated once to select the target records; they are not dynamic per
generated record. create jobs cannot define a top-level domain because they create new
records instead of targeting existing ones.
By default, values are generated and written once per target record. With batched="True", one
value set is generated for the whole executable job or subjob, and one ORM write call updates
that recordset:
Example
<write model="res.partner" ref="customers" batched="True">
<field name="active" eval="True"/>
</write>
batched is only valid on write and function blocks. create blocks do not accept it,
because ORM create already receives a list of generated values. Large jobs can still be split
into subjobs, so each subjob performs its own write.
Function jobs¶
Use <function/> to call a model method on targeted records. This is useful when records cannot
be created directly in their final business state and must go through a transition method. For
example, invoices are created as drafts and can then be posted by calling _post:
Example
<function model="account.move" name="_post" ref="moves" batched="True">
<arg name="soft" eval="False"/>
</function>
Function blocks follow the same ref and domain targeting rules as write blocks. The
name attribute selects the method. Methods decorated with @api.model are called on the
empty model recordset once per job; regular record methods are called on the targeted records.
Declare method arguments with <arg/>. Arguments support the same generation attributes as
<value/>. Named arguments become keyword arguments, while unnamed arguments become positional
arguments in declaration order:
Example
<function model="x.model" name="action" ref="records">
<arg eval="'first positional'"/>
<arg eval="42"/>
<arg name="flag" eval="True"/>
</function>
In JSON, positional arguments use numeric string keys such as "0" and "1". With the
default batched="False", arguments are generated and the method is called once per target
record. With batched="True", one argument set is generated and the method is called once on the
target recordset of each executable job or subjob. This batching distinction applies to regular
record methods; @api.model methods are always called once per job.
Blueprint inheritance¶
Blueprints support Odoo-style view inheritance via inherit_id. A child blueprint applies
XPath or positional specs to its parent’s XML definition:
Example
<record id="custom_blueprint" model="populate.blueprint">
<field name="name">Custom Blueprint</field>
<field name="inherit_id" ref="base_module.parent_blueprint"/>
<field name="definition_xml" type="xml">
<!-- Change record count -->
<create model="res.partner" position="attributes">
<attribute name="count">2000</attribute>
</create>
<!-- Add a new field to an existing create block -->
<create model="res.partner" position="inside">
<field name="website" generator="fake.url"/>
</create>
<!-- Add a new create block after an existing one -->
<create model="res.partner" position="after">
<create model="res.users" count="50" id="new_users">
<field name="name" generator="fake.name"/>
<field name="login" generator="fake.user_name" unique="True"/>
</create>
</create>
</field>
</record>
Supported positions: attributes, inside, before, after, replace. XPath
expressions (<xpath expr="..." position="...">) work as well. Chained inheritance
(grandchild blueprints) is supported; circular inheritance is detected and rejected.
Importing blueprints¶
While blueprint inheritance extends one
blueprint, <import/> composes several independent XML blueprints into a new scenario. The
imported operation blocks are inserted where the import appears and become ordinary jobs of the
caller’s session:
Example
<create model="sale.order" count="500" id="orders">
<!-- ... -->
</create>
<import ref="product.fake_product_demo" as="catalog"/>
<create model="sale.order.line" count="2000">
<field name="order_id" ref="orders"/>
<field name="product_id"
ref="catalog/product_templates.product_variant_ids"/>
</create>
The ref attribute must contain the fully qualified external ID of another
populate.blueprint record. An <import/> must be a direct child of the blueprint’s
<data/> root.
The optional as attribute adds a namespace to every id declared by the imported
blueprint and to its internal references. This avoids conflicts when a scenario imports multiple
blueprints with the same IDs, or imports the same blueprint more than once. Without as, IDs
are left unchanged and must not conflict with IDs in the caller or another import.
The slash (/) separates namespace components, while the dot (.) keeps its existing
meaning of traversing a relational field. For example, in
catalog/product_templates.product_variant_ids, catalog/product_templates is the populate
reference and product_variant_ids is the relational field path.
Customize an import without modifying its source blueprint by adding the same inheritance
specifications supported by inherit_id as children of <import/>:
Example
<import ref="product.fake_product_demo" as="catalog">
<xpath expr="//create[@id='product_templates']" position="attributes">
<attribute name="count">250</attribute>
</xpath>
<xpath expr="//create[@id='product_supplier_info']" position="replace"/>
</import>
The XPath expressions target the source blueprint’s original IDs because the specifications are
applied before the namespace. Inheritance applied to the composed blueprint instead sees the
namespaced IDs, such as catalog/product_templates.
Imports are resolved recursively, so their namespaces compose. For example, importing a
catalog namespace under a sales namespace produces references such as
sales/catalog/products. Recursive combinations of imports and inheritance are rejected.
Important
Imports are only supported by XML blueprints; a JSON-only blueprint cannot be imported. The caller’s module must depend on the module that provides the imported blueprint. When both are in the same module, the imported blueprint must be defined earlier in the module’s populate data files.
Imports are resolved when a session is created. New sessions use the latest blueprint definitions, whereas an existing session keeps the jobs instantiated from the definitions available at its creation.
Sessions and resuming¶
Each run creates a Session (populate.session) that tracks every job and the records it
produced. If execution is interrupted (Ctrl+C, crash, etc.), you can resume where you left
off:
Example
# Resume the most recent unfinished session
$ odoo-bin populate -d mydb --resume
# Resume a specific session by ID
$ odoo-bin populate -d mydb --resume 42
Sessions also guarantee deterministic generation: providing the same --seed with the
same blueprint produces the same data every time.
Parallel execution¶
Pass -j (or -j auto) to split large jobs across multiple worker processes. Each
job that exceeds the internal batch size is automatically divided into sub-jobs distributed to
the pool.
Example
$ odoo-bin populate -d mydb -b my_blueprint --scale 50 -j auto
Parallelism can be disabled per operation block with parallel="False" when the model’s
constraints require sequential writes. The multiprocessing backend is controlled by the
environment variable ODOO_POPULATE_MULTIPROCESS_ENABLE (defaults to True).
Automatic retry on constraint violations¶
The session executor includes a retry mechanism for transient database constraint failures. When a job triggers one of the following PostgreSQL violations, the job’s seed is re-rolled and the entire job is re-executed with a fresh set of random values (up to 5 attempts):
Violation |
Common cause |
Hint |
|---|---|---|
|
Two generated records collide on a unique index |
Use a generator that produces more varied values, or add |
|
A required column received |
Add |
|
A generated value fails a |
Adjust generator parameters to stay within the constraint |
|
Generated values violate an exclusion constraint |
Adjust generator parameters to stay within the constraint |
This means blueprints don’t need to be perfectly tuned upfront – occasional constraint failures due to randomness are handled transparently. Only violations that persist across all retry attempts surface as errors.
Writing custom generators¶
You can create custom generators by subclassing
odoo.addons.populate.generators.Generator and placing the code in your module’s
populate/ package (with an __init__.py). The generator is automatically registered when
the module is loaded.
Example
from odoo.addons.populate.generators import Generator
class SequentialEmail(Generator):
"""Generates email addresses like user_0001@example.com, user_0002@example.com, ..."""
name = 'my_module.sequential_email'
allowed_on = ('char', 'value')
def __init__(self, domain_name='example.com', **kwargs):
super().__init__(**kwargs)
self.domain_name = domain_name
self._counter = 0
def _next(self, known_vals):
self._counter += 1
return f'user_{self._counter:04d}@{self.domain_name}'
@classmethod
def convert_to_kwargs(cls, attrs):
kwargs = super().convert_to_kwargs(attrs)
if 'domain_name' in attrs:
kwargs['domain_name'] = attrs['domain_name']
return kwargs
Key requirements:
name(class attribute, required)A unique string identifier for the generator. Convention:
<module_name>.<generator_name>.allowed_on(class attribute, optional)Tuple of compatible field types. Include
valueto allow the generator on generated values and arguments. Set toNoneto allow any target._next(self, known_vals)(method, required)Generate and return the next value.
known_valsis a dict of target names to their already-generated values for the current record (only targets listed independsare guaranteed to be present).convert_to_kwargs(cls, attrs)(classmethod, optional)Override to convert XML/JSON attributes into
__init__keyword arguments. Always callsuper().convert_to_kwargs(attrs)first to handle the standard attributes (values,null_ratio,distribution,unique).
Once registered, the generator can be used in any blueprint:
Example
<field name="email" generator="my_module.sequential_email"
domain_name="mycompany.com"/>
Guidelines¶
The following guidelines are not hard rules, but they will help you write blueprints, and handle some edge-cases.
Choose counts that scale cleanly across tiers¶
A blueprint’s count values should produce a useful, browsable dataset at --scale 1 and
remain coherent at higher scales. A practical approach is to target three tiers:
1x (base) – a standalone demo or development dataset. Large enough to exercise pagination, search, and filters, but small enough to populate in seconds. Around 10 000 records for the main transactional model is a reasonable baseline.
10x – load-testing size (~100 000 records). Reveals UI slowdowns and unindexed query bottlenecks.
100x – stress-test size (~1 000 000 records). Surfaces ORM or PostgreSQL scalability limits.
Example
$ odoo-bin populate -d mydb -b my_module.demo # 1x — 10 000 tasks
$ odoo-bin populate -d mydb -b my_module.demo --scale 10 # 10x — 100 000 tasks
$ odoo-bin populate -d mydb -b my_module.demo --scale 100 # 100x — 1 000 000 tasks
Keep ratios realistic. Absolute counts matter less than the ratio between related models. If you create 120 projects and 10 000 tasks, that is roughly 80 tasks per project – a plausible average. At 100x, that becomes 12 000 projects and 1 000 000 tasks, which keeps the same ratio.
Master data ignores scale. Stage definitions, attribute sets, and similar configuration
records should always use scale="False" so they stay at their fixed count across all tiers.
Eight task stages at 1x is still eight task stages at 100x.
Use context to disable side effects¶
Bulk population is significantly faster when mail notifications, field tracking, and automatic record creation are disabled.
Example
<create model="account.move" count="15000" id="invoices"
context="{'mail_auto_subscribe_no_notify': True}">
...
</create>
Use partition="True" to avoid serialization errors in multi-worker mode¶
When creating child records in parallel (e.g. order lines for orders), you
should add partition="True" on the parent field whenever the parent model
has a stored computed field that depends on the children.
Example
<create model="sale.order.line" count="20000" id="order_lines">
<field name="order_id" ref="sale_orders" partition="True"/>
</create>
Without partitioning, workers pick parent IDs at random. Two workers can end
up creating lines for the same order simultaneously. Because sale.order
has stored computed fields that recompute when order_line changes,
both workers will try to write to the same order row at the same time.
PostgreSQL detects this conflict and raises a serialization error.
With partition="True", each worker is assigned a distinct, non-overlapping
subset of parent IDs. No two workers ever touch the same parent, so the
concurrent writes never collide and the serialization error cannot occur.
Use generated values for intermediate logic¶
Generated values are never written to the database, but make blueprints clearer and more maintainable. Use them for:
Correlated fields – generate a value once, reuse it in several persisted fields:
Example
<value name="first_name" generator="fake.first_name"/>
<value name="last_name" generator="fake.last_name"/>
<field name="name" eval="first_name + ' ' + last_name"/>
<field name="email"
eval="first_name.lower() + '.' + last_name.lower() + '@example.com'"/>
Multi-field uniqueness – pack multiple fields into a tuple and mark it unique, then unpack:
Example
<value name="generated_product_id" generator="relation.one"
comodel_name="product.product" ref="products"/>
<value name="generated_partner_id" generator="relation.one"
comodel_name="res.partner" ref="customers"/>
<value name="unique_pair" eval="(generated_product_id, generated_partner_id)" unique="True"/>
<field name="product_id" eval="unique_pair[0]"/>
<field name="partner_id" eval="unique_pair[1]"/>
Necessary when there is a composite unique constraint on two fields, but adding unique=True
on only one of the fields will restrain the possible combinations too much.
Computed quantities – derive a ratio, then apply it:
Example
<value name="ratio" generator="scalar.float"
start="0" end="1" distribution="beta(alpha=2, beta=2)"/>
<field name="qty_delivered" eval="product_uom_qty * ratio"/>
Use eval to derive values from parent records¶
When a child record needs a value that matches its parent (e.g. a subtask inherits its parent’s
project), use eval with model.browse() or env[...]:
Example
<!-- Subtasks inherit the project from their parent task -->
<field name="parent_id" ref="parent_tasks"/>
<field name="project_id" eval="model.browse(parent_id).project_id.id"/>
<!-- Invoice currency matches the journal's currency -->
<field name="currency_id"
eval="(journal := env['account.journal'].browse(journal_id)).currency_id.id
or journal.company_id.currency_id.id"/>
Use write blocks for two-phase creation¶
Some models require fields to be set in a specific order, or need a second pass to simulate
realistic state transitions. Use <write/> to update records that were created earlier:
Example
<!-- Phase 1: create product templates without variants -->
<create model="product.template" count="5000" id="templates"
context="{'create_product_product': False}">
<field name="name" generator="fake.catch_phrase"/>
</create>
<!-- Phase 2: add attribute lines (triggers variant creation) -->
<create model="product.template.attribute.line" count="8000" id="attr_lines">
<field name="product_tmpl_id" ref="templates"/>
...
</create>
<!-- Phase 3: update the generated variants -->
<write model="product.product" ref="templates.product_variant_ids">
<field name="default_code" generator="fake.ean13" unique="True"/>
</write>