[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.
This commit is contained in:
Benjamin Frantzen (bfr)
2020-09-10 13:28:30 +00:00
parent 68eb3a988a
commit c95a3e20d3
6 changed files with 151 additions and 118 deletions
@@ -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)
+6 -3
View File
@@ -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',
@@ -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:
@@ -68,6 +68,9 @@ Four person modern office workstation</ram:Name>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:RateApplicablePercent>0.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount currencyID="USD">1400.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
@@ -95,6 +98,9 @@ Four person modern office workstation</ram:Name>
<ram:CityName>San Francisco</ram:CityName>
<ram:CountryID>US</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">VAT789</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
@@ -116,6 +122,9 @@ Four person modern office workstation</ram:Name>
<ram:CityName>Fremont</ram:CityName>
<ram:CountryID>US</ram:CountryID>
</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">VAT12345</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:BuyerTradeParty>
<ram:BuyerOrderReferencedDocument>
+6 -4
View File
@@ -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,
}
@@ -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()