Skip to content

Cyllo Configuration & Maintenance Documentation

Introduction

1. Module Manifest Configuration

Every Cyllo module must define a manifest.py at its root. This file controls how the module is registered, loaded, and displayed in the Apps menu. Canonical Manifest Template

{
    'name': 'Cyllo My Feature',
    'version': '1.0.0',                  # Always use Cyllo 1.x.x format
    'category': 'Sales',                 # Must match an existing Cyllo category
    'summary': 'One-line description of what this module does',
    'description': 'Extended description for the Apps listing page.',
    'author': 'Your Name, Cyllo',        # Always append ", Cyllo"
    'company': 'Cyllo',
    'maintainer': 'Cyllo',
    'website': 'https://www.cyllo.com',
    'license': 'LGPL-3',
    'images': ['static/description/banner.png'],  # Required for App Store listing

    'depends': [
        'cyllo_base',    # Required for all Cyllo-branded modules
        'sale',          # Add standard Cyllo modules as needed
    ],

    'data': [
        # Load order matters -- security files must come first
        'security/ir.model.access.csv',
        'security/security.xml',
        'data/data.xml',
        'views/my_model_views.xml',
        'views/menu_views.xml',
        'reports/my_report_action.xml',
        'reports/my_report.xml',
    ],

    'demo': [
        'demo/demo.xml',
    ],

    'assets': {
        'web.assets_backend': [
            'my_module/static/src/components/**/*.js',
            'my_module/static/src/components/**/*.xml',
            'my_module/static/src/css/*.css',
        ],
    },

    'installable': True,
    'application': False,       # True only for top-level app modules
    'auto_install': False,      # True only for glue/bridge modules
}

Key Manifest Fields

FieldRequiredDescription
nameYesHuman-readable module name shown in Apps
versionYesSemantic version in major.minor.patch format
categoryYesCyllo category (Sales, Accounting, HR, etc.)
dependsYesList of modules that must load before this one
dataYesOrdered list of XML/CSV files to load on install
installableYesMust be True for the module to appear in the App list
applicationNoSet True for top-level apps with their own main menu
auto_installNoSet True for bridge modules that auto-enable when all dependencies are installed
imagesRecommendedPath to a banner image for the App Store listing

