267 lines
13 KiB
Python
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")
|
|
)
|