Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.0][MIG] sale_order_ubl #47

Merged
merged 12 commits into from
Oct 30, 2018
58 changes: 58 additions & 0 deletions sale_order_ubl/README.rst
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.
3 changes: 3 additions & 0 deletions sale_order_ubl/__init__.py
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
15 changes: 15 additions & 0 deletions sale_order_ubl/__manifest__.py
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,
}
4 changes: 4 additions & 0 deletions sale_order_ubl/models/__init__.py
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
24 changes: 24 additions & 0 deletions sale_order_ubl/models/report.py
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
194 changes: 194 additions & 0 deletions sale_order_ubl/models/sale.py
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
2 changes: 2 additions & 0 deletions sale_order_ubl/readme/CONTRIBUTORS.rst
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>
10 changes: 10 additions & 0 deletions sale_order_ubl/readme/DESCRIPTION.rst
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.
3 changes: 3 additions & 0 deletions sale_order_ubl/tests/__init__.py
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
32 changes: 32 additions & 0 deletions sale_order_ubl/tests/test_ubl_generate.py
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)