Files
flectra/addons/account_edi_ubl/models/account_edi_format.py
T
2021-05-31 08:13:27 +00:00

202 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, models, fields, tools, _
from flectra.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr
from flectra.tests.common import Form
from flectra.exceptions import UserError
from flectra.osv import expression
from pathlib import PureWindowsPath
import logging
_logger = logging.getLogger(__name__)
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
def _create_invoice_from_ubl(self, tree):
invoice = self.env['account.move']
journal = invoice._get_default_journal()
move_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice'
element = tree.find('.//{*}InvoiceTypeCode')
if element is not None and element.text == '381':
move_type = 'in_refund' if move_type == 'in_invoice' else 'out_refund'
invoice = invoice.with_context(default_move_type=move_type, default_journal_id=journal.id)
return self._import_ubl(tree, invoice)
def _update_invoice_from_ubl(self, tree, invoice):
invoice = invoice.with_context(default_move_type=invoice.move_type, default_journal_id=invoice.journal_id.id)
return self._import_ubl(tree, invoice)
def _import_ubl(self, tree, invoice):
""" Decodes an UBL invoice into an invoice.
:param tree: the UBL tree to decode.
:param invoice: the invoice to update or an empty recordset.
:returns: the invoice where the UBL data was imported.
"""
def _get_ubl_namespaces():
''' 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
namespaces = _get_ubl_namespaces()
with Form(invoice.with_context(account_predictive_bills_disable_prediction=True)) 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)])
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:CompanyID', 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']
# 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:
# 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([('barcode', '=', product_ean13)])
if domains:
product = self.env['product.product'].search(expression.OR(domains), limit=1)
if product:
invoice_line_form.product_id = product
# 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)
price_unit = elements and float(elements[0].text) or 0.0
elements = eline.xpath('cbc:LineExtensionAmount', namespaces=namespaces)
line_extension_amount = elements and float(elements[0].text) or 0.0
invoice_line_form.price_unit = price_unit or line_extension_amount / invoice_line_form.quantity or 0.0
# Name
elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces)
if elements and elements[0].text:
invoice_line_form.name = elements[0].text
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:
invoice_line_form.name = "%s (%s)" % (partner_name or '', invoice_form.invoice_date)
# 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)
invoice = invoice_form.save()
# 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:
text = attachment_data[0].text
# Normalize the name of the file : some e-fff emitters put the full path of the file
# (Windows or Linux style) and/or the name of the xml instead of the pdf.
# Get only the filename with a pdf extension.
name = PureWindowsPath(attachment_name[0].text).stem + '.pdf'
attachments |= self.env['ir.attachment'].create({
'name': name,
'res_id': invoice.id,
'res_model': 'account.move',
'datas': text + '=' * (len(text) % 3), # Fix incorrect padding
'type': 'binary',
'mimetype': 'application/pdf',
})
if attachments:
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
return invoice