[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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user