Load Order Rules Data files under data are loaded in the exact order listed. Follow this ordering:

  1. Security files (ir.model.access.csv, security.xml)
  2. Configuration data (data/data.xml)
  3. Views (views/*.xml)
  4. Menus (views/menu_views.xml) — always last among views
  5. Reports (reports/*.xml)
  6. Email templates (data/mail_template*.xml)
  7. Wizards (wizards/*.xml) — if they have their own views

2. Version Numbering & Upgrade Strategy

Cyllo uses a three-part semantic versioning scheme for all modules. Version Format

{major}.{minor}.

PartIncrement WhenAction Required on Upgrade
majorModel schema changes (new columns, renamed fields, new relational tables)Migration script required
minorNew non-breaking features (new views, new optional fields)Module upgrade required (-u module_name)
patchBug fixes, logic corrections, view tweaksServer restart required

First Release Convention All new modules start at 1.0.0. Never start at 0.x.x for production modules.

Checking Installed Module Version
 In a Python shell or migration script
module = env['ir.module.module'].search([('name', '=', 'my_module')], limit=1)
print(module.installed_version)  # e.g. '1.2.3'
Running an Upgrade
## Upgrade a single module
./cyllo-bin -u my_module -d my_database

## Upgrade multiple modules
./cyllo-bin -u module_a,module_b -d my_database

## Upgrade all installed modules (use with caution in production)
./cyllo-bin -u all -d my_database

3. Configuration Parameters (ir.config_parameter)

ir.config_parameter is Cyllo's key-value store for system-wide technical settings. Values are stored as strings and are accessible from any model or controller.

Reading a Parameter
from odoo import models, api

class MyModel(models.Model):
    """Example showing how to read a system config parameter."""

    _name = 'my.model'
    _description = 'My Model'

    def get_api_url(self):
        """Return the configured external API endpoint URL."""
        return self.env['ir.config_parameter'].sudo().get_param(
            'my_module.api_url',
            default='https://api.example.com'
        )

Writing a Parameter

self.env['ir.config_parameter'].sudo().set_param(
    'my_module.api_url',
    'https://api.example.com/v2'
)

Defining Parameters in Data Files

<!-- data/config_data.xml -->
<odoo>
    <data noupdate="1">
        <record id="param_api_url" model="ir.config_parameter">
            <field name="key">my_module.api_url</field>
            <field name="value">https://api.example.com</field>
        </record>
    </data>
</odoo>

Note

Use noupdate="1" for parameters that administrators may change after installation. This prevents your default value from overwriting their customization on module upgrade.

Naming Convention Always prefix parameter keys with your module name to avoid collisions:

my_module.setting_name       # Good
setting_name                 # Bad -- risks collision with other modules

4. System Settings & res.config.settings

For user-facing settings that appear in the Settings menu (Technical → General Settings), extend res.config.settings. Adding a Settings Field models/res_config_settings.py

import logging
from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class ResConfigSettings(models.TransientModel):
    """Extend system settings to expose My Module configuration in the UI."""

    _inherit = 'res.config.settings'

    my_module_api_url = fields.Char(
        string='API Endpoint URL',
        config_parameter='my_module.api_url',
        help='Base URL for the external API integration.',
    )
    my_module_enable_sync = fields.Boolean(
        string='Enable Auto-Sync',
        config_parameter='my_module.enable_sync',
    )

    @api.model
    def get_values(self):
        """Load current parameter values into the settings form."""
        res = super().get_values()
        res['my_module_api_url'] = self.env['ir.config_parameter'].sudo().get_param(
            'my_module.api_url', default=''
        )
        return res

    def set_values(self):
        """Persist updated settings back to ir.config_parameter."""
        super().set_values()
        self.env['ir.config_parameter'].sudo().set_param(
            'my_module.api_url', self.my_module_api_url or ''
        )
        _logger.info('my_module: API URL updated to %s', self.my_module_api_url)

Shortcut: For simple fields, use config_parameter='key.name' directly on the field definition. Cyllo will automatically wire get_values and set_values for you. Only override those methods when you need custom logic.

Adding the Settings to the UI

<!-- views/res_config_settings_views.xml -->
<odoo>
    <record id="res_config_settings_view_my_module" model="ir.ui.view">
        <field name="name">res.config.settings.view.my.module</field>
        <field name="model">res.config.settings</field>
        <field name="inherit_id" ref="base_setup.action_general_configuration"/>
        <field name="arch" type="xml">
            <xpath expr="//div[@id='integrations']" position="after">
                <div id="my_module_settings" class="o_setting_box">
                    <div class="o_setting_left_pane">
                        <field name="my_module_enable_sync"/>
                    </div>
                    <div class="o_setting_right_pane">
                        <label for="my_module_enable_sync" string="My Module Integration"/>
                        <div class="text-muted">
                            Enable automatic synchronization with the external service.
                        </div>
                        <div attrs="{'invisible': [('my_module_enable_sync', '=', False)]}">
                            <label for="my_module_api_url" string="API Endpoint"/>
                            <field name="my_module_api_url" class="o_light_label"/>
                        </div>
                    </div>
                </div>
            </xpath>
        </field>
    </record>
</odoo>

5. Data Files & Demo Data

Regular Data (data/) Data files under data/ are loaded on install and upgrade. Use them for:

  • Default records (sequence definitions, stages, status values)
  • Email templates
  • System parameters
  • Report paper formats
<!-- data/data.xml -->
<odoo>
    <data noupdate="1">
        <!-- Sequence for auto-numbering -->
        <record id="seq_my_model" model="ir.sequence">
            <field name="name">My Model Sequence</field>
            <field name="code">my.model</field>
            <field name="prefix">MM/%(year)s/</field>
            <field name="padding">4</field>
            <field name="company_id" eval="False"/>
        </record>

        <!-- Default stage records -->
        <record id="stage_new" model="my.model.stage">
            <field name="name">New</field>
            <field name="sequence">1</field>
        </record>
    </data>
</odoo>

noupdate Behaviour

ValueEffect
noupdate="0" (default)Records are re-created or updated on every module upgrade
noupdate="1"Records are only created on first install; upgrades skip them

Use noupdate="1" for any record that administrators are expected to customize (stages, sequences, email templates, settings). Demo Data (demo/) Demo data is only loaded when the database is created with the Load Demo Data option enabled. Use it for:

  • Sample records that demonstrate module functionality
  • Test partner/product/order records
<!-- demo/demo.xml -->
<odoo>
    <record id="demo_order_1" model="my.model">
        <field name="name">Demo Record 001</field>
        <field name="partner_id" ref="base.res_partner_1"/>
        <field name="state">draft</field>
    </record>
</odoo>

6. Security Configuration

Access Control Lists (ACL) ACLs are defined in security/ir.model.access.csv. Every model that stores data must have at least one ACL entry. id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_my_model_user,my.model user,model_my_model,base.group_user,1,1,1,0 access_my_model_manager,my.model manager,model_my_model,base.group_system,1,1,1,1

Column reference:

ColumnDescription
idUnique XML ID for this access rule
nameHuman-readable label (shown in debug mode)
model_id:idXML ID of the model: model_ + model name with dots replaced by underscores
group_id:idXML ID of the security group. Omit to apply to all users.
perm_read/write/create/unlink1 to grant, 0 to deny

Security Groups

Define custom groups in security/security.xml:

<odoo>
    <data>
        <record id="group_my_module_user" model="res.groups">
            <field name="name">My Module / User</field>
            <field name="category_id" ref="base.module_category_sales_sales"/>
            <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
        </record>

        <record id="group_my_module_manager" model="res.groups">
            <field name="name">My Module / Manager</field>
            <field name="category_id" ref="base.module_category_sales_sales"/>
            <field name="implied_ids" eval="[(4, ref('group_my_module_user'))]"/>
        </record>
    </data>
</odoo>

Record Rules (Row-Level Security)

Use record rules to restrict which records a group can see:

<record id="rule_my_model_user" model="ir.rule">
    <field name="name">My Model: Users see own records</field>
    <field name="model_id" ref="model_my_model"/>
    <field name="groups" eval="[(4, ref('group_my_module_user'))]"/>
    <field name="domain_force">[('user_id', '=', user.id)]</field>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="True"/>
    <field name="perm_create" eval="True"/>
    <field name="perm_unlink" eval="False"/>
</record>

7. Scheduled Actions (Cron Jobs)

Define recurring server-side jobs in data files. Cyllo cron jobs run via the ir.cron model. Defining a Cron Job

<odoo>
    <data noupdate="1">
        <record id="cron_sync_external" model="ir.cron">
            <field name="name">My Module: Sync External Data</field>
            <field name="model_id" ref="my_module.model_my_model"/>
            <field name="state">code</field>
            <field name="code">model.action_sync_all()</field>
            <field name="interval_number">1</field>
            <field name="interval_type">hours</field>  <!-- minutes, hours, days, weeks, months -->
            <field name="numbercall">-1</field>         <!-- -1 = run indefinitely -->
            <field name="active">True</field>
            <field name="priority">5</field>            <!-- Lower = higher priority (1-10) -->
        </record>
    </data>
</odoo>

The Method Called by the Cron

@api.model
def action_sync_all(self):
    """Synchronize all active records with the external service.

    Called by the ir.cron job 'My Module: Sync External Data'.
    Processes records in batches to avoid timeouts.

    Returns:
        None
    """
    records = self.search([('active', '=', True)])
    for batch in self._batched(records, size=100):
        batch._sync_external()
        self.env.cr.commit()   # Commit each batch independently

Interval Types Reference

interval_typeMeaning
minutesEvery N minutes
hoursEvery N hours
daysEvery N days
weeksEvery N weeks
monthsEvery N months

8. Database Maintenance

Cyllo Database Backup Module Cyllo ships with a dedicated Database Backup Management module that automates backups without manual intervention. To configure:

  1. Navigate to Settings → Database Backup Management
  2. Create a new backup configuration
  3. Set your database name and master password
  4. Choose backup format: zip (with filestore) or dump (SQL only)
  5. Set schedule frequency and configure a destination

Supported destinations:

DestinationNotes
Local filesystemPath on the server
Google DriveRequires OAuth credentials
Amazon S3Requires access key and secret
DropboxRequires Dropbox app token
SFTPRemote server over SSH
FTPRemote server over FTP

Manual Backup via CLI

## Create a zip backup (includes filestore)
./cyllo-bin --database my_db --backup-format zip --backup-dest /var/backups/

## Restore from backup
./cyllo-bin --database my_db --restore-file /var/backups/my_db_backup.zip

Cleaning Transient Records

TransientModels (models.TransientModel) auto-clean via a built-in cron job. To manually clean:

## Run in a shell
env['base.automation']._gc_transient_models()

Or adjust the built-in cron interval: Settings → Technical → Scheduled Actions → "Base: Auto-vacuum internal data". Vacuuming the PostgreSQL Database After bulk deletions, reclaim disk space:

## Connect to PostgreSQL
psql -U cyllo -d my_database

## Reclaim space from deleted rows
VACUUM ANALYZE;

## Full vacuum (locks table — run during maintenance window)
VACUUM FULL ANALYZE;

9. Logging & Debugging

Python Logger Setup

Every Cyllo Python file that generates logs must declare a module-level logger: import logging

_logger = logging.getLogger(__name__)

Usage inside methods:

_logger.debug('Processing record %s', record.name)
_logger.info('Sync completed: %d records updated', count)
_logger.warning('API rate limit approaching: %d calls remaining', remaining)
_logger.error('Failed to connect to API: %s', str(e))
_logger.exception('Unexpected error in sync')   # Includes full traceback

Log Level Configuration

In cyllo.conf:

[options]
log_level = info                        # debug, info, warning, error, critical
log_handler = :INFO,werkzeug:WARNING    # Override per-logger
logfile = /var/log/cyllo/cyllo.log

To enable debug logging only for your module without affecting the rest of the system:
log_handler = :WARNING,my_module:DEBUG

Debug Mode via URL

Activate debug mode from the browser without a server restart:

## Enable debug mode
https://yourdomain.com/web?debug=1

## Enable debug mode with assets (forces JS/CSS recompile)
https://yourdomain.com/web?debug=assets

Or navigate to Settings → Activate the developer mode. Useful Debug Queries

## Print a model's field definitions
env['my.model'].fields_get()

## Print all rules applied to a model
env['ir.rule'].search([('model_id.model', '=', 'my.model')])

## Check which groups a user belongs to
env.user.groups_id.mapped('full_name')

## Check what module a view belongs to
env['ir.ui.view'].search([('model', '=', 'my.model')]).mapped('module')

10. Asset Management & Static Files

Asset Bundle Declaration

Frontend assets (JS, CSS, OWL components) are registered in manifest.py under the assets key:

'assets': {
    # Backend (main web client)
    'web.assets_backend': [
        'my_module/static/src/components/**/*.js',
        'my_module/static/src/components/**/*.xml',
        'my_module/static/src/css/my_styles.css',
    ],
    # Point-of-Sale assets
    'point_of_sale.assets': [
        'my_module/static/src/pos/**/*.js',
    ],
    # Website frontend
    'web.assets_frontend': [
        'my_module/static/src/website/*.css',
    ],
}

