470 lines
19 KiB
Python
470 lines
19 KiB
Python
from collections import defaultdict
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class EsgMetric(models.Model):
|
|
_name = 'esg.metric'
|
|
_description = 'Metric'
|
|
_inherit = [
|
|
'mail.thread',
|
|
]
|
|
|
|
def _get_score_selection(self):
|
|
return [
|
|
('1', 'Negligible'),
|
|
('2', 'Low'),
|
|
('3', 'Moderate'),
|
|
('4', 'High'),
|
|
('5', 'Critical'),
|
|
]
|
|
|
|
def _get_default_dates(self):
|
|
return self.env['res.company'].sudo().ESG_REPORT_DEFAULT_COMPANY.compute_fiscalyear_dates(fields.Date.today())
|
|
|
|
name = fields.Char(required=True, tracking=True)
|
|
state = fields.Selection(
|
|
selection=[
|
|
('draft', 'Draft'),
|
|
('in_progress', 'In Progress'),
|
|
('under_review', 'Under Review'),
|
|
('done', 'Done'),
|
|
],
|
|
default='draft',
|
|
required=True,
|
|
tracking=True,
|
|
)
|
|
status = fields.Selection(
|
|
selection=[
|
|
('actual', 'Actual'),
|
|
('potential', 'Potential'),
|
|
],
|
|
help='Indicates whether the metric reflects a situation that has already occurred (Actual) or one that may occur in the future (Potential).',
|
|
)
|
|
state_color = fields.Integer(compute='_compute_state_color', export_string_translation=False)
|
|
priority = fields.Selection(
|
|
string='Priority',
|
|
selection=[
|
|
('0', 'Low priority'),
|
|
('1', 'Medium priority'),
|
|
('2', 'High priority'),
|
|
('3', 'Urgent'),
|
|
],
|
|
default='0',
|
|
compute='_compute_priority',
|
|
store=True,
|
|
)
|
|
esrs_id = fields.Many2one('esg.esrs', 'ESRS', required=True)
|
|
category = fields.Selection(related='esrs_id.category')
|
|
esrs_section = fields.Char(related='esrs_id.section')
|
|
esrs_code = fields.Char(related='esrs_id.code')
|
|
type = fields.Selection(
|
|
string='Type',
|
|
selection=[
|
|
('positive_impact', 'Positive Impact'),
|
|
('negative_impact', 'Negative Impact'),
|
|
('risk', 'Risk'),
|
|
('opportunity', 'Opportunity'),
|
|
],
|
|
required=True,
|
|
)
|
|
company_id = fields.Many2one('res.company')
|
|
tag_ids = fields.Many2many('esg.tag', string='Tags')
|
|
date_start = fields.Date(
|
|
string='Reporting Date',
|
|
default=lambda self: self._get_default_dates()['date_from'],
|
|
)
|
|
date_end = fields.Date(
|
|
string='Reporting End Date',
|
|
default=lambda self: self._get_default_dates()['date_to'],
|
|
)
|
|
materiality_type = fields.Selection(
|
|
string='Materiality',
|
|
selection=[
|
|
('non_material', 'Non-Material'),
|
|
('financial', 'Financial Material'),
|
|
('impact', 'Impact Material'),
|
|
('double', 'Double Material'),
|
|
],
|
|
compute='_compute_materiality_type',
|
|
store=True,
|
|
)
|
|
materiality_type_color = fields.Integer(compute='_compute_materiality_type_color', export_string_translation=False)
|
|
gap_analysis = fields.Selection(
|
|
string='Gap Analysis',
|
|
selection=[
|
|
('available', 'Available'),
|
|
('missing', 'Missing'),
|
|
],
|
|
help='Indicates if data for this metric/DR is available.\n'
|
|
'Available: data exists and ready to report.\n'
|
|
'Missing: data needs to be collected or actions taken to make it available.'
|
|
)
|
|
justification = fields.Text(
|
|
string='Justification',
|
|
help='For topics marked as immaterial, a clear explanation is required to support transparency in the CSRD report. '
|
|
'For material topics, providing a justification is recommended as it adds valuable context.',
|
|
)
|
|
notes = fields.Html()
|
|
attachment_ids = fields.One2many(
|
|
'ir.attachment',
|
|
compute='_compute_attachment_ids',
|
|
string='Documents (PDF)',
|
|
readonly=False,
|
|
)
|
|
|
|
# Score fields
|
|
impact_severity_score = fields.Selection(
|
|
string='Impact Severity',
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_impact_severity_score',
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
help='How significant this metric is for people or the environment?',
|
|
)
|
|
display_impact_severity_score = fields.Selection(
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_display_impact_severity_score',
|
|
export_string_translation=False,
|
|
help='How significant this metric is for people or the environment?',
|
|
)
|
|
scale_value = fields.Selection(
|
|
string='Scale',
|
|
selection=lambda self: self._get_score_selection(),
|
|
help='How critical are the impacts caused by this activity on people or environment?',
|
|
)
|
|
scope_value = fields.Selection(
|
|
string='Scope',
|
|
selection=lambda self: self._get_score_selection(),
|
|
help='How widespread are the impacts caused by this activity?',
|
|
)
|
|
remediability_value = fields.Selection(
|
|
string='Remediability',
|
|
selection=lambda self: self._get_score_selection(),
|
|
help='How easy is to fix or reverse the impacts once it occurs?',
|
|
)
|
|
financial_severity_score = fields.Selection(
|
|
string='Financial Severity',
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_financial_severity_score',
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
help='How significant this metric is for the company\'s finances?',
|
|
)
|
|
display_financial_severity_score = fields.Selection(
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_display_financial_severity_score',
|
|
export_string_translation=False,
|
|
help='How significant this metric is for the company\'s finances?',
|
|
)
|
|
financial_impacts_value = fields.Selection(
|
|
string='Financial Impacts',
|
|
selection=lambda self: self._get_score_selection(),
|
|
help='If these impacts occur, how large could the financial effects be?',
|
|
)
|
|
likelihood_value = fields.Selection(
|
|
string='Likelihood',
|
|
selection=[
|
|
('1', 'Very Unlikely'),
|
|
('2', 'Unlikely'),
|
|
('3', 'Possible'),
|
|
('4', 'Likely'),
|
|
('5', 'Almost Certain'),
|
|
],
|
|
help='How likely is this financial effect to occur?',
|
|
)
|
|
stakeholder_impact_severity_score = fields.Selection(
|
|
string='Stakeholder Impact Severity Score',
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_stakeholder_impact_severity_score',
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
help='Average impact score given by stakeholders for this metric.',
|
|
)
|
|
stakeholder_financial_severity_score = fields.Selection(
|
|
string='Stakeholder Financial Severity Score',
|
|
selection=lambda self: self._get_score_selection(),
|
|
compute='_compute_stakeholder_financial_severity_score',
|
|
readonly=False,
|
|
store=True,
|
|
tracking=True,
|
|
help='Average financial score given by stakeholders for this metric.',
|
|
)
|
|
is_stakeholder_impact_score_manually_edited = fields.Boolean(export_string_translation=False)
|
|
is_stakeholder_financial_score_manually_edited = fields.Boolean(export_string_translation=False)
|
|
|
|
# Survey fields
|
|
survey_ids = fields.Many2many('survey.survey', help='Sent out Surveys')
|
|
survey_answer_count = fields.Integer(compute='_compute_survey_answer_statistic')
|
|
survey_answer_done_count = fields.Integer(compute='_compute_survey_answer_statistic')
|
|
|
|
@api.depends('state_color')
|
|
def _compute_state_color(self):
|
|
state_color = {
|
|
'draft': 4, # light blue
|
|
'under_review': 2, # orange
|
|
'done': 10, # green / success
|
|
False: 0, # default grey -- for studio
|
|
'in_progress': 0, # grey
|
|
}
|
|
for metric in self:
|
|
metric.state_color = state_color[metric.state]
|
|
|
|
@api.depends('type', 'status')
|
|
def _compute_impact_severity_score(self):
|
|
for metric in self:
|
|
if not metric.impact_severity_score:
|
|
continue
|
|
metric.impact_severity_score = metric._get_impact_severity_score()
|
|
|
|
@api.depends('scale_value', 'scope_value', 'remediability_value', 'likelihood_value', 'type', 'status')
|
|
def _compute_display_impact_severity_score(self):
|
|
for metric in self:
|
|
metric.display_impact_severity_score = metric._get_impact_severity_score()
|
|
|
|
@api.depends('status')
|
|
def _compute_financial_severity_score(self):
|
|
for metric in self:
|
|
if not metric.financial_severity_score:
|
|
continue
|
|
metric.financial_severity_score = metric._get_financial_severity_score()
|
|
|
|
@api.depends('financial_impacts_value', 'likelihood_value', 'status')
|
|
def _compute_display_financial_severity_score(self):
|
|
for metric in self:
|
|
metric.display_financial_severity_score = metric._get_financial_severity_score()
|
|
|
|
@api.depends('impact_severity_score', 'financial_severity_score')
|
|
def _compute_materiality_type(self):
|
|
non_negligable_scores = ['3', '4', '5']
|
|
for metric in self:
|
|
is_high_financial = metric.financial_severity_score in non_negligable_scores
|
|
if metric.impact_severity_score in non_negligable_scores:
|
|
metric.materiality_type = 'double' if is_high_financial else 'impact'
|
|
else:
|
|
metric.materiality_type = 'financial' if is_high_financial else 'non_material'
|
|
|
|
@api.depends('materiality_type')
|
|
def _compute_materiality_type_color(self):
|
|
materiality_type_color = {
|
|
'impact': 2, # orange
|
|
'financial': 2, # orange
|
|
'double': 1, # red / danger
|
|
'non_material': 0,
|
|
False: 0, # default grey -- for studio
|
|
}
|
|
for metric in self:
|
|
metric.materiality_type_color = materiality_type_color[metric.materiality_type]
|
|
|
|
@api.depends('materiality_type', 'gap_analysis')
|
|
def _compute_priority(self):
|
|
for metric in self:
|
|
if metric.materiality_type == 'non_material':
|
|
metric.priority = '0'
|
|
continue
|
|
is_double_material = metric.materiality_type == 'double'
|
|
if metric.gap_analysis == 'missing':
|
|
metric.priority = '3' if is_double_material else '2'
|
|
elif is_double_material:
|
|
metric.priority = '2'
|
|
else:
|
|
metric.priority = '1'
|
|
|
|
def _compute_attachment_ids(self):
|
|
for metric in self:
|
|
attachment_ids = self.env['ir.attachment'].search([('res_id', '=', metric.id), ('res_model', '=', 'esg.metric')]).ids
|
|
message_attachment_ids = metric.mapped('message_ids.attachment_ids').ids # from mail_thread
|
|
metric.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))]
|
|
|
|
@api.depends('survey_ids.user_input_ids.state', 'survey_ids.user_input_ids.user_input_line_ids')
|
|
def _compute_stakeholder_impact_severity_score(self):
|
|
score_per_metric = {}
|
|
for metric, questions in self.env['esg.metric.to.survey.question']._read_group(
|
|
domain=[('metric_id', 'in', self.ids), ('metric_score_type', '=', 'impact')],
|
|
groupby=['metric_id'],
|
|
aggregates=['question_id:recordset'],
|
|
):
|
|
result = self.env['survey.user_input.line']._read_group(
|
|
domain=[('user_input_id.state', '=', 'done'), ('question_id', 'in', questions.ids)],
|
|
aggregates=['answer_score:avg'],
|
|
)
|
|
if result:
|
|
score_per_metric[metric] = result[0][0]
|
|
|
|
for metric in self:
|
|
score = score_per_metric.get(metric, 0.0)
|
|
if score:
|
|
if score <= 1:
|
|
metric.stakeholder_impact_severity_score = '1'
|
|
elif score <= 2:
|
|
metric.stakeholder_impact_severity_score = '2'
|
|
elif score <= 3:
|
|
metric.stakeholder_impact_severity_score = '3'
|
|
elif score <= 4:
|
|
metric.stakeholder_impact_severity_score = '4'
|
|
else:
|
|
metric.stakeholder_impact_severity_score = '5'
|
|
else:
|
|
metric.stakeholder_impact_severity_score = False
|
|
|
|
@api.depends('survey_ids.user_input_ids.state', 'survey_ids.user_input_ids.user_input_line_ids')
|
|
def _compute_stakeholder_financial_severity_score(self):
|
|
score_per_metric = {}
|
|
for metric, questions in self.env['esg.metric.to.survey.question']._read_group(
|
|
domain=[('metric_id', 'in', self.ids), ('metric_score_type', '=', 'financial')],
|
|
groupby=['metric_id'],
|
|
aggregates=[('question_id:recordset')],
|
|
):
|
|
result = self.env['survey.user_input.line']._read_group(
|
|
domain=[('user_input_id.state', '=', 'done'), ('question_id', 'in', questions.ids)],
|
|
aggregates=['answer_score:avg'],
|
|
)
|
|
if result:
|
|
score_per_metric[metric] = result[0][0]
|
|
|
|
for metric in self:
|
|
score = score_per_metric.get(metric, 0.0)
|
|
if score:
|
|
if score <= 1:
|
|
metric.stakeholder_financial_severity_score = '1'
|
|
elif score <= 2:
|
|
metric.stakeholder_financial_severity_score = '2'
|
|
elif score <= 3:
|
|
metric.stakeholder_financial_severity_score = '3'
|
|
elif score <= 4:
|
|
metric.stakeholder_financial_severity_score = '4'
|
|
else:
|
|
metric.stakeholder_financial_severity_score = '5'
|
|
else:
|
|
metric.stakeholder_financial_severity_score = False
|
|
|
|
@api.depends('survey_ids')
|
|
def _compute_survey_answer_statistic(self):
|
|
user_input_read_group = self.env['survey.user_input']._read_group(
|
|
[('survey_id', 'in', self.survey_ids.ids)],
|
|
['survey_id', 'state'],
|
|
['__count'],
|
|
)
|
|
stat_per_survey = defaultdict(lambda: {'answer_done_count': 0, 'answer_count': 0})
|
|
for survey, state, count in user_input_read_group:
|
|
stat_per_survey[survey]['answer_count'] += count
|
|
if state == 'done':
|
|
stat_per_survey[survey]['answer_done_count'] += count
|
|
for metric in self:
|
|
answer_count = answer_done_count = 0
|
|
for survey in metric.survey_ids:
|
|
answer_count += stat_per_survey[survey]['answer_count']
|
|
answer_done_count += stat_per_survey[survey]['answer_done_count']
|
|
metric.survey_answer_count = answer_count
|
|
metric.survey_answer_done_count = answer_done_count
|
|
|
|
def _get_impact_severity_score(self):
|
|
self.ensure_one()
|
|
scores = [int(self.scale_value), int(self.scope_value)]
|
|
if self.type not in ['positive_impact', 'opportunity']:
|
|
scores.append(int(self.remediability_value))
|
|
if self.status != 'actual':
|
|
scores.append(int(self.likelihood_value))
|
|
score = sum(scores) / len(scores)
|
|
if 0 <= score < 1:
|
|
return '1'
|
|
elif 1 <= score < 2:
|
|
return '2'
|
|
elif 2 <= score < 3:
|
|
return '3'
|
|
elif 3 <= score < 4:
|
|
return '4'
|
|
else:
|
|
return '5'
|
|
|
|
def _get_financial_severity_score(self):
|
|
scores = [int(self.financial_impacts_value)]
|
|
if self.status != 'actual':
|
|
scores.append(int(self.likelihood_value))
|
|
score = sum(scores) / len(scores)
|
|
if 0 <= score < 1:
|
|
return '1'
|
|
elif 1 <= score < 2:
|
|
return '2'
|
|
elif 2 <= score < 3:
|
|
return '3'
|
|
elif 3 <= score < 4:
|
|
return '4'
|
|
else:
|
|
return '5'
|
|
|
|
def action_open_impact_severity_score(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': self.env._('%(name)s Impact Severity Assessement', name=self.name),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'esg.metric',
|
|
'target': 'new',
|
|
'views': [(self.env.ref('esg_csrd.esg_metric_impact_severity_score_form_view').id, 'form')],
|
|
'res_id': self.id,
|
|
}
|
|
|
|
def action_save_impact_scores(self):
|
|
self.ensure_one()
|
|
self.impact_severity_score = self.display_impact_severity_score
|
|
|
|
def action_open_financial_severity_score(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': self.env._('%(name)s Financial Severity Assessement', name=self.name),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'esg.metric',
|
|
'target': 'new',
|
|
'views': [(self.env.ref('esg_csrd.esg_metric_financial_severity_score_form_view').id, 'form')],
|
|
'res_id': self.id,
|
|
}
|
|
|
|
def action_save_financial_scores(self):
|
|
self.ensure_one()
|
|
self.financial_severity_score = self.display_financial_severity_score
|
|
|
|
def action_ask_stakeholder_review(self):
|
|
if not (self.env.user.has_group('survey.group_survey_user') and self.env.user.has_group('esg.esg_group_manager')):
|
|
raise UserError(self.env._('You need ESG manager and Survey user access rights in order to access this action.'))
|
|
metrics = self
|
|
if not metrics:
|
|
fiscal_year_dates = self.env['res.company'].sudo().ESG_REPORT_DEFAULT_COMPANY.compute_fiscalyear_dates(fields.Date.today())
|
|
metrics = self.search([('date_start', '<=', fiscal_year_dates['date_to']), ('date_end', '>=', fiscal_year_dates['date_from'])])
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'esg.metric.ask.feedback',
|
|
'target': 'new',
|
|
'name': self.env._('Stakeholder Review'),
|
|
'context': {
|
|
'default_metric_ids': metrics.ids,
|
|
}
|
|
}
|
|
|
|
def action_open_metric_survey_input_lines(self):
|
|
self.ensure_one()
|
|
if not (self.env.user.has_group('survey.group_survey_user') and self.env.user.has_group('esg.esg_group_manager')):
|
|
raise UserError(self.env._('You need ESG manager and Survey user access rights in order to access this action.'))
|
|
view_id = self.env.ref('esg_csrd.esg_survey_user_input_view_list', raise_if_not_found=False)
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'survey.user_input',
|
|
'name': self.env._('Stakeholder Reviews'),
|
|
'views': [[view_id.id, 'list'], [False, 'form']],
|
|
'domain': [('survey_id', 'in', self.survey_ids.ids)],
|
|
}
|
|
|
|
def action_apply_stakeholder_impact_score(self):
|
|
self.ensure_one()
|
|
self._compute_stakeholder_impact_severity_score()
|
|
self.is_stakeholder_impact_score_manually_edited = False
|
|
|
|
def action_apply_stakeholder_financial_score(self):
|
|
self.ensure_one()
|
|
self._compute_stakeholder_financial_severity_score()
|
|
self.is_stakeholder_financial_score_manually_edited = False
|