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()