diff --git a/addons/account/static/src/js/tours/account.js b/addons/account/static/src/js/tours/account.js index 3b30f0ee869..51d1ba5ab97 100644 --- a/addons/account/static/src/js/tours/account.js +++ b/addons/account/static/src/js/tours/account.js @@ -82,8 +82,8 @@ registry.category("web_tour.tours").add('account_tour', { content: _t("Send the invoice to the customer and check what he'll receive."), position: "bottom", }, { - trigger: "button[name=action_open_partners_without_email]", - extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]", + trigger: "div[name=partner_missing_email] a", + extra_trigger: "[name=move_type] [raw-value=out_invoice]", content: _t("Complete the partner data with email"), }, { trigger: ".o_field_widget[name=email] input, input[name=email]", diff --git a/addons/account/wizard/account_move_send.py b/addons/account/wizard/account_move_send.py index 4cd18956480..0661de1d9a8 100644 --- a/addons/account/wizard/account_move_send.py +++ b/addons/account/wizard/account_move_send.py @@ -41,7 +41,7 @@ class AccountMoveSend(models.TransientModel): readonly=False, ) display_mail_composer = fields.Boolean(compute='_compute_send_mail_extra_fields') - send_mail_warning_message = fields.Boolean(compute='_compute_send_mail_extra_fields') + send_mail_warning_message = fields.Json(compute='_compute_send_mail_extra_fields') send_mail_readonly = fields.Boolean(compute='_compute_send_mail_extra_fields') mail_template_id = fields.Many2one( comodel_name='mail.template', @@ -254,7 +254,17 @@ class AccountMoveSend(models.TransientModel): wizard.display_mail_composer = wizard.mode == 'invoice_single' invoices_without_mail_data = wizard.move_ids.filtered(lambda x: not x.partner_id.email) wizard.send_mail_readonly = invoices_without_mail_data == wizard.move_ids - wizard.send_mail_warning_message = bool(invoices_without_mail_data) and (wizard.checkbox_send_mail or wizard.send_mail_readonly) + if not (invoices_without_mail_data and wizard.checkbox_send_mail or wizard.send_mail_readonly): + wizard.send_mail_warning_message = False + else: + partners = invoices_without_mail_data.partner_id + wizard.send_mail_warning_message = { + **(wizard.send_mail_warning_message or {}), + 'partner_missing_email': { + 'message': _("Partner(s) should have an email address."), + 'action_text': _("View Partner(s)"), + 'action': partners._get_records_action(name=_("Check Partner(s)")) + }} @api.depends('mail_template_id') def _compute_mail_lang(self): @@ -326,25 +336,8 @@ class AccountMoveSend(models.TransientModel): # ------------------------------------------------------------------------- def action_open_partners_without_email(self, res_ids=None): - partners = self.move_ids.mapped("partner_id").filtered(lambda x: not x.email) - if len(partners) == 1: - return { - 'type': 'ir.actions.act_window', - 'res_model': 'res.partner', - 'view_mode': 'form', - 'target': 'current', - 'res_id': partners.id, - } - else: - return { - 'type': 'ir.actions.act_window', - 'res_model': 'res.partner', - 'view_mode': 'tree,form', - 'target': 'current', - 'name': _('Partners without email'), - 'context': {'create': False, 'delete': False}, - 'domain': [('id', 'in', partners.ids)], - } + # TODO: remove this method in master + return self.move_ids.mapped("partner_id").filtered(lambda x: not x.email)._get_records_action(name=_("Partners without email")) @api.model def _need_invoice_document(self, invoice): diff --git a/addons/account/wizard/account_move_send_views.xml b/addons/account/wizard/account_move_send_views.xml index 77f536d1be0..051f2462727 100644 --- a/addons/account/wizard/account_move_send_views.xml +++ b/addons/account/wizard/account_move_send_views.xml @@ -18,16 +18,8 @@ - -
- +
+
diff --git a/addons/l10n_it/models/account_tax.py b/addons/l10n_it/models/account_tax.py index 08b22ee1952..ea8d47b5413 100644 --- a/addons/l10n_it/models/account_tax.py +++ b/addons/l10n_it/models/account_tax.py @@ -51,6 +51,11 @@ class AccountTax(models.Model): if tax.l10n_it_exempt_reason == 'N6' and tax._l10n_it_is_split_payment(): raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'")) + def _l10n_it_get_tax_kind(self): + if self.amount_type == 'percent' and self.amount >= 0: + return 'vat' + return None + def _l10n_it_filter_kind(self, kind): """ This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund).""" return self if kind == 'vat' else self.env['account.tax'] diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py index d80191d59ae..44e9abeaf2f 100644 --- a/addons/l10n_it_edi/__manifest__.py +++ b/addons/l10n_it_edi/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Italy - E-invoicing', 'countries': ['it'], - 'version': '0.3', + 'version': '0.4', 'depends': [ 'l10n_it', 'account_edi_proxy_client', diff --git a/addons/l10n_it_edi/models/account_move.py b/addons/l10n_it_edi/models/account_move.py index 58dae2581b0..96676b7a0bd 100644 --- a/addons/l10n_it_edi/models/account_move.py +++ b/addons/l10n_it_edi/models/account_move.py @@ -115,8 +115,8 @@ class AccountMove(models.Model): @api.depends('commercial_partner_id.l10n_it_pa_index', 'company_id') def _compute_l10n_it_partner_pa(self): for move in self: - move.l10n_it_partner_pa = (move.country_code == 'IT' and move.commercial_partner_id.l10n_it_pa_index and - len(move.commercial_partner_id.l10n_it_pa_index) == 6) + partner = move.commercial_partner_id + move.l10n_it_partner_pa = partner and partner._l10n_it_edi_is_public_administration() @api.depends('move_type', 'line_ids.tax_tag_ids') def _compute_l10n_it_edi_is_self_invoice(self): @@ -184,10 +184,8 @@ class AccountMove(models.Model): """ self.ensure_one() - if (errors := self._l10n_it_edi_export_data_check()): - message = _("Errors occured while creating the e-invoice file.") - message += "\n- " + "\n- ".join(errors) - raise UserError(message) + if self._l10n_it_edi_export_data_check(): + raise UserError(_("The invoices you're trying to send have incomplete or incorrect data, please verify before sending.")) attachment_vals = self._l10n_it_edi_get_attachment_values(pdf_values=None) self.env['ir.attachment'].create(attachment_vals) @@ -460,10 +458,11 @@ class AccountMove(models.Model): self.ensure_one() template_reference = self.env.ref('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', raise_if_not_found=False) buyer = self.commercial_partner_id + checks = ['partner_address_missing', 'partner_vat_codice_fiscale_missing'] return bool( template_reference and not self.l10n_it_edi_is_self_invoice - and self._l10n_it_edi_export_buyer_data_check() + and list(buyer._l10n_it_edi_export_check(checks).keys()) == ['partner_address_missing'] and (not buyer.country_id or buyer.country_id.code == 'IT') and (buyer.l10n_it_codice_fiscale or (buyer.vat and (buyer.vat[:2].upper() == 'IT' or buyer.vat[:2].isdecimal()))) and self.amount_total <= 400 @@ -1031,117 +1030,67 @@ class AccountMove(models.Model): # ------------------------------------------------------------------------- def _l10n_it_edi_export_data_check(self): - errors = self._l10n_it_edi_base_export_data_check() - if not self._l10n_it_edi_is_simplified(): - errors += self._l10n_it_edi_export_buyer_data_check() + """ This function checks the Settings, Company, Partners, Moves involved in the + sending activity and returns an errors dictionary ready for the + actionable_errors widget to display. """ + + companies = self.mapped("company_id") + companies_partners = companies.mapped("partner_id") + moves_full = self.filtered(lambda m: not m._l10n_it_edi_is_simplified()) + moves_simplified = self.filtered(lambda m: m._l10n_it_edi_is_simplified()) + + full = moves_full.mapped("commercial_partner_id").filtered(lambda p: p not in companies_partners) + simplified = moves_simplified.mapped("commercial_partner_id").filtered(lambda p: p not in companies_partners | full) + representatives = companies.mapped("l10n_it_tax_representative_partner_id").filtered(lambda p: p not in companies_partners | simplified | full) + + return { + **companies._l10n_it_edi_export_check(), + **full._l10n_it_edi_export_check(['partner_address_missing']), + **simplified._l10n_it_edi_export_check(['partner_country_missing']), + **(simplified | full)._l10n_it_edi_export_check(['partner_vat_codice_fiscale_missing']), + **representatives._l10n_it_edi_export_check(['partner_vat_missing']), + **self._l10n_it_edi_base_export_check(), + **self._l10n_it_edi_export_taxes_check(), + } + + def _l10n_it_edi_base_export_check(self): + def build_error(message, records): + return { + 'message': message, + **({ + 'action_text': _("View invoice(s)"), + 'action': records._get_records_action(name=_("Invoice(s) to check")), + } if len(self) > 1 else {}) + } + + errors = {} + if moves := self.filtered(lambda move: move.l10n_it_edi_is_self_invoice and move._l10n_it_edi_services_or_goods() == 'both'): + errors['move_reverse_charge_with_mixed_services_and_goods'] = build_error( + message=_("Cannot apply Reverse Charge to bills which contains both services and goods."), + records=moves) + if pa_moves := self.filtered(lambda move: move.company_id.partner_id._l10n_it_edi_is_public_administration()): + if moves := pa_moves.filtered(lambda move: move.l10n_it_origin_document_type): + message = _("Your company belongs to the Public Administration, please fill out Origin Document Type field in the Electronic Invoicing tab.") + errors['move_missing_origin_document'] = build_error(message=message, records=moves) + if moves := pa_moves.filtered(lambda move: move.l10n_it_origin_document_date and move.l10n_it_origin_document_date > fields.Date.today()): + message = _("The Origin Document Date cannot be in the future.") + errors['move_future_origin_document_date'] = build_error(message=message, records=moves) return errors - def _l10n_it_edi_format_export_data_errors(self): - messages = ( - self._l10n_it_edi_format_errors(move.name + ":" if len(self) > 1 else False, move_warnings) - for move in self if (move_warnings := move._l10n_it_edi_export_data_check()) - ) - return Markup("
").join(messages) or False - - def _l10n_it_edi_base_export_data_check(self): - errors = [] - seller = self.company_id - buyer = self.commercial_partner_id - is_self_invoice = self.l10n_it_edi_is_self_invoice - if is_self_invoice: - seller, buyer = buyer, seller - - # <1.1.1.1> - if not seller.country_id: - errors.append(_("%s must have a country", seller.display_name)) - - # <1.1.1.2> - if not self.company_id.vat: - errors.append(_("%s must have a VAT number", seller.display_name)) - if seller.vat and len(seller.vat) > 30: - errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat)) - - # <1.2.1.2> - if not is_self_invoice and not seller.l10n_it_codice_fiscale: - errors.append(_("%s must have a codice fiscale number", seller.display_name)) - - # <1.2.1.8> - if not is_self_invoice and not seller.l10n_it_tax_system: - errors.append(_("The seller's company must have a tax system.")) - - # <1.2.2> - if not seller.street and not seller.street2: - errors.append(_("%s must have a street.", seller.display_name)) - if not seller.zip: - errors.append(_("%s must have a post code.", seller.display_name)) - elif len(seller.zip) != 5 and seller.country_id.code == 'IT': - errors.append(_("%s must have a post code of length 5.", seller.display_name)) - if not seller.city: - errors.append(_("%s must have a city.", seller.display_name)) - if not seller.country_id: - errors.append(_("%s must have a country.", seller.display_name)) - - if not is_self_invoice and seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat: - errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name)) - - # <1.4.1> - if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT': - errors.append(_("The buyer, %s, or his company must have a VAT number and/or a tax code (Codice Fiscale).", buyer.display_name)) - - if is_self_invoice and self._l10n_it_edi_services_or_goods() == 'both': - errors.append(_("Cannot apply Reverse Charge to a bill which contains both services and goods.")) - - if is_self_invoice and not buyer.partner_id.l10n_it_pa_index: - errors.append(_("Vendor bills sent as self-invoices to the SdI require a valid PA Index (Codice Destinatario) on the company's contact.")) - - for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id): - if not tax_line.tax_line_id.l10n_it_exempt_reason and tax_line.tax_line_id.amount == 0: - errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name)) - - if self.l10n_it_partner_pa: - if not self.l10n_it_origin_document_type: - errors.append(_("This invoice targets the Public Administration, please fill out" - " Origin Document Type field in the Electronic Invoicing tab.")) - if self.l10n_it_origin_document_date and self.l10n_it_origin_document_date > fields.Date.today(): - errors.append(_("The Origin Document Date cannot be in the future.")) - - errors += self._l10n_it_edi_export_taxes_data_check() - - return errors - - def _l10n_it_edi_export_taxes_data_check(self): - """ - Can be overridden by submodules like l10n_it_edi_withholding, which also allows for withholding and pension_fund taxes. - """ - errors = [] - for invoice_line in self.invoice_line_ids.filtered(lambda x: x.display_type == 'product'): - all_taxes = invoice_line.tax_ids.flatten_taxes_hierarchy() - vat_taxes = all_taxes.filtered(lambda t: t.amount_type == 'percent' and t.amount >= 0) - if len(vat_taxes) != 1: - errors.append(_("In line %s, you must select one and only one VAT tax.", invoice_line.name)) - return errors - - def _l10n_it_edi_export_buyer_data_check(self): - errors = [] - buyer = self.commercial_partner_id - - # <1.4.2> - if not buyer.street and not buyer.street2: - errors.append(_("%s must have a street.", buyer.display_name)) - if not buyer.country_id: - errors.append(_("%s must have a country.", buyer.display_name)) - if not buyer.zip: - errors.append(_("%s must have a post code.", buyer.display_name)) - elif len(buyer.zip) != 5 and buyer.country_id.code == 'IT': - errors.append(_("%s must have a post code of length 5.", buyer.display_name)) - if not buyer.city: - errors.append(_("%s must have a city.", buyer.display_name)) - - for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id): - if not tax_line.tax_line_id.l10n_it_exempt_reason and tax_line.tax_line_id.amount == 0: - errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name)) - - return errors + def _l10n_it_edi_export_taxes_check(self): + if move_lines := self.mapped("invoice_line_ids").filtered(lambda line: + line.display_type == 'product' + and len(line.tax_ids.flatten_taxes_hierarchy()._l10n_it_filter_kind('vat')) != 1 + ): + return { + 'move_only_one_vat_tax_per_line': { + 'message': _("Invoices must have exactly one VAT tax set per line."), + **({ + 'action_text': _("View invoice(s)"), + 'action': move_lines.mapped("move_id")._get_records_action(name=_("Check taxes on invoice lines")), + } if len(self) > 1 else {}) + }} + return {} def _l10n_it_edi_get_formatters(self): def format_alphanumeric(text, maxlen=None): diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py index e7503cf2623..24b06655036 100644 --- a/addons/l10n_it_edi/models/res_company.py +++ b/addons/l10n_it_edi/models/res_company.py @@ -115,3 +115,41 @@ class ResCompany(models.Model): def _compute_l10n_it_edi_proxy_user_id(self): for company in self: company.l10n_it_edi_proxy_user_id = company.account_edi_proxy_client_ids.filtered(lambda x: x.proxy_type == 'l10n_it_edi') + + def _l10n_it_edi_export_check(self): + checks = { + 'company_vat_codice_fiscale_missing': { + 'fields': [('vat', 'l10n_it_codice_fiscale')], + 'message': _("Company/ies should have a VAT number or Codice Fiscale."), + }, + 'company_address_missing': { + 'fields': [('street', 'street2'), ('zip',), ('city',), ('country_id',)], + 'message': _("Company/ies should have a complete address, verify their Street, City, Zipcode and Country."), + }, + 'company_l10n_it_tax_system_missing': { + 'fields': [('l10n_it_tax_system',)], + 'message': _("Company/ies should have a Tax System"), + }, + } + errors = {} + for key, check in checks.items(): + for fields_tuple in check.pop('fields'): + if invalid_records := self.filtered(lambda record: not any(record[field] for field in fields_tuple)): + errors[key] = { + 'message': check['message'], + 'action_text': _("View Company/ies"), + 'action': invalid_records._get_records_action(name=_("Check Company Data")), + } + if self.filtered(lambda x: not x.l10n_it_edi_proxy_user_id): + new_context = { + **self.env.context, + 'module': 'account', + 'default_search_setting': _("Italian Electronic Invoicing"), + 'bin_size': False, + } + errors['settings_l10n_it_edi_proxy_user_id'] = { + 'message': _("You must accept the terms and conditions in the Settings to use the IT EDI."), + 'action_text': _("View Settings"), + 'action': self.env['res.config.settings']._get_records_action(name=_("Settings"), context=new_context), + } + return errors diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py index f2f7f3073a5..9d5089aa1e8 100644 --- a/addons/l10n_it_edi/models/res_partner.py +++ b/addons/l10n_it_edi/models/res_partner.py @@ -36,8 +36,7 @@ class ResPartner(models.Model): def _l10n_it_edi_is_public_administration(self): """ Returns True if the destination of the FatturaPA belongs to the Public Administration. """ self.ensure_one() - return len(self.l10n_it_pa_index or '') == 6 - + return self.country_id.code == 'IT' and len(self.l10n_it_pa_index or '') == 6 def _l10n_it_edi_get_values(self): """ Generates all partner values needed by l10n_it_edi XML export. @@ -148,3 +147,39 @@ class ResPartner(models.Model): for record in self: if record.l10n_it_codice_fiscale and (not codicefiscale.is_valid(record.l10n_it_codice_fiscale) and not iva.is_valid(record.l10n_it_codice_fiscale)): raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345670546' for businesses.", record.l10n_it_codice_fiscale)) + + def _l10n_it_edi_export_check(self, checks=None): + checks = checks or ['partner_vat_codice_fiscale_missing', 'partner_address_missing'] + fields_to_check = { + 'partner_vat_missing': { + 'fields': [('vat',)], + 'message': _("Partner(s) should have a VAT number."), + }, + 'partner_vat_codice_fiscale_missing': { + 'fields': [('vat', 'l10n_it_codice_fiscale')], + 'message': _("Partner(s) should have a VAT number or Codice Fiscale."), + }, + 'partner_country_missing': { + 'fields': [('country_id',)], + 'message': _("Partner(s) should have a Country when used for simplified invoices."), + }, + 'partner_address_missing': { + 'fields': [('street', 'street2'), ('zip',), ('city',), ('country_id',)], + 'message': _("Partner(s) should have a complete address, verify their Street, City, Zipcode and Country."), + }, + } + selected_checks = {k: v for k, v in fields_to_check.items() if k in checks} + single_views = [(False, 'form')] + list_view = (self.env.ref('l10n_it_edi.res_partner_tree_l10n_it', raise_if_not_found=False)) + multi_views = [(list_view.id if list_view else False, 'list'), (False, 'form')] + errors = {} + for key, check in selected_checks.items(): + for fields_tuple in check['fields']: + if invalid_records := self.filtered(lambda record: not any(record[field] for field in fields_tuple)): + views = single_views if len(invalid_records) == 1 else multi_views + errors[key] = { + 'message': check['message'], + 'action_text': _("View Partner(s)"), + 'action': invalid_records._get_records_action(name=_("Check Partner(s)"), views=views), + } + return errors diff --git a/addons/l10n_it_edi/tests/test_edi_export.py b/addons/l10n_it_edi/tests/test_edi_export.py index 2254dc36e4d..71e67f94b3e 100644 --- a/addons/l10n_it_edi/tests/test_edi_export.py +++ b/addons/l10n_it_edi/tests/test_edi_export.py @@ -230,10 +230,7 @@ class TestItEdiExport(TestItEdi): }), ], }) - - expected = ['Alessi must have a street.', 'Alessi must have a country.', 'Alessi must have a post code.', 'Alessi must have a city.'] - actual = invoice._l10n_it_edi_export_data_check() - self.assertEqual(expected, actual) + self.assertEqual(['partner_address_missing'], list(invoice._l10n_it_edi_export_data_check().keys())) def test_invoice_non_domestic_simplified(self): invoice = self.env['account.move'].with_company(self.company).create({ @@ -249,9 +246,7 @@ class TestItEdiExport(TestItEdi): }), ], }) - expected = ['Alessi must have a street.', 'Alessi must have a post code.', 'Alessi must have a city.'] - actual = invoice._l10n_it_edi_export_data_check() - self.assertEqual(expected, actual) + self.assertEqual(['partner_address_missing'], list(invoice._l10n_it_edi_export_data_check().keys())) def test_invoice_zero_percent_taxes(self): tax_zero_percent_hundred_percent_repartition = self.env['account.tax'].with_company(self.company).create({ diff --git a/addons/l10n_it_edi/views/l10n_it_view.xml b/addons/l10n_it_edi/views/l10n_it_view.xml index b6d5e0a67a5..ff889c7e103 100644 --- a/addons/l10n_it_edi/views/l10n_it_view.xml +++ b/addons/l10n_it_edi/views/l10n_it_view.xml @@ -1,6 +1,19 @@ + + res.partner.tree.l10n.it + primary + res.partner + + + + + + + + + res.partner.form.l10n.it res.partner @@ -109,7 +122,7 @@ diff --git a/addons/l10n_it_edi/views/res_config_settings_views.xml b/addons/l10n_it_edi/views/res_config_settings_views.xml index 17d38c879b2..dd040d0fbe7 100644 --- a/addons/l10n_it_edi/views/res_config_settings_views.xml +++ b/addons/l10n_it_edi/views/res_config_settings_views.xml @@ -7,7 +7,7 @@ - +
diff --git a/addons/l10n_it_edi/wizard/account_move_send.py b/addons/l10n_it_edi/wizard/account_move_send.py index 9bb86d8525f..acca4a88a48 100644 --- a/addons/l10n_it_edi/wizard/account_move_send.py +++ b/addons/l10n_it_edi/wizard/account_move_send.py @@ -1,22 +1,22 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup, escape + from odoo import _, api, fields, models +from odoo.addons.base.models.ir_qweb_fields import nl2br class AccountMoveSend(models.TransientModel): _inherit = 'account.move.send' - l10n_it_edi_warning_message = fields.Html(compute='_compute_l10n_it_edi_xml_export') - + l10n_it_edi_warning_message = fields.Html(compute='_compute_l10n_it_edi_warning_message') + l10n_it_edi_actionable_errors = fields.Json(compute='_compute_l10n_it_edi_xml_export') l10n_it_edi_enable_xml_export = fields.Boolean(compute='_compute_l10n_it_edi_xml_export') l10n_it_edi_readonly_xml_export = fields.Boolean(compute='_compute_l10n_it_edi_xml_export') l10n_it_edi_checkbox_xml_export = fields.Boolean('E-invoice XML', compute='_compute_l10n_it_edi_checkbox_xml_export', store=True, - readonly=False, - help="Create the e-invoice XML ready to be sent to the Italian Tax Agency.\n" - "It is set as readonly if a report has already been created, to avoid inconsistencies.\n" - "To re-enable it, delete the PDF attachment.") + readonly=False) l10n_it_edi_enable_send = fields.Boolean(compute='_compute_l10n_it_edi_enable_readonly_send') l10n_it_edi_readonly_send = fields.Boolean(compute='_compute_l10n_it_edi_enable_readonly_send') @@ -37,30 +37,54 @@ class AccountMoveSend(models.TransientModel): # COMPUTE/CONSTRAINS METHODS # ------------------------------------------------------------------------- + @api.depends('l10n_it_edi_actionable_errors') + def _compute_l10n_it_edi_warning_message(self): + # To be removed -- Proxy feature to be replaced with actionable_errors as soon as the user updates the module + for wizard in self: + messages = [] + if wizard.l10n_it_edi_actionable_errors: + messages.append(_("Please upgrade the Italian EDI module to update this widget.")) + messages.append(_("Go to Applications page and update the 'Italia - Fatturazione Elettronica' module.")) + messages.append("") + for error_key, error_data in wizard.l10n_it_edi_actionable_errors.items(): + message = error_data['message'] + split = error_key.split("_") + if len(split) > 1 and (model_id := { + 'partner': 'res.partner', + 'move': 'account.move', + 'company': 'res.company' + }.get(split[0], None)): + action = error_data['action'] + if 'res_id' in action: + record_ids = [action['res_id']] + else: + record_ids = action['domain'][0][2] + records = self.env[model_id].browse(record_ids) + message = f"{message} - {', '.join(records.mapped('display_name'))}" + messages.append(nl2br(escape(message))) + wizard.l10n_it_edi_warning_message = Markup("
").join(messages) + @api.depends('move_ids') def _compute_l10n_it_edi_xml_export(self): for wizard in self: if wizard.company_id.account_fiscal_country_id.code == 'IT': - if not wizard.company_id.l10n_it_edi_proxy_user_id: - wizard.l10n_it_edi_warning_message = _("You must accept the terms and conditions in the Settings to use the IT EDI.") - else: - wizard.l10n_it_edi_warning_message = wizard.move_ids._l10n_it_edi_format_export_data_errors() has_pdf_but_no_xml = any(move.invoice_pdf_report_id and not move.l10n_it_edi_attachment_id for move in wizard.move_ids) all_have_xml = all(move.l10n_it_edi_attachment_id for move in wizard.move_ids) + wizard.l10n_it_edi_actionable_errors = self.move_ids._l10n_it_edi_export_data_check() wizard.l10n_it_edi_enable_xml_export = any(m._l10n_it_edi_ready_for_xml_export() for m in wizard.move_ids) - wizard.l10n_it_edi_readonly_xml_export = bool(wizard.l10n_it_edi_warning_message) or has_pdf_but_no_xml or all_have_xml + wizard.l10n_it_edi_readonly_xml_export = wizard.l10n_it_edi_actionable_errors or has_pdf_but_no_xml or all_have_xml else: - wizard.l10n_it_edi_warning_message = False + wizard.l10n_it_edi_actionable_errors = False wizard.l10n_it_edi_enable_xml_export = False wizard.l10n_it_edi_readonly_xml_export = False - @api.depends('move_ids', 'l10n_it_edi_checkbox_xml_export', 'l10n_it_edi_warning_message') + @api.depends('move_ids', 'l10n_it_edi_checkbox_xml_export', 'l10n_it_edi_actionable_errors') def _compute_l10n_it_edi_enable_readonly_send(self): for wizard in self: if wizard.company_id.account_fiscal_country_id.code == 'IT': xml_already_sent = all(m.l10n_it_edi_state not in (False, 'rejected') for m in wizard.move_ids) wizard.l10n_it_edi_enable_send = wizard.l10n_it_edi_checkbox_xml_export - wizard.l10n_it_edi_readonly_send = bool(wizard.l10n_it_edi_warning_message) or xml_already_sent + wizard.l10n_it_edi_readonly_send = bool(wizard.l10n_it_edi_actionable_errors or xml_already_sent) else: wizard.l10n_it_edi_enable_send = False wizard.l10n_it_edi_readonly_send = False diff --git a/addons/l10n_it_edi/wizard/account_move_send_views.xml b/addons/l10n_it_edi/wizard/account_move_send_views.xml index da8aad0425a..6d1bca4edf6 100644 --- a/addons/l10n_it_edi/wizard/account_move_send_views.xml +++ b/addons/l10n_it_edi/wizard/account_move_send_views.xml @@ -10,15 +10,18 @@ - +
+
diff --git a/addons/l10n_it_edi_withholding/models/account_move.py b/addons/l10n_it_edi_withholding/models/account_move.py index 5beadd517c2..18489f1238f 100644 --- a/addons/l10n_it_edi_withholding/models/account_move.py +++ b/addons/l10n_it_edi_withholding/models/account_move.py @@ -19,10 +19,11 @@ class AccountMove(models.Model): @api.depends('amount_total_signed') def _compute_amount_extended(self): for move in self: - totals = dict(vat=0.0, withholding=0.0, pension_fund=0.0) + totals = {None: 0.0, 'vat':0.0, 'withholding': 0.0, 'pension_fund': 0.0} if move.is_invoice(True): for line in [line for line in move.line_ids if line.tax_line_id]: - totals[line.tax_line_id._l10n_it_get_tax_kind()] -= line.balance + kind = line.tax_line_id._l10n_it_get_tax_kind() + totals[kind] -= line.balance move.l10n_it_amount_vat_signed = totals['vat'] move.l10n_it_amount_withholding_signed = totals['withholding'] move.l10n_it_amount_pension_fund_signed = totals['pension_fund'] diff --git a/addons/l10n_it_edi_withholding/models/account_tax.py b/addons/l10n_it_edi_withholding/models/account_tax.py index 0e3b3ba1657..324b27e9de3 100644 --- a/addons/l10n_it_edi_withholding/models/account_tax.py +++ b/addons/l10n_it_edi_withholding/models/account_tax.py @@ -89,7 +89,7 @@ class AccountTax(models.Model): def _l10n_it_get_tax_kind(self): return ((self.l10n_it_withholding_type and 'withholding') or (self.l10n_it_pension_fund_type and 'pension_fund') - or 'vat') + or super()._l10n_it_get_tax_kind()) def _l10n_it_filter_kind(self, kind): """ Filters taxes depending on _l10n_it_get_tax_kind. """