From 154eedc9ae101ab581745ba0ed6ce88ed1a0bff8 Mon Sep 17 00:00:00 2001 From: Borruso Date: Mon, 3 Oct 2022 18:03:46 +0200 Subject: [PATCH] [ADD] l10n_it_fatturapa_out: edit invoice sent SDI Co-authored-by: guidgaro75 --- l10n_it_fatturapa_out/__manifest__.py | 1 + l10n_it_fatturapa_out/models/account.py | 231 ++++++++++++++++++ l10n_it_fatturapa_out/security/groups.xml | 12 + .../tests/test_fatturapa_xml_validation.py | 30 +++ .../wizard/wizard_export_fatturapa.py | 6 +- 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 l10n_it_fatturapa_out/security/groups.xml diff --git a/l10n_it_fatturapa_out/__manifest__.py b/l10n_it_fatturapa_out/__manifest__.py index 6ea28da9497c..c7bf7f848d66 100644 --- a/l10n_it_fatturapa_out/__manifest__.py +++ b/l10n_it_fatturapa_out/__manifest__.py @@ -29,6 +29,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 d9380eaa13be..f1b97de8fff9 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,237 @@ 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!" + ) + ) + lr = 0 + for new_line_ritenuta in 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, + ) + lr += 1 + + 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!" + ) + ) + ld = 0 + for new_line_details in 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, + ) + ld += 1 + + 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!" + ) + ) + lr = 0 + for new_line_riepilogo in 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, + ) + lr += 1 + + 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(AccountInvoice, self).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/tests/test_fatturapa_xml_validation.py b/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py index e957358c5a45..10477a54791f 100644 --- a/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py +++ b/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py @@ -938,3 +938,33 @@ def test_validate_invoice(self): invoice.action_post() self.assertEqual(invoice.state, "posted") + + 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 f4279a6f8a28..460d117cb800 100644 --- a/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py +++ b/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py @@ -244,7 +244,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 "")