diff --git a/l10n_it_fatturapa_out/models/account.py b/l10n_it_fatturapa_out/models/account.py
index e036a368a496..a305a7acfe78 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 = {
@@ -95,11 +100,243 @@ 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(
+ _(
+ "{} isn't equal to tag in file e-invoice already created!".format(
+ 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(
+ _(
+ "{} isn't equal to tag in file e-invoice already created!".format(
+ "DatiGeneraliDocumento/DatiRitenuta"
+ )
+ )
+ )
+ 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(
+ _(
+ "{} isn't equal to tag in file e-invoice already created!".format(
+ "DatiBeniServizi/DettaglioLinee"
+ )
+ )
+ )
+ 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(
+ _(
+ "{} isn't equal to tag in file e-invoice already created!".format(
+ "DatiBeniServizi/DatiRiepilogo"
+ )
+ )
+ )
+ 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/res_groups.xml b/l10n_it_fatturapa_out/security/res_groups.xml
index 9d656f38aa6e..7e5456de778e 100644
--- a/l10n_it_fatturapa_out/security/res_groups.xml
+++ b/l10n_it_fatturapa_out/security/res_groups.xml
@@ -8,4 +8,12 @@
Allow to force e-invoice export state
+
+
+ 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 ac64e1e76a64..e160f26e9cfe 100644
--- a/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py
+++ b/l10n_it_fatturapa_out/tests/test_fatturapa_xml_validation.py
@@ -959,3 +959,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 d333c1f776f3..ff09d99ce22a 100644
--- a/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py
+++ b/l10n_it_fatturapa_out/wizard/wizard_export_fatturapa.py
@@ -242,7 +242,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 "")