[IMP] l10n_be_edi: adaptation to account_edi + export in EFFF
- Support for account_edi workflows + efff as an account.edi.format - Added export to EFFF. - Based on UBL (account_edi_ubl). TASK ID : 2154703
This commit is contained in:
@@ -11,8 +11,9 @@ invoices. The UBL standard became the `ISO/IEC 19845
|
||||
(cf the `official announce <http://www.prweb.com/releases/2016/01/prweb13186919.htm>`_).
|
||||
Belgian e-invoicing uses the UBL 2.0 using the e-fff protocol.
|
||||
""",
|
||||
'depends': ['account', 'l10n_be', 'account_facturx'],
|
||||
'depends': ['l10n_be', 'account_edi'],
|
||||
'data': [
|
||||
'data/account_edi_data.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="edi_efff_1" model="account.edi.format">
|
||||
<field name="name">E-FFF (BE)</field>
|
||||
<field name="code">efff_1</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_edi_format
|
||||
from . import account_move
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, fields
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
def _is_efff(self, filename, tree):
|
||||
return self.code == 'efff_1' and tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice'
|
||||
|
||||
def _create_invoice_from_xml_tree(self, filename, tree):
|
||||
self.ensure_one()
|
||||
if self._is_efff(filename, tree):
|
||||
return self._import_ubl(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_efff(filename, tree):
|
||||
return self._import_ubl(tree, invoice)
|
||||
return super()._update_invoice_from_xml_tree(filename, tree, invoice)
|
||||
|
||||
def _export_invoice_to_attachment(self, invoice):
|
||||
self.ensure_one()
|
||||
if self.code != 'efff_1':
|
||||
return super()._export_invoice_to_attachment(invoice)
|
||||
|
||||
# Create file content.
|
||||
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
xml_content += self.env.ref('account_edi_ubl.export_ubl_invoice')._render(invoice._get_ubl_values())
|
||||
vat = invoice.company_id.partner_id.commercial_partner_id.vat
|
||||
xml_name = 'efff-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_')) # official naming convention
|
||||
return {'name': xml_name,
|
||||
'datas': xml_content,
|
||||
'res_model': 'account.move',
|
||||
'res_id': invoice._origin.id,
|
||||
'mimetype': 'application/xml'
|
||||
}
|
||||
|
||||
def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice):
|
||||
self.ensure_one()
|
||||
if self.code != 'efff_1':
|
||||
return super()._export_invoice_to_embed_to_pdf(pdf_content, invoice)
|
||||
return False # ubl must not be embedded to PDF.
|
||||
@@ -1,181 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.tests.common import Form
|
||||
from odoo.osv import expression
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
@api.model
|
||||
def _get_ubl_namespaces(self, tree):
|
||||
''' If the namespace is declared with xmlns='...', the namespaces map contains the 'None' key that causes an
|
||||
TypeError: empty namespace prefix is not supported in XPath
|
||||
Then, we need to remap arbitrarily this key.
|
||||
|
||||
:param tree: An instance of etree.
|
||||
:return: The namespaces map without 'None' key.
|
||||
'''
|
||||
namespaces = tree.nsmap
|
||||
namespaces['inv'] = namespaces.pop(None)
|
||||
return namespaces
|
||||
|
||||
@api.model
|
||||
def _detect_ubl_2_1(self, tree, file_name):
|
||||
# Quick check the tree looks like an UBL 2.1 file.
|
||||
flag = tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice'
|
||||
error = None
|
||||
|
||||
return {'flag': flag, 'error': error}
|
||||
|
||||
@api.model
|
||||
def _decode_ubl_2_1(self, tree):
|
||||
self.ensure_one()
|
||||
namespaces = self._get_ubl_namespaces(tree)
|
||||
|
||||
elements = tree.xpath('//cbc:InvoiceTypeCode', namespaces=namespaces)
|
||||
if elements:
|
||||
type_code = elements[0].text
|
||||
type = 'in_refund' if type_code == '381' else 'in_invoice'
|
||||
else:
|
||||
type = 'in_invoice'
|
||||
|
||||
default_journal = self.with_context(default_move_type=type)._get_default_journal()
|
||||
|
||||
with Form(self.with_context(default_move_type=type, default_journal_id=default_journal.id)) as invoice_form:
|
||||
# Reference
|
||||
elements = tree.xpath('//cbc:ID', namespaces=namespaces)
|
||||
if elements:
|
||||
invoice_form.ref = elements[0].text
|
||||
elements = tree.xpath('//cbc:InstructionID', namespaces=namespaces)
|
||||
if elements:
|
||||
invoice_form.payment_reference = elements[0].text
|
||||
|
||||
# Dates
|
||||
elements = tree.xpath('//cbc:IssueDate', namespaces=namespaces)
|
||||
if elements:
|
||||
invoice_form.invoice_date = elements[0].text
|
||||
elements = tree.xpath('//cbc:PaymentDueDate', namespaces=namespaces)
|
||||
if elements:
|
||||
invoice_form.invoice_date_due = elements[0].text
|
||||
# allow both cbc:PaymentDueDate and cbc:DueDate
|
||||
elements = tree.xpath('//cbc:DueDate', namespaces=namespaces)
|
||||
invoice_form.invoice_date_due = invoice_form.invoice_date_due or elements and elements[0].text
|
||||
|
||||
# Currency
|
||||
elements = tree.xpath('//cbc:DocumentCurrencyCode', namespaces=namespaces)
|
||||
currency_code = elements and elements[0].text or ''
|
||||
currency = self.env['res.currency'].search([('name', '=', currency_code.upper())], limit=1)
|
||||
if elements:
|
||||
invoice_form.currency_id = currency
|
||||
|
||||
# Incoterm
|
||||
elements = tree.xpath('//cbc:TransportExecutionTerms/cac:DeliveryTerms/cbc:ID', namespaces=namespaces)
|
||||
if elements:
|
||||
invoice_form.invoice_incoterm_id = self.env['account.incoterms'].search([('code', '=', elements[0].text)], limit=1)
|
||||
|
||||
# Partner
|
||||
partner_element = tree.xpath('//cac:AccountingSupplierParty/cac:Party', namespaces=namespaces)
|
||||
if partner_element:
|
||||
domains = []
|
||||
partner_element = partner_element[0]
|
||||
elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Name', namespaces=namespaces)
|
||||
if elements:
|
||||
partner_name = elements[0].text
|
||||
domains.append([('name', 'ilike', partner_name)])
|
||||
elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone', namespaces=namespaces)
|
||||
if elements:
|
||||
partner_telephone = elements[0].text
|
||||
domains.append([('phone', '=', partner_telephone), ('mobile', '=', partner_telephone)])
|
||||
elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ElectronicMail', namespaces=namespaces)
|
||||
if elements:
|
||||
partner_mail = elements[0].text
|
||||
domains.append([('email', '=', partner_mail)])
|
||||
elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:ID', namespaces=namespaces)
|
||||
if elements:
|
||||
partner_id = elements[0].text
|
||||
domains.append([('vat', 'like', partner_id)])
|
||||
|
||||
if domains:
|
||||
partner = self.env['res.partner'].search(expression.OR(domains), limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
else:
|
||||
invoice_form.partner_id = self.env['res.partner']
|
||||
|
||||
# Regenerate PDF
|
||||
attachments = self.env['ir.attachment']
|
||||
elements = tree.xpath('//cac:AdditionalDocumentReference', namespaces=namespaces)
|
||||
for element in elements:
|
||||
attachment_name = element.xpath('cbc:ID', namespaces=namespaces)
|
||||
attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces)
|
||||
if attachment_name and attachment_data:
|
||||
attachments |= self.env['ir.attachment'].create({
|
||||
'name': attachment_name[0].text,
|
||||
'res_id': self.id,
|
||||
'res_model': 'account.move',
|
||||
'datas': attachment_data[0].text,
|
||||
'type': 'binary',
|
||||
})
|
||||
if attachments:
|
||||
self.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
|
||||
# Lines
|
||||
lines_elements = tree.xpath('//cac:InvoiceLine', namespaces=namespaces)
|
||||
for eline in lines_elements:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
||||
# Quantity
|
||||
elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces)
|
||||
quantity = elements and float(elements[0].text) or 1.0
|
||||
invoice_line_form.quantity = quantity
|
||||
|
||||
# Price Unit
|
||||
elements = eline.xpath('cac:Price/cbc:PriceAmount', namespaces=namespaces)
|
||||
invoice_line_form.price_unit = elements and float(elements[0].text) or 0.0
|
||||
|
||||
# Name
|
||||
elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces)
|
||||
invoice_line_form.name = elements and elements[0].text or ''
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%month%', str(fields.Date.to_date(invoice_form.invoice_date).month)) # TODO: full name in locale
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%year%', str(fields.Date.to_date(invoice_form.invoice_date).year))
|
||||
|
||||
# Product
|
||||
elements = eline.xpath('cac:Item/cac:SellersItemIdentification/cbc:ID', namespaces=namespaces)
|
||||
domains = []
|
||||
if elements:
|
||||
product_code = elements[0].text
|
||||
domains.append([('default_code', '=', product_code)])
|
||||
elements = eline.xpath('cac:Item/cac:StandardItemIdentification/cbc:ID[@schemeID=\'GTIN\']', namespaces=namespaces)
|
||||
if elements:
|
||||
product_ean13 = elements[0].text
|
||||
domains.append([('ean13', '=', product_ean13)])
|
||||
if domains:
|
||||
product = self.env['product.product'].search(expression.OR(domains), limit=1)
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
|
||||
# Taxes
|
||||
taxes_elements = eline.xpath('cac:TaxTotal/cac:TaxSubtotal', namespaces=namespaces)
|
||||
invoice_line_form.tax_ids.clear()
|
||||
for etax in taxes_elements:
|
||||
elements = etax.xpath('cbc:Percent', namespaces=namespaces)
|
||||
if elements:
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', self.env.company.id),
|
||||
('amount', '=', float(elements[0].text)),
|
||||
('type_tax_use', '=', invoice_form.journal_id.type),
|
||||
], order='sequence ASC', limit=1)
|
||||
if tax:
|
||||
invoice_line_form.tax_ids.add(tax)
|
||||
|
||||
return invoice_form.save()
|
||||
|
||||
@api.model
|
||||
def _get_xml_decoders(self):
|
||||
# Override
|
||||
ubl_decoders = [('UBL 2.1', self._detect_ubl_2_1, self._decode_ubl_2_1)]
|
||||
return super(AccountMove, self)._get_xml_decoders() + ubl_decoders
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _get_ubl_values(self):
|
||||
values = super(AccountMove, self)._get_ubl_values()
|
||||
|
||||
# E-fff uses ubl_version 2.0, account_edi_ubl supports ubl_version 2.1 but generates 2.0 UBL
|
||||
# so we only need to override the version to be compatible with E-FFF
|
||||
values['ubl_version'] = 2.0
|
||||
|
||||
return values
|
||||
@@ -25,8 +25,11 @@ class TestUBL(common.TransactionCase):
|
||||
'res_model': 'account.move',
|
||||
})
|
||||
|
||||
invoice_count = len(self.env['account.move'].search([]))
|
||||
|
||||
invoice.message_post(attachment_ids=[attachment_id.id])
|
||||
|
||||
self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
|
||||
self.assertEqual(invoice.amount_total, 666.50)
|
||||
self.assertEqual(invoice.amount_tax, 115.67)
|
||||
self.assertEqual(invoice.partner_id, self.partner_id)
|
||||
|
||||
Reference in New Issue
Block a user