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 substructures
  • options - this can enable or disable a number of Konfoo features and is discuessed in Options
  • types - 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
  • 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 Entity
    • title: - 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 supports wizard and simple traversal strategies and thus only parallel 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 the wizard strategy and list item traversal is fixed to the simple 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 contain input nodes.
    • Each group node will generate a wizard step
  • 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

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 - Like self, 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 error
  • to_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
  • 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

Data processing

  • values(data: Map[], key: str) -> any[] - returns values at key from every object in data
  • distinct(str[]) -> str[] - returns all distinct strings in the input list
  • coalesce(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
  • unique(data_source_name: str, index_name: str, query: str) -> Option<Map>
    • lookup specific row from unique index
  • 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 matched value 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 match value 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 of exec nodes
  • context - current configuration state
  • api - 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
  • 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.
  • 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.
  • 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 parties
    • strip_empty - optional, enables the feature to strip rule outputs that are designated "empty"
  • meta - Provides options to send per-configuration metadata to 3rd party systems
  • rules - 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 the root Domain Entity
    • require - List of expressions that must evaluate to a "truthy" value for this rule to be evaluated
    • custom
      • 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 table
  • index_name is the name of the unique index you wish to query
  • query 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 table
  • index_name is the name of the unique index you wish to query
  • query 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:

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.

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 new mrp.bom.line instance
  • Knows to look up the value for product_id from product.product using Odoo DOM [('default_code', '=', 'TRAFO-0100')]
  • Knows to look up the value for product_uom_id from uom.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 has default_code set to 'GLASS-TEMPLATE'
  • Override default_code with a computed value
  • Set the custom fields product_width and product_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 of Window
  • Set product_uom_id and product_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

settings

Datasets

konfoo app

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.

datasets

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.

dataset view

You can see your custom filter in action by clicking on the "N RECORDS" link under the Domain field.

dataset records

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.

edit dataset

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;

scheduled action