diff --git a/delivery_tnt_oca/README.rst b/delivery_tnt_oca/README.rst new file mode 100644 index 0000000000..73054c364d --- /dev/null +++ b/delivery_tnt_oca/README.rst @@ -0,0 +1,114 @@ +================ +Delivery TNT OCA +================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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%2Fdelivery--carrier-lightgray.png?logo=github + :target: https://github.com/OCA/delivery-carrier/tree/14.0/delivery_tnt_oca + :alt: OCA/delivery-carrier +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/delivery-carrier-14-0/delivery-carrier-14-0-delivery_tnt_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/99/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds `TNT `_ to the available carriers. + +It allows you to register shippings, generate labels, get rates from order and read +shipping states using TNT webservice, so no need of exchanging +any kind of file. + +When a sales order is created in Odoo and the TNT carrier is assigned, the shipping +price that will be obtained will be the price that the TNT webservice estimates +according to the order information (address and products). + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +#. Add a carrier account with delivery type ``tnt_oca`` and fill in your credentials +#. Configure in Odoo all required fields of the TNT tab according to the information provided by the TNT user (Username WS , Password WS, Account, etc) + +Usage +===== + +You have to set the created shipping method in the delivery order to ship: + +* When the picking is 'Transferred', a *Create Shipping Label* button appears. Just click on it, and if all went well, the label will be 'attached'. +* If the shipment creation process fails, a validation error will appear displaying TNT error. +* A periodical state check will be done querying TNT services. + +Known issues / Roadmap +====================== + +* It is not possible to cancel a shipment through webservice. + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Víctor Martínez + * Pedro M. Baeza + +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. + +.. |maintainer-victoralmau| image:: https://github.com/victoralmau.png?size=40px + :target: https://github.com/victoralmau + :alt: victoralmau + +Current `maintainer `__: + +|maintainer-victoralmau| + +This module is part of the `OCA/delivery-carrier `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/delivery_tnt_oca/__init__.py b/delivery_tnt_oca/__init__.py new file mode 100644 index 0000000000..31660d6a96 --- /dev/null +++ b/delivery_tnt_oca/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/delivery_tnt_oca/__manifest__.py b/delivery_tnt_oca/__manifest__.py new file mode 100644 index 0000000000..d10747eceb --- /dev/null +++ b/delivery_tnt_oca/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2021-2022 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Delivery TNT OCA", + "summary": "Integrate TNT webservice", + "version": "14.0.1.0.0", + "category": "Delivery", + "website": "https://github.com/OCA/delivery-carrier", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "delivery", + "delivery_package_number", + "delivery_state", + "product_dimension", + "base_sparse_field", + ], + "external_dependencies": {"python": ["dicttoxml", "xmltodict"]}, + "data": [ + "views/delivery_carrier_view.xml", + "report/picking_templates.xml", + "report/stock_report_views.xml", + ], + "installable": True, + "maintainers": ["victoralmau"], +} diff --git a/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Pricing Integration Guide v3 Schema.pdf b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Pricing Integration Guide v3 Schema.pdf new file mode 100644 index 0000000000..161db4229a Binary files /dev/null and b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Pricing Integration Guide v3 Schema.pdf differ diff --git a/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Shipping Integration Guide v3.pdf b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Shipping Integration Guide v3.pdf new file mode 100644 index 0000000000..b3ddfbc5bb Binary files /dev/null and b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect Shipping Integration Guide v3.pdf differ diff --git a/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect_Tracking_V3_1.pdf b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect_Tracking_V3_1.pdf new file mode 100644 index 0000000000..c37e836016 Binary files /dev/null and b/delivery_tnt_oca/doc/ExpressConnect/ExpressConnect_Tracking_V3_1.pdf differ diff --git a/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel Integration Guide v1.10.pdf b/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel Integration Guide v1.10.pdf new file mode 100644 index 0000000000..8e02a624fe Binary files /dev/null and b/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel Integration Guide v1.10.pdf differ diff --git a/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel_Integration Guide_Arrangements.pdf b/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel_Integration Guide_Arrangements.pdf new file mode 100644 index 0000000000..e0c6c66721 Binary files /dev/null and b/delivery_tnt_oca/doc/ExpressLabel/ExpressLabel_Integration Guide_Arrangements.pdf differ diff --git a/delivery_tnt_oca/i18n/delivery_tnt_oca.pot b/delivery_tnt_oca/i18n/delivery_tnt_oca.pot new file mode 100644 index 0000000000..9206d90c00 --- /dev/null +++ b/delivery_tnt_oca/i18n/delivery_tnt_oca.pot @@ -0,0 +1,349 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_tnt_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.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: delivery_tnt_oca +#: model:ir.actions.report,print_report_name:delivery_tnt_oca.label_delivery_tnt_oca +msgid "'TNT-%s' % object.carrier_tracking_ref" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex10 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex10 +msgid "10:00 Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__12d +msgid "12:00 EXPRESS" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ec12 +msgid "12:00 Economy Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex12 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex12 +msgid "12:00 Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__09d +msgid "9:00 EXPRESS" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex09 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex09 +msgid "9:00 Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Account" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_product_packaging__package_carrier_type +msgid "Carrier" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_collect_time_from +msgid "Collect time from" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_collect_time_to +msgid "Collect time to" +msgstr "" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Credentials" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_type__d +msgid "Document (paper/manuals/reports)" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_line_of_business__1 +msgid "Domestic transfers" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_n__48n +msgid "ECONOMY EXPRESS" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ec +msgid "Economy Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex +msgid "Express" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__15d +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_n__15n +msgid "GLOBAL EXPRESS" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_line_of_business__2 +msgid "International non-domestic transfers" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_line_of_business +msgid "Line of business" +msgstr "" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Misc" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_type__n +msgid "Non-document (packages)" +msgstr "" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Password" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_payment_indicator +msgid "Payment indicator" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_product_packaging +msgid "Product Packaging" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code +msgid "Product code" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code_d +msgid "Product code (Docs)" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code_n +msgid "Product code (Non docs)" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service +msgid "Product service" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service_d +msgid "Product service (Docs)" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service_n +msgid "Product service (Non docs)" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_type +msgid "Product type" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__delivery_type +msgid "Provider" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_payment_indicator__r +msgid "Receiver pays" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_payment_indicator__s +msgid "Sender pays" +msgstr "" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "" +"Sending to TNT\n" +"%s" +msgstr "" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "Server not reachable, please try again later" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_delivery_carrier +msgid "Shipping Methods" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__delivery_type__tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__product_packaging__package_carrier_type__tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "TNT" +msgstr "" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/delivery_carrier.py:0 +#, python-format +msgid "TNT API does not allow you to cancel a shipment." +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.actions.report,name:delivery_tnt_oca.label_delivery_tnt_oca +msgid "TNT Label (ZPL)" +msgstr "" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "Timeout: the server did not reply within 60s" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_cluster_code +msgid "Tnt Consignment Cluster Code" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_data +msgid "Tnt Consignment Data" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_date +msgid "Tnt Consignment Date" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_destination_depot +msgid "Tnt Consignment Destination Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_destination_depot_day +msgid "Tnt Consignment Destination Depot Day" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_free_circulation +msgid "Tnt Consignment Free Circulation" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_market +msgid "Tnt Consignment Market" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_mumber +msgid "Tnt Consignment Mumber" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_option +msgid "Tnt Consignment Option" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_origin_depot +msgid "Tnt Consignment Origin Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_product +msgid "Tnt Consignment Product" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_sort_split +msgid "Tnt Consignment Sort Split" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_transit_depot +msgid "Tnt Consignment Transit Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_transport +msgid "Tnt Consignment Transport" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_xray +msgid "Tnt Consignment Xray" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_piece_barcode +msgid "Tnt Piece Barcode" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_piece_data +msgid "Tnt Piece Data" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Username" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_account +msgid "WS Account" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_password +msgid "WS Password" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_username +msgid "WS Username" +msgstr "" diff --git a/delivery_tnt_oca/i18n/es.po b/delivery_tnt_oca/i18n/es.po new file mode 100644 index 0000000000..f1d876d719 --- /dev/null +++ b/delivery_tnt_oca/i18n/es.po @@ -0,0 +1,353 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_tnt_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-11-04 15:09+0000\n" +"PO-Revision-Date: 2021-11-04 16:10+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 2.3\n" + +#. module: delivery_tnt_oca +#: model:ir.actions.report,print_report_name:delivery_tnt_oca.label_delivery_tnt_oca +msgid "'TNT-%s' % object.carrier_tracking_ref" +msgstr "'TNT-%s' % object.carrier_tracking_ref" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex10 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex10 +msgid "10:00 Express" +msgstr "10:00 Express" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__12d +msgid "12:00 EXPRESS" +msgstr "12:00 EXPRESS" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ec12 +msgid "12:00 Economy Express" +msgstr "12:00 Economy Express" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex12 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex12 +msgid "12:00 Express" +msgstr "12:00 Express" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__09d +msgid "9:00 EXPRESS" +msgstr "9:00 EXPRESS" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex09 +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex09 +msgid "9:00 Express" +msgstr "9:00 Express" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Account" +msgstr "Cuenta" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_product_packaging__package_carrier_type +msgid "Carrier" +msgstr "Transportista" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_collect_time_from +msgid "Collect time from" +msgstr "Hora de recogida inicio" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_collect_time_to +msgid "Collect time to" +msgstr "Hora de recogida fin" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Credentials" +msgstr "Credenciales" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_type__d +msgid "Document (paper/manuals/reports)" +msgstr "Documento (papel/manuales/informes)" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_line_of_business__1 +msgid "Domestic transfers" +msgstr "Transferencias nacionales" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_n__48n +msgid "ECONOMY EXPRESS" +msgstr "ECONOMY EXPRESS" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ec +msgid "Economy Express" +msgstr "Economy Express" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_d__ex +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_service_n__ex +msgid "Express" +msgstr "Express" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_d__15d +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_code_n__15n +msgid "GLOBAL EXPRESS" +msgstr "GLOBAL EXPRESS" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_line_of_business__2 +msgid "International non-domestic transfers" +msgstr "Transferencias internacionales no nacionales" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_line_of_business +msgid "Line of business" +msgstr "Línea de negocio" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Misc" +msgstr "Varios" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_product_type__n +msgid "Non-document (packages)" +msgstr "No documentos (paquetes)" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Password" +msgstr "Contraseña" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_payment_indicator +msgid "Payment indicator" +msgstr "Indicador de pago" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_product_packaging +msgid "Product Packaging" +msgstr "Empaquetado del producto" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code +msgid "Product code" +msgstr "Código de producto" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code_d +msgid "Product code (Docs)" +msgstr "Código de producto (Documentos)" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_code_n +msgid "Product code (Non docs)" +msgstr "Código de producto (No documentos)" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service +msgid "Product service" +msgstr "Servicio de producto" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service_d +msgid "Product service (Docs)" +msgstr "Servicio de producto (Documentos)" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_service_n +msgid "Product service (Non docs)" +msgstr "Servicio de producto (No documentos)" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_product_type +msgid "Product type" +msgstr "Tipo de producto" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__delivery_type +msgid "Provider" +msgstr "Proveedor" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_payment_indicator__r +msgid "Receiver pays" +msgstr "Pagado por el receptor" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__tnt_payment_indicator__s +msgid "Sender pays" +msgstr "Pagado por el remitente" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "" +"Sending to TNT\n" +"%s" +msgstr "" +"Envío a TNT\n" +"%s" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "Server not reachable, please try again later" +msgstr "El servidor no responde, por favor vuelve a intentarlo más tarde" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_delivery_carrier +msgid "Shipping Methods" +msgstr "Métodos de envío" + +#. module: delivery_tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__delivery_carrier__delivery_type__tnt_oca +#: model:ir.model.fields.selection,name:delivery_tnt_oca.selection__product_packaging__package_carrier_type__tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "TNT" +msgstr "TNT" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/delivery_carrier.py:0 +#, python-format +msgid "TNT API does not allow you to cancel a shipment." +msgstr "La API de TNT no le permite cancelar un envío." + +#. module: delivery_tnt_oca +#: model:ir.actions.report,name:delivery_tnt_oca.label_delivery_tnt_oca +msgid "TNT Label (ZPL)" +msgstr "Etiqueta TNT (ZPL)" + +#. module: delivery_tnt_oca +#: code:addons/delivery_tnt_oca/models/tnt_request.py:0 +#, python-format +msgid "Timeout: the server did not reply within 60s" +msgstr "El servidor no ha contestado en 60s" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_cluster_code +msgid "Tnt Consignment Cluster Code" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_data +msgid "Tnt Consignment Data" +msgstr "Datos de envío de TNT" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_date +msgid "Tnt Consignment Date" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_destination_depot +msgid "Tnt Consignment Destination Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_destination_depot_day +msgid "Tnt Consignment Destination Depot Day" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_free_circulation +msgid "Tnt Consignment Free Circulation" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_market +msgid "Tnt Consignment Market" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_mumber +msgid "Tnt Consignment Mumber" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_option +msgid "Tnt Consignment Option" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_origin_depot +msgid "Tnt Consignment Origin Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_product +msgid "Tnt Consignment Product" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_sort_split +msgid "Tnt Consignment Sort Split" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_transit_depot +msgid "Tnt Consignment Transit Depot" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_transport +msgid "Tnt Consignment Transport" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_consignment_xray +msgid "Tnt Consignment Xray" +msgstr "" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_piece_barcode +msgid "Tnt Piece Barcode" +msgstr "Código de barras" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_stock_picking__tnt_piece_data +msgid "Tnt Piece Data" +msgstr "Datos de pieza de TNT" + +#. module: delivery_tnt_oca +#: model:ir.model,name:delivery_tnt_oca.model_stock_picking +msgid "Transfer" +msgstr "Albarán" + +#. module: delivery_tnt_oca +#: model_terms:ir.ui.view,arch_db:delivery_tnt_oca.view_delivery_carrier_tnt_oca_form +msgid "Username" +msgstr "Usuario" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_account +msgid "WS Account" +msgstr "Cuenta" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_password +msgid "WS Password" +msgstr "Contraseña" + +#. module: delivery_tnt_oca +#: model:ir.model.fields,field_description:delivery_tnt_oca.field_delivery_carrier__tnt_oca_ws_username +msgid "WS Username" +msgstr "Usuario" diff --git a/delivery_tnt_oca/models/__init__.py b/delivery_tnt_oca/models/__init__.py new file mode 100644 index 0000000000..acc7ff48f0 --- /dev/null +++ b/delivery_tnt_oca/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import delivery_carrier +from . import product_packaging +from . import stock_picking +from . import tnt_request diff --git a/delivery_tnt_oca/models/delivery_carrier.py b/delivery_tnt_oca/models/delivery_carrier.py new file mode 100644 index 0000000000..f145980bb5 --- /dev/null +++ b/delivery_tnt_oca/models/delivery_carrier.py @@ -0,0 +1,181 @@ +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 + +from odoo import _, api, fields, models + +from .tnt_request import TntRequest + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + delivery_type = fields.Selection( + selection_add=[("tnt_oca", "TNT")], + ondelete={"tnt_oca": "set default"}, + ) + tnt_oca_ws_username = fields.Char(string="WS Username") + tnt_oca_ws_password = fields.Char(string="WS Password") + tnt_oca_ws_account = fields.Char(string="WS Account") + # Misc + tnt_product_type = fields.Selection( + selection=[ + ("D", "Document (paper/manuals/reports)"), + ("N", "Non-document (packages)"), + ], + default="N", + string="Product type", + ) + tnt_product_code = fields.Char( + compute="_compute_tnt_product_code", + string="Product code", + ) + tnt_product_code_d = fields.Selection( + selection=[ + ("09D", "9:00 EXPRESS"), + ("12D", "12:00 EXPRESS"), + ("15D", "GLOBAL EXPRESS"), + ], + default="09D", + string="Product code (Docs)", + ) + tnt_product_code_n = fields.Selection( + selection=[("15N", "GLOBAL EXPRESS"), ("48N", "ECONOMY EXPRESS")], + default="15N", + string="Product code (Non docs)", + ) + tnt_product_service = fields.Char( + compute="_compute_tnt_product_service", + string="Product service", + ) + tnt_product_service_d = fields.Selection( + selection=[ + ("EX", "Express"), + ("EX09", "9:00 Express"), + ("EX10", "10:00 Express"), + ("EX12", "12:00 Express"), + ], + default="EX", + string="Product service (Docs)", + ) + tnt_product_service_n = fields.Selection( + selection=[ + ("EC", "Economy Express"), + ("EC12", "12:00 Economy Express"), + ("EX", "Express"), + ("EX09", "9:00 Express"), + ("EX10", "10:00 Express"), + ("EX12", "12:00 Express"), + ], + default="EX", + string="Product service (Non docs)", + ) + tnt_payment_indicator = fields.Selection( + selection=[("S", "Sender pays"), ("R", "Receiver pays")], + default="S", + string="Payment indicator", + ) + tnt_line_of_business = fields.Selection( + selection=[ + ("1", "Domestic transfers"), + ("2", "International non-domestic transfers"), + ], + default="1", + string="Line of business", + ) + tnt_collect_time_from = fields.Float(default=10.5, string="Collect time from") + tnt_collect_time_to = fields.Float(default=16, string="Collect time to") + + @api.depends("delivery_type", "tnt_product_type") + def _compute_tnt_product_code(self): + self.tnt_product_code = self.tnt_product_code + for item in self.filtered(lambda x: x.delivery_type == "tnt_oca"): + item.tnt_product_code = ( + item.tnt_product_code_d + if item.tnt_product_type == "D" + else item.tnt_product_code_n + ) + + @api.depends("delivery_type", "tnt_product_type") + def _compute_tnt_product_service(self): + self.tnt_product_service = self.tnt_product_service + for item in self.filtered(lambda x: x.delivery_type == "tnt_oca"): + item.tnt_product_service = ( + item.tnt_product_service_d + if item.tnt_product_type == "D" + else item.tnt_product_service_n + ) + + def tnt_oca_rate_shipment(self, order): + tnt_request = TntRequest(self, order) + response = tnt_request.rate_shipment() + if response["success"]: + response["price"] = self._tnt_oca_get_response_price( + response, order.currency_id, order.company_id + ) + return { + "success": response["success"], + "price": response["price"], + "error_message": False, + "warning_message": False, + } + + def _tnt_oca_get_response_price(self, response, currency, company): + """We need to convert the price if the currency is different.""" + price = float(response["price"]) + if response["currency"] != currency.name: + price = currency._convert( + price, + self.env["res.currency"].search([("name", "=", response["currency"])]), + company, + fields.Date.today(), + ) + return price + + def _tnt_oca_action_label(self, picking): + report_name = "delivery_tnt_oca.label_delivery_tnt_oca_template" + iar = self.env["ir.actions.report"] + res = iar._get_report_from_name(report_name).render_qweb_text(picking.ids) + return self.env["ir.attachment"].create( + { + "name": "TNT-%s.txt" % picking.carrier_tracking_ref, + "type": "binary", + "datas": base64.b64encode(res[0]), + "res_model": picking._name, + "res_id": picking.id, + } + ) + + def tnt_oca_send_shipping(self, pickings): + return [self.tnt_oca_create_shipping(p) for p in pickings] + + def tnt_oca_create_shipping(self, picking): + self.ensure_one() + tnt_request = TntRequest(self, picking) + tnt_request._send_shipping() + tnt_request._get_label_info() + self._tnt_oca_action_label(picking) + return { + "exact_price": 0, + "tracking_number": picking.carrier_tracking_ref, + } + + def tnt_oca_tracking_state_update(self, picking): + self.ensure_one() + if picking.carrier_tracking_ref: + tnt_request = TntRequest(self, picking) + response = tnt_request.tracking_state_update() + picking.delivery_state = response["delivery_state"] + picking.tracking_state_history = response["tracking_state_history"] + + def tnt_oca_cancel_shipment(self, pickings): + raise NotImplementedError( + _("""TNT API does not allow you to cancel a shipment.""") + ) + + def tnt_oca_get_tracking_link(self, picking): + return "%s/%s?searchType=con&cons=%s" % ( + "https://www.tnt.com", + "express/es_es/site/herramientas-envio/seguimiento.html", + picking.carrier_tracking_ref, + ) diff --git a/delivery_tnt_oca/models/product_packaging.py b/delivery_tnt_oca/models/product_packaging.py new file mode 100644 index 0000000000..a0ceb83890 --- /dev/null +++ b/delivery_tnt_oca/models/product_packaging.py @@ -0,0 +1,9 @@ +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ProductPackaging(models.Model): + _inherit = "product.packaging" + + package_carrier_type = fields.Selection(selection_add=[("tnt_oca", "TNT")]) diff --git a/delivery_tnt_oca/models/stock_picking.py b/delivery_tnt_oca/models/stock_picking.py new file mode 100644 index 0000000000..968bdeffd0 --- /dev/null +++ b/delivery_tnt_oca/models/stock_picking.py @@ -0,0 +1,27 @@ +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + tnt_consignment_data = fields.Serialized() + tnt_consignment_mumber = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_date = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_free_circulation = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_sort_split = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_destination_depot = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_destination_depot_day = fields.Integer( + sparse="tnt_consignment_data" + ) + tnt_consignment_cluster_code = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_origin_depot = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_product = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_option = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_market = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_transport = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_transit_depot = fields.Char(sparse="tnt_consignment_data") + tnt_consignment_xray = fields.Char(sparse="tnt_consignment_data") + tnt_piece_data = fields.Serialized() + tnt_piece_barcode = fields.Char(sparse="tnt_piece_data") diff --git a/delivery_tnt_oca/models/tnt_request.py b/delivery_tnt_oca/models/tnt_request.py new file mode 100644 index 0000000000..b202abde67 --- /dev/null +++ b/delivery_tnt_oca/models/tnt_request.py @@ -0,0 +1,445 @@ +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import json +import logging +import re + +import dicttoxml +import requests +import xmltodict + +from odoo import _, fields, tools +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) +dicttoxml.LOG.setLevel(logging.ERROR) + + +class TntRequest(object): + def __init__(self, carrier, record): + self.carrier = carrier + self.record = record + self.appVersion = 3.0 + self.product_type = self.carrier.tnt_product_type + self.product_code = self.carrier.tnt_product_code + self.product_service = self.carrier.tnt_product_service + self.username = self.carrier.tnt_oca_ws_username + self.password = self.carrier.tnt_oca_ws_password + self.account = self.carrier.tnt_oca_ws_account + self.url = "https://express.tnt.com" + auth_encoding = "%s:%s" % (self.username, self.password) + self.authorization = base64.b64encode(auth_encoding.encode("utf-8")).decode( + "utf-8" + ) + + def _send_api_request( + self, url, data=None, auth=True, content_type="application/xml" + ): + if data is None: + data = {} + tnt_last_request = ("URL: {}\nData: {}").format(self.url, data) + self.carrier.log_xml(tnt_last_request, "tnt_last_request") + try: + headers = {"Content-Type": content_type} + if auth: + headers["Authorization"] = "Basic {}".format(self.authorization) + res = requests.post(url=url, data=data, headers=headers, timeout=60) + res.raise_for_status() + self.carrier.log_xml(res.text or "", "tnt_last_response") + res = res.text + except requests.exceptions.Timeout: + raise UserError(_("Timeout: the server did not reply within 60s")) + except (ValueError, requests.exceptions.ConnectionError): + raise UserError(_("Server not reachable, please try again later")) + except requests.exceptions.HTTPError as e: + raise UserError( + _("{}\n{}".format(e, res.json().get("Message", "") if res.text else "")) + ) + return res + + def _partner_to_shipping_data(self, partner): + return { + "country": partner.country_id.code, + "town": partner.city, + "postcode": partner.zip, + } + + def _prepare_product(self): + return { + "id": self.product_code, + "type": self.product_type, + "options": {"option": {"optionCode": self.product_code}}, + } + + def _prepare_account(self, partner): + return { + "accountNumber": self.account, + "accountCountry": partner.country_id.code, + } + + def _prepare_rate_shipment(self): + totalWeight = 0 + totalVolume = 0 + for line in self.record.order_line.filtered( + lambda x: x.product_id + and (x.product_id.weight > 0 or x.product_id.volume > 0) + ): + totalWeight += line.product_id.weight * line.product_uom_qty + totalVolume += line.product_id.volume * line.product_uom_qty + data = { + "appId": "PC", + "appVersion": self.appVersion, + "priceCheck": { + "rateId": self.record.name, + "sender": self._partner_to_shipping_data( + self.record.company_id.partner_id + ), + "delivery": self._partner_to_shipping_data( + self.record.partner_shipping_id + ), + "collectionDateTime": self.record.expected_date, + "product": self._prepare_product(), + "account": self._prepare_account(self.record.company_id.partner_id), + "currency": self.record.currency_id.name, + "priceBreakDown": True, + "consignmentDetails": { + "totalWeight": totalWeight, + "totalVolume": totalVolume, + "totalNumberOfPieces": 1, + }, + }, + } + return dicttoxml.dicttoxml( + data, attr_type=False, custom_root="priceRequest" + ).decode("utf-8") + + def rate_shipment(self): + response = self._send_api_request( + url="%s/expressconnect/pricing/getprice" % self.url, + data=self._prepare_rate_shipment(), + ) + response = json.loads(json.dumps(xmltodict.parse(response)))["document"] + if "errors" in response and "priceResponse" not in response: + errors = response["errors"]["brokenRule"] + if type(errors) is not list: + errors = [errors] + raise UserError( + _("Sending to TNT\n%s") + % ("\n".join("%(code)s %(description)s" % error for error in errors)) + ) + res = { + "success": False, + "price": 0, + } + if "priceResponse" in response: + service = response["priceResponse"]["ratedServices"]["ratedService"] + res["success"] = True + res["price"] = service["totalPrice"] + res["currency"] = response["priceResponse"]["ratedServices"]["currency"] + return res + + # ShippingSevice + def _quant_package_data_from_picking(self): + data_total = self._get_data_total_shipping() + return { + "ITEMS": self.record.number_of_packages, + "DESCRIPTION": self.record.name, + "LENGTH": data_total["length"], + "HEIGHT": data_total["height"], + "WIDTH": data_total["width"], + "WEIGHT": data_total["weight"], + } + + def _prepare_address(self, partner): + return { + "COMPANYNAME": partner.name, + "STREETADDRESS1": partner.street, + "CITY": partner.city, + "PROVINCE": partner.state_id.name, + "POSTCODE": partner.zip, + "COUNTRY": partner.country_id.code, + "ACCOUNT": self.account, + "VAT": partner.vat or "", + "CONTACTNAME": partner.name, + "CONTACTDIALCODE": "0000", + "CONTACTTELEPHONE": partner.phone, + "CONTACTEMAIL": partner.email, + } + + def _prepare_collection(self, partner): + address = self._prepare_address(partner) + del address["ACCOUNT"] + shipdate = self.record.scheduled_date.date() + return { + "COLLECTION": { + "COLLECTIONADDRESS": address, + "SHIPDATE": "%s/%s/%s" % (shipdate.day, shipdate.month, shipdate.year), + "PREFCOLLECTTIME": { + "FROM": tools.format_duration(self.carrier.tnt_collect_time_from), + "TO": tools.format_duration(self.carrier.tnt_collect_time_to), + }, + "COLLINSTRUCTIONS": "", + } + } + + def _prepare_sender(self): + data = self._prepare_address(self.record.company_id.partner_id) + collection = self._prepare_collection(self.record.company_id.partner_id) + data.update(collection) + return data + + def _get_data_total_shipping(self): + weight = self.record.shipping_weight + volume = p_length = height = width = 0 + if self.record.package_ids: + weight = sum(self.record.package_ids.mapped("quant_ids.quantity")) + height = max(self.record.package_ids.mapped("height")) + width = max(self.record.package_ids.mapped("width")) + p_length = max(self.record.package_ids.mapped("length")) + for quant in self.record.package_ids.mapped("quant_ids"): + volume += quant.product_id.volume * quant.quantity + else: + lines = self.record.move_line_ids_without_package + for line in lines.filtered(lambda x: x.qty_done > 0): + volume += line.product_id.volume * line.qty_done + p_length += line.product_id.product_length * line.qty_done + height += line.product_id.product_height * line.qty_done + width += line.product_id.product_width * line.qty_done + return { + "weight": weight, + "volume": volume, + "length": p_length, + "height": height, + "width": width, + } + + def _prepare_create_shipping(self): + receiver = self._prepare_address(self.record.company_id.partner_id) + del receiver["ACCOUNT"] + delivery = self._prepare_address(self.record.partner_id) + del delivery["ACCOUNT"] + data_total = self._get_data_total_shipping() + package = self._quant_package_data_from_picking() + data = { + "LOGIN": { + "COMPANY": self.username, + "PASSWORD": self.password, + "APPID": "IN", + "APPVERSION": self.appVersion, + }, + "CONSIGNMENTBATCH": { + "SENDER": self._prepare_sender(), + "CONSIGNMENT": { + "CONREF": self.record.name, + "DETAILS": { + "RECEIVER": receiver, + "DELIVERY": delivery, + "CUSTOMERREF": self.record.name, + "CONTYPE": self.product_type, + "PAYMENTIND": self.carrier.tnt_payment_indicator, + "ITEMS": self.record.number_of_packages, + "TOTALWEIGHT": data_total["weight"], + "TOTALVOLUME": data_total["volume"], + "SERVICE": self.product_code, + "OPTION": "PR", + "DESCRIPTION": "", + "DELIVERYINST": "", + "PACKAGE": package, + # Campos no especificados en la documentación pero requeridos + "INVOICENUMBER": "", + "PURCHASEORDERNUMBER": "", + "INCOTERMS": "", + "DISCOUNT": "", + "INSURANCECHARGES": "", + "FREIGHTCHARGES": "", + "OTHERCHARGES": "", + }, + }, + }, + "ACTIVITY": { + "CREATE": {"CONREF": self.record.name}, + "BOOK": {"CONREF": self.record.name}, + "SHIP": {"CONREF": self.record.name}, + "PRINT": { + "CONNOTE": {"CONREF": self.record.name}, + "LABEL": {"CONREF": self.record.name}, + "MANIFEST": {"CONREF": self.record.name}, + }, + }, + } + xml_info = dicttoxml.dicttoxml( + data, attr_type=False, custom_root="ESHIPPER" + ).decode("utf-8") + return "xml_in=%s" % xml_info + + def _action_picking(self, action, complete_id): + new_data = "xml_in=%s:%s" % (action, complete_id) + response = self._send_api_request( + url="%s/expressconnect/shipping/ship" % self.url, + data=new_data, + auth=False, + content_type="application/x-www-form-urlencoded", + ) + res = json.loads(json.dumps(xmltodict.parse(response))) + if "document" in res: + res = res["document"] + return res + + def _send_shipping(self): + response = self._send_api_request( + url="%s/expressconnect/shipping/ship" % self.url, + data=self._prepare_create_shipping(), + auth=False, + content_type="application/x-www-form-urlencoded", + ) + complete_id = response.replace("COMPLETE:", "") + res = self._action_picking("GET_RESULT", complete_id) + if "ERROR" in res: + errors = res["ERROR"] + if type(errors) is not list: + errors = [errors] + raise UserError( + _("Sending to TNT\n%s") + % ("\n".join("%(CODE)s %(DESCRIPTION)s" % error for error in errors)) + ) + if "CREATE" in res: + self.record.carrier_tracking_ref = res["CREATE"]["CONNUMBER"] + + # TrackSevice + def _prepare_state_update(self): + data = { + "SearchCriteria": {"ConsignmentNumber": self.record.carrier_tracking_ref}, + "LevelOfDetail": {"Summary": ""}, + } + xml_info = dicttoxml.dicttoxml( + data, attr_type=False, custom_root="TrackRequest" + ).decode("utf-8") + return "xml_in=%s" % xml_info + + def tracking_state_update(self): + response = self._send_api_request( + url="%s/expressconnect/track.do" % self.url, + data=self._prepare_state_update(), + content_type="application/x-www-form-urlencoded", + ) + response = json.loads(json.dumps(xmltodict.parse(response))) + SummaryCode = response["TrackResponse"]["Consignment"]["SummaryCode"] + mapped_states = { + "INT": "in_transit", + "DEL": "customer_delivered", + "EXC": "incidence", + "CNF": "shipping_recorded_in_carrier", + } + return { + "delivery_state": mapped_states.get(SummaryCode, "incidence"), + "tracking_state_history": SummaryCode, + } + + # TntLabel + def _prepare_label_address(self, partner): + return { + "name": partner.name, + "addressLine1": partner.street, + "town": partner.city, + "exactMatch": "Y", + "province": partner.state_id.name, + "postcode": partner.zip, + "country": partner.country_id.code, + } + + def _prepare_label(self): + data_total = self._get_data_total_shipping() + data = { + "consignment": { + "consignmentIdentity": { + "consignmentNumber": re.sub( + "[^0-9]", "", self.record.carrier_tracking_ref + ), + "customerReference": self.record.name, + }, + "collectionDateTime": fields.Datetime.today(), + "sender": self._prepare_label_address( + self.record.company_id.partner_id + ), + "delivery": self._prepare_label_address(self.record.partner_id), + "product": { + "lineOfBusiness": self.record.carrier_id.tnt_line_of_business, + "groupId": 0, + "subGroupId": 0, + "id": self.product_service, + "type": self.product_type, + "option": self.product_code, + }, + "account": self._prepare_account(self.record.company_id.partner_id), + "totalNumberOfPieces": self.record.number_of_packages, + "pieceLine": { + "identifier": 1, + "goodsDescription": self.record.name, + "pieceMeasurements": { + "length": data_total["length"], + "width": data_total["width"], + "height": data_total["height"], + "weight": data_total["weight"], + }, + "pieces": { + "sequenceNumbers": self.record.number_of_packages, + "pieceReference": "", + }, + }, + } + } + return dicttoxml.dicttoxml( + data, attr_type=False, custom_root="labelRequest" + ).decode("utf-8") + + def _get_label_info(self): + if not self.record.carrier_tracking_ref: + return + response = self._send_api_request( + url="%s/expresslabel/documentation/getlabel" % self.url, + data=self._prepare_label(), + content_type="application/x-www-form-urlencoded", + ) + res = json.loads(json.dumps(xmltodict.parse(response))) + if "labelResponse" in res: + res = res["labelResponse"] + if "brokenRules" in res: + errors = res["brokenRules"] + if type(errors) is not list: + errors = [errors] + raise UserError( + _("Sending to TNT\n%s") + % ( + "\n".join( + "%(errorCode)s %(errorDescription)s" % error for error in errors + ) + ) + ) + res = res["consignment"] + p_data = res["pieceLabelData"] + c_data = res["consignmentLabelData"] + twoDBarcode_text_split = p_data["twoDBarcode"]["#text"].split("|") + c_data_fcd = c_data["freeCirculationDisplay"] + c_data_dd = c_data["destinationDepot"] + vals = { + "tnt_consignment_mumber": c_data["consignmentNumber"], + "tnt_consignment_date": twoDBarcode_text_split[-2], + "tnt_consignment_free_circulation": c_data_fcd["#text"], + "tnt_consignment_sort_split": c_data["sortSplitText"], + "tnt_consignment_destination_depot": c_data_dd["depotCode"], + "tnt_consignment_destination_depot_day": c_data_dd["dueDayOfMonth"], + "tnt_consignment_cluster_code": c_data["clusterCode"], + "tnt_consignment_origin_depot": c_data["originDepot"]["depotCode"], + "tnt_consignment_product": c_data["product"]["#text"], + "tnt_consignment_option": twoDBarcode_text_split[19], + "tnt_consignment_market": c_data["marketDisplay"]["#text"], + "tnt_consignment_transport": c_data["transportDisplay"]["#text"], + "tnt_piece_barcode": p_data["barcode"]["#text"], + } + if "transitDepots" in c_data and c_data["transitDepots"]: + transitDepot = c_data["transitDepots"]["transitDepot"] + vals["tnt_consignment_transit_depot"] = transitDepot["depotCode"] + if "xrayDisplay" in c_data and "#text" in c_data["xrayDisplay"]: + vals["tnt_consignment_xray"] = c_data["xrayDisplay"]["#text"] + self.record.write(vals) diff --git a/delivery_tnt_oca/readme/CONFIGURE.rst b/delivery_tnt_oca/readme/CONFIGURE.rst new file mode 100644 index 0000000000..90636802fa --- /dev/null +++ b/delivery_tnt_oca/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +To configure this module, you need to: + +#. Add a carrier account with delivery type ``tnt_oca`` and fill in your credentials +#. Configure in Odoo all required fields of the TNT tab according to the information provided by the TNT user (Username WS , Password WS, Account, etc) diff --git a/delivery_tnt_oca/readme/CONTRIBUTORS.rst b/delivery_tnt_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..5fb7130530 --- /dev/null +++ b/delivery_tnt_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `_: + + * Víctor Martínez + * Pedro M. Baeza diff --git a/delivery_tnt_oca/readme/DESCRIPTION.rst b/delivery_tnt_oca/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..5b44cc7403 --- /dev/null +++ b/delivery_tnt_oca/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module adds `TNT `_ to the available carriers. + +It allows you to register shippings, generate labels, get rates from order and read +shipping states using TNT webservice, so no need of exchanging +any kind of file. + +When a sales order is created in Odoo and the TNT carrier is assigned, the shipping +price that will be obtained will be the price that the TNT webservice estimates +according to the order information (address and products). diff --git a/delivery_tnt_oca/readme/ROADMAP.rst b/delivery_tnt_oca/readme/ROADMAP.rst new file mode 100644 index 0000000000..f7ff1874c1 --- /dev/null +++ b/delivery_tnt_oca/readme/ROADMAP.rst @@ -0,0 +1 @@ +* It is not possible to cancel a shipment through webservice. diff --git a/delivery_tnt_oca/readme/USAGE.rst b/delivery_tnt_oca/readme/USAGE.rst new file mode 100644 index 0000000000..190355954c --- /dev/null +++ b/delivery_tnt_oca/readme/USAGE.rst @@ -0,0 +1,5 @@ +You have to set the created shipping method in the delivery order to ship: + +* When the picking is 'Transferred', a *Create Shipping Label* button appears. Just click on it, and if all went well, the label will be 'attached'. +* If the shipment creation process fails, a validation error will appear displaying TNT error. +* A periodical state check will be done querying TNT services. diff --git a/delivery_tnt_oca/report/picking_templates.xml b/delivery_tnt_oca/report/picking_templates.xml new file mode 100644 index 0000000000..baa69b0abb --- /dev/null +++ b/delivery_tnt_oca/report/picking_templates.xml @@ -0,0 +1,114 @@ + + + + + + diff --git a/delivery_tnt_oca/report/stock_report_views.xml b/delivery_tnt_oca/report/stock_report_views.xml new file mode 100644 index 0000000000..07a591efaf --- /dev/null +++ b/delivery_tnt_oca/report/stock_report_views.xml @@ -0,0 +1,14 @@ + + + + TNT Label (ZPL) + stock.picking + qweb-text + delivery_tnt_oca.label_delivery_tnt_oca_template + delivery_tnt_oca.label_delivery_tnt_oca_template + + diff --git a/delivery_tnt_oca/static/description/icon.png b/delivery_tnt_oca/static/description/icon.png new file mode 100644 index 0000000000..cec15d5ab3 Binary files /dev/null and b/delivery_tnt_oca/static/description/icon.png differ diff --git a/delivery_tnt_oca/static/description/index.html b/delivery_tnt_oca/static/description/index.html new file mode 100644 index 0000000000..066fdee924 --- /dev/null +++ b/delivery_tnt_oca/static/description/index.html @@ -0,0 +1,457 @@ + + + + + + +Delivery TNT OCA + + + +
+

