summaryrefslogtreecommitdiff
path: root/addons/l10n_it_edi
diff options
context:
space:
mode:
authorBenjamin Frantzen (bfr) <bfr@odoo.com>2020-05-26 07:47:54 +0000
committerBenjamin Frantzen (bfr) <bfr@odoo.com>2020-05-29 07:16:30 +0000
commitac0381d216c7b24b1fe5d9091d655d25d074cc81 (patch)
tree7439d710b24306e6db64e3b7883827f980809f93 /addons/l10n_it_edi
parent5b5ee381398090d6af9ce68041babb9953cd9e73 (diff)
[IMP] l10n_it_edi: adaptation to account_edi
- Support for account_edi workflows (only import) + fattura_pa as an account.edi.format. - XML containing multiple invoices cannot be imported from the chatter.
Diffstat (limited to 'addons/l10n_it_edi')
-rw-r--r--addons/l10n_it_edi/__manifest__.py2
-rw-r--r--addons/l10n_it_edi/data/account_edi_data.xml12
-rw-r--r--addons/l10n_it_edi/models/__init__.py1
-rw-r--r--addons/l10n_it_edi/models/account_edi_format.py421
-rw-r--r--addons/l10n_it_edi/models/account_invoice.py410
-rw-r--r--addons/l10n_it_edi/models/ir_mail_server.py2
6 files changed, 451 insertions, 397 deletions
diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py
index 917dc15c231..a119942594c 100644
--- a/addons/l10n_it_edi/__manifest__.py
+++ b/addons/l10n_it_edi/__manifest__.py
@@ -7,6 +7,7 @@
'depends': [
'l10n_it',
'fetchmail',
+ 'account_edi'
],
'author': 'Odoo',
'description': """
@@ -16,6 +17,7 @@ E-invoice implementation
'website': 'http://www.odoo.com/',
'data': [
'security/ir.model.access.csv',
+ 'data/account_edi_data.xml',
'data/invoice_it_template.xml',
'views/l10n_it_view.xml',
],
diff --git a/addons/l10n_it_edi/data/account_edi_data.xml b/addons/l10n_it_edi/data/account_edi_data.xml
new file mode 100644
index 00000000000..9dbd1dec98d
--- /dev/null
+++ b/addons/l10n_it_edi/data/account_edi_data.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <record id="edi_fatturaPA" model="account.edi.format">
+ <field name="name">Fattura PA (IT)</field>
+ <field name="code">fattura_pa</field>
+ <field name="hide_on_journal">import</field>
+ </record>
+
+ </data>
+</odoo> \ No newline at end of file
diff --git a/addons/l10n_it_edi/models/__init__.py b/addons/l10n_it_edi/models/__init__.py
index a7b2d212437..e2f3f69717a 100644
--- a/addons/l10n_it_edi/models/__init__.py
+++ b/addons/l10n_it_edi/models/__init__.py
@@ -4,5 +4,6 @@
from . import res_partner
from . import res_company
from . import account_invoice
+from . import account_edi_format
from . import ir_mail_server
from . import ddt
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
new file mode 100644
index 00000000000..ab2d7230407
--- /dev/null
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -0,0 +1,421 @@
+# -*- coding:utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, fields, _
+from odoo.tests.common import Form
+from odoo.exceptions import UserError
+from odoo.tools import float_repr
+
+import re
+from datetime import date, datetime
+import logging
+import base64
+
+
+_logger = logging.getLogger(__name__)
+
+DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
+
+
+class AccountEdiFormat(models.Model):
+ _inherit = 'account.edi.format'
+
+ def _check_filename_is_fattura_pa(self, filename):
+ return re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", filename)
+
+ def _is_fattura_pa(self, filename, tree):
+ return self.code == 'fattura_pa' and self._check_filename_is_fattura_pa(filename)
+
+ def _create_invoice_from_xml_tree(self, filename, tree):
+ self.ensure_one()
+ if self._is_fattura_pa(filename, tree):
+ return self._import_fattura_pa(tree, self.env['account.move'])
+ return super()._create_invoice_from_xml_tree(filename, tree)
+
+ def _update_invoice_from_xml_tree(self, filename, tree, invoice):
+ self.ensure_one()
+ if self._is_fattura_pa(filename, tree):
+ if len(tree.xpath('//FatturaElettronicaBody')) > 1:
+ invoice.message_post(body='The attachment contains multiple invoices, this invoice was not updated from it.',
+ message_type='comment',
+ subtype_xmlid='mail.mt_note',
+ author_id=self.env.ref('base.partner_root').id)
+ else:
+ return self._import_fattura_pa(tree, invoice)
+ return super()._update_invoice_from_xml_tree(filename, tree, invoice)
+
+ def _import_fattura_pa(self, tree, invoice):
+ """ Decodes a fattura_pa invoice into an invoice.
+
+ :param tree: the fattura_pa tree to decode.
+ :param invoice: the invoice to update or an empty recordset.
+ :returns: the invoice where the fattura_pa data was imported.
+ """
+ invoices = self.env['account.move']
+ first_run = True
+
+ # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch
+ for body_tree in tree.xpath('//FatturaElettronicaBody'):
+ if not first_run or not invoice:
+ # make sure all the iterations create a new invoice record (except the first which could have already created one)
+ invoice = self.env['account.move']
+ first_run = False
+
+ 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))
+
+ # 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).
+
+ elements = tree.xpath('//CessionarioCommittente//IdCodice')
+ company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
+ if not company:
+ elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
+ 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)
+ else:
+ company = self.env.company
+ if elements:
+ _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text)
+ else:
+ _logger.info(_('Company not found. The company\'s user is set by default.'))
+
+ if not self.env.is_superuser():
+ 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)) as invoice_form:
+ message_to_log = []
+
+ # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
+ elements = tree.xpath('//CedentePrestatore//IdCodice')
+ partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
+ if not partner:
+ elements = tree.xpath('//CedentePrestatore//CodiceFiscale')
+ partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
+ if not partner:
+ elements = tree.xpath('//DatiTrasmissione//Email')
+ partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
+ if partner:
+ invoice_form.partner_id = partner
+ else:
+ message_to_log.append("%s<br/>%s" % (
+ _("Vendor not found, useful informations from XML file:"),
+ invoice._compose_info_message(
+ tree, './/CedentePrestatore')))
+
+ # Numbering attributed by the transmitter. <1.1.2>
+ elements = tree.xpath('//ProgressivoInvio')
+ if elements:
+ invoice_form.payment_reference = elements[0].text
+
+ elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero')
+ if elements:
+ invoice_form.ref = elements[0].text
+
+ # Currency. <2.1.1.2>
+ elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa')
+ if elements:
+ currency_str = elements[0].text
+ currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
+ if currency != self.env.company.currency_id and currency.active:
+ invoice_form.currency_id = currency
+
+ # Date. <2.1.1.3>
+ elements = body_tree.xpath('.//DatiGeneraliDocumento/Data')
+ if elements:
+ date_str = elements[0].text
+ date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
+ invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
+
+ # Dati Bollo. <2.1.1.6>
+ elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
+ if elements:
+ invoice_form.l10n_it_stamp_duty = float(elements[0].text)
+
+ # List of all amount discount (will be add after all article to avoid to have a negative sum)
+ discount_list = []
+ percentage_global_discount = 1.0
+
+ # Global discount. <2.1.1.8>
+ discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione')
+ total_discount_amount = 0.0
+ if discount_elements:
+ for discount_element in discount_elements:
+ discount_line = discount_element.xpath('.//Tipo')
+ discount_sign = -1
+ if discount_line and discount_line[0].text == 'SC':
+ discount_sign = 1
+ discount_percentage = discount_element.xpath('.//Percentuale')
+ if discount_percentage and discount_percentage[0].text:
+ percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign
+
+ discount_amount_text = discount_element.xpath('.//Importo')
+ if discount_amount_text and discount_amount_text[0].text:
+ discount_amount = float(discount_amount_text[0].text) * discount_sign * -1
+ discount = {}
+ discount["seq"] = 0
+
+ if discount_amount < 0:
+ discount["name"] = _('GLOBAL DISCOUNT')
+ else:
+ discount["name"] = _('GLOBAL EXTRA CHARGE')
+ discount["amount"] = discount_amount
+ discount["tax"] = []
+ discount_list.append(discount)
+
+ # Comment. <2.1.1.11>
+ elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale')
+ for element in elements:
+ invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text)
+
+ # Informations relative to the purchase order, the contract, the agreement,
+ # the reception phase or invoices previously transmitted
+ # <2.1.2> - <2.1.6>
+ for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
+ elements = body_tree.xpath('.//DatiGenerali/' + document_type)
+ if elements:
+ for element in elements:
+ message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"),
+ invoice._compose_info_message(element, '.')))
+
+ # Dati DDT. <2.1.8>
+ elements = body_tree.xpath('.//DatiGenerali/DatiDDT')
+ if elements:
+ message_to_log.append("%s<br/>%s" % (
+ _("Transport informations from XML file:"),
+ invoice._compose_info_message(body_tree, './/DatiGenerali/DatiDDT')))
+
+ # Due date. <2.4.2.5>
+ elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento')
+ if elements:
+ date_str = elements[0].text
+ date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
+ invoice_form.invoice_date_due = fields.Date.to_string(date_obj)
+
+ # Total amount. <2.4.2.6>
+ elements = body_tree.xpath('.//ImportoPagamento')
+ amount_total_import = 0
+ for element in elements:
+ amount_total_import += float(element.text)
+ if amount_total_import:
+ message_to_log.append(_("Total amount from the XML File: %s") % (
+ amount_total_import))
+
+ # Bank account. <2.4.2.13>
+ if invoice_form.move_type not in ('out_invoice', 'in_refund'):
+ elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN')
+ if elements:
+ if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id:
+ bank = self.env['res.partner.bank'].search([
+ ('acc_number', '=', elements[0].text),
+ ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id)
+ ])
+ else:
+ bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)])
+ if bank:
+ invoice_form.partner_bank_id = bank
+ else:
+ message_to_log.append("%s<br/>%s" % (
+ _("Bank account not found, useful informations from XML file:"),
+ invoice._compose_multi_info_message(
+ body_tree, ['.//DatiPagamento//Beneficiario',
+ './/DatiPagamento//IstitutoFinanziario',
+ './/DatiPagamento//IBAN',
+ './/DatiPagamento//ABI',
+ './/DatiPagamento//CAB',
+ './/DatiPagamento//BIC',
+ './/DatiPagamento//ModalitaPagamento'])))
+ else:
+ elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento')
+ if elements:
+ message_to_log.append("%s<br/>%s" % (
+ _("Bank account not found, useful informations from XML file:"),
+ self._compose_info_message(body_tree, './/DatiPagamento')))
+
+ # Invoice lines. <2.2.1>
+ elements = body_tree.xpath('.//DettaglioLinee')
+ if elements:
+ for element in elements:
+ with invoice_form.invoice_line_ids.new() as invoice_line_form:
+
+ # Sequence.
+ line_elements = element.xpath('.//NumeroLinea')
+ if line_elements:
+ invoice_line_form.sequence = int(line_elements[0].text) * 2
+
+ # Product.
+ line_elements = element.xpath('.//Descrizione')
+ if line_elements:
+ invoice_line_form.name = " ".join(line_elements[0].text.split())
+
+ elements_code = element.xpath('.//CodiceArticolo')
+ if elements_code:
+ for element_code in elements_code:
+ type_code = element_code.xpath('.//CodiceTipo')[0]
+ code = element_code.xpath('.//CodiceValore')[0]
+ if type_code.text == 'EAN':
+ product = self.env['product.product'].search([('barcode', '=', code.text)])
+ if product:
+ invoice_line_form.product_id = product
+ break
+ if partner:
+ product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)])
+ if product_supplier and product_supplier.product_id:
+ invoice_line_form.product_id = product_supplier.product_id
+ break
+ if not invoice_line_form.product_id:
+ for element_code in elements_code:
+ code = element_code.xpath('.//CodiceValore')[0]
+ product = self.env['product.product'].search([('default_code', '=', code.text)])
+ if product:
+ invoice_line_form.product_id = product
+ break
+
+ # Price Unit.
+ line_elements = element.xpath('.//PrezzoUnitario')
+ if line_elements:
+ invoice_line_form.price_unit = float(line_elements[0].text)
+
+ # Quantity.
+ line_elements = element.xpath('.//Quantita')
+ if line_elements:
+ invoice_line_form.quantity = float(line_elements[0].text)
+ else:
+ invoice_line_form.quantity = 1
+
+ # Taxes
+ tax_element = element.xpath('.//AliquotaIVA')
+ natura_element = element.xpath('.//Natura')
+ invoice_line_form.tax_ids.clear()
+ if tax_element and tax_element[0].text:
+ percentage = float(tax_element[0].text)
+ if natura_element and natura_element[0].text:
+ l10n_it_kind_exoneration = natura_element[0].text
+ tax = self.env['account.tax'].search([
+ ('company_id', '=', invoice_form.company_id.id),
+ ('amount_type', '=', 'percent'),
+ ('type_tax_use', '=', 'purchase'),
+ ('amount', '=', percentage),
+ ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration),
+ ], limit=1)
+ else:
+ tax = self.env['account.tax'].search([
+ ('company_id', '=', invoice_form.company_id.id),
+ ('amount_type', '=', 'percent'),
+ ('type_tax_use', '=', 'purchase'),
+ ('amount', '=', percentage),
+ ], limit=1)
+ l10n_it_kind_exoneration = ''
+
+ if tax:
+ invoice_line_form.tax_ids.add(tax)
+ else:
+ if l10n_it_kind_exoneration:
+ message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % (
+ percentage,
+ l10n_it_kind_exoneration,
+ invoice_line_form.name))
+ else:
+ message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % (
+ percentage,
+ invoice_line_form.name))
+
+ # Discount in cascade mode.
+ # if 3 discounts : -10% -50€ -20%
+ # the result must be : (((price -10%)-50€) -20%)
+ # Generic form : (((price -P1%)-A1€) -P2%)
+ # It will be split in two parts: fix amount and pourcent amount
+ # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€)
+ # pourcent: 1-(1-P2)*(1-P5)
+ # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all
+ # percentage present after the fix amount)
+ line_elements = element.xpath('.//ScontoMaggiorazione')
+ total_discount_amount = 0.0
+ total_discount_percentage = percentage_global_discount
+ if line_elements:
+ for line_element in line_elements:
+ discount_line = line_element.xpath('.//Tipo')
+ discount_sign = -1
+ if discount_line and discount_line[0].text == 'SC':
+ discount_sign = 1
+ discount_percentage = line_element.xpath('.//Percentuale')
+ if discount_percentage and discount_percentage[0].text:
+ pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign
+ total_discount_percentage *= pourcentage_actual
+ total_discount_amount *= pourcentage_actual
+
+ discount_amount = line_element.xpath('.//Importo')
+ if discount_amount and discount_amount[0].text:
+ total_discount_amount += float(discount_amount[0].text) * discount_sign * -1
+
+ # Save amount discount.
+ if total_discount_amount != 0:
+ discount = {}
+ discount["seq"] = invoice_line_form.sequence + 1
+
+ if total_discount_amount < 0:
+ discount["name"] = _('DISCOUNT: ') + invoice_line_form.name
+ else:
+ discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name
+ discount["amount"] = total_discount_amount
+ discount["tax"] = []
+ for tax in invoice_line_form.tax_ids:
+ discount["tax"].append(tax)
+ discount_list.append(discount)
+ invoice_line_form.discount = (1 - total_discount_percentage) * 100
+
+ # Apply amount discount.
+ for discount in discount_list:
+ with invoice_form.invoice_line_ids.new() as invoice_line_form_discount:
+ invoice_line_form_discount.tax_ids.clear()
+ invoice_line_form_discount.sequence = discount["seq"]
+ invoice_line_form_discount.name = discount["name"]
+ invoice_line_form_discount.price_unit = discount["amount"]
+
+ new_invoice = invoice_form.save()
+ new_invoice.l10n_it_send_state = "other"
+
+ elements = body_tree.xpath('.//Allegati')
+ if elements:
+ for element in elements:
+ name_attachment = element.xpath('.//NomeAttachment')[0].text
+ attachment_64 = str.encode(element.xpath('.//Attachment')[0].text)
+ attachment_64 = self.env['ir.attachment'].create({
+ 'name': name_attachment,
+ 'datas': attachment_64,
+ 'type': 'binary',
+ })
+
+ # default_res_id is had to context to avoid facturx to import his content
+ # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it
+ new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post(
+ body=(_("Attachment from XML")),
+ attachment_ids=[attachment_64.id]
+ )
+
+ for message in message_to_log:
+ new_invoice.message_post(body=message)
+
+ invoices += new_invoice
+ return invoices
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index c511de477d8..62e40420247 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -43,9 +43,17 @@ class AccountMove(models.Model):
l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False)
- l10n_it_einvoice_name = fields.Char(readonly=True, copy=False)
+ l10n_it_einvoice_name = fields.Char(compute='_compute_l10n_it_einvoice')
- l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", copy=False)
+ l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice')
+
+ @api.depends('edi_document_ids')
+ def _compute_l10n_it_einvoice(self):
+ fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
+ for invoice in self:
+ einvoice = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == fattura_pa)
+ invoice.l10n_it_einvoice_id = einvoice
+ invoice.l10n_it_einvoice_name = einvoice.name
def post(self):
# OVERRIDE
@@ -158,17 +166,17 @@ class AccountMove(models.Model):
'codice': invoice.company_id.l10n_it_codice_fiscale,
'progressive_number': progressive_number.zfill(5),
}
- invoice.l10n_it_einvoice_name = report_name
data = b"<?xml version='1.0' encoding='UTF-8'?>" + invoice._export_as_xml()
description = _('Italian invoice: %s') % invoice.move_type
- invoice.l10n_it_einvoice_id = self.env['ir.attachment'].create({
+ invoice.edi_document_ids = self.env['ir.attachment'].create({
'name': report_name,
'res_id': invoice.id,
'res_model': invoice._name,
'datas': base64.encodebytes(data),
'description': description,
'type': 'binary',
+ 'edi_format_id': self.env.ref('l10n_it_edi.edi_fatturaPA').id
})
invoice.message_post(
@@ -276,8 +284,8 @@ class AccountMove(models.Model):
raise UserError(_("%s isn't in a right state. It must be in a 'Not yet send' or 'Invalid' state.") % (self.display_name))
message = self.env['mail.message'].create({
- 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_id.name),
- 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.company.l10n_it_address_recipient_fatturapa),
+ 'subject': _('Sending file: %s') % (self.l10n_it_einvoice_name),
+ 'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_name, self.env.company.l10n_it_address_recipient_fatturapa),
'author_id': self.env.user.partner_id.id,
'email_from': self.env.company.l10n_it_address_send_fatturapa,
'reply_to': self.env.company.l10n_it_address_send_fatturapa,
@@ -301,382 +309,6 @@ class AccountMove(models.Model):
)
self.l10n_it_send_state = 'invalid'
- def _import_xml_invoice(self, tree):
- ''' Extract invoice values from the E-Invoice xml tree passed as parameter.
-
- :param content: The tree of the xml file.
- :return: A dictionary containing account.invoice values to create/update it.
- '''
-
- invoices = self.env['account.move']
- multi = False
-
- # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch
- for body_tree in tree.xpath('//FatturaElettronicaBody'):
- if multi:
- # make sure all the iterations create a new invoice record (except the first which could have already created one)
- self = self.env['account.move']
- multi = True
-
- elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
- if elements and elements[0].text and elements[0].text == 'TD01':
- self_ctx = self.with_context(default_move_type='in_invoice')
- elif elements and elements[0].text and elements[0].text == 'TD04':
- self_ctx = self.with_context(default_move_type='in_refund')
- else:
- _logger.info(_('Document type not managed: %s.') % (elements[0].text))
-
- # 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).
-
- elements = tree.xpath('//CessionarioCommittente//IdCodice')
- company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
- if not company:
- elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
- 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)
- else:
- company = self.env.company
- if elements:
- _logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text)
- else:
- _logger.info(_('Company not found. The company\'s user is set by default.'))
-
- if not self.env.is_superuser():
- 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':
- type = 'in_invoice'
- elif elements and elements[0].text and elements[0].text == 'TD04':
- type = 'in_refund'
- # self could be a single record (editing) or be empty (new).
- with Form(self.with_context(default_move_type=type)) as invoice_form:
- message_to_log = []
-
- # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
- elements = tree.xpath('//CedentePrestatore//IdCodice')
- partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
- if not partner:
- elements = tree.xpath('//CedentePrestatore//CodiceFiscale')
- partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
- if not partner:
- elements = tree.xpath('//DatiTrasmissione//Email')
- partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
- if partner:
- invoice_form.partner_id = partner
- else:
- message_to_log.append("%s<br/>%s" % (
- _("Vendor not found, useful informations from XML file:"),
- self._compose_info_message(
- tree, './/CedentePrestatore')))
-
- # Numbering attributed by the transmitter. <1.1.2>
- elements = tree.xpath('//ProgressivoInvio')
- if elements:
- invoice_form.payment_reference = elements[0].text
-
- elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero')
- if elements:
- invoice_form.ref = elements[0].text
-
- # Currency. <2.1.1.2>
- elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa')
- if elements:
- currency_str = elements[0].text
- currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
- if currency != self.env.company.currency_id and currency.active:
- invoice_form.currency_id = currency
-
- # Date. <2.1.1.3>
- elements = body_tree.xpath('.//DatiGeneraliDocumento/Data')
- if elements:
- date_str = elements[0].text
- date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
- invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
-
- # Dati Bollo. <2.1.1.6>
- elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
- if elements:
- invoice_form.l10n_it_stamp_duty = float(elements[0].text)
-
- # List of all amount discount (will be add after all article to avoid to have a negative sum)
- discount_list = []
- percentage_global_discount = 1.0
-
- # Global discount. <2.1.1.8>
- discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione')
- total_discount_amount = 0.0
- if discount_elements:
- for discount_element in discount_elements:
- discount_line = discount_element.xpath('.//Tipo')
- discount_sign = -1
- if discount_line and discount_line[0].text == 'SC':
- discount_sign = 1
- discount_percentage = discount_element.xpath('.//Percentuale')
- if discount_percentage and discount_percentage[0].text:
- percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign
-
- discount_amount_text = discount_element.xpath('.//Importo')
- if discount_amount_text and discount_amount_text[0].text:
- discount_amount = float(discount_amount_text[0].text) * discount_sign * -1
- discount = {}
- discount["seq"] = 0
-
- if discount_amount < 0:
- discount["name"] = _('GLOBAL DISCOUNT')
- else:
- discount["name"] = _('GLOBAL EXTRA CHARGE')
- discount["amount"] = discount_amount
- discount["tax"] = []
- discount_list.append(discount)
-
- # Comment. <2.1.1.11>
- elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale')
- for element in elements:
- invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text)
-
- # Informations relative to the purchase order, the contract, the agreement,
- # the reception phase or invoices previously transmitted
- # <2.1.2> - <2.1.6>
- for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
- elements = body_tree.xpath('.//DatiGenerali/' + document_type)
- if elements:
- for element in elements:
- message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"),
- self._compose_info_message(element, '.')))
-
- # Dati DDT. <2.1.8>
- elements = body_tree.xpath('.//DatiGenerali/DatiDDT')
- if elements:
- message_to_log.append("%s<br/>%s" % (
- _("Transport informations from XML file:"),
- self._compose_info_message(body_tree, './/DatiGenerali/DatiDDT')))
-
- # Due date. <2.4.2.5>
- elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento')
- if elements:
- date_str = elements[0].text
- date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
- invoice_form.invoice_date_due = fields.Date.to_string(date_obj)
-
- # Total amount. <2.4.2.6>
- elements = body_tree.xpath('.//ImportoPagamento')
- amount_total_import = 0
- for element in elements:
- amount_total_import += float(element.text)
- if amount_total_import:
- message_to_log.append(_("Total amount from the XML File: %s") % (
- amount_total_import))
-
- # Bank account. <2.4.2.13>
- if invoice_form.type not in ('out_invoice', 'in_refund'):
- elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN')
- if elements:
- if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id:
- bank = self.env['res.partner.bank'].search([
- ('acc_number', '=', elements[0].text),
- ('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id)
- ])
- else:
- bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)])
- if bank:
- invoice_form.partner_bank_id = bank
- else:
- message_to_log.append("%s<br/>%s" % (
- _("Bank account not found, useful informations from XML file:"),
- self._compose_multi_info_message(
- body_tree, ['.//DatiPagamento//Beneficiario',
- './/DatiPagamento//IstitutoFinanziario',
- './/DatiPagamento//IBAN',
- './/DatiPagamento//ABI',
- './/DatiPagamento//CAB',
- './/DatiPagamento//BIC',
- './/DatiPagamento//ModalitaPagamento'])))
- else:
- elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento')
- if elements:
- message_to_log.append("%s<br/>%s" % (
- _("Bank account not found, useful informations from XML file:"),
- self._compose_info_message(body_tree, './/DatiPagamento')))
-
- # Invoice lines. <2.2.1>
- elements = body_tree.xpath('.//DettaglioLinee')
- if elements:
- for element in elements:
- with invoice_form.invoice_line_ids.new() as invoice_line_form:
-
- # Sequence.
- line_elements = element.xpath('.//NumeroLinea')
- if line_elements:
- invoice_line_form.sequence = int(line_elements[0].text) * 2
-
- # Product.
- line_elements = element.xpath('.//Descrizione')
- if line_elements:
- invoice_line_form.name = " ".join(line_elements[0].text.split())
-
- elements_code = element.xpath('.//CodiceArticolo')
- if elements_code:
- for element_code in elements_code:
- type_code = element_code.xpath('.//CodiceTipo')[0]
- code = element_code.xpath('.//CodiceValore')[0]
- if type_code.text == 'EAN':
- product = self.env['product.product'].search([('barcode', '=', code.text)])
- if product:
- invoice_line_form.product_id = product
- break
- if partner:
- product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)])
- if product_supplier and product_supplier.product_id:
- invoice_line_form.product_id = product_supplier.product_id
- break
- if not invoice_line_form.product_id:
- for element_code in elements_code:
- code = element_code.xpath('.//CodiceValore')[0]
- product = self.env['product.product'].search([('default_code', '=', code.text)])
- if product:
- invoice_line_form.product_id = product
- break
-
- # Price Unit.
- line_elements = element.xpath('.//PrezzoUnitario')
- if line_elements:
- invoice_line_form.price_unit = float(line_elements[0].text)
-
- # Quantity.
- line_elements = element.xpath('.//Quantita')
- if line_elements:
- invoice_line_form.quantity = float(line_elements[0].text)
- else:
- invoice_line_form.quantity = 1
-
- # Taxes
- tax_element = element.xpath('.//AliquotaIVA')
- natura_element = element.xpath('.//Natura')
- invoice_line_form.tax_ids.clear()
- if tax_element and tax_element[0].text:
- percentage = float(tax_element[0].text)
- if natura_element and natura_element[0].text:
- l10n_it_kind_exoneration = natura_element[0].text
- tax = self.env['account.tax'].search([
- ('company_id', '=', invoice_form.company_id.id),
- ('amount_type', '=', 'percent'),
- ('type_tax_use', '=', 'purchase'),
- ('amount', '=', percentage),
- ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration),
- ], limit=1)
- else:
- tax = self.env['account.tax'].search([
- ('company_id', '=', invoice_form.company_id.id),
- ('amount_type', '=', 'percent'),
- ('type_tax_use', '=', 'purchase'),
- ('amount', '=', percentage),
- ], limit=1)
- l10n_it_kind_exoneration = ''
-
- if tax:
- invoice_line_form.tax_ids.add(tax)
- else:
- if l10n_it_kind_exoneration:
- message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % (
- percentage,
- l10n_it_kind_exoneration,
- invoice_line_form.name))
- else:
- message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % (
- percentage,
- invoice_line_form.name))
-
- # Discount in cascade mode.
- # if 3 discounts : -10% -50€ -20%
- # the result must be : (((price -10%)-50€) -20%)
- # Generic form : (((price -P1%)-A1€) -P2%)
- # It will be split in two parts: fix amount and pourcent amount
- # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€)
- # pourcent: 1-(1-P2)*(1-P5)
- # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all
- # percentage present after the fix amount)
- line_elements = element.xpath('.//ScontoMaggiorazione')
- total_discount_amount = 0.0
- total_discount_percentage = percentage_global_discount
- if line_elements:
- for line_element in line_elements:
- discount_line = line_element.xpath('.//Tipo')
- discount_sign = -1
- if discount_line and discount_line[0].text == 'SC':
- discount_sign = 1
- discount_percentage = line_element.xpath('.//Percentuale')
- if discount_percentage and discount_percentage[0].text:
- pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign
- total_discount_percentage *= pourcentage_actual
- total_discount_amount *= pourcentage_actual
-
- discount_amount = line_element.xpath('.//Importo')
- if discount_amount and discount_amount[0].text:
- total_discount_amount += float(discount_amount[0].text) * discount_sign * -1
-
- # Save amount discount.
- if total_discount_amount != 0:
- discount = {}
- discount["seq"] = invoice_line_form.sequence + 1
-
- if total_discount_amount < 0:
- discount["name"] = _('DISCOUNT: ') + invoice_line_form.name
- else:
- discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name
- discount["amount"] = total_discount_amount
- discount["tax"] = []
- for tax in invoice_line_form.tax_ids:
- discount["tax"].append(tax)
- discount_list.append(discount)
- invoice_line_form.discount = (1 - total_discount_percentage) * 100
-
- # Apply amount discount.
- for discount in discount_list:
- with invoice_form.invoice_line_ids.new() as invoice_line_form_discount:
- invoice_line_form_discount.tax_ids.clear()
- invoice_line_form_discount.sequence = discount["seq"]
- invoice_line_form_discount.name = discount["name"]
- invoice_line_form_discount.price_unit = discount["amount"]
-
- new_invoice = invoice_form.save()
- new_invoice.l10n_it_send_state = "other"
-
- elements = body_tree.xpath('.//Allegati')
- if elements:
- for element in elements:
- name_attachment = element.xpath('.//NomeAttachment')[0].text
- attachment_64 = str.encode(element.xpath('.//Attachment')[0].text)
- attachment_64 = self.env['ir.attachment'].create({
- 'name': name_attachment,
- 'datas': attachment_64,
- 'type': 'binary',
- })
-
- # default_res_id is had to context to avoid facturx to import his content
- # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it
- new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post(
- body=(_("Attachment from XML")),
- attachment_ids=[attachment_64.id]
- )
-
- for message in message_to_log:
- new_invoice.message_post(body=message)
-
- invoices += new_invoice
- return invoices
-
def _compose_info_message(self, tree, element_tags):
output_str = ""
elements = tree.xpath(element_tags)
@@ -703,20 +335,6 @@ class AccountMove(models.Model):
output_str += "<li>%s: %s</li>" % (element.tag, text)
return output_str + "</ul>"
- @api.model
- def _get_xml_decoders(self):
- # Override
- ubl_decoders = [('Italy', self._detect_italy_edi, self._import_xml_invoice)]
- return super(AccountMove, self)._get_xml_decoders() + ubl_decoders
-
- @api.model
- def _detect_italy_edi(self, tree, file_name):
- # Quick check the file name looks like the one of an Italian EDI XML.
- flag = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", file_name)
- error = None
-
- return {'flag': flag, 'error': error}
-
class AccountTax(models.Model):
_name = "account.tax"
_inherit = "account.tax"
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index 24ccd392a8e..8830af16b93 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -140,7 +140,7 @@ class FetchmailServer(models.Model):
except Exception:
raise UserError(_('The xml file is badly formatted : {}').format(att_name))
- invoice = self.env['account.move']._import_xml_invoice(tree)
+ 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()