Skip to content

Commit

Permalink
Merge pull request #47 from onesteinbv/11_mig_sale_order_ubl
Browse files Browse the repository at this point in the history
[11.0][MIG] sale_order_ubl
  • Loading branch information
pedrobaeza committed Oct 30, 2018
2 parents 6bbb7a3 + f4c1d15 commit 96c6e40
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 0 deletions.
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)

0 comments on commit 96c6e40

Please sign in to comment.