328 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			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
 |