"
+
+ for element_tag in element_tags:
+ elements = tree.xpath(element_tag, namespaces=tree.nsmap)
+ if not elements:
+ continue
+ for element in elements:
+ text = " ".join(element.text.split())
+ if text:
+ output_str += "
%s: %s
" % (element.tag, text)
+ return output_str + "
"
+
+class AccountTax(models.Model):
+ _name = "account.tax"
+ _inherit = "account.tax"
+
+ l10n_it_vat_due_date = fields.Selection([
+ ("I", "[I] IVA ad esigibilità immediata"),
+ ("D", "[D] IVA ad esigibilità differita"),
+ ("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date")
+
+ l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.")
+ l10n_it_kind_exoneration = fields.Selection(selection=[
+ ("N1", "[N1] Escluse ex art. 15"),
+ ("N2", "[N2] Non soggette"),
+ ("N3", "[N3] Non imponibili"),
+ ("N4", "[N4] Esenti"),
+ ("N5", "[N5] Regime del margine / IVA non esposta in fattura"),
+ ("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"),
+ ("N7", "[N7] IVA assolta in altro stato UE (vendite a distanza ex art. 40 c. 3 e 4 e art. 41 c. 1 lett. b, DL 331/93; prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-sexies lett. f, g, art. 74-sexies DPR 633/72)")],
+ string="Exoneration",
+ help="Exoneration type",
+ default="N1")
+ l10n_it_law_reference = fields.Char(string="Law Reference", size=100)
+
+ @api.constrains('l10n_it_has_exoneration',
+ 'l10n_it_kind_exoneration',
+ 'l10n_it_law_reference',
+ 'amount',
+ 'l10n_it_vat_due_date')
+ def _check_exoneration_with_no_tax(self):
+ for tax in self:
+ if tax.l10n_it_has_exoneration:
+ if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
+ raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.")
+ if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S':
+ raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'"))
+
+class ImportInvoiceImportWizard(models.TransientModel):
+ _name = 'account.invoice.import.wizard'
+ _inherit = 'account.invoice.import.wizard'
+
+ @api.multi
+ def _create_invoice_from_file(self, attachment):
+ if attachment.mimetype == 'application/xml' and re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.name):
+ if self.env['account.invoice'].search([('l10n_it_einvoice_name', '=', attachment.name)], limit=1):
+ # invoice already exist
+ raise UserError(_('E-invoice already exist: %s') % attachment.name)
+ self = self.with_context(default_journal_id= self.journal_id.id)
+ invoice = self.env['account.invoice']._import_xml_invoice(base64.decodestring(attachment.datas), attachment)
+ else:
+ invoice = super(ImportInvoiceImportWizard, self)._create_invoice_from_file(attachment)
+ return invoice
diff --git a/addons/l10n_it_edi/models/ddt.py b/addons/l10n_it_edi/models/ddt.py
new file mode 100644
index 00000000000..61464a7d76a
--- /dev/null
+++ b/addons/l10n_it_edi/models/ddt.py
@@ -0,0 +1,21 @@
+# -*- coding:utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, api
+
+class L10nItDdt(models.Model):
+ _name = 'l10n_it.ddt'
+ _description = 'Transport Document'
+
+ invoice_id = fields.One2many('account.invoice', 'l10n_it_ddt_id', string='Invoice Reference',
+ ondelete='cascade')
+
+ name = fields.Char(string="Numero DDT", size=20, help="Transport document number", required=True)
+ date = fields.Date(string="Data DDT", help="Transport document date", required=True)
+
+ @api.multi
+ def name_get(self):
+ res = []
+ for ddt in self:
+ res.append((ddt.id, ("%s (%s)") % (ddt.name, ddt.date)))
+ return res
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
new file mode 100644
index 00000000000..86fa674fa55
--- /dev/null
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -0,0 +1,359 @@
+# -*- 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 dateutil
+import pytz
+import base64
+try:
+ from xmlrpc import client as xmlrpclib
+except ImportError:
+ import xmlrpclib
+
+
+from lxml import etree
+from datetime import datetime
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import ValidationError
+from odoo.tools import pycompat
+
+
+_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', 'type')
+ def _check_pec(self):
+ for record in self:
+ if record.l10n_it_is_pec and record.type != 'imap':
+ raise ValidationError("PEC mail server must be of type IMAP.")
+
+ @api.multi
+ 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.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, pycompat.text_type):
+ message = message.encode('utf-8')
+ msg_txt = email.message_from_bytes(message)
+
+ 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.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.type, server.name, (count - failed), failed)
+ except Exception:
+ _logger.info("General failure when trying to fetch mail from %s server %s.", 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):
+ body, attachments = self.env['mail.thread']._message_extract_payload(msg_txt)
+ from_address = tools.decode_smtp_header(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.invoice'].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.encodestring(att_content),
+ 'datas_fname': att_name,
+ 'type': 'binary',
+ })
+
+ invoice = self.env['account.invoice']._import_xml_invoice(att_content, invoice_attachment)
+ 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.invoice'].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', namespaces=tree.nsmap)
+ if elements and elements[0].text:
+ filename = elements[0].text
+ else:
+ return
+
+ related_invoice = self.env['account.invoice'].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. %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', namespaces=tree.nsmap)
+ 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.invoice'].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: %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.invoice'].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 : %s") % (error))
+ )
+ activity_vals = {
+ 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
+ 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id
+ }
+ related_invoice.activity_schedule(summary='Rejection notice', **activity_vals)
+
+ 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.invoice'].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 informations: %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.invoice'].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', namespaces=tree.nsmap)
+ 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 %s") % (related_invoice.l10n_it_send_state, info))
+ )
+ if related_invoice.l10n_it_send_state == 'delivered_refused':
+ activity_vals = {
+ 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
+ 'user_id': related_invoice.user_id.id if related_invoice.user_id else self.env.user.id
+ }
+ related_invoice.activity_schedule(summary='Outcome notice: Refused', **activity_vals)
+
+ # 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.invoice'].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 %s") % (filename, info))
+ )
+
+ def _return_multi_line_xml(self, tree, element_tags):
+ output_str = "
"
+
+ for element_tag in element_tags:
+ elements = tree.xpath(element_tag, namespaces=tree.nsmap)
+ if not elements:
+ continue
+ for element in elements:
+ text = " ".join(element.text.split())
+ if text:
+ output_str += "
"
+
+ elements = tree.xpath('//Errore', namespaces=tree.nsmap)
+ if not elements:
+ return
+ for element in elements:
+ descrizione = " ".join(element[1].text.split())
+ if descrizione:
+ output_str += "
"
diff --git a/addons/l10n_it_edi/models/res_company.py b/addons/l10n_it_edi/models/res_company.py
new file mode 100644
index 00000000000..a0cce53b9a5
--- /dev/null
+++ b/addons/l10n_it_edi/models/res_company.py
@@ -0,0 +1,110 @@
+# -*- coding:utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+TAX_SYSTEM = [
+ ("RF01", "[RF01] Ordinario"),
+ ("RF02", "[RF02] Contribuenti minimi (art.1, c.96-117, L. 244/07)"),
+ ("RF04", "[RF04] Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)"),
+ ("RF05", "[RF05] Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)"),
+ ("RF06", "[RF06] Commercio fiammiferi (art.74, c.1, DPR 633/72)"),
+ ("RF07", "[RF07] Editoria (art.74, c.1, DPR 633/72)"),
+ ("RF08", "[RF08] Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)"),
+ ("RF09", "[RF09] Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)"),
+ ("RF10", "[RF10] Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)"),
+ ("RF11", "[RF11] Agenzie viaggi e turismo (art.74-ter, DPR 633/72)"),
+ ("RF12", "[RF12] Agriturismo (art.5, c.2, L. 413/91)"),
+ ("RF13", "[RF13] Vendite a domicilio (art.25-bis, c.6, DPR 600/73)"),
+ ("RF14", "[RF14] Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)"),
+ ("RF15", "[RF15] Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)"),
+ ("RF16", "[RF16] IVA per cassa P.A. (art.6, c.5, DPR 633/72)"),
+ ("RF17", "[RF17] IVA per cassa (art. 32-bis, DL 83/2012)"),
+ ("RF18", "[RF18] Altro"),
+ ("RF19", "[RF19] Regime forfettario (art.1, c.54-89, L. 190/2014)"),
+]
+
+class ResCompany(models.Model):
+ _name = 'res.company'
+ _inherit = 'res.company'
+
+ l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16, related='partner_id.l10n_it_codice_fiscale',
+ store=True, readonly=False, help="Fiscal code of your company")
+ l10n_it_tax_system = fields.Selection(selection=TAX_SYSTEM, string="Tax System",
+ help="Please select the Tax system to which you are subjected.")
+
+ # PEC server
+ l10n_it_mail_pec_server_id = fields.Many2one('ir.mail_server', string="Server PEC",
+ help="Configure your PEC-mail server to send electronic invoices.")
+ l10n_it_address_recipient_fatturapa = fields.Char(string="Government PEC-mail",
+ help="Enter Government PEC-mail address. Ex: sdi01@pec.fatturapa.it")
+ l10n_it_address_send_fatturapa = fields.Char(string="Company PEC-mail",
+ help="Enter your company PEC-mail address. Ex: yourcompany@pec.mail.it")
+
+
+ # Economic and Administrative Index
+ l10n_it_has_eco_index = fields.Boolean(default=False,
+ help="The seller/provider is a company listed on the register of companies and as\
+ such must also indicate the registration data on all documents (art. 2250, Italian\
+ Civil Code)")
+ l10n_it_eco_index_office = fields.Many2one('res.country.state', domain="[('country_id','=','IT')]",
+ string="Province of the register-of-companies office")
+ l10n_it_eco_index_number = fields.Char(string="Number in register of companies", size=20,
+ help="This field must contain the number under which the\
+ seller/provider is listed on the register of companies.")
+ l10n_it_eco_index_share_capital = fields.Float(default="0.0", size=15, string="Share capital actually paid up",
+ help="Mandatory if the seller/provider is a company with share\
+ capital (SpA, SApA, Srl), this field must contain the amount\
+ of share capital actually paid up as resulting from the last\
+ financial statement")
+ l10n_it_eco_index_sole_shareholder = fields.Selection(
+ [
+ ("NO", "Not a limited liability company"),
+ ("SU", "Socio unico"),
+ ("SM", "Più soci")],
+ string="Shareholder")
+ l10n_it_eco_index_liquidation_state = fields.Selection(
+ [
+ ("LS", "The company is in a state of liquidation"),
+ ("LN", "The company is not in a state of liquidation")],
+ string="Liquidation state")
+
+
+ # Tax representative
+ l10n_it_has_tax_representative = fields.Boolean(default=False,
+ help="The seller/provider is a non-resident subject which\
+ carries out transactions in Italy with relevance for VAT\
+ purposes and which takes avail of a tax representative in\
+ Italy")
+ l10n_it_tax_representative_partner_id = fields.Many2one('res.partner', string='Tax representative partner')
+
+ @api.constrains('l10n_it_has_eco_index',
+ 'l10n_it_eco_index_office',
+ 'l10n_it_eco_index_number',
+ 'l10n_it_eco_index_share_capital',
+ 'l10n_it_eco_index_sole_shareholder',
+ 'l10n_it_eco_index_liquidation_state')
+ def _check_eco_admin_index(self):
+ for record in self:
+ if not record.l10n_it_has_eco_index:
+ continue
+ if not record.l10n_it_eco_index_office\
+ or not record.l10n_it_eco_index_number\
+ or not record.l10n_it_eco_index_share_capital\
+ or not record.l10n_it_eco_index_sole_shareholder\
+ or not record.l10n_it_eco_index_liquidation_state:
+ raise ValidationError("All fields about the Economic and Administrative Index must be completed.")
+
+ @api.constrains('l10n_it_has_tax_representative',
+ 'l10n_it_tax_representative_partner_id')
+ def _check_tax_representative(self):
+ for record in self:
+ if not record.l10n_it_has_tax_representative:
+ continue
+ if not record.l10n_it_tax_representative_partner_id:
+ raise ValidationError("You must select a tax representative.")
+ if not record.l10n_it_tax_representative_partner_id.vat:
+ raise ValidationError("Your tax representative partner must have a tax number.")
+ if not record.l10n_it_tax_representative_partner_id.country_id:
+ raise ValidationError("Your tax representative partner must have a country.")
diff --git a/addons/l10n_it_edi/models/res_partner.py b/addons/l10n_it_edi/models/res_partner.py
new file mode 100644
index 00000000000..e0a78f24d58
--- /dev/null
+++ b/addons/l10n_it_edi/models/res_partner.py
@@ -0,0 +1,28 @@
+# -*- coding:utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+class ResPartner(models.Model):
+ _name = 'res.partner'
+ _inherit = 'res.partner'
+
+ l10n_it_pec_email = fields.Char(string="PEC e-mail")
+ l10n_it_codice_fiscale = fields.Char(string="Codice Fiscale", size=16)
+ l10n_it_pa_index = fields.Char(string="PA index",
+ size=7,
+ help="Must contain the 6-character (or 7) code, present in the PA\
+ Index in the information relative to the electronic invoicing service,\
+ associated with the office which, within the addressee administration, deals\
+ with receiving (and processing) the invoice.")
+
+ _sql_constraints = [
+ ('l10n_it_codice_fiscale',
+ "CHECK(l10n_it_codice_fiscale IS NULL OR LENGTH(l10n_it_codice_fiscale) >= 11)",
+ "Codice fiscale must have between 11 and 16 characters."),
+
+ ('l10n_it_pa_index',
+ "CHECK(l10n_it_pa_index IS NULL OR LENGTH(l10n_it_pa_index) >= 6)",
+ "PA index must have between 6 and 7 characters."),
+ ]
diff --git a/addons/l10n_it_edi/security/ir.model.access.csv b/addons/l10n_it_edi/security/ir.model.access.csv
new file mode 100644
index 00000000000..22c0da9c159
--- /dev/null
+++ b/addons/l10n_it_edi/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_it_ddt_manager","it_ddt manager","model_l10n_it_ddt","account.group_account_invoice",1,1,1,1
\ No newline at end of file
diff --git a/addons/l10n_it_edi/tests/__init__.py b/addons/l10n_it_edi/tests/__init__.py
new file mode 100644
index 00000000000..d7b285d123c
--- /dev/null
+++ b/addons/l10n_it_edi/tests/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import test_generate
diff --git a/addons/l10n_it_edi/tests/test_generate.py b/addons/l10n_it_edi/tests/test_generate.py
new file mode 100644
index 00000000000..60592ade8c7
--- /dev/null
+++ b/addons/l10n_it_edi/tests/test_generate.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+import time
+
+from odoo import fields
+from odoo.tests.common import TransactionCase, Form
+
+class TestItalianElectronicInvoice(TransactionCase):
+ def test_state(self):
+ f = Form(self.env['account.invoice'])
+ f.partner_id = self.env.ref('base.res_partner_12')
+ with f.invoice_line_ids.new() as l:
+ l.product_id = self.env.ref('product.product_product_3')
+ invoice = f.save()
+
+ # I check that Initially customer invoice state is "Draft"
+ self.assertEqual(invoice.state, 'draft')
+
+ # I called the "Confirm Draft Invoices" wizard
+ w = Form(self.env['account.invoice.confirm']).save()
+ # I clicked on Confirm Invoices Button
+ w.with_context(
+ active_model='account.invoice',
+ active_id=invoice.id,
+ active_ids=invoice.ids,
+ type='out_invoice',
+ ).invoice_confirm()
+
+ # I check that customer invoice state is "Open"
+ self.assertEqual(invoice.state, 'open')
+
+ # Electronic invoice must be present and have the same name as l10n_it_einvoice_name
+ self.assertEqual(invoice.l10n_it_einvoice_id.name, invoice.l10n_it_einvoice_name)
diff --git a/addons/l10n_it_edi/views/l10n_it_view.xml b/addons/l10n_it_edi/views/l10n_it_view.xml
new file mode 100644
index 00000000000..7ae8222bf79
--- /dev/null
+++ b/addons/l10n_it_edi/views/l10n_it_view.xml
@@ -0,0 +1,265 @@
+
+
+
+ fetchmail.server.form.l10n.it
+ fetchmail.server
+ 20
+
+
+
+
+
+
+
+
+
+
+
+ account.tax.form.l10n.it
+ account.tax
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.form.l10n.it
+ res.partner
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.company.form.l10n.it
+ res.company
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The seller/provider is a company listed on the register of companies and as
+ such must also indicate the registration data on all documents (art. 2250, Italian
+ Civil Code)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The seller/provider is a non-resident subject which carries out transactions in Italy
+ with relevance for VAT purposes and which takes avail of a tax representative in Italy
+