summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorPaolo (pgi) <pgi@odoo.com>2021-05-21 11:00:53 +0000
committerPaolo (pgi) <pgi@odoo.com>2021-05-21 11:19:18 +0000
commited723be917637320fc4d2787bdf505e4a7a8216d (patch)
tree2b6e4dfccd323c3623898800de69cb3d977a2fb5 /addons
parent0c4111850ac2bf54c71e9da04fac5b0c651021cd (diff)
[FIX] l10n_it_edi: Vendor bill is not being created from PEC
When one or more vendor bills in Italian e-invoice format were sent to the registered PEC mailbox, if they are in the PKCS#7 format ('.xml.p7m'), nothing happened. The e-invoice content should be automatically decoded and the invoice should be created as soon as the xml file is fetched from the PEC mailbox. A new remove_signature function has been added in a tools/remove_signature.py file to handle this case. The OpenSSL and pyOpenSSL libraries must be installed. They are not in requirements.txt but they're probably available. New tests has been provided for the signed email reading method. opw-2460485 closes odoo/odoo#71138 Signed-off-by: Josse Colpaert <jco@openerp.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/l10n_it_edi/__init__.py1
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py18
-rw-r--r--addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml.p7mbin0 -> 7083 bytes
-rw-r--r--addons/l10n_it_edi/tests/test_ir_mail_server.py15
-rw-r--r--addons/l10n_it_edi/tools/__init__.py4
-rw-r--r--addons/l10n_it_edi/tools/remove_signature.py47
6 files changed, 82 insertions, 3 deletions
diff --git a/addons/l10n_it_edi/__init__.py b/addons/l10n_it_edi/__init__.py
index dc5e6b693d1..54a1e867bac 100644
--- a/addons/l10n_it_edi/__init__.py
+++ b/addons/l10n_it_edi/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+from . import tools
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index 57f6eb6cb1b..a2a6205e31e 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -16,6 +16,7 @@ from xmlrpc import client as xmlrpclib
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError, UserError
+from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature
_logger = logging.getLogger(__name__)
@@ -133,9 +134,20 @@ class FetchmailServer(models.Model):
if split_underscore[1] in ['RC', 'NS', 'MC', 'MT', 'EC', 'SE', 'NE', 'DT']:
# we have a receipt
self._message_receipt_invoice(split_underscore[1], attachment)
- elif re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname):
- # we have a new E-invoice
- att_content_data = attachment.content.encode()
+ else:
+ match = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname)
+ # If match, we have an invoice.
+ if match:
+ # If it's signed, the content has a bytes type and we just remove the signature's envelope
+ if match.groups()[1] == 'xml.p7m':
+ att_content_data = remove_signature(attachment.content)
+ # If the envelope cannot be removed, the remove_signature returns None, so we skip
+ if not att_content_data:
+ _logger.warning("E-invoice couldn't be read: %s", attachment.fname)
+ continue
+ # Otherwise, it should be an utf-8 encoded XML string
+ else:
+ att_content_data = attachment.content.encode()
self._create_invoice_from_mail(att_content_data, attachment.fname, from_address)
else:
if split_underscore[1] == 'AT':
diff --git a/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml.p7m b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml.p7m
new file mode 100644
index 00000000000..ecb48417c5a
--- /dev/null
+++ b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml.p7m
Binary files differ
diff --git a/addons/l10n_it_edi/tests/test_ir_mail_server.py b/addons/l10n_it_edi/tests/test_ir_mail_server.py
index c6dd9efe989..8e803d9ad56 100644
--- a/addons/l10n_it_edi/tests/test_ir_mail_server.py
+++ b/addons/l10n_it_edi/tests/test_ir_mail_server.py
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import datetime
import logging
from collections import namedtuple
from unittest.mock import patch
from odoo import tools
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature
_logger = logging.getLogger(__name__)
@@ -51,7 +53,9 @@ class PecMailServerTests(AccountEdiTestCommon):
# invoice_filename2 is used for vendor bill tests
cls.invoice_filename1 = 'IT01234567890_FPR01.xml'
cls.invoice_filename2 = 'IT01234567890_FPR02.xml'
+ cls.signed_invoice_filename = 'IT01234567890_FPR01.xml.p7m'
cls.invoice_content = cls._get_test_file_content(cls.invoice_filename1)
+ cls.signed_invoice_content = cls._get_test_file_content(cls.signed_invoice_filename)
cls.invoice = cls.env['account.move'].create({
'move_type': 'in_invoice',
'ref': '01234567890'
@@ -85,6 +89,8 @@ class PecMailServerTests(AccountEdiTestCommon):
def _create_invoice(self, content, filename):
""" Create an invoice from given attachment content """
with patch.object(self.server._cr, 'commit', return_value=None):
+ if filename.endswith(".p7m"):
+ content = remove_signature(content)
return self.server._create_invoice_from_mail(content, filename, 'fake@address.be')
# -----------------------------
@@ -98,6 +104,15 @@ class PecMailServerTests(AccountEdiTestCommon):
invoices = self._create_invoice(self.invoice_content, self.invoice_filename2)
self.assertTrue(bool(invoices))
+ def test_receive_signed_vendor_bill(self):
+ """ Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
+ invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
+ self.assertRecordValues(invoices, [{
+ 'name': 'BILL/2014/12/0001',
+ 'date': datetime.date(2014, 12, 18),
+ 'ref': '01234567890',
+ }])
+
def test_receive_same_vendor_bill_twice(self):
""" Test that the second time we are receiving a PEC mail with the same attachment, the second is discarded """
content = self.fake_test_content.encode()
diff --git a/addons/l10n_it_edi/tools/__init__.py b/addons/l10n_it_edi/tools/__init__.py
new file mode 100644
index 00000000000..8fee2c7329f
--- /dev/null
+++ b/addons/l10n_it_edi/tools/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import remove_signature
diff --git a/addons/l10n_it_edi/tools/remove_signature.py b/addons/l10n_it_edi/tools/remove_signature.py
new file mode 100644
index 00000000000..1816b7fd1a7
--- /dev/null
+++ b/addons/l10n_it_edi/tools/remove_signature.py
@@ -0,0 +1,47 @@
+# -*- coding:utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import warnings
+
+_logger = logging.getLogger(__name__)
+
+try:
+ from OpenSSL import crypto as ssl_crypto
+ import OpenSSL._util as ssl_util
+except ImportError:
+ ssl_crypto = None
+ _logger.warning("Cannot import library 'OpenSSL' for PKCS#7 envelope extraction.")
+
+
+def remove_signature(content):
+ """ Remove the PKCS#7 envelope from given content, making a '.xml.p7m' file content readable as it was '.xml'.
+ As OpenSSL may not be installed, in that case a warning is issued and None is returned. """
+
+ # Prevent using the library if it had import errors
+ if not ssl_crypto:
+ _logger.warning("Error reading the content, check if the OpenSSL library is installed for for PKCS#7 envelope extraction.")
+ return None
+
+ # Load some tools from the library
+ null = ssl_util.ffi.NULL
+ verify = ssl_util.lib.PKCS7_verify
+
+ # By default ignore the validity of the certificates, just validate the structure
+ flags = ssl_util.lib.PKCS7_NOVERIFY | ssl_util.lib.PKCS7_NOSIGS
+
+ # Read the signed data fron the content
+ out_buffer = ssl_crypto._new_mem_buf()
+
+ # This method is deprecated, but there are actually no alternatives
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ loaded_data = ssl_crypto.load_pkcs7_data(ssl_crypto.FILETYPE_ASN1, content)
+
+ # Verify the signature
+ if verify(loaded_data._pkcs7, null, null, null, out_buffer, flags) != 1:
+ ssl_crypto._raise_current_error()
+
+ # Get the content as a byte-string
+ decoded_content = ssl_crypto._bio_to_string(out_buffer)
+ return decoded_content