1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
|