Skip to content

Commit

Permalink
[FIX] l10n_it_edi: vendor bill is not being created from PEC
Browse files Browse the repository at this point in the history
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#69254

Signed-off-by: Josse Colpaert <jco@openerp.com>
  • Loading branch information
lordkrandel committed Apr 15, 2021
1 parent a2d63e8 commit 1bf2e88
Show file tree
Hide file tree
Showing 7 changed files with 485 additions and 58 deletions.
37 changes: 16 additions & 21 deletions addons/l10n_it_edi/models/account_edi_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,21 @@ def _import_fattura_pa(self, tree, invoice):
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).
Expand All @@ -127,7 +135,7 @@ def _import_fattura_pa(self, tree, invoice):
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:
Expand All @@ -139,21 +147,8 @@ def _import_fattura_pa(self, tree, invoice):
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>
Expand Down
106 changes: 69 additions & 37 deletions addons/l10n_it_edi/models/ir_mail_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import email.policy
import dateutil
import pytz
import base64

from lxml import etree
from datetime import datetime
Expand All @@ -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:
Expand All @@ -47,7 +59,16 @@ def fetch_mail(self):
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:
Expand Down Expand Up @@ -114,7 +135,8 @@ def _attachment_invoice(self, msg_txt):
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
Expand All @@ -123,35 +145,54 @@ def _attachment_invoice(self, msg_txt):
_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
Expand Down Expand Up @@ -183,8 +224,7 @@ def _message_AT_invoice(self, attachment_zip):
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
Expand All @@ -197,8 +237,9 @@ def _message_AT_invoice(self, attachment_zip):
)

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 {}
Expand All @@ -213,9 +254,7 @@ def _message_receipt_invoice(self, receipt_type, attachment):
# 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
Expand All @@ -229,9 +268,7 @@ def _message_receipt_invoice(self, receipt_type, attachment):
# 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
Expand All @@ -249,9 +286,7 @@ def _message_receipt_invoice(self, receipt_type, attachment):
# 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
Expand All @@ -274,9 +309,7 @@ def _message_receipt_invoice(self, receipt_type, attachment):
# 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
Expand Down Expand Up @@ -316,8 +349,7 @@ def _message_receipt_invoice(self, receipt_type, attachment):
# 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
Expand Down
4 changes: 4 additions & 0 deletions addons/l10n_it_edi/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions addons/l10n_it_edi/tests/expected_xmls/IT01234567890_FPR01.xml
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 1bf2e88

Please sign in to comment.