242 lines
11 KiB
Python
242 lines
11 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import re
|
|
|
|
from collections import defaultdict
|
|
from lxml import etree
|
|
from stdnum.nl.btw import compact
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import RedirectWarning
|
|
from odoo.tools import cleanup_xml_node
|
|
|
|
|
|
class L10n_Nl_ReportsEcSalesReportHandler(models.AbstractModel):
|
|
_name = 'l10n_nl_reports.ec.sales.report.handler'
|
|
_inherit = ['account.ec.sales.report.handler']
|
|
_description = 'Dutch EC Sales Report Custom Handler'
|
|
|
|
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
|
lines = []
|
|
|
|
totals_by_column_group = {
|
|
key: {
|
|
column['expression_label']: 0.0 if column['figure_type'] == 'monetary' else ''
|
|
for column in options['columns']
|
|
} for key in options['column_groups']
|
|
}
|
|
|
|
for partner, results in self._query_partners(report, options):
|
|
partner_values = defaultdict(dict)
|
|
|
|
for column_group_key in options['column_groups']:
|
|
partner_sum = results.get(column_group_key, {})
|
|
country_code = partner_sum.get('country_code', 'UNKNOWN')
|
|
|
|
# For Greece, the ISO 3166 code (GR) and European Union code (EL) is not the same.
|
|
# Since this is a european report, we need the European Union code.
|
|
if country_code == 'GR':
|
|
country_code = 'EL'
|
|
|
|
line_total = partner_sum.get('goods', 0.0) + partner_sum.get('services', 0.0) + partner_sum.get('triangular', 0.0)
|
|
|
|
partner_values[column_group_key].update({
|
|
'country_code': country_code,
|
|
'partner_name': '',
|
|
'vat': self._format_vat(partner_sum.get('full_vat_number'), country_code),
|
|
'amount_product': partner_sum.get('goods', 0.0),
|
|
'amount_service': partner_sum.get('services', 0.0),
|
|
'amount_triangular': partner_sum.get('triangular', 0.0),
|
|
'total': line_total,
|
|
})
|
|
|
|
totals_by_column_group[column_group_key]['amount_product'] += partner_sum.get('goods', 0.0)
|
|
totals_by_column_group[column_group_key]['amount_service'] += partner_sum.get('services', 0.0)
|
|
totals_by_column_group[column_group_key]['amount_triangular'] += partner_sum.get('triangular', 0.0)
|
|
totals_by_column_group[column_group_key]['total'] += line_total
|
|
|
|
lines.append((0, self._get_report_line_partner(report, options, partner, partner_values)))
|
|
|
|
lines.append((0, self._get_report_line_total(report, options, totals_by_column_group)))
|
|
return lines
|
|
|
|
def _caret_options_initializer(self):
|
|
"""
|
|
Add custom caret option for the report to link to the partner and allow cleaner overrides.
|
|
"""
|
|
return {
|
|
'nl_icp_partner': [
|
|
{'name': _("View Partner"), 'action': 'caret_option_open_record_form'},
|
|
],
|
|
}
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options)
|
|
options['buttons'].append({'name': "XBRL", 'sequence': 40, 'action': 'open_xbrl_wizard', 'file_export_type': 'XBRL'})
|
|
options.get('sales_report_taxes', {}).update(self._get_tax_tags_for_nl_sales_report())
|
|
|
|
def _get_tax_tags_for_nl_sales_report(self):
|
|
goods_tag = self.env.ref('l10n_nl.tax_report_rub_3bg_tag', raise_if_not_found=False)
|
|
services_tag = self.env.ref('l10n_nl.tax_report_rub_3bs_tag', raise_if_not_found=False)
|
|
triangular_tag = self.env.ref('l10n_nl.tax_report_rub_3bt_tag', raise_if_not_found=False)
|
|
if goods_tag and services_tag and triangular_tag:
|
|
return {
|
|
'goods': goods_tag._get_matching_tags().ids,
|
|
'services': services_tag._get_matching_tags().ids,
|
|
'triangular': triangular_tag._get_matching_tags().ids,
|
|
'use_taxes_instead_of_tags': False,
|
|
}
|
|
goods_tax = self.env['account.chart.template'].ref('btw_X0_producten', raise_if_not_found=False)
|
|
services_tax = self.env['account.chart.template'].ref('btw_X0_diensten', raise_if_not_found=False)
|
|
triangular_tax = self.env['account.chart.template'].ref('btw_X0_ABC_levering', raise_if_not_found=False)
|
|
return {
|
|
'goods': [goods_tax.id] if goods_tax else [],
|
|
'services': [services_tax.id] if services_tax else [],
|
|
'triangular': [triangular_tax.id] if triangular_tax else [],
|
|
'use_taxes_instead_of_tags': True,
|
|
}
|
|
|
|
@api.model
|
|
def _format_vat(self, vat, country_code):
|
|
""" VAT numbers must be reported without country code, and grouped by 4
|
|
characters, with a space between each pair of groups.
|
|
"""
|
|
if vat:
|
|
if vat[:2].lower() == country_code.lower():
|
|
vat = vat[2:]
|
|
return vat
|
|
return None
|
|
|
|
def open_xbrl_wizard(self, options):
|
|
res = self.env['l10n_nl_reports.tax.report.handler'].open_xbrl_wizard(options)
|
|
res.update({
|
|
'name': _('EC Sales (ICP) SBR'),
|
|
'res_model': 'l10n_nl_reports.sbr.icp.wizard',
|
|
})
|
|
for ec_tax_filter in res['context']['options']['ec_tax_filter_selection']:
|
|
ec_tax_filter['selected'] = True
|
|
return res
|
|
|
|
def export_icp_report_to_xbrl(self, options):
|
|
# This will generate the XBRL file (similar style to XML).
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
lines = report._get_lines(options)
|
|
data = self._generate_codes_values(report, lines, options)
|
|
|
|
date_to = fields.Date.to_date(options['date']['date_to'])
|
|
template_xmlid = 'l10n_nl_reports.icp_report_sbr'
|
|
if date_to.year == 2024:
|
|
# We still need to support the NT18 taxonomy for 2024 until that declaration period is over.
|
|
template_xmlid = 'l10n_nl_reports.icp_report_sbr_nt18'
|
|
elif date_to.year == 2025:
|
|
# We still need to support the NT19 taxonomy for 2025 until that declaration period is over.
|
|
template_xmlid = 'l10n_nl_reports.icp_report_sbr_nt19'
|
|
|
|
report_template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
|
if not report_template:
|
|
raise RedirectWarning(
|
|
message=_(
|
|
"We couldn't find the correct export template for the year %(year)s. Please upgrade your module 'Netherlands - Accounting Reports' and try again.",
|
|
year=date_to.year,
|
|
),
|
|
action=self.env.ref('base.open_module_tree').id,
|
|
button_text=_("Go to Apps"),
|
|
additional_context={
|
|
'search_default_name': 'l10n_nl_reports',
|
|
'search_default_extra': True,
|
|
},
|
|
)
|
|
|
|
xbrl = self.env['ir.qweb']._render(report_template.id, data)
|
|
xbrl_element = etree.fromstring(xbrl)
|
|
xbrl_file = etree.tostring(cleanup_xml_node(xbrl_element, remove_blank_nodes=False), xml_declaration=True, encoding='utf-8')
|
|
return {
|
|
'file_name': report.get_default_report_filename(options, 'xbrl'),
|
|
'file_content': xbrl_file,
|
|
'file_type': 'xml',
|
|
}
|
|
|
|
def _generate_codes_values(self, report, lines, options):
|
|
|
|
def get_country_and_vat(line, colname_to_idx):
|
|
country = line['columns'][colname_to_idx['country_code']].get('name')
|
|
vat = line['columns'][colname_to_idx['vat']].get('name')
|
|
|
|
country = (country or '').strip().upper()
|
|
vat = (vat or '').strip().upper()
|
|
vat = re.sub(r'[^A-Z0-9]', '', vat)
|
|
|
|
return country, vat
|
|
|
|
def update_icp_context(contexts_map, country, vat):
|
|
key = (country, vat)
|
|
if key in contexts_map:
|
|
return contexts_map[key]['contextRef']
|
|
|
|
ctx_id = f"ICP_{country}_{vat or 'NOVAT'}"
|
|
|
|
contexts_map[key] = {
|
|
'contextRef': ctx_id,
|
|
'country': country,
|
|
'VATIdentificationNumberNational': vat,
|
|
}
|
|
return ctx_id
|
|
|
|
codes_values = options.get('codes_values', {})
|
|
vat_identification_division = codes_values.get('VATIdentificationNumberNLFiscalEntityDivision')
|
|
if vat_identification_division is None:
|
|
sender_vat = report._get_sender_company_for_export(options).vat
|
|
vat_identification_division = compact(sender_vat) if sender_vat else ''
|
|
|
|
codes_values.update({
|
|
'IntraCommunitySupplies': [],
|
|
'IntraCommunityServices': [],
|
|
'IntraCommunityABCSupplies': [],
|
|
'VATIdentificationNumberNLFiscalEntityDivision': vat_identification_division,
|
|
})
|
|
|
|
icp_contexts_map = {}
|
|
|
|
colname_to_idx = {col['expression_label']: idx for idx, col in enumerate(options.get('columns', []))}
|
|
company_currency = self.env.company.currency_id
|
|
|
|
for line in lines:
|
|
if not line['columns'][colname_to_idx['vat']].get('no_format', 0):
|
|
continue
|
|
|
|
country, vat = get_country_and_vat(line, colname_to_idx)
|
|
|
|
amount_product = line['columns'][colname_to_idx['amount_product']].get('no_format', 0)
|
|
if company_currency.compare_amounts(amount_product, 0):
|
|
ctx_id = update_icp_context(icp_contexts_map, country, vat)
|
|
codes_values['IntraCommunitySupplies'].append({
|
|
'CountryCodeISO': country,
|
|
'SuppliesAmount': str(int(amount_product)),
|
|
'VATIdentificationNumberNational': vat,
|
|
'contextRef': ctx_id,
|
|
})
|
|
|
|
amount_service = line['columns'][colname_to_idx['amount_service']].get('no_format', 0)
|
|
if company_currency.compare_amounts(amount_service, 0):
|
|
ctx_id = update_icp_context(icp_contexts_map, country, vat)
|
|
codes_values['IntraCommunityServices'].append({
|
|
'CountryCodeISO': country,
|
|
'ServicesAmount': str(int(amount_service)),
|
|
'VATIdentificationNumberNational': vat,
|
|
'contextRef': ctx_id,
|
|
})
|
|
|
|
amount_triangular = line['columns'][colname_to_idx['amount_triangular']].get('no_format', 0)
|
|
if company_currency.compare_amounts(amount_triangular, 0):
|
|
ctx_id = update_icp_context(icp_contexts_map, country, vat)
|
|
codes_values['IntraCommunityABCSupplies'].append({
|
|
'CountryCodeISO': country,
|
|
'SuppliesAmount': str(int(amount_triangular)),
|
|
'VATIdentificationNumberNational': vat,
|
|
'contextRef': ctx_id,
|
|
})
|
|
|
|
codes_values['contexts'] = list(icp_contexts_map.values())
|
|
|
|
return codes_values
|