Forcing Asset Recompilation

During development, if CSS or JS changes are not reflected:

## Restart with asset update
./cyllo-bin -u my_module -d my_database

## Or clear the asset cache from the UI:
## Settings → Technical → User Interface → Assets Bundles → delete all records

Static File Structure

my_module/
└── static/
    ├── description/
    │   └── banner.png          # App Store listing image (1200x630px recommended)
    └── src/
        ├── components/
        │   ├── my_widget.js
        │   └── my_widget.xml
        ├── css/
        │   └── my_styles.css
        └── img/
            └── logo.png

11. Module Upgrade & Migration

Writing a Migration Script

When a module version bump involves model changes (column renames, type changes, data transformations), create a migration script:

my_module/
└── migrations/
    └── 1.1.0/
        ├── pre-migrate.py      # Runs before the ORM applies schema changes
        └── post-migrate.py     # Runs after schema changes are applied

Pre-migration Example

## migrations/1.1.0/pre-migrate.py

import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
    """Rename column before ORM recreates the field under the new name.

    Args:
        cr: Database cursor.
        version (str): Previous installed version string (e.g. '1.0.3').
    """
    if not version:
        return   # Fresh install -- skip migration

    _logger.info('my_module 1.1.0: renaming old_field_name to new_field_name')
    cr.execute("""
        ALTER TABLE my_model
        RENAME COLUMN old_field_name TO new_field_name
    """)