Delivery TNT OCA

+ + +

Beta License: AGPL-3 OCA/delivery-carrier Translate me on Weblate Try me on Runbot

+

This module adds TNT to the available carriers.

+

It allows you to register shippings, generate labels, get rates from order and read +shipping states using TNT webservice, so no need of exchanging +any kind of file.

+

When a sales order is created in Odoo and the TNT carrier is assigned, the shipping +price that will be obtained will be the price that the TNT webservice estimates +according to the order information (address and products).

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Add a carrier account with delivery type tnt_oca and fill in your credentials
  2. +
  3. Configure in Odoo all required fields of the TNT tab according to the information provided by the TNT user (Username WS , Password WS, Account, etc)
  4. +
+
+
+

Usage

+

You have to set the created shipping method in the delivery order to ship:

+
    +
  • When the picking is ‘Transferred’, a Create Shipping Label button appears. Just click on it, and if all went well, the label will be ‘attached’.
  • +
  • If the shipment creation process fails, a validation error will appear displaying TNT error.
  • +
  • A periodical state check will be done querying TNT services.
  • +
+
+
+

Known issues / Roadmap

+
    +
  • It is not possible to cancel a shipment through webservice.
  • +
+
+
+

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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Víctor Martínez
    • +
    • Pedro M. Baeza
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

