From ca797bb960b411d9f88e78b6ddb61d3b1a8df0d2 Mon Sep 17 00:00:00 2001 From: "Ali Alfie (alal)" Date: Wed, 5 Jul 2023 19:01:07 +0300 Subject: [PATCH] [IMP] l10n_es_edi_facturae: import facturae Allows for importing FACe XML files. task-3387230 X-original-commit: 6b970d73b8feac1903e271770f7df77fc2d96b71 Part-of: odoo/odoo#137321 --- .../models/account_move.py | 239 ++++++++++++- .../tests/data/import_multiple_invoices.xml | 338 ++++++++++++++++++ .../tests/data/import_withholding_invoice.xml | 260 ++++++++++++++ .../tests/test_edi_xml.py | 93 ++++- 4 files changed, 926 insertions(+), 4 deletions(-) create mode 100644 addons/l10n_es_edi_facturae/tests/data/import_multiple_invoices.xml create mode 100644 addons/l10n_es_edi_facturae/tests/data/import_withholding_invoice.xml diff --git a/addons/l10n_es_edi_facturae/models/account_move.py b/addons/l10n_es_edi_facturae/models/account_move.py index c2c40afa927..282d801f735 100644 --- a/addons/l10n_es_edi_facturae/models/account_move.py +++ b/addons/l10n_es_edi_facturae/models/account_move.py @@ -1,9 +1,12 @@ -from collections import defaultdict +import re -from odoo import fields, models, api, _ +from collections import defaultdict +from markupsafe import escape, Markup + +from odoo import fields, models, api, _, Command from odoo.exceptions import UserError from odoo.tools import float_repr, date_utils -from odoo.tools.xml_utils import cleanup_xml_node +from odoo.tools.xml_utils import cleanup_xml_node, find_xml_value PHONE_CLEAN_TABLE = str.maketrans({" ": None, "-": None, "(": None, ")": None, "+": None}) COUNTRY_CODE_MAP = { @@ -379,3 +382,233 @@ class AccountMove(models.Model): def _get_edi_doc_attachments_to_export(self): # EXTENDS 'account' return super()._get_edi_doc_attachments_to_export() + self.l10n_es_edi_facturae_xml_id + + # ------------------------------------------------------------------------- + # IMPORT + # ------------------------------------------------------------------------- + + def _get_edi_decoder(self, file_data, new=False): + def is_facturae(tree): + return tree.tag == '{http://www.facturae.es/Facturae/2014/v3.2.1/Facturae}Facturae' + + if file_data['type'] == 'xml' and is_facturae(file_data['xml_tree']): + return self._import_invoice_facturae + + return super()._get_edi_decoder(file_data, new=new) + + def _import_invoice_facturae(self, invoice, file_data, new=False): + tree = file_data['xml_tree'] + is_bill = invoice.move_type.startswith('in_') + partner = self._import_get_partner(tree, is_bill) + self._import_invoice_facturae_invoices(invoice, partner, tree) + + def _import_get_partner(self, tree, is_bill): + # If we're dealing with a vendor bill, then the partner is the seller party, if an invoice then it's the buyer. + party = tree.xpath('//SellerParty') if is_bill else tree.xpath('//BuyerParty') + if party: + partner_vals = self._import_extract_partner_values(party[0]) + return self._import_create_or_retrieve_partner(partner_vals) + return None + + def _import_extract_partner_values(self, party_node): + name = find_xml_value('.//CorporateName|.//Name', party_node) + first_surname = find_xml_value('.//FirstSurname', party_node) + second_surname = find_xml_value('.//SecondSurname', party_node) + phone = find_xml_value('.//Telephone', party_node) + mail = find_xml_value('.//ElectronicMail', party_node) + country_code = find_xml_value('.//CountryCode', party_node) + vat = find_xml_value('.//TaxIdentificationNumber', party_node) + + full_name = ' '.join(part for part in [name, first_surname, second_surname] if part) + + return {'name': full_name, 'vat': vat, 'phone': phone, 'email': mail, 'country_code': country_code} + + def _import_create_or_retrieve_partner(self, partner_vals): + name = partner_vals['name'] + vat = partner_vals['vat'] + phone = partner_vals['phone'] + mail = partner_vals['email'] + country_code = partner_vals['country_code'] + + partner = self.env['res.partner']._retrieve_partner(name=name, vat=vat, phone=phone, mail=mail) + + if not partner and name: + partner_vals = {'name': name, 'email': mail, 'phone': phone} + country = self.env['res.country'].search([('code', '=', country_code.lower())]) if country_code else False + if country: + partner_vals['country_id'] = country.id + partner = self.env['res.partner'].create(partner_vals) + if vat and self.env['res.partner']._run_vat_test(vat, country): + partner.vat = vat + + return partner + + def _import_invoice_facturae_invoices(self, invoice, partner, tree): + invoices = tree.xpath('//Invoice') + if not invoices: + return + + self._import_invoice_facturae_invoice(invoice, partner, invoices[0]) + + # There might be other invoices inside the facturae. + for node in invoices[1:]: + other_invoice = invoice.create({ + 'journal_id': invoice.journal_id.id, + 'move_type': invoice.move_type + }) + with other_invoice._get_edi_creation(): + self._import_invoice_facturae_invoice(other_invoice, partner, node) + other_invoice.message_post(body=escape(_("Created from attachment in %s")) % invoice._get_html_link()) + + def _import_invoice_facturae_invoice(self, invoice, partner, tree): + logs = [] + + # ==== move_type ==== + invoice_total = find_xml_value('.//InvoiceTotal', tree) + is_refund = float(invoice_total) < 0 if invoice_total else False + if is_refund: + invoice.move_type = "in_refund" if invoice.move_type.startswith("in_") else "out_refund" + ref_multiplier = -1.0 if is_refund else 1.0 + + # ==== partner_id ==== + if partner: + invoice.partner_id = partner + else: + logs.append(_("Customer/Vendor could not be found and could not be created due to missing data in the XML.")) + + # ==== currency_id ==== + invoice_currency_code = find_xml_value('.//InvoiceCurrencyCode', tree) + if invoice_currency_code: + currency = self.env['res.currency'].search([('name', '=', invoice_currency_code)], limit=1) + if currency: + invoice.currency_id = currency + else: + logs.append(_("Could not retrieve currency: %s. Did you enable the multicurrency option " + "and activate the currency?", invoice_currency_code)) + + # ==== invoice date ==== + if issue_date := find_xml_value('.//IssueDate', tree): + invoice.invoice_date = issue_date + + # ==== invoice_date_due ==== + if end_date := find_xml_value('.//InstallmentDueDate', tree): + invoice.invoice_date_due = end_date + + # ==== ref ==== + if invoice_number := find_xml_value('.//InvoiceNumber', tree): + invoice.ref = invoice_number + + # ==== narration ==== + invoice.narration = "\n".join( + ref.text + for ref in tree.xpath('.//LegalReference') + if ref.text + ) + + # === invoice_line_ids === + logs += self._import_invoice_fill_lines(invoice, tree, ref_multiplier) + + body = Markup("%s") % _("Invoice imported from Factura-E XML file.") + + if logs: + body += Markup("") \ + % Markup().join(Markup("
  • %s
  • ") % log for log in logs) + + invoice.message_post(body=body) + + return logs + + def _import_invoice_fill_lines(self, invoice, tree, ref_multiplier): + lines = tree.xpath('.//InvoiceLine') + logs = [] + vals_list = [] + for line in lines: + line_vals = {'move_id': invoice.id} + + # ==== name ==== + if item_description := find_xml_value('.//ItemDescription', line): + product = self._search_product_for_import(item_description) + if product: + line_vals['product_id'] = product.id + else: + logs.append(_("The product '%s' could not be found.", item_description)) + line_vals['name'] = item_description + + # ==== quantity ==== + line_vals['quantity'] = find_xml_value('.//Quantity', line) or 1 + + # ==== price_unit ==== + price_unit = find_xml_value('.//UnitPriceWithoutTax', line) + line_vals['price_unit'] = ref_multiplier * float(price_unit) if price_unit else 1.0 + + # ==== discount ==== + discounts = line.xpath('.//DiscountRate') + discount_rate = 0.0 + for discount in discounts: + discount_rate += float(discount.text) + + charges = line.xpath('.//ChargeRate') + charge_rate = 0.0 + for charge in charges: + charge_rate += float(charge.text) + + discount_rate -= charge_rate + line_vals['discount'] = discount_rate + + # ==== tax_ids ==== + taxes_withheld_nodes = line.xpath('.//TaxesWithheld/Tax') + taxes_outputs_nodes = line.xpath('.//TaxesOutputs/Tax') + is_purchase = invoice.move_type.startswith('in') + tax_ids = [] + logs += self._import_fill_invoice_line_taxes(invoice, line_vals, tax_ids, taxes_outputs_nodes, False, is_purchase) + logs += self._import_fill_invoice_line_taxes(invoice, line_vals, tax_ids, taxes_withheld_nodes, True, is_purchase) + line_vals['tax_ids'] = [Command.set(tax_ids)] + vals_list.append(line_vals) + + invoice.invoice_line_ids = self.env['account.move.line'].create(vals_list) + return logs + + def _import_fill_invoice_line_taxes(self, invoice, line_vals, tax_ids, tax_nodes, is_withheld, is_purchase): + logs = [] + for tax_node in tax_nodes: + tax_rate = find_xml_value('.//TaxRate', tax_node) + if tax_rate: + # Since the 'TaxRate' node isn't guaranteed to be a percentage, we can find out by + # applying the tax rate on the taxable base, and if it's equal to the tax amount + # then we can say this is a percentage, otherwise a fixed amount. + taxable_base = find_xml_value('.//TaxableBase/TotalAmount', tax_node) + tax_amount = find_xml_value('.//TaxAmount/TotalAmount', tax_node) + is_fixed = False + + if taxable_base and tax_amount and invoice.currency_id.compare_amounts(float(taxable_base) * (float(tax_rate) / 100), float(tax_amount)) != 0: + is_fixed = True + + tax_excl = self._search_tax_for_import(invoice.company_id, float(tax_rate), is_fixed, is_withheld, is_purchase, price_included=False) + + if tax_excl: + tax_ids.append(tax_excl.id) + elif tax_incl := self._search_tax_for_import(invoice.company_id, float(tax_rate), is_fixed, is_withheld, is_purchase, price_included=True): + tax_ids.append(tax_incl) + line_vals['price_unit'] *= (1.0 + float(tax_rate) / 100.0) + else: + logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", tax_rate, line_vals.get('name', ""))) + + return logs + + def _search_tax_for_import(self, company, amount, is_fixed, is_withheld, is_purchase, price_included): + taxes = self.env['account.tax'].search([ + ('company_id', '=', company.id), + ('amount', '=', -1.0 * amount if is_withheld else amount), + ('amount_type', '=', 'fixed' if is_fixed else 'percent'), + ('type_tax_use', '=', 'purchase' if is_purchase else 'sale'), + ('price_include', '=', price_included), + ], limit=1) + + return taxes + + def _search_product_for_import(self, item_description): + # Exported Odoo XML will have item_description = "[default_code] name". + # We can check if it follows the same format and search for the product with the default code and the name. + code_and_name = re.match(r"(\[(?P.*?)\]\s)?(?P.*)", item_description).groupdict() + product = self.env['product.product']._retrieve_product(**code_and_name) + return product diff --git a/addons/l10n_es_edi_facturae/tests/data/import_multiple_invoices.xml b/addons/l10n_es_edi_facturae/tests/data/import_multiple_invoices.xml new file mode 100644 index 00000000000..7f556bc6d47 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/import_multiple_invoices.xml @@ -0,0 +1,338 @@ + + + + 3.2.1 + I + EM + + INV/2023/00005 + 1 + + 2186.20 + + + 2186.20 + + + 2186.20 + + EUR + + + + + + F + R + ES59962470K + + + +
    A
    + 22591 + Candasnos + Zaragoza + ESP +
    + + http://www.esexample.com + info@company.esexample.com + +
    +
    + + + J + E + US12345677 + + + Azure Interior + Azure Interior + +
    4557 De Silva St
    + 94538 Fremont + California + USA +
    + + 8709310505 + http://www.azure-interior.com + azure.Interior24@example.com + +
    +
    +
    + + + + INV/2023/00005 + FC + OO + + + 2023-08-01 + EUR + EUR + en + + + + 01 + 21.000 + + 320.00 + + + 67.20 + + + + 01 + 0.000 + + 1799.00 + + + 0.00 + + + + + 2119.00 + 2119.00 + 67.20 + 0.00 + 2186.20 + 2186.20 + 2186.20 + + + + [E-COM07] Large Cabinet + 1.0 + 01 + 320.00 + 320.00 + 320.00 + + + 01 + 21.000 + + 320.00 + + + 67.20 + + + + + + [E-COM09] Large Desk + 1.0 + 01 + 1799.00 + 1799.00 + 1799.00 + + + 01 + 0.000 + + 1799.00 + + + 0.00 + + + + + + + Terms and conditions. + + + + 2023-08-31 + + + + + + INV/2023/00006 + FC + OO + + + 2023-07-01 + EUR + EUR + en + + + + 01 + 21.000 + + 960.00 + + + 201.60 + + + + + 960.00 + 960.00 + 201.60 + 0.00 + 1161.60 + 1161.60 + 1161.60 + + + + [E-COM07] Large Cabinet + 3.0 + 01 + 320.00 + 960.00 + 960.00 + + + 01 + 21.000 + + 960.00 + + + 201.60 + + + + + + + Legal References. + + + + 2023-07-31 + + + + + + + + + + + + + + cElLMko1b3H9dDrcO0nLhBA0DM7NWpAVuFDk7r+HMxM= + + + + 1fyEQGjgpXKz9ie50e2WwFRk5kXklJNr10YHVMWBHeo= + + + + rUjANojsduwjEzBt8hX6sFsG9F1mwQ1X/rJhxY1MOGI= + + + ecCZUOChRyxRpMRo8PaqBDKsuiWf8IOavpwLDSaD1+ZjEKF20D7GUfMoDbMTmotpw+qybmuQQZVQ +CRpl+7efRdcIGNjKwI1hxdbwFd41SCU3HXZKJ5B6mOdJjW4uBnXce157lI4U/liyHmdRyW1K3WZc +RCh4GrJ5M7N662JfhVOLhZaKSXFI7+s517HD5mJ4tw84UG7j3OgAfVqH3BZe+oM8boTJAUtFyNlO +o8YWWWgvV6LNkqbKkP5J2dcrwQmaUuISeQ7+/BU0T7OfdbpFGtvz1PrI2d9YTFHrH+aWU5gZ96kX +cmr2Ab2uGYRNXqIpHEA9DcY0pkapd6CNgPv5sg== + + + + MIIEMTCCAxmgAwIBAgIUXMxVyBDULPbIaeKVK9bJwftsQdswDQYJKoZIhvcNAQELBQAwgacxCzAJ +BgNVBAYTAkJFMREwDwYDVQQIDAhXYWxsb25pYTEgMB4GA1UEBwwXR3JhbmQtUm9zaWVyZS1Ib3R0 +b21vbnQxFTATBgNVBAoMDFRlc3QgT2RvbyBTQTEXMBUGA1UECwwOUiZEIEFjY291bnRpbmcxFzAV +BgNVBAMMDlImRCBBY2NvdW50aW5nMRowGAYJKoZIhvcNAQkBFgtyZEBvZG9vLmNvbTAeFw0yMzAy +MDkwODU1NDhaFw0zMzAyMDYwODU1NDhaMIGnMQswCQYDVQQGEwJCRTERMA8GA1UECAwIV2FsbG9u +aWExIDAeBgNVBAcMF0dyYW5kLVJvc2llcmUtSG90dG9tb250MRUwEwYDVQQKDAxUZXN0IE9kb28g +U0ExFzAVBgNVBAsMDlImRCBBY2NvdW50aW5nMRcwFQYDVQQDDA5SJkQgQWNjb3VudGluZzEaMBgG +CSqGSIb3DQEJARYLcmRAb2Rvby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx +eId7+OTIUl/GWhSo+y+E+8yclmgdgWope+fyq4jYACSID2rrxcScAgmGcGfvcNJ7e4aUoqrGg7Ob +FIuDbGjVg5RFUjDljzvV97em7KuPf/qHVWAVXrKkD6JgV7KRp38RLyBLdgbwZqgP0QRBM6HLl1nC +H6TMrVtTOskjeJ9jd1VACBlMWFpaTQl4dcnM6W8o7e5OYSru6u9xuqyQQim06YyjwAuxfQEMO1OV +n3cQlrqpbOI5SNu3vi6eb0z4n1KEw8t2zeF+QrMSNzykb3CJVOaLI8uyYcZXow7RcSy0HZNu4TZS +nQRgODbxrFlcfSHz2hSqxWAXW4/He594Gbw9AgMBAAGjUzBRMB0GA1UdDgQWBBSjLlcJeL3Yg2aL +L7aElSBMhQqLXjAfBgNVHSMEGDAWgBSjLlcJeL3Yg2aLL7aElSBMhQqLXjAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCf9DCON7ehvD+x/EXLIDhJUzsWxyICj0T+cMw7BBrJVQ94 +6RJlR9m3sNwIy0VoYQKNJ1JGda15YVpppyJTVSj11Ig8rG6/l06ysNnzJ6+aA2ba1Z0ycFFkX+Cl +By7sIqAn+HbAJ6M4sAzQKvoWoaFA4uSbZGLLf2NKMNGIsTBO3rtSEt8zqJBTVuX03Sxp9mJIjIXi +HfrCCDyBtXxnnBpTnMFy74mHg2Xbu5si85nxnNzYC0RTbd7qxEO0OEBQGz5AMH/kyzNnyu9WvLlg +lfRpb8ioiJ9ZFLNIsZtRA60R5NWDpACwADa9WnfqzH8lHnEaIXwsziU+SSAIz8kSdCwQ + + + + + sXiHe/jkyFJfxloUqPsvhPvMnJZoHYFqKXvn8quI2AAkiA9q68XEnAIJhnBn73DSe3uGlKKqxoOz +mxSLg2xo1YOURVIw5Y871fe3puyrj3/6h1VgFV6ypA+iYFeykad/ES8gS3YG8GaoD9EEQTOhy5dZ +wh+kzK1bUzrJI3ifY3dVQAgZTFhaWk0JeHXJzOlvKO3uTmEq7urvcbqskEIptOmMo8ALsX0BDDtT +lZ93EJa6qWziOUjbt74unm9M+J9ShMPLds3hfkKzEjc8pG9wiVTmiyPLsmHGV6MO0XEstB2TbuE2 +Up0EYDg28axZXH0h89oUqsVgF1uPx3ufeBm8PQ== + + AQAB + + + + + + + + + 2023-08-31T09:51:34.576445 + + + + + cP4cRSOphs7/CsqPwdGujvo7xkSkh8CfvFPAsQf/zRE= + + + C=BE, CN=R&D Accounting, L=Grand-Rosiere-Hottomont, O=Test Odoo SA, OU=R&D Accounting, ST=Wallonia + 529783975559440394003723764779526236797615751643 + + + + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + Política de firma electrónica para facturación electrónica con formato Facturae + + + + a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8= + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + + + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + +
    diff --git a/addons/l10n_es_edi_facturae/tests/data/import_withholding_invoice.xml b/addons/l10n_es_edi_facturae/tests/data/import_withholding_invoice.xml new file mode 100644 index 00000000000..d6e94e9b0b2 --- /dev/null +++ b/addons/l10n_es_edi_facturae/tests/data/import_withholding_invoice.xml @@ -0,0 +1,260 @@ + + + + 3.2.1 + I + EM + + INV/2023/00009 + 1 + + 323.20 + + + 323.20 + + + 323.20 + + EUR + + + + + + F + R + ES59962470K + + + +
    A
    + 22591 + Candasnos + Zaragoza + ESP +
    + + http://www.esexample.com + info@company.esexample.com + +
    +
    + + + J + E + US12345677 + + + Azure Interior + Azure Interior + +
    4557 De Silva St
    + 94538 Fremont + California + USA +
    + + 8709310505 + http://www.azure-interior.com + azure.Interior24@example.com + +
    +
    +
    + + + + INV/2023/00009 + FC + OO + + + 2023-09-01 + EUR + EUR + en + + + + 01 + 21.000 + + 320.00 + + + 67.20 + + + + + + 01 + 20.000 + + 320.00 + + + 64.00 + + + + + 320.00 + 320.00 + 67.20 + 64.00 + 323.20 + 323.20 + 323.20 + + + + [E-COM07] Large Cabinet + 1.0 + 01 + 320.00 + 320.00 + 320.00 + + + 01 + 21.000 + + 320.00 + + + 67.20 + + + + + + 01 + 20.000 + + 320.00 + + + 64.00 + + + + + + + + + + + + + + + + + y+DiG9thnFpt6SvkhDhV/8PYAzfhj0KKTXnZjbCbfa8= + + + + jx6wLH7wWnh3HQD4ixOqAWULf2lT9BAxbbWDzxloxo4= + + + + pti8uQOjoaRhHT1DyGg8Z0BKxxhgQMfyrHMbdiHzS/M= + + + qSZAhTS36ed1Vl0hPHZHDrY1tJtr0gcjY7sgqZgw163wdqZYiV5WWmM8l6ttc1voNrrw1XCENXMz +Sur4lCKCQdIA400uvRM1tY8LoptM5GJswZoLnfZS2lkK8xAC8vDoBAKEaIVjuBd8Gi+Oztz6OiJG ++EIT8mMPmZ9jCt3XExMn8dZRaTeB6XwTH8GGyvgWjXVxEbSTD+BiRthORMeNwAdl3KiCrzgrYbNu +QaozjiZQGfpj2hJt2+zKvhpEML1nWRQUVrM2sjBxhhewQTmxavzG1xL3MHBzT5//YZYfsSNFJbE0 +imJThhtOwrZvWqgN/B8wfhrS2eZkue9w0cX2cg== + + + + MIIEMTCCAxmgAwIBAgIUXMxVyBDULPbIaeKVK9bJwftsQdswDQYJKoZIhvcNAQELBQAwgacxCzAJ +BgNVBAYTAkJFMREwDwYDVQQIDAhXYWxsb25pYTEgMB4GA1UEBwwXR3JhbmQtUm9zaWVyZS1Ib3R0 +b21vbnQxFTATBgNVBAoMDFRlc3QgT2RvbyBTQTEXMBUGA1UECwwOUiZEIEFjY291bnRpbmcxFzAV +BgNVBAMMDlImRCBBY2NvdW50aW5nMRowGAYJKoZIhvcNAQkBFgtyZEBvZG9vLmNvbTAeFw0yMzAy +MDkwODU1NDhaFw0zMzAyMDYwODU1NDhaMIGnMQswCQYDVQQGEwJCRTERMA8GA1UECAwIV2FsbG9u +aWExIDAeBgNVBAcMF0dyYW5kLVJvc2llcmUtSG90dG9tb250MRUwEwYDVQQKDAxUZXN0IE9kb28g +U0ExFzAVBgNVBAsMDlImRCBBY2NvdW50aW5nMRcwFQYDVQQDDA5SJkQgQWNjb3VudGluZzEaMBgG +CSqGSIb3DQEJARYLcmRAb2Rvby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx +eId7+OTIUl/GWhSo+y+E+8yclmgdgWope+fyq4jYACSID2rrxcScAgmGcGfvcNJ7e4aUoqrGg7Ob +FIuDbGjVg5RFUjDljzvV97em7KuPf/qHVWAVXrKkD6JgV7KRp38RLyBLdgbwZqgP0QRBM6HLl1nC +H6TMrVtTOskjeJ9jd1VACBlMWFpaTQl4dcnM6W8o7e5OYSru6u9xuqyQQim06YyjwAuxfQEMO1OV +n3cQlrqpbOI5SNu3vi6eb0z4n1KEw8t2zeF+QrMSNzykb3CJVOaLI8uyYcZXow7RcSy0HZNu4TZS +nQRgODbxrFlcfSHz2hSqxWAXW4/He594Gbw9AgMBAAGjUzBRMB0GA1UdDgQWBBSjLlcJeL3Yg2aL +L7aElSBMhQqLXjAfBgNVHSMEGDAWgBSjLlcJeL3Yg2aLL7aElSBMhQqLXjAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCf9DCON7ehvD+x/EXLIDhJUzsWxyICj0T+cMw7BBrJVQ94 +6RJlR9m3sNwIy0VoYQKNJ1JGda15YVpppyJTVSj11Ig8rG6/l06ysNnzJ6+aA2ba1Z0ycFFkX+Cl +By7sIqAn+HbAJ6M4sAzQKvoWoaFA4uSbZGLLf2NKMNGIsTBO3rtSEt8zqJBTVuX03Sxp9mJIjIXi +HfrCCDyBtXxnnBpTnMFy74mHg2Xbu5si85nxnNzYC0RTbd7qxEO0OEBQGz5AMH/kyzNnyu9WvLlg +lfRpb8ioiJ9ZFLNIsZtRA60R5NWDpACwADa9WnfqzH8lHnEaIXwsziU+SSAIz8kSdCwQ + + + + + sXiHe/jkyFJfxloUqPsvhPvMnJZoHYFqKXvn8quI2AAkiA9q68XEnAIJhnBn73DSe3uGlKKqxoOz +mxSLg2xo1YOURVIw5Y871fe3puyrj3/6h1VgFV6ypA+iYFeykad/ES8gS3YG8GaoD9EEQTOhy5dZ +wh+kzK1bUzrJI3ifY3dVQAgZTFhaWk0JeHXJzOlvKO3uTmEq7urvcbqskEIptOmMo8ALsX0BDDtT +lZ93EJa6qWziOUjbt74unm9M+J9ShMPLds3hfkKzEjc8pG9wiVTmiyPLsmHGV6MO0XEstB2TbuE2 +Up0EYDg28axZXH0h89oUqsVgF1uPx3ufeBm8PQ== + + AQAB + + + + + + + + + 2023-09-01T12:16:38.095681 + + + + + cP4cRSOphs7/CsqPwdGujvo7xkSkh8CfvFPAsQf/zRE= + + + C=BE, CN=R&D Accounting, L=Grand-Rosiere-Hottomont, O=Test Odoo SA, OU=R&D Accounting, ST=Wallonia + 529783975559440394003723764779526236797615751643 + + + + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + Política de firma electrónica para facturación electrónica con formato Facturae + + + + a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8= + + + + http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf + + + + + + + + + + urn:oid:1.2.840.10003.5.109.10 + + + text/xml + + + + + + + +
    diff --git a/addons/l10n_es_edi_facturae/tests/test_edi_xml.py b/addons/l10n_es_edi_facturae/tests/test_edi_xml.py index 3f8dc943f81..7b3973dd2f1 100644 --- a/addons/l10n_es_edi_facturae/tests/test_edi_xml.py +++ b/addons/l10n_es_edi_facturae/tests/test_edi_xml.py @@ -8,7 +8,7 @@ from unittest.mock import patch import lxml from freezegun import freeze_time -from odoo import Command +from odoo import Command, fields from odoo.addons.account_edi.tests.common import AccountEdiTestCommon from odoo.exceptions import UserError from odoo.tests import tagged @@ -251,3 +251,94 @@ class TestEdiFacturaeXmls(AccountEdiTestCommon): self.assertEqual(result['type'], 'ir.actions.act_url') self.assertEqual(invoice.invoice_line_ids[0].price_subtotal, 0.0) + + def test_import_multiple_invoices(self): + with file_open("l10n_es_edi_facturae/tests/data/import_multiple_invoices.xml", "rt") as f: + imported_xml = lxml.etree.fromstring(f.read().encode()) + + moves = self.env['account.move'].create({'move_type': 'out_invoice'}) + moves._import_invoice_facturae(moves, {'xml_tree': imported_xml}) + + moves += self.env['account.move'].search([('ref', '=', 'INV/2023/00006'), ('company_id', '=', self.company_data['company'].id)], limit=1) + + partner = self.env['res.partner'].search([ + ('name', '=', 'Azure Interior'), + ('email', '=', 'azure.Interior24@example.com'), + ]) + currency = self.env['res.currency'].search([('name', '=', 'EUR')]) + + self.assertRecordValues(moves, [ + { + 'partner_id': partner.id, + 'amount_total': 2186.20, + 'amount_untaxed': 2119.0, + 'amount_tax': 67.2, + 'move_type': 'out_invoice', + 'currency_id': currency.id, + 'invoice_date': fields.Date.from_string('2023-08-01'), + 'invoice_date_due': fields.Date.from_string('2023-08-31'), + 'ref': 'INV/2023/00005', + 'narration': '

    Terms and conditions.

    ', + }, + { + 'partner_id': partner.id, + 'amount_total': 1161.60, + 'amount_untaxed': 960.0, + 'amount_tax': 201.60, + 'move_type': 'out_invoice', + 'currency_id': currency.id, + 'invoice_date': fields.Date.from_string('2023-07-01'), + 'invoice_date_due': fields.Date.from_string('2023-07-31'), + 'ref': 'INV/2023/00006', + 'narration': '

    Legal References.

    ', + }, + ]) + + # Check first invoice's lines. + self.assertRecordValues(moves[0].invoice_line_ids, [ + { + 'name': '[E-COM07] Large Cabinet', + 'price_unit': 320.0, + 'quantity': 1.0, + 'price_total': 387.2, + 'discount': 0.0, + }, + { + 'name': '[E-COM09] Large Desk', + 'price_unit': 1799.0, + 'quantity': 1.0, + 'price_total': 1799, + 'discount': 0.0, + } + ]) + + # Check second invoice's lines. + self.assertRecordValues(moves[1].invoice_line_ids, [ + { + 'name': '[E-COM07] Large Cabinet', + 'price_unit': 320.0, + 'quantity': 3.0, + 'price_total': 1161.60, + 'discount': 0.0, + }, + ]) + + def test_import_withheld_taxes(self): + with file_open("l10n_es_edi_facturae/tests/data/import_withholding_invoice.xml", "rt") as f: + imported_xml = lxml.etree.fromstring(f.read().encode()) + + move = self.env['account.move'].create({'move_type': 'out_invoice'}) + move._import_invoice_facturae(move, {'xml_tree': imported_xml}) + + self.assertRecordValues(move, [ + { + 'amount_total': 323.2, + 'amount_untaxed': 320.0, + 'amount_tax': 3.2, + }, + ]) + + tax_amounts = [tax.amount for tax in move.invoice_line_ids.tax_ids] + + # Check first invoice's lines. + self.assertEqual(tax_amounts, [21.0, -20.0])