Post-migration Example

## migrations/1.1.0/post-migrate.py

import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
    """Backfill computed column values after ORM migration.

    Args:
        cr: Database cursor.
        version (str): Previous installed version string.
    """
    if not version:
        return

    _logger.info('my_module 1.1.0: backfilling status field')
    cr.execute("""
        UPDATE my_model
        SET status = 'active'
        WHERE status IS NULL
          AND active = True
    """)
    _logger.info('Backfill complete: %d rows updated', cr.rowcount)

Safe Column Operations

## Check if column exists before renaming (defensive pattern)
cr.execute("""
    SELECT column_name
    FROM information_schema.columns
    WHERE table_name = 'my_model'
      AND column_name = 'old_field_name'
""")
if cr.fetchone():
    cr.execute("ALTER TABLE my_model RENAME COLUMN old_field_name TO new_field_name")

12. Performance Tuning

Database Indexing Add indexes to fields used in domain filters and _order:

## In model field definition
partner_id = fields.Many2one('res.partner', index=True)
date = fields.Date(index=True)
state = fields.Selection([...], index=True)

For composite indexes (multiple columns), use _sql_constraints or a post_init_hook:

def _post_init_hook(cr, registry):
    """Create composite index after module install."""
    cr.execute("""
        CREATE INDEX IF NOT EXISTS my_model_partner_date_idx
        ON my_model (partner_id, date DESC)
    """)

