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
| Field | Required | Description |
|---|---|---|
| name | Yes | Human-readable module name shown in Apps |
| version | Yes | Semantic version in major.minor.patch format |
| category | Yes | Cyllo category (Sales, Accounting, HR, etc.) |
| depends | Yes | List of modules that must load before this one |
| data | Yes | Ordered list of XML/CSV files to load on install |
| installable | Yes | Must be True for the module to appear in the App list |
| application | No | Set True for top-level apps with their own main menu |
| auto_install | No | Set True for bridge modules that auto-enable when all dependencies are installed |
| images | Recommended | Path 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:
- Security files (ir.model.access.csv, security.xml)
- Configuration data (data/data.xml)
- Views (views/*.xml)
- Menus (views/menu_views.xml) — always last among views
- Reports (reports/*.xml)
- Email templates (data/mail_template*.xml)
- 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}.
| Part | Increment When | Action Required on Upgrade |
|---|---|---|
| major | Model schema changes (new columns, renamed fields, new relational tables) | Migration script required |
| minor | New non-breaking features (new views, new optional fields) | Module upgrade required (-u module_name) |
| patch | Bug fixes, logic corrections, view tweaks | Server 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_database3. 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 modules4. 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
| Value | Effect |
|---|---|
| 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:
| Column | Description |
|---|---|
| id | Unique XML ID for this access rule |
| name | Human-readable label (shown in debug mode) |
| model_id:id | XML ID of the model: model_ + model name with dots replaced by underscores |
| group_id:id | XML ID of the security group. Omit to apply to all users. |
| perm_read/write/create/unlink | 1 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 independentlyInterval Types Reference
| interval_type | Meaning |
|---|---|
| minutes | Every N minutes |
| hours | Every N hours |
| days | Every N days |
| weeks | Every N weeks |
| months | Every 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:
- Navigate to Settings → Database Backup Management
- Create a new backup configuration
- Set your database name and master password
- Choose backup format: zip (with filestore) or dump (SQL only)
- Set schedule frequency and configure a destination
Supported destinations:
| Destination | Notes |
|---|---|
| Local filesystem | Path on the server |
| Google Drive | Requires OAuth credentials |
| Amazon S3 | Requires access key and secret |
| Dropbox | Requires Dropbox app token |
| SFTP | Remote server over SSH |
| FTP | Remote 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.zipCleaning 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 tracebackLog 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:DEBUGDebug 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=assetsOr 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 recordsStatic 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.png11. 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 appliedPre-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
| Type | When to Use | Trade-off |
|---|---|---|
| store=True | Field is frequently searched or sorted | Uses disk space; must recompute on dependency changes |
| store=False (default) | Field is only displayed, rarely searched | No 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 productionEnvironment 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
| Setting | Development | Production |
|---|---|---|
| workers | 0 (single-threaded) | CPU count × 2 |
| log_level | debug | warning or info |
| list_db | True | False |
| admin_passwd | Simple | Strong, hashed |
| max_cron_threads | 1 | 2–4 |
| Demo data | Enabled | Disabled |
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_moduleUpdate 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_dbClear 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
| Task | Command |
|---|---|
| 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
| Model | Purpose |
|---|---|
| ir.config_parameter | Key-value system settings |
| ir.cron | Scheduled/recurring jobs |
| ir.sequence | Auto-incrementing sequences |
| ir.rule | Record-level access rules |
| ir.model.access | Model-level ACL entries |
| res.config.settings | User-facing settings form |
| ir.module.module | Installed module registry |
| ir.ui.view | View definitions |

