From 9036d41327fdb8935fb7ec194ea95bac1b4b3f08 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 12 Sep 2016 01:56:58 +0200 Subject: [PATCH 01/67] Add modules account_invoice_ubl + base_ubl_payment --- account_invoice_ubl/README.rst | 57 ++++ account_invoice_ubl/__init__.py | 3 + account_invoice_ubl/__openerp__.py | 23 ++ account_invoice_ubl/models/__init__.py | 5 + account_invoice_ubl/models/account_invoice.py | 271 ++++++++++++++++++ account_invoice_ubl/models/company.py | 16 ++ account_invoice_ubl/models/report.py | 32 +++ account_invoice_ubl/tests/__init__.py | 3 + .../tests/test_ubl_generate.py | 29 ++ account_invoice_ubl/views/account_invoice.xml | 25 ++ account_invoice_ubl/views/company.xml | 24 ++ 11 files changed, 488 insertions(+) create mode 100644 account_invoice_ubl/README.rst create mode 100644 account_invoice_ubl/__init__.py create mode 100644 account_invoice_ubl/__openerp__.py create mode 100644 account_invoice_ubl/models/__init__.py create mode 100644 account_invoice_ubl/models/account_invoice.py create mode 100644 account_invoice_ubl/models/company.py create mode 100644 account_invoice_ubl/models/report.py create mode 100644 account_invoice_ubl/tests/__init__.py create mode 100644 account_invoice_ubl/tests/test_ubl_generate.py create mode 100644 account_invoice_ubl/views/account_invoice.xml create mode 100644 account_invoice_ubl/views/company.xml diff --git a/account_invoice_ubl/README.rst b/account_invoice_ubl/README.rst new file mode 100644 index 0000000000..03ccafd406 --- /dev/null +++ b/account_invoice_ubl/README.rst @@ -0,0 +1,57 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=================== +Account Invoice UBL +=================== + +This module adds support for UBL, the `Universal Business Language (UBL) `_ standard, on invoices. The UBL standard became the `ISO/IEC 19845 `_ standard in December 2015 (cf the `official announce _`). + +With this module, you can generate customer invoices/refunds: + +* in PDF format with an embedded UBL XML file +* as an XML file with an optional embedded PDF file + +Configuration +============= + +On the form view of the company, there is an option to embed the PDF of the invoices inside the XML invoice. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/95/8.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_invoice_ubl/__init__.py b/account_invoice_ubl/__init__.py new file mode 100644 index 0000000000..cde864bae2 --- /dev/null +++ b/account_invoice_ubl/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/account_invoice_ubl/__openerp__.py b/account_invoice_ubl/__openerp__.py new file mode 100644 index 0000000000..65fce548f9 --- /dev/null +++ b/account_invoice_ubl/__openerp__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Account Invoice UBL', + 'version': '8.0.1.0.0', + 'category': 'Accounting & Finance', + 'license': 'AGPL-3', + 'summary': 'Generate UBL XML file for customer invoices/refunds', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'website': 'http://www.akretion.com', + 'depends': [ + 'account', + 'account_payment_partner', + 'base_ubl_payment', + ], + 'data': [ + 'views/company.xml', + 'views/account_invoice.xml', + ], + 'installable': True, +} diff --git a/account_invoice_ubl/models/__init__.py b/account_invoice_ubl/models/__init__.py new file mode 100644 index 0000000000..787bf4baa3 --- /dev/null +++ b/account_invoice_ubl/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import company +from . import account_invoice +from . import report diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py new file mode 100644 index 0000000000..9873dfce01 --- /dev/null +++ b/account_invoice_ubl/models/account_invoice.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, api, _ +from lxml import etree +from openerp.tools import float_is_zero, float_round +from openerp.exceptions import Warning as UserError +import logging + +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): + ubl_version = etree.SubElement( + parent_node, ns['cbc'] + 'UBLVersionID') + ubl_version.text = '2.1' + 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_attachments(self, parent_node, ns): + if ( + self.company_id.embed_pdf_in_ubl_xml_invoice and + not self._context.get('no_embedded_pdf')): + docu_reference = etree.SubElement( + parent_node, ns['cac'] + 'AdditionalDocumentReference') + docu_reference_id = etree.SubElement( + docu_reference, ns['cbc'] + 'ID') + docu_reference_id.text = 'Invoice-' + self.number + '.pdf' + attach_node = etree.SubElement( + docu_reference, ns['cac'] + 'Attachment') + binary_node = etree.SubElement( + attach_node, ns['cbc'] + 'EmbeddedDocumentBinaryObject', + mimeCode="application/pdf") + ctx = self._context.copy() + ctx['no_embedded_ubl_xml'] = True + pdf_inv = self.pool['report'].get_pdf( + self._cr, self._uid, [self.id], 'account.report_invoice', + context=ctx) + binary_node.text = pdf_inv.encode('base64') + + @api.multi + def _ubl_add_legal_monetary_total(self, parent_node, ns): + monetary_total = etree.SubElement( + parent_node, ns['cac'] + 'LegalMonetaryTotal') + cur_name = self.currency_id.name + line_total = etree.SubElement( + monetary_total, ns['cbc'] + 'LineExtensionAmount', + currencyID=cur_name) + line_total.text = unicode(self.amount_untaxed) + tax_excl_total = etree.SubElement( + monetary_total, ns['cbc'] + 'TaxExclusiveAmount', + currencyID=cur_name) + tax_excl_total.text = unicode(self.amount_untaxed) + tax_incl_total = etree.SubElement( + monetary_total, ns['cbc'] + 'TaxInclusiveAmount', + currencyID=cur_name) + tax_incl_total.text = unicode(self.amount_total) + prepaid_amount = etree.SubElement( + monetary_total, ns['cbc'] + 'PrepaidAmount', + currencyID=cur_name) + prepaid_amount.text = unicode(self.amount_total - self.residual) + payable_amount = etree.SubElement( + monetary_total, ns['cbc'] + 'PayableAmount', + currencyID=cur_name) + payable_amount.text = unicode(self.residual) + + @api.multi + def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): + 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') + line_id = etree.SubElement(line_root, ns['cbc'] + 'ID') + line_id.text = unicode(line_number) + uom_unece_code = False + # on v8, uos_id is not a required field on account.invoice.line + if iline.uos_id and iline.uos_id.unece_code: + uom_unece_code = iline.uos_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 = unicode(qty) + line_amount = etree.SubElement( + line_root, ns['cbc'] + 'LineExtensionAmount', + currencyID=cur_name) + line_amount.text = unicode(iline.price_subtotal) + self._ubl_add_invoice_line_tax_total(iline, line_root, ns) + self._ubl_add_item( + iline.name, iline.product_id, line_root, ns, type='sale') + 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 = unicode(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 = unicode(qty) + + def _ubl_add_invoice_line_tax_total(self, iline, parent_node, ns): + cur_name = self.currency_id.name + prec = self.env['decimal.precision'].precision_get('Account') + 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_id.compute_all( + price, iline.quantity, product=iline.product_id, + partner=self.partner_id) + tax_total = float_round( + res_taxes['total_included'] - res_taxes['total'], + precision_digits=prec) + tax_amount_node = etree.SubElement( + tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) + tax_amount_node.text = unicode(tax_total) + 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) + + @api.multi + def get_delivery_partner(self): + self.ensure_one() + # NON, car nécessite un lien vers sale + + @api.multi + def _ubl_add_tax_total(self, xml_root, ns): + 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) + tax_amount_node.text = unicode(self.amount_tax) + for tline in self.tax_line: + if not tline.base_code_id: + raise UserError(_( + "Missing base code on tax line '%s'.") % tline.name) + taxes = self.env['account.tax'].search([ + ('base_code_id', '=', tline.base_code_id.id)]) + if not taxes: + raise UserError(_( + "The tax code '%s' is not linked to a tax.") + % tline.base_code_id.name) + tax = taxes[0] + self._ubl_add_tax_subtotal( + tline.base, tline.amount, tax, cur_name, tax_total_node, ns) + + @api.multi + def generate_invoice_ubl_xml_etree(self): + nsmap, ns = self._ubl_get_nsmap_namespace('Invoice-2') + xml_root = etree.Element('Invoice', nsmap=nsmap) + self._ubl_add_header(xml_root, ns) + self._ubl_add_attachments(xml_root, ns) + self._ubl_add_supplier_party( + False, self.company_id, 'AccountingSupplierParty', xml_root, ns) + self._ubl_add_customer_party( + self.partner_id, False, 'AccountingCustomerParty', xml_root, ns) + # delivery_partner = self.get_delivery_partner() + # self._ubl_add_delivery(delivery_partner, xml_root, ns) + # Put paymentmeans block even when invoice is paid ? + self._ubl_add_payment_means( + self.partner_bank_id, self.payment_mode_id, self.date_due, + xml_root, ns) + if self.payment_term: + self._ubl_add_payment_terms(self.payment_term, xml_root, ns) + self._ubl_add_tax_total(xml_root, ns) + self._ubl_add_legal_monetary_total(xml_root, ns) + + line_number = 0 + for iline in self.invoice_line: + line_number += 1 + self._ubl_add_invoice_line(xml_root, iline, line_number, ns) + return xml_root + + @api.multi + def generate_ubl_xml_string(self): + 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') + xml_root = self.generate_invoice_ubl_xml_etree() + xsd_filename = 'UBL-Invoice-2.1.xsd' + xml_string = etree.tostring( + xml_root, pretty_print=True, encoding='UTF-8', + xml_declaration=True) + self._check_xml_schema( + xml_string, 'base_ubl/data/xsd-2.1/maindoc/' + xsd_filename) + logger.debug( + 'Invoice UBL XML file generated for account invoice ID %d ' + '(state %s)', self.id, self.state) + logger.debug(xml_string) + return xml_string + + @api.multi + def get_ubl_filename(self): + """This method is designed to be inherited""" + return 'UBL-Invoice-2.1.xml' + + @api.multi + def embed_ubl_xml_in_pdf(self, pdf_content): + self.ensure_one() + if ( + self.type in ('out_invoice', 'out_refund') and + self.state in ('open', 'paid')): + ubl_filename = self.get_ubl_filename() + xml_string = self.generate_ubl_xml_string() + pdf_content = self.embed_xml_in_pdf( + xml_string, ubl_filename, pdf_content) + 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') + xml_string = self.generate_ubl_xml_string() + filename = self.get_ubl_filename() + attach = self.env['ir.attachment'].create({ + 'name': filename, + 'res_id': self.id, + 'res_model': unicode(self._model), + 'datas': xml_string.encode('base64'), + '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 diff --git a/account_invoice_ubl/models/company.py b/account_invoice_ubl/models/company.py new file mode 100644 index 0000000000..bb65815304 --- /dev/null +++ b/account_invoice_ubl/models/company.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + embed_pdf_in_ubl_xml_invoice = fields.Boolean( + string='Embed PDF in UBL XML Invoice', + help="If active, the standalone UBL Invoice XML file will include the " + "PDF of the invoice in base64 under the node " + "'AdditionalDocumentReference'. For example, to be compliant with the " + "e-fff standard used in Belgium, you should activate this option.") diff --git a/account_invoice_ubl/models/report.py b/account_invoice_ubl/models/report.py new file mode 100644 index 0000000000..b40f17518d --- /dev/null +++ b/account_invoice_ubl/models/report.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api +import logging + +logger = logging.getLogger(__name__) + + +class Report(models.Model): + _inherit = 'report' + + @api.v7 + def get_pdf( + self, cr, uid, ids, report_name, html=None, data=None, + context=None): + """We go through that method when the PDF is generated for the 1st + time and also when it is read from the attachment. + This method is specific to QWeb""" + if context is None: + context = {} + pdf_content = super(Report, self).get_pdf( + cr, uid, ids, report_name, html=html, data=data, context=context) + if ( + report_name == 'account.report_invoice' and + len(ids) == 1 and + not context.get('no_embedded_ubl_xml')): + invoice = self.pool['account.invoice'].browse( + cr, uid, ids[0], context=dict(context, no_embedded_pdf=True)) + pdf_content = invoice.embed_ubl_xml_in_pdf(pdf_content) + return pdf_content diff --git a/account_invoice_ubl/tests/__init__.py b/account_invoice_ubl/tests/__init__.py new file mode 100644 index 0000000000..99dcf3142e --- /dev/null +++ b/account_invoice_ubl/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_ubl_generate diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py new file mode 100644 index 0000000000..7351dcee3b --- /dev/null +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp.tests.common import TransactionCase +from openerp import workflow + + +class TestUblInvoice(TransactionCase): + + def test_ubl_generate(self): + ro = self.registry['report'] + buo = self.env['base.ubl'] + for i in range(5): + i += 1 + invoice = self.env.ref('account.invoice_%d' % i) + invoice_filename = invoice.get_ubl_filename() + # validate invoice + workflow.trg_validate( + self.uid, 'account.invoice', invoice.id, 'invoice_open', + self.cr) + if invoice.type not in ('out_invoice', 'out_refund'): + continue + # I didn't manage to make it work with new api :-( + pdf_file = ro.get_pdf( + self.cr, self.uid, invoice.ids, + 'account.report_invoice') + res = buo.get_xml_files_from_pdf(pdf_file) + self.assertTrue(invoice_filename in res) diff --git a/account_invoice_ubl/views/account_invoice.xml b/account_invoice_ubl/views/account_invoice.xml new file mode 100644 index 0000000000..4ec56c7ea0 --- /dev/null +++ b/account_invoice_ubl/views/account_invoice.xml @@ -0,0 +1,25 @@ + + + + + + + + + account_invoice_ubl.customer.invoice.form + account.invoice + + + + + + + + + diff --git a/account_invoice_ubl/views/company.xml b/account_invoice_ubl/views/company.xml new file mode 100644 index 0000000000..8aa46c4ba4 --- /dev/null +++ b/account_invoice_ubl/views/company.xml @@ -0,0 +1,24 @@ + + + + + + + + + account_invoice_ubl.company.form + res.company + + + + + + + + + + + From dbd7f6e3395a6917b92885d1261d8d004903d9cf Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 12 Sep 2016 23:16:41 +0200 Subject: [PATCH 02/67] Add partner identification hook in UBL XML party block generation Print numbers with the approriate number of digits in the XML --- account_invoice_ubl/models/account_invoice.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 9873dfce01..e71b7c8307 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -64,26 +64,28 @@ def _ubl_add_legal_monetary_total(self, parent_node, ns): monetary_total = etree.SubElement( parent_node, ns['cac'] + 'LegalMonetaryTotal') cur_name = self.currency_id.name + prec = self.env['decimal.precision'].precision_get('Account') line_total = etree.SubElement( monetary_total, ns['cbc'] + 'LineExtensionAmount', currencyID=cur_name) - line_total.text = unicode(self.amount_untaxed) + 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 = unicode(self.amount_untaxed) + 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 = unicode(self.amount_total) + tax_incl_total.text = '%0.*f' % (prec, self.amount_total) prepaid_amount = etree.SubElement( monetary_total, ns['cbc'] + 'PrepaidAmount', currencyID=cur_name) - prepaid_amount.text = unicode(self.amount_total - self.residual) + 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 = unicode(self.residual) + payable_amount.text = '%0.*f' % (prec, self.residual) @api.multi def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): @@ -93,6 +95,7 @@ def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): dpo = self.env['decimal.precision'] qty_precision = dpo.precision_get('Product Unit of Measure') price_precision = dpo.precision_get('Product Price') + account_precision = dpo.precision_get('Account') line_id = etree.SubElement(line_root, ns['cbc'] + 'ID') line_id.text = unicode(line_number) uom_unece_code = False @@ -111,7 +114,7 @@ def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): line_amount = etree.SubElement( line_root, ns['cbc'] + 'LineExtensionAmount', currencyID=cur_name) - line_amount.text = unicode(iline.price_subtotal) + line_amount.text = '%0.*f' % (account_precision, iline.price_subtotal) self._ubl_add_invoice_line_tax_total(iline, line_root, ns) self._ubl_add_item( iline.name, iline.product_id, line_root, ns, type='sale') From df46ae86ac9f66ce3b98e9766b1e4d9afdbef70d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sat, 17 Sep 2016 22:38:12 +0200 Subject: [PATCH 03/67] Add support for UBL 2.0 (required for e-fff) --- account_invoice_ubl/models/account_invoice.py | 81 +++++++++++-------- .../tests/test_ubl_generate.py | 15 ++-- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index e71b7c8307..83b77801d2 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -16,10 +16,10 @@ class AccountInvoice(models.Model): _inherit = ['account.invoice', 'base.ubl'] @api.multi - def _ubl_add_header(self, parent_node, ns): + def _ubl_add_header(self, parent_node, ns, version='2.1'): ubl_version = etree.SubElement( parent_node, ns['cbc'] + 'UBLVersionID') - ubl_version.text = '2.1' + 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') @@ -38,7 +38,7 @@ def _ubl_add_header(self, parent_node, ns): doc_currency.text = self.currency_id.name @api.multi - def _ubl_add_attachments(self, parent_node, ns): + 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')): @@ -60,7 +60,7 @@ def _ubl_add_attachments(self, parent_node, ns): binary_node.text = pdf_inv.encode('base64') @api.multi - def _ubl_add_legal_monetary_total(self, parent_node, ns): + 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 @@ -88,7 +88,8 @@ def _ubl_add_legal_monetary_total(self, parent_node, ns): payable_amount.text = '%0.*f' % (prec, self.residual) @api.multi - def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): + 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') @@ -115,9 +116,11 @@ def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): 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) + 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') + 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) @@ -137,7 +140,8 @@ def _ubl_add_invoice_line(self, parent_node, iline, line_number, ns): base_qty = etree.SubElement(price_node, ns['cbc'] + 'BaseQuantity') base_qty.text = unicode(qty) - def _ubl_add_invoice_line_tax_total(self, iline, parent_node, ns): + def _ubl_add_invoice_line_tax_total( + self, iline, parent_node, ns, version='2.1'): cur_name = self.currency_id.name prec = self.env['decimal.precision'].precision_get('Account') tax_total_node = etree.SubElement(parent_node, ns['cac'] + 'TaxTotal') @@ -155,7 +159,8 @@ def _ubl_add_invoice_line_tax_total(self, iline, parent_node, ns): 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) + False, res_tax['amount'], tax, cur_name, tax_total_node, ns, + version=version) @api.multi def get_delivery_partner(self): @@ -163,7 +168,7 @@ def get_delivery_partner(self): # NON, car nécessite un lien vers sale @api.multi - def _ubl_add_tax_total(self, xml_root, ns): + 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') @@ -182,48 +187,51 @@ def _ubl_add_tax_total(self, xml_root, ns): % tline.base_code_id.name) tax = taxes[0] self._ubl_add_tax_subtotal( - tline.base, tline.amount, tax, cur_name, tax_total_node, ns) + tline.base, tline.amount, tax, cur_name, tax_total_node, ns, + version=version) @api.multi - def generate_invoice_ubl_xml_etree(self): - nsmap, ns = self._ubl_get_nsmap_namespace('Invoice-2') + 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) - self._ubl_add_attachments(xml_root, ns) + self._ubl_add_header(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) + False, self.company_id, 'AccountingSupplierParty', xml_root, ns, + version=version) self._ubl_add_customer_party( - self.partner_id, False, 'AccountingCustomerParty', xml_root, ns) + self.partner_id, False, 'AccountingCustomerParty', xml_root, ns, + version=version) # delivery_partner = self.get_delivery_partner() # self._ubl_add_delivery(delivery_partner, xml_root, ns) # Put paymentmeans block even when invoice is paid ? self._ubl_add_payment_means( self.partner_bank_id, self.payment_mode_id, self.date_due, - xml_root, ns) + xml_root, ns, version=version) if self.payment_term: - self._ubl_add_payment_terms(self.payment_term, xml_root, ns) - self._ubl_add_tax_total(xml_root, ns) - self._ubl_add_legal_monetary_total(xml_root, ns) + self._ubl_add_payment_terms( + self.payment_term, 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: line_number += 1 - self._ubl_add_invoice_line(xml_root, iline, line_number, ns) + self._ubl_add_invoice_line( + xml_root, iline, line_number, ns, version=version) return xml_root @api.multi - def generate_ubl_xml_string(self): + 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') - xml_root = self.generate_invoice_ubl_xml_etree() - xsd_filename = 'UBL-Invoice-2.1.xsd' + xml_root = self.generate_invoice_ubl_xml_etree(version=version) xml_string = etree.tostring( xml_root, pretty_print=True, encoding='UTF-8', xml_declaration=True) - self._check_xml_schema( - xml_string, 'base_ubl/data/xsd-2.1/maindoc/' + xsd_filename) + 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) @@ -231,9 +239,14 @@ def generate_ubl_xml_string(self): return xml_string @api.multi - def get_ubl_filename(self): + def get_ubl_filename(self, version='2.1'): """This method is designed to be inherited""" - return 'UBL-Invoice-2.1.xml' + 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 embed_ubl_xml_in_pdf(self, pdf_content): @@ -241,8 +254,9 @@ def embed_ubl_xml_in_pdf(self, pdf_content): if ( self.type in ('out_invoice', 'out_refund') and self.state in ('open', 'paid')): - ubl_filename = self.get_ubl_filename() - xml_string = self.generate_ubl_xml_string() + 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) return pdf_content @@ -252,8 +266,9 @@ def attach_ubl_xml_file_button(self): self.ensure_one() assert self.type in ('out_invoice', 'out_refund') assert self.state in ('open', 'paid') - xml_string = self.generate_ubl_xml_string() - filename = self.get_ubl_filename() + version = self.get_ubl_version() + xml_string = self.generate_ubl_xml_string(version=version) + filename = self.get_ubl_filename(version=version) attach = self.env['ir.attachment'].create({ 'name': filename, 'res_id': self.id, diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index 7351dcee3b..61fd164f4b 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -14,16 +14,17 @@ def test_ubl_generate(self): for i in range(5): i += 1 invoice = self.env.ref('account.invoice_%d' % i) - invoice_filename = invoice.get_ubl_filename() # validate invoice workflow.trg_validate( self.uid, 'account.invoice', invoice.id, 'invoice_open', self.cr) if invoice.type not in ('out_invoice', 'out_refund'): continue - # I didn't manage to make it work with new api :-( - pdf_file = ro.get_pdf( - self.cr, self.uid, invoice.ids, - 'account.report_invoice') - res = buo.get_xml_files_from_pdf(pdf_file) - self.assertTrue(invoice_filename in res) + for version in ['2.0', '2.1']: + # I didn't manage to make it work with new api :-( + pdf_file = ro.get_pdf( + self.cr, self.uid, invoice.ids, + 'account.report_invoice', context={'ubl_version': version}) + res = buo.get_xml_files_from_pdf(pdf_file) + invoice_filename = invoice.get_ubl_filename(version=version) + self.assertTrue(invoice_filename in res) From 2661687b16d065dd8160b14c35a339fd00e9acb4 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sat, 17 Sep 2016 22:45:17 +0200 Subject: [PATCH 04/67] Update README --- account_invoice_ubl/README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/account_invoice_ubl/README.rst b/account_invoice_ubl/README.rst index 03ccafd406..d8c0305ac4 100644 --- a/account_invoice_ubl/README.rst +++ b/account_invoice_ubl/README.rst @@ -6,13 +6,15 @@ Account Invoice UBL =================== -This module adds support for UBL, the `Universal Business Language (UBL) `_ standard, on invoices. The UBL standard became the `ISO/IEC 19845 `_ standard in December 2015 (cf the `official announce _`). +This module adds support for UBL, the `Universal Business Language (UBL) `_ standard, on invoices. The UBL 2.1 standard became the `ISO/IEC 19845 `_ standard in December 2015 (cf the `official announce _`). With this module, you can generate customer invoices/refunds: * in PDF format with an embedded UBL XML file * as an XML file with an optional embedded PDF file +This module supports UBL version 2.1 (used by default) and 2.0. + Configuration ============= From 75f2e5a021c50f7527ab6cc89b855c8f4d174f7f Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 19 Sep 2016 22:38:57 +0200 Subject: [PATCH 05/67] Handle lang in UBL XML file generation --- account_invoice_ubl/models/account_invoice.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 83b77801d2..4c1d13eae1 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -227,7 +227,15 @@ def generate_ubl_xml_string(self, version='2.1'): assert self.state in ('open', 'paid') assert self.type in ('out_invoice', 'out_refund') logger.debug('Starting to generate UBL XML Invoice file') - xml_root = self.generate_invoice_ubl_xml_etree(version=version) + 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 additionnal 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) @@ -248,6 +256,10 @@ 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): self.ensure_one() From ce07eb7a9516efbba6c5398f25b34be1b037e2da Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 18 Oct 2016 23:02:55 +0200 Subject: [PATCH 06/67] 8.0 Add support for partner bank matching on invoice update (#6) Add support for partner bank matching on invoice update (before, it was only supported on invoice creation) --- account_invoice_ubl/README.rst | 4 +-- account_invoice_ubl/i18n/fr.po | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 account_invoice_ubl/i18n/fr.po diff --git a/account_invoice_ubl/README.rst b/account_invoice_ubl/README.rst index d8c0305ac4..a482a9ae7a 100644 --- a/account_invoice_ubl/README.rst +++ b/account_invoice_ubl/README.rst @@ -25,13 +25,13 @@ Usage .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/95/8.0 + :target: https://runbot.odoo-community.org/runbot/226/8.0 Bug Tracker =========== Bugs are tracked on `GitHub Issues -`_. In case of trouble, please +`_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. diff --git a/account_invoice_ubl/i18n/fr.po b/account_invoice_ubl/i18n/fr.po new file mode 100644 index 0000000000..6bf0815b9c --- /dev/null +++ b/account_invoice_ubl/i18n/fr.po @@ -0,0 +1,65 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_ubl +# +# Translators: +# OCA Transbot , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-11-12 13:36+0000\n" +"PO-Revision-Date: 2016-11-12 13:36+0000\n" +"Last-Translator: OCA Transbot , 2016\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: account_invoice_ubl +#: model:ir.model,name:account_invoice_ubl.model_res_company +msgid "Companies" +msgstr "" + +#. module: account_invoice_ubl +#: field:res.company,embed_pdf_in_ubl_xml_invoice:0 +msgid "Embed PDF in UBL XML Invoice" +msgstr "" + +#. module: account_invoice_ubl +#: view:account.invoice:account_invoice_ubl.invoice_form +msgid "Generate UBL XML File" +msgstr "" + +#. module: account_invoice_ubl +#: help:res.company,embed_pdf_in_ubl_xml_invoice:0 +msgid "" +"If active, the standalone UBL Invoice XML file will include the PDF of the " +"invoice in base64 under the node 'AdditionalDocumentReference'. For example," +" to be compliant with the e-fff standard used in Belgium, you should " +"activate this option." +msgstr "" + +#. module: account_invoice_ubl +#: model:ir.model,name:account_invoice_ubl.model_account_invoice +msgid "Invoice" +msgstr "Facture" + +#. module: account_invoice_ubl +#: code:addons/account_invoice_ubl/models/account_invoice.py:180 +#, python-format +msgid "Missing base code on tax line '%s'." +msgstr "" + +#. module: account_invoice_ubl +#: model:ir.model,name:account_invoice_ubl.model_report +msgid "Report" +msgstr "" + +#. module: account_invoice_ubl +#: code:addons/account_invoice_ubl/models/account_invoice.py:185 +#, python-format +msgid "The tax code '%s' is not linked to a tax." +msgstr "" From 6e8d75500cf4e0dd1b848a8f085cfdec280362f7 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 15 Feb 2017 15:11:22 +0100 Subject: [PATCH 07/67] Prepare v10 branch Rename __openerp__.py to __manifest__.py and set installable to False --- account_invoice_ubl/{__openerp__.py => __manifest__.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename account_invoice_ubl/{__openerp__.py => __manifest__.py} (96%) diff --git a/account_invoice_ubl/__openerp__.py b/account_invoice_ubl/__manifest__.py similarity index 96% rename from account_invoice_ubl/__openerp__.py rename to account_invoice_ubl/__manifest__.py index 65fce548f9..a664b81345 100644 --- a/account_invoice_ubl/__openerp__.py +++ b/account_invoice_ubl/__manifest__.py @@ -19,5 +19,5 @@ 'views/company.xml', 'views/account_invoice.xml', ], - 'installable': True, + 'installable': False, } From 4e476f0ffdd6e875efaecd653e55e0d794e38446 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 20 Feb 2017 00:12:07 +0100 Subject: [PATCH 08/67] Port base_business_document_import* to v10 Also port all the modules that generate the XML documents: account_invoice_ubl, account_invoice_zugferd, purchase_order_ubl and sale_order_ubl --- account_invoice_ubl/README.rst | 2 +- account_invoice_ubl/__manifest__.py | 7 ++-- account_invoice_ubl/models/account_invoice.py | 41 +++++++------------ account_invoice_ubl/models/company.py | 4 +- account_invoice_ubl/models/report.py | 22 ++++------ .../tests/test_ubl_generate.py | 9 ++-- account_invoice_ubl/views/account_invoice.xml | 10 ++--- account_invoice_ubl/views/company.xml | 24 ----------- 8 files changed, 37 insertions(+), 82 deletions(-) delete mode 100644 account_invoice_ubl/views/company.xml diff --git a/account_invoice_ubl/README.rst b/account_invoice_ubl/README.rst index a482a9ae7a..648cc84aa1 100644 --- a/account_invoice_ubl/README.rst +++ b/account_invoice_ubl/README.rst @@ -25,7 +25,7 @@ Usage .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/226/8.0 + :target: https://runbot.odoo-community.org/runbot/226/10.0 Bug Tracker =========== diff --git a/account_invoice_ubl/__manifest__.py b/account_invoice_ubl/__manifest__.py index a664b81345..d40d296e3a 100644 --- a/account_invoice_ubl/__manifest__.py +++ b/account_invoice_ubl/__manifest__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Account Invoice UBL', - 'version': '8.0.1.0.0', + 'version': '10.0.1.0.0', 'category': 'Accounting & Finance', 'license': 'AGPL-3', 'summary': 'Generate UBL XML file for customer invoices/refunds', @@ -16,8 +16,7 @@ 'base_ubl_payment', ], 'data': [ - 'views/company.xml', 'views/account_invoice.xml', ], - 'installable': False, + 'installable': True, } diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 4c1d13eae1..854912e2e4 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api, _ +from odoo import models, api from lxml import etree -from openerp.tools import float_is_zero, float_round -from openerp.exceptions import Warning as UserError +from odoo.tools import float_is_zero, float_round import logging logger = logging.getLogger(__name__) @@ -100,9 +99,9 @@ def _ubl_add_invoice_line( line_id = etree.SubElement(line_root, ns['cbc'] + 'ID') line_id.text = unicode(line_number) uom_unece_code = False - # on v8, uos_id is not a required field on account.invoice.line - if iline.uos_id and iline.uos_id.unece_code: - uom_unece_code = iline.uos_id.unece_code + # 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', @@ -146,11 +145,11 @@ def _ubl_add_invoice_line_tax_total( prec = self.env['decimal.precision'].precision_get('Account') 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_id.compute_all( - price, iline.quantity, product=iline.product_id, + 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'], + 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) @@ -175,20 +174,10 @@ def _ubl_add_tax_total(self, xml_root, ns, version='2.1'): tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) tax_amount_node.text = unicode(self.amount_tax) - for tline in self.tax_line: - if not tline.base_code_id: - raise UserError(_( - "Missing base code on tax line '%s'.") % tline.name) - taxes = self.env['account.tax'].search([ - ('base_code_id', '=', tline.base_code_id.id)]) - if not taxes: - raise UserError(_( - "The tax code '%s' is not linked to a tax.") - % tline.base_code_id.name) - tax = taxes[0] + for tline in self.tax_line_ids: self._ubl_add_tax_subtotal( - tline.base, tline.amount, tax, cur_name, tax_total_node, ns, - version=version) + 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'): @@ -208,14 +197,14 @@ def generate_invoice_ubl_xml_etree(self, version='2.1'): self._ubl_add_payment_means( self.partner_bank_id, self.payment_mode_id, self.date_due, xml_root, ns, version=version) - if self.payment_term: + if self.payment_term_id: self._ubl_add_payment_terms( - self.payment_term, xml_root, ns, version=version) + 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: + for iline in self.invoice_line_ids: line_number += 1 self._ubl_add_invoice_line( xml_root, iline, line_number, ns, version=version) diff --git a/account_invoice_ubl/models/company.py b/account_invoice_ubl/models/company.py index bb65815304..ef3827fb87 100644 --- a/account_invoice_ubl/models/company.py +++ b/account_invoice_ubl/models/company.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields +from odoo import models, fields class ResCompany(models.Model): diff --git a/account_invoice_ubl/models/report.py b/account_invoice_ubl/models/report.py index b40f17518d..b1d63164df 100644 --- a/account_invoice_ubl/models/report.py +++ b/account_invoice_ubl/models/report.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, api +from odoo import models, api import logging logger = logging.getLogger(__name__) @@ -11,22 +11,18 @@ class Report(models.Model): _inherit = 'report' - @api.v7 - def get_pdf( - self, cr, uid, ids, report_name, html=None, data=None, - context=None): + @api.model + def get_pdf(self, docids, report_name, html=None, data=None): """We go through that method when the PDF is generated for the 1st time and also when it is read from the attachment. This method is specific to QWeb""" - if context is None: - context = {} pdf_content = super(Report, self).get_pdf( - cr, uid, ids, report_name, html=html, data=data, context=context) + docids, report_name, html=html, data=data) if ( report_name == 'account.report_invoice' and - len(ids) == 1 and - not context.get('no_embedded_ubl_xml')): - invoice = self.pool['account.invoice'].browse( - cr, uid, ids[0], context=dict(context, no_embedded_pdf=True)) + len(docids) == 1 and + not self._context.get('no_embedded_ubl_xml')): + invoice = self.env['account.invoice'].with_context( + no_embedded_pdf=True).browse(docids[0]) pdf_content = invoice.embed_ubl_xml_in_pdf(pdf_content) return pdf_content diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index 61fd164f4b..b1ed4064cb 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# © 2016 Akretion (Alexis de Lattre ) +# © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp.tests.common import TransactionCase -from openerp import workflow +from odoo.tests.common import TransactionCase class TestUblInvoice(TransactionCase): @@ -15,9 +14,7 @@ def test_ubl_generate(self): i += 1 invoice = self.env.ref('account.invoice_%d' % i) # validate invoice - workflow.trg_validate( - self.uid, 'account.invoice', invoice.id, 'invoice_open', - self.cr) + invoice.action_invoice_open() if invoice.type not in ('out_invoice', 'out_refund'): continue for version in ['2.0', '2.1']: diff --git a/account_invoice_ubl/views/account_invoice.xml b/account_invoice_ubl/views/account_invoice.xml index 4ec56c7ea0..5e8f5b48c1 100644 --- a/account_invoice_ubl/views/account_invoice.xml +++ b/account_invoice_ubl/views/account_invoice.xml @@ -1,11 +1,10 @@ - - + @@ -13,7 +12,7 @@ account.invoice - @@ -21,5 +20,4 @@ - - + diff --git a/account_invoice_ubl/views/company.xml b/account_invoice_ubl/views/company.xml deleted file mode 100644 index 8aa46c4ba4..0000000000 --- a/account_invoice_ubl/views/company.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - account_invoice_ubl.company.form - res.company - - - - - - - - - - - From 14cd1eb730e16fae8b25f5c406c2a8d99500a272 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 23 Feb 2017 00:43:59 +0100 Subject: [PATCH 09/67] Rename account_invoice_zugferd to account_invoice_factur-x Rename account_invoice_import_zugferd to account_invoice_import_factur-x Add module to support py3o reporting engine: --- account_invoice_ubl/models/account_invoice.py | 28 +++++++++++-------- account_invoice_ubl/models/report.py | 7 +++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 854912e2e4..64250bf337 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -154,12 +154,13 @@ def _ubl_add_invoice_line_tax_total( tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) tax_amount_node.text = unicode(tax_total) - 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) + 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) @api.multi def get_delivery_partner(self): @@ -174,10 +175,12 @@ def _ubl_add_tax_total(self, xml_root, ns, version='2.1'): tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) tax_amount_node.text = unicode(self.amount_tax) - 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) + prec = self.env['decimal.precision'].precision_get('Account') + 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'): @@ -250,7 +253,7 @@ def get_ubl_lang(self): return self.partner_id.lang or 'en_US' @api.multi - def embed_ubl_xml_in_pdf(self, pdf_content): + 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 @@ -259,7 +262,8 @@ def embed_ubl_xml_in_pdf(self, pdf_content): 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) + xml_string, ubl_filename, + pdf_content=pdf_content, pdf_file=pdf_file) return pdf_content @api.multi diff --git a/account_invoice_ubl/models/report.py b/account_invoice_ubl/models/report.py index b1d63164df..35d752a184 100644 --- a/account_invoice_ubl/models/report.py +++ b/account_invoice_ubl/models/report.py @@ -18,11 +18,14 @@ def get_pdf(self, docids, report_name, html=None, data=None): This method is specific to QWeb""" pdf_content = super(Report, self).get_pdf( docids, report_name, html=html, data=data) + invoice_reports = [ + 'account.report_invoice', + 'account.account_invoice_report_duplicate_main'] if ( - report_name == 'account.report_invoice' and + report_name in invoice_reports and len(docids) == 1 and not self._context.get('no_embedded_ubl_xml')): invoice = self.env['account.invoice'].with_context( no_embedded_pdf=True).browse(docids[0]) - pdf_content = invoice.embed_ubl_xml_in_pdf(pdf_content) + pdf_content = invoice.embed_ubl_xml_in_pdf(pdf_content=pdf_content) return pdf_content From d8fd0d40411d0230ea0cce1d0aca849d66ff643f Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 23 Feb 2017 23:44:11 +0100 Subject: [PATCH 10/67] FIX crash on self._model Add active buttons on invoice import config --- account_invoice_ubl/models/account_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 64250bf337..5fb30bde2b 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -277,7 +277,7 @@ def attach_ubl_xml_file_button(self): attach = self.env['ir.attachment'].create({ 'name': filename, 'res_id': self.id, - 'res_model': unicode(self._model), + 'res_model': unicode(self._name), 'datas': xml_string.encode('base64'), 'datas_fname': filename, # I have default_type = 'out_invoice' in context, so 'type' From 6638fe63932ad98b7b0034226d20e0c1caa15806 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 27 Feb 2017 23:23:59 +0100 Subject: [PATCH 11/67] Continue port of modules for v10.0, in particular sale_order_import_* module Fix spelling mistake and other remarks on README by Tarteo --- account_invoice_ubl/models/account_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 5fb30bde2b..dc2fa839b6 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -225,7 +225,7 @@ def generate_ubl_xml_string(self, version='2.1'): # 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 additionnal code to handle the 2 langs + # 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( From 9a40884c733e67111c21c47f0616528e320cab97 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 28 Feb 2017 21:51:43 +0100 Subject: [PATCH 12/67] Port purchase_order_import* to v10.0 Add ubl invoice generation option in accounting config page --- account_invoice_ubl/__manifest__.py | 1 + account_invoice_ubl/models/__init__.py | 1 + .../models/account_config_settings.py | 12 ++++++++++ .../views/account_config_settings.xml | 24 +++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 account_invoice_ubl/models/account_config_settings.py create mode 100644 account_invoice_ubl/views/account_config_settings.xml diff --git a/account_invoice_ubl/__manifest__.py b/account_invoice_ubl/__manifest__.py index d40d296e3a..d52bde9bd8 100644 --- a/account_invoice_ubl/__manifest__.py +++ b/account_invoice_ubl/__manifest__.py @@ -17,6 +17,7 @@ ], 'data': [ 'views/account_invoice.xml', + 'views/account_config_settings.xml', ], 'installable': True, } diff --git a/account_invoice_ubl/models/__init__.py b/account_invoice_ubl/models/__init__.py index 787bf4baa3..721058d5b2 100644 --- a/account_invoice_ubl/models/__init__.py +++ b/account_invoice_ubl/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from . import company +from . import account_config_settings from . import account_invoice from . import report diff --git a/account_invoice_ubl/models/account_config_settings.py b/account_invoice_ubl/models/account_config_settings.py new file mode 100644 index 0000000000..dcf91f98dd --- /dev/null +++ b/account_invoice_ubl/models/account_config_settings.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# © 2017 Akretion - Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields + + +class AccountConfigSettings(models.TransientModel): + _inherit = 'account.config.settings' + + embed_pdf_in_ubl_xml_invoice = fields.Boolean( + related='company_id.embed_pdf_in_ubl_xml_invoice') diff --git a/account_invoice_ubl/views/account_config_settings.xml b/account_invoice_ubl/views/account_config_settings.xml new file mode 100644 index 0000000000..0b77c47d37 --- /dev/null +++ b/account_invoice_ubl/views/account_config_settings.xml @@ -0,0 +1,24 @@ + + + + + + + account_invoice_ubl.account.config.settings + account.config.settings + + +
+
+ +
+
+
+
+ + +
From 2a8ba94aafafa035b93b8ad8fa4b9531c105b99a Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 17 Mar 2017 21:38:53 +0100 Subject: [PATCH 13/67] Stop using precision_get('Account') (this decimal precision doesn't exist in v10) in account_invoice_factur-x and account_invoice_ubl --- account_invoice_ubl/models/account_invoice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index dc2fa839b6..3e71b0d3a6 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -63,7 +63,7 @@ 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.env['decimal.precision'].precision_get('Account') + prec = self.currency_id.decimal_places line_total = etree.SubElement( monetary_total, ns['cbc'] + 'LineExtensionAmount', currencyID=cur_name) @@ -95,7 +95,7 @@ def _ubl_add_invoice_line( dpo = self.env['decimal.precision'] qty_precision = dpo.precision_get('Product Unit of Measure') price_precision = dpo.precision_get('Product Price') - account_precision = dpo.precision_get('Account') + account_precision = self.currency_id.decimal_places line_id = etree.SubElement(line_root, ns['cbc'] + 'ID') line_id.text = unicode(line_number) uom_unece_code = False @@ -142,7 +142,7 @@ def _ubl_add_invoice_line( def _ubl_add_invoice_line_tax_total( self, iline, parent_node, ns, version='2.1'): cur_name = self.currency_id.name - prec = self.env['decimal.precision'].precision_get('Account') + 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( @@ -175,7 +175,7 @@ def _ubl_add_tax_total(self, xml_root, ns, version='2.1'): tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) tax_amount_node.text = unicode(self.amount_tax) - prec = self.env['decimal.precision'].precision_get('Account') + prec = self.currency_id.decimal_places if not float_is_zero(self.amount_tax, precision_digits=prec): for tline in self.tax_line_ids: self._ubl_add_tax_subtotal( From 50d11441dadf1cb4f12d3daaff279d6fedd344b5 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 1 Jun 2017 13:41:59 +0200 Subject: [PATCH 14/67] Update test suite --- .../tests/test_ubl_generate.py | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index b1ed4064cb..da6633c380 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -2,26 +2,20 @@ # © 2016-2017 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import TransactionCase +from odoo.addons.account_payment_unece.tests.test_account_invoice import \ + TestAccountInvoice -class TestUblInvoice(TransactionCase): +class TestUblInvoice(TestAccountInvoice): def test_ubl_generate(self): - ro = self.registry['report'] + ro = self.env['report'] buo = self.env['base.ubl'] - for i in range(5): - i += 1 - invoice = self.env.ref('account.invoice_%d' % i) - # validate invoice - invoice.action_invoice_open() - if invoice.type not in ('out_invoice', 'out_refund'): - continue - for version in ['2.0', '2.1']: - # I didn't manage to make it work with new api :-( - pdf_file = ro.get_pdf( - self.cr, self.uid, invoice.ids, - 'account.report_invoice', context={'ubl_version': version}) - res = buo.get_xml_files_from_pdf(pdf_file) - invoice_filename = invoice.get_ubl_filename(version=version) - self.assertTrue(invoice_filename in res) + invoice = self.test_only_create_invoice() + for version in ['2.0', '2.1']: + # I didn't manage to make it work with new api :-( + pdf_file = ro.with_context(ubl_version=version).get_pdf( + [invoice.id], 'account.report_invoice') + res = buo.get_xml_files_from_pdf(pdf_file) + invoice_filename = invoice.get_ubl_filename(version=version) + self.assertTrue(invoice_filename in res) From 1a10285a1dea5c07d9b0613a454a7247663de8e5 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 24 Aug 2017 00:06:18 +0200 Subject: [PATCH 15/67] Improve + modularize generation of UBL (up-port from v8 PR) Add script mass_invoice_import.py Code cleanup --- account_invoice_ubl/models/account_invoice.py | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/account_invoice_ubl/models/account_invoice.py b/account_invoice_ubl/models/account_invoice.py index 3e71b0d3a6..6ce5742347 100644 --- a/account_invoice_ubl/models/account_invoice.py +++ b/account_invoice_ubl/models/account_invoice.py @@ -36,6 +36,36 @@ def _ubl_add_header(self, parent_node, ns, version='2.1'): 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.iteritems(): + 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 ( @@ -110,7 +140,7 @@ def _ubl_add_invoice_line( quantity = etree.SubElement( line_root, ns['cbc'] + 'InvoicedQuantity') qty = iline.quantity - quantity.text = unicode(qty) + quantity.text = '%0.*f' % (qty_precision, qty) line_amount = etree.SubElement( line_root, ns['cbc'] + 'LineExtensionAmount', currencyID=cur_name) @@ -130,14 +160,14 @@ def _ubl_add_invoice_line( price_unit = float_round( iline.price_subtotal / float(qty), precision_digits=price_precision) - price_amount.text = unicode(price_unit) + 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 = unicode(qty) + base_qty.text = '%0.*f' % (qty_precision, qty) def _ubl_add_invoice_line_tax_total( self, iline, parent_node, ns, version='2.1'): @@ -153,7 +183,7 @@ def _ubl_add_invoice_line_tax_total( precision_digits=prec) tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) - tax_amount_node.text = unicode(tax_total) + 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']) @@ -174,8 +204,8 @@ def _ubl_add_tax_total(self, xml_root, ns, version='2.1'): tax_total_node = etree.SubElement(xml_root, ns['cac'] + 'TaxTotal') tax_amount_node = etree.SubElement( tax_total_node, ns['cbc'] + 'TaxAmount', currencyID=cur_name) - tax_amount_node.text = unicode(self.amount_tax) 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( @@ -187,6 +217,9 @@ 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, @@ -235,7 +268,7 @@ def generate_ubl_xml_string(self, version='2.1'): logger.debug( 'Invoice UBL XML file generated for account invoice ID %d ' '(state %s)', self.id, self.state) - logger.debug(xml_string) + logger.debug(xml_string.decode('utf-8')) return xml_string @api.multi From 89997ef86e566256379b3b3278f95e221f350d94 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sat, 14 Oct 2017 23:39:55 +0200 Subject: [PATCH 16/67] Disable get_pdf() in all tests because it doesn't work in Travis --- account_invoice_ubl/tests/test_ubl_generate.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index da6633c380..4658a729e3 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -9,13 +9,15 @@ class TestUblInvoice(TestAccountInvoice): def test_ubl_generate(self): - ro = self.env['report'] - buo = self.env['base.ubl'] + # ro = self.env['report'] + # buo = self.env['base.ubl'] invoice = self.test_only_create_invoice() for version in ['2.0', '2.1']: - # I didn't manage to make it work with new api :-( - pdf_file = ro.with_context(ubl_version=version).get_pdf( - [invoice.id], 'account.report_invoice') - res = buo.get_xml_files_from_pdf(pdf_file) - invoice_filename = invoice.get_ubl_filename(version=version) - self.assertTrue(invoice_filename in res) + # get_pdf() doesn't work in Travis and I don't know why + # So I disable it for the moment + # pdf_file = ro.with_context(ubl_version=version).get_pdf( + # [invoice.id], 'account.report_invoice') + # res = buo.get_xml_files_from_pdf(pdf_file) + # invoice_filename = invoice.get_ubl_filename(version=version) + # self.assertTrue(invoice_filename in res) + invoice.generate_invoice_ubl_xml_etree(version=version) From c041b2128c609a42ef5081360816f7ca94581e60 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sun, 15 Oct 2017 23:39:02 +0200 Subject: [PATCH 17/67] [10.0] restore get_pdf() in tests (#31) * sale_order_ubl + purchase_order_ubl: restore get_pdf() in tests using HttpCase * Restore get_pdf() in tests of account_invoice_factur-x and account_invoice_ubl modules * Update oca_dependencies.txt --- account_invoice_ubl/tests/test_ubl_generate.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index 4658a729e3..1d35d75819 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -9,15 +9,12 @@ class TestUblInvoice(TestAccountInvoice): def test_ubl_generate(self): - # ro = self.env['report'] - # buo = self.env['base.ubl'] + ro = self.env['report'] + buo = self.env['base.ubl'] invoice = self.test_only_create_invoice() for version in ['2.0', '2.1']: - # get_pdf() doesn't work in Travis and I don't know why - # So I disable it for the moment - # pdf_file = ro.with_context(ubl_version=version).get_pdf( - # [invoice.id], 'account.report_invoice') - # res = buo.get_xml_files_from_pdf(pdf_file) - # invoice_filename = invoice.get_ubl_filename(version=version) - # self.assertTrue(invoice_filename in res) - invoice.generate_invoice_ubl_xml_etree(version=version) + pdf_file = ro.with_context(ubl_version=version).get_pdf( + [invoice.id], 'account.report_invoice') + res = buo.get_xml_files_from_pdf(pdf_file) + invoice_filename = invoice.get_ubl_filename(version=version) + self.assertTrue(invoice_filename in res) From 2982e396304881421d0420f216e08f2980e13402 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 17 Jan 2018 21:15:48 +0100 Subject: [PATCH 18/67] Add module account_e-invoice_generate to ensure compatibility for XML embedding in PDF between account_invoice_factur-x and account_invoice_ubl --- account_invoice_ubl/README.rst | 5 ++++- account_invoice_ubl/__init__.py | 1 + account_invoice_ubl/__manifest__.py | 5 +++-- account_invoice_ubl/models/company.py | 5 ++++- account_invoice_ubl/models/report.py | 10 +++++++--- account_invoice_ubl/post_install.py | 13 +++++++++++++ account_invoice_ubl/tests/test_ubl_generate.py | 2 ++ .../views/account_config_settings.xml | 4 ++-- 8 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 account_invoice_ubl/post_install.py diff --git a/account_invoice_ubl/README.rst b/account_invoice_ubl/README.rst index 648cc84aa1..0461ed0937 100644 --- a/account_invoice_ubl/README.rst +++ b/account_invoice_ubl/README.rst @@ -18,7 +18,10 @@ This module supports UBL version 2.1 (used by default) and 2.0. Configuration ============= -On the form view of the company, there is an option to embed the PDF of the invoices inside the XML invoice. +In the menu *Accounting > Configuration > Settings*, check the value of 2 options: + +* *XML Format embedded in PDF invoice* : if you want to have an UBL XML file embedded inside the PDF invoice, set it to *Universal Business Language (UBL)* +* if you work directly with XML invoices and you want to have the PDF invoice in base64 inside the XML file, enable the *Embed PDF in UBL XML Invoice*. Usage ===== diff --git a/account_invoice_ubl/__init__.py b/account_invoice_ubl/__init__.py index cde864bae2..de67edb299 100644 --- a/account_invoice_ubl/__init__.py +++ b/account_invoice_ubl/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import models +from .post_install import set_xml_format_in_pdf_invoice_to_ubl diff --git a/account_invoice_ubl/__manifest__.py b/account_invoice_ubl/__manifest__.py index d52bde9bd8..000adb5dba 100644 --- a/account_invoice_ubl/__manifest__.py +++ b/account_invoice_ubl/__manifest__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2016-2017 Akretion (Alexis de Lattre ) +# © 2016-2018 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { @@ -11,7 +11,7 @@ 'author': 'Akretion,Odoo Community Association (OCA)', 'website': 'http://www.akretion.com', 'depends': [ - 'account', + 'account_e-invoice_generate', 'account_payment_partner', 'base_ubl_payment', ], @@ -19,5 +19,6 @@ 'views/account_invoice.xml', 'views/account_config_settings.xml', ], + 'post_init_hook': 'set_xml_format_in_pdf_invoice_to_ubl', 'installable': True, } diff --git a/account_invoice_ubl/models/company.py b/account_invoice_ubl/models/company.py index ef3827fb87..c9b645c4f8 100644 --- a/account_invoice_ubl/models/company.py +++ b/account_invoice_ubl/models/company.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2016-2017 Akretion (Alexis de Lattre ) +# © 2016-2018 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields @@ -8,6 +8,9 @@ class ResCompany(models.Model): _inherit = 'res.company' + xml_format_in_pdf_invoice = fields.Selection( + selection_add=[('ubl', 'Universal Business Language (UBL)')], + default='ubl') embed_pdf_in_ubl_xml_invoice = fields.Boolean( string='Embed PDF in UBL XML Invoice', help="If active, the standalone UBL Invoice XML file will include the " diff --git a/account_invoice_ubl/models/report.py b/account_invoice_ubl/models/report.py index 35d752a184..b61bf3bd5d 100644 --- a/account_invoice_ubl/models/report.py +++ b/account_invoice_ubl/models/report.py @@ -25,7 +25,11 @@ def get_pdf(self, docids, report_name, html=None, data=None): report_name in invoice_reports and len(docids) == 1 and not self._context.get('no_embedded_ubl_xml')): - invoice = self.env['account.invoice'].with_context( - no_embedded_pdf=True).browse(docids[0]) - pdf_content = invoice.embed_ubl_xml_in_pdf(pdf_content=pdf_content) + invoice = self.env['account.invoice'].browse(docids[0]) + if ( + invoice.type in ('out_invoice', 'out_refund') and + invoice.company_id.xml_format_in_pdf_invoice == 'ubl'): + pdf_content = invoice.with_context( + no_embedded_pdf=True).embed_ubl_xml_in_pdf( + pdf_content=pdf_content) return pdf_content diff --git a/account_invoice_ubl/post_install.py b/account_invoice_ubl/post_install.py new file mode 100644 index 0000000000..8b2f343c4e --- /dev/null +++ b/account_invoice_ubl/post_install.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2018 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, SUPERUSER_ID + + +def set_xml_format_in_pdf_invoice_to_ubl(cr, registry): + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + companies = env['res.company'].search([]) + companies.write({'xml_format_in_pdf_invoice': 'ubl'}) + return diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index 1d35d75819..0f8372b223 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -12,6 +12,8 @@ def test_ubl_generate(self): ro = self.env['report'] buo = self.env['base.ubl'] invoice = self.test_only_create_invoice() + if invoice.company_id.xml_format_in_pdf_invoice != 'ubl': + invoice.company_id.xml_format_in_pdf_invoice = 'ubl' for version in ['2.0', '2.1']: pdf_file = ro.with_context(ubl_version=version).get_pdf( [invoice.id], 'account.report_invoice') diff --git a/account_invoice_ubl/views/account_config_settings.xml b/account_invoice_ubl/views/account_config_settings.xml index 0b77c47d37..470dec28ce 100644 --- a/account_invoice_ubl/views/account_config_settings.xml +++ b/account_invoice_ubl/views/account_config_settings.xml @@ -9,9 +9,9 @@ account_invoice_ubl.account.config.settings account.config.settings - + -
+
@@ -435,7 +435,7 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/edi project on GitHub.

+

This module is part of the OCA/edi project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/account_invoice_ubl/tests/test_ubl_generate.py b/account_invoice_ubl/tests/test_ubl_generate.py index e3b4eebc63..0cb2cc27c3 100644 --- a/account_invoice_ubl/tests/test_ubl_generate.py +++ b/account_invoice_ubl/tests/test_ubl_generate.py @@ -2,7 +2,7 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo.addons.account_payment_unece.tests.test_account_invoice import \ +from odoo.addons.account_tax_unece.tests.test_account_invoice import \ TestAccountInvoice @@ -15,8 +15,10 @@ def test_ubl_generate(self): if invoice.company_id.xml_format_in_pdf_invoice != 'ubl': invoice.company_id.xml_format_in_pdf_invoice = 'ubl' for version in ['2.0', '2.1']: - pdf_file = ro.with_context(ubl_version=version).render_qweb_pdf( - invoice.ids)[0] + pdf_file = ro.with_context( + ubl_version=version, + force_report_rendering=True + ).render_qweb_pdf(invoice.ids)[0] res = buo.get_xml_files_from_pdf(pdf_file) invoice_filename = invoice.get_ubl_filename(version=version) self.assertTrue(invoice_filename in res) From 6cd58b0cf13415abc21a0b3fa9174760e7387c53 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 30 Apr 2019 15:34:38 +0200 Subject: [PATCH 23/67] [FIX] Related field not readonly --- account_invoice_ubl/i18n/es.po | 40 +++++++++++++------ account_invoice_ubl/i18n/fr.po | 37 ++++++++++++----- .../models/res_config_settings.py | 3 +- .../static/description/index.html | 2 +- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/account_invoice_ubl/i18n/es.po b/account_invoice_ubl/i18n/es.po index 6461d763cd..d4acd526b0 100644 --- a/account_invoice_ubl/i18n/es.po +++ b/account_invoice_ubl/i18n/es.po @@ -24,19 +24,25 @@ msgid "Companies" msgstr "Compañías" #. module: account_invoice_ubl -#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company_embed_pdf_in_ubl_xml_invoice -#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_config_settings_embed_pdf_in_ubl_xml_invoice +#: model:ir.model,name:account_invoice_ubl.model_res_config_settings +#, fuzzy +msgid "Config Settings" +msgstr "account.config.settings" + +#. module: account_invoice_ubl +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company__embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_config_settings__embed_pdf_in_ubl_xml_invoice msgid "Embed PDF in UBL XML Invoice" msgstr "" #. module: account_invoice_ubl -#: model:ir.ui.view,arch_db:account_invoice_ubl.invoice_form +#: model_terms:ir.ui.view,arch_db:account_invoice_ubl.invoice_form msgid "Generate UBL XML File" msgstr "" #. module: account_invoice_ubl -#: model:ir.model.fields,help:account_invoice_ubl.field_res_company_embed_pdf_in_ubl_xml_invoice -#: model:ir.model.fields,help:account_invoice_ubl.field_res_config_settings_embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,help:account_invoice_ubl.field_res_company__embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,help:account_invoice_ubl.field_res_config_settings__embed_pdf_in_ubl_xml_invoice msgid "" "If active, the standalone UBL Invoice XML file will include the PDF of the " "invoice in base64 under the node 'AdditionalDocumentReference'. For example, " @@ -45,7 +51,7 @@ msgid "" msgstr "" #. module: account_invoice_ubl -#: model:ir.ui.view,arch_db:account_invoice_ubl.view_account_config_settings +#: model_terms:ir.ui.view,arch_db:account_invoice_ubl.view_account_config_settings msgid "Include the PDF of the invoice in the standalone UBL Invoice XML file." msgstr "" @@ -55,15 +61,23 @@ msgid "Invoice" msgstr "Factura" #. module: account_invoice_ubl -#: model:ir.model,name:account_invoice_ubl.model_ir_actions_report -msgid "ir.actions.report" +#: selection:res.company,xml_format_in_pdf_invoice:0 +msgid "None" msgstr "" #. module: account_invoice_ubl -#: model:ir.model,name:account_invoice_ubl.model_res_config_settings +#: model:ir.model,name:account_invoice_ubl.model_ir_actions_report #, fuzzy -msgid "res.config.settings" -msgstr "account.config.settings" +#| msgid "Report" +msgid "Report Action" +msgstr "Informe" -#~ msgid "Report" -#~ msgstr "Informe" +#. module: account_invoice_ubl +#: selection:res.company,xml_format_in_pdf_invoice:0 +msgid "Universal Business Language (UBL)" +msgstr "" + +#. module: account_invoice_ubl +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company__xml_format_in_pdf_invoice +msgid "XML Format embedded in PDF invoice" +msgstr "" diff --git a/account_invoice_ubl/i18n/fr.po b/account_invoice_ubl/i18n/fr.po index ebda437982..06fa8cf7de 100644 --- a/account_invoice_ubl/i18n/fr.po +++ b/account_invoice_ubl/i18n/fr.po @@ -24,19 +24,25 @@ msgid "Companies" msgstr "Sociétés" #. module: account_invoice_ubl -#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company_embed_pdf_in_ubl_xml_invoice -#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_config_settings_embed_pdf_in_ubl_xml_invoice +#: model:ir.model,name:account_invoice_ubl.model_res_config_settings +#, fuzzy +msgid "Config Settings" +msgstr "account.config.settings" + +#. module: account_invoice_ubl +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company__embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_config_settings__embed_pdf_in_ubl_xml_invoice msgid "Embed PDF in UBL XML Invoice" msgstr "" #. module: account_invoice_ubl -#: model:ir.ui.view,arch_db:account_invoice_ubl.invoice_form +#: model_terms:ir.ui.view,arch_db:account_invoice_ubl.invoice_form msgid "Generate UBL XML File" msgstr "" #. module: account_invoice_ubl -#: model:ir.model.fields,help:account_invoice_ubl.field_res_company_embed_pdf_in_ubl_xml_invoice -#: model:ir.model.fields,help:account_invoice_ubl.field_res_config_settings_embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,help:account_invoice_ubl.field_res_company__embed_pdf_in_ubl_xml_invoice +#: model:ir.model.fields,help:account_invoice_ubl.field_res_config_settings__embed_pdf_in_ubl_xml_invoice msgid "" "If active, the standalone UBL Invoice XML file will include the PDF of the " "invoice in base64 under the node 'AdditionalDocumentReference'. For example, " @@ -45,7 +51,7 @@ msgid "" msgstr "" #. module: account_invoice_ubl -#: model:ir.ui.view,arch_db:account_invoice_ubl.view_account_config_settings +#: model_terms:ir.ui.view,arch_db:account_invoice_ubl.view_account_config_settings msgid "Include the PDF of the invoice in the standalone UBL Invoice XML file." msgstr "" @@ -54,13 +60,22 @@ msgstr "" msgid "Invoice" msgstr "" +#. module: account_invoice_ubl +#: selection:res.company,xml_format_in_pdf_invoice:0 +msgid "None" +msgstr "" + #. module: account_invoice_ubl #: model:ir.model,name:account_invoice_ubl.model_ir_actions_report -msgid "ir.actions.report" +msgid "Report Action" msgstr "" #. module: account_invoice_ubl -#: model:ir.model,name:account_invoice_ubl.model_res_config_settings -#, fuzzy -msgid "res.config.settings" -msgstr "account.config.settings" +#: selection:res.company,xml_format_in_pdf_invoice:0 +msgid "Universal Business Language (UBL)" +msgstr "" + +#. module: account_invoice_ubl +#: model:ir.model.fields,field_description:account_invoice_ubl.field_res_company__xml_format_in_pdf_invoice +msgid "XML Format embedded in PDF invoice" +msgstr "" diff --git a/account_invoice_ubl/models/res_config_settings.py b/account_invoice_ubl/models/res_config_settings.py index 038ee6210f..8b4cc574c3 100644 --- a/account_invoice_ubl/models/res_config_settings.py +++ b/account_invoice_ubl/models/res_config_settings.py @@ -9,4 +9,5 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' embed_pdf_in_ubl_xml_invoice = fields.Boolean( - related='company_id.embed_pdf_in_ubl_xml_invoice') + related='company_id.embed_pdf_in_ubl_xml_invoice', + readonly=False) diff --git a/account_invoice_ubl/static/description/index.html b/account_invoice_ubl/static/description/index.html index 849e1efbef..b175ad54d0 100644 --- a/account_invoice_ubl/static/description/index.html +++ b/account_invoice_ubl/static/description/index.html @@ -3,7 +3,7 @@ - + Account Invoice UBL