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 @@
This module is maintained by the OCA.
-
-
-
+
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 "")