Konfoo Documentation
Welcome to using Konfoo!
This manual outlines all aspects of Konfoo domain specific language (DSL from here on out). There is no particular order in which this documentation must be consumed - use the index on the left to navigate to the parts that interest you.
Konfoo at a glance
At the lowest level everything that makes up Konfoo configurator setup falls into one of three categories:
- Parts that define data (the overall structure, options and metadata) are written in YAML 1.2,
- everything that is scriptable (i.e. computed on the fly) on the backend is written in Rhai,
- GUI event handlers on the browser side are written in a limited subset of JavaScript.
Understanding of these languages is not assumed and is hopefully mitigated by ample number of examples, but at least a working knowledge of these technologies is encouraged.
Each configurator is made up of at least two parts:
- Domain - defines the underlying data structure
- Form logic - defines how the GUI maps user input to actual data structure fields
All parts of Konfoo configuration are designed to be version control friendly and human readable.
What is covered here
This documentation is divided into three main sections:
- Basic Operation - discusses how to design data models for storing input from your users and how to bind GUI elements to data structure elements
- Advanced topics - discusses scripting, aggregators and lookup tables
- Integration - describes Odoo specific features available in Konfoo
Domain
The other parts wouldn't make much sense without first defining the resulting data structure where the user input will live. For this reason the domain definition file is the one that sets Konfoo options and ties together all the parts that make up a fully functioning configurator.
It is probably best to start with a simple example:
root: MyDomainEntity
options:
form: my-form.yml
MyDomainEntity:
text_field: string
bool_field: bool
The above defines a single domain entity MyDomainEntity
that has two configurable fields called text_field
and bool_field
- as you can probably guess the value after the key signifies the type, more on that in Types.
You may have also noticed that there are a few reserved root-level keys that have special meaning, these are:
root
- which defines the main domain entity that will contain all the rest of the configuration substructuresoptions
- this can enable or disable a number of Konfoo features and is discuessed in Optionstypes
- this allows you to define new types that can be reused across all domain entities (see Type Definitions)
Every other root-level key in this YAML document is considered to be a domain entity and is expected to have the following structure:
NameOfDomainEntity:
field_1: field_type
field_2: field_type
# ...
field_N: field_type
It is important to note that every configuration is a tree structure where there is always a single domain entity at the top (root
). Each domain entity can in turn contain other entities - either as a value on a field or as elements of a list.
Options
The options
block in the main domain definition file primarily deals with tying everything together.
All paths are relative to the main file.
The example below outlines all options available in the main domain definition file:
options:
form: form.yml
aggregators:
- cost-summary.yml
- bom.yml
data:
- products.yml
- math-constants.yml
scripts:
common: common-functions.rhai
form
- this expects a single relative path to the form definition file
- see Form Description Language for more
aggregators
- expects a list of relative paths, each defines a single aggregator
- see Aggregators for more
data
- expects a list of relative paths, each can define a number of lookup tables
- see Lookup Tables for more
scripts
- expects key-value pairs
- the key is the global module name (e.g.
common::
in above example) - the value is a relative path to a script file
- see Modules for more
Domain Entities
First, an example of a configurable mug:
Mug:
meta:
title: Awesome Custom Mug
label: string
has_handle: bool
color:
type: choice
constraint: one
options:
- Black
- Sepia
- White
height:
type: numeric
min: 50
max: 150
step: 1
diameter:
type: numeric
min: 80
max: 100
step: 1
Fields
The domain entity structure is designed to be rather straight forward. In the example above the Mug
can have any number of fields, each with a corresponding Data Type.
Field names can be pretty much anything, but a few restrictions apply:
- Field names should not contain any whitespace characters
meta
is a reserved keyword and cannot be used as a field name
In the example above each field in the Domain Entity must declare a type. While some types can be declared with a shorthand notation (e.g. bool
) others have parameters that you can specify to constrain the input or enable other features.
A field with a bool
type could just as well be declared like this:
has_handle:
type: bool
Since the boolean type has no other parameters the short notation is more efficient.
Field structure
As you probably could already guess all field declarations share an underlying structure:
field_name:
type: <type name>
constraint: <optional|one|0..1|0..n>
type_specific_parameter1: ...
type_specific_parameter2: ...
# etc
The type
parameter is required, everything else is usually optional (i.e. with a sane default value) unless it is required by the specific type.
Metadata
Besides fields each entity can also contain a metadata block (meta
). Metadata is mainly there to support user-defined behaviors and is not processed by Konfoo.
The one exception to that is title
- this is used in various places when Konfoo needs to display a name for the domain entity in a user-friendly way.
Composition
The domain definition file can contain any number of different domain entities. Of course for them to be useful they need to be referenced by other entities.
To demonstrate such a system consider a very primitive shopping cart:
root: Cart
Cart:
products:
type: list
model: Mug
Mug:
color:
type: choice
constraint: one
options:
- Black
- Sepia
- White
Data Types
List of supported types
Numeric type
lucky_number:
type: numeric
default: 5 # initial value to use on this field
min: 1 # default: 0
max: 9 # default: Decimal::MAX (79228162514264337593543950335)
step: 1 # default: 1
String and multi-line string types
single_line_text: string
multi_line_text:
type: string
widget: text
Boolean type
yes_or_no: bool
Choice type
size:
type: choice
constraint: one # default: one | <one|optional|0..1|0..n>
widget: radio
options:
- Small
- Medium
- Large
extras:
type: choice
constraint: 0..n
widget: dropdown
options:
- Olives
- Peach
- Avocado
# Provides a searchable auto-complete style widget
continent:
type: choice
constraint: optional
widget: search
options:
- Africa
- Antarctica
- Asia
- Australia
- Europe
- North America
- South America
Color types
color: rgb
# yields a hex color code in format: #aabbcc
color_with_alpha: rgba
# yields a color code in format: rgba(255, 255, 255, 0.5)
Date type
date_field: date
# yields a date value in ISO format: 2022-12-31
List type
mugs:
type: list
constraint: 0..n
model: Mug # required
add: true # default: true
delete: true # default: true
Computed fields
volume:
type: compute
onchange:
- height
- diameter
code: |
fn compute_volume() {
not_null(self.height);
not_null(self.diameter);
let r = to_float(self.diameter) * 0.5;
return PI * r * r * to_float(self.height);
}
- Computed fields are automatically evaluated when any of the
onchange
fields are updated - The values are computed on the server side
- See Compute Fields for more
Type Definitions
In some cases you may wish to reuse a type definition.
root: Wall
Wall:
material_inside: WallMaterial
material_outside: WallMaterial
types:
WallMaterial:
type: choice
constraint: one
widget: dropdown
options:
- Plywood
- Plating
The structure for defining a new type is the same as the field structure.
You can also override parameters when using a custom type:
Wall:
material_inside: WallMaterial
material_outside:
type: WallMaterial
widget: radio
Overriding custom type parameters is available from
> 0.4.0
version onward.
Form Description Language
In upcoming versions the Form Description Language is planned to undergo major improvements.
For each Domain Entity defined in the previous chapter you can define how the input is gathered from the user for each particular entity.
The form description file is then linked to in the main domain definition file under options
by setting a relative path to your form description file to the form
key.
As an example here is a form definition for the Mug
defined in the previous chapter:
Mug:
- parallel:
- meta:
title: Size of mug
- input:
field: height
title: Mug Height
- input:
field: diameter
title: Mug Diameter
- parallel:
- meta:
title: Styling choices
- input:
field: has_handle
title: Has Handle
- input:
field: label
title: Text to print
- input:
field: color
title: Color
On the root level of this file Konfoo expects keys with the same names as the Domain Entities the form logic would be be used for. So for Mug
we define Mug
here as well.
Everything underneath the Domain Entity name should be list of Nodes.
Nodes
In the example above several different types of nodes are used:
- parallel
- A group node that signals Konfoo to ask input for all sub-nodes in one step
- Expects a list of sub-nodes under it
- input
- An input node maps a field in the Domain Entity to a form control
- Input nodes cannot have sub-nodes
field:
- field name in the Domain Entitytitle:
- the label to display to the user
- meta
- This provides metadata to any parent node
- Meta nodes expect key-value pairs under it
title
key is used as a user-friendly label for the particular parent node
For in-depth description see Types of Nodes. To understand how the web client builds the resulting forms see Traversal Strategies.
Types of Nodes
Input nodes
Maps Domain Entity fields to form controls. Cannot contain sub-nodes.
Example:
- input:
field: field_name # required
title: Field Name # default: field name
enabled: true # default: true
# meta - optional
# useful for user-defined behaviors
meta:
my_ref: important-input
# none - optional
# used by widgets that have a "no value" state
# (e.g. choice type with a dropdown widget)
none: Please select
# Events
# This function is called when the value changes and is successfully saved.
onchange: |
function(model, ctx, api) {
log('Field "field_name" has changed');
}
Group nodes
Depending on the traversal strategy a group node can indicate to Konfoo that the contained input
nodes must be considered together.
All group nodes expect the structure under them to be a list of other nodes.
In version
<= 0.4.0
the scene-graph only supportswizard
andsimple
traversal strategies and thus onlyparallel
group nodes are allowed.
A parallel
group node signals Konfoo to ask input for all sub-nodes in one step.
Example:
- parallel:
- input:
field: field1
title: First Field
- input:
field: field2
title: Second Field
Meta nodes
The purpose of meta
nodes is to provide metadata key-value pairs to the immediate parent node. This exists to support user-defined behaviors.
The only exception is the title
key which is used by Konfoo to display a user-friendly label for the node.
Example:
- parallel:
- meta:
title: Size
- input:
field: width
title: Width (mm)
- input:
field: height
title: Height (mm)
Exec nodes
exec
nodes allow you to execute code on the web client side. They are executed when they are encountered and that depends on the traversal strategy. This is useful for when you need to dynamically change something depending on the current state of the configuration.
Example:
- parallel:
- input:
field: my_field
title: My Field
- exec:
fn: |
function(model, ctx, api) {
const root = api.getRef(ctx.root);
if (!root)
return;
log('Field value is:', root.fields.my_field);
}
Model nodes
The model
nodes cannot be defined by the form description language itself. Instead they are generated at the boundaries of domain entities: for instance one domain entity contains a list of another domain entity.
Traversal Strategies
In version
<= 0.4.0
scene-graph traversal is fixed to thewizard
strategy and list item traversal is fixed to thesimple
strategy.This is subject to change in upcoming versions.
There are currently two strategies for generating the GUI layout from the nodes in the scene-graph:
- Wizard strategy
- Expects a list of
parallel
group nodes that containinput
nodes. - Each group node will generate a wizard step
- Expects a list of
- Simple strategy
- Expects a list of
input
nodes - For each
input
node a form control with a widget corresponding to the Domain Entity field is generated
- Expects a list of
Consider a form like this:
Mug:
- parallel:
- meta:
title: Size of mug
- input:
field: height
title: Mug Height
- input:
field: diameter
title: Mug Diameter
- parallel:
- meta:
title: Styling choices
- input:
field: has_handle
title: Has Handle
- input:
field: label
title: Text to print
- input:
field: color
title: Color
The above description builds a scene-graph that the Konfoo web-client will follow when gathering input. The resulting scene-graph will look like this:
+-- ModelNode(Mug)
|
+-- GroupNode(parallel, title="Size of mug")
| |
| +-- InputNode(height)
| +-- InputNode(diameter)
|
+-- GroupNode(parallel, title="Styling choices")
|
+-- InputNode(has_handle)
+-- InputNode(label)
+-- InputNode(color)
The meta
nodes are already merged into respective parent nodes.
Scripting
This chapter discusses how various scripting facilities in Konfoo work.
- Compute Fields - describes the features available to computed fields
- Expressions - describes the features available to aggregator expressions
- Modules - how to group shared functionality into a script module
- API Reference - list of all available API-s in the server-side script language
- Client-side API - list of all available API-s in the client-side script language
Compute Fields
A computed field is a field that cannot be set by the web-client. Instead it is re-evaluated every time any of it's dependencies are updated.
An example:
Square:
width: numeric
height: numeric
area:
type: compute
onchange:
- width
- height
code: |
fn compute_area() {
not_null(self.width);
not_null(self.height);
return to_float(self.width) * to_float(self.height);
}
Every compute field code block has access to a few global constants:
self
- This constant allows convenient access to all the data fields on the Domain Entity this compute field is being evaluated for.root
- Likeself
, but provides full read-only access to the root level Domain Entity. This include instance ID-s and other metadata besides data fields.by_id
- Provides a map for looking up any Domain Entity by their ID.
The example above also makes use of a few APIs:
not_null
- this is a special assertion function that stops the evaluation of the computed field safely without altering its value or causing an errorto_float
- this ensures we use the same datatype for our calculation in case our numeric value was an integer
For more please see API Reference.
Note that a computed field can still be displayed as a form element like any other field.
Program structure
When compiling the compute field program the entry-point is picked by the following rules:
- If a function with a name
main
is found then this is the entry-point - Otherwise the first non-anonymous function is designated as entry-point
For instance a more complex compute field could look like this:
computed_field:
type: compute
onchange:
- param1
- param2
code: |
fn compute_param2() {
not_null(self.param2);
return to_float(self.param2) * 0.42;
}
fn main() {
not_null(self.param1);
let bias = some_module::do_complex_calculation(self.param1);
not_null(bias);
return bias + compute_param2();
}
In versions
<= 0.4.0
using expressions in the compute fields is not supported. The code block must define a function.
Modules
As the complexity of your system grows you may wish to extract common functionality to a single place for maintainability. Script modules are how organizing shared code in Konfoo works.
All script modules are accessible from both compute fields and aggregators.
To define a new script module add a scripts
entry to your main domain definition file options
block:
options:
scripts:
my_module: relative/path/to/my_module.rhai
This will load my_module.rhai
from your path as a my_module::
global script module.
If my_module.rhai
looked like this:
fn my_function() {
return 42;
}
then in a compute field you could access that function like this:
computed_field:
type: compute
onchange:
- param
code: |
fn main() {
not_null(self.param);
return my_module::my_function() * self.param;
}
Expressions
Currently expressions are only usable in Aggregators - primarily to avoid needless boilerplate for defining a function for every calculation and variable access.
In version
<= 0.4.0
compute fields do not support expressions.
Expressions are prefixed with the string (expr)
and have access to everything compute fields have access to. For more please see API Reference.
An example of an aggregator rule:
- window_hinges:
domain: Window
require:
- self.hinges_product_code
- self.hinges_qty
model: mrp.bom.line
product.product.default_code: (expr) self.hinges_product_code
uom.uom.name: pcs
product_qty: (expr) self.hinges_qty
Any aggregator rule's output field that has the expression prefix is treated as code and the result of the evaluation is assigned as value to the output field. Anything else (such as uom.uom.name: pcs
) is treated as-is (in this case the string "pcs"
).
Normally expressions are expected to return a value that will be assigned to the field the expression is evaluated for. That does not mean you cannot do more complex operations inside the expression:
product_qty: |
(expr)
let magic_quantity = some_module::calculate_magic_stuff();
self.hinges_qty + magic_quantity
API Reference
Assertion functions
not_null(any)
- stops script execution gracefully when parameter is()
(unity aka null)
Math functions
min(i64, i64) -> i64
min(f64, f64) -> f64
max(i64, i64) -> i64
max(f64, f64) -> f64
round(f64 | i64) -> f64
- See rhai standard number functions for built-in functions.
String functions
join(array, delimiter: str, strict: bool) -> str
- Will join an array of strings into a string interleaved by delimiter
- Default for
delimiter
is a single space ( - Strict mode will not convert any non-string array elements into strings and will raise an error instead. This is false by default.
- See rhai standard string functions for built-in functions.
Type casting
to_int(f64 | i64) -> i64
to_float(f64 | i64) -> f64
to_int(str) -> i64
- if the value cannot be parsed the function will return
0
- use
parse_int
for finer control over this
- if the value cannot be parsed the function will return
to_float(str) -> f64
- if the value cannot be parsed the function will return a
NaN
constant - use
parse_float
for finer control over this
- if the value cannot be parsed the function will return a
Data processing
values(data: Map[], key: str) -> any[]
- returns values atkey
from every object indata
distinct(str[]) -> str[]
- returns all distinct strings in the input listcoalesce(any[]) -> any
- returns the first non-unity value in input
Lookup tables
group_by(data_source_name: str, index_name: str, query: str) -> Map[]
- lookup rows from
group
index
- lookup rows from
unique(data_source_name: str, index_name: str, query: str) -> Option<Map>
- lookup specific row from
unique
index
- lookup specific row from
group_filter(data_source_name: str, groups: ["<group index name>", ...], values: [<value1>, ...]) -> Map[]
- Drill-down filter for filtering results by several groups in left-to-right order.
- Each
(group, value)
pair will be run on only the results that matchedvalue
in the previous pair.
group_filter_inverse(data_source_name: str, groups: ["<group index name>", ...], values: [<value1>, ...]) -> Map[]
- Inverted drill-down filter for filtering results by several groups in left-to-right order.
- Each
(group, value)
pair will be run on only the results that did not matchvalue
in the previous pair.
Client-side Scripting
Client-side scripting is done in sandboxed limited JavaScript. There are two places where JavaScript code is executed: exec
nodes and input
node onchange
handlers.
All of the client-side script blocks must define a single self-contained function with the following signature:
function(model, context, api) {
/* ... */
}
model
- the current Domain Entity this field belongs to or the parent Domain Entity in case ofexec
nodescontext
- current configuration stateapi
- API object (see below)
All of these variables can easily be inspected by using console.log
.
API object
The client-side API object provides the following methods:
api.getRef(ref: ModelRef): ModelInstance | null
- lookup Domain Entity instance by reference object
api.getNode(): Node
- get current scene-graph node
api.setModelValue(modelId: string, modelName: string, fieldName: string, value: any)
- Causes a change delta in client-side just like editing a field would.
- Please note that all changes made in a client-side function are committed only after the function has returned.
Node object
The user interface of Konfoo is made up of Nodes that are arranged in a tree structure that is almost identical to the one you describe in the form definition file.
The Node
object returned by api.getNode()
provides the following interface:
isEnabled(): boolean
- Returns the enabled state of the Node
setEnabled(enabled: boolean)
- Sets the Node's enabled state. Nodes that are disabled are not displayed. This is useful for conditionally hiding nodes.
isLeaf(): boolean
- Return
true
when this Node contains no sub-nodes
- Return
getType(): NodeType
- Returns Node type. Can be one of the following:
'MODEL'
'INPUT'
'EXEC'
'META'
'GROUP'
getModel(): Model
- Returns the Model instance bound to this Node
getParent(): Node | null
- Returns the parent Node of this Node or
null
if the Node is already the root node of the user interface scene hierarchy.
- Returns the parent Node of this Node or
getSubnodes(): Node[]
- Returns a list of sub-nodes of the given Node.
findSubnode(metaKey: string, value: any): Node | null
- Returns the first match of a direct sub-node of the given Node by matching the given metadata key to the given value.
- Returns
null
if no match was found.
findNeighbour(metaKey: string, value: any): Node | null
- Returns the first match of neighbouring Node of the given Node by matching the given metadata key to the given value. This is equivalent of
node.getParent().findSubnode(...)
. - Returns
null
if no match was found.
- Returns the first match of neighbouring Node of the given Node by matching the given metadata key to the given value. This is equivalent of
getMetadata(key: string)
- Returns a metadata value for a given key.
setMetadata(key: string, value: any)
- Sets a metadata value for a given key.
Model object
The Model object is a client-side definition of the underlying Konfoo Domain Entity. This should be treated as read-only, any changes to Model state are not persisted in any way. This is Javascript though and you can do as you wish.
Aggregators
Konfoo provides a general purpose aggregator framework for generating of all sorts of outputs based on the initial configuration.
Some common use-cases:
- Bill of materials
- Estimates (time, cost, etc)
- Summaries
- Work instructions
Aggregators are described as a set of rules that all can be applied to some Domain Entity in the configuration state.
Each aggregator accepts the following root level keys:
options
- Options for the aggregator:name
- optional, default:null
path
- required, used to access the aggregator results by 3rd partiesstrip_empty
- optional, enables the feature to strip rule outputs that are designated "empty"
meta
- Provides options to send per-configuration metadata to 3rd party systemsrules
- List of Aggregator Rules
To add the aggregator to your Konfoo project you must include it in the aggregators
section in your main domain definition file:
options:
aggregators:
- bom.yml
The example below illustrates a bill of materials ruleset for calculating the amount of clay needed to make the Mug
from previous chapters.
# General options for this aggregator
options:
name: Bill of materials # optional, default: null
path: bom # required
# `path` is used in the output url: <konfoo-host>/agg/bom/<session>
# `------------------------------------------------´´´
# Takes a list of rule output object fields
# If the output object has any such field and
# and the value on the field is () or 0 the rule's output is discarded
strip_empty:
- product_qty
# Metadata - usually to provide extra information to 3rd party software
meta:
template_product: KONFOO-TEMPLATE
name: |
(expr)
if root.fields.project_name != () {
`MUG-{root.fields.height}-{root.fields.diameter}`
} else { () }
a_static_value: 42
# The actual aggregator rules
rules:
- amount_of_clay:
domain: Mug
require:
- self.height
- self.diameter
product_code: CLAY-001
unit: g
product_qty: |
(expr) clay_calculations::calculate_mug_clay(self.height, self.diameter)
- clay_for_handle:
domain: Mug
require:
- self.has_handle
product_code: CLAY-001
unit: g
product_qty: (expr) clay_calculations::calculate_handle_clay(self.height)
The above aggregator would then yield the following structure from the aggregator endpoint:
(e.g. by curl https://<konfoo-host>/agg/bom/<configuration-id>
)
{
"data": [
{
"__id__": "amount_of_clay",
"product_code": "CLAY-001",
"product_qty": 390,
"unit": "g"
},
{
"__id__": "clay_for_handle",
"product_code": "CLAY-001",
"product_qty": 78,
"unit": "g"
}
],
"meta": {
"product_name": "MUG-440-120",
"a_static_value": 42
},
"name": "Bill of materials"
}
Aggregator rules
Rules for defining aggregator rules:
- Every rule entry has a few reserved keywords:
domain
- Domain Entity name this rule is for, defaults to theroot
Domain Entityrequire
- List of expressions that must evaluate to a "truthy" value for this rule to be evaluatedcustom
- Allows overriding the output of the rule by returning an Map object yourself
- This does not require
(expr)
prefix - Any other fields besides the reserved ones are ignored when custom is present
- Every other key in the rule object is exported to output
- If a value for a key is prefixed by
(expr)
it is considered code and is evaluated when the aggregated result is requested - The evaluated expression is expected to return a value, but that is not enforced (null is OK)
- Every rule in the
rules
list is executed sequentially - Every key in the rule is evaluated in order of declaration
- Each rule is executed with it's own fresh scope - meaning variables defined in a previous key are available in the next
- Rule is only executed if every expression in
require
evaluates to a "truthy" value - Rules are expanded for every instance of the Domain Entity set
domain
- meaning one rule could be executed multiple times if there are multiple instances of a Domain Entity in the configuration - Every rule has access to the same script modules and APIs as compute functions
- The
self
constant refers to the specific Domain Entity instance the rule is currently being evaluated for
Let's look at one of the rules from the above example more closely:
rules:
# This is the ID of the rule.
# One rule could have multiple results in the aggregated object and this
# provides a trace back to from which rule the object originated from.
# This value is always present on the "__id__" field in the output.
- amount_of_clay:
domain: Mug # References the Domain Entity this rule deals with; default: root model
# Here we require that `height` and `diameter` fields of Mug are set
require:
- self.height
- self.diameter
# These two are static string values
product_code: CLAY-001
unit: g
# This value is calculated by a function in a script module
product_qty: |
(expr) clay_calculations::calculate_mug_clay(self.height, self.diameter)
Aggregator Metadata (Odoo)
The meta
block in each aggregator can provide arbitrary information for the integration software.
The following details are demonstrated using Odoo ERP integration.
An example
options:
# ... omitted ...
meta:
# Controls which Odoo product.template is copied for this instance.
template_product: KONFOO-TEMPLATE
# When set to true the `sale.order.name` is prepended to the product's `name` value computed below.
# Default is true.
use_parent_name_prefix: true
# Delimiter string used when generating the configure product's name. Default is a single space.
product_name_delimiter: "-"
# Simple parameters like these are set on the new product.template instance.
name: (expr) my_module::compute_product_name(root)
description: <b>Product Description</b>
# `parent.` prefix provides access to the containing `sale.order` data model.
parent.origin: Made by Konfoo
# `line.` prefix provides access to the `sale.order.line` data model that refers to the product above.
line.name: (expr) my_module::generate_long_product_description(root)
rules:
# ... omitted ...
Post-processing
Konfoo supports post-processing the output of the aggregator ruleset with predefined algorithms.
Example:
options:
name: Example Bill of Materials
path: bom
# Sum identical aggregator items
process:
- operation: sum_identical
params:
- product_qty
rules:
- example_rule1:
domain: Example
model: mrp.bom.line
require:
- self.product_id # let `product_id` be 1234
- self.qty_field # let `qty_field` be 2
product_id: (expr) self.product_id
product_qty: (expr) self.qty_field
product_uom_id: Units
- example_rule2:
domain: Example
model: mrp.bom.line
require:
- self.product_id
- self.qty_field
product_id: (expr) self.product_id
product_qty: (expr) self.qty_field
product_uom_id: Units
This will result in a rule output of:
{
"__id__": "summed-0", // This is generated automatically
"__instance__": "01GW9C7YEHP4YYEXAMPLEVALUE",
"domain": "Example",
"model": "mrp.bom.line",
"product_id": 1234,
"product_qty": 4, // 2 * self.qty_field
"product_uom_id": "Units"
}
Supported post-processing operations
sum_identical
Will sum the fields listed in params
of any output items (rules) that are otherwise identical.
The fields listed in params
and internal fields like __id__
and __instance__
are exempt from the equality test of output items.
NOTE: only fields with values that are convertible to floating point numbers can be summed.
strip_zero
Will remove any rule outputs that have a value of 0 on any of the fields given in params
(logical OR
).
This handles both integer and floating point values.
Lookup Tables
Often times you need other related data, that is not explicitly specified in the configurator by the end user, to create something useful out of the user's choices.
For this purpose Konfoo provides lookup tables:
products:
unique:
code: Product Code
group:
tag: Tag
csv: |
"Product Code","Tag","Weight","Unit"
"MUG-001","mug","300.0","g"
"MUG-002","mug","400.0","g"
"MUG-003","mug","260.0","g"
"CLAY-2K","clay","2000.0","g"
"CLAY-10K","clay","10000.0","g"
To connect your file containing your table to your Konfoo project include it in the data
section of options
in your main domain definition file:
options:
data:
- products.yml
- math-constants.yml
# etc
You can include multiple files in the data
section and each file can contain multiple lookup tables. This allows you to keep related data together and have good structural organization across your project.
The names of the lookup tables must be unique within in one Konfoo project even if the tables are defined in different files.
Lookup indices
Lookup tables provide two types of lookup index:
- Unique - can only be set on a column that has unique values
- Group - can be set on any column and will group rows by identical values in that column
While the above example defines only one index of either kind you can define however many your particular table needs.
Derived fields
Lookup tables also provide a way to auto-fill choice
field options.
See more in the derived fields chapter.
Unique Index
To look up data from the unique
index:
unique(data_source_name: str, index_name: str, query: str) -> Option<Map>
data_source_name
is the name you gave your lookup tableindex_name
is the name of the unique index you wish to queryquery
is the value that the index will search for- The return value is either a
Map
object containing the matched row or()
A query from our example table from earlier could look like this:
let value = unique("products", "code", "CLAY-2K");
/*
value = #{
"Product Code": "CLAY-2K",
"Tag": "clay",
"Weight": "2000.0",
"Unit": "g"
}
*/
Group Index
To look up data from the group
index:
group_by(data_source_name: str, index_name: str, query: str) -> Map[]
data_source_name
is the name you gave your lookup tableindex_name
is the name of the unique index you wish to queryquery
is the value that the index will search for- The return value is either a list of
Map
objects containing the matched rows
A query from our example table from earlier could look like this:
let values = group_by("products", "tag", "mug");
/*
values = [
#{
"Product Code": "MUG-001",
"Tag": "mug",
"Weight": "300.0",
"Unit": "g"
},
#{
"Product Code": "MUG-002",
"Tag": "mug",
"Weight": "400.0",
"Unit": "g"
},
#{
"Product Code": "MUG-003",
"Tag": "mug",
"Weight": "260.0",
"Unit": "g"
}
]
*/
Derived Fields
When you have indices defined on your lookup table you can also use the data in the index itself to populate choice fields.
Consider a choice field for selecting a product:
CartLine:
product:
type: choice
constraint: one
widget: dropdown
options:
- MUG-001
- MUG-002
- MUG-003
- CLAY-2K
- CLAY-10K
quantity:
type: numeric
min: 1
max: 100
step: 1
The same could be expressed like this assuming a lookup table products
from the previous chapter exists:
CartLine:
product:
type: choice
constraint: one
widget: dropdown
options: derive.products.unique.code
quantity:
type: numeric
min: 1
max: 100
step: 1
Odoo
Konfoo is designed to be easily integratable with any 3rd party software by providing a simple interface for designing aggregator rulesets which can produce meaningful data for any kind of software system from a potentially complex configuration object. See Aggregators for more.
For Odoo we offer a ready made integration module that allows you to:
- launch the product configurator directly from a Sale Order;
- add the resulting customized product to the order when you complete the configuration workflow;
- automatically generate a bill of materials with components and operations from the configuration;
- create custom datasets from any odoo model and synchronize them to Konfoo to be used as lookup tables;
- automate synchornizing datasets when changes to data happen;
For the Odoo module to work you must first configure it under Odoo Settings:
Aggregator rules for Odoo
The BOM creation is achieved via Aggregators that use output keys with special syntax that make use of Odoo features.
There are currently two field names that are reserved and handled specially to facilitate Odoo functionality:
- model - specifies which Odoo model to instantiate with this rule
- template - specifies which Odoo model instance to copy as a basis when instatiating an object from this rule
Note that the
template
key is usually paired with a lookup syntax (see examples below). While this is not mandatory it is generally the practical way of using this feature.
There are currently three types of Odoo-specific keys that can be used in aggregator rules:
- Assignment with a lookup:
some_record_id := my.model.reference_code: some lookup value
- Assignment from a locally created model:
some_field := a_field_on_the_rules_model: rule_name
- Note that you can only reference models created by rules that operate in the same
domain
and are created for the same instance of that particular domain.
- Assignment of a value computed in the aggregator
some_field: 1234
Configuration
Konfoo always allows you to create instances of the following models:
mrp.bom.line
mrp.routing.workcenter
product.product
You can allow additional models in the Konfoo module under Technical → Allowed models.
Examples
Basic BOM line
Consider an aggregator like this:
rules:
- trafos:
domain: Light
require:
- self.product
- self.quantity
- util::lights_unit_is_continuous(self.product)
model: mrp.bom.line
product_id := product.product.default_code: TRAFO-0100
product_uom_id := uom.uom.name: pcs
product_qty: (expr) round(to_float(self.quantity) / 6.0 + 0.5)
From the output of this aggregator the Odoo module will:
- Know based on
model
value that this rule must create a newmrp.bom.line
instance - Knows to look up the value for
product_id
fromproduct.product
using Odoo DOM[('default_code', '=', 'TRAFO-0100')]
- Knows to look up the value for
product_uom_id
fromuom.uom
by[('name', '=', 'pcs')]
- Knows to set
product_qty
value to the value computed by the aggregator
Creating a new product
This example creates a new product.product
instance based on a template product, sets some custom attributes and then uses it on a BOM line.
rules:
- window_glass_product:
domain: Window
require:
- self.material
- self.width
- self.height
model: product.product
template := product.product.default_code: GLASS-TEMPLATE
default_code: (expr) `GLASS-W${self.width}-H${self.height}`
product_width: (expr) self.width
product_height: (expr) self.height
- window:
domain: Window
require:
- self.material
- self.unit
- self.width
- self.height
model: mrp.bom.line
product_id := id: window_glass_product
product_uom_id := uom.uom.name: (expr) self.unit
product_qty: (expr) self.width * self.height
The first rule (window_glass_product
) will:
- Create an instance of
product.product
, but it will copy it from a product that hasdefault_code
set to'GLASS-TEMPLATE'
- Override
default_code
with a computed value - Set the custom fields
product_width
andproduct_height
to the computed values
The second rule (window
) will:
- Set
product_id
value to the product generated in the previous rule for this instance ofWindow
- Set
product_uom_id
andproduct_qty
values just as in the previous example
Data Synchronization
Before you can use the data sync functionality make sure you've set up Konfoo Sync URL
and Konfoo Sync Key
parameters under Settings → Konfoo
Datasets
Opening the Konfoo application you are first presented with a datasets management interface.
Here you can configure all lookup tables that you wish to export to your Konfoo instance. Every dataset described here is exported to Konfoo as a single Lookup Table.
Let's look at the dataset test
from the above example. In the detail view you can see that it exports records of type Product Template
and filters them to not contain any products that have the Internal Reference
field set to "KONFOO-TEMPLATE"
. Naturally if you wish to export every record of the given model the domain filter can be left empty.
Every column can potentially be indexed with a Unique or a Group index. Unique indices require that the exported values actually are unique.
The id
column is always exported automatically for every Odoo model and is always set to have a unique index.
You can see your custom filter in action by clicking on the "N RECORDS" link under the Domain
field.
When editing a dataset keep in mind that some changes may require the entire dataset to be resynchronized to Konfoo. This is usually not a problem, but, for instance, changing the name of the dataset here also means you need to account for that change in your configurator source code.
When used in Konfoo this will behave just like any other Lookup Table configuration:
test:
unique:
id: id
default_code: Tootekood
group:
name: Nimi
csv: |
"id","Nimi","Tootekood","Mõõtühik"
2,mock1,mock1,Units
4,Fäänsi Nimi 三角形,mock3,Units
3,"Toode, Komaga",mock2,Units
Automation
By default the Konfoo module creates a Scheduled Action (Settings → Technical → Scheduled Actions) called Konfoo - Sync datasets
. This action is by default disabled and you should configure it to fit your use-case.
This action:
- can be run multiple times and will continue where it left off the last time;
- will respect any cron thread time limit you have configured in Odoo;
- will only synchronize records that have changed since the last time they were synchronized;
- sends data in batches - the batch size can be configured in settings;