victoralmau

+

This module is part of the OCA/delivery-carrier project on GitHub.

+

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

+
+
+
+ + diff --git a/delivery_tnt_oca/tests/__init__.py b/delivery_tnt_oca/tests/__init__.py new file mode 100644 index 0000000000..31dae48866 --- /dev/null +++ b/delivery_tnt_oca/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_delivery_tnt diff --git a/delivery_tnt_oca/tests/test_delivery_tnt.py b/delivery_tnt_oca/tests/test_delivery_tnt.py new file mode 100644 index 0000000000..2c11bde6e2 --- /dev/null +++ b/delivery_tnt_oca/tests/test_delivery_tnt.py @@ -0,0 +1,119 @@ +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import timedelta + +from odoo.exceptions import UserError +from odoo.tests import Form, common + + +class DeliveryTnt(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.carrier = cls.env["delivery.carrier"].search( + [("delivery_type", "=", "tnt_oca")] + ) + cls.company = cls.env.company + cls.partner = cls.env["res.partner"].create( + { + "name": "Test partner", + "country_id": cls.company.partner_id.country_id.id, + "phone": cls.company.partner_id.phone, + "email": "test@odoo.com", + "street": cls.company.partner_id.street, + "city": cls.company.partner_id.city, + "zip": cls.company.partner_id.zip, + "state_id": cls.company.partner_id.state_id.id, + "vat": cls.company.partner_id.vat, + } + ) + cls.product = cls.env.ref("product.product_delivery_01") + cls.product.write( + { + "weight": 1, + "volume": 1, + "product_length": 1, + "product_width": 1, + "product_height": 1, + "sale_delay": 3, + } + ) + cls.sale = cls._create_sale_order(cls) + cls.picking = cls.sale.picking_ids[0] + cls.picking.move_lines.quantity_done = 1 + + def _create_sale_order(self): + order_form = Form(self.env["sale.order"]) + order_form.partner_id = self.partner + with order_form.order_line.new() as line_form: + line_form.product_id = self.product + line_form.product_uom_qty = 1 + sale = order_form.save() + if self.carrier: + delivery_wizard = Form( + self.env["choose.delivery.carrier"].with_context( + {"default_order_id": sale.id, "default_carrier_id": self.carrier.id} + ) + ).save() + delivery_wizard.button_confirm() + sale.action_confirm() + return sale + + def test_order_tnt_oca_rate_shipment(self): + if not self.carrier or self.carrier.prod_environment: + self.skipTest("Without TNT carrier created") + res = self.carrier.tnt_oca_rate_shipment(self.sale) + self.assertGreater(res["price"], 0) + self.assertTrue(res["success"]) + + def test_order_tnt_oca_rate_shipment_error(self): + if not self.carrier or self.carrier.prod_environment: + self.skipTest("Without TNT carrier created") + self.product.volume = 0 + with self.assertRaises(UserError): + self.carrier.tnt_oca_rate_shipment(self.sale) + + def test_order_tnt_oca_rate_shipment_currency_extra(self): + if not self.carrier or self.carrier.prod_environment: + self.skipTest("Without TNT carrier created") + usd = self.env.ref("base.USD") + eur = self.env.ref("base.EUR") + currency = self.env.company.currency_id + currency_extra = eur if currency == usd else usd + self.sale.currency_id = currency_extra + res = self.carrier.tnt_oca_rate_shipment(self.sale) + self.assertGreater(res["price"], 0) + self.assertTrue(res["success"]) + + def test_delivery_carrier_tnt_oca_integration(self): + if not self.carrier or self.carrier.prod_environment: + self.skipTest("Without TNT carrier created") + self.picking.action_confirm() + self.picking.action_assign() + # Increase +1 day to prevent error according from today + new_date = self.picking.scheduled_date + timedelta(days=1) + if new_date.weekday() == 5: + new_date += timedelta(days=2) + elif new_date.weekday() == 6: + new_date += timedelta(days=1) + self.picking.scheduled_date = new_date + self.picking.send_to_shipper() + self.assertEquals(self.picking.message_attachment_count, 1) + self.assertTrue(self.picking.carrier_tracking_ref) + self.assertFalse(self.picking.tracking_state_history) + self.assertEqual(self.picking.delivery_state, "shipping_recorded_in_carrier") + self.picking.tracking_state_update() + self.assertEqual(self.picking.tracking_state_history, "CNF") + with self.assertRaises(NotImplementedError): + self.picking.cancel_shipment() + self.assertEqual(self.picking.tracking_state_history, "CNF") + + def test_delivery_carrier_tnt_oca_integration_error(self): + if not self.carrier or self.carrier.prod_environment: + self.skipTest("Without TNT carrier created") + self.picking.action_confirm() + self.picking.action_assign() + new_date = self.picking.scheduled_date + timedelta(days=-1) + self.picking.scheduled_date = new_date + with self.assertRaises(UserError): + self.picking.send_to_shipper() diff --git a/delivery_tnt_oca/views/delivery_carrier_view.xml b/delivery_tnt_oca/views/delivery_carrier_view.xml new file mode 100644 index 0000000000..e57e324944 --- /dev/null +++ b/delivery_tnt_oca/views/delivery_carrier_view.xml @@ -0,0 +1,81 @@ + + + + + delivery.carrier + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 1cd269ed90..b42b243160 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ # generated from manifests external_dependencies +dicttoxml PyPDF2 roulier unidecode +xmltodict zeep diff --git a/setup/delivery_tnt_oca/odoo/addons/delivery_tnt_oca b/setup/delivery_tnt_oca/odoo/addons/delivery_tnt_oca new file mode 120000 index 0000000000..3bf29778f8 --- /dev/null +++ b/setup/delivery_tnt_oca/odoo/addons/delivery_tnt_oca @@ -0,0 +1 @@ +../../../../delivery_tnt_oca \ No newline at end of file diff --git a/setup/delivery_tnt_oca/setup.py b/setup/delivery_tnt_oca/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/delivery_tnt_oca/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)