Files
enterprise-addons/l10n_be_reports/models/annual_statement_custom_handler.py

267 lines
13 KiB
Python

from dateutil.relativedelta import relativedelta
import json
import re
from lxml import etree
from odoo import fields, models, _
from odoo.fields import Domain
from odoo.exceptions import UserError, RedirectWarning
from odoo.tools.misc import get_lang
PREFIX = 'l10n_be_reports.'
REPORT_MATCHINGS = {
((f'{PREFIX}account_financial_report_bs_comp_acon', f'{PREFIX}account_financial_report_pl_comp_a'), f'{PREFIX}xbrl_template_acon'),
((f'{PREFIX}account_financial_report_bs_comp_acap', f'{PREFIX}account_financial_report_pl_comp_a'), f'{PREFIX}xbrl_template_acap'),
((f'{PREFIX}account_financial_report_bs_comp_fcon', f'{PREFIX}account_financial_report_pl_comp_f'), f'{PREFIX}xbrl_template_fcon'),
((f'{PREFIX}account_financial_report_bs_comp_fcap', f'{PREFIX}account_financial_report_pl_comp_f'), f'{PREFIX}xbrl_template_fcap'),
((f'{PREFIX}account_financial_report_bs_asso_a', f'{PREFIX}account_financial_report_pl_asso_a'), f'{PREFIX}xbrl_template_asso_a'),
((f'{PREFIX}account_financial_report_bs_asso_f', f'{PREFIX}account_financial_report_pl_asso_f'), f'{PREFIX}xbrl_template_asso_f'),
}
class AnnualStatementReportHandler(models.AbstractModel):
_name = 'annual.statement.report.handler'
_inherit = ['account.report.custom.handler']
_description = 'Annual Statement Custom Handler'
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
if report.country_id.code == 'BE':
options['buttons'].append({'name': "XBRL", 'sequence': 30, 'action': 'open_xbrl_wizard', 'file_export_type': 'XBRL'})
def open_xbrl_wizard(self, options):
return {
'name': _('XBRL Export'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'l10n_be_reports.xbrl.export.wizard',
'target': 'new',
'context': {
'options': options,
},
'views': [[False, 'form']],
}
def export_to_xbrl(self, options):
return {
'type': 'ir_actions_account_report_download',
'data': {
'model': self.env.context.get('model'),
'options': json.dumps(options),
'file_generator': 'generate_xbrl_file',
}
}
def generate_xbrl_file(self, options):
"""This will generate the XBRL file (similar style to XML)."""
report = self.env['account.report'].browse(options['report_id'])
if report.country_id.code != 'BE':
raise UserError(_("XBRL export is only applicable for Belgium reports. Please change to Belgium annual statement report."))
last_deed_date = options.get('last_deed_date')
current_fiscal_date = self.env.company.compute_fiscalyear_dates(fields.Date.to_date(options['date']['date_to']))
options = report.get_options({
**options,
'date': {
**options.get('date', {}),
'date_from': current_fiscal_date['date_from'],
'date_to': current_fiscal_date['date_to'],
},
})
options['xbrl_current_fiscal_date'] = current_fiscal_date
self._validate_company_data()
# To extract house number: 2 capture groups, one for number, one for street
match = re.findall(r'(\b\d+\w*\b)|((?:\b(?!\d)\w+\b(?:\s+|$))+)', self.env.company.street)
house_number = next((m[0] for m in match if m[0]), None) or '0'
street = next((m[1] for m in match if m[1]), None) or self.env.company.street
data = {
'company_lang': lang if (lang := get_lang(self.env).code.split('_')[0]) in ('fr', 'nl', 'de') else 'en',
'company_registry': self.env.company.company_registry or self.env.company.vat[2:],
'date': fields.Date.context_today(self).strftime('%Y-%m-%d'),
'date_from': current_fiscal_date['date_from'],
'date_to': current_fiscal_date['date_to'],
'company_type': "lgf:" + self.env.company.l10n_be_company_type_id.xbrl_code,
'company_name': self.env.company.name,
'company_street': street.strip(),
'company_house_number': house_number,
'company_postal_code': "pcd:m" + self.env.company.zip,
'company_region': "cct:" + self.env.company.l10n_be_region_id.xbrl_code,
'company_country': "cty:mBE",
'last_deed_date': last_deed_date,
}
xml_id_mapping = report.section_report_ids.get_external_id()
chosen_report_xml_ids = set(xml_id_mapping.values())
chosen_pair = [pair for pair in REPORT_MATCHINGS if chosen_report_xml_ids.issuperset(pair[0])]
if len(chosen_pair) != 1:
raise UserError(_("Please add a single balance sheet and a single profit and loss report that have a matching (Abbr or Full) format for the XBRL export."))
report_xmlids, template_xmlid = chosen_pair[0]
balance_sheet_report = self.env.ref(report_xmlids[0])
chosen_reports = report.section_report_ids.filtered(
lambda r: xml_id_mapping.get(r.id) in report_xmlids
)
for chosen_report in chosen_reports:
report_options = chosen_report.get_options({
'date': {
'date_from': current_fiscal_date['date_from'],
'date_to': current_fiscal_date['date_to'],
},
})
lines = chosen_report._get_lines(report_options)
report_line_codes = {
report_line.id: report_line.code
for report_line in chosen_report.line_ids
}
balance_index = next((i for i, col in enumerate(report_options['columns']) if col['expression_label'] == 'balance'), -1)
if balance_index == -1:
raise UserError(_("The report used for XBRL export should have a 'balance' column."))
for line in lines:
balance_col = line['columns'][balance_index]
line_code = report_line_codes.get(balance_col['report_line_id'])
if line_code == 'BE_BS_499':
if balance_col['no_format'] > 0:
raise UserError(_("The 499 accounts should always be zero before exporting XBRL. Please correct your accounts and try again."))
elif line_code:
data[line_code] = round(balance_col['no_format'], 2)
data.update(self._get_previous_balance_sheet_data(balance_sheet_report, options))
data.update(self._get_disclosure_data(report, options))
data.update(self._get_extra_file_data(options))
report_template = self.env.ref(template_xmlid, raise_if_not_found=False)
xbrl = self.env['ir.qweb']._render(report_template.id, data)
xbrl_element = etree.fromstring(xbrl)
xbrl_file = etree.tostring(xbrl_element, xml_declaration=True, encoding='utf-8')
return {
'file_name': report.get_default_report_filename(options, 'xbrl'),
'file_content': xbrl_file,
'file_type': 'xml',
}
def _get_previous_balance_sheet_data(self, report, options):
""" Gets the balance sheet data of the previous fiscal year. The key is the same as the current period with a 'P' suffix. """
previous_fiscal_date = self.env.company.compute_fiscalyear_dates(
options['xbrl_current_fiscal_date']['date_from'] - relativedelta(days=1)
)
report_options = report.get_options({
'date': {
'date_from': previous_fiscal_date['date_from'],
'date_to': previous_fiscal_date['date_to'],
}
})
previous_period_data = {}
lines = report._get_lines(report_options)
report_line_codes = {
report_line.id: report_line.code
for report_line in report.line_ids
}
balance_index = next((i for i, col in enumerate(report_options['columns']) if col['expression_label'] == 'balance'))
for line in lines:
balance_col = line['columns'][balance_index]
line_code = report_line_codes.get(balance_col['report_line_id'])
if line_code and line_code != 'BE_BS_499':
previous_period_data[f'{line_code}P'] = round(balance_col['no_format'], 2)
return previous_period_data
def _get_disclosure_data(self, report, options):
def get_balance(custom_domain, date_scope='from_beginning'):
domain = report._get_options_domain(options, date_scope) & Domain.AND([
Domain('parent_state', '=', 'posted'),
Domain(custom_domain),
])
[(balance,)] = self.env['account.move.line']._read_group(domain, aggregates=['balance:sum'])
balance = abs(balance) or 0.0
return round(balance, 2)
def get_account_code_domain(code_suffixes):
return Domain.OR([Domain('account_id.code', '=like', account_code) for account_code in code_suffixes])
date_to = fields.Date.to_date(options['date']['date_to'])
one_year_mark = fields.Date.to_string(date_to + relativedelta(years=1))
five_year_mark = fields.Date.to_string(date_to + relativedelta(years=5))
extra_data_specs = [
# Financial fixed assets
# Acquisition value at the end of the period
('BE_EXP_8395', [('account_id.code', 'in', [
'280000', '281000', '281100', '281200', '281700',
'282000', '283000', '283100', '283200', '283700',
'284000', '285000', '285100', '285200', '285700', '288000',
])]),
# Capital gains at the end of the period
('BE_EXP_8455', [('account_id.code', '=like', '28_800')]),
# Amounts written down at the end of the period
('BE_EXP_8525', [('account_id.code', '=like', '28_900')]),
# Uncalled amounts at the end of the period
('BE_EXP_8555', [('account_id.code', 'in', ['280100', '282100', '284100'])]),
# Tangible fixed assets
# Acquisition value at the end of the period
('BE_EXP_8199', get_account_code_domain(['22%0', '23%0', '24%0', '25%0', '26%0', '27%0'])),
# Revaluation surpluses at the end of the period
('BE_EXP_8259', get_account_code_domain(['22%8', '23%8', '24%8', '25%8', '26%8', '27%8'])),
# Depreciations and amounts written down at the end of the period
('BE_EXP_8329', get_account_code_domain(['22%9', '23%9', '24%9', '25%9', '26%9', '27%9'])),
# Amounts payable for more than 1 year
# Amounts payable with a remaining term of more than one but not more than five years
('BE_EXP_8912', [
('account_id.code', '=like', '17%'),
('date_maturity', '>', one_year_mark),
('date_maturity', '<=', five_year_mark),
]),
# Amounts payable with a remaining term of more than five years
('BE_EXP_8913', [
('account_id.code', '=like', '17%'),
('date_maturity', '>', five_year_mark),
]),
# Depreciation written off formation expenses, intangible and tangible fixed assets
# Formation expenses
('BE_EXP_8079', [('account_id.code', '=', '630000')], 'from_fiscalyear'),
# Intangible and tangible fixed assets
('BE_EXP_8279', [('account_id.code', 'in', ['630100', '630800', '630200', '630900'])], 'from_fiscalyear'),
]
disclosure_data = {}
for code, domain, *args in extra_data_specs:
disclosure_data[code] = get_balance(domain, *args)
return disclosure_data
def _get_extra_file_data(self, options):
"""Method to be overridden if some extra data should be passed to the file generator."""
return {}
def _validate_company_data(self):
"""Validates that the company has all required data for generating the XBRL file."""
required_address_fields = ('vat', 'street', 'zip', 'l10n_be_region_id', 'l10n_be_company_type_id')
missing_fields = []
for field in required_address_fields:
if not self.env.company[field]:
missing_fields.append(self.env['res.company']._fields[field].string)
if missing_fields:
field = ', '.join(missing_fields)
action = {
'view_mode': 'form',
'res_model': 'res.company',
'type': 'ir.actions.act_window',
'res_id': self.env.company.id,
'views': [[self.env.ref('base.view_company_form').id, 'form']],
}
raise RedirectWarning(
_("Please fill in the following company data before generating the XBRL file: %s", field),
action,
_("Go to company configuration")
)