Hi everyone!
I’m working on an improved version of the Odoo node for n8n, with a modular structure and several enhancements.
I’ve already fixed the issue where search domain values were incorrectly treated as strings instead of booleans, numbers, or arrays (see forum post). Someone else also addressed this via a PR that added support for custom domain filters, based on this issue. I took a different approach but also incorporated their workaround.
Now, I’m working on improving the getAll
operation to allow building complex Odoo search domains directly from the n8n UI.
Currently, filters are added as a simple AND
combination:
[('name', '=', 'John Doe')]
[('is_company', '=', True)]
[('create_date', '>=', '2024-01-01')]
But I want users to create advanced, nested domains like:
['|',
['&',
('active', '=', True),
['|',
('name', 'ilike', 'Jean'),
('name', 'ilike', 'Marie')
]
],
['!',
('email', 'ilike', 'banned.com')
]
]
Odoo’s UI handles this beautifully:
This would eliminate the need for workarounds like the domain filter PR, and allow dynamic field selection based on the chosen model.
I’d still keep the raw JSON input option for power users who prefer full control.
Here’s the challenge: I can’t find any support for recursive fixedCollection
inputs in n8n. Has anyone managed something like this? My current solution forces users into a rigid 3-level filter structure—unusable for simple filters and too inflexible for deep nesting.
Any tips or ideas on how to make this truly flexible?
Here is the current getAll
operation part and code :
import {
type IDataObject,
IExecuteFunctions,
INodeProperties,
updateDisplayOptions,
} from 'n8n-workflow';
import {
type IOdooFilterOperations,
OdooCredentialsInterface,
odooHTTPRequest,
processFilters,
} from '../../GenericFunctions';
import { fieldsToInclude, returnAllOrLimit } from '../../descriptions';
export const logicalOperator: INodeProperties[] = [
{
displayName: 'Logical Operator',
name: 'logicalOperator',
type: 'options',
options: [
{
name: '&',
value: 'AND',
description: 'Logical AND (default). Combines the next two criteria or groups. Arity: 2',
},
{
name: '|',
value: 'OR',
description: 'Logical OR. Combines the next two criteria or groups. Arity: 2',
},
{
name: '!',
value: 'NOT',
description: 'Logical NOT. Negates the next criterion or group. Arity: 1',
},
],
default: 'AND',
},
];
export const searchDomain: INodeProperties[] = [
{
displayName: 'Search Domain',
name: 'searchDomain',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Domain Search',
},
default: {},
description: 'Filter request by applying filters',
placeholder: 'Add Domain Search',
options: [
{
displayName: 'Search Domain',
name: 'searchDomain',
values: [
{
displayName: 'Field Name or ID',
name: 'fieldName',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
default: '',
typeOptions: {
loadOptionsDependsOn: ['customResource'],
loadOptionsMethod: 'getModelFields',
},
},
{
displayName: 'Operator',
name: 'operator',
type: 'options',
default: 'equalTo',
description: 'Specify an operator',
options: [
{
name: '=',
value: 'equalTo',
description: 'Equals to',
},
{
name: '!=',
value: 'notEqualTo',
description: 'Not equals to',
},
{
name: '>',
value: 'greaterThan',
description: 'Greater than',
},
{
name: '>=',
value: 'greaterOrEqualThan',
description: 'Greater than or equal to',
},
{
name: '<',
value: 'lesserThan',
description: 'Less than',
},
{
name: '<=',
value: 'lesserOrEqualThan',
description: 'Less than or equal to',
},
{
name: '=?',
value: 'unsetOrEqualTo',
description:
'Unset or equals to (returns true if value is either None or False, otherwise behaves like =)',
},
{
name: '=Like',
value: 'equalLike',
description:
'Matches field_name against the value pattern. An underscore _ in the pattern matches any single character; a percent sign % matches any string of zero or more characters.',
},
{
name: 'Like',
value: 'like',
description:
'Matches field_name against the %value% pattern. Similar to =like but wraps value with % before matching.',
},
{
name: 'Not Like',
value: 'notLike',
description: 'Doesn’t match against the %value% pattern',
},
{
name: 'Ilike',
value: 'ilike',
description: 'Case insensitive like',
},
{
name: 'Not Ilike',
value: 'notIlike',
description: 'Case insensitive not like',
},
{
name: '=Ilike',
value: 'equalIlike',
description: 'Case insensitive =like',
},
{
name: 'In',
value: 'in',
description:
'Is equal to any of the items from value. Value should be a list of items.',
},
{
name: 'Not In',
value: 'notIn',
description: 'Is unequal to all of the items from value',
},
{
name: 'Child_of',
value: 'childOf',
description:
'Is a child (descendant) of a value record. Takes the semantics of the model into account (i.e., following the relationship field named by _parent_name).',
},
{
name: 'Parent_of',
value: 'parentOf',
description:
'Is a parent (ascendant) of a value record. Takes the semantics of the model into account (i.e., following the relationship field named by _parent_name).',
},
{
name: 'Any',
value: 'any',
description:
'Matches if any record in the relationship traversal through field_name (Many2one, One2many, or Many2many) satisfies the provided domain value',
},
{
name: 'Not Any',
value: 'notAny',
description:
'Matches if no record in the relationship traversal through field_name (Many2one, One2many, or Many2many) satisfies the provided domain value',
},
],
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Specify value for comparison',
},
],
},
],
},
];
export const subNestedLogicalGroup: INodeProperties[] = [
{
displayName: 'Sub Nested Logical Group',
name: 'subNestedLogicalGroup',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Sub Nested Logical Group',
},
default: {},
description: 'Sub nested logical group to combine multiple filters',
placeholder: 'Add Sub Nested Logical Group',
options: [
{
displayName: 'Sub Nested Logical Group',
name: 'subNestedLogicalGroup',
values: [...logicalOperator, ...searchDomain],
},
],
},
];
export const nestedLogicalGroup: INodeProperties[] = [
{
displayName: 'Nested Logical Group',
name: 'nestedLogicalGroup',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Nested Logical Group',
},
default: {},
description: 'Nested logical group to combine multiple filters',
placeholder: 'Add Nested Logical Group',
options: [
{
displayName: 'Nested Logical Group',
name: 'nestedLogicalGroup',
values: [...logicalOperator, ...subNestedLogicalGroup],
},
],
},
];
export const logicalGroup: INodeProperties[] = [
{
displayName: 'Logical Group',
name: 'logicalGroup',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Logical Group',
},
default: {},
placeholder: 'Add Logical Group',
description: 'Logical group to combine multiple filters',
options: [
{
displayName: 'Logical Group',
name: 'logicalGroup',
values: [...logicalOperator, ...nestedLogicalGroup],
},
],
},
];
export const properties: INodeProperties[] = [
...returnAllOrLimit,
...fieldsToInclude,
...logicalGroup,
];
const displayOptions = {
show: {
resource: ['custom'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);