[IMP] l10n_es_edi_facturae: import facturae

Allows for importing FACe XML files.

task-3387230

X-original-commit: 6b970d73b8feac1903e271770f7df77fc2d96b71
Part-of: odoo/odoo#137321
This commit is contained in:
Ali Alfie (alal)
2023-10-03 07:19:01 +00:00
parent de13b9030b
commit ca797bb960
4 changed files with 926 additions and 4 deletions
@@ -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("<strong>%s</strong>") % _("Invoice imported from Factura-E XML file.")
if logs:
body += Markup("<ul>%s</ul>") \
% Markup().join(Markup("<li>%s</li>") % 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<default_code>.*?)\]\s)?(?P<name>.*)", item_description).groupdict()
product = self.env['product.product']._retrieve_product(**code_and_name)
return product
@@ -0,0 +1,338 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<fac:Facturae xmlns:fac="http://www.facturae.es/Facturae/2014/v3.2.1/Facturae">
<FileHeader>
<SchemaVersion>3.2.1</SchemaVersion>
<Modality>I</Modality>
<InvoiceIssuerType>EM</InvoiceIssuerType>
<Batch>
<BatchIdentifier>INV/2023/00005</BatchIdentifier>
<InvoicesCount>1</InvoicesCount>
<TotalInvoicesAmount>
<TotalAmount>2186.20</TotalAmount>
</TotalInvoicesAmount>
<TotalOutstandingAmount>
<TotalAmount>2186.20</TotalAmount>
</TotalOutstandingAmount>
<TotalExecutableAmount>
<TotalAmount>2186.20</TotalAmount>
</TotalExecutableAmount>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
</Batch>
</FileHeader>
<Parties>
<SellerParty>
<TaxIdentification>
<PersonTypeCode>F</PersonTypeCode>
<ResidenceTypeCode>R</ResidenceTypeCode>
<TaxIdentificationNumber>ES59962470K</TaxIdentificationNumber>
</TaxIdentification>
<Individual>
<AddressInSpain>
<Address>A</Address>
<PostCode>22591</PostCode>
<Town>Candasnos</Town>
<Province>Zaragoza</Province>
<CountryCode>ESP</CountryCode>
</AddressInSpain>
<ContactDetails>
<WebAddress>http://www.esexample.com</WebAddress>
<ElectronicMail>info@company.esexample.com</ElectronicMail>
</ContactDetails>
</Individual>
</SellerParty>
<BuyerParty>
<TaxIdentification>
<PersonTypeCode>J</PersonTypeCode>
<ResidenceTypeCode>E</ResidenceTypeCode>
<TaxIdentificationNumber>US12345677</TaxIdentificationNumber>
</TaxIdentification>
<LegalEntity>
<CorporateName>Azure Interior</CorporateName>
<TradeName>Azure Interior</TradeName>
<OverseasAddress>
<Address>4557 De Silva St</Address>
<PostCodeAndTown>94538 Fremont</PostCodeAndTown>
<Province>California</Province>
<CountryCode>USA</CountryCode>
</OverseasAddress>
<ContactDetails>
<Telephone>8709310505</Telephone>
<WebAddress>http://www.azure-interior.com</WebAddress>
<ElectronicMail>azure.Interior24@example.com</ElectronicMail>
</ContactDetails>
</LegalEntity>
</BuyerParty>
</Parties>
<Invoices>
<Invoice>
<InvoiceHeader>
<InvoiceNumber>INV/2023/00005</InvoiceNumber>
<InvoiceDocumentType>FC</InvoiceDocumentType>
<InvoiceClass>OO</InvoiceClass>
</InvoiceHeader>
<InvoiceIssueData>
<IssueDate>2023-08-01</IssueDate>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
<TaxCurrencyCode>EUR</TaxCurrencyCode>
<LanguageName>en</LanguageName>
</InvoiceIssueData>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>67.20</TotalAmount>
</TaxAmount>
</Tax>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>0.000</TaxRate>
<TaxableBase>
<TotalAmount>1799.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>0.00</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
<InvoiceTotals>
<TotalGrossAmount>2119.00</TotalGrossAmount>
<TotalGrossAmountBeforeTaxes>2119.00</TotalGrossAmountBeforeTaxes>
<TotalTaxOutputs>67.20</TotalTaxOutputs>
<TotalTaxesWithheld>0.00</TotalTaxesWithheld>
<InvoiceTotal>2186.20</InvoiceTotal>
<TotalOutstandingAmount>2186.20</TotalOutstandingAmount>
<TotalExecutableAmount>2186.20</TotalExecutableAmount>
</InvoiceTotals>
<Items>
<InvoiceLine>
<ItemDescription>[E-COM07] Large Cabinet</ItemDescription>
<Quantity>1.0</Quantity>
<UnitOfMeasure>01</UnitOfMeasure>
<UnitPriceWithoutTax>320.00</UnitPriceWithoutTax>
<TotalCost>320.00</TotalCost>
<GrossAmount>320.00</GrossAmount>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>67.20</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
</InvoiceLine>
<InvoiceLine>
<ItemDescription>[E-COM09] Large Desk</ItemDescription>
<Quantity>1.0</Quantity>
<UnitOfMeasure>01</UnitOfMeasure>
<UnitPriceWithoutTax>1799.00</UnitPriceWithoutTax>
<TotalCost>1799.00</TotalCost>
<GrossAmount>1799.00</GrossAmount>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>0.000</TaxRate>
<TaxableBase>
<TotalAmount>1799.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>0.00</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
</InvoiceLine>
</Items>
<LegalLiterals>
<LegalReference>Terms and conditions.</LegalReference>
</LegalLiterals>
<PaymentDetails>
<Installment>
<InstallmentDueDate>2023-08-31</InstallmentDueDate>
</Installment>
</PaymentDetails>
</Invoice>
<Invoice>
<InvoiceHeader>
<InvoiceNumber>INV/2023/00006</InvoiceNumber>
<InvoiceDocumentType>FC</InvoiceDocumentType>
<InvoiceClass>OO</InvoiceClass>
</InvoiceHeader>
<InvoiceIssueData>
<IssueDate>2023-07-01</IssueDate>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
<TaxCurrencyCode>EUR</TaxCurrencyCode>
<LanguageName>en</LanguageName>
</InvoiceIssueData>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>960.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>201.60</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
<InvoiceTotals>
<TotalGrossAmount>960.00</TotalGrossAmount>
<TotalGrossAmountBeforeTaxes>960.00</TotalGrossAmountBeforeTaxes>
<TotalTaxOutputs>201.60</TotalTaxOutputs>
<TotalTaxesWithheld>0.00</TotalTaxesWithheld>
<InvoiceTotal>1161.60</InvoiceTotal>
<TotalOutstandingAmount>1161.60</TotalOutstandingAmount>
<TotalExecutableAmount>1161.60</TotalExecutableAmount>
</InvoiceTotals>
<Items>
<InvoiceLine>
<ItemDescription>[E-COM07] Large Cabinet</ItemDescription>
<Quantity>3.0</Quantity>
<UnitOfMeasure>01</UnitOfMeasure>
<UnitPriceWithoutTax>320.00</UnitPriceWithoutTax>
<TotalCost>960.00</TotalCost>
<GrossAmount>960.00</GrossAmount>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>960.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>201.60</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
</InvoiceLine>
</Items>
<LegalLiterals>
<LegalReference>Legal References.</LegalReference>
</LegalLiterals>
<PaymentDetails>
<Installment>
<InstallmentDueDate>2023-07-31</InstallmentDueDate>
</Installment>
</PaymentDetails>
</Invoice>
</Invoices>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature-Document-1796c070f59109773ed3d7e92a20952003e69560">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference Type="http://www.w3.org/2000/09/xmldsig#Object" URI="" Id="Reference-Document-1796c070f59109773ed3d7e92a20952003e69560">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>cElLMko1b3H9dDrcO0nLhBA0DM7NWpAVuFDk7r+HMxM=</ds:DigestValue>
</ds:Reference>
<ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignatureProperties-Document-1796c070f59109773ed3d7e92a20952003e69560">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>1fyEQGjgpXKz9ie50e2WwFRk5kXklJNr10YHVMWBHeo=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="#KeyInfo-Document-1796c070f59109773ed3d7e92a20952003e69560">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>rUjANojsduwjEzBt8hX6sFsG9F1mwQ1X/rJhxY1MOGI=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>ecCZUOChRyxRpMRo8PaqBDKsuiWf8IOavpwLDSaD1+ZjEKF20D7GUfMoDbMTmotpw+qybmuQQZVQ
CRpl+7efRdcIGNjKwI1hxdbwFd41SCU3HXZKJ5B6mOdJjW4uBnXce157lI4U/liyHmdRyW1K3WZc
RCh4GrJ5M7N662JfhVOLhZaKSXFI7+s517HD5mJ4tw84UG7j3OgAfVqH3BZe+oM8boTJAUtFyNlO
o8YWWWgvV6LNkqbKkP5J2dcrwQmaUuISeQ7+/BU0T7OfdbpFGtvz1PrI2d9YTFHrH+aWU5gZ96kX
cmr2Ab2uGYRNXqIpHEA9DcY0pkapd6CNgPv5sg==
</ds:SignatureValue>
<ds:KeyInfo Id="KeyInfo-Document-1796c070f59109773ed3d7e92a20952003e69560">
<ds:X509Data>
<ds:X509Certificate>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
</ds:X509Certificate>
</ds:X509Data>
<ds:KeyValue>
<ds:RSAKeyValue>
<ds:Modulus>sXiHe/jkyFJfxloUqPsvhPvMnJZoHYFqKXvn8quI2AAkiA9q68XEnAIJhnBn73DSe3uGlKKqxoOz
mxSLg2xo1YOURVIw5Y871fe3puyrj3/6h1VgFV6ypA+iYFeykad/ES8gS3YG8GaoD9EEQTOhy5dZ
wh+kzK1bUzrJI3ifY3dVQAgZTFhaWk0JeHXJzOlvKO3uTmEq7urvcbqskEIptOmMo8ALsX0BDDtT
lZ93EJa6qWziOUjbt74unm9M+J9ShMPLds3hfkKzEjc8pG9wiVTmiyPLsmHGV6MO0XEstB2TbuE2
Up0EYDg28axZXH0h89oUqsVgF1uPx3ufeBm8PQ==
</ds:Modulus>
<ds:Exponent>AQAB
</ds:Exponent>
</ds:RSAKeyValue>
</ds:KeyValue>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature-Document-1796c070f59109773ed3d7e92a20952003e69560">
<xades:SignedProperties Id="SignatureProperties-Document-1796c070f59109773ed3d7e92a20952003e69560">
<xades:SignedSignatureProperties>
<xades:SigningTime>2023-08-31T09:51:34.576445</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>cP4cRSOphs7/CsqPwdGujvo7xkSkh8CfvFPAsQf/zRE=</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName>C=BE, CN=R&amp;D Accounting, L=Grand-Rosiere-Hottomont, O=Test Odoo SA, OU=R&amp;D Accounting, ST=Wallonia</ds:X509IssuerName>
<ds:X509SerialNumber>529783975559440394003723764779526236797615751643</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
<xades:SignaturePolicyIdentifier>
<xades:SignaturePolicyId>
<xades:SigPolicyId>
<xades:Identifier>http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf</xades:Identifier>
<xades:Description>Política de firma electrónica para facturación electrónica con formato Facturae</xades:Description>
</xades:SigPolicyId>
<xades:SigPolicyHash>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8=</ds:DigestValue>
</xades:SigPolicyHash>
<xades:SigPolicyQualifiers>
<xades:SigPolicyQualifier>
<xades:SPURI>http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf</xades:SPURI>
</xades:SigPolicyQualifier>
</xades:SigPolicyQualifiers>
</xades:SignaturePolicyId>
</xades:SignaturePolicyIdentifier>
</xades:SignedSignatureProperties>
<xades:SignedDataObjectProperties>
<xades:DataObjectFormat ObjectReference="#Reference-Document-1796c070f59109773ed3d7e92a20952003e69560">
<xades:Description/>
<xades:ObjectIdentifier>
<xades:Identifier Qualifier="OIDAsURN">urn:oid:1.2.840.10003.5.109.10</xades:Identifier>
<xades:Description/>
</xades:ObjectIdentifier>
<xades:MimeType>text/xml</xades:MimeType>
<xades:Encoding/>
</xades:DataObjectFormat>
</xades:SignedDataObjectProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
</fac:Facturae>
@@ -0,0 +1,260 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<fac:Facturae xmlns:fac="http://www.facturae.es/Facturae/2014/v3.2.1/Facturae">
<FileHeader>
<SchemaVersion>3.2.1</SchemaVersion>
<Modality>I</Modality>
<InvoiceIssuerType>EM</InvoiceIssuerType>
<Batch>
<BatchIdentifier>INV/2023/00009</BatchIdentifier>
<InvoicesCount>1</InvoicesCount>
<TotalInvoicesAmount>
<TotalAmount>323.20</TotalAmount>
</TotalInvoicesAmount>
<TotalOutstandingAmount>
<TotalAmount>323.20</TotalAmount>
</TotalOutstandingAmount>
<TotalExecutableAmount>
<TotalAmount>323.20</TotalAmount>
</TotalExecutableAmount>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
</Batch>
</FileHeader>
<Parties>
<SellerParty>
<TaxIdentification>
<PersonTypeCode>F</PersonTypeCode>
<ResidenceTypeCode>R</ResidenceTypeCode>
<TaxIdentificationNumber>ES59962470K</TaxIdentificationNumber>
</TaxIdentification>
<Individual>
<AddressInSpain>
<Address>A</Address>
<PostCode>22591</PostCode>
<Town>Candasnos</Town>
<Province>Zaragoza</Province>
<CountryCode>ESP</CountryCode>
</AddressInSpain>
<ContactDetails>
<WebAddress>http://www.esexample.com</WebAddress>
<ElectronicMail>info@company.esexample.com</ElectronicMail>
</ContactDetails>
</Individual>
</SellerParty>
<BuyerParty>
<TaxIdentification>
<PersonTypeCode>J</PersonTypeCode>
<ResidenceTypeCode>E</ResidenceTypeCode>
<TaxIdentificationNumber>US12345677</TaxIdentificationNumber>
</TaxIdentification>
<LegalEntity>
<CorporateName>Azure Interior</CorporateName>
<TradeName>Azure Interior</TradeName>
<OverseasAddress>
<Address>4557 De Silva St</Address>
<PostCodeAndTown>94538 Fremont</PostCodeAndTown>
<Province>California</Province>
<CountryCode>USA</CountryCode>
</OverseasAddress>
<ContactDetails>
<Telephone>8709310505</Telephone>
<WebAddress>http://www.azure-interior.com</WebAddress>
<ElectronicMail>azure.Interior24@example.com</ElectronicMail>
</ContactDetails>
</LegalEntity>
</BuyerParty>
</Parties>
<Invoices>
<Invoice>
<InvoiceHeader>
<InvoiceNumber>INV/2023/00009</InvoiceNumber>
<InvoiceDocumentType>FC</InvoiceDocumentType>
<InvoiceClass>OO</InvoiceClass>
</InvoiceHeader>
<InvoiceIssueData>
<IssueDate>2023-09-01</IssueDate>
<InvoiceCurrencyCode>EUR</InvoiceCurrencyCode>
<TaxCurrencyCode>EUR</TaxCurrencyCode>
<LanguageName>en</LanguageName>
</InvoiceIssueData>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>67.20</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
<TaxesWithheld>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>20.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>64.00</TotalAmount>
</TaxAmount>
</Tax>
</TaxesWithheld>
<InvoiceTotals>
<TotalGrossAmount>320.00</TotalGrossAmount>
<TotalGrossAmountBeforeTaxes>320.00</TotalGrossAmountBeforeTaxes>
<TotalTaxOutputs>67.20</TotalTaxOutputs>
<TotalTaxesWithheld>64.00</TotalTaxesWithheld>
<InvoiceTotal>323.20</InvoiceTotal>
<TotalOutstandingAmount>323.20</TotalOutstandingAmount>
<TotalExecutableAmount>323.20</TotalExecutableAmount>
</InvoiceTotals>
<Items>
<InvoiceLine>
<ItemDescription>[E-COM07] Large Cabinet</ItemDescription>
<Quantity>1.0</Quantity>
<UnitOfMeasure>01</UnitOfMeasure>
<UnitPriceWithoutTax>320.00</UnitPriceWithoutTax>
<TotalCost>320.00</TotalCost>
<GrossAmount>320.00</GrossAmount>
<TaxesOutputs>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>21.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>67.20</TotalAmount>
</TaxAmount>
</Tax>
</TaxesOutputs>
<TaxesWithheld>
<Tax>
<TaxTypeCode>01</TaxTypeCode>
<TaxRate>20.000</TaxRate>
<TaxableBase>
<TotalAmount>320.00</TotalAmount>
</TaxableBase>
<TaxAmount>
<TotalAmount>64.00</TotalAmount>
</TaxAmount>
</Tax>
</TaxesWithheld>
</InvoiceLine>
</Items>
</Invoice>
</Invoices>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="Signature-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference Type="http://www.w3.org/2000/09/xmldsig#Object" URI="" Id="Reference-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>y+DiG9thnFpt6SvkhDhV/8PYAzfhj0KKTXnZjbCbfa8=</ds:DigestValue>
</ds:Reference>
<ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#SignatureProperties-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>jx6wLH7wWnh3HQD4ixOqAWULf2lT9BAxbbWDzxloxo4=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="#KeyInfo-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>pti8uQOjoaRhHT1DyGg8Z0BKxxhgQMfyrHMbdiHzS/M=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>qSZAhTS36ed1Vl0hPHZHDrY1tJtr0gcjY7sgqZgw163wdqZYiV5WWmM8l6ttc1voNrrw1XCENXMz
Sur4lCKCQdIA400uvRM1tY8LoptM5GJswZoLnfZS2lkK8xAC8vDoBAKEaIVjuBd8Gi+Oztz6OiJG
+EIT8mMPmZ9jCt3XExMn8dZRaTeB6XwTH8GGyvgWjXVxEbSTD+BiRthORMeNwAdl3KiCrzgrYbNu
QaozjiZQGfpj2hJt2+zKvhpEML1nWRQUVrM2sjBxhhewQTmxavzG1xL3MHBzT5//YZYfsSNFJbE0
imJThhtOwrZvWqgN/B8wfhrS2eZkue9w0cX2cg==
</ds:SignatureValue>
<ds:KeyInfo Id="KeyInfo-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<ds:X509Data>
<ds:X509Certificate>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
</ds:X509Certificate>
</ds:X509Data>
<ds:KeyValue>
<ds:RSAKeyValue>
<ds:Modulus>sXiHe/jkyFJfxloUqPsvhPvMnJZoHYFqKXvn8quI2AAkiA9q68XEnAIJhnBn73DSe3uGlKKqxoOz
mxSLg2xo1YOURVIw5Y871fe3puyrj3/6h1VgFV6ypA+iYFeykad/ES8gS3YG8GaoD9EEQTOhy5dZ
wh+kzK1bUzrJI3ifY3dVQAgZTFhaWk0JeHXJzOlvKO3uTmEq7urvcbqskEIptOmMo8ALsX0BDDtT
lZ93EJa6qWziOUjbt74unm9M+J9ShMPLds3hfkKzEjc8pG9wiVTmiyPLsmHGV6MO0XEstB2TbuE2
Up0EYDg28axZXH0h89oUqsVgF1uPx3ufeBm8PQ==
</ds:Modulus>
<ds:Exponent>AQAB
</ds:Exponent>
</ds:RSAKeyValue>
</ds:KeyValue>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#Signature-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<xades:SignedProperties Id="SignatureProperties-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<xades:SignedSignatureProperties>
<xades:SigningTime>2023-09-01T12:16:38.095681</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>cP4cRSOphs7/CsqPwdGujvo7xkSkh8CfvFPAsQf/zRE=</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName>C=BE, CN=R&amp;D Accounting, L=Grand-Rosiere-Hottomont, O=Test Odoo SA, OU=R&amp;D Accounting, ST=Wallonia</ds:X509IssuerName>
<ds:X509SerialNumber>529783975559440394003723764779526236797615751643</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
<xades:SignaturePolicyIdentifier>
<xades:SignaturePolicyId>
<xades:SigPolicyId>
<xades:Identifier>http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf</xades:Identifier>
<xades:Description>Política de firma electrónica para facturación electrónica con formato Facturae</xades:Description>
</xades:SigPolicyId>
<xades:SigPolicyHash>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>a/vDoueJq9zxWGCFwdsXdlXtkFSEek4XVtmlQCEDaE8=</ds:DigestValue>
</xades:SigPolicyHash>
<xades:SigPolicyQualifiers>
<xades:SigPolicyQualifier>
<xades:SPURI>http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf</xades:SPURI>
</xades:SigPolicyQualifier>
</xades:SigPolicyQualifiers>
</xades:SignaturePolicyId>
</xades:SignaturePolicyIdentifier>
</xades:SignedSignatureProperties>
<xades:SignedDataObjectProperties>
<xades:DataObjectFormat ObjectReference="#Reference-Document-16e396210b43c90f11328fb78cefe178860e0dd7">
<xades:Description/>
<xades:ObjectIdentifier>
<xades:Identifier Qualifier="OIDAsURN">urn:oid:1.2.840.10003.5.109.10</xades:Identifier>
<xades:Description/>
</xades:ObjectIdentifier>
<xades:MimeType>text/xml</xades:MimeType>
<xades:Encoding/>
</xades:DataObjectFormat>
</xades:SignedDataObjectProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
</fac:Facturae>
@@ -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': '<p>Terms and conditions.</p>',
},
{
'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': '<p>Legal References.</p>',
},
])
# 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])