summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaolo (pgi) <pgi@odoo.com>2021-04-07 13:12:33 +0000
committerPaolo (pgi) <pgi@odoo.com>2021-04-15 09:17:16 +0000
commit1bf2e8805503d9bbebec58df9933abab3db70bd2 (patch)
treee2cf70fece31cb55b186cb463166f96afa51e4bf
parenta2d63e8ae5d6bc0e822d12d724b5e3ff5a979cad (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, nothing happened. The e-invoice related vendor bill should be automatically created when the xml file is fetched from the PEC mailbox instead. - There was a traceback when account_move.l10n_it_einvoice_name was searched because it's a non-stored computed field. - Also, the field name account_move.source_email was used, but it was renamed to invoice_source_email. - The xml parsing etree.fromstring() was incorrectly used with string arguments instead of bytes. - The context was mistakenly initialized twice on invoice creation. Tests have been provided for the email reading methods. opw-2460485 closes odoo/odoo#69254 Signed-off-by: Josse Colpaert <jco@openerp.com>
-rw-r--r--addons/l10n_it_edi/models/account_edi_format.py37
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py106
-rw-r--r--addons/l10n_it_edi/tests/__init__.py4
-rw-r--r--addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml113
-rw-r--r--addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_DT_001.xml71
-rw-r--r--addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_RC_001.xml72
-rw-r--r--addons/l10n_it_edi/tests/test_ir_mail_server.py140
7 files changed, 485 insertions, 58 deletions
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index 6fccb89c706..58abd2ad12d 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -109,13 +109,21 @@ class AccountEdiFormat(models.Model):
invoice = self.env['account.move']
first_run = False
+ # Refund type.
+ # TD01 == invoice
+ # TD02 == advance/down payment on invoice
+ # TD03 == advance/down payment on fee
+ # TD04 == credit note
+ # TD05 == debit note
+ # TD06 == fee
+ # For unsupported document types, just assume in_invoice, and log that the type is unsupported
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
- if elements and elements[0].text and elements[0].text == 'TD01':
- self_ctx = invoice.with_context(default_move_type='in_invoice')
- elif elements and elements[0].text and elements[0].text == 'TD04':
- self_ctx = invoice.with_context(default_move_type='in_refund')
- else:
- _logger.info('Document type not managed: %s.', elements[0].text)
+ move_type = 'in_invoice'
+ if elements and elements[0].text and elements[0].text == 'TD04':
+ move_type = 'in_refund'
+ elif elements and elements[0].text and elements[0].text != 'TD01':
+ _logger.info('Document type not managed: %s. Invoice type is set by default.', elements[0].text)
+ invoice_ctx = invoice.with_context(default_move_type=move_type)
# type must be present in the context to get the right behavior of the _default_journal method (account.move).
# journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
@@ -127,7 +135,7 @@ class AccountEdiFormat(models.Model):
company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
if company:
- self_ctx = self_ctx.with_context(company_id=company.id)
+ invoice_ctx = invoice_ctx.with_context(company_id=company.id)
else:
company = self.env.company
if elements:
@@ -139,21 +147,8 @@ class AccountEdiFormat(models.Model):
if self.env.company != company:
raise UserError(_("You can only import invoice concern your current company: %s", self.env.company.display_name))
- # Refund type.
- # TD01 == invoice
- # TD02 == advance/down payment on invoice
- # TD03 == advance/down payment on fee
- # TD04 == credit note
- # TD05 == debit note
- # TD06 == fee
- elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
- if elements and elements[0].text and elements[0].text == 'TD01':
- move_type = 'in_invoice'
- elif elements and elements[0].text and elements[0].text == 'TD04':
- move_type = 'in_refund'
# move could be a single record (editing) or be empty (new).
- with Form(invoice.with_context(default_move_type=move_type,
- account_predictive_bills_disable_prediction=True)) as invoice_form:
+ with Form(invoice_ctx.with_context(account_predictive_bills_disable_prediction=True)) as invoice_form:
message_to_log = []
# Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index 1e59cbaf02d..57f6eb6cb1b 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -9,7 +9,6 @@ import email
import email.policy
import dateutil
import pytz
-import base64
from lxml import etree
from datetime import datetime
@@ -28,6 +27,19 @@ class FetchmailServer(models.Model):
l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.")
l10n_it_last_uid = fields.Integer(string='Last message UID', default=1)
+ def _search_edi_invoice(self, att_name, send_state=False):
+ """ Search sent l10n_it_edi fatturaPA invoices """
+
+ conditions = [
+ ('move_id', "!=", False),
+ ('edi_format_id.code', '=', 'fattura_pa'),
+ ('attachment_id.name', '=', att_name),
+ ]
+ if send_state:
+ conditions.append(('move_id.l10n_it_send_state', '=', send_state))
+
+ return self.env['account.edi.document'].search(conditions, limit=1).move_id
+
@api.constrains('l10n_it_is_pec', 'server_type')
def _check_pec(self):
for record in self:
@@ -47,7 +59,16 @@ class FetchmailServer(models.Model):
imap_server = server.connect()
imap_server.select()
- result, data = imap_server.uid('search', None, '(FROM "@pec.fatturapa.it")', '(UID %s:*)' % (server.l10n_it_last_uid))
+ # Only download new emails
+ email_filter = ['(UID %s:*)' % (server.l10n_it_last_uid)]
+
+ # The l10n_it_edi.fatturapa_bypass_incoming_address_filter prevents the sender address check on incoming email.
+ bypass_incoming_address_filter = self.env['ir.config_parameter'].get_param('l10n_it_edi.bypass_incoming_address_filter', False)
+ if not bypass_incoming_address_filter:
+ email_filter.append('(FROM "@pec.fatturapa.it")')
+
+ data = imap_server.uid('search', None, *email_filter)[1]
+
new_max_uid = server.l10n_it_last_uid
for uid in data[0].split():
if int(uid) <= server.l10n_it_last_uid:
@@ -114,7 +135,8 @@ class FetchmailServer(models.Model):
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
- self._create_invoice_from_mail(attachment.content, attachment.fname, from_address)
+ att_content_data = attachment.content.encode()
+ self._create_invoice_from_mail(att_content_data, attachment.fname, from_address)
else:
if split_underscore[1] == 'AT':
# Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito
@@ -123,35 +145,54 @@ class FetchmailServer(models.Model):
_logger.info('New E-invoice in zip file: %s', attachment.fname)
self._create_invoice_from_mail_with_zip(attachment, from_address)
- def _create_invoice_from_mail(self, att_content, att_name, from_address):
- if self.env['account.move'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1):
- # invoice already exist
+ def _create_invoice_from_mail(self, att_content_data, att_name, from_address):
+ """ Creates an invoice from the content of an email present in ir.attachments
+
+ :param att_content_data: The 'utf-8' encoded bytes string representing the content of the attachment.
+ :param att_name: The attachment's file name.
+ :param from_address: The sender address of the email.
+ """
+
+ invoices = self.env['account.move']
+
+ # Check if we already imported the email as an attachment
+ existing = self.env['ir.attachment'].search([('name', '=', att_name), ('res_model', '=', 'account.move')])
+ if existing:
_logger.info('E-invoice already exist: %s', att_name)
- return
+ return invoices
- invoice_attachment = self.env['ir.attachment'].create({
- 'name': att_name,
- 'datas': base64.encodebytes(att_content),
- 'type': 'binary',
- })
+ # Create the new attachment for the file
+ self.env['ir.attachment'].create({
+ 'name': att_name,
+ 'raw': att_content_data,
+ 'res_model': 'account.move',
+ 'type': 'binary'})
+ # Decode the file.
try:
- tree = etree.fromstring(att_content)
+ tree = etree.fromstring(att_content_data)
except Exception:
- raise UserError(_('The xml file is badly formatted : {}').format(att_name))
+ _logger.info('The xml file is badly formatted: %s', att_name)
+ return invoices
- invoice = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
- invoice.l10n_it_send_state = "new"
- invoice.source_email = from_address
- self._cr.commit()
+ # Create the invoice
+ invoices = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
+ if not invoices:
+ _logger.info('E-invoice not found in file: %s', att_name)
+ return invoices
- _logger.info('New E-invoice: %s', att_name)
+ invoices.l10n_it_send_state = 'new'
+ invoices.invoice_source_email = from_address
+ self._cr.commit()
+ _logger.info('New E-invoices (%s), ids: %s', att_name, [x.id for x in invoices])
+ return invoices
def _create_invoice_from_mail_with_zip(self, attachment_zip, from_address):
with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z:
for att_name in z.namelist():
- if self.env['account.move'].search([('l10n_it_einvoice_name', '=', att_name)], limit=1):
+ existing = self.env['ir.attachment'].search([('name', '=', att_name), ('res_model', '=', 'account.move')])
+ if existing:
# invoice already exist
_logger.info('E-invoice in zip file (%s) already exist: %s', attachment_zip.fname, att_name)
continue
@@ -183,8 +224,7 @@ class FetchmailServer(models.Model):
else:
return
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename)])
+ related_invoice = self._search_edi_invoice(filename)
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', filename)
return
@@ -197,8 +237,9 @@ class FetchmailServer(models.Model):
)
def _message_receipt_invoice(self, receipt_type, attachment):
+
try:
- tree = etree.fromstring(attachment.content)
+ tree = etree.fromstring(attachment.content.encode())
except:
_logger.info('Error in decoding new receipt file: %s', attachment.fname)
return {}
@@ -213,9 +254,7 @@ class FetchmailServer(models.Model):
# Delivery receipt
# This is the receipt sent by the ES to the transmitting subject to communicate
# delivery of the file to the addressee
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename),
- ('l10n_it_send_state', '=', 'sent')])
+ related_invoice = self._search_edi_invoice(filename, 'sent')
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
@@ -229,9 +268,7 @@ class FetchmailServer(models.Model):
# Rejection notice
# This is the receipt sent by the ES to the transmitting subject if one or more of
# the checks carried out by the ES on the file received do not have a successful result.
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename),
- ('l10n_it_send_state', '=', 'sent')])
+ related_invoice = self._search_edi_invoice(filename, 'sent')
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
@@ -249,9 +286,7 @@ class FetchmailServer(models.Model):
# Failed delivery notice
# This is the receipt sent by the ES to the transmitting subject if the file is not
# delivered to the addressee.
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename),
- ('l10n_it_send_state', '=', 'sent')])
+ related_invoice = self._search_edi_invoice(filename, 'sent')
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
@@ -274,9 +309,7 @@ class FetchmailServer(models.Model):
# This is the receipt sent by the ES to the invoice sender to communicate the result
# (acceptance or refusal of the invoice) of the checks carried out on the document by
# the addressee.
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename),
- ('l10n_it_send_state', '=', 'delivered')])
+ related_invoice = self._search_edi_invoice(filename, 'delivered')
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
@@ -316,8 +349,7 @@ class FetchmailServer(models.Model):
# This is the receipt sent by the ES to both the invoice sender and the invoice
# addressee to communicate the expiry of the maximum term for communication of
# acceptance/refusal.
- related_invoice = self.env['account.move'].search([
- ('l10n_it_einvoice_name', '=', filename), ('l10n_it_send_state', '=', 'delivered')])
+ related_invoice = self._search_edi_invoice(filename, 'delivered')
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
diff --git a/addons/l10n_it_edi/tests/__init__.py b/addons/l10n_it_edi/tests/__init__.py
new file mode 100644
index 00000000000..c2bcfc21455
--- /dev/null
+++ b/addons/l10n_it_edi/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_ir_mail_server
diff --git a/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml
new file mode 100644
index 00000000000..936b6d4abc3
--- /dev/null
+++ b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<p:FatturaElettronica versione="FPR12" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
+xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd">
+ <FatturaElettronicaHeader>
+ <DatiTrasmissione>
+ <IdTrasmittente>
+ <IdPaese>IT</IdPaese>
+ <IdCodice>01234560157</IdCodice>
+ </IdTrasmittente>
+ <ProgressivoInvio>00001</ProgressivoInvio>
+ <FormatoTrasmissione>FPR12</FormatoTrasmissione>
+ <CodiceDestinatario>ABC1234</CodiceDestinatario>
+ <ContattiTrasmittente/>
+ </DatiTrasmissione>
+ <CedentePrestatore>
+ <DatiAnagrafici>
+ <IdFiscaleIVA>
+ <IdPaese>IT</IdPaese>
+ <IdCodice>01234560157</IdCodice>
+ </IdFiscaleIVA>
+ <Anagrafica>
+ <Denominazione>SOCIETA' ALPHA SRL</Denominazione>
+ </Anagrafica>
+ <RegimeFiscale>RF19</RegimeFiscale>
+ </DatiAnagrafici>
+ <Sede>
+ <Indirizzo>VIALE ROMA 543</Indirizzo>
+ <CAP>07100</CAP>
+ <Comune>SASSARI</Comune>
+ <Provincia>SS</Provincia>
+ <Nazione>IT</Nazione>
+ </Sede>
+ </CedentePrestatore>
+ <CessionarioCommittente>
+ <DatiAnagrafici>
+ <CodiceFiscale>01234560157</CodiceFiscale>
+ <Anagrafica>
+ <Denominazione>DITTA BETA</Denominazione>
+ </Anagrafica>
+ </DatiAnagrafici>
+ <Sede>
+ <Indirizzo>VIA TORINO 38-B</Indirizzo>
+ <CAP>00145</CAP>
+ <Comune>ROMA</Comune>
+ <Provincia>RM</Provincia>
+ <Nazione>IT</Nazione>
+ </Sede>
+ </CessionarioCommittente>
+ </FatturaElettronicaHeader>
+ <FatturaElettronicaBody>
+ <DatiGenerali>
+ <DatiGeneraliDocumento>
+ <TipoDocumento>TD01</TipoDocumento>
+ <Divisa>EUR</Divisa>
+ <Data>2014-12-18</Data>
+ <Numero>01234567890</Numero>
+ <Causale>LA FATTURA FA RIFERIMENTO AD UNA OPERAZIONE AAAA BBBBBBBBBBBBBBBBBB CCC DDDDDDDDDDDDDDD E FFFFFFFFFFFFFFFFFFFF GGGGGGGGGG HHHHHHH II LLLLLLLLLLLLLLLLL MMM NNNNN OO PPPPPPPPPPP QQQQ RRRR SSSSSSSSSSSSSS</Causale>
+ <Causale>SEGUE DESCRIZIONE CAUSALE NEL CASO IN CUI NON SIANO STATI SUFFICIENTI 200 CARATTERI AAAAAAAAAAA BBBBBBBBBBBBBBBBB</Causale>
+ </DatiGeneraliDocumento>
+ <DatiOrdineAcquisto>
+ <RiferimentoNumeroLinea>1</RiferimentoNumeroLinea>
+ <IdDocumento>66685</IdDocumento>
+ <NumItem>1</NumItem>
+ </DatiOrdineAcquisto>
+ <DatiContratto>
+ <RiferimentoNumeroLinea>1</RiferimentoNumeroLinea>
+ <IdDocumento>01234567890</IdDocumento>
+ <Data>2012-09-01</Data>
+ <NumItem>5</NumItem>
+ <CodiceCUP>01234567890abc</CodiceCUP>
+ <CodiceCIG>456def</CodiceCIG>
+ </DatiContratto>
+ <DatiTrasporto>
+ <DatiAnagraficiVettore>
+ <IdFiscaleIVA>
+ <IdPaese>IT</IdPaese>
+ <IdCodice>24681012141</IdCodice>
+ </IdFiscaleIVA>
+ <Anagrafica>
+ <Denominazione>Trasporto spa</Denominazione>
+ </Anagrafica>
+ </DatiAnagraficiVettore>
+ <DataOraConsegna>2012-10-22T16:46:12.000+02:00</DataOraConsegna>
+ </DatiTrasporto>
+ </DatiGenerali>
+ <DatiBeniServizi>
+ <DettaglioLinee>
+ <NumeroLinea>1</NumeroLinea>
+ <Descrizione>DESCRIZIONE DELLA FORNITURA</Descrizione>
+ <Quantita>5.00</Quantita>
+ <PrezzoUnitario>1.00</PrezzoUnitario>
+ <PrezzoTotale>5.00</PrezzoTotale>
+ <AliquotaIVA>22.00</AliquotaIVA>
+ </DettaglioLinee>
+ <DatiRiepilogo>
+ <AliquotaIVA>22.00</AliquotaIVA>
+ <ImponibileImporto>5.00</ImponibileImporto>
+ <Imposta>1.10</Imposta>
+ <EsigibilitaIVA>I</EsigibilitaIVA>
+ </DatiRiepilogo>
+ </DatiBeniServizi>
+ <DatiPagamento>
+ <CondizioniPagamento>TP01</CondizioniPagamento>
+ <DettaglioPagamento>
+ <ModalitaPagamento>MP01</ModalitaPagamento>
+ <DataScadenzaPagamento>2015-01-30</DataScadenzaPagamento>
+ <ImportoPagamento>6.10</ImportoPagamento>
+ </DettaglioPagamento>
+ </DatiPagamento>
+ </FatturaElettronicaBody>
+</p:FatturaElettronica>
diff --git a/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_DT_001.xml b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_DT_001.xml
new file mode 100644
index 00000000000..10788771960
--- /dev/null
+++ b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_DT_001.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="DT_v1.0.xsl"?>
+<types:NotificaDecorrenzaTermini xmlns:types="http://www.fatturapa.gov.it/sdi/messaggi/v1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IntermediarioConDupliceRuolo="Si" versione="1.0" xsi:schemaLocation="http://www.fatturapa.gov.it/sdi/messaggi/v1.0 MessaggiTypes_v1.0.xsd http://www.w3.org/2000/09/xmldsig# xmldsig-core-schema.xsd">
+ <IdentificativoSdI>111</IdentificativoSdI>
+ <NomeFile>IT01234567890_FPR01.xml</NomeFile>
+ <Descrizione>Notifica di esempio</Descrizione>
+ <MessageId>123456</MessageId>
+ <Note>Esempio</Note>
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature1">
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+ <ds:Reference Id="reference-document" URI="">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">
+ <XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="subtract">/descendant::ds:Signature</XPath>
+ </ds:Transform>
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>g6h8KnGd+Y4DCdnGk5oIUbBwjJB3MMGlyizaFyCqH7I=</ds:DigestValue>
+ </ds:Reference>
+ <ds:Reference Id="reference-signedpropeties" Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignedProperties_1">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>LkOlfB97QK/evb7mYg+KkxW3BSiZre63y3Qeh/rV28E=</ds:DigestValue>
+ </ds:Reference>
+ <ds:Reference Id="reference-keyinfo" URI="#KeyInfoId">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>BaZyFTXyxM8aIJhtiemem1lEwKR75ksXb33lsMqD89w=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue Id="SignatureValue1">
+Z8/Kt/ZF/syaHxYr6/qoTz+nTJe3IV1m9Hj3WPOl1CZ/p5intUORW0IinpMum4rvPkLYpKPVbi39
+WCJujEqVOVFw5xezZlwmrRghmUeyTyKazK7mKEEMXCad+FGCZj2Gz1nkqi5aNyNX/lN7m9Ix7rZ8
+br3Fi3bi3nNMdyUmwog=
+</ds:SignatureValue>
+ <ds:KeyInfo Id="KeyInfoId">
+ <ds:X509Data>
+ <ds:X509Certificate>
+MIIEYDCCA0igAwIBAgIDEIgbMA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNVBAYTAklUMR4wHAYDVQQK
+ExVBZ2VuemlhIGRlbGxlIEVudHJhdGUxGzAZBgNVBAsTElNlcnZpemkgVGVsZW1hdGljaTEhMB8G
+A1UEAxMYQ0EgQWdlbnppYSBkZWxsZSBFbnRyYXRlMB4XDTExMDcwNDEzMTkyNFoXDTE0MDcwNDEz
+MTkyM1owdDELMAkGA1UEBhMCSVQxHjAcBgNVBAoTFUFnZW56aWEgZGVsbGUgRW50cmF0ZTEbMBkG
+A1UECxMSU2Vydml6aSBUZWxlbWF0aWNpMSgwJgYDVQQDEx9TaXN0ZW1hIEludGVyc2NhbWJpbyBG
+YXR0dXJhIFBBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMxOQj1dj6xgQBwB/S5naHvVqP
+FL25Y3GnAulrcaeO8ZFFK5fWKPgiBwfyJ7qdlzn/RF7y+w92XLgh9zROmNlIjsJcp3rRwsAiKjuW
+CkqwVXE0/Qtvxpo2Eovk1SV4+rf+7WKSHtabjmWXbM2FVccyN2AOvfR4WAdpr4hHkoEIiwIDAQAB
+o4IBhDCCAYAwDgYDVR0PAQH/BAQDAgZAMIGZBgNVHSMEgZEwgY6AFOpEPx8Z4zc+q6qUgqWf6/wW
+un+1oXGkbzBtMQswCQYDVQQGEwJJVDEeMBwGA1UEChMVQWdlbnppYSBkZWxsZSBFbnRyYXRlMRsw
+GQYDVQQLExJTZXJ2aXppIFRlbGVtYXRpY2kxITAfBgNVBAMTGENBIEFnZW56aWEgZGVsbGUgRW50
+cmF0ZYIDEGJwMIGyBgNVHR8EgaowgacwgaSggaGggZ6GgZtsZGFwOi8vY2Fkcy5lbnRyYXRlLmZp
+bmFuemUuaXQvY24lM2RDQSUyMEFnZW56aWElMjBkZWxsZSUyMEVudHJhdGUsb3UlM2RTZXJ2aXpp
+JTIwVGVsZW1hdGljaSxvJTNkQWdlbnppYSUyMGRlbGxlJTIwRW50cmF0ZSxjJTNkaXQ/Y2VydGlm
+aWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHQ4EFgQUn+JY07NI6xlrCUXERiHoFFN66dkwDQYJKoZI
+hvcNAQEFBQADggEBALZ0po2uLhLyZ8uiVfQUCAQd8s5o8ZJw2mcgZc/iaoNmDfcslZnTLWeuT6Gr
+UFgG0uc1rY0UwWx/R1UOyc0ZesRo7Z6+kFmVubT1tbjLMuLjjUIyt4zWeNjf4PwNS0+s6Y6eC8tx
+fOJmQNGQIbujWhAejoIteG01ciGeeII6AMnGK8KvbCA0UZmWl3Bou49zWajiEjtHFGkq/WNfDwRa
+Fd4UWjR+UWS3rLahV7iOfh/+Yy7h1F0RzQuPJk7TCm7iHyc9QtgwxHHCmknRyNXMv6DeTOfK8ciq
+uFWd6DasmblXLUm+uqhsWVRIkj2Bz63bpjuJU+8ptRfxHrVnzyCr9M4=
+</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ <ds:Object>
+ <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature1">
+ <xades:SignedProperties Id="SignedProperties_1">
+ <xades:SignedSignatureProperties>
+ <xades:SigningTime>2014-06-05T14:27:51Z</xades:SigningTime>
+ </xades:SignedSignatureProperties>
+ </xades:SignedProperties>
+ </xades:QualifyingProperties>
+ </ds:Object>
+ </ds:Signature>
+</types:NotificaDecorrenzaTermini>
diff --git a/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_RC_001.xml b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_RC_001.xml
new file mode 100644
index 00000000000..ab0725e4023
--- /dev/null
+++ b/addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01_RC_001.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="RC_v1.0.xsl"?>
+<types:RicevutaConsegna xmlns:types="http://www.fatturapa.gov.it/sdi/messaggi/v1.0" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IntermediarioConDupliceRuolo="Si" versione="1.0" xsi:schemaLocation="http://www.fatturapa.gov.it/sdi/messaggi/v1.0 MessaggiTypes_v1.0.xsd ">
+ <IdentificativoSdI>111</IdentificativoSdI>
+ <NomeFile>IT01234567890_FPR01.xml</NomeFile>
+ <DataOraRicezione>2013-06-06T12:00:00Z</DataOraRicezione>
+ <DataOraConsegna>2013-06-06T12:01:00Z</DataOraConsegna>
+ <Destinatario>
+ <Codice>AAA111</Codice>
+ <Descrizione>Amministrazione di prova</Descrizione>
+ </Destinatario>
+ <MessageId>123456</MessageId>
+ <Note>Esempio</Note>
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature1">
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+ <ds:Reference Id="reference-document" URI="">
+ <ds:Transforms>
+ <ds:Transform Algorithm="http://www.w3.org/2002/06/xmldsig-filter2">
+ <XPath xmlns="http://www.w3.org/2002/06/xmldsig-filter2" Filter="subtract">/descendant::ds:Signature</XPath>
+ </ds:Transform>
+ </ds:Transforms>
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>c+5ntDV6t4+PxIKEU6rbCUGu3ne9RMxoADu4yK4XIak=</ds:DigestValue>
+ </ds:Reference>
+ <ds:Reference Id="reference-signedpropeties" Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignedProperties_1">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>AhiGZ+LPENybg4dQwMwjg0Nxdxzu+3M5i0w+UI6X89E=</ds:DigestValue>
+ </ds:Reference>
+ <ds:Reference Id="reference-keyinfo" URI="#KeyInfoId">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>BaZyFTXyxM8aIJhtiemem1lEwKR75ksXb33lsMqD89w=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue Id="SignatureValue1">G0FOBC+E8JKtJ5K2C+LBlvv3oarzkub7w2q5U1UQZnobWmFBbZ4WzgBNTMKUjdi2ZLkUpOSEwedf
+VLgl5SyhaKYY6TizDNbxededjUpqKhyIgaWBLc/iDI6H//x+3axnLU4WwFzdr3AwqPQjPuugGX07
+gOcjBHtbr7ie2Wr//o8=</ds:SignatureValue>
+ <ds:KeyInfo Id="KeyInfoId">
+ <ds:X509Data>
+ <ds:X509Certificate>MIIEYDCCA0igAwIBAgIDEIgbMA0GCSqGSIb3DQEBBQUAMG0xCzAJBgNVBAYTAklUMR4wHAYDVQQK
+ExVBZ2VuemlhIGRlbGxlIEVudHJhdGUxGzAZBgNVBAsTElNlcnZpemkgVGVsZW1hdGljaTEhMB8G
+A1UEAxMYQ0EgQWdlbnppYSBkZWxsZSBFbnRyYXRlMB4XDTExMDcwNDEzMTkyNFoXDTE0MDcwNDEz
+MTkyM1owdDELMAkGA1UEBhMCSVQxHjAcBgNVBAoTFUFnZW56aWEgZGVsbGUgRW50cmF0ZTEbMBkG
+A1UECxMSU2Vydml6aSBUZWxlbWF0aWNpMSgwJgYDVQQDEx9TaXN0ZW1hIEludGVyc2NhbWJpbyBG
+YXR0dXJhIFBBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMxOQj1dj6xgQBwB/S5naHvVqP
+FL25Y3GnAulrcaeO8ZFFK5fWKPgiBwfyJ7qdlzn/RF7y+w92XLgh9zROmNlIjsJcp3rRwsAiKjuW
+CkqwVXE0/Qtvxpo2Eovk1SV4+rf+7WKSHtabjmWXbM2FVccyN2AOvfR4WAdpr4hHkoEIiwIDAQAB
+o4IBhDCCAYAwDgYDVR0PAQH/BAQDAgZAMIGZBgNVHSMEgZEwgY6AFOpEPx8Z4zc+q6qUgqWf6/wW
+un+1oXGkbzBtMQswCQYDVQQGEwJJVDEeMBwGA1UEChMVQWdlbnppYSBkZWxsZSBFbnRyYXRlMRsw
+GQYDVQQLExJTZXJ2aXppIFRlbGVtYXRpY2kxITAfBgNVBAMTGENBIEFnZW56aWEgZGVsbGUgRW50
+cmF0ZYIDEGJwMIGyBgNVHR8EgaowgacwgaSggaGggZ6GgZtsZGFwOi8vY2Fkcy5lbnRyYXRlLmZp
+bmFuemUuaXQvY24lM2RDQSUyMEFnZW56aWElMjBkZWxsZSUyMEVudHJhdGUsb3UlM2RTZXJ2aXpp
+JTIwVGVsZW1hdGljaSxvJTNkQWdlbnppYSUyMGRlbGxlJTIwRW50cmF0ZSxjJTNkaXQ/Y2VydGlm
+aWNhdGVSZXZvY2F0aW9uTGlzdDAdBgNVHQ4EFgQUn+JY07NI6xlrCUXERiHoFFN66dkwDQYJKoZI
+hvcNAQEFBQADggEBALZ0po2uLhLyZ8uiVfQUCAQd8s5o8ZJw2mcgZc/iaoNmDfcslZnTLWeuT6Gr
+UFgG0uc1rY0UwWx/R1UOyc0ZesRo7Z6+kFmVubT1tbjLMuLjjUIyt4zWeNjf4PwNS0+s6Y6eC8tx
+fOJmQNGQIbujWhAejoIteG01ciGeeII6AMnGK8KvbCA0UZmWl3Bou49zWajiEjtHFGkq/WNfDwRa
+Fd4UWjR+UWS3rLahV7iOfh/+Yy7h1F0RzQuPJk7TCm7iHyc9QtgwxHHCmknRyNXMv6DeTOfK8ciq
+uFWd6DasmblXLUm+uqhsWVRIkj2Bz63bpjuJU+8ptRfxHrVnzyCr9M4=</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ <ds:Object>
+ <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature1">
+ <xades:SignedProperties Id="SignedProperties_1">
+ <xades:SignedSignatureProperties>
+ <xades:SigningTime>2014-06-05T14:24:28Z</xades:SigningTime>
+ </xades:SignedSignatureProperties>
+ </xades:SignedProperties>
+ </xades:QualifyingProperties>
+ </ds:Object>
+ </ds:Signature>
+</types:RicevutaConsegna>
diff --git a/addons/l10n_it_edi/tests/test_ir_mail_server.py b/addons/l10n_it_edi/tests/test_ir_mail_server.py
new file mode 100644
index 00000000000..c6dd9efe989
--- /dev/null
+++ b/addons/l10n_it_edi/tests/test_ir_mail_server.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from collections import namedtuple
+from unittest.mock import patch
+
+from odoo import tools
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+
+_logger = logging.getLogger(__name__)
+
+
+class PecMailServerTests(AccountEdiTestCommon):
+ """ Main test class for the l10n_it_edi vendor bills XML import from a PEC mail account"""
+
+ fake_test_content = """<?xml version="1.0" encoding="UTF-8"?>
+ <p:FatturaElettronica versione="FPR12" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd">
+ <FatturaElettronicaHeader>
+ <CessionarioCommittente>
+ <DatiAnagrafici>
+ <CodiceFiscale>01234560157</CodiceFiscale>
+ </DatiAnagrafici>
+ </CessionarioCommittente>
+ </FatturaElettronicaHeader>
+ <FatturaElettronicaBody>
+ <DatiGenerali>
+ <DatiGeneraliDocumento>
+ <TipoDocumento>TD02</TipoDocumento>
+ </DatiGeneraliDocumento>
+ </DatiGenerali>
+ </FatturaElettronicaBody>
+ </p:FatturaElettronica>"""
+
+ @classmethod
+ def setUpClass(cls):
+ """ Setup the test class with a PEC mail server and a fake fatturaPA content """
+
+ super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic',
+ edi_format_ref='l10n_it_edi.edi_fatturaPA')
+
+ # Initialize the company's codice fiscale
+ cls.company = cls.company_data['company']
+ cls.company.l10n_it_codice_fiscale = 'IT01234560157'
+
+ # Build test data.
+ # invoice_filename1 is used for vendor bill receipts tests
+ # invoice_filename2 is used for vendor bill tests
+ cls.invoice_filename1 = 'IT01234567890_FPR01.xml'
+ cls.invoice_filename2 = 'IT01234567890_FPR02.xml'
+ cls.invoice_content = cls._get_test_file_content(cls.invoice_filename1)
+ cls.invoice = cls.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'ref': '01234567890'
+ })
+ cls.attachment = cls.env['ir.attachment'].create({
+ 'name': cls.invoice_filename1,
+ 'raw': cls.invoice_content,
+ 'res_id': cls.invoice.id,
+ 'res_model': 'account.move',
+ })
+ cls.edi_document = cls.env['account.edi.document'].create({
+ 'edi_format_id': cls.edi_format.id,
+ 'move_id': cls.invoice.id,
+ 'attachment_id': cls.attachment.id,
+ 'state': 'sent'
+ })
+
+ # Initialize the fetchmail server that has to be tested
+ cls.server = cls.env['fetchmail.server'].sudo().create({
+ 'name': 'test_server',
+ 'server_type': 'imap',
+ 'l10n_it_is_pec': True})
+
+ @classmethod
+ def _get_test_file_content(cls, filename):
+ """ Get the content of a test file inside this module """
+ path = 'l10n_it_edi/tests/expected_xmls/' + filename
+ with tools.file_open(path, mode='rb') as test_file:
+ return test_file.read()
+
+ def _create_invoice(self, content, filename):
+ """ Create an invoice from given attachment content """
+ with patch.object(self.server._cr, 'commit', return_value=None):
+ return self.server._create_invoice_from_mail(content, filename, 'fake@address.be')
+
+ # -----------------------------
+ #
+ # Vendor bills
+ #
+ # -----------------------------
+
+ def test_receive_vendor_bill(self):
+ """ Test a sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
+ invoices = self._create_invoice(self.invoice_content, self.invoice_filename2)
+ self.assertTrue(bool(invoices))
+
+ 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()
+ for result in [True, False]:
+ invoice = self._create_invoice(content, self.invoice_filename2)
+ self.assertEqual(result, bool(invoice))
+
+ # -----------------------------
+ #
+ # Receipts
+ #
+ # -----------------------------
+
+ def _test_receipt(self, receipt_type, source_state, destination_state):
+ """ Test a receipt from the ones in the module's test files """
+
+ # Simulate the 'sent' state of the move, even if we didn't actually send an email in this test
+ self.invoice.l10n_it_send_state = source_state
+
+ # Create a fake receipt from the test file
+ receipt_filename = 'IT01234567890_FPR01_%s_001.xml' % receipt_type
+ receipt_content = self._get_test_file_content(receipt_filename).decode()
+
+ create_mail_attachment = namedtuple('Attachment', ('fname', 'content', 'info'))
+ receipt_mail_attachment = create_mail_attachment(receipt_filename, receipt_content, {})
+
+ # Simulate the arrival of the receipt
+ with patch.object(self.server._cr, 'commit', return_value=None):
+ self.server._message_receipt_invoice(receipt_type, receipt_mail_attachment)
+
+ # Check the Destination state of the edi_document
+ self.assertTrue(destination_state, self.edi_document.state)
+
+ def test_ricevuta_consegna(self):
+ """ Test a receipt adapted from https://www.fatturapa.gov.it/export/documenti/messaggi/v1.0/IT01234567890_11111_RC_001.xml """
+ self._test_receipt('RC', 'sent', 'delivered')
+
+ def test_decorrenza_termini(self):
+ """ Test a receipt adapted from https://www.fatturapa.gov.it/export/documenti/messaggi/v1.0/IT01234567890_11111_DT_001.xml """
+ self._test_receipt('DT', 'delivered', 'delivered_expired')