summaryrefslogtreecommitdiff
path: root/addons/account_edi_ubl/models/account_edi_format.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/account_edi_ubl/models/account_edi_format.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/account_edi_ubl/models/account_edi_format.py')
-rw-r--r--addons/account_edi_ubl/models/account_edi_format.py201
1 files changed, 201 insertions, 0 deletions
diff --git a/addons/account_edi_ubl/models/account_edi_format.py b/addons/account_edi_ubl/models/account_edi_format.py
new file mode 100644
index 00000000..981568bf
--- /dev/null
+++ b/addons/account_edi_ubl/models/account_edi_format.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, models, fields, tools, _
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr
+from odoo.tests.common import Form
+from odoo.exceptions import UserError
+from odoo.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