# -*- coding: utf-8 -*- from odoo import fields, models, api, _ from odoo.exceptions import ValidationError class BaseDashboard(models.Model): _name = 'base.dashbord' _description = 'Dashboard Builder' _order = 'sequence' sequence = fields.Integer() name = fields.Char( string='Card name', translate=True ) model_name = fields.Char(string='Model Name') model_id = fields.Many2one( string='Model', comodel_name='ir.model' ) line_ids = fields.One2many( string='Lines', comodel_name='base.dashbord.line', inverse_name='board_id' ) icon_type = fields.Selection( [('image', 'Image'), ('icon', 'Icon Library')], string='Icon Type', default='image', required=True ) icon_name = fields.Char( string='Icon Class', help="FontAwesome class (e.g., 'fa-plane', 'fa-users'). See https://fontawesome.com/v4/icons/" ) icon_preview_html = fields.Html( compute='_compute_icon_preview', string="Icon/Image Preview" ) card_image = fields.Binary(string='Card Image') is_self_service = fields.Boolean(string='Self Service?') is_financial_impact = fields.Boolean(string='Without Financial Impact?') form_view_id = fields.Many2one('ir.ui.view', string='Form View') list_view_id = fields.Many2one('ir.ui.view', string='List View') action_id = fields.Many2one('ir.actions.act_window', string='Action') field_id = fields.Many2one('ir.model.fields', string='Fields') is_button = fields.Boolean(string='Is Button') is_stage = fields.Boolean(string='Is Stage', compute='_compute_field', store=True) is_double = fields.Boolean(string='Is Double', compute='_compute_field', store=True) is_state = fields.Boolean(string='Is State', compute='_compute_field', store=True) action_domain = fields.Char( string='Action Domain', compute='_compute_action_domain', store=True ) action_context = fields.Char( string='Action Context', compute='_compute_action_domain', store=True ) relation = fields.Char(string='Relation') search_field = fields.Char( string='Search Field', required=True, default='employee_id.user_id' ) @api.depends('icon_type', 'icon_name', 'card_image') def _compute_icon_preview(self): for record in self: if record.icon_type == 'icon' and record.icon_name: record.icon_preview_html = f'
' elif record.icon_type == 'image' and record.card_image: try: img_rec = record.with_context(bin_size=False) image_data = img_rec.card_image.decode('utf-8') if isinstance(img_rec.card_image, bytes) else img_rec.card_image record.icon_preview_html = f'
' except Exception: record.icon_preview_html = '
' else: record.icon_preview_html = '
' def unlink_nodes(self): for rec in self: rec.is_button = False nodes = self.env['node.state'].sudo().search([ ('model_id', '=', rec.model_id.id), ('is_workflow', '=', False) ]) nodes.sudo().unlink() def unlink(self): for rec in self: rec.unlink_nodes() return super(BaseDashboard, self).unlink() @api.constrains('action_id') def _check_action_id(self): for record in self: is_record = self.sudo().search_count([('action_id', '=', record.action_id.id)]) if is_record > 1: raise ValidationError(_('There is already a record for this action.')) @api.depends('action_id') def _compute_action_domain(self): for record in self: record.action_domain = record.action_id.domain if record.action_id else False record.action_context = record.action_id.context if record.action_id else False @api.onchange('model_id') def _get_stage_value(self): for rec in self: if rec.model_id: rec.model_name = rec.model_id.model @api.depends('model_name') def _compute_field(self): for rec in self: rec.is_stage = False rec.is_double = False rec.is_state = False if rec.model_id and rec.model_name and rec.model_name in self.env: model = self.env[rec.model_name] # hr.holidays has special case (can have both state and stage) if rec.model_name in ('hr.holidays', 'hr.leave'): rec.is_double = True elif 'state' in model._fields: rec.is_state = True elif 'stage_id' in model._fields: rec.is_stage = True @api.depends('name', 'model_id') def _compute_display_name(self): for record in self: if record.name: record.display_name = record.name elif record.model_id: record.display_name = record.model_id.name else: record.display_name = _('Dashboard Card') def _get_stage(self, rel): """Get stages from relation model and create them in intermediate table""" for rec in self: current_model = self.env['stage.stage'].sudo().search([('model_id', '=', rec.model_id.id)]) stage_ids = self.env[rel].sudo().search([]) if not current_model: for stage in stage_ids: value = stage.with_context(lang=self.env.user.lang).name self.env['stage.stage'].sudo().create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'stage_id': stage.id, 'name': stage.name, 'value': value }) else: self.update_selection() def update_selection(self): """Update states/stages when dynamic workflow changes""" odoo_dynamic_workflow = self.env['ir.module.module'].sudo().search([ ('name', '=', 'odoo_dynamic_workflow') ]) for rec in self: if odoo_dynamic_workflow and odoo_dynamic_workflow.state == 'installed': if rec.model_name and rec.model_name in self.env: model = self.env[rec.model_name] work_folow_active = self.env['odoo.workflow'].sudo().search([ ('model_id', '=', rec.model_id.id), ('active', '=', True) ]) state = self.env['node.state'].sudo().search([('is_workflow', '=', True)]) work_folow_name = work_folow_active.node_ids.filtered( lambda r: r.code_node == False and r.active == True ).mapped("node_name") state_name = state.mapped('state') if not rec.is_stage and rec.model_name not in ('hr.holidays', 'hr.leave'): for line in work_folow_active.node_ids: if not line.code_node and line.active: if not self.env['node.state'].sudo().search([('state', '=', line.node_name)]): self.env['node.state'].create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'action_id': rec.action_id.id, 'state': line.node_name, 'name': line.name, 'is_workflow': True }) diffs = list(set(state_name) - set(work_folow_name)) self.env['node.state'].sudo().search([('state', 'in', diffs)]).unlink() # Handle stage updates if rec.is_stage: rel = self.env['ir.model.fields'].sudo().search([ ('model_id', '=', rec.model_id.id), ('name', '=', 'stage_id') ]) current_model = self.env['stage.stage'].sudo().search([('model_id', '=', rec.model_id.id)]).ids if rel: rel_ids = self.env[rel.relation].sudo().search([]) for r in rel_ids: if r.id not in current_model: self.env['stage.stage'].create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'stage_id': r.id, 'name': r.name }) def compute_selection(self): """Compute states or stages depending on chosen model""" for rec in self: rec.is_button = True if not rec.model_name or rec.model_name not in self.env: raise ValidationError(_('Please select a valid model first.')) model = self.env[rec.model_name] current_model = self.env['node.state'].sudo().search([('model_id', '=', rec.model_id.id)]) # Handle hr.holidays/hr.leave (can have both states and stages) if rec.model_name in ('hr.holidays', 'hr.leave'): rec.is_double = True if not current_model: if 'state' in model._fields: nodes = model._fields.get('state')._description_selection(self.env) for node in nodes: self.env['node.state'].create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'action_id': rec.action_id.id, 'state': node[0], 'name': node[1] }) rel = self.env['ir.model.fields'].sudo().search([ ('model_id', '=', rec.model_id.id), ('name', '=', 'stage_id') ]) if rel: rel_ids = self.env[rel.relation].sudo().search([]) for r in rel_ids: if hasattr(r, 'state') and r.state == 'approved': self.env['node.state'].create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'action_id': rec.action_id.id, 'stage_id': r.id, 'name': r.name, 'is_holiday_workflow': True }) else: self.update_selection() # Handle state-based models elif 'state' in model._fields: if not current_model: nodes = model._fields.get('state')._description_selection(self.env) for node in nodes: self.env['node.state'].create({ 'model_id': rec.model_id.id, 'form_view_id': rec.form_view_id.id, 'list_view_id': rec.list_view_id.id, 'action_id': rec.action_id.id, 'state': node[0], 'name': node[1] }) else: self.update_selection() # Handle stage-based models elif 'stage_id' in model._fields: rel = self.env['ir.model.fields'].sudo().search([ ('model_id', '=', rec.model_id.id), ('name', '=', 'stage_id') ]) if rel: self._get_stage(rel.relation) else: raise ValidationError(_('This model has no states nor stages.')) class BaseDashboardLine(models.Model): _name = 'base.dashbord.line' _description = 'Dashboard Builder Line' name = fields.Char(string='Name') group_ids = fields.Many2many( string='Groups', comodel_name='res.groups' ) board_id = fields.Many2one( string='Dashboard', comodel_name='base.dashbord', ondelete='cascade' ) state_id = fields.Many2one( string='State', comodel_name='node.state' ) stage_id = fields.Many2one( string='Stage', comodel_name='stage.stage' ) model_id = fields.Many2one( string='Model', comodel_name='ir.model' ) model_name = fields.Char(string='Model Name') sequence = fields.Integer(string='Sequence') @api.onchange('state_id') def onchange_state_id(self): if self.state_id: state_ids = [stat.state_id.id for stat in self.board_id.line_ids] if state_ids.count(self.state_id.id) > 2: raise ValidationError(_('This state is already selected.')) @api.onchange('stage_id') def onchange_stage_id(self): if self.stage_id: stage_ids = [stat.stage_id.id for stat in self.board_id.line_ids] if stage_ids.count(self.stage_id.id) > 2: raise ValidationError(_('This stage is already selected.')) class NodeState(models.Model): _name = 'node.state' _description = 'Dashboard Node State' name = fields.Char(string='Name', translate=True) state = fields.Char(string='State', translate=True) stage_id = fields.Char(string='Stage', readonly=True) is_workflow = fields.Boolean(string='Is Workflow', readonly=True) is_holiday_workflow = fields.Boolean(string='Is Holiday Workflow', readonly=True) model_id = fields.Many2one( string='Model', comodel_name='ir.model', readonly=True ) form_view_id = fields.Many2one( 'ir.ui.view', string='Form View', readonly=True ) list_view_id = fields.Many2one( 'ir.ui.view', string='List View', readonly=True ) action_id = fields.Many2one( 'ir.actions.act_window', string='Action', readonly=True ) @api.depends('name', 'state') def _compute_display_name(self): for record in self: if self.env.user.lang == 'en_US': record.display_name = record.state or record.name or '' else: record.display_name = record.name or record.state or '' class StageStage(models.Model): _name = 'stage.stage' _description = 'Dashboard Stage' name = fields.Char(string='Name', translate=True, readonly=True) stage_id = fields.Char(string='Stage', readonly=True) value = fields.Char(string='Value', readonly=True) model_id = fields.Many2one( string='Model', comodel_name='ir.model', readonly=True ) form_view_id = fields.Many2one( 'ir.ui.view', string='Form View', readonly=True ) list_view_id = fields.Many2one( 'ir.ui.view', string='List View', readonly=True ) action_id = fields.Many2one( 'ir.actions.act_window', string='Action', readonly=True ) @api.depends('name', 'value') def _compute_display_name(self): for record in self: if self.env.user.lang == 'en_US': record.display_name = record.name or '' else: record.display_name = record.value or record.name or ''