odoo/ext/custom-addons/account_invoice_ubl/models/account_invoice.py

328 lines
14 KiB
Python

# Copyright 2016-2017 Akretion (http://www.akretion.com)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import base64
from lxml import etree
import logging
from odoo import models, api
from odoo.tools import float_is_zero, float_round
logger = logging.getLogger(__name__)
class AccountInvoice(models.Model):
_name = 'account.invoice'
_inherit = ['account.invoice', 'base.ubl']
@api.multi
def _ubl_add_header(self, parent_node, ns, version='2.1'):
ubl_version = etree.SubElement(
parent_node, ns['cbc'] + 'UBLVersionID')
ubl_version.text = version
doc_id = etree.SubElement(parent_node, ns['cbc'] + 'ID')
doc_id.text = self.number
issue_date = etree.SubElement(parent_node, ns['cbc'] + 'IssueDate')
issue_date.text = self.date_invoice
type_code = etree.SubElement(
parent_node, ns['cbc'] + 'InvoiceTypeCode')
if self.type == 'out_invoice':
type_code.text = '380'
elif self.type == 'out_refund':
type_code.text = '381'
if self.comment:
note = etree.SubElement(parent_node, ns['cbc'] + 'Note')
note.text = self.comment
doc_currency = etree.SubElement(
parent_node, ns['cbc'] + 'DocumentCurrencyCode')
doc_currency.text = self.currency_id.name
@api.multi
def _ubl_add_order_reference(self, parent_node, ns, version='2.1'):
self.ensure_one()
if self.name:
order_ref = etree.SubElement(
parent_node, ns['cac'] + 'OrderReference')
order_ref_id = etree.SubElement(
order_ref, ns['cbc'] + 'ID')
order_ref_id.text = self.name
@api.multi
def _ubl_get_contract_document_reference_dict(self):
'''Result: dict with key = Doc Type Code, value = ID'''
self.ensure_one()
return {}
@api.multi
def _ubl_add_contract_document_reference(
self, parent_node, ns, version='2.1'):
self.ensure_one()
cdr_dict = self._ubl_get_contract_document_reference_dict()
for doc_type_code, doc_id in cdr_dict.items():
cdr = etree.SubElement(
parent_node, ns['cac'] + 'ContractDocumentReference')
cdr_id = etree.SubElement(cdr, ns['cbc'] + 'ID')
cdr_id.text = doc_id
cdr_type_code = etree.SubElement(
cdr, ns['cbc'] + 'DocumentTypeCode')
cdr_type_code.text = doc_type_code
@api.multi
def _ubl_add_attachments(self, parent_node, ns, version='2.1'):
if (
self.company_id.embed_pdf_in_ubl_xml_invoice and
not self._context.get('no_embedded_pdf')):
filename = 'Invoice-' + self.number + '.pdf'
docu_reference = etree.SubElement(
parent_node, ns['cac'] + 'AdditionalDocumentReference')
docu_reference_id = etree.SubElement(
docu_reference, ns['cbc'] + 'ID')
docu_reference_id.text = filename
attach_node = etree.SubElement(
docu_reference, ns['cac'] + 'Attachment')
binary_node = etree.SubElement(
attach_node, ns['cbc'] + 'EmbeddedDocumentBinaryObject',
mimeCode="application/pdf", filename=filename)
ctx = dict()
ctx['no_embedded_ubl_xml'] = True
pdf_inv = self.with_context(ctx).env.ref(
'account.account_invoices').render_qweb_pdf(self.ids)[0]
binary_node.text = base64.b64encode(pdf_inv)
@api.multi
def _ubl_add_legal_monetary_total(self, parent_node, ns, version='2.1'):
monetary_total = etree.SubElement(
parent_node, ns['cac'] + 'LegalMonetaryTotal')
cur_name = self.currency_id.name
prec = self.currency_id.decimal_places
line_total = etree.SubElement(
monetary_total, ns['cbc'] + 'LineExtensionAmount',
currencyID=cur_name)
line_total.text = '%0.*f' % (prec, self.amount_untaxed)
tax_excl_total = etree.SubElement(
monetary_total, ns['cbc'] + 'TaxExclusiveAmount',
currencyID=cur_name)
tax_excl_total.text = '%0.*f' % (prec, self.amount_untaxed)
tax_incl_total = etree.SubElement(
monetary_total, ns['cbc'] + 'TaxInclusiveAmount',
currencyID=cur_name)
tax_incl_total.text = '%0.*f' % (prec, self.amount_total)
prepaid_amount = etree.SubElement(
monetary_total, ns['cbc'] + 'PrepaidAmount',
currencyID=cur_name)
prepaid_value = self.amount_total - self.residual
prepaid_amount.text = '%0.*f' % (prec, prepaid_value)
payable_amount = etree.SubElement(
monetary_total, ns['cbc'] + 'PayableAmount',
currencyID=cur_name)
payable_amount.text = '%0.*f' % (prec, self.residual)
@api.multi
def _ubl_add_invoice_line(
self, parent_node, iline, line_number, ns, version='2.1'):
cur_name = self.currency_id.name
line_root = etree.SubElement(
parent_node, ns['cac'] + 'InvoiceLine')
dpo = self.env['decimal.precision']
qty_precision = dpo.precision_get('Product Unit of Measure')
price_precision = dpo.precision_get('Product Price')
account_precision = self.currency_id.decimal_places
line_id = etree.SubElement(line_root, ns['cbc'] + 'ID')
line_id.text = str(line_number)
uom_unece_code = False
# uom_id is not a required field on account.invoice.line
if iline.uom_id and iline.uom_id.unece_code:
uom_unece_code = iline.uom_id.unece_code
if uom_unece_code:
quantity = etree.SubElement(
line_root, ns['cbc'] + 'InvoicedQuantity',
unitCode=uom_unece_code)
else:
quantity = etree.SubElement(
line_root, ns['cbc'] + 'InvoicedQuantity')
qty = iline.quantity
quantity.text = '%0.*f' % (qty_precision, qty)
line_amount = etree.SubElement(
line_root, ns['cbc'] + 'LineExtensionAmount',
currencyID=cur_name)
line_amount.text = '%0.*f' % (account_precision, iline.price_subtotal)
self._ubl_add_invoice_line_tax_total(
iline, line_root, ns, version=version)
self._ubl_add_item(
iline.name, iline.product_id, line_root, ns, type='sale',
version=version)
price_node = etree.SubElement(line_root, ns['cac'] + 'Price')
price_amount = etree.SubElement(
price_node, ns['cbc'] + 'PriceAmount', currencyID=cur_name)
price_unit = 0.0
# Use price_subtotal/qty to compute price_unit to be sure
# to get a *tax_excluded* price unit
if not float_is_zero(qty, precision_digits=qty_precision):
price_unit = float_round(
iline.price_subtotal / float(qty),
precision_digits=price_precision)
price_amount.text = '%0.*f' % (price_precision, price_unit)
if uom_unece_code:
base_qty = etree.SubElement(
price_node, ns['cbc'] + 'BaseQuantity',
unitCode=uom_unece_code)
else:
base_qty = etree.SubElement(price_node, ns['cbc'] + 'BaseQuantity')
base_qty.text = '%0.*f' % (qty_precision, qty)
def _ubl_add_invoice_line_tax_total(
self, iline, parent_node, ns, version='2.1'):
cur_name = self.currency_id.name
prec = self.currency_id.decimal_places
tax_total_node = etree.SubElement(parent_node, ns['cac'] + 'TaxTotal')
price = iline.price_unit * (1 - (iline.discount or 0.0) / 100.0)
res_taxes = iline.invoice_line_tax_ids.compute_all(
price, quantity=iline.quantity, product=iline.product_id,
partner=self.partner_id)
tax_total = float_round(
res_taxes['total_included'] - res_taxes['total_excluded'],
precision_digits=prec)
tax_amount_node = etree.SubElement(
tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name)
tax_amount_node.text = '%0.*f' % (prec, tax_total)
if not float_is_zero(tax_total, precision_digits=prec):
for res_tax in res_taxes['taxes']:
tax = self.env['account.tax'].browse(res_tax['id'])
# we don't have the base amount in res_tax :-(
self._ubl_add_tax_subtotal(
False, res_tax['amount'], tax, cur_name, tax_total_node,
ns, version=version)
def _ubl_add_tax_total(self, xml_root, ns, version='2.1'):
self.ensure_one()
cur_name = self.currency_id.name
tax_total_node = etree.SubElement(xml_root, ns['cac'] + 'TaxTotal')
tax_amount_node = etree.SubElement(
tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name)
prec = self.currency_id.decimal_places
tax_amount_node.text = '%0.*f' % (prec, self.amount_tax)
if not float_is_zero(self.amount_tax, precision_digits=prec):
for tline in self.tax_line_ids:
self._ubl_add_tax_subtotal(
tline.base, tline.amount, tline.tax_id, cur_name,
tax_total_node, ns, version=version)
@api.multi
def generate_invoice_ubl_xml_etree(self, version='2.1'):
nsmap, ns = self._ubl_get_nsmap_namespace('Invoice-2', version=version)
xml_root = etree.Element('Invoice', nsmap=nsmap)
self._ubl_add_header(xml_root, ns, version=version)
self._ubl_add_order_reference(xml_root, ns, version=version)
self._ubl_add_contract_document_reference(
xml_root, ns, version=version)
self._ubl_add_attachments(xml_root, ns, version=version)
self._ubl_add_supplier_party(
False, self.company_id, 'AccountingSupplierParty', xml_root, ns,
version=version)
self._ubl_add_customer_party(
self.partner_id, False, 'AccountingCustomerParty', xml_root, ns,
version=version)
# the field 'partner_shipping_id' is defined in the 'sale' module
if hasattr(self, 'partner_shipping_id') and self.partner_shipping_id:
self._ubl_add_delivery(self.partner_shipping_id, xml_root, ns)
# Put paymentmeans block even when invoice is paid ?
payment_identifier = self.get_payment_identifier()
self._ubl_add_payment_means(
self.partner_bank_id, self.payment_mode_id, self.date_due,
xml_root, ns, payment_identifier=payment_identifier,
version=version)
if self.payment_term_id:
self._ubl_add_payment_terms(
self.payment_term_id, xml_root, ns, version=version)
self._ubl_add_tax_total(xml_root, ns, version=version)
self._ubl_add_legal_monetary_total(xml_root, ns, version=version)
line_number = 0
for iline in self.invoice_line_ids:
line_number += 1
self._ubl_add_invoice_line(
xml_root, iline, line_number, ns, version=version)
return xml_root
@api.multi
def generate_ubl_xml_string(self, version='2.1'):
self.ensure_one()
assert self.state in ('open', 'paid')
assert self.type in ('out_invoice', 'out_refund')
logger.debug('Starting to generate UBL XML Invoice file')
lang = self.get_ubl_lang()
# The aim of injecting lang in context
# is to have the content of the XML in the partner's lang
# but the problem is that the error messages will also be in
# that lang. But the error messages should almost never
# happen except the first days of use, so it's probably
# not worth the additional code to handle the 2 langs
xml_root = self.with_context(lang=lang).\
generate_invoice_ubl_xml_etree(version=version)
xml_string = etree.tostring(
xml_root, pretty_print=True, encoding='UTF-8',
xml_declaration=True)
self._ubl_check_xml_schema(xml_string, 'Invoice', version=version)
logger.debug(
'Invoice UBL XML file generated for account invoice ID %d '
'(state %s)', self.id, self.state)
logger.debug(xml_string.decode('utf-8'))
return xml_string
@api.multi
def get_ubl_filename(self, version='2.1'):
"""This method is designed to be inherited"""
return 'UBL-Invoice-%s.xml' % version
@api.multi
def get_ubl_version(self):
version = self._context.get('ubl_version') or '2.1'
return version
@api.multi
def get_ubl_lang(self):
return self.partner_id.lang or 'en_US'
@api.multi
def embed_ubl_xml_in_pdf(self, pdf_content=None, pdf_file=None):
self.ensure_one()
if (
self.type in ('out_invoice', 'out_refund') and
self.state in ('open', 'paid')):
version = self.get_ubl_version()
ubl_filename = self.get_ubl_filename(version=version)
xml_string = self.generate_ubl_xml_string(version=version)
pdf_content = self.embed_xml_in_pdf(
xml_string, ubl_filename,
pdf_content=pdf_content, pdf_file=pdf_file)
return pdf_content
@api.multi
def attach_ubl_xml_file_button(self):
self.ensure_one()
assert self.type in ('out_invoice', 'out_refund')
assert self.state in ('open', 'paid')
version = self.get_ubl_version()
xml_string = self.generate_ubl_xml_string(version=version)
filename = self.get_ubl_filename(version=version)
ctx = {}
attach = self.env['ir.attachment'].with_context(ctx).create({
'name': filename,
'res_id': self.id,
'res_model': str(self._name),
'datas': base64.b64encode(xml_string),
'datas_fname': filename,
# I have default_type = 'out_invoice' in context, so 'type'
# would take 'out_invoice' value by default !
'type': 'binary',
})
action = self.env['ir.actions.act_window'].for_xml_id(
'base', 'action_attachment')
action.update({
'res_id': attach.id,
'views': False,
'view_mode': 'form,tree'
})
return action