diff options
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') |
