# -*- coding: utf-8 -*- from odoo import api, fields, models class SaasApp(models.Model): """Mirrors REAL installed Odoo "apps" (not technical dependency modules) so the public /trial "Choose your Apps" page only shows what is actually installed on this internal instance - exactly like the odoo.com/trial reference page. Odoo's ir.module.module already flags genuine apps via the `application = True` field (set by each module's __manifest__.py with 'application': True). Pure dependency/technical modules (e.g. 'mail', 'web', 'base_setup') have application = False and are therefore excluded automatically - no manual curation needed. """ _name = 'saas.app' _description = 'SaaS Sellable App (synced from installed Odoo apps)' _order = 'category_sequence, sequence, name' name = fields.Char(required=True) technical_module_name = fields.Char( required=True, help="Technical name of the Odoo module to install, e.g. 'website', 'crm', 'account'." ) module_id = fields.Many2one('ir.module.module', string='Source Module', ondelete='cascade') icon = fields.Image('Icon', max_width=128, max_height=128) icon_url = fields.Char() # fallback path to module's static icon if no binary icon description = fields.Text() category_id = fields.Many2one('ir.module.category', string='App Category') category_sequence = fields.Integer(related='category_id.sequence', store=True) sequence = fields.Integer(default=10) active = fields.Boolean(default=True) _sql_constraints = [ ('module_unique', 'unique(module_id)', 'Each installed app can only be listed once.'), ] @api.model def _sync_from_installed_modules(self): """Step: Internal Apps Mirror Reads every module on THIS Odoo instance that is: - state = 'installed' (actually installed internally) - application = True (a real "app", not a dependency module) and upserts a saas.app record for each, so the public /trial page always reflects exactly what is installed - nothing more. """ Module = self.env['ir.module.module'].sudo() installed_apps = Module.search([ ('state', '=', 'installed'), ('application', '=', True), ]) existing = self.sudo().search([]) existing_by_module = {a.module_id.id: a for a in existing} for module in installed_apps: vals = { 'name': module.shortdesc or module.name, 'technical_module_name': module.name, 'module_id': module.id, 'description': module.summary or module.description, 'category_id': module.category_id.id if module.category_id else False, 'icon_url': f'/{module.name}/static/description/icon.png', 'active': True, } if module.id in existing_by_module: existing_by_module[module.id].sudo().write(vals) else: self.sudo().create(vals) # Deactivate saas.app entries whose module got uninstalled meanwhile stale = existing.filtered(lambda a: a.module_id.id not in installed_apps.ids) stale.sudo().write({'active': False}) return True