diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account_edi_ubl/models/account_edi_format.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.py | 201 |
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 |