Prefetching & Batch Processing

Bad — N+1 query problem

for record in records:
   print(record.partner_id.name)   # Triggers a query per record

## Good -- prefetch in one query
records.mapped('partner_id.name')

Batch large operations

def process_in_batches(records, size=500):
   """Process records in batches to avoid memory and timeout issues."""
   for i in range(0, len(records), size):
       batch = records[i:i + size]
       batch._do_work()
       self.env.cr.commit()

Stored vs. Non-stored Computed Fields

TypeWhen to UseTrade-off
store=TrueField is frequently searched or sortedUses disk space; must recompute on dependency changes
store=False (default)Field is only displayed, rarely searchedNo disk usage; computed on every read

Cache Invalidation Clear ORM cache for specific records after raw SQL updates records.invalidate_recordset()

Clear a specific field's cache records.invalidate_recordset(fnames=['total', 'state'])

13. Multi-Company Configuration

Company-Aware Models

class MyModel(models.Model):
   """A model with multi-company isolation."""

   _name = 'my.model'
   _description = 'My Model'

   company_id = fields.Many2one(
       'res.company',
       string='Company',
       required=True,
       default=lambda self: self.env.company,
   )

   _sql_constraints = [
       ('name_company_uniq', 'unique(name, company_id)',
        'Name must be unique per company.'),
   ]

Company-Based Record Rules

<record id="rule_my_model_company" model="ir.rule">
   <field name="name">My Model: Multi-company isolation</field>
   <field name="model_id" ref="my_module.model_my_model"/>
   <field name="domain_force">
       ['|', ('company_id', '=', False),
              ('company_id', 'in', company_ids)]
   </field>
</record>

Reading Company-Specific Settings

Read parameter for current company

def _get_company_param(self, key):
  """Retrieve a company-scoped configuration parameter.

  Args:
      key (str): Parameter key suffix (module.key format).

  Returns:
      str: Parameter value, or empty string if not set.
  """
  full_key = f'{key}_{self.env.company.id}'
  return self.env['ir.config_parameter'].sudo().get_param(full_key, '')

