# -*- 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(""" %(vatnum)s %(code)s %(amount).2f """) % 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""" %(representative_node)s %(vatnum)s %(company_name)s %(street)s %(post_code)s %(city)s %(country)s %(email)s %(phone)s {"%(month)s" if month else ""} {"%(quarter)s" if quarter else ""} %(year)s """) % xml_data data_rslt = data_head + data_clientinfo + Markup(""" """) 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, }