From c95a3e20d3de25f7c99350930c1f18f604a3cb1a Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Tue, 8 Sep 2020 13:09:32 +0000 Subject: [PATCH] [IMP] account_edi, account_edi_ubl, account_edi_facturx: added helpers to retrieve partner, product and taxes When parsing XML to import invoice, different edi will need to find records for partner, product and taxes based on the values on the xml. The helpers avoids code duplication. --- .../account_edi/models/account_edi_format.py | 82 +++++++++++++++++- addons/account_edi_facturx/__manifest__.py | 9 +- .../models/account_edi_format.py | 75 ++++++----------- .../test_file/test_facturx.xml | 9 ++ addons/account_edi_ubl/__manifest__.py | 10 ++- .../models/account_edi_format.py | 84 ++++++------------- 6 files changed, 151 insertions(+), 118 deletions(-) diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py index d22519e4bf2..ef3ce16166d 100644 --- a/addons/account_edi/models/account_edi_format.py +++ b/addons/account_edi/models/account_edi_format.py @@ -4,6 +4,7 @@ from odoo import models, fields, api from odoo.exceptions import UserError from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter +from odoo.osv import expression from lxml import etree import base64 @@ -341,9 +342,9 @@ class AccountEdiFormat(models.Model): res = False try: if file_data['type'] == 'xml': - res = edi_format._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree']) + res = edi_format.with_company(self.env.company)._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree']) elif file_data['type'] == 'pdf': - res = edi_format._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader']) + res = edi_format.with_company(self.env.company)._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader']) file_data['pdf_reader'].stream.close() except Exception as e: _logger.exception("Error importing attachment \"%s\" as invoice with format \"%s\"", file_data['filename'], edi_format.name, str(e)) @@ -366,9 +367,9 @@ class AccountEdiFormat(models.Model): res = False try: if file_data['type'] == 'xml': - res = edi_format._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice) + res = edi_format.with_company(self.env.company)._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice) elif file_data['type'] == 'pdf': - res = edi_format._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice) + res = edi_format.with_company(self.env.company)._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice) file_data['pdf_reader'].stream.close() except Exception as e: _logger.exception("Error importing attachment \"%s\" as invoice with format \"%s\"", file_data['filename'], edi_format.name, str(e)) @@ -379,3 +380,76 @@ class AccountEdiFormat(models.Model): res.write({'extract_state': 'done'}) return res return self.env['account.move'] + + #################################################### + # Import helpers + #################################################### + + def _find_value(self, xpath, xml_element, namespaces=None): + element = xml_element.xpath(xpath, namespaces=namespaces) + return element[0].text if element else None + + def _retrieve_partner(self, name=None, phone=None, mail=None, vat=None): + '''Search all partners and find one that matches one of the parameters. + + :param name: The name of the partner. + :param phone: The phone or mobile of the partner. + :param mail: The mail of the partner. + :param vat: The vat number of the partner. + :returns: A partner or an empty recordset if not found. + ''' + domains = [] + for value, domain in ( + (name, [('name', 'ilike', name)]), + (phone, expression.OR([[('phone', '=', phone)], [('mobile', '=', phone)]])), + (mail, [('email', '=', mail)]), + (vat, [('vat', 'like', vat)]), + ): + if value is not None: + domains.append(domain) + + domain = expression.OR(domains) + return self.env['res.partner'].search(domain, limit=1) + + def _retrieve_product(self, name=None, default_code=None, ean13=None, barcode=None): + '''Search all products and find one that matches one of the parameters. + + :param name: The name of the product. + :param default_code: The default_code of the product. + :param ean13: The ean13 of the product. + :returns: A product or an empty recordset if not found. + ''' + domains = [] + for value, domain in ( + (name, ('name', 'ilike', name)), + (default_code, ('default_code', '=', default_code)), + (ean13, ('ean13', '=', ean13)), + (barcode, ('barcode', '=', barcode)), + ): + if value is not None: + domains.append([domain]) + + domain = expression.OR(domains) + return self.env['product.product'].search(domain, limit=1) + + def _retrieve_tax(self, amount, type_tax_use): + '''Search all taxes and find one that matches all of the parameters. + + :param amount: The amount of the tax. + :param type_tax_use: The type of the tax. + :returns: A tax or an empty recordset if not found. + ''' + domains = [ + [('amount', '=', float(amount))], + [('type_tax_use', '=', type_tax_use)] + ] + + return self.env['account.tax'].search(expression.AND(domains), order='sequence ASC', limit=1) + + def _retrieve_currency(self, code): + '''Search all currencies and find one that matches the code. + + :param code: The code of the currency. + :returns: A currency or an empty recordset if not found. + ''' + return self.env['res.currency'].search([('name', '=', code.upper())], limit=1) diff --git a/addons/account_edi_facturx/__manifest__.py b/addons/account_edi_facturx/__manifest__.py index 3e7e16ffb11..afda4077736 100644 --- a/addons/account_edi_facturx/__manifest__.py +++ b/addons/account_edi_facturx/__manifest__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- { - 'name' : 'Import/Export invoices with Factur-X', - 'version' : '1.0', + 'name': 'Import/Export invoices with Factur-X', + 'description': ''' + Support for invoice Export/Import in Factur-x format (1.0.04). + ''', + 'version': '1.0', 'category': 'Accounting/Accounting', - 'depends' : ['account_edi'], + 'depends': ['account_edi'], 'data': [ 'data/account_edi_data.xml', 'data/facturx_templates.xml', diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py index d7a9483c2e8..196553ddc40 100644 --- a/addons/account_edi_facturx/models/account_edi_format.py +++ b/addons/account_edi_facturx/models/account_edi_format.py @@ -23,13 +23,6 @@ DEFAULT_FACTURX_DATE_FORMAT = '%Y%m%d' class AccountEdiFormat(models.Model): _inherit = 'account.edi.format' - def _is_compatible_with_journal(self, journal): - self.ensure_one() - res = super()._is_compatible_with_journal(journal) - if self.code != 'facturx_1_0_05': - return res - return journal.type == 'sale' - def _post_invoice_edi(self, invoices, test_mode=False): self.ensure_one() if self.code != 'facturx_1_0_05': @@ -138,6 +131,9 @@ class AccountEdiFormat(models.Model): :returns: the invoice where the factur-x data was imported. """ + def _find_value(xpath, element=tree): + return self._find_value(xpath, element, tree.nsmap) + amount_total_import = None default_move_type = False @@ -183,19 +179,12 @@ class AccountEdiFormat(models.Model): # self could be a single record (editing) or be empty (new). with Form(invoice.with_context(default_move_type=default_move_type)) as invoice_form: - # Partner (first step to avoid warning 'Warning! You must first select a partner.'). partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty' - elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap) - partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1) - if not partner: - elements = tree.xpath('//ram:' + partner_type + '/ram:Name', namespaces=tree.nsmap) - partner_name = elements and elements[0].text - partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1) - if not partner: - elements = tree.xpath('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap) - partner = elements and self.env['res.partner'].search([('email', '=', elements[0].text)], limit=1) - if partner: - invoice_form.partner_id = partner + invoice_form.partner_id = self._retrieve_partner( + name=_find_value('//ram:' + partner_type + '/ram:Name'), + mail=_find_value('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']'), + vat=_find_value('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID'), + ) # Reference. elements = tree.xpath('//rsm:ExchangedDocument/ram:ID', namespaces=tree.nsmap) @@ -217,11 +206,9 @@ class AccountEdiFormat(models.Model): if elements: # Currency. - if elements[0].attrib.get('currencyID'): - currency_str = elements[0].attrib['currencyID'] - 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 + currency_str = elements[0].attrib.get('currencyID', None) + if currency_str: + invoice_form.currency_id = self._retrieve_currency(currency_str) # Store xml total amount. amount_total_import = total_amount * refund_sign @@ -252,20 +239,14 @@ class AccountEdiFormat(models.Model): invoice_line_form.sequence = int(line_elements[0].text) # Product. - line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:Name', namespaces=tree.nsmap) - if line_elements: - invoice_line_form.name = line_elements[0].text - line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:SellerAssignedID', namespaces=tree.nsmap) - if line_elements and line_elements[0].text: - product = self.env['product.product'].search([('default_code', '=', line_elements[0].text)]) - if product: - invoice_line_form.product_id = product - if not invoice_line_form.product_id: - line_elements = element.xpath('.//ram:SpecifiedTradeProduct/ram:GlobalID', namespaces=tree.nsmap) - if line_elements and line_elements[0].text: - product = self.env['product.product'].search([('barcode', '=', line_elements[0].text)]) - if product: - invoice_line_form.product_id = product + name = _find_value('.//ram:SpecifiedTradeProduct/ram:Name', element) + if name: + invoice_line_form.name = name + invoice_line_form.product_id = self._retrieve_product( + default_code=_find_value('.//ram:SpecifiedTradeProduct/ram:SellerAssignedID', element), + name=_find_value('.//ram:SpecifiedTradeProduct/ram:Name', element), + barcode=_find_value('.//ram:SpecifiedTradeProduct/ram:GlobalID', element) + ) # Quantity. line_elements = element.xpath('.//ram:SpecifiedLineTradeDelivery/ram:BilledQuantity', namespaces=tree.nsmap) @@ -287,20 +268,16 @@ class AccountEdiFormat(models.Model): invoice_line_form.discount = float(line_elements[0].text) # Taxes - line_elements = element.xpath('.//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', namespaces=tree.nsmap) + tax_element = element.xpath('.//ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent', namespaces=tree.nsmap) invoice_line_form.tax_ids.clear() - for tax_element in line_elements: - percentage = float(tax_element.text) - - tax = self.env['account.tax'].search([ - ('company_id', '=', invoice_form.company_id.id), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', invoice_form.journal_id.type), - ('amount', '=', percentage), - ], limit=1) - + for eline in tax_element: + tax = self._retrieve_tax( + amount=eline.text, + type_tax_use=invoice_form.journal_id.type + ) if tax: invoice_line_form.tax_ids.add(tax) + elif amount_total_import: # No lines in BASICWL. with invoice_form.invoice_line_ids.new() as invoice_line_form: diff --git a/addons/account_edi_facturx/test_file/test_facturx.xml b/addons/account_edi_facturx/test_file/test_facturx.xml index 22b3ffdfcbd..b01acbf2f90 100644 --- a/addons/account_edi_facturx/test_file/test_facturx.xml +++ b/addons/account_edi_facturx/test_file/test_facturx.xml @@ -68,6 +68,9 @@ Four person modern office workstation + + 0.0 + 1400.00 @@ -95,6 +98,9 @@ Four person modern office workstation San Francisco US + + VAT789 + @@ -116,6 +122,9 @@ Four person modern office workstation Fremont US + + VAT12345 + diff --git a/addons/account_edi_ubl/__manifest__.py b/addons/account_edi_ubl/__manifest__.py index afa10b999c1..7502b86b97a 100644 --- a/addons/account_edi_ubl/__manifest__.py +++ b/addons/account_edi_ubl/__manifest__.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- { - 'name' : 'Import/Export invoices with generic UBL', - 'version' : '1.0', + 'name': 'Import/Export invoices with generic UBL', + 'description': ''' + Support for Export/Import in UBL format (2.1). + ''', + 'version': '1.0', 'category': 'Accounting/Accounting', - 'depends' : ['account_edi'], + 'depends': ['account_edi'], 'data': [ 'data/ubl_templates.xml', ], 'installable': True, 'application': False, - 'auto_install': True, } diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py index 7aa98dc9806..647f695bafa 100644 --- a/addons/account_edi_ubl/models/account_edi_format.py +++ b/addons/account_edi_ubl/models/account_edi_format.py @@ -37,6 +37,10 @@ class AccountEdiFormat(models.Model): return namespaces namespaces = _get_ubl_namespaces() + + def _find_value(xpath, element=tree): + return self._find_value(xpath, element, namespaces) + if not invoice: invoice = self.env['account.move'].create({}) @@ -70,10 +74,8 @@ class AccountEdiFormat(models.Model): 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: + currency = self._retrieve_currency(_find_value('//cbc:DocumentCurrencyCode')) + if currency: invoice_form.currency_id = currency # Incoterm @@ -82,36 +84,12 @@ class AccountEdiFormat(models.Model): 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)]) - else: - 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 - partner_name = partner.name - else: - invoice_form.partner_id = self.env['res.partner'] + invoice_form.partner_id = self._retrieve_partner( + name=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:Name'), + phone=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone'), + mail=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:ElectronicMail'), + vat=_find_value('//cac:AccountingSupplierParty/cac:Party//cbc:ID'), + ) # Regenerate PDF attachments = self.env['ir.attachment'] @@ -135,19 +113,11 @@ class AccountEdiFormat(models.Model): for eline in lines_elements: with invoice_form.invoice_line_ids.new() as invoice_line_form: # 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 + invoice_line_form.product_id = self._retrieve_product( + default_code=_find_value('cac:Item/cac:SellersItemIdentification/cbc:ID', eline), + name=_find_value('cac:Item/cbc:Name', eline), + ean13=_find_value('cac:Item/cac:StandardItemIdentification/cbc:ID[@schemeID=\'0160\']', eline) + ) # Quantity elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces) @@ -168,20 +138,18 @@ class AccountEdiFormat(models.Model): 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)) else: + partner_name = _find_value('//cac:AccountingSupplierParty/cac:Party//cbc:Name') invoice_line_form.name = "%s (%s)" % (partner_name or '', invoice_form.invoice_date) # Taxes - taxes_elements = eline.xpath('cac:TaxTotal/cac:TaxSubtotal', namespaces=namespaces) + tax_element = 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) + for eline in tax_element: + tax = self._retrieve_tax( + amount=_find_value('cbc:Percent', eline), + type_tax_use=invoice_form.journal_id.type + ) + if tax: + invoice_line_form.tax_ids.add(tax) return invoice_form.save()