-
-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #47 from onesteinbv/11_mig_sale_order_ubl
[11.0][MIG] sale_order_ubl
- Loading branch information
Showing
10 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png | ||
:target: https://www.gnu.org/licenses/agpl | ||
:alt: License: AGPL-3 | ||
|
||
============== | ||
Sale Order UBL | ||
============== | ||
|
||
This module adds support for UBL, the `Universal Business Language (UBL) <http://ubl.xml.org/>`_ standard, | ||
on sale orders. The UBL 2.1 standard became the | ||
`ISO/IEC 19845 <http://www.iso.org/iso/catalogue_detail.htm?csnumber=66370>`_ standard | ||
in December 2015 (cf the `official announce <http://www.prweb.com/releases/2016/01/prweb13186919.htm>`_). | ||
|
||
With this module, when you generate the sale order report: | ||
|
||
* on a draft/sent quotation, the PDF file will have an embedded XML *Quotation* file compliant with the UBL 2.1 or 2.0 standard. | ||
|
||
* on a confirmed sale order, the PDF file will have an embedded XML *Order Response Simple* file compliant with the UBL 2.1 or 2.0 standard. | ||
|
||
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/11.0 | ||
|
||
Bug Tracker | ||
=========== | ||
|
||
|
||
Bugs are tracked on `GitHub Issues | ||
<https://github.com/OCA/edi/issues>`_. In case of trouble, please | ||
check there if your issue has already been reported. If you spotted it first, | ||
help us smash it by providing detailed and welcomed feedback. | ||
|
||
Credits | ||
======= | ||
|
||
Contributors | ||
------------ | ||
|
||
* Alexis de Lattre <alexis.delattre@akretion.com> | ||
* Andrea Stirpe <a.stirpe@onestein.nl> | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# © 2016-2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>) | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
{ | ||
'name': 'Sale Order UBL', | ||
'version': '11.0.1.0.0', | ||
'category': 'Sales', | ||
'license': 'AGPL-3', | ||
'summary': 'Embed UBL XML file inside the PDF quotation', | ||
'author': 'Akretion,Odoo Community Association (OCA)', | ||
'website': 'https://github.com/OCA/edi/', | ||
'depends': ['sale', 'base_ubl'], | ||
'data': [], | ||
'installable': True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from . import sale | ||
from . import report |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# © 2016-2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>) | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import api, models | ||
|
||
|
||
class IrActionsReport(models.Model): | ||
_inherit = "ir.actions.report" | ||
|
||
@api.multi | ||
def render_qweb_pdf(self, res_ids=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""" | ||
pdf_content = super(IrActionsReport, self).render_qweb_pdf( | ||
res_ids, data) | ||
if ( | ||
len(self) == 1 and | ||
self.report_name == 'sale.report_saleorder' and | ||
len(res_ids) == 1 and | ||
not self._context.get('no_embedded_ubl_xml')): | ||
order = self.env['sale.order'].browse(res_ids[0]) | ||
pdf_content = order.embed_ubl_xml_in_pdf(pdf_content=pdf_content) | ||
return pdf_content |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# © 2016-2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>) | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import api, fields, models | ||
from lxml import etree | ||
import logging | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class SaleOrder(models.Model): | ||
_name = 'sale.order' | ||
_inherit = ['sale.order', 'base.ubl'] | ||
|
||
@api.model | ||
def get_quotation_states(self): | ||
return ['draft', 'sent'] | ||
|
||
@api.model | ||
def get_order_states(self): | ||
return ['sale', 'done'] | ||
|
||
@api.multi | ||
def _ubl_add_header(self, doc_type, parent_node, ns, version='2.1'): | ||
now_utc = fields.Datetime.now() | ||
date = now_utc[:10] | ||
time = now_utc[11:] | ||
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.name | ||
issue_date = etree.SubElement(parent_node, ns['cbc'] + 'IssueDate') | ||
issue_date.text = date | ||
if doc_type == 'quotation': | ||
issue_time = etree.SubElement(parent_node, ns['cbc'] + 'IssueTime') | ||
issue_time.text = time | ||
if self.note: | ||
note = etree.SubElement(parent_node, ns['cbc'] + 'Note') | ||
note.text = self.note | ||
if doc_type == 'quotation': | ||
doc_currency = etree.SubElement( | ||
parent_node, ns['cbc'] + 'PricingCurrencyCode') | ||
doc_currency.text = self.currency_id.name | ||
|
||
@api.multi | ||
def _ubl_add_quoted_monetary_total(self, parent_node, ns, version='2.1'): | ||
monetary_total = etree.SubElement( | ||
parent_node, ns['cac'] + 'QuotedMonetaryTotal') | ||
line_total = etree.SubElement( | ||
monetary_total, ns['cbc'] + 'LineExtensionAmount', | ||
currencyID=self.currency_id.name) | ||
line_total.text = str(self.amount_untaxed) | ||
tax_inclusive_amount = etree.SubElement( | ||
monetary_total, ns['cbc'] + 'TaxInclusiveAmount', | ||
currencyID=self.currency_id.name) | ||
tax_inclusive_amount.text = str(self.amount_total) | ||
payable_amount = etree.SubElement( | ||
monetary_total, ns['cbc'] + 'PayableAmount', | ||
currencyID=self.currency_id.name) | ||
payable_amount.text = str(self.amount_total) | ||
|
||
@api.multi | ||
def _ubl_add_quotation_line( | ||
self, parent_node, oline, line_number, ns, version='2.1'): | ||
line_root = etree.SubElement( | ||
parent_node, ns['cac'] + 'QuotationLine') | ||
dpo = self.env['decimal.precision'] | ||
qty_precision = dpo.precision_get('Product Unit of Measure') | ||
price_precision = dpo.precision_get('Product Price') | ||
self._ubl_add_line_item( | ||
line_number, oline.name, oline.product_id, 'sale', | ||
oline.product_uom_qty, oline.product_uom, line_root, ns, | ||
currency=self.currency_id, price_subtotal=oline.price_subtotal, | ||
qty_precision=qty_precision, price_precision=price_precision, | ||
version=version) | ||
|
||
@api.multi | ||
def generate_quotation_ubl_xml_etree(self, version='2.1'): | ||
nsmap, ns = self._ubl_get_nsmap_namespace( | ||
'Quotation-2', version=version) | ||
xml_root = etree.Element('Quotation', nsmap=nsmap) | ||
doc_type = 'quotation' | ||
self._ubl_add_header(doc_type, xml_root, ns, version=version) | ||
|
||
self._ubl_add_supplier_party( | ||
False, self.company_id, 'SellerSupplierParty', xml_root, ns, | ||
version=version) | ||
if version == '2.1': | ||
self._ubl_add_customer_party( | ||
self.partner_id, False, 'BuyerCustomerParty', xml_root, ns, | ||
version=version) | ||
self._ubl_add_delivery( | ||
self.partner_shipping_id, xml_root, ns, version=version) | ||
if hasattr(self, 'incoterm') and self.incoterm: | ||
self._ubl_add_delivery_terms( | ||
self.incoterm, xml_root, ns, version=version) | ||
self._ubl_add_quoted_monetary_total(xml_root, ns, version=version) | ||
|
||
line_number = 0 | ||
for oline in self.order_line: | ||
line_number += 1 | ||
self._ubl_add_quotation_line( | ||
xml_root, oline, line_number, ns, version=version) | ||
return xml_root | ||
|
||
@api.multi | ||
def generate_order_response_simple_ubl_xml_etree(self, version='2.1'): | ||
nsmap, ns = self._ubl_get_nsmap_namespace( | ||
'OrderResponseSimple-2', version=version) | ||
xml_root = etree.Element('OrderResponseSimple', nsmap=nsmap) | ||
doc_type = 'order' | ||
self._ubl_add_header(doc_type, xml_root, ns, version=version) | ||
|
||
accepted_indicator = etree.SubElement( | ||
xml_root, ns['cbc'] + 'AcceptedIndicator') | ||
accepted_indicator.text = 'true' | ||
order_reference = etree.SubElement( | ||
xml_root, ns['cac'] + 'OrderReference') | ||
order_reference_id = etree.SubElement( | ||
order_reference, ns['cbc'] + 'ID') | ||
order_reference_id.text = self.client_order_ref or 'Missing' | ||
self._ubl_add_supplier_party( | ||
False, self.company_id, 'SellerSupplierParty', xml_root, ns, | ||
version=version) | ||
self._ubl_add_customer_party( | ||
self.partner_id, False, 'BuyerCustomerParty', xml_root, ns, | ||
version=version) | ||
return xml_root | ||
|
||
@api.multi | ||
def generate_ubl_xml_string(self, doc_type, version='2.1'): | ||
self.ensure_one() | ||
assert doc_type in ('quotation', 'order'), 'wrong doc_type' | ||
logger.debug('Starting to generate UBL XML %s file', doc_type) | ||
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 | ||
if doc_type == 'quotation': | ||
xml_root = self.with_context(lang=lang).\ | ||
generate_quotation_ubl_xml_etree(version=version) | ||
document = 'Quotation' | ||
elif doc_type == 'order': | ||
xml_root = self.with_context(lang=lang).\ | ||
generate_order_response_simple_ubl_xml_etree(version=version) | ||
document = 'OrderResponseSimple' | ||
xml_string = etree.tostring( | ||
xml_root, pretty_print=True, encoding='UTF-8', | ||
xml_declaration=True) | ||
self._ubl_check_xml_schema(xml_string, document, version=version) | ||
logger.debug( | ||
'%s UBL XML file generated for sale order ID %d (state %s)', | ||
doc_type, self.id, self.state) | ||
logger.debug(xml_string) | ||
return xml_string | ||
|
||
@api.multi | ||
def get_ubl_filename(self, doc_type, version='2.1'): | ||
"""This method is designed to be inherited""" | ||
if doc_type == 'quotation': | ||
return 'UBL-Quotation-%s.xml' % version | ||
elif doc_type == 'order': | ||
return 'UBL-OrderResponseSimple-%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() | ||
doc_type = False | ||
if self.state in self.get_quotation_states(): | ||
doc_type = 'quotation' | ||
elif self.state in self.get_order_states(): | ||
doc_type = 'order' | ||
if doc_type: | ||
version = self.get_ubl_version() | ||
ubl_filename = self.get_ubl_filename(doc_type, version=version) | ||
xml_string = self.generate_ubl_xml_string( | ||
doc_type, version=version) | ||
pdf_content = self.embed_xml_in_pdf( | ||
xml_string, ubl_filename, | ||
pdf_content=pdf_content, pdf_file=pdf_file) | ||
return pdf_content |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* Alexis de Lattre <alexis.delattre@akretion.com> | ||
* Andrea Stirpe <a.stirpe@onestein.nl> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
This module adds support for UBL, the `Universal Business Language (UBL) <http://ubl.xml.org/>`_ standard, | ||
on sale orders. The UBL 2.1 standard became the | ||
`ISO/IEC 19845 <http://www.iso.org/iso/catalogue_detail.htm?csnumber=66370>`_ standard | ||
in December 2015 (cf the `official announce <http://www.prweb.com/releases/2016/01/prweb13186919.htm>`_). | ||
|
||
With this module, when you generate the sale order report: | ||
|
||
* on a draft/sent quotation, the PDF file will have an embedded XML *Quotation* file compliant with the UBL 2.1 or 2.0 standard. | ||
|
||
* on a confirmed sale order, the PDF file will have an embedded XML *Order Response Simple* file compliant with the UBL 2.1 or 2.0 standard. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from . import test_ubl_generate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# © 2016-2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>) | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from odoo.tests.common import HttpCase | ||
|
||
|
||
class TestUblOrderImport(HttpCase): | ||
|
||
def test_ubl_generate(self): | ||
ro = self.env.ref('sale.action_report_saleorder') | ||
soo = self.env['sale.order'] | ||
buo = self.env['base.ubl'] | ||
quotation_states = soo.get_quotation_states() | ||
order_states = soo.get_order_states() | ||
for i in range(8): | ||
i += 1 | ||
order = self.env.ref('sale.sale_order_%d' % i) | ||
for version in ['2.0', '2.1']: | ||
pdf_file = ro.with_context( | ||
ubl_version=version | ||
).render_qweb_pdf(order.ids)[0] | ||
res = buo.get_xml_files_from_pdf(pdf_file) | ||
if order.state in quotation_states: | ||
filename = order.get_ubl_filename( | ||
'quotation', version=version) | ||
self.assertTrue(filename in res) | ||
elif order.state in order_states: | ||
filename = order.get_ubl_filename( | ||
'order', | ||
version=version | ||
) | ||
self.assertTrue(filename in res) |