Files
odoo_source/addons/l10n_it_edi/models/ir_mail_server.py
T
Martin Trigaux e433bc57ff [FIX] *: rephrase, correct typos
Courtesy of Transifex's translators for reporting bad/unclear sentences.

closes odoo/odoo#61804

X-original-commit: f5d0f4a1ebf406583889e9e04912d40c0693343e
Related: odoo/enterprise#14769
Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
2020-11-16 16:47:28 +00:00

372 lines
18 KiB
Python

# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import zipfile
import io
import re
import logging
import email
import email.policy
import dateutil
import pytz
import base64
from lxml import etree
from datetime import datetime
from xmlrpc import client as xmlrpclib
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError, UserError
_logger = logging.getLogger(__name__)
class FetchmailServer(models.Model):
_name = 'fetchmail.server'
_inherit = 'fetchmail.server'
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)
@api.constrains('l10n_it_is_pec', 'server_type')
def _check_pec(self):
for record in self:
if record.l10n_it_is_pec and record.server_type != 'imap':
raise ValidationError(_("PEC mail server must be of type IMAP."))
def fetch_mail(self):
""" WARNING: meant for cron usage only - will commit() after each email! """
MailThread = self.env['mail.thread']
for server in self.filtered(lambda s: s.l10n_it_is_pec):
_logger.info('start checking for new emails on %s PEC server %s', server.server_type, server.name)
count, failed = 0, 0
imap_server = None
try:
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))
new_max_uid = server.l10n_it_last_uid
for uid in data[0].split():
if int(uid) <= server.l10n_it_last_uid:
# We get always minimum 1 message. If no new message, we receive the newest already managed.
continue
result, data = imap_server.uid('fetch', uid, '(RFC822)')
if not data[0]:
continue
message = data[0][1]
# To leave the mail in the state in which they were.
if "Seen" not in data[1].decode("utf-8"):
imap_server.uid('STORE', uid, '+FLAGS', '\\Seen')
else:
imap_server.uid('STORE', uid, '-FLAGS', '\\Seen')
# See details in message_process() in mail_thread.py
if isinstance(message, xmlrpclib.Binary):
message = bytes(message.data)
if isinstance(message, str):
message = message.encode('utf-8')
msg_txt = email.message_from_bytes(message, policy=email.policy.SMTP)
try:
self._attachment_invoice(msg_txt)
new_max_uid = max(new_max_uid, int(uid))
except Exception:
_logger.info('Failed to process mail from %s server %s.', server.server_type, server.name, exc_info=True)
failed += 1
self._cr.commit()
count += 1
server.write({'l10n_it_last_uid': new_max_uid})
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.server_type, server.name, (count - failed), failed)
except Exception:
_logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type, server.name, exc_info=True)
finally:
if imap_server:
imap_server.close()
imap_server.logout()
server.write({'date': fields.Datetime.now()})
return super(FetchmailServer, self.filtered(lambda s: not s.l10n_it_is_pec)).fetch_mail()
def _attachment_invoice(self, msg_txt):
parsed_values = self.env['mail.thread']._message_parse_extract_payload(msg_txt)
body, attachments = parsed_values['body'], parsed_values['attachments']
from_address = msg_txt.get('from')
for attachment in attachments:
split_attachment = attachment.fname.rpartition('.')
if len(split_attachment) < 3:
_logger.info('E-invoice filename not compliant: %s', attachment.fname)
continue
attachment_name = split_attachment[0]
attachment_ext = split_attachment[2]
split_underscore = attachment_name.rsplit('_', 2)
if len(split_underscore) < 2:
_logger.info('E-invoice filename not compliant: %s', attachment.fname)
continue
if attachment_ext != 'zip':
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
self._create_invoice_from_mail(attachment.content, attachment.fname, from_address)
else:
if split_underscore[1] == 'AT':
# Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito
self._message_AT_invoice(attachment)
else:
_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
_logger.info('E-invoice already exist: %s', att_name)
return
invoice_attachment = self.env['ir.attachment'].create({
'name': att_name,
'datas': base64.encodebytes(att_content),
'type': 'binary',
})
try:
tree = etree.fromstring(att_content)
except Exception:
raise UserError(_('The xml file is badly formatted : {}').format(att_name))
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()
_logger.info('New E-invoice: %s', att_name)
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):
# invoice already exist
_logger.info('E-invoice in zip file (%s) already exist: %s', attachment_zip.fname, att_name)
continue
att_content = z.open(att_name).read()
self._create_invoice_from_mail(att_content, att_name, from_address)
def _message_AT_invoice(self, attachment_zip):
with zipfile.ZipFile(io.BytesIO(attachment_zip.content)) as z:
for attachment_name in z.namelist():
split_name_attachment = attachment_name.rpartition('.')
if len(split_name_attachment) < 3:
continue
split_underscore = split_name_attachment[0].rsplit('_', 2)
if len(split_underscore) < 2:
continue
if split_underscore[1] == 'AT':
attachment = z.open(attachment_name).read()
_logger.info('New AT receipt for: %s', split_underscore[0])
try:
tree = etree.fromstring(attachment)
except:
_logger.info('Error in decoding new receipt file: %s', attachment_name)
return
elements = tree.xpath('//NomeFile')
if elements and elements[0].text:
filename = elements[0].text
else:
return
related_invoice = self.env['account.move'].search([
('l10n_it_einvoice_name', '=', filename)])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', filename)
return
related_invoice.l10n_it_send_state = 'failed_delivery'
info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//MessageId', '//PecMessageId', '//Note'])
related_invoice.message_post(
body=(_("ES certify that it has received the invoice and that the file \
could not be delivered to the addressee. <br/>%s") % (info))
)
def _message_receipt_invoice(self, receipt_type, attachment):
try:
tree = etree.fromstring(attachment.content)
except:
_logger.info('Error in decoding new receipt file: %s', attachment.fname)
return {}
elements = tree.xpath('//NomeFile')
if elements and elements[0].text:
filename = elements[0].text
else:
return {}
if receipt_type == 'RC':
# 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')])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
related_invoice.l10n_it_send_state = 'delivered'
info = self._return_multi_line_xml(tree, ['//IdentificativoSdI', '//DataOraRicezione', '//DataOraConsegna', '//Note'])
related_invoice.message_post(
body=(_("E-Invoice is delivery to the destinatory:<br/>%s") % (info))
)
elif receipt_type == 'NS':
# 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')])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
related_invoice.l10n_it_send_state = 'invalid'
error = self._return_error_xml(tree)
related_invoice.message_post(
body=(_("Errors in the E-Invoice :<br/>%s") % (error))
)
related_invoice.activity_schedule(
'mail.mail_activity_data_todo',
summary='Rejection notice',
user_id=related_invoice.invoice_user_id.id if related_invoice.invoice_user_id else self.env.user.id)
elif receipt_type == 'MC':
# 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')])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
info = self._return_multi_line_xml(tree, [
'//IdentificativoSdI',
'//DataOraRicezione',
'//Descrizione',
'//MessageId',
'//Note'])
related_invoice.message_post(
body=(_("The E-invoice is not delivered to the addressee. The Exchange System is\
unable to deliver the file to the Public Administration. The Exchange System will\
contact the PA to report the problem and request that they provide a solution. \
During the following 15 days, the Exchange System will try to forward the FatturaPA\
file to the Administration in question again. More information:<br/>%s") % (info))
)
elif receipt_type == 'NE':
# Outcome notice
# 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')])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
elements = tree.xpath('//Esito')
if elements and elements[0].text:
if elements[0].text == 'EC01':
related_invoice.l10n_it_send_state = 'delivered_accepted'
elif elements[0].text == 'EC02':
related_invoice.l10n_it_send_state = 'delivered_refused'
info = self._return_multi_line_xml(tree,
['//Esito',
'//Descrizione',
'//IdentificativoSdI',
'//DataOraRicezione',
'//DataOraConsegna',
'//Note'
])
related_invoice.message_post(
body=(_("Outcome notice: %s<br/>%s") % (related_invoice.l10n_it_send_state, info))
)
if related_invoice.l10n_it_send_state == 'delivered_refused':
related_invoice.activity_schedule(
'mail.mail_activity_todo',
user_id=related_invoice.invoice_user_id.id if related_invoice.invoice_user_id else self.env.user.id,
summary='Outcome notice: Refused')
# elif receipt_type == 'MT':
# Metadata file
# This is the file sent by the ES to the addressee together with the invoice file,
# containing the main reference data of the file useful for processing, including
# the IdentificativoSDI.
# Useless for Odoo
elif receipt_type == 'DT':
# Deadline passed notice
# 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')])
if not related_invoice:
_logger.info('Error: invoice not found for receipt file: %s', attachment.fname)
return
related_invoice.l10n_it_send_state = 'delivered_expired'
info = self._return_multi_line_xml(tree, [
'//Descrizione',
'//IdentificativoSdI',
'//Note'])
related_invoice.message_post(
body=(_("Expiration of the maximum term for communication of acceptance/refusal:\
%s<br/>%s") % (filename, info))
)
def _return_multi_line_xml(self, tree, element_tags):
output_str = "<ul>"
for element_tag in element_tags:
elements = tree.xpath(element_tag)
if not elements:
continue
for element in elements:
if element.text:
text = " ".join(element.text.split())
output_str += "<li>%s: %s</li>" % (element.tag, text)
return output_str + "</ul>"
def _return_error_xml(self, tree):
output_str = "<ul>"
elements = tree.xpath('//Errore')
if not elements:
return
for element in elements:
descrizione = " ".join(element[1].text.split())
if descrizione:
output_str += "<li>Errore %s: %s</li>" % (element[0].text, descrizione)
return output_str + "</ul>"
class IrMailServer(models.Model):
_name = "ir.mail_server"
_inherit = "ir.mail_server"
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
body_alternative=None, subtype_alternative='plain'):
if self.env.context.get('wo_bounce_return_path') and headers:
headers['Return-Path'] = email_from
return super(IrMailServer, self).build_email(email_from, email_to, subject, body, email_cc=email_cc, email_bcc=email_bcc, reply_to=reply_to,
attachments=attachments, message_id=message_id, references=references, object_id=object_id, subtype=subtype, headers=headers,
body_alternative=body_alternative, subtype_alternative=subtype_alternative)