14. Environment-Specific Configuration

cyllo.conf Key Settings

[options]
addons_path = /opt/cyllo/cyllo,/opt/cyllo/custom_addons
db_host = localhost
db_port = 5432
db_user = cyllo
db_password = secret
db_name = my_database

## Performance
workers = 4                 # Number of HTTP worker processes (set to CPU cores × 2)
max_cron_threads = 2        # Dedicated cron worker threads
limit_time_cpu = 60         # CPU time limit per request (seconds)
limit_time_real = 120       # Real-time limit per request (seconds)
limit_memory_soft = 2147483648   # 2 GB soft memory limit
limit_memory_hard = 2684354560   # 2.5 GB hard memory limit

## Logging
log_level = info
logfile = /var/log/cyllo/cyllo.log

## Email
smtp_server = smtp.gmail.com
smtp_port = 587
smtp_ssl = starttls
smtp_user = noreply@yourcompany.com
smtp_password = app_password_here

## Security
admin_passwd = your_master_password
list_db = False             # Disable the database list page in production

Environment Variable Overrides

Cyllo respects environment variables for sensitive values in containerized deployments: export ODOO_DB_HOST=postgres export ODOO_DB_PORT=5432 export ODOO_DB_USER=cyllo export ODOO_DB_PASSWORD=secret

Development vs. Production Checklist

SettingDevelopmentProduction
workers0 (single-threaded)CPU count × 2
log_leveldebugwarning or info
list_dbTrueFalse
admin_passwdSimpleStrong, hashed
max_cron_threads12–4
Demo dataEnabledDisabled

15. Common Maintenance Tasks

Restart the Cyllo Server

## Systemd service
sudo systemctl restart cyllo

## Docker Compose
docker-compose restart web

## Direct process
kill -HUP $(cat /var/run/cyllo.pid)

Reinstall a Module (Clean Slate)

## Uninstall then reinstall -- WARNING: deletes module data
./cyllo-bin -d my_database shell <<EOF
env['ir.module.module'].search([('name', '=', 'my_module')]).button_uninstall()
env.cr.commit()
EOF

## Then reinstall
./cyllo-bin -d my_database -i my_module

Update Translations

## Export PO file for translation
./cyllo-bin --i18n-export=/tmp/my_module.po --modules=my_module --language=ar_SA -d my_db

## Import translated PO file
./cyllo-bin --i18n-import=/tmp/my_module_ar.po --language=ar_SA --modules=my_module -d my_db

Clear Sessions

Remove all browser sessions (forces all users to log in again) rm -rf /var/lib/cyllo/sessions/*

Inspect the Installed Module State

## In an Odoo shell (./cyllo-bin shell -d my_database)
module = env['ir.module.module'].search([('name', '=', 'my_module')])
print(module.state)            # installed, uninstalled, to upgrade, etc.
print(module.installed_version)
print(module.latest_version)

Reset a Sequence

## Reset a sequence counter (e.g., after demo data cleanup)
seq = env['ir.sequence'].search([('code', '=', 'my.model')])
seq.write({'number_next': 1})

Force-Recompute a Stored Computed Field

## In a shell or migration script
records = env['my.model'].search([])
records._compute_total()       # Call the compute method directly
env.cr.commit()

Quick Reference

CLI Commands Summary

TaskCommand
Install module./cyllo-bin -i my_module -d my_db
Upgrade module./cyllo-bin -u my_module -d my_db
Open Python shell./cyllo-bin shell -d my_db
Run tests./cyllo-bin --test-enable -u my_module -d my_db
Export translation./cyllo-bin --i18n-export=out.po --modules=my_module -d my_db
Scaffold new module./cyllo-bin scaffold my_module /path/to/addons

Useful Model References

ModelPurpose
ir.config_parameterKey-value system settings
ir.cronScheduled/recurring jobs
ir.sequenceAuto-incrementing sequences
ir.ruleRecord-level access rules
ir.model.accessModel-level ACL entries
res.config.settingsUser-facing settings form
ir.module.moduleInstalled module registry
ir.ui.viewView definitions