diff --git a/l10n_it_fatturapa_out/__manifest__.py b/l10n_it_fatturapa_out/__manifest__.py index dd878875eb55..05077ab44c03 100644 --- a/l10n_it_fatturapa_out/__manifest__.py +++ b/l10n_it_fatturapa_out/__manifest__.py @@ -30,6 +30,7 @@ "views/partner_view.xml", "views/company_view.xml", "data/l10n_it_fatturapa_out_data.xml", + "security/groups.xml", "security/ir.model.access.csv", "security/rules.xml", ], diff --git a/l10n_it_fatturapa_out/models/account.py b/l10n_it_fatturapa_out/models/account.py index 138fb6732974..34f3e3f81a53 100644 --- a/l10n_it_fatturapa_out/models/account.py +++ b/l10n_it_fatturapa_out/models/account.py @@ -1,8 +1,13 @@ # Copyright 2014 Davide Corio # Copyright 2016 Lorenzo Battistini - Agile Business Group +import base64 + +from lxml import etree + from odoo import api, fields, models from odoo.exceptions import UserError +from odoo.http import request from odoo.tools.translate import _ fatturapa_attachment_state_mapping = { @@ -99,11 +104,235 @@ def preventive_checks(self): ) return + @api.model + def check_tag(self, new_xml, original_xml, tags, precision=None): + """ + This function check if tag in new xml generated after function write() + is the same of original xml + + :param new_xml: new xml generated after function write() + :param original_xml: original xml linked to invoice + :param tags: tags of xml to check + :param precision: precision to apply on text tag for check + :return: True if tags is the same else + """ + for tag in tags: + new_tag_text = new_xml.find(tag) is not None and new_xml.find(tag).text + original_tag_text = ( + original_xml.find(tag) is not None and original_xml.find(tag).text + ) + if precision: + new_tag_text = "{text:.{precision}f}".format( + text=float(new_tag_text), precision=precision + ) + original_tag_text = "{text:.{precision}f}".format( + text=float(original_tag_text), precision=precision + ) + if (new_tag_text or "").strip() != (original_tag_text or "").strip(): + raise UserError( + _("%(tag)s isn't equal to tag in file e-invoice already created!") + % {"tag": tag[2:]} + ) + + def check_CessionarioCommittente(self, new_xml, original_xml): + list_tag = [ + ".//CessionarioCommittente/DatiAnagrafici/IdFiscaleIVA/IdPaese", + ".//CessionarioCommittente/DatiAnagrafici/IdFiscaleIVA/IdCodice", + ] + self.check_tag(new_xml, original_xml, list_tag) + + def check_DatiGeneraliDocumento(self, new_xml, original_xml): + price_precision = self.env["decimal.precision"].precision_get( + "Product Price for XML e-invoices" + ) + + list_tag = [ + ".//DatiGeneraliDocumento/Data", + ".//DatiGeneraliDocumento/TipoDocumento", + ".//DatiGeneraliDocumento/Divisa", + ".//DatiGeneraliDocumento/Numero", + ] + self.check_tag(new_xml, original_xml, list_tag) + list_tag = [ + ".//DatiGeneraliDocumento/ImportoTotaleDocumento", + ] + self.check_tag(new_xml, original_xml, list_tag, price_precision) + + if len(new_xml.findall(".//DatiGeneraliDocumento/DatiRitenuta")) != len( + original_xml.findall(".//DatiGeneraliDocumento/DatiRitenuta") + ): + raise UserError( + _( + "DatiGeneraliDocumento/DatiRitenuta " + "isn't equal to tag in file e-invoice already created!" + ) + ) + for lr, new_line_ritenuta in enumerate( + new_xml.findall(".//DatiGeneraliDocumento/DatiRitenuta") + ): + original_line_ritenuta = original_xml.findall( + ".//DatiGeneraliDocumento/DatiRitenuta" + )[lr] + list_tag_DatiRitenuta = [ + ".//TipoRitenuta", + ".//CausalePagamento", + ] + self.check_tag( + new_line_ritenuta, original_line_ritenuta, list_tag_DatiRitenuta + ) + list_tag_DatiRitenuta = [ + ".//ImportoRitenuta", + ".//AliquotaRitenuta", + ] + self.check_tag( + new_line_ritenuta, + original_line_ritenuta, + list_tag_DatiRitenuta, + price_precision, + ) + + def check_DatiBeniServizi(self, new_xml, original_xml): + price_precision = self.env["decimal.precision"].precision_get( + "Product Price for XML e-invoices" + ) + uom_precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + + if len(new_xml.findall(".//DatiBeniServizi/DettaglioLinee")) != len( + original_xml.findall(".//DatiBeniServizi/DettaglioLinee") + ): + raise UserError( + _( + "DatiBeniServizi/DettaglioLinee " + "isn't equal to tag in file e-invoice already created!" + ) + ) + for ld, new_line_details in enumerate( + new_xml.findall(".//DatiBeniServizi/DettaglioLinee") + ): + original_line_details = original_xml.findall( + ".//DatiBeniServizi/DettaglioLinee" + )[ld] + list_tag_DettaglioLinee = [ + ".//NumeroLinea", + ".//CodiceTipo", + ".//CodiceValore", + ".//Descrizione", + ".//Natura", + ".//Ritenuta", + ] + self.check_tag( + new_line_details, original_line_details, list_tag_DettaglioLinee + ) + list_tag_DettaglioLinee = [ + ".//Quantita", + ] + self.check_tag( + new_line_details, + original_line_details, + list_tag_DettaglioLinee, + uom_precision, + ) + list_tag_DettaglioLinee = [ + ".//PrezzoUnitario", + ".//AliquotaIVA", + ".//PrezzoTotale", + ] + self.check_tag( + new_line_details, + original_line_details, + list_tag_DettaglioLinee, + price_precision, + ) + + if len(new_xml.findall(".//DatiBeniServizi/DatiRiepilogo")) != len( + original_xml.findall(".//DatiBeniServizi/DatiRiepilogo") + ): + raise UserError( + _( + "DatiBeniServizi/DatiRiepilogo " + "isn't equal to tag in file e-invoice already created!" + ) + ) + for lr, new_line_riepilogo in enumerate( + new_xml.findall(".//DatiBeniServizi/DatiRiepilogo") + ): + original_line_riepilogo = original_xml.findall( + ".//DatiBeniServizi/DatiRiepilogo" + )[lr] + list_tag_DatiRiepilogo = [ + ".//AliquotaIVA", + ".//ImponibileImporto", + ".//Imposta", + ] + self.check_tag( + new_line_riepilogo, + original_line_riepilogo, + list_tag_DatiRiepilogo, + price_precision, + ) + + def elements_equal(self, new_xml, original_xml): + self.check_CessionarioCommittente(new_xml, original_xml) + self.check_DatiGeneraliDocumento(new_xml, original_xml) + self.check_DatiBeniServizi(new_xml, original_xml) + + def check_move_confirmable(self): + self.ensure_one() + + if not self.state == "posted" and not ( + request + and request.params.get("method", False) + and request.params["method"] == "action_post" + ): + return True + return False + + def write(self, vals): + is_draft = {} + for move in self: + is_draft[move.id] = True if move.state == "draft" else False + res = super().write(vals) + for move in self: + if ( + move.is_sale_document() + and move.fatturapa_attachment_out_id + and is_draft[move.id] + and not move.state == "cancel" + and not move.env.context.get("skip_check_xml", False) + and not ( + request + and request.params.get("method", False) + and request.params["method"] == "button_draft" + ) + ): + context_partner = self.env.context.copy() + context_partner.update({"lang": move.partner_id.lang}) + context_partner.update(skip_check_xml=True) + fatturapa, progressivo_invio = self.env[ + "wizard.export.fatturapa" + ].exportInvoiceXML(move.partner_id, [move.id], context=context_partner) + new_xml_content = fatturapa.to_xml(self.env) + original_xml_content = base64.decodebytes( + move.fatturapa_attachment_out_id.datas + ) + parser = etree.XMLParser(remove_blank_text=True) + new_xml = etree.fromstring(new_xml_content, parser) + original_xml = etree.fromstring(original_xml_content, parser) + move.elements_equal(new_xml, original_xml) + if move.check_move_confirmable(): + move.with_context(skip_check_xml=True).action_post() + return res + def button_draft(self): for invoice in self: if ( invoice.fatturapa_state != "error" and invoice.fatturapa_attachment_out_id + and not self.env.user.has_group( + "l10n_it_fatturapa_out.group_edit_invoice_sent_sdi" + ) ): raise UserError( _( diff --git a/l10n_it_fatturapa_out/security/groups.xml b/l10n_it_fatturapa_out/security/groups.xml new file mode 100644 index 000000000000..b99f28224411 --- /dev/null +++ b/l10n_it_fatturapa_out/security/groups.xml @@ -0,0 +1,12 @@ + + + + + Edit Invoice Sent SDI + Can reset to draft and then edit invoice sent to SDI + + + + diff --git a/l10n_it_fatturapa_out/static/description/index.html b/l10n_it_fatturapa_out/static/description/index.html index 29acafecb90b..04c3eebc8d82 100644 --- a/l10n_it_fatturapa_out/static/description/index.html +++ b/l10n_it_fatturapa_out/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -486,9 +485,7 @@

