summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorBenjamin Frantzen (bfr) <bfr@odoo.com>2020-05-26 07:47:30 +0000
committerBenjamin Frantzen (bfr) <bfr@odoo.com>2020-05-29 07:16:30 +0000
commit5b5ee381398090d6af9ce68041babb9953cd9e73 (patch)
treed2e3161353a034f236960a0c9629035839833cea /addons
parent9e3d70c3fadd57e7a7588a3749a3a79b29f17bec (diff)
[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
Diffstat (limited to 'addons')
-rw-r--r--addons/l10n_be_edi/__manifest__.py3
-rw-r--r--addons/l10n_be_edi/data/account_edi_data.xml11
-rw-r--r--addons/l10n_be_edi/models/__init__.py5
-rw-r--r--addons/l10n_be_edi/models/account_edi_format.py46
-rw-r--r--addons/l10n_be_edi/models/account_invoice.py181
-rw-r--r--addons/l10n_be_edi/models/account_move.py16
-rw-r--r--addons/l10n_be_edi/tests/test_ubl.py3
7 files changed, 81 insertions, 184 deletions
diff --git a/addons/l10n_be_edi/__manifest__.py b/addons/l10n_be_edi/__manifest__.py
index eb4be7f9998..c9bf6a9b841 100644
--- a/addons/l10n_be_edi/__manifest__.py
+++ b/addons/l10n_be_edi/__manifest__.py
@@ -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,
diff --git a/addons/l10n_be_edi/data/account_edi_data.xml b/addons/l10n_be_edi/data/account_edi_data.xml
new file mode 100644
index 00000000000..de4c58d4ba9
--- /dev/null
+++ b/addons/l10n_be_edi/data/account_edi_data.xml
@@ -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> \ No newline at end of file
diff --git a/addons/l10n_be_edi/models/__init__.py b/addons/l10n_be_edi/models/__init__.py
index ab922af0d1e..047fc3ef4d9 100644
--- a/addons/l10n_be_edi/models/__init__.py
+++ b/addons/l10n_be_edi/models/__init__.py
@@ -1,3 +1,4 @@
-# -*- coding: utf-8 -*-
+# -*- encoding: utf-8 -*-
-from . import account_invoice
+from . import account_edi_format
+from . import account_move
diff --git a/addons/l10n_be_edi/models/account_edi_format.py b/addons/l10n_be_edi/models/account_edi_format.py
new file mode 100644
index 00000000000..23bdf93c953
--- /dev/null
+++ b/addons/l10n_be_edi/models/account_edi_format.py
@@ -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.
diff --git a/addons/l10n_be_edi/models/account_invoice.py b/addons/l10n_be_edi/models/account_invoice.py
deleted file mode 100644
index f0acf9ecb9c..00000000000
--- a/addons/l10n_be_edi/models/account_invoice.py
+++ /dev/null
@@ -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
diff --git a/addons/l10n_be_edi/models/account_move.py b/addons/l10n_be_edi/models/account_move.py
new file mode 100644
index 00000000000..50ce265a54a
--- /dev/null
+++ b/addons/l10n_be_edi/models/account_move.py
@@ -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
diff --git a/addons/l10n_be_edi/tests/test_ubl.py b/addons/l10n_be_edi/tests/test_ubl.py
index cbbf385b459..1c4348a2aa7 100644
--- a/addons/l10n_be_edi/tests/test_ubl.py
+++ b/addons/l10n_be_edi/tests/test_ubl.py
@@ -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)