272 lines
14 KiB
Python
272 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import re
|
|
from odoo import fields, models, _
|
|
from odoo.tools.float_utils import float_compare, float_repr
|
|
from odoo.tools.mail import html2plaintext
|
|
from odoo.exceptions import UserError
|
|
from datetime import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
class L10n_LuReportHandler(models.AbstractModel):
|
|
_name = 'l10n_lu.report.handler'
|
|
_inherit = ['account.report.custom.handler']
|
|
_description = 'Luxembourgish Financial Report Custom Handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
|
options.setdefault('buttons', []).append(
|
|
{'name': _('XML'), 'sequence': 30, 'action': 'open_report_export_wizard_accounts_report', 'branch_allowed': True}
|
|
)
|
|
|
|
def get_report_filename(self, options):
|
|
# we can't determine milliseconds using fields.Datetime, hence used python's `datetime`
|
|
company = self.env.company
|
|
agent = company.account_representative_id
|
|
now_datetime = datetime.now()
|
|
file_ref_data = {
|
|
'ecdf_prefix': agent and agent.l10n_lu_agent_ecdf_prefix or company.ecdf_prefix,
|
|
'datetime': now_datetime.strftime('%Y%m%dT%H%M%S%f')[:-4]
|
|
}
|
|
filename = '{ecdf_prefix}X{datetime}'.format(**file_ref_data)
|
|
# `FileReference` element of exported XML must have same `filename` as above. So, we pass it from
|
|
# here and get it from options in `_get_lu_electronic_report_values` and pass it further to template.
|
|
if options:
|
|
options['filename'] = filename
|
|
return filename
|
|
|
|
def get_electronic_report_values(self, options):
|
|
company = self.env.company
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
vat = report.get_vat_for_export(options, raise_warning=False)
|
|
if vat and vat.startswith("LU"): # Remove LU prefix in the XML
|
|
vat = vat[2:]
|
|
return {
|
|
'filename': options.get('filename'),
|
|
'lang': 'EN',
|
|
'interface' : 'MODL5',
|
|
'vat_number' : vat or "NE",
|
|
'matr_number' : company.matr_number or "NE",
|
|
'rcs_number' : company.company_registry or "NE",
|
|
}
|
|
|
|
def _validate_ecdf_prefix(self):
|
|
ecdf_prefix = self.env.company.ecdf_prefix
|
|
if not ecdf_prefix:
|
|
raise UserError(_('Please set valid eCDF Prefix for your company.'))
|
|
re_valid_prefix = re.compile(r'[0-9|A-Z]{6}$')
|
|
if not re_valid_prefix.match(ecdf_prefix):
|
|
msg = _('eCDF Prefix `{0}` associated with `{1}` company is invalid.\nThe expected format is ABCD12 (Six digits of numbers or capital letters)')
|
|
raise UserError(msg.format(ecdf_prefix, self.env.company.display_name))
|
|
return True
|
|
|
|
def get_financial_reports(self):
|
|
return {
|
|
self.env.ref("l10n_lu_reports.account_financial_report_l10n_lu_bs").id : 'CA_BILAN',
|
|
self.env.ref("l10n_lu_reports.account_financial_report_l10n_lu_bs_abr").id : 'CA_BILANABR',
|
|
self.env.ref("l10n_lu_reports.account_financial_report_l10n_lu_pl").id : 'CA_COMPP',
|
|
self.env.ref("l10n_lu_reports.account_financial_report_l10n_lu_pl_abr").id : 'CA_COMPPABR'
|
|
}
|
|
|
|
def get_financial_electronic_report_values(self, options):
|
|
|
|
def _format_amount(amount):
|
|
return float_repr(amount, 2).replace('.', ',') if amount else '0,00'
|
|
|
|
values = {}
|
|
|
|
def _report_useful_fields(amount, field, parent_field, required):
|
|
"""Only reports fields containing values or that are required."""
|
|
# All required fields are always reported; all others reported only if different from 0.00.
|
|
if float_compare(amount, 0.0, 2) != 0 or required:
|
|
values.update({field: {'value': _format_amount(amount), 'field_type': 'number'}})
|
|
# The parent needs to be added even if at 0, if some child lines are filled in
|
|
if parent_field and not values.get(parent_field):
|
|
values.update({parent_field: {'value': '0,00', 'field_type': 'number'}})
|
|
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
lu_template_values = self.get_electronic_report_values(options)
|
|
|
|
# Add comparison filter to get data from last year
|
|
report_options = report.get_options({**options, 'comparison': {
|
|
'filter': 'same_last_year',
|
|
'number_period': 1,
|
|
}})
|
|
if options['date'].get('filter') == 'custom' and options['date'].get('mode') == 'range':
|
|
report_options['date']['date_from'] = options['date'].get('date_from')
|
|
lines = report._get_lines(report_options)
|
|
|
|
report_line = self.env['account.report.line']
|
|
date_from = fields.Date.from_string(report_options['date'].get('date_from'))
|
|
date_to = fields.Date.from_string(report_options['date'].get('date_to'))
|
|
values.update({
|
|
'01': {'value': date_from.strftime("%d/%m/%Y"), 'field_type': 'char'},
|
|
'02': {'value': date_to.strftime("%d/%m/%Y"), 'field_type': 'char'},
|
|
'03': {'value': self.env.company.currency_id.name, 'field_type': 'char'}
|
|
})
|
|
|
|
# we only need `account.report.line` records' IDs, so we need to check the model
|
|
# as some of these could be `account.account` records
|
|
for line in lines:
|
|
model, res_id = self.env['account.report']._get_model_info_from_id(line['id'])
|
|
if model != 'account.report.line':
|
|
continue
|
|
|
|
# financial report's `code` would contain alpha-numeric string like `LU_BS_XXX/LU_BSABR_XXX`
|
|
# where characters at last three positions will be digits, hence we split with `_`
|
|
# and build dictionary having `code` as dictionary key
|
|
split_line_code = (report_line.browse(res_id).code or '').split('_') or []
|
|
columns = line['columns']
|
|
# since we have enabled comparison by default, `columns` element will atleast have two dictionary items.
|
|
# First dict will be holding current year's balance and second one will be holding previous year's balance.
|
|
if len(split_line_code) > 2:
|
|
parent_code = None
|
|
parent_id = report_line.browse(res_id).parent_id
|
|
if parent_id and parent_id.code:
|
|
parent_split_code = parent_id.code.split('_')
|
|
if len(parent_split_code) > 2:
|
|
parent_code = parent_split_code[2]
|
|
|
|
required = line['level'] == 0 or split_line_code[2] in ['201', '202', '405', '406']
|
|
# current year balance
|
|
_report_useful_fields(columns[0]['no_format'], split_line_code[2], parent_code, required)
|
|
# previous year balance
|
|
_report_useful_fields(columns[1]['no_format'], str(int(split_line_code[2]) + 1), parent_code and str(int(parent_code) + 1), required)
|
|
|
|
lu_template_values.update({
|
|
'forms': [{
|
|
'declaration_type': self.get_financial_reports()[report.id],
|
|
'year': date_from.year,
|
|
'period': "1",
|
|
'field_values': values
|
|
}]
|
|
})
|
|
return lu_template_values
|
|
|
|
def export_to_xml(self, options):
|
|
self._validate_ecdf_prefix()
|
|
|
|
lu_template_values = self.get_financial_electronic_report_values(options)
|
|
|
|
rendered_content = self.env['ir.qweb']._render('l10n_lu_reports.l10n_lu_electronic_report_template_2_0', lu_template_values)
|
|
content = "\n".join(re.split(r'\n\s*\n', rendered_content))
|
|
|
|
return {
|
|
'file_name': self.env['account.report'].browse(options['report_id']).get_report_filename(options) + '.xml',
|
|
'file_content': "<?xml version='1.0' encoding='UTF-8'?>" + content,
|
|
'file_type': 'xml',
|
|
}
|
|
|
|
def get_xml_2_0_report_values(self, options, references=False):
|
|
"""Returns the formatted report values for this financial report.
|
|
(Balance sheet: https://ecdf-developer.b2g.etat.lu/ecdf/forms/popup/CA_BILAN_COMP/2020/en/2/preview),
|
|
Profit&Loss: https://ecdf-developer.b2g.etat.lu/ecdf/forms/popup/CA_COMPP_COMP/2020/en/2/preview)
|
|
Adds the possibility to add references to the report and the form model number to
|
|
get_electronic_report_values.
|
|
|
|
:param options: the report options
|
|
:param references: whether the annotations on the financial report should be added to the report as references
|
|
:returns: the formatted report values
|
|
"""
|
|
def _get_references(report):
|
|
"""
|
|
This returns the annotations on all financial reports, linked to the corresponding report reference field.
|
|
These will be used as references in the report.
|
|
"""
|
|
annotations = self.env['account.report.annotation'].search([
|
|
('message_id.model', '=', 'account.account'),
|
|
])
|
|
if not annotations:
|
|
return {}, {}
|
|
|
|
annotations_by_account = {a.message_id.res_id: a for a in annotations if a.message_id.res_id}
|
|
annotated_account_ids = set(annotations_by_account)
|
|
|
|
# children without prefix grouping collapsing them
|
|
print_options = report.get_options({**options, 'unfold_all': True})
|
|
lines = report._get_lines(print_options)
|
|
|
|
# Build map: account_id -> parent account.report.line id
|
|
account_to_report_line_id = {}
|
|
for line in lines:
|
|
parsed = report._parse_line_id(line['id'])
|
|
report_line_id = account_id = None
|
|
for _markup, model, value in parsed:
|
|
if model == 'account.report.line':
|
|
report_line_id = value
|
|
elif model == 'account.account' and value in annotated_account_ids:
|
|
account_id = value
|
|
if report_line_id and account_id:
|
|
account_to_report_line_id[account_id] = report_line_id
|
|
|
|
if not account_to_report_line_id:
|
|
return {}, {}
|
|
|
|
report_lines = {l.id: l for l in self.env['account.report.line'].browse(account_to_report_line_id.values())}
|
|
accounts = {a.id: a for a in self.env['account.account'].browse(account_to_report_line_id.keys())}
|
|
|
|
references, names = {}, {}
|
|
digit_pattern = re.compile(r'\d+')
|
|
for account_id, report_line_id in account_to_report_line_id.items():
|
|
annotation = annotations_by_account.get(account_id)
|
|
line = report_lines.get(report_line_id)
|
|
if not annotation or not line:
|
|
continue
|
|
if match := digit_pattern.search(str(line.code)):
|
|
# References in the eCDF report have codes equal to the report code of the referred account + 1000
|
|
code = str(int(match.group()) + 1000)
|
|
references[code] = {'value': html2plaintext(annotation.message_id.body), 'field_type': 'char'}
|
|
if account := accounts.get(account_id):
|
|
names[code] = account.code
|
|
return references, names
|
|
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
if not self.env.context.get('skip_options_recompute'):
|
|
options = report.get_options(options)
|
|
lu_template_values = self.get_financial_electronic_report_values(options)
|
|
for form in lu_template_values['forms']:
|
|
if references:
|
|
references, names = _get_references(report)
|
|
# Only add those references on accounts with reported values (for the current or previous year);
|
|
# the reference has an eCDF code equal to the report code of the referred account for the current year + 1000,
|
|
# to equal to the report code of the ref. account for the previous year + 999
|
|
references = {r: references[r] for r in references
|
|
if str(int(r) - 1000) in form['field_values'] or str(int(r) - 999) in form['field_values']}
|
|
names = {r: names[r] for r in references
|
|
if str(int(r) - 1000) in form['field_values'] or str(int(r) - 999) in form['field_values']}
|
|
# Check the length of the references <= 10 (XML report limit)
|
|
if any([len(r['value']) > 10 for r in references.values()]):
|
|
raise UserError(
|
|
_(
|
|
"Some references are not in the requested format (max. 10 characters):\n %(references)s\nCannot export them.",
|
|
references="\n ".join(
|
|
[
|
|
f"{names[i[0]]}: {i[1]['value']}"
|
|
for i in references.items()
|
|
if len(i[1]["value"]) > 10
|
|
],
|
|
),
|
|
),
|
|
)
|
|
for ref in references:
|
|
form['field_values'].update({ref: references[ref]})
|
|
model = 2 if form['year'] == 2020 else 1
|
|
form['model'] = model
|
|
return lu_template_values['forms']
|
|
|
|
def open_report_export_wizard_accounts_report(self, options):
|
|
""" Creates a new export wizard for this report."""
|
|
new_context = self.env.context.copy()
|
|
new_context['report_generation_options'] = options
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Export'),
|
|
'view_mode': 'form',
|
|
'res_model': 'l10n_lu.generate.accounts.report',
|
|
'target': 'new',
|
|
'views': [[self.env.ref('l10n_lu_reports.view_l10n_lu_generate_accounts_report').id, 'form']],
|
|
'context': new_context,
|
|
}
|