691 lines
29 KiB
Python
691 lines
29 KiB
Python
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()
|
|
|
|
rec_name ="model_id "
|
|
|
|
|
|
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='Line',
|
|
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")
|
|
|
|
@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:
|
|
# Dynamic Icon Preview - Class based sizing
|
|
record.icon_preview_html = f'<div class="dashboard-icon-preview-box icon-mode"><i class="fa {record.icon_name}"></i></div>'
|
|
elif record.icon_type == 'image' and record.card_image:
|
|
# Actual Image Preview
|
|
# CRITICAL: Use bin_size=False to ensure we get DATA not SIZE TEXT (e.g. "25 KB")
|
|
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'<div class="dashboard-icon-preview-box image-mode"><img src="data:image/png;base64,{image_data}"/></div>'
|
|
except Exception:
|
|
record.icon_preview_html = '<div class="dashboard-icon-preview-box error-mode"><i class="fa fa-exclamation-triangle"></i></div>'
|
|
else:
|
|
# Default Placeholder
|
|
record.icon_preview_html = '<div class="dashboard-icon-preview-box default-mode"><i class="fa fa-th-large"></i></div>'
|
|
|
|
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_domain',
|
|
compute='_compute_action_domain',
|
|
store=True )
|
|
relation = fields.Char(
|
|
string='action_domain',
|
|
)
|
|
search_field = fields.Char(string='Search field', required=True, default='employee_id.user_id')
|
|
|
|
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 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
|
|
record.action_context = record.action_id.context
|
|
|
|
|
|
@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:
|
|
if rec.model_id:
|
|
#use those fileds to attr invisiable
|
|
model = self.env[rec.model_name]
|
|
#model holidays has special case,
|
|
#becuase when holiday work flow installed records of leave can hold state or stage depends on holiday type.
|
|
if rec.model_name == 'hr.holidays':
|
|
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 name_get(self):
|
|
result = []
|
|
for record in self:
|
|
if record.name:
|
|
name = record.name
|
|
else:
|
|
name = record.model_id.name
|
|
result.append((record.id, name))
|
|
return result
|
|
|
|
def _get_stage(self,rel):
|
|
#this method recive the relation model and return the stage on this obj
|
|
#we get the stage ids for rel 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:
|
|
# TODO: find a way to fix translation without searching in translation model &
|
|
#we got translaton issue stage name is translatable ,so we get the stage translation
|
|
value = stage.with_context(lang=self.env.user.lang).name
|
|
#value = self.env['ir.translation'].sudo().search([('source','=',stage.name)],limit=1).value
|
|
stage = 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):
|
|
'''
|
|
in this method we need to check if new states or stages were added
|
|
we have a module in which new state will be added "odoo_dynamic_workflow",
|
|
so we need to check if its installed , and get only the new state and update the intermediate object with it.
|
|
same goes when deleting state , we search for the deleted ones and remove them from our intermediate object.
|
|
we have three types of lines choose either stage or state or both depends on the model,
|
|
both is only when the modeli is "hr.holidays",because there is a workflow mdouel that replace the state with stage depends on holiday type
|
|
|
|
'''
|
|
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':
|
|
model = self.env[rec.model_name]
|
|
work_folow_active = self.env['odoo.workflow'].sudo().search([('model_id','=',rec.model_id.id),('active','=',True)])
|
|
work_folow_inactive = self.env['odoo.workflow'].sudo().search([('model_id','=',rec.model_id.id),('active','=',False)])
|
|
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 != 'hr.holidays':
|
|
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)]):
|
|
state = 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)))
|
|
state = self.env['node.state'].sudo().search([('state','in',diffs)]).unlink()
|
|
|
|
for line in work_folow_inactive.node_ids:
|
|
to_remove = []
|
|
if not line.code_node:
|
|
to_remove.append(line.node_name)
|
|
for stat in rec.line_ids:
|
|
if stat.state_id.state == line.node_name or stat.state_id.name == line.name or stat.state_id.state == line.node_name or stat.state_id.name == line.name:
|
|
stat.unlink()
|
|
self.env['node.state'].sudo().search([('state','in',to_remove)]).unlink()
|
|
|
|
|
|
|
|
|
|
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
|
|
rel_ids = self.env[rel.relation].sudo().search([])
|
|
if current_model:
|
|
for rel in rel_ids:
|
|
if rel.id not in current_model:
|
|
stage = 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':rel.id ,'name':rel.name,})
|
|
|
|
if rec.model_name == 'hr.holidays':
|
|
rel = self.env['ir.model.fields'].sudo().search([('model_id','=',rec.model_id.id),('name','=','stage_id')])
|
|
current_model = self.env['node.state'].sudo().search([('model_id','=',rec.model_id.id)]).mapped('name')
|
|
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)]):
|
|
state = 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})
|
|
if rel:
|
|
rel_ids = self.env[rel.relation].sudo().search([('state','=','approved')]).mapped('name')
|
|
diffs = (list(set(rel_ids) - set(current_model)))
|
|
for diff in diffs:
|
|
stage = 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':rel.id ,'state':diff,'name':diff,'is_holiday_workflow':True})
|
|
|
|
holiday_workflow_leave = self.env['node.state'].sudo().search([('is_holiday_workflow','=',True)]).mapped( "name" )
|
|
diffs = (list(set(holiday_workflow_leave) - set(rel_ids)))
|
|
state = self.env['node.state'].sudo().search([('name','in',diffs)]).unlink()
|
|
|
|
for line in work_folow_inactive.node_ids:
|
|
diff = []
|
|
if not line.code_node:
|
|
diff.append(line.node_name)
|
|
for stat in rec.line_ids:
|
|
if stat.state_id.state == line.node_name or stat.state_id.name == line.name or stat.state_id.state == line.node_name or stat.state_id.name == line.name:
|
|
stat.unlink()
|
|
self.env['node.state'].sudo().search([('state','in',diffs)]).unlink()
|
|
|
|
def compute_selection(self):
|
|
''' method used to compute the states or stages depends on chosen model
|
|
it looks in model fields and see if it contains state or stage fields
|
|
then dive in those fields and get their values and create them in a middle obj
|
|
yet "hr.holidays could have both states and stages if holiday workflow module is installed"
|
|
'''
|
|
|
|
for rec in self:
|
|
rec.is_button = True
|
|
model = self.env[rec.model_name]
|
|
current_model = self.env['node.state'].sudo().search([('model_id','=',rec.model_id.id)])
|
|
if rec.model_name == 'hr.holidays':
|
|
rec.is_double = True
|
|
if not current_model:
|
|
nodes = model._fields.get('state')._description_selection(self.env)
|
|
rel = self.env['ir.model.fields'].sudo().search([('model_id','=',rec.model_id.id),('name','=','stage_id')])
|
|
|
|
|
|
for node in nodes:
|
|
state = 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]})
|
|
if rel:
|
|
rel_ids = self.env[rel.relation].sudo().search([])
|
|
for rel in rel_ids:
|
|
if rel.state == 'approved':
|
|
stage = 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':rel.id ,'name':rel.name,'is_holiday_workflow':True})
|
|
|
|
else:
|
|
self.update_selection()
|
|
|
|
if 'state' in model._fields and rec.model_name != 'hr.holidays' :
|
|
if not current_model:
|
|
nodes = model._fields.get('state')._description_selection(self.env)
|
|
|
|
for node in nodes:
|
|
state = 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()
|
|
|
|
elif 'stage_id' in model._fields and rec.model_name != 'hr.holidays':
|
|
rel = self.env['ir.model.fields'].sudo().search([('model_id','=',rec.model_id.id),('name','=','stage_id')])
|
|
#call method to get the stage of the model
|
|
self._get_stage(rel.relation)
|
|
|
|
if 'state' not in model._fields and not 'stage_id' in model._fields :
|
|
#raise error if no state nor state
|
|
raise ValidationError(_('This model has no states nor stages.'))
|
|
|
|
|
|
class BaseDashboardUser(models.Model):
|
|
_name = 'base.dashbord.line'
|
|
|
|
|
|
name = fields.Char(
|
|
string='name',
|
|
)
|
|
|
|
group_ids = fields.Many2many(
|
|
string='Group',
|
|
comodel_name='res.groups',
|
|
|
|
|
|
)
|
|
board_id = fields.Many2one(
|
|
string='Bord',
|
|
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):
|
|
|
|
state_ids = []
|
|
if self.state_id:
|
|
for stat in self.board_id.line_ids:
|
|
state_ids.append(stat.state_id.id)
|
|
if state_ids.count(self.state_id.id) > 2:
|
|
raise ValidationError(_('already choose this state'))
|
|
|
|
|
|
@api.onchange('stage_id')
|
|
def onchange_stage_id(self):
|
|
|
|
stage_ids = []
|
|
if self.stage_id:
|
|
for stat in self.board_id.line_ids:
|
|
stage_ids.append(stat.stage_id.id)
|
|
if stage_ids.count(self.stage_id.id) > 2:
|
|
raise ValidationError(_('already choose this stage'))
|
|
|
|
|
|
class NodeState(models.Model):
|
|
_name = 'node.state'
|
|
|
|
@api.depends('name', 'state')
|
|
def name_get(self):
|
|
result = []
|
|
for record in self:
|
|
if self.env.user.lang == 'en_US':
|
|
if record.state:
|
|
name = record.state
|
|
if self.env.user.lang == 'ar_001' or self.env.user.lang == 'ar_EG':
|
|
if record.name:
|
|
name = record.name
|
|
result.append((record.id, name))
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
class StageStage(models.Model):
|
|
_name = 'stage.stage'
|
|
|
|
|
|
@api.depends('name', 'value')
|
|
def name_get(self):
|
|
result = []
|
|
for record in self:
|
|
if self.env.user.lang == 'en_US':
|
|
if record.name:
|
|
name = record.name
|
|
if self.env.user.lang == 'ar_001' or self.env.user.lang == 'ar_EG':
|
|
if record.value:
|
|
name = record.value
|
|
result.append((record.id, name))
|
|
return result
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
class DashboardConfigSettings(models.TransientModel):
|
|
"""Dashboard Theme Settings - Configurable colors from UI"""
|
|
_inherit = 'res.config.settings'
|
|
_description = 'Dashboard Configuration Settings'
|
|
|
|
# Dashboard Theme Colors
|
|
dashboard_primary_color = fields.Char(
|
|
string='Primary Color',
|
|
config_parameter='system_dashboard_classic.primary_color',
|
|
default='#0891b2',
|
|
help='Main accent color for the dashboard (e.g., #0891b2)'
|
|
)
|
|
|
|
dashboard_secondary_color = fields.Char(
|
|
string='Secondary Color',
|
|
config_parameter='system_dashboard_classic.secondary_color',
|
|
default='#1e293b',
|
|
help='Secondary color for headers and dark elements (e.g., #1e293b)'
|
|
)
|
|
|
|
dashboard_success_color = fields.Char(
|
|
string='Success Color',
|
|
config_parameter='system_dashboard_classic.success_color',
|
|
default='#10b981',
|
|
help='Success/Online status color (e.g., #10b981)'
|
|
)
|
|
|
|
dashboard_warning_color = fields.Char(
|
|
string='Warning Color',
|
|
config_parameter='system_dashboard_classic.warning_color',
|
|
default='#f59e0b',
|
|
help='Warning/Remaining balance color (e.g., #f59e0b)'
|
|
)
|
|
|
|
# Dashboard Statistics Visibility Settings
|
|
# NOTE: We DON'T use config_parameter here because Odoo's default behavior
|
|
# converts any non-empty string to True via bool(). Instead, we manually
|
|
# handle storage in get_values/set_values.
|
|
dashboard_show_annual_leave = fields.Boolean(
|
|
string='Show Annual Leave',
|
|
default=True,
|
|
help='Show/Hide Annual Leave statistics card in dashboard header'
|
|
)
|
|
|
|
dashboard_show_salary_slips = fields.Boolean(
|
|
string='Show Salary Slips',
|
|
default=True,
|
|
help='Show/Hide Salary Slips statistics card in dashboard header'
|
|
)
|
|
|
|
dashboard_show_timesheet = fields.Boolean(
|
|
string='Show Weekly Timesheet',
|
|
default=True,
|
|
help='Show/Hide Weekly Timesheet statistics card in dashboard header'
|
|
)
|
|
|
|
dashboard_show_attendance_hours = fields.Boolean(
|
|
string='Show Attendance Hours',
|
|
default=True,
|
|
help='Show/Hide Attendance Hours statistics card in dashboard header'
|
|
)
|
|
|
|
# Attendance Check-in/out Section
|
|
dashboard_show_attendance_section = fields.Boolean(
|
|
string='Show Attendance Section',
|
|
default=True,
|
|
help='Show/Hide the Attendance Check-in/out section in dashboard header. When hidden, statistics cards will expand to fill the space.'
|
|
)
|
|
|
|
# Enable/Disable Check-in/out Button
|
|
dashboard_enable_attendance_button = fields.Boolean(
|
|
string='Enable Check-in/out Button',
|
|
default=False,
|
|
help='Enable/Disable the Check-in/Check-out button functionality. When disabled, the button will appear but will not function.'
|
|
)
|
|
|
|
# Chart Type Selection Fields
|
|
dashboard_annual_leave_chart_type = fields.Selection([
|
|
('donut', 'Donut'),
|
|
('pie', 'Pie'),
|
|
], default='donut', string='Annual Leave Chart',
|
|
config_parameter='system_dashboard_classic.annual_leave_chart_type')
|
|
|
|
dashboard_salary_slips_chart_type = fields.Selection([
|
|
('donut', 'Donut'),
|
|
('pie', 'Pie'),
|
|
], default='donut', string='Salary Slips Chart',
|
|
config_parameter='system_dashboard_classic.salary_slips_chart_type')
|
|
|
|
dashboard_timesheet_chart_type = fields.Selection([
|
|
('donut', 'Donut'),
|
|
('pie', 'Pie'),
|
|
], default='donut', string='Weekly Timesheet Chart',
|
|
config_parameter='system_dashboard_classic.timesheet_chart_type')
|
|
|
|
dashboard_attendance_hours_chart_type = fields.Selection([
|
|
('donut', 'Donut'),
|
|
('pie', 'Pie'),
|
|
], default='donut', string='Attendance Hours Chart',
|
|
config_parameter='system_dashboard_classic.attendance_hours_chart_type')
|
|
|
|
# Periodic Refresh Settings
|
|
dashboard_refresh_enabled = fields.Boolean(
|
|
string='Enable Auto Refresh',
|
|
default=False,
|
|
config_parameter='system_dashboard_classic.refresh_enabled',
|
|
help='Automatically refresh attendance status and approval count at regular intervals'
|
|
)
|
|
|
|
dashboard_refresh_interval = fields.Integer(
|
|
string='Refresh Interval (seconds)',
|
|
default=60,
|
|
config_parameter='system_dashboard_classic.refresh_interval',
|
|
help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds / 1 hour)'
|
|
)
|
|
|
|
# Work Timer Settings
|
|
dashboard_show_work_timer = fields.Boolean(
|
|
string='Show Work Timer',
|
|
default=False,
|
|
help='Display live work hour countdown showing remaining time until end of planned shift. Updates every second and shows progress bar.'
|
|
)
|
|
|
|
|
|
|
|
|
|
@api.model
|
|
def get_values(self):
|
|
"""Override to properly load Boolean visibility settings from ir.config_parameter
|
|
|
|
We store these as explicit strings ('True'/'False') and convert them back to
|
|
Python booleans here.
|
|
"""
|
|
res = super(DashboardConfigSettings, self).get_values()
|
|
|
|
ICPSudo = self.env['ir.config_parameter'].sudo()
|
|
|
|
# Helper to safely get boolean value from string storage
|
|
def get_bool_param(key, default='True'):
|
|
value = ICPSudo.get_param(key, default)
|
|
return value == 'True'
|
|
|
|
res.update(
|
|
dashboard_show_annual_leave=get_bool_param('system_dashboard_classic.show_annual_leave'),
|
|
dashboard_show_salary_slips=get_bool_param('system_dashboard_classic.show_salary_slips'),
|
|
dashboard_show_timesheet=get_bool_param('system_dashboard_classic.show_timesheet'),
|
|
dashboard_show_attendance_hours=get_bool_param('system_dashboard_classic.show_attendance_hours'),
|
|
dashboard_show_attendance_section=get_bool_param('system_dashboard_classic.show_attendance_section'),
|
|
dashboard_enable_attendance_button=get_bool_param('system_dashboard_classic.enable_attendance_button', 'False'),
|
|
dashboard_show_work_timer=get_bool_param('system_dashboard_classic.show_work_timer'),
|
|
)
|
|
return res
|
|
|
|
|
|
def set_values(self):
|
|
"""Override to explicitly store Boolean visibility settings as strings
|
|
|
|
We store as 'True' or 'False' strings to prevent Odoo from deleting
|
|
the parameter when value is False.
|
|
"""
|
|
res = super(DashboardConfigSettings, self).set_values()
|
|
|
|
# Explicitly store visibility settings as string values
|
|
ICPSudo = self.env['ir.config_parameter'].sudo()
|
|
ICPSudo.set_param('system_dashboard_classic.show_annual_leave',
|
|
'True' if self.dashboard_show_annual_leave else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.show_salary_slips',
|
|
'True' if self.dashboard_show_salary_slips else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.show_timesheet',
|
|
'True' if self.dashboard_show_timesheet else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.show_attendance_hours',
|
|
'True' if self.dashboard_show_attendance_hours else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.show_attendance_section',
|
|
'True' if self.dashboard_show_attendance_section else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.enable_attendance_button',
|
|
'True' if self.dashboard_enable_attendance_button else 'False')
|
|
ICPSudo.set_param('system_dashboard_classic.show_work_timer',
|
|
'True' if self.dashboard_show_work_timer else 'False')
|
|
|
|
return res
|
|
|
|
|
|
@api.model
|
|
def get_stats_visibility(self):
|
|
"""API method to get statistics visibility settings for JavaScript
|
|
|
|
Returns dict with boolean values for each visibility setting.
|
|
Default is True (show) if parameter doesn't exist.
|
|
"""
|
|
ICPSudo = self.env['ir.config_parameter'].sudo()
|
|
|
|
# Helper to safely get boolean value
|
|
def get_bool_param(key, default='True'):
|
|
value = ICPSudo.get_param(key, default)
|
|
# Handle various possible values
|
|
if value in ('True', 'true', True, '1', 1):
|
|
return True
|
|
return False
|
|
|
|
return {
|
|
'show_annual_leave': get_bool_param('system_dashboard_classic.show_annual_leave'),
|
|
'show_salary_slips': get_bool_param('system_dashboard_classic.show_salary_slips'),
|
|
'show_timesheet': get_bool_param('system_dashboard_classic.show_timesheet'),
|
|
'show_attendance_hours': get_bool_param('system_dashboard_classic.show_attendance_hours'),
|
|
'show_attendance_section': get_bool_param('system_dashboard_classic.show_attendance_section'),
|
|
}
|
|
|
|
@api.model
|
|
def get_chart_types(self):
|
|
"""API method to get chart type settings for JavaScript
|
|
|
|
Returns dict with chart type for each card.
|
|
Default is 'donut' if parameter doesn't exist.
|
|
"""
|
|
ICPSudo = self.env['ir.config_parameter'].sudo()
|
|
|
|
return {
|
|
'annual_leave_chart': ICPSudo.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'),
|
|
'salary_slips_chart': ICPSudo.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'),
|
|
'timesheet_chart': ICPSudo.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'),
|
|
'attendance_hours_chart': ICPSudo.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'),
|
|
}
|