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

[12.0][MIG] purchase_order_ubl + [ADD] purchase_stock_ubl #99

Merged
merged 20 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions purchase_order_ubl/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
==================
Purchase Order UBL
==================

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github
:target: https://github.com/OCA/edi/tree/11.0/purchase_order_ubl
:alt: OCA/edi
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/edi-11-0/edi-11-0-purchase_order_ubl
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/226/11.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds support for UBL, the `Universal Business Language (UBL) <http://ubl.xml.org/>`_ standard,
on purchase 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 purchase order or RFQ report:

* on a draft/RFQ/Bid Received purchase order, the PDF file will have an embedded XML *Request For Quotation* file compliant with the UBL 2.1 or 2.0 standard.

* on an approved purchase order, the PDF file will have an embedded XML *Order* file compliant with the UBL 2.1 or 2.0 standard.

If your supplier has Odoo and has installed the module *sale_order_import_ubl*, he will be able to import the PDF file and it will automatically create the quotation/sale order.

**Table of contents**

.. contents::
:local:

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 smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/edi/issues/new?body=module:%20purchase_order_ubl%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Akretion

Contributors
~~~~~~~~~~~~

* Alexis de Lattre <alexis.delattre@akretion.com>
* Andrea Stirpe <a.stirpe@onestein.nl>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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 <https://github.com/OCA/edi/tree/11.0/purchase_order_ubl>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions purchase_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 purchase_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': 'Purchase Order UBL',
'version': '12.0.1.0.0',
'category': 'Purchase Management',
'license': 'AGPL-3',
'summary': 'Embed UBL XML file inside the PDF purchase order',
'author': 'Akretion,Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/edi/',
'depends': ['purchase', 'base_ubl'],
'data': [],
'installable': True,
}
25 changes: 25 additions & 0 deletions purchase_order_ubl/i18n/purchase_order_ubl.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_order_ubl
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: purchase_order_ubl
#: model:ir.model,name:purchase_order_ubl.model_purchase_order
msgid "Purchase Order"
msgstr ""

#. module: purchase_order_ubl
#: model:ir.model,name:purchase_order_ubl.model_ir_actions_report
msgid "ir.actions.report"
msgstr ""

4 changes: 4 additions & 0 deletions purchase_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 purchase
from . import report
220 changes: 220 additions & 0 deletions purchase_order_ubl/models/purchase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# © 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 models, fields, api
from lxml import etree
import logging

logger = logging.getLogger(__name__)


class PurchaseOrder(models.Model):
_name = 'purchase.order'
_inherit = ['purchase.order', 'base.ubl']

@api.model
def get_rfq_states(self):
return ['draft', 'sent', 'to approve']

@api.model
def get_order_states(self):
return ['purchase', 'done']

@api.multi
def _ubl_add_header(self, doc_type, parent_node, ns, version='2.1'):
if doc_type == 'rfq':
now_utc = fields.Datetime.to_string(fields.Datetime.now())
date = now_utc[:10]
time = now_utc[11:]
currency_node_name = 'PricingCurrencyCode'
elif doc_type == 'order':
date = self.date_approve or self.date_order.date()
date = fields.Date.to_string(date)
currency_node_name = 'DocumentCurrencyCode'
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 == 'rfq': # IssueTime is required on RFQ, not on order
issue_time = etree.SubElement(parent_node, ns['cbc'] + 'IssueTime')
issue_time.text = time
if self.notes:
note = etree.SubElement(parent_node, ns['cbc'] + 'Note')
note.text = self.notes
doc_currency = etree.SubElement(
parent_node, ns['cbc'] + currency_node_name)
doc_currency.text = self.currency_id.name

@api.multi
def _ubl_add_monetary_total(self, parent_node, ns, version='2.1'):
monetary_total = etree.SubElement(
parent_node, ns['cac'] + 'AnticipatedMonetaryTotal')
line_total = etree.SubElement(
monetary_total, ns['cbc'] + 'LineExtensionAmount',
currencyID=self.currency_id.name)
line_total.text = str(self.amount_untaxed)
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_rfq_line(
self, parent_node, oline, line_number, ns, version='2.1'):
line_root = etree.SubElement(
parent_node, ns['cac'] + 'RequestForQuotationLine')
self._ubl_add_line_item(
line_number, oline.name, oline.product_id, 'purchase',
oline.product_qty, oline.product_uom, line_root, ns,
seller=self.partner_id.commercial_partner_id, version=version)

