270 lines
12 KiB
Python
270 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import time
|
|
|
|
from markupsafe import Markup
|
|
from stdnum.be.vat import compact
|
|
|
|
from odoo import _, fields, models
|
|
from odoo.exceptions import UserError
|
|
from .account_report import _raw_phonenumber, _get_xml_export_representative_node
|
|
|
|
|
|
class L10n_BeEcSalesReportHandler(models.AbstractModel):
|
|
_name = 'l10n_be.ec.sales.report.handler'
|
|
_inherit = ['account.ec.sales.report.handler']
|
|
_description = 'Belgian EC Sales Report Custom Handler'
|
|
|
|
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
|
"""
|
|
This method is used to get the dynamic lines of the report and adds a comparative test linked to the tax report.
|
|
"""
|
|
lines = super()._dynamic_lines_generator(report, options, all_column_groups_expression_totals, warnings=warnings)
|
|
colname_to_idx = {col['expression_label']: idx for idx, col in enumerate(options['columns'])}
|
|
|
|
if lines:
|
|
total = lines[-1][-1]['columns'][colname_to_idx['balance']]['no_format']
|
|
|
|
# This test requires the total, so needs to be checked after the lines are computed, but before the rendering
|
|
# of the template. This is why we add it here even if it's not an option per se.
|
|
if warnings is not None and not self.total_consistent_with_tax_report(options, total):
|
|
warnings['l10n_be_reports.sales_report_warning_cross_check'] = {'alert_type': 'warning'}
|
|
|
|
return lines
|
|
|
|
def _caret_options_initializer(self):
|
|
"""
|
|
Add custom caret option for the report to link to the partner and allow cleaner overrides.
|
|
"""
|
|
return {
|
|
'ec_sales': [
|
|
{'name': _("View Partner"), 'action': 'caret_option_open_record_form'},
|
|
{'name': _("Audit"), 'action': 'ec_sales_list_open_invoices', 'action_param': 'id'},
|
|
],
|
|
}
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
"""
|
|
Add the invoice lines search domain that is specific to the country.
|
|
Typically, the taxes account.report.expression ids relative to the country for the triangular, sale of goods
|
|
or services.
|
|
:param dict options: Report options
|
|
:return dict: The modified options dictionary
|
|
"""
|
|
super()._init_core_custom_options(report, options, previous_options)
|
|
ec_operation_category = options.get('sales_report_taxes', {'goods': tuple(), 'triangular': tuple(), 'services': tuple()})
|
|
ec_operation_category.update(self._get_tax_tags_for_belgian_sales_report())
|
|
|
|
# Change the names of the taxes to specific ones that are dependant to the tax type
|
|
ec_operation_category['operation_category'] = {
|
|
'goods': 'L (46L)',
|
|
'triangular': 'T (46T)',
|
|
'services': 'S (44)',
|
|
}
|
|
options.update({'sales_report_taxes': ec_operation_category})
|
|
|
|
def _get_tax_tags_for_belgian_sales_report(self):
|
|
report_46L_expression = self.env.ref('l10n_be.tax_report_line_46L_tag')
|
|
report_46T_expression = self.env.ref('l10n_be.tax_report_line_46T_tag')
|
|
report_44_expression = self.env.ref('l10n_be.tax_report_line_44_tag')
|
|
report_48s44_expression = self.env.ref('l10n_be.tax_report_line_48s44_tag')
|
|
report_48s46T_expression = self.env.ref('l10n_be.tax_report_line_48s46T_tag')
|
|
report_48s46L_expression = self.env.ref('l10n_be.tax_report_line_48s46L_tag')
|
|
|
|
return {
|
|
'goods': tuple((report_46L_expression + report_48s46L_expression)._get_matching_tags().ids),
|
|
'triangular': tuple((report_46T_expression + report_48s46T_expression)._get_matching_tags().ids),
|
|
'services': tuple((report_44_expression + report_48s44_expression)._get_matching_tags().ids),
|
|
}
|
|
|
|
def total_consistent_with_tax_report(self, options, total):
|
|
""" Belgian EC Sales taxes report total must match
|
|
Tax Report lines 44 + 46L + 46T - 48s44 - 48s46L - 48s46T.
|
|
"""
|
|
vat_report = self.env.ref('l10n_be.tax_report_vat')
|
|
tax_report_options = vat_report.get_options(options)
|
|
|
|
expressions = self.env['account.report.expression']
|
|
for expression_xmlid in ('l10n_be.tax_report_line_44_tag',
|
|
'l10n_be.tax_report_line_46L_tag',
|
|
'l10n_be.tax_report_line_46T_tag',
|
|
'l10n_be.tax_report_line_48s44_tag',
|
|
'l10n_be.tax_report_line_48s46L_tag',
|
|
'l10n_be.tax_report_line_48s46T_tag'):
|
|
expressions |= self.env.ref(expression_xmlid)
|
|
|
|
tax_total = 0.0
|
|
tax_total_grouped = vat_report._compute_expression_totals_for_each_column_group(expressions, tax_report_options)
|
|
for expr_dict in tax_total_grouped.values():
|
|
for expression, expr_total in expr_dict.items():
|
|
if expression.formula[:2] == '48':
|
|
tax_total -= expr_total.get('value', 0.)
|
|
else:
|
|
tax_total += expr_total.get('value', 0.)
|
|
return self.env.company.currency_id.compare_amounts(tax_total, total) == 0
|
|
|
|
def export_to_xml_sales_report(self, options):
|
|
colname_to_idx = {col['expression_label']: idx for idx, col in enumerate(options.get('columns', []))}
|
|
# Check
|
|
company = self.env.company
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
company_vat = report.get_vat_for_export(options)
|
|
if not company_vat:
|
|
raise UserError(_('No VAT number associated with your company.'))
|
|
default_address = company.partner_id.address_get()
|
|
address = default_address.get('invoice', company.partner_id)
|
|
if not address.email:
|
|
raise UserError(_('No email address associated with the company.'))
|
|
if not address.phone:
|
|
raise UserError(_('No phone associated with the company.'))
|
|
|
|
# Generate xml
|
|
post_code = street = city = country = data_clientinfo = ''
|
|
company_vat = company_vat.replace(' ', '').upper()
|
|
|
|
seq_declarantnum = self.env['ir.sequence'].next_by_code('declarantnum')
|
|
dnum = compact(company_vat) + seq_declarantnum[-4:]
|
|
ads = None
|
|
addr = company.partner_id.address_get(['invoice'])
|
|
phone = email = city = post_code = street = country = company_country = ''
|
|
|
|
if addr.get('invoice', False):
|
|
ads = self.env['res.partner'].browse([addr['invoice']])[0]
|
|
|
|
if ads:
|
|
if ads.phone:
|
|
phone = _raw_phonenumber(ads.phone)
|
|
elif address.phone:
|
|
phone = _raw_phonenumber(address.phone)
|
|
if ads.email:
|
|
email = ads.email
|
|
if ads.city:
|
|
city = ads.city
|
|
if ads.zip:
|
|
post_code = ads.zip
|
|
if ads.street:
|
|
street = ads.street
|
|
if ads.street2:
|
|
street += ' ' + ads.street2
|
|
|
|
company_country = ads.country_id.code if ads.country_id else 'BE'
|
|
|
|
options['no_format'] = True
|
|
lines = report._get_lines(options)
|
|
data_clientinfo = ''
|
|
seq = 0
|
|
clients = {}
|
|
for line in lines[:-1]: # Remove total line
|
|
country = line['columns'][colname_to_idx['country_code']].get('name', '')
|
|
vat = line['columns'][colname_to_idx['vat_number']].get('name', '').replace(' ', '').upper()
|
|
normalised_vat = (country + vat)
|
|
amount = line['columns'][colname_to_idx['balance']]['no_format']
|
|
code = line['columns'][colname_to_idx['sales_type_code']]['name'][:1]
|
|
if self.env.company.currency_id.is_zero(amount):
|
|
continue
|
|
if not vat:
|
|
raise UserError(_('No vat number defined for %s.', line['name']))
|
|
if (normalised_vat, code) in clients:
|
|
clients[normalised_vat, code]['amount'] += amount
|
|
else:
|
|
seq += 1
|
|
clients[normalised_vat, code] = {
|
|
'vatnum': vat,
|
|
'vat': normalised_vat,
|
|
'country': country,
|
|
'amount': amount,
|
|
'code': code,
|
|
'seq': seq,
|
|
}
|
|
|
|
for client in clients.values():
|
|
data_clientinfo += Markup("""
|
|
<ns2:IntraClient SequenceNumber="%(seq)s">
|
|
<ns2:CompanyVATNumber issuedBy="%(country)s">%(vatnum)s</ns2:CompanyVATNumber>
|
|
<ns2:Code>%(code)s</ns2:Code>
|
|
<ns2:Amount>%(amount).2f</ns2:Amount>
|
|
</ns2:IntraClient>""") % client
|
|
|
|
xml_data = {
|
|
'clientnbr': seq,
|
|
'amountsum': lines[-1]['columns'][colname_to_idx['balance']]['no_format'] if lines else 0,
|
|
}
|
|
|
|
date_from = fields.Date.from_string(options['date'].get('date_from'))
|
|
period_type = options['date'].get('period_type')
|
|
month = date_from.month if period_type == 'month' else None
|
|
quarter = (date_from.month - 1) // 3 + 1 if period_type == 'quarter' else None
|
|
|
|
xml_data.update({
|
|
'company_name': company.name,
|
|
'company_vat': company_vat,
|
|
'vatnum': compact(company_vat),
|
|
'sender_date': str(time.strftime('%Y-%m-%d')),
|
|
'street': street,
|
|
'city': city,
|
|
'post_code': post_code,
|
|
'country': company_country,
|
|
'email': email,
|
|
'phone': _raw_phonenumber(phone),
|
|
'year': date_from.year,
|
|
'month': month,
|
|
'quarter': quarter,
|
|
'comments': '',
|
|
'issued_by': 'BE',
|
|
'dnum': dnum,
|
|
'representative_node': _get_xml_export_representative_node(report),
|
|
})
|
|
|
|
data_head = Markup(f"""<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
<ns2:IntraConsignment xmlns="http://www.minfin.fgov.be/InputCommon" xmlns:ns2="http://www.minfin.fgov.be/IntraConsignment" IntraListingsNbr="1">
|
|
%(representative_node)s
|
|
<ns2:IntraListing SequenceNumber="1" ClientsNbr="%(clientnbr)s" DeclarantReference="%(dnum)s" AmountSum="%(amountsum).2f">
|
|
<ns2:Declarant>
|
|
<VATNumber>%(vatnum)s</VATNumber>
|
|
<Name>%(company_name)s</Name>
|
|
<Street>%(street)s</Street>
|
|
<PostCode>%(post_code)s</PostCode>
|
|
<City>%(city)s</City>
|
|
<CountryCode>%(country)s</CountryCode>
|
|
<EmailAddress>%(email)s</EmailAddress>
|
|
<Phone>%(phone)s</Phone>
|
|
</ns2:Declarant>
|
|
<ns2:Period>
|
|
{"<ns2:Month>%(month)s</ns2:Month>" if month else ""}
|
|
{"<ns2:Quarter>%(quarter)s</ns2:Quarter>" if quarter else ""}
|
|
<ns2:Year>%(year)s</ns2:Year>
|
|
</ns2:Period>""") % xml_data
|
|
|
|
data_rslt = data_head + data_clientinfo + Markup("""
|
|
</ns2:IntraListing>
|
|
</ns2:IntraConsignment>""")
|
|
|
|
return {
|
|
'file_name': report.get_default_report_filename(options, 'xml'),
|
|
'file_content': data_rslt.encode('ISO-8859-1', 'ignore'),
|
|
'file_type': 'xml',
|
|
}
|
|
|
|
def ec_sales_list_open_invoices(self, options, params=None):
|
|
ec_category, _model, res_id = self.env['account.report']._parse_line_id(params['line_id'])[-1]
|
|
|
|
domain = [
|
|
('move_id.move_type', 'in', self.env['account.move'].get_sale_types(include_receipts=True)),
|
|
('move_id.date', '>=', options['date']['date_from']),
|
|
('move_id.date', '<=', options['date']['date_to']),
|
|
('tax_tag_ids', 'in', options['sales_report_taxes'][ec_category]),
|
|
]
|
|
|
|
return {
|
|
'name': _('EC Sales List Audit'),
|
|
'type': 'ir.actions.act_window',
|
|
'views': [[self.env.ref('account.view_move_line_tree').id, 'list'], [False, 'form']],
|
|
'res_model': 'account.move.line',
|
|
'context': {
|
|
'search_default_partner_id': res_id,
|
|
'search_default_group_by_partner': 1,
|
|
'expand': 1,
|
|
},
|
|
'domain': domain,
|
|
}
|