Files
enterprise-addons/esg/models/account_move_line.py
T

216 lines
12 KiB
Python

from collections import defaultdict
from itertools import product
from odoo import api, fields, models
from odoo.fields import Domain
from odoo.tools.sql import column_exists, create_column
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
esg_emission_factor_id = fields.Many2one('esg.emission.factor', string='Emission Factor', compute='_compute_esg_emission_factor_id', inverse='_inverse_esg_emission_factor_id', store=True, readonly=False, index='btree_not_null')
esg_uncertainty_value = fields.Float(string='Uncertainty (%)', related='esg_emission_factor_id.esg_uncertainty_value')
esg_uncertainty_absolute_value = fields.Float(string='Uncertainty (kgCO₂e)', compute='_compute_esg_uncertainty_absolute_value')
esg_emission_multiplicator = fields.Float(compute='_compute_esg_emission_multiplicator', store=True, export_string_translation=False) # Technical field, storing the multiplicator to apply to the gas volumes (important for the report)
esg_emissions_value = fields.Float(string='Emissions (kgCO₂e)', compute='_compute_esg_emissions_value')
def _auto_init(self):
if not column_exists(self.env.cr, "account_move_line", "esg_emission_factor_id"):
# Create the column to avoid computation during installation
create_column(self.env.cr, "account_move_line", "esg_emission_factor_id", "int4")
create_column(self.env.cr, "account_move_line", "esg_emission_multiplicator", "float8")
return super()._auto_init()
@api.depends(
'quantity', 'price_subtotal', 'esg_emission_factor_id.compute_method', 'esg_emission_factor_id.currency_id',
'esg_emission_factor_id.uom_id', 'product_uom_id', 'currency_id', 'move_type', 'account_type',
)
def _compute_esg_emission_multiplicator(self):
for line in self:
if line.account_type not in self.env['account.account'].ESG_VALID_ACCOUNT_TYPES or not line.esg_emission_factor_id:
line.esg_emission_multiplicator = 0
elif line.esg_emission_factor_id.compute_method == 'monetary':
aml_currency = line.currency_id or line.company_id.currency_id
if aml_currency and line.esg_emission_factor_id.currency_id:
line.esg_emission_multiplicator = aml_currency._convert(
from_amount=line.price_subtotal,
to_currency=line.esg_emission_factor_id.currency_id,
date=line.date,
round=False,
)
else:
line.esg_emission_multiplicator = 0
else:
aml_uom = line.product_uom_id or line.esg_emission_factor_id.uom_id
if aml_uom and line.esg_emission_factor_id.uom_id:
line.esg_emission_multiplicator = aml_uom._compute_quantity(
qty=line.quantity,
to_unit=line.esg_emission_factor_id.uom_id,
round=False,
)
else:
line.esg_emission_multiplicator = 0
if line.move_type == 'in_refund':
line.esg_emission_multiplicator *= -1
@api.depends('esg_emission_multiplicator', 'esg_emission_factor_id.esg_emissions_value')
def _compute_esg_emissions_value(self):
for line in self:
if line.account_type not in self.env['account.account'].ESG_VALID_ACCOUNT_TYPES:
line.esg_emissions_value = 0
else:
line.esg_emissions_value = line.esg_emission_factor_id.esg_emissions_value * line.esg_emission_multiplicator
@api.depends('esg_emissions_value', 'esg_uncertainty_value')
def _compute_esg_uncertainty_absolute_value(self):
for line in self:
if line.account_type not in self.env['account.account'].ESG_VALID_ACCOUNT_TYPES:
line.esg_uncertainty_absolute_value = 0
else:
line.esg_uncertainty_absolute_value = line.esg_emissions_value * line.esg_uncertainty_value
@api.depends('product_id', 'account_id', 'partner_id')
def _compute_esg_emission_factor_id(self):
move_lines = self.filtered(lambda aml: aml.account_type in self.env['account.account'].ESG_VALID_ACCOUNT_TYPES)
if move_lines:
move_lines.with_context(auto_generate_esg_assignation_rule=False)._assign_factors_to_move_lines(no_match_reset=True)
def _inverse_esg_emission_factor_id(self):
if not (
self.env.context.get('auto_generate_esg_assignation_rule', True)
and (move_lines_with_emission_factor := self.filtered('esg_emission_factor_id'))
):
return
assignation_vals_list = []
esg_emission_factor_id_assignation_key = {}
# domain to search a rule containing the same combinaison (product_id, partner_id, account_id) linked
# to another emission_factor to remove it since the user breaks it
assignation_rules_to_remove_domain = Domain.FALSE
# domain to search the existing assignation rules to avoid creating the same rule
assignation_rules_to_check_domain = Domain.FALSE
lines_per_group = defaultdict(self.env['account.move.line'].browse)
for line in move_lines_with_emission_factor:
for combinaison in product(*[(v, False) for v in (line.product_id.id, line.partner_id.id, line.account_id.id)]):
lines_per_group[combinaison] |= line
expected_nb_matches_per_group = {
(product_id, partner_id, account_id): 1 + [product_id, partner_id, account_id].count(False)
for product_id in [True, False]
for partner_id in [True, False]
for account_id in [True, False]
if product_id or partner_id or account_id
}
processed_account_move_line_ids = set()
for (product_bool, partner_bool, account_bool), expected_nb_matches in expected_nb_matches_per_group.items():
for (product_id, partner_id, account_id), lines in lines_per_group.items():
if (
product_bool != bool(product_id)
or partner_bool != bool(partner_id)
or account_bool != bool(account_id)
or all(line.id in processed_account_move_line_ids for line in lines)
):
continue
esg_emission_factor = lines.esg_emission_factor_id
combination_domain = Domain([
('product_id', '=', product_id),
('partner_id', '=', partner_id),
('account_id', '=', account_id),
])
if len(esg_emission_factor) > 1:
# break assignation rules (to remove if exists)
assignation_rules_to_remove_domain |= combination_domain
continue
assignation_rules_to_remove_domain |= combination_domain & Domain([
('esg_emission_factor_id', '!=', esg_emission_factor.id),
])
if len(lines) >= expected_nb_matches + 1:
processed_account_move_line_ids.update(lines.ids)
esg_emission_factor_id_assignation_key[product_id, partner_id, account_id] = esg_emission_factor.id
assignation_rules_to_check_domain |= combination_domain
continue
domain = Domain([('esg_emission_factor_id', '!=', False), ('id', 'not in', lines.ids)])
if product_id:
domain &= Domain([('product_id', '=', product_id)])
if partner_id:
domain &= Domain([('partner_id', '=', partner_id)])
if account_id:
domain &= Domain([('account_id', '=', account_id)])
other_move_lines = self.env['account.move.line'].search(domain, limit=10)
nb_matches = len(lines) - 1
for other_line in other_move_lines:
if other_line.esg_emission_factor_id == esg_emission_factor:
nb_matches += 1
if nb_matches >= expected_nb_matches:
processed_account_move_line_ids.update(lines.ids)
esg_emission_factor_id_assignation_key[product_id, partner_id, account_id] = esg_emission_factor.id
assignation_rules_to_check_domain |= combination_domain
break
if not assignation_rules_to_check_domain.is_false():
assignation_rule_read_group = self.env['esg.assignation.line']._read_group(
assignation_rules_to_check_domain,
['product_id', 'partner_id', 'account_id'],
['esg_emission_factor_id:recordset']
)
esg_emission_factor_per_rule = {
(product.id, partner.id, account.id): emission_factors
for product, partner, account, emission_factors in assignation_rule_read_group
}
for (product_id, partner_id, account_id), esg_emission_factor_id in esg_emission_factor_id_assignation_key.items():
esg_emission_factor = esg_emission_factor_per_rule.get((product_id, partner_id, account_id), self.env['esg.emission.factor'])
if not esg_emission_factor:
assignation_vals_list.append({
'product_id': product_id,
'partner_id': partner_id,
'account_id': account_id,
'esg_emission_factor_id': esg_emission_factor_id,
})
assignation_rules_to_remove_domain |= Domain([
('product_id', '=', product_id),
('partner_id', '=', partner_id),
('account_id', '=', account_id),
('esg_emission_factor_id', '!=', esg_emission_factor_id),
])
elif esg_emission_factor.id != esg_emission_factor_id:
# broken rule, should be removed
assignation_rules_to_remove_domain |= Domain([('product_id', '=', product_id), ('partner_id', '=', partner_id), ('account_id', '=', account_id)])
if not assignation_rules_to_remove_domain.is_false():
assignation_rules_to_remove = self.env['esg.assignation.line'].search(assignation_rules_to_remove_domain)
if assignation_rules_to_remove:
assignation_rules_to_remove.unlink()
if assignation_vals_list:
self.env['esg.assignation.line'].create(assignation_vals_list)
def _assign_factors_to_move_lines(self, factors=None, no_match_reset=False):
domain = [
'|', '|',
('product_id', 'in', self.product_id.ids),
('partner_id', 'in', self.partner_id.ids),
('account_id', 'in', self.account_id.ids),
]
if factors:
domain = Domain.AND([domain, [('esg_emission_factor_id', 'in', factors.ids)]])
factor_per_rule = {
(rule.product_id.id, rule.partner_id.id, rule.account_id.id): rule.esg_emission_factor_id.id
for rule in self.env['esg.assignation.line'].search(domain)
}
modified_aml_ids = []
for line in self:
initial_rule = (line.product_id.id, line.partner_id.id, line.account_id.id)
combinations = list(product(*[(v, False) for v in initial_rule]))
assigned = False
for rule in combinations:
if rule in factor_per_rule:
line.esg_emission_factor_id = factor_per_rule[rule]
modified_aml_ids.append(line.id)
assigned = True
break
if not assigned and (no_match_reset or line.esg_emission_factor_id in factors):
line.esg_emission_factor_id = False
modified_aml_ids.append(line.id)
return tuple(modified_aml_ids)