@api.multi
def _ubl_add_order_line(
self, parent_node, oline, line_number, ns, version='2.1'):
line_root = etree.SubElement(
parent_node, ns['cac'] + 'OrderLine')
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, 'purchase',
oline.product_qty, oline.product_uom, line_root, ns,
seller=self.partner_id.commercial_partner_id,
currency=self.currency_id, price_subtotal=oline.price_subtotal,
qty_precision=qty_precision, price_precision=price_precision,
version=version)

@api.multi
def get_delivery_partner(self):
self.ensure_one()
if self.dest_address_id:
return self.dest_address_id
return self.company_id.partner_id

@api.multi
def generate_rfq_ubl_xml_etree(self, version='2.1'):
nsmap, ns = self._ubl_get_nsmap_namespace(
'RequestForQuotation-2', version=version)
xml_root = etree.Element('RequestForQuotation', nsmap=nsmap)
doc_type = 'rfq'
self._ubl_add_header(doc_type, xml_root, ns, version=version)

# The order of SellerSupplierParty / BuyerCustomerParty is different
# between RFQ and Order !
self._ubl_add_supplier_party(
self.partner_id, False, 'SellerSupplierParty', xml_root, ns,
version=version)
if version == '2.1':
self._ubl_add_customer_party(
False, self.company_id, 'BuyerCustomerParty', xml_root, ns,
version=version)
delivery_partner = self.get_delivery_partner()
self._ubl_add_delivery(delivery_partner, xml_root, ns, version=version)
if self.incoterm_id:
self._ubl_add_delivery_terms(
self.incoterm_id, xml_root, ns, version=version)

line_number = 0
for oline in self.order_line:
line_number += 1
self._ubl_add_rfq_line(
xml_root, oline, line_number, ns, version=version)
return xml_root

@api.multi
def generate_order_ubl_xml_etree(self, version='2.1'):
nsmap, ns = self._ubl_get_nsmap_namespace('Order-2', version=version)
xml_root = etree.Element('Order', nsmap=nsmap)
doc_type = 'order'
self._ubl_add_header(doc_type, xml_root, ns, version=version)

self._ubl_add_customer_party(
False, self.company_id, 'BuyerCustomerParty', xml_root, ns,
version=version)
self._ubl_add_supplier_party(
self.partner_id, False, 'SellerSupplierParty', xml_root, ns,
version=version)
delivery_partner = self.get_delivery_partner()
self._ubl_add_delivery(delivery_partner, xml_root, ns, version=version)
if self.incoterm_id:
self._ubl_add_delivery_terms(
self.incoterm_id, xml_root, ns, version=version)
if self.payment_term_id:
self._ubl_add_payment_terms(
self.payment_term_id, xml_root, ns, version=version)
self._ubl_add_monetary_total(xml_root, ns, version=version)

line_number = 0
for oline in self.order_line:
line_number += 1
self._ubl_add_order_line(
xml_root, oline, line_number, 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 ('order', 'rfq'), '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 == 'order':
xml_root = self.with_context(lang=lang).\
generate_order_ubl_xml_etree(version=version)
document = 'Order'
elif doc_type == 'rfq':
xml_root = self.with_context(lang=lang).\
generate_rfq_ubl_xml_etree(version=version)
document = 'RequestForQuotation'
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 purchase 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 == 'rfq':
return 'UBL-RequestForQuotation-%s.xml' % version
elif doc_type == 'order':
return 'UBL-Order-%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_rfq_states():
doc_type = 'rfq'
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
30 changes: 30 additions & 0 deletions purchase_order_ubl/models/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# © 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 Report(models.Model):
_inherit = "ir.actions.report"

@api.multi
def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=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"""
purchase_reports = self._get_purchase_order_ubl_reports()
if (
len(self) == 1 and
self.report_name in purchase_reports and
res_ids and
len(res_ids) == 1 and
not self._context.get('no_embedded_ubl_xml')):
order = self.env['purchase.order'].browse(res_ids[0])
pdf_content = order.embed_ubl_xml_in_pdf(pdf_content=pdf_content)
return super()._post_pdf(
save_in_attachment, pdf_content=pdf_content, res_ids=res_ids)

def _get_purchase_order_ubl_reports(self):
return [
'purchase.report_purchaseorder',
'purchase.report_purchasequotation']
2 changes: 2 additions & 0 deletions purchase_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>
Loading