Contributors

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py b/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py index 325b82e5072c..ab862bdd4ef3 100644 --- a/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py +++ b/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py @@ -1118,3 +1118,33 @@ def test_18_xml_export(self): # XML doc to be validated xml_content = base64.decodebytes(attachment.datas) self.check_content(xml_content, "IT06363391001_00018.xml") + + def test_edit_invoice_sent_sdi(self): + """ + Check e-invoice tags after edit invoice. + """ + invoice = self._create_invoice() + invoice.action_post() + self.run_wizard(invoice.id) + self.assertEqual(invoice.state, "posted") + self.assertTrue(invoice.fatturapa_attachment_out_id.exists()) + + with self.assertRaises(UserError), self.cr.savepoint(): + invoice.with_user(self.account_manager.id).button_draft() + + self.account_manager.groups_id += self.env.ref( + "l10n_it_fatturapa_out.group_edit_invoice_sent_sdi" + ) + invoice.with_user(self.account_manager.id).button_draft() + self.assertEqual(invoice.state, "draft") + + with self.assertRaises(UserError), self.cr.savepoint(): + move_form = Form(invoice) + with move_form.invoice_line_ids.edit(0) as line_form: + line_form.price_unit = 800 + move_form.save() + + move_form = Form(invoice) + move_form.invoice_payment_term_id = self.account_payment_term + move_form.save() + self.assertEqual(invoice.state, "posted") diff --git a/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py b/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py index d232ee8e0d3e..e26b1033c39f 100644 --- a/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py +++ b/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py @@ -257,7 +257,11 @@ def exportInvoiceXML(self, partner, invoice_ids, attach=False, context=None): # generate attachments (PDF version of invoice) for inv in invoice_ids: - if not attach and inv.fatturapa_attachment_out_id: + if ( + not attach + and inv.fatturapa_attachment_out_id + and not context.get("skip_check_xml", False) + ): raise UserError( _("E-invoice export file still present for invoice %s.") % (inv.name or "")