diff options
| author | Benjamin Frantzen (bfr) <bfr@odoo.com> | 2020-05-26 07:47:30 +0000 |
|---|---|---|
| committer | Benjamin Frantzen (bfr) <bfr@odoo.com> | 2020-05-29 07:16:30 +0000 |
| commit | 5b5ee381398090d6af9ce68041babb9953cd9e73 (patch) | |
| tree | d2e3161353a034f236960a0c9629035839833cea | |
| parent | 9e3d70c3fadd57e7a7588a3749a3a79b29f17bec (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
| -rw-r--r-- | addons/l10n_be_edi/__manifest__.py | 3 | ||||
| -rw-r--r-- | addons/l10n_be_edi/data/account_edi_data.xml | 11 | ||||
| -rw-r--r-- | addons/l10n_be_edi/models/__init__.py | 5 | ||||
| -rw-r--r-- | addons/l10n_be_edi/models/account_edi_format.py | 46 | ||||
| -rw-r--r-- | addons/l10n_be_edi/models/account_invoice.py | 181 | ||||
| -rw-r--r-- | addons/l10n_be_edi/models/account_move.py | 16 | ||||
| -rw-r--r-- | addons/l10n_be_edi/tests/test_ubl.py | 3 |
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) |
