diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9cd16292 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +# generated from manifests external_dependencies diff --git a/setup/shipment_advice_planner_toursolver/odoo/addons/shipment_advice_planner_toursolver b/setup/shipment_advice_planner_toursolver/odoo/addons/shipment_advice_planner_toursolver new file mode 120000 index 00000000..0aeeff3f --- /dev/null +++ b/setup/shipment_advice_planner_toursolver/odoo/addons/shipment_advice_planner_toursolver @@ -0,0 +1 @@ +../../../../shipment_advice_planner_toursolver \ No newline at end of file diff --git a/setup/shipment_advice_planner_toursolver/setup.py b/setup/shipment_advice_planner_toursolver/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/shipment_advice_planner_toursolver/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/shipment_advice_planner_toursolver_queue_job/odoo/addons/shipment_advice_planner_toursolver_queue_job b/setup/shipment_advice_planner_toursolver_queue_job/odoo/addons/shipment_advice_planner_toursolver_queue_job new file mode 120000 index 00000000..20f1b0aa --- /dev/null +++ b/setup/shipment_advice_planner_toursolver_queue_job/odoo/addons/shipment_advice_planner_toursolver_queue_job @@ -0,0 +1 @@ +../../../../shipment_advice_planner_toursolver_queue_job \ No newline at end of file diff --git a/setup/shipment_advice_planner_toursolver_queue_job/setup.py b/setup/shipment_advice_planner_toursolver_queue_job/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/shipment_advice_planner_toursolver_queue_job/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/shipment_advice_planner_toursolver/README.rst b/shipment_advice_planner_toursolver/README.rst new file mode 100644 index 00000000..26955ae1 --- /dev/null +++ b/shipment_advice_planner_toursolver/README.rst @@ -0,0 +1,119 @@ +================================== +Shipment Advice Planner Toursolver +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:609d6463314ab82e94e0c041422625c9a936c8e2f3290eb6965989b6aa0bbcab + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fstock--logistics--transport-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-transport/tree/16.0/shipment_advice_planner_toursolver + :alt: OCA/stock-logistics-transport +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-transport-16-0/stock-logistics-transport-16-0-shipment_advice_planner_toursolver + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-transport&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extend the shipment advice planner engine to add a new planning +method based on geo-localization. It uses the GEOCONCEPT solution called +TourSolver to plan the shipment advices according to the location of the customers +by calculating the best route according to the available resources. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Before configuring this module, you must configure your account and your +resources on the TourSolver website. Then you should be able to get your api key +and paste it in the default backend. You can access to backends list under the +setting menu of the inventory module. + +The second step is to create the resources. You can add the properties you want +TourSolver to use in its calculation of the best route. + +For advanced planning, you can define your customer delivery windows. It will be +considered in TourSolver calculation. + +For more details about the properties you can use, please consult +[TourSolver documentation](https://mygeoconcept.com/doc/6cc656Uv7gARvG6T/ts-cloud-doc/docs/en/toursolver-cloud-book/) + +Usage +===== + +In the shipment planner you will see a new option added to the planning methods. +If you select TourSolver, a TourSolver task per warehouse will be created for +the pickings you want to plan. + +The task will pass through different states by a dedicated cron and at the end +of the process will create shipment advices according the returned result from +TourSolver. + +Known issues / Roadmap +====================== + +At this stage, the TourSolver task is managed by a cron which will synchronize +its status with TourSolver and get the result when it is ready. +Another approach is available using queue jobs where the sync task will be +rescheduled until it gets the optimization result. + +Ideally, a webhook can be developed to be notified by toursolver when the result +is ready. He will ensure that the result is obtained as soon as possible. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon +* Souheil Bejaoui + +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/stock-logistics-transport `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/shipment_advice_planner_toursolver/__init__.py b/shipment_advice_planner_toursolver/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/shipment_advice_planner_toursolver/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/shipment_advice_planner_toursolver/__manifest__.py b/shipment_advice_planner_toursolver/__manifest__.py new file mode 100644 index 00000000..4160148f --- /dev/null +++ b/shipment_advice_planner_toursolver/__manifest__.py @@ -0,0 +1,40 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shipment Advice Planner Toursolver", + "summary": """Shipment advices planning by geo-optimization (TourSolver)""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-transport", + "depends": ["shipment_advice_planner", "base_time_window"], + "data": [ + # data + "data/toursolver_backend.xml", + "data/ir_ui_menu.xml", + "data/ir_sequence.xml", + "data/ir_cron.xml", + # security + "security/toursolver_backend.xml", + "security/toursolver_resource.xml", + "security/toursolver_task.xml", + "security/toursolver_delivery_window.xml", + "security/toursolver_backend_option_definition.xml", + # views + "views/toursolver_backend.xml", + "views/res_config_settings.xml", + "views/toursolver_resource.xml", + "views/stock_picking.xml", + "views/toursolver_delivery_window.xml", + "views/toursolver_task.xml", + "views/res_partner.xml", + # wizards + "wizards/shipment_advice_planner.xml", + ], + "demo": [ + "demo/toursolver_backend.xml", + "demo/toursolver_resource.xml", + "demo/res_compnay.xml", + ], +} diff --git a/shipment_advice_planner_toursolver/data/ir_cron.xml b/shipment_advice_planner_toursolver/data/ir_cron.xml new file mode 100644 index 00000000..1ff18130 --- /dev/null +++ b/shipment_advice_planner_toursolver/data/ir_cron.xml @@ -0,0 +1,16 @@ + + + + + Synchronize toursolver tasks + + model._cron_sync_task() + 5 + minutes + -1 + + + + + diff --git a/shipment_advice_planner_toursolver/data/ir_sequence.xml b/shipment_advice_planner_toursolver/data/ir_sequence.xml new file mode 100644 index 00000000..195f8d44 --- /dev/null +++ b/shipment_advice_planner_toursolver/data/ir_sequence.xml @@ -0,0 +1,12 @@ + + + + + TourSolver Task + toursolver.task + TST/ + 8 + + + diff --git a/shipment_advice_planner_toursolver/data/ir_ui_menu.xml b/shipment_advice_planner_toursolver/data/ir_ui_menu.xml new file mode 100644 index 00000000..0449bfbe --- /dev/null +++ b/shipment_advice_planner_toursolver/data/ir_ui_menu.xml @@ -0,0 +1,12 @@ + + + + + + TourSolver + + + + + diff --git a/shipment_advice_planner_toursolver/data/toursolver_backend.xml b/shipment_advice_planner_toursolver/data/toursolver_backend.xml new file mode 100644 index 00000000..b3ad55a0 --- /dev/null +++ b/shipment_advice_planner_toursolver/data/toursolver_backend.xml @@ -0,0 +1,10 @@ + + + + + + TourSolver + + + diff --git a/shipment_advice_planner_toursolver/demo/res_compnay.xml b/shipment_advice_planner_toursolver/demo/res_compnay.xml new file mode 100644 index 00000000..34aeb34d --- /dev/null +++ b/shipment_advice_planner_toursolver/demo/res_compnay.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/demo/toursolver_backend.xml b/shipment_advice_planner_toursolver/demo/toursolver_backend.xml new file mode 100644 index 00000000..cb925b1c --- /dev/null +++ b/shipment_advice_planner_toursolver/demo/toursolver_backend.xml @@ -0,0 +1,31 @@ + + + + + + + + + https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/ + fake_api_key + + + + + + diff --git a/shipment_advice_planner_toursolver/demo/toursolver_resource.xml b/shipment_advice_planner_toursolver/demo/toursolver_resource.xml new file mode 100644 index 00000000..5a75bf49 --- /dev/null +++ b/shipment_advice_planner_toursolver/demo/toursolver_resource.xml @@ -0,0 +1,26 @@ + + + + + + R1 + D1 + + + + + + + R2 + D2 + + + + diff --git a/shipment_advice_planner_toursolver/i18n/fr.po b/shipment_advice_planner_toursolver/i18n/fr.po new file mode 100644 index 00000000..0f6e696d --- /dev/null +++ b/shipment_advice_planner_toursolver/i18n/fr.po @@ -0,0 +1,666 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shipment_advice_planner_toursolver +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-02 15:18+0000\n" +"PO-Revision-Date: 2023-05-02 15:18+0000\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: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "minutes" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "seconds" +msgstr "secondes" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__aborted +msgid "Aborted" +msgstr "Abandonné" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__active +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__active +msgid "Active" +msgstr "Actif" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "" +"Add the required options for route calculation by TourSolver.\n" +" You can check the" +msgstr "" +"Ajoutez les otpions nécessaires au calcul de la route par TourSolver.\n" +" Vous pouvez vérifier le" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "" +"Add the required properties for route calculation by TourSolver.\n" +" You can check the" +msgstr "" +"Ajoutez les propriétés nécessaires au calcul de la route par TourSolver.\n" +" Vous pouvez vérifier le" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__api_key +msgid "Api Key" +msgstr "Clef d'API" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "Archived" +msgstr "Archivé" + +#. module: shipment_advice_planner_toursolver +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_backend_menu +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "Backend" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__backend_options_definition +msgid "Backend Options Definition" +msgstr "Options de définition du backend" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Check Status" +msgstr "Vérifiez le statut" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_company +msgid "Companies" +msgstr "Sociétés" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_config_settings +msgid "Config Settings" +msgstr "Configuration" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_partner +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__partner_id +msgid "Contact" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Data" +msgstr "Données" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__date +msgid "Date" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_travel_penalty +msgid "" +"Default value for the The cost for a resource of driving for one distance " +"unit. Can be specified on resource level using `workPenalty`option" +msgstr "" +"Valeur par défaut du coût d'une ressource conducteur pour une unité de distance. " +"Peut être spécifiée au niveau de la ressource à l'aide de l'option 'workPenalty'." + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_work_penalty +msgid "" +"Default value for the cost of a resource working for an hour. Can be " +"specified on resource level using `travelPenalty`option" +msgstr "" +"Valeur par défaut du coût d'une ressource travaillant pendant une heure. Peut " +"être spécifiée au niveau de la ressource à l'aide de l'option 'travelPenalty'." + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__definition_id +msgid "Definition" +msgstr "Définition" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_window_disabled +msgid "Delivery Window Disabled" +msgstr "Fenêtre de livraison désactivée" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__delivery_resource_ids +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__delivery_resource_ids +msgid "Delivery resources" +msgstr "Ressource de livraison" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_window_ids +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_window_ids +msgid "Delivery windows" +msgstr "Fenêtres de livraison" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__dock_id +msgid "Dock" +msgstr "Quai" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__done +msgid "Done" +msgstr "Fait" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__draft +msgid "Draft" +msgstr "Brouillon" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__duration +msgid "Duration in seconds allowed to the computation of the optimization" +msgstr "Durée en secondes permmise pour le calcul d'optimisation" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_duration +msgid "Duration in seconds needed to deliver a customer" +msgstr "Durée en secondes nécessaire pour livrer un client" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__error +msgid "Error" +msgstr "Erreur" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__loading_duration +msgid "Fixed initial loading time" +msgstr "Temps de chargement initial fixe" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_duration +msgid "Fixed time spent delivering a customer" +msgstr "Temps fixe pour la livraison d'un client" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_start +msgid "From" +msgstr "De" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Get Result" +msgstr "Obtenir le résultat" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_delivery_window_search_view +msgid "Group By" +msgstr "Groupé par" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__id +msgid "ID" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_task__task_id +msgid "" +"Identifier of the task submitted to the TourSolver service to optimize the " +"planning/path." +msgstr "" +"Identifiant de la tâche soumise au service TourSolver pour optimiser le " +"planning/chemin." +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__partner_defaul_delivery_window_start +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__partner_default_delivery_window_end +msgid "If no delivery winodow specified on the partner this will be used" +msgstr "Si aucun fenêtre de livraison n'est spécifiée sur le partenaire, ceci sera utilisé" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_window_ids +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_window_ids +msgid "" +"If specified, delivery is only possible into the specified time windows. " +"(Leaves empty if no restriction)" +msgstr "" +"Si indiqué, la livraison ne sera possible que dans les fenêtres temporelles spécifiées. " +"(laissez vide si aucune restriction)" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_resource__use_delivery_person_coordinates_as_end +msgid "" +"If true the computed delivery will end at the delivery person's address. " +"Otherwise it will end at the warehouse address" +msgstr "" +"Si coché, la livraison calculée se terminera à l'adresse du contact de livraison. " +"Sinon, ce sera à l'adresse de l'entrepôt." + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__in_progress +msgid "In progress" +msgstr "En cours" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__loading_duration +msgid "Loading time in minutes" +msgstr "Temps de chargement en minutes" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__name +msgid "Name" +msgstr "Nom" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__duration +msgid "Optimization process max duration" +msgstr "Durée max du processus d'optimisation" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__backend_options +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "Options" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__partner_id +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_delivery_window_search_view +msgid "Partner" +msgstr "Partenaire" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__partner_defaul_delivery_window_start +msgid "Partner Defaul Delivery Window Start" +msgstr "Début de la fenêtre de livraison par défaut du partenaire" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__partner_default_delivery_window_end +msgid "Partner Default Delivery Window End" +msgstr "Fin de la fenêtre de livraison par défaut du partenaire" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_delivery_window_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_delivery_window_menu +msgid "Partner Delivery Windows" +msgstr "Fenêtres de livraison du partnaire" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__picking_ids +msgid "Pickings" +msgstr "Transferts" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Pickings to plan" +msgstr "Transferts à planifier" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_backend.py:0 +#, python-format +msgid "Please configure your timezone in your user preferences" +msgstr "Veuillez configurer votre fuseau horaire dans vos préférences utilisateur" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__resource_properties +msgid "Properties" +msgstr "Propriétés" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__request_data +msgid "Request Data" +msgstr "Demander les données" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__request_data_filename +msgid "Request Data Filename" +msgstr "Demander le nom de fichier des données" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_properties_definition +msgid "Resource Properties" +msgstr "Propriétés de la ressource" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_travel_penalty +msgid "Resource fixed cost travelling/hour" +msgstr "Ressource coût fixe déplacement/heure" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_work_penalty +msgid "Resource fixed cost working/hour" +msgstr "Ressource coût fixe travail/heure" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "Resource properties" +msgstr "Propriétés de la ressource" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__resource_id +msgid "ResourceId" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_resource_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_resource_menu +msgid "Resources" +msgstr "Ressources" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_data +msgid "Result Data" +msgstr "Résultats" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_data_filename +msgid "Result Data Filename" +msgstr "Nom de fichier résultat" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_json +msgid "Result Json" +msgstr "Résultat JSON" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Send Request" +msgstr "Envoyer la requête" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#: model:ir.model,name:shipment_advice_planner_toursolver.model_shipment_advice +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +#, python-format +msgid "Shipment Advice" +msgstr "Conseil d'expédition" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_shipment_advice_planner +msgid "Shipment Advice Planner" +msgstr "Planificateur d'avis d'expédition" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__shipment_advice_ids +msgid "Shipment Advices" +msgstr "Conseils d'expédition" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__shipment_planning_method +msgid "Shipment Planning Method" +msgstr "Méthode de planification d'expédition" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "Shipment advices planning by geo-optimization (TourSolver)" +msgstr "Planification d'avis d'expédition par géo-optimisation (TourSolver)" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__state +msgid "State" +msgstr "État" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_status +msgid "Status of the optimization task provided by the TourSolver service." +msgstr "Statut de la tâche d'optimisation par le service TourSolver." + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__success +msgid "Success" +msgstr "Succès" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.server,name:shipment_advice_planner_toursolver.ir_cron_sync_toursolver_task_ir_actions_server +#: model:ir.cron,cron_name:shipment_advice_planner_toursolver.ir_cron_sync_toursolver_task +msgid "Synchronize toursolver tasks" +msgstr "Synchronisez les tâches Toursolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_task_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_task_menu +msgid "Tasks" +msgstr "Tâches" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "" +"The backend used for TourSolver api call in the\n" +" course of\n" +" shipment advice planning based on delivery\n" +" geo-localization" +msgstr "" +"Le backend utilisé pout les appels à l'api TourSolver dans le\n" +" cadre de la\n" +" planification des avis d'expédition basée\n" +" sur la livraison géo-localisée" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "" +"The following partner ids are not expected into the optimization result: " +"%(names)s" +msgstr "" +"Les partenaires suivants ne sont pas censés apparaître dans le résultat de l'optimisation: " +"%(names)s" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "The following partners are not found into the optimization result: %s" +msgstr "Les partenaires suivants sont absents du résultat de l'optimisation: %s" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_stock_picking__toursolver_shipment_advice_rank +msgid "" +"The rank given by TourSolver to this picking in the set of planned stops of " +"the related shipment advice" +msgstr "" +"Le rang attribué à ce transfert par TourSolver dans l'ensemble des arrêts prévus du " +"avis d'expédition lié" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "There is no active backend for TourSolver." +msgstr "Il n'y a pas de backend actif pour TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_weekday_ids +msgid "Time Window Weekday" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_end +msgid "To" +msgstr "À" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_backend_act_window +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__shipment_advice_planner__shipment_planning_method__toursolver +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_setting_menu +msgid "TourSolver" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_backend +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_company__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_config_settings__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_backend_id +msgid "TourSolver Backend" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_delivery_window +msgid "TourSolver Delivery Window" +msgstr "Fenêtre de livraison TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_resource_id +msgid "" +"TourSolver resource to be propgated to the shipment advice in simpleplanning" +" method" +msgstr "Méthode" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_status +msgid "TourSolver status" +msgstr "Statu de TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_backend_option_definition +msgid "Toursolver Backend Option Definition" +msgstr "Définition des options du backend de TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_duration +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_duration +msgid "Toursolver Delivery Duration" +msgstr "Durée de la livraison TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_error_message +msgid "Toursolver Error Message" +msgstr "Messages d'erreur TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_resource +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice__toursolver_resource_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_resource_id +msgid "Toursolver Resource" +msgstr "Ressource TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_stock_picking__toursolver_shipment_advice_rank +msgid "Toursolver Shipment Advice Rank" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_task +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice__toursolver_task_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_task_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_stock_picking__toursolver_task_id +msgid "Toursolver Task" +msgstr "Tâche TourSolver" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__task_id +msgid "Toursolver task id" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__url +msgid "Url" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__use_delivery_person_coordinates_as_end +msgid "Use Delivery Person Coordinates As End" +msgstr "Utiliser les coordonnées du contact de livraison comme point d'arrivée" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__warehouse_id +msgid "Warehouse" +msgstr "Entrepôt" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_shipment_advice_planner__delivery_resource_ids +msgid "delivery resources to be considered in geo-optimazation" +msgstr "Ressources de livraison eintervenant dans la géo-optiisation" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "documentation" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "to learn more.
" +msgstr "pour en savoir plus.
" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "" +"to learn more.
\n" +" Note that it is not necessary to add the id property.\n" +" The mandatory field resourceId will be used in the optimization request." +msgstr "" +"pour en savoir plus.
\n" +" Notez qu'il n'est pas nécessaire d'ajouter la propriété id.\n" +" Le champ obligatoire resourceId sera utilisé pour la requête d'optimisation." diff --git a/shipment_advice_planner_toursolver/i18n/shipment_advice_planner_toursolver.pot b/shipment_advice_planner_toursolver/i18n/shipment_advice_planner_toursolver.pot new file mode 100644 index 00000000..76708096 --- /dev/null +++ b/shipment_advice_planner_toursolver/i18n/shipment_advice_planner_toursolver.pot @@ -0,0 +1,642 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * shipment_advice_planner_toursolver +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-02 15:18+0000\n" +"PO-Revision-Date: 2023-05-02 15:18+0000\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: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "minutes" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "seconds" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__aborted +msgid "Aborted" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__active +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__active +msgid "Active" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "" +"Add the required options for route calculation by TourSolver.\n" +" You can check the" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "" +"Add the required properties for route calculation by TourSolver.\n" +" You can check the" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__api_key +msgid "Api Key" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "Archived" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_backend_menu +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "Backend" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__backend_options_definition +msgid "Backend Options Definition" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Check Status" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_company +msgid "Companies" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_res_partner +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__partner_id +msgid "Contact" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__create_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__create_uid +msgid "Created by" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__create_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__create_date +msgid "Created on" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Data" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__date +msgid "Date" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_travel_penalty +msgid "" +"Default value for the The cost for a resource of driving for one distance " +"unit. Can be specified on resource level using `workPenalty`option" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_work_penalty +msgid "" +"Default value for the cost of a resource working for an hour. Can be " +"specified on resource level using `travelPenalty`option" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__definition_id +msgid "Definition" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_window_disabled +msgid "Delivery Window Disabled" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__delivery_resource_ids +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__delivery_resource_ids +msgid "Delivery resources" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_window_ids +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_window_ids +msgid "Delivery windows" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__display_name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__display_name +msgid "Display Name" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__dock_id +msgid "Dock" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__done +msgid "Done" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__draft +msgid "Draft" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__duration +msgid "Duration in seconds allowed to the computation of the optimization" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_duration +msgid "Duration in seconds needed to deliver a customer" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__error +msgid "Error" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__loading_duration +msgid "Fixed initial loading time" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__delivery_duration +msgid "Fixed time spent delivering a customer" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_start +msgid "From" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Get Result" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_delivery_window_search_view +msgid "Group By" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__id +msgid "ID" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_task__task_id +msgid "" +"Identifier of the task submitted to the TourSolver service to optimize the " +"planning/path." +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__partner_defaul_delivery_window_start +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__partner_default_delivery_window_end +msgid "If no delivery winodow specified on the partner this will be used" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_window_ids +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_window_ids +msgid "" +"If specified, delivery is only possible into the specified time windows. " +"(Leaves empty if no restriction)" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_resource__use_delivery_person_coordinates_as_end +msgid "" +"If true the computed delivery will end at the delivery person's address. " +"Otherwise it will end at the warehouse address" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__in_progress +msgid "In progress" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource____last_update +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task____last_update +msgid "Last Modified on" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__write_uid +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend_option_definition__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__write_date +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__write_date +msgid "Last Updated on" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_backend__loading_duration +msgid "Loading time in minutes" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__name +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__name +msgid "Name" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__duration +msgid "Optimization process max duration" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__backend_options +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "Options" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__partner_id +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_delivery_window_search_view +msgid "Partner" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__partner_defaul_delivery_window_start +msgid "Partner Defaul Delivery Window Start" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__partner_default_delivery_window_end +msgid "Partner Default Delivery Window End" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_delivery_window_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_delivery_window_menu +msgid "Partner Delivery Windows" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__picking_ids +msgid "Pickings" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Pickings to plan" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_backend.py:0 +#, python-format +msgid "Please configure your timezone in your user preferences" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__resource_properties +msgid "Properties" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__request_data +msgid "Request Data" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__request_data_filename +msgid "Request Data Filename" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_properties_definition +msgid "Resource Properties" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_travel_penalty +msgid "Resource fixed cost travelling/hour" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__resource_default_work_penalty +msgid "Resource fixed cost working/hour" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "Resource properties" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__resource_id +msgid "ResourceId" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_resource_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_resource_menu +msgid "Resources" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_data +msgid "Result Data" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_data_filename +msgid "Result Data Filename" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__result_json +msgid "Result Json" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +msgid "Send Request" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#: model:ir.model,name:shipment_advice_planner_toursolver.model_shipment_advice +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_task_form_view +#, python-format +msgid "Shipment Advice" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_shipment_advice_planner +msgid "Shipment Advice Planner" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__shipment_advice_ids +msgid "Shipment Advices" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__shipment_planning_method +msgid "Shipment Planning Method" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "Shipment advices planning by geo-optimization (TourSolver)" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__state +msgid "State" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_status +msgid "Status of the optimization task provided by the TourSolver service." +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__toursolver_task__state__success +msgid "Success" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.server,name:shipment_advice_planner_toursolver.ir_cron_sync_toursolver_task_ir_actions_server +#: model:ir.cron,cron_name:shipment_advice_planner_toursolver.ir_cron_sync_toursolver_task +msgid "Synchronize toursolver tasks" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_task_act_window +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_task_menu +msgid "Tasks" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.res_config_settings_form_view +msgid "" +"The backend used for TourSolver api call in the\n" +" course of\n" +" shipment advice planning based on delivery\n" +" geo-localization" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "" +"The following partner ids are not expected into the optimization result: " +"%(names)s" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "The following partners are not found into the optimization result: %s" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_stock_picking__toursolver_shipment_advice_rank +msgid "" +"The rank given by TourSolver to this picking in the set of planned stops of " +"the related shipment advice" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#. odoo-python +#: code:addons/shipment_advice_planner_toursolver/models/toursolver_task.py:0 +#, python-format +msgid "There is no active backend for TourSolver." +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_weekday_ids +msgid "Time Window Weekday" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_delivery_window__time_window_end +msgid "To" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.actions.act_window,name:shipment_advice_planner_toursolver.toursolver_backend_act_window +#: model:ir.model.fields.selection,name:shipment_advice_planner_toursolver.selection__shipment_advice_planner__shipment_planning_method__toursolver +#: model:ir.ui.menu,name:shipment_advice_planner_toursolver.toursolver_setting_menu +msgid "TourSolver" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_backend +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_company__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_config_settings__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__toursolver_backend_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_backend_id +msgid "TourSolver Backend" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_delivery_window +msgid "TourSolver Delivery Window" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_resource_id +msgid "" +"TourSolver resource to be propgated to the shipment advice in simpleplanning" +" method" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_status +msgid "TourSolver status" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_backend_option_definition +msgid "Toursolver Backend Option Definition" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_partner__toursolver_delivery_duration +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_res_users__toursolver_delivery_duration +msgid "Toursolver Delivery Duration" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__toursolver_error_message +msgid "Toursolver Error Message" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_resource +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice__toursolver_resource_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_resource_id +msgid "Toursolver Resource" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_stock_picking__toursolver_shipment_advice_rank +msgid "Toursolver Shipment Advice Rank" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_toursolver_task +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice__toursolver_task_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_shipment_advice_planner__toursolver_task_id +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_stock_picking__toursolver_task_id +msgid "Toursolver Task" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__task_id +msgid "Toursolver task id" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model,name:shipment_advice_planner_toursolver.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_backend__url +msgid "Url" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_resource__use_delivery_person_coordinates_as_end +msgid "Use Delivery Person Coordinates As End" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,field_description:shipment_advice_planner_toursolver.field_toursolver_task__warehouse_id +msgid "Warehouse" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model:ir.model.fields,help:shipment_advice_planner_toursolver.field_shipment_advice_planner__delivery_resource_ids +msgid "delivery resources to be considered in geo-optimazation" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "documentation" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_backend_form_view +msgid "to learn more.
" +msgstr "" + +#. module: shipment_advice_planner_toursolver +#: model_terms:ir.ui.view,arch_db:shipment_advice_planner_toursolver.toursolver_resource_form_view +msgid "" +"to learn more.
\n" +" Note that it is not necessary to add the id property.\n" +" The mandatory field resourceId will be used in the optimization request." +msgstr "" diff --git a/shipment_advice_planner_toursolver/models/__init__.py b/shipment_advice_planner_toursolver/models/__init__.py new file mode 100644 index 00000000..1f9c6c01 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/__init__.py @@ -0,0 +1,10 @@ +from . import toursolver_backend_option_definition +from . import toursolver_backend +from . import res_company +from . import res_config_settings +from . import toursolver_resource +from . import toursolver_task +from . import stock_picking +from . import toursolver_delivery_window +from . import res_partner +from . import shipment_advice diff --git a/shipment_advice_planner_toursolver/models/res_company.py b/shipment_advice_planner_toursolver/models/res_company.py new file mode 100644 index 00000000..df9e1d9d --- /dev/null +++ b/shipment_advice_planner_toursolver/models/res_company.py @@ -0,0 +1,13 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + + _inherit = "res.company" + + toursolver_backend_id = fields.Many2one( + comodel_name="toursolver.backend", string="TourSolver Backend" + ) diff --git a/shipment_advice_planner_toursolver/models/res_config_settings.py b/shipment_advice_planner_toursolver/models/res_config_settings.py new file mode 100644 index 00000000..a17f3e9a --- /dev/null +++ b/shipment_advice_planner_toursolver/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + + _inherit = "res.config.settings" + + toursolver_backend_id = fields.Many2one( + related="company_id.toursolver_backend_id", readonly=False + ) diff --git a/shipment_advice_planner_toursolver/models/res_partner.py b/shipment_advice_planner_toursolver/models/res_partner.py new file mode 100644 index 00000000..215be39a --- /dev/null +++ b/shipment_advice_planner_toursolver/models/res_partner.py @@ -0,0 +1,35 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ResPartner(models.Model): + + _inherit = "res.partner" + + toursolver_delivery_window_ids = fields.One2many( + comodel_name="toursolver.delivery.window", + inverse_name="partner_id", + string="Delivery windows", + help="If specified, delivery is only possible into the specified " + "time windows. (Leaves empty if no restriction)", + ) + toursolver_delivery_duration = fields.Integer() + + def _get_delivery_windows(self, day_name): + """ + Return the list of delivery windows by partner id for the given day. + + :param day: The day name (see time.weekday) + :return: dict partner_id:[delivery_window, ] + """ + self.ensure_one() + week_day_id = self.env["time.weekday"]._get_id_by_name(day_name) + return self.env["toursolver.delivery.window"].search( + [ + ("partner_id", "=", self.id), + ("time_window_weekday_ids", "in", week_day_id), + ] + ) diff --git a/shipment_advice_planner_toursolver/models/shipment_advice.py b/shipment_advice_planner_toursolver/models/shipment_advice.py new file mode 100644 index 00000000..e6d8d9d5 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/shipment_advice.py @@ -0,0 +1,16 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ShipmentAdvice(models.Model): + + _inherit = "shipment.advice" + + toursolver_resource_id = fields.Many2one( + comodel_name="toursolver.resource", string="Toursolver Resource", readonly=True + ) + toursolver_task_id = fields.Many2one( + comodel_name="toursolver.task", string="Toursolver Task", readonly=True + ) diff --git a/shipment_advice_planner_toursolver/models/stock_picking.py b/shipment_advice_planner_toursolver/models/stock_picking.py new file mode 100644 index 00000000..2f8969e6 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/stock_picking.py @@ -0,0 +1,57 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.osv.expression import AND, OR + + +class StockPicking(models.Model): + _inherit = "stock.picking" + _order = ( + "toursolver_shipment_advice_rank asc, priority desc, scheduled_date asc," + " id desc" + ) + + toursolver_task_id = fields.Many2one( + comodel_name="toursolver.task", readonly=True, copy=False + ) + toursolver_shipment_advice_rank = fields.Integer( + readonly=True, + string="Toursolver Rank", + help="The rank given by TourSolver to this picking in the set of planned stops" + " of the related shipment advice", + ) + + @api.model + def _get_compute_picking_to_plan_ids_depends(self): + return super()._get_compute_picking_to_plan_ids_depends() + [ + "toursolver_task_id", + "toursolver_task_id.state", + ] + + def _compute_can_be_planned_in_shipment_advice(self): + res = super()._compute_can_be_planned_in_shipment_advice() + for rec in self: + rec.can_be_planned_in_shipment_advice = ( + rec.can_be_planned_in_shipment_advice + and ( + not rec.toursolver_task_id + or rec.toursolver_task_id.state not in ("draft", "in_progress") + ) + ) + return res + + def _search_can_be_planned_in_shipment_advice(self, operator, value): + domain = super()._search_can_be_planned_in_shipment_advice(operator, value) + if (operator == "=" and value) or (operator == "!=" and not value): + extra_domain = [ + "|", + ("toursolver_task_id", "=", False), + ("toursolver_task_id.state", "not in", ("draft", "in_progress")), + ] + return AND([domain, extra_domain]) + extra_domain = [ + ("toursolver_task_id", "!=", False), + ("toursolver_task_id.state", "in", ("draft", "in_progress")), + ] + return OR([domain, extra_domain]) diff --git a/shipment_advice_planner_toursolver/models/tools.py b/shipment_advice_planner_toursolver/models/tools.py new file mode 100644 index 00000000..7d979aa8 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/tools.py @@ -0,0 +1,8 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +def seconds_to_duration(sec): + m, s = divmod(sec, 60) + h, m = divmod(m, 60) + return f"{h:02d}:{m:02d}:{s:02d}" diff --git a/shipment_advice_planner_toursolver/models/toursolver_backend.py b/shipment_advice_planner_toursolver/models/toursolver_backend.py new file mode 100644 index 00000000..114f763d --- /dev/null +++ b/shipment_advice_planner_toursolver/models/toursolver_backend.py @@ -0,0 +1,108 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from .tools import seconds_to_duration + + +class TourSolverBackend(models.Model): + _name = "toursolver.backend" + _description = "TourSolver Backend" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + resource_properties_definition = fields.PropertiesDefinition( + string="Resource Properties" + ) + url = fields.Char() + api_key = fields.Char() + delivery_window_disabled = fields.Boolean() + partner_defaul_delivery_window_start = fields.Float( + default=8.0, + help="If no delivery winodow specified on the partner this will be used", + ) + partner_default_delivery_window_end = fields.Float( + default=17.0, + help="If no delivery winodow specified on the partner this will be used", + ) + delivery_duration = fields.Integer( + string="Fixed time spent delivering a customer", + help="Duration in seconds needed to deliver a customer", + default=180, + ) + duration = fields.Integer( + string="Optimization process max duration", + help="Duration in seconds allowed to the computation of the optimization", + ) + loading_duration = fields.Integer( + string="Fixed initial loading time", help="Loading time in minutes" + ) + resource_default_work_penalty = fields.Float( + string="Resource fixed cost working/hour", + help="Default value for the cost of a resource working for an hour. " + "Can be specified on resource level using `travelPenalty`option", + ) + resource_default_travel_penalty = fields.Float( + string="Resource fixed cost travelling/hour", + help="Default value for the The cost for a resource of driving for one distance" + " unit. Can be specified on resource level using `workPenalty`option", + ) + definition_id = fields.Many2one( + comodel_name="toursolver.backend.option.definition", readonly=True + ) + backend_options = fields.Properties( + string="Options", + copy=True, + definition="definition_id.backend_options_definition", + ) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for rec in records: + rec.definition_id = rec.definition_id.create({}) + return records + + def _get_partner_delivery_duration(self, partner): + self.ensure_one() + return ( + partner.toursolver_delivery_duration + if partner.toursolver_delivery_duration + else self.delivery_duration + ) + + def _get_work_start_time(self): + """ + Return the start time of the delivery to geo-optimize. + + The start time is now + the geo optimization duration + """ + self.ensure_one() + tz_name = self.env.context.get("tz") or self.env.user.tz + if not tz_name: + raise UserError( + _("Please configure your timezone in your user preferences") + ) + m, s = divmod(self.duration, 60) + now = fields.Datetime.context_timestamp(self, datetime.now()) + return now + timedelta(minutes=m, seconds=s) + + def _get_work_start_time_formatted(self): + return self._get_work_start_time().strftime("%H:%M:00") + + def _get_loading_duration_formatted(self): + h, m = divmod(self.loading_duration, 60) + return f"{h:02d}:{m:02d}:00" + + def _get_backend_default_options(self): + return {"maxOptimDuration": seconds_to_duration(self.duration)} + + def _get_backend_options(self): + self.ensure_one() + result = self._get_backend_default_options() + result.update({p.get("string"): p.get("value") for p in self.backend_options}) + return result diff --git a/shipment_advice_planner_toursolver/models/toursolver_backend_option_definition.py b/shipment_advice_planner_toursolver/models/toursolver_backend_option_definition.py new file mode 100644 index 00000000..71e6f3b4 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/toursolver_backend_option_definition.py @@ -0,0 +1,12 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ToursolverBackendOptionDefinition(models.Model): + + _name = "toursolver.backend.option.definition" + _description = "Toursolver Backend Option Definition" + + backend_options_definition = fields.PropertiesDefinition() diff --git a/shipment_advice_planner_toursolver/models/toursolver_delivery_window.py b/shipment_advice_planner_toursolver/models/toursolver_delivery_window.py new file mode 100644 index 00000000..78aab199 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/toursolver_delivery_window.py @@ -0,0 +1,18 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ToursolverDeliveryWindow(models.Model): + + _name = "toursolver.delivery.window" + _inherit = "time.window.mixin" + _description = "TourSolver Delivery Window" + _order = "partner_id, time_window_start" + _time_window_overlap_check_field = "partner_id" + + partner_id = fields.Many2one( + comodel_name="res.partner", required=True, index=True, ondelete="cascade" + ) diff --git a/shipment_advice_planner_toursolver/models/toursolver_resource.py b/shipment_advice_planner_toursolver/models/toursolver_resource.py new file mode 100644 index 00000000..8ee7a11d --- /dev/null +++ b/shipment_advice_planner_toursolver/models/toursolver_resource.py @@ -0,0 +1,49 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ToursolverResource(models.Model): + + _name = "toursolver.resource" + _description = "Toursolver Resource" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + toursolver_backend_id = fields.Many2one( + comodel_name="toursolver.backend", + string="TourSolver Backend", + required=True, + ) + resource_id = fields.Char(string="ResourceId", required=True) + resource_properties = fields.Properties( + "Properties", + definition="toursolver_backend_id.resource_properties_definition", + copy=True, + ) + use_delivery_person_coordinates_as_end = fields.Boolean( + help="If true the computed delivery will end at the delivery person's " + "address. Otherwise it will end at the warehouse address" + ) + partner_id = fields.Many2one(comodel_name="res.partner", string="Contact") + + def _get_resource_default_properties(self): + self.ensure_one() + work_start_time = self.toursolver_backend_id._get_work_start_time_formatted() + loading_duration = self.toursolver_backend_id._get_loading_duration_formatted() + return { + "travelPenalty": self.toursolver_backend_id.resource_default_travel_penalty, + "workPenalty": self.toursolver_backend_id.resource_default_work_penalty, + "workStartTime": work_start_time, + "fixedLoadingDuration": loading_duration, + } + + def _get_resource_properties(self): + self.ensure_one() + result = self._get_resource_default_properties() + result.update( + {p.get("string"): p.get("value") for p in self.resource_properties} + ) + result["id"] = self.resource_id + return result diff --git a/shipment_advice_planner_toursolver/models/toursolver_task.py b/shipment_advice_planner_toursolver/models/toursolver_task.py new file mode 100644 index 00000000..14618254 --- /dev/null +++ b/shipment_advice_planner_toursolver/models/toursolver_task.py @@ -0,0 +1,524 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json +import logging +from collections import defaultdict +from urllib.parse import urlencode, urlparse, urlunparse + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from .tools import seconds_to_duration + +_logger = logging.getLogger("TourSolver Connexion") + + +class ToursolverTask(models.Model): + + _name = "toursolver.task" + _description = "Toursolver Task" + + name = fields.Char(readonly=True) + picking_ids = fields.One2many( + comodel_name="stock.picking", + string="Pickings", + readonly=True, + inverse_name="toursolver_task_id", + ) + date = fields.Date(readonly=True, default=fields.Date.context_today) + task_id = fields.Char( + "Toursolver task id", + help="Identifier of the task submitted to the TourSolver service to " + "optimize the planning/path.", + readonly=True, + ) + toursolver_status = fields.Char( + "TourSolver status", + help="Status of the optimization task provided by the TourSolver service.", + readonly=True, + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("in_progress", "In progress"), + ("aborted", "Aborted"), + ("error", "Error"), + ("success", "Success"), + ("done", "Done"), + ("cancelled", "Cancelled"), + ], + readonly=True, + store=True, + compute="_compute_state", + ) + toursolver_error_message = fields.Text(readonly=True) + warehouse_id = fields.Many2one(comodel_name="stock.warehouse", readonly=True) + dock_id = fields.Many2one(comodel_name="stock.dock", readonly=True) + toursolver_backend_id = fields.Many2one( + comodel_name="toursolver.backend", + string="TourSolver Backend", + default="_get_toursolver_backend", + readonly=True, + ) + delivery_resource_ids = fields.Many2many( + comodel_name="toursolver.resource", string="Delivery resources", readonly=True + ) + result_data = fields.Binary(attachment=True, readonly=True) + result_data_filename = fields.Char(readonly=True) + result_json = fields.Json(compute="_compute_result_json") + request_data = fields.Binary(readonly=True) + request_data_filename = fields.Char(readonly=True) + shipment_advice_ids = fields.One2many( + comodel_name="shipment.advice", + inverse_name="toursolver_task_id", + string="Shipment Advices", + readonly=True, + ) + + @api.depends("request_data") + def _compute_result_json(self): + for record in self: + val = {} + if record.request_data: + val = json.loads(base64.b64decode(record.result_data)) + record.result_json = val + + @api.model_create_multi + def create(self, vals_list): + sequence_model = self.env["ir.sequence"] + for vals in vals_list: + name = sequence_model.next_by_code("toursolver.task") + if vals.get("name"): + name = f"{name} {vals.get('name')}" + vals["name"] = name + return super().create(vals_list) + + @api.depends("toursolver_status") + def _compute_state(self): + for record in self: + status = record.toursolver_status + status = status and status.lower() + if not status: + state = "draft" + elif status in ("error", "failed"): + state = "error" + elif status == "aborted": + state = "cancelled" + elif status == "terminated": + state = "success" + elif status == "done": + state = "done" + else: + state = "in_progress" + record.state = state + + @api.model + def _get_default_toursolver_backend(self): + user_company = self.env.company + if ( + not user_company.toursolver_backend_id + or not user_company.toursolver_backend_id.active + ): + raise ValidationError(_("There is no active backend for TourSolver.")) + return user_company.toursolver_backend_id + + def _toursolver_query_url(self, action, **url_params): + backend = self.toursolver_backend_id + if not backend.url or not backend.api_key: + raise ValidationError(_("The tousolver backend is not configured.")) + baseurl = backend.url + url_params = url_params or {} + url_params["tsCloudApiKey"] = backend.api_key + url_parts = list(urlparse(baseurl)) + url_parts[2] = url_parts[2] + action + url_parts[4] = urlencode(url_params) + return urlunparse(url_parts) + + def _toursolver_post(self, action, json_request): + self.ensure_one() + url = self._toursolver_query_url(action) + response = requests.post( + url, + json=json_request, + headers={"Accept": "application/json"}, + timeout=(3, 5), + ) + return self._toursolver_check_response(response) + + def _toursolver_get(self, action, **kwargs): + self.ensure_one() + url = self._toursolver_query_url(action, **kwargs) + response = requests.get( + url, + headers={"Accept": "application/json"}, + timeout=(3, 5), + ) + return self._toursolver_check_response(response) + + def _toursolver_check_response(self, response): + """ + Check if the response is OK and process error according. + + Return json content if OK otherwise False + """ + try: + self.toursolver_error_message = False + response.raise_for_status() + except requests.HTTPError as http_error: + msg = "\n".join( + filter(None, [http_error.args[0], response.content.decode()]) + ) + self._toursolver_notify_error(msg) + return {} + result = response.json() + if result["status"] == "ERROR": + self._toursolver_notify_error(result["message"]) + return {} + return result + + def _toursolver_notify_error(self, error_msg): + self.toursolver_error_message = error_msg + self.toursolver_status = "error" + _logger.error(error_msg) + + def button_send_request(self): + for rec in self: + rec._toursolver_send_request() + + def _toursolver_send_request(self): + self.ensure_one() + if self.task_id: + return True + json_request = self._toursolver_post_json_request() + self.request_data = base64.b64encode(json.dumps(json_request).encode()) + self.request_data_filename = f"{self.name} request data.json" + response = self._toursolver_post(action="optimize", json_request=json_request) + if response: + self.toursolver_status = response.get("status") + self.task_id = response.get("taskId") + return True + + def _toursolver_post_json_request(self): + self.ensure_one() + ret = self._toursolver_json_request_metas() + ret["depots"] = self._toursolver_json_request_depots() + ret["orders"] = self._toursolver_json_request_orders() + ret["resources"] = self._toursolver_json_request_resources() + ret["options"] = self._toursolver_json_request_options() + ret["language"] = self.env.user.lang + ret["simulationName"] = self.name + return ret + + def _toursolver_json_request_metas(self): + return { + "simulationName": self.name, + "countryCode": self.env.company.country_id.code, + "beginDate": self._toursolver_format_date(self.date), + "language": self.env.user.lang, + } + + @api.model + def _toursolver_format_date(self, date): + return date.strftime("%Y-%m-%d") + + def _toursolver_json_request_depots(self): + address = self.warehouse_id.partner_id + return [ + { + "x": address.partner_longitude, + "y": address.partner_latitude, + "id": f"dep_{address.id}", + } + ] + + def _toursolver_json_request_orders(self): + return [ + self._toursolver_json_request_order(partner) + for partner in self._toursolver_partners_to_deliver() + ] + + def _toursolver_partners_to_deliver(self): + return self.picking_ids.mapped("partner_id") + + def _toursolver_json_request_order(self, partner): + backend = self.toursolver_backend_id + order = self._toursolver_json_request_order_common(partner) + custom_data_map = self._toursolver_json_request_order_custom_data_map(partner) + if custom_data_map: + order["customDataMap"] = custom_data_map + if not backend.delivery_window_disabled: + order["timeWindows"] = self._toursolver_json_request_order_time_window( + partner + ) + return order + + def _toursolver_json_request_order_common(self, partner): + self.ensure_one() + backend = self.toursolver_backend_id + phones = filter(None, (partner.mobile or None, partner.phone or None)) + delivery_duration = backend._get_partner_delivery_duration(partner) + return { + "customerId": partner.ref, + "fixedVisitDuration": seconds_to_duration(delivery_duration), + "id": partner.id, + "label": partner.display_name, + "phone": "| ".join(phones), + "type": 0, # delivery, + "x": partner.partner_longitude, + "y": partner.partner_latitude, + } + + @api.model + def _toursolver_json_request_order_custom_data_map(self, partner): + custom_data_map = {} + if partner.comment: + custom_data_map["notes"] = partner.comment + if not all( + char == "" or char.isspace() for char in partner.contact_address.split("\n") + ): + custom_data_map["address"] = partner.contact_address + return custom_data_map + + @api.model + def _toursolver_json_request_order_time_window(self, partner): + delivery_windows = partner._get_delivery_windows( + str(fields.Date.from_string(self.date).weekday()) + ) + time_windows = [] + if delivery_windows: + for window in delivery_windows: + time_windows.append( + { + "beginTime": window.float_to_time_repr( + window.time_window_start + ), + "endTime": window.float_to_time_repr(window.time_window_end), + } + ) + else: + time_windows.append(self._toursolver_default_delivery_window()) + return time_windows + + def _toursolver_default_delivery_window(self): + self.ensure_one() + delivery_window_model = self.env["toursolver.delivery.window"] + backend = self.toursolver_backend_id + return { + "beginTime": delivery_window_model.float_to_time_repr( + backend.partner_defaul_delivery_window_start + ), + "endTime": delivery_window_model.float_to_time_repr( + backend.partner_default_delivery_window_end + ), + } + + def _toursolver_json_request_resources(self): + return [ + self._toursolver_json_request_resource(resource) + for resource in self.delivery_resource_ids + ] + + def _toursolver_json_request_resource(self, resource): + res = resource._get_resource_properties() + res.update(self._toursolver_json_request_resource_start_end_position(resource)) + return res + + def _toursolver_json_request_resource_start_end_position(self, resource): + address = self.warehouse_id.partner_id + res = { + "startX": address.partner_longitude, + "startY": address.partner_latitude, + "endX": address.partner_longitude, + "endY": address.partner_latitude, + } + if resource.use_delivery_person_coordinates_as_end and resource.partner_id: + res.update( + { + "endX": resource.partner_id.partner_longitude, + "endY": resource.partner_id.partner_latitude, + } + ) + return res + + def _toursolver_json_request_options(self): + self.ensure_one() + res = self.toursolver_backend_id._get_backend_options() + res.update( + { + "maxOptimDuration": seconds_to_duration( + self.toursolver_backend_id.duration + ) + } + ) + return res + + def button_check_status(self): + for rec in self: + rec._toursolver_check_status() + + def _toursolver_check_status(self): + self.ensure_one() + result = self._toursolver_get(action="status", taskId=self.task_id) + if not result: + return + self.toursolver_status = result["optimizeStatus"] + if self.state == "error" and result.get("message"): + self.toursolver_error_message = result["message"] + + def button_get_result(self): + for rec in self: + rec._toursolver_get_result() + + def _toursolver_get_result(self): + self.ensure_one() + result = self._toursolver_get(action="result", taskId=self.task_id) + if not result: + return + self.result_data = base64.b64encode(json.dumps(result).encode()) + self.result_data_filename = f"{self.name} result data.json" + self._toursolver_validate_result() + if not self.shipment_advice_ids and self.state in ("success", "done"): + self._toursolver_create_shipment_advices() + self._toursolver_sort_planned_picking() + if self.state == "success": + self.toursolver_status = "done" + + def _toursolver_validate_result(self): + self.ensure_one() + expected_partners = set(self._toursolver_partners_to_deliver().ids) + received_partners = self._toursolver_planned_partner_ids() + missing_partners = self.env["res.partner"].browse( + list(expected_partners - received_partners) + ) + unexpected_partner_ids = list(received_partners - expected_partners) + error_messages = [] + if missing_partners: + error_messages.append( + _( + "The following partners are not found into the " + "optimization result: %s" + ) + % ", ".join(missing_partners.mapped("name")) + ) + if unexpected_partner_ids: + error_messages.append( + _( + "The following partner ids are not expected into the " + "optimization result: %(names)s" + ) + % dict(names=", ".join([str(i) for i in unexpected_partner_ids])) + ) + if error_messages: + self.write( + { + "toursolver_status": "failed", + "toursolver_error_message": "\n".join(error_messages), + } + ) + + def _toursolver_planned_partner_ids_by_resource_id(self): + result = defaultdict(list) + for order in self.result_json["plannedOrders"]: + if ( + order.get("resourceId") + and order.get("stopId") + and order.get("stopId").isdigit() + and order.get("stopType", 0) == 0 + ): + result[order.get("resourceId")].append(int(order.get("stopId"))) + return result + + def _toursolver_planned_partner_ids(self): + planned_partner_by_resource = ( + self._toursolver_planned_partner_ids_by_resource_id() + ) + res = [] + for partner_ids in planned_partner_by_resource.values(): + res.extend(partner_ids) + return set(res) + + def _toursolver_pickings_to_plan_by_resource(self): + for ( + resource_id, + partner_ids, + ) in self._toursolver_planned_partner_ids_by_resource_id().items(): + resource = self.delivery_resource_ids.filtered( + lambda r, r_id=resource_id: r.resource_id == r_id + ) + pickings_to_plan = self.picking_ids.filtered( + lambda p, p_ids=partner_ids: p.partner_id.id in p_ids + ) + yield resource, pickings_to_plan + + def _toursolver_new_shipment_advice_planer(self, resource, pickings_to_plan): + planner = self.env["shipment.advice.planner"].new({}) + planner.warehouse_id = self.warehouse_id + planner.shipment_planning_method = "simple" + planner.picking_to_plan_ids = pickings_to_plan + planner.toursolver_resource_id = resource + planner.toursolver_task_id = self + planner.dock_id = self.dock_id + return planner + + def _toursolver_create_shipment_advices(self): + self.ensure_one() + for ( + resource, + pickings_to_plan, + ) in self._toursolver_pickings_to_plan_by_resource(): + planner = self._toursolver_new_shipment_advice_planer( + resource, pickings_to_plan + ) + planner._plan_shipments_for_method() + + def button_show_shipment_advice(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Shipment Advice"), + "view_mode": "calendar,tree,form", + "res_model": self.shipment_advice_ids._name, + "domain": [("toursolver_task_id", "=", self.id)], + "context": self.env.context, + } + + @api.model + def _cron_sync_task(self): + for task in self.search([("state", "=", "in_progress")]): + task.button_check_status() + for task in self.search([("state", "=", "success")]): + task.button_get_result() + for task in self.search([("state", "=", "draft")]): + task.button_send_request() + + def _toursolver_sort_planned_picking(self): + self.ensure_one() + for shipment in self.shipment_advice_ids: + if not shipment.toursolver_resource_id: + continue + rank = 1 + for partner_id in self._toursolver_planned_partner_ids_sorted( + shipment.toursolver_resource_id.resource_id + ): + picks = shipment.planned_picking_ids.filtered( + lambda pick, p_id=partner_id: pick.partner_id.id == p_id + ) + picks.write({"toursolver_shipment_advice_rank": rank}) + rank += 1 + + def _toursolver_planned_partner_ids_sorted(self, resource_id): + for order in self.result_json["plannedOrders"]: + if ( + order.get("resourceId") == resource_id + and order.get("stopId") + and order.get("stopId").isdigit() + and order.get("stopType", 0) == 0 + ): + yield int(order.get("stopId")) + + def button_cancel(self): + self.write({"toursolver_status": "aborted"}) diff --git a/shipment_advice_planner_toursolver/readme/CONFIGURE.rst b/shipment_advice_planner_toursolver/readme/CONFIGURE.rst new file mode 100644 index 00000000..d351e4e6 --- /dev/null +++ b/shipment_advice_planner_toursolver/readme/CONFIGURE.rst @@ -0,0 +1,13 @@ +Before configuring this module, you must configure your account and your +resources on the TourSolver website. Then you should be able to get your api key +and paste it in the default backend. You can access to backends list under the +setting menu of the inventory module. + +The second step is to create the resources. You can add the properties you want +TourSolver to use in its calculation of the best route. + +For advanced planning, you can define your customer delivery windows. It will be +considered in TourSolver calculation. + +For more details about the properties you can use, please consult +[TourSolver documentation](https://mygeoconcept.com/doc/6cc656Uv7gARvG6T/ts-cloud-doc/docs/en/toursolver-cloud-book/) diff --git a/shipment_advice_planner_toursolver/readme/CONTRIBUTORS.rst b/shipment_advice_planner_toursolver/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..7bbe49c9 --- /dev/null +++ b/shipment_advice_planner_toursolver/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Laurent Mignon +* Souheil Bejaoui diff --git a/shipment_advice_planner_toursolver/readme/DESCRIPTION.rst b/shipment_advice_planner_toursolver/readme/DESCRIPTION.rst new file mode 100644 index 00000000..a5c9983b --- /dev/null +++ b/shipment_advice_planner_toursolver/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module extend the shipment advice planner engine to add a new planning +method based on geo-localization. It uses the GEOCONCEPT solution called +TourSolver to plan the shipment advices according to the location of the customers +by calculating the best route according to the available resources. diff --git a/shipment_advice_planner_toursolver/readme/ROADMAP.rst b/shipment_advice_planner_toursolver/readme/ROADMAP.rst new file mode 100644 index 00000000..1fa4a6fd --- /dev/null +++ b/shipment_advice_planner_toursolver/readme/ROADMAP.rst @@ -0,0 +1,7 @@ +At this stage, the TourSolver task is managed by a cron which will synchronize +its status with TourSolver and get the result when it is ready. +Another approach is available using queue jobs where the sync task will be +rescheduled until it gets the optimization result. + +Ideally, a webhook can be developed to be notified by toursolver when the result +is ready. He will ensure that the result is obtained as soon as possible. diff --git a/shipment_advice_planner_toursolver/readme/USAGE.rst b/shipment_advice_planner_toursolver/readme/USAGE.rst new file mode 100644 index 00000000..f3c3a564 --- /dev/null +++ b/shipment_advice_planner_toursolver/readme/USAGE.rst @@ -0,0 +1,7 @@ +In the shipment planner you will see a new option added to the planning methods. +If you select TourSolver, a TourSolver task per warehouse will be created for +the pickings you want to plan. + +The task will pass through different states by a dedicated cron and at the end +of the process will create shipment advices according the returned result from +TourSolver. diff --git a/shipment_advice_planner_toursolver/security/toursolver_backend.xml b/shipment_advice_planner_toursolver/security/toursolver_backend.xml new file mode 100644 index 00000000..bf820024 --- /dev/null +++ b/shipment_advice_planner_toursolver/security/toursolver_backend.xml @@ -0,0 +1,26 @@ + + + + + + tourSolver.backend access stock manager + + + + + + + + + + tourSolver.backend access stock user + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/security/toursolver_backend_option_definition.xml b/shipment_advice_planner_toursolver/security/toursolver_backend_option_definition.xml new file mode 100644 index 00000000..730ab214 --- /dev/null +++ b/shipment_advice_planner_toursolver/security/toursolver_backend_option_definition.xml @@ -0,0 +1,36 @@ + + + + + + toursolver.backend.option.definition access stock manager + + + + + + + + + + toursolver.backend.option.definition access stock user + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/security/toursolver_delivery_window.xml b/shipment_advice_planner_toursolver/security/toursolver_delivery_window.xml new file mode 100644 index 00000000..391b9f6e --- /dev/null +++ b/shipment_advice_planner_toursolver/security/toursolver_delivery_window.xml @@ -0,0 +1,23 @@ + + + + + toursolver.delivery.window access read + + + + + + + + + toursolver.delivery.window access read + + + + + + + + diff --git a/shipment_advice_planner_toursolver/security/toursolver_resource.xml b/shipment_advice_planner_toursolver/security/toursolver_resource.xml new file mode 100644 index 00000000..26b07bd9 --- /dev/null +++ b/shipment_advice_planner_toursolver/security/toursolver_resource.xml @@ -0,0 +1,16 @@ + + + + + + toursolver.resource access stock manager + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/security/toursolver_task.xml b/shipment_advice_planner_toursolver/security/toursolver_task.xml new file mode 100644 index 00000000..f2df59f5 --- /dev/null +++ b/shipment_advice_planner_toursolver/security/toursolver_task.xml @@ -0,0 +1,24 @@ + + + + + + toursolver.task access all + + + + + + + + + toursolver.task access system + + + + + + + + diff --git a/shipment_advice_planner_toursolver/static/description/icon.png b/shipment_advice_planner_toursolver/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/shipment_advice_planner_toursolver/static/description/icon.png differ diff --git a/shipment_advice_planner_toursolver/static/description/index.html b/shipment_advice_planner_toursolver/static/description/index.html new file mode 100644 index 00000000..7863f3c2 --- /dev/null +++ b/shipment_advice_planner_toursolver/static/description/index.html @@ -0,0 +1,459 @@ + + + + + + +Shipment Advice Planner Toursolver + + + +
+

Shipment Advice Planner Toursolver

+ + +

Beta License: AGPL-3 OCA/stock-logistics-transport Translate me on Weblate Try me on Runboat

+

This module extend the shipment advice planner engine to add a new planning +method based on geo-localization. It uses the GEOCONCEPT solution called +TourSolver to plan the shipment advices according to the location of the customers +by calculating the best route according to the available resources.

+

Table of contents

+ +
+

Configuration

+

Before configuring this module, you must configure your account and your +resources on the TourSolver website. Then you should be able to get your api key +and paste it in the default backend. You can access to backends list under the +setting menu of the inventory module.

+

The second step is to create the resources. You can add the properties you want +TourSolver to use in its calculation of the best route.

+

For advanced planning, you can define your customer delivery windows. It will be +considered in TourSolver calculation.

+

For more details about the properties you can use, please consult +[TourSolver documentation](https://mygeoconcept.com/doc/6cc656Uv7gARvG6T/ts-cloud-doc/docs/en/toursolver-cloud-book/)

+
+
+

Usage

+

In the shipment planner you will see a new option added to the planning methods. +If you select TourSolver, a TourSolver task per warehouse will be created for +the pickings you want to plan.

+

The task will pass through different states by a dedicated cron and at the end +of the process will create shipment advices according the returned result from +TourSolver.

+
+
+

Known issues / Roadmap

+

At this stage, the TourSolver task is managed by a cron which will synchronize +its status with TourSolver and get the result when it is ready. +Another approach is available using queue jobs where the sync task will be +rescheduled until it gets the optimization result.

+

Ideally, a webhook can be developed to be notified by toursolver when the result +is ready. He will ensure that the result is obtained as soon as possible.

+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

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.

+

This module is part of the OCA/stock-logistics-transport project on GitHub.

+

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

+
+
+
+ + diff --git a/shipment_advice_planner_toursolver/tests/__init__.py b/shipment_advice_planner_toursolver/tests/__init__.py new file mode 100644 index 00000000..bfd39f0d --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/__init__.py @@ -0,0 +1,4 @@ +from . import common +from . import test_shipment_advice_planner_toursolver +from . import test_toursolver_delivery_window +from . import test_picking_can_be_planned diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_check_status.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_check_status.yaml new file mode 100644 index 00000000..09b3ba20 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_check_status.yaml @@ -0,0 +1,126 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000033", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [{"id": "D1", + "mobileLogin": "d1@email.com", "openStart": false, "loadBeforeDeparture": true, + "noReload": true, "globalCapacity": 9999, "useAllCapacities": false, "startX": + 0.0, "startY": 0.0, "endX": 0.0, "endY": 0.0, "workStartTime": "17:06:00", + "fixedLoadingDuration": "00:00:00", "travelPenalty": 0.0, "workPenalty": 0.0}, + {"id": "D2", "mobileLogin": "d2@email.com", "openStart": false, + "loadBeforeDeparture": true, "noReload": true, "globalCapacity": 9999, + "useAllCapacities": false, "startX": 0.0, "startY": 0.0, "endX": 0.0, "endY": + 0.0, "workStartTime": "17:06:00", "fixedLoadingDuration": "00:00:00", + "travelPenalty": 0.0, "workPenalty": 0.0}], "options": {"vehicleCode": + "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "1596" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79906C387487JaRUXIS522KDCZY-fTQA"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:06:41 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909201.771.7484.161928|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=45D3D8DADCEA1C3851D762D556A1B0E9; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/status?taskId=7FFFFE79906C387487JaRUXIS522KDCZY-fTQA&tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","optimizeStatus":"TERMINATED","startTime":1676909201276,"initialCost":1,"currentCost":1,"initialDriveDistance":0,"currentDriveDistance":0,"initialDriveTime":0,"currentDriveTime":0,"initialDriveCost":0,"currentDriveCost":0,"initialWorkTime":360,"currentWorkTime":360,"initialWorkCost":1,"currentWorkCost":1,"initialOverWorkTime":0,"currentOverWorkTime":0,"initialOverWorkCost":0,"currentOverWorkCost":0,"initialRestTime":0,"currentRestTime":0,"initialNightsCost":0,"currentNightsCost":0,"initialCourierCost":0,"currentCourierCost":0,"initialDeliveryCost":0,"currentDeliveryCost":0,"initialFixedCost":0,"currentFixedCost":0,"initialPickUpQuantity":0.0,"currentPickUpQuantity":0.0,"initialDeliveredQuantity":0.0,"currentDeliveredQuantity":0.0,"initialUnplannedVisits":0,"currentUnplannedVisits":0,"initialPlannedVisits":0,"currentPlannedVisits":0,"currentVisitsNb":2,"initialLateTime":900,"currentLateTime":900,"initialWaitTime":0,"currentWaitTime":0,"mileageChartRemainingTime":0,"initialCo2":0.0,"currentCo2":0.0,"initialOpenTourNumber":1,"currentOpenTourNumber":1,"subOptimNb":0,"subOptimWaitingNb":0,"subOptimRunningNb":0,"subOptimFinishedNb":0,"subOptimErrorNb":0,"subOptimAbortedNb":0,"simulationId":"7FFFFE79906C38C9u6yeii62ShSIMh2ukRBq3g","positionInQueue":0}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:07:42 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909263.283.7650.97585|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=AAF28F9CBCD9A8737E7D9ADF870EBC50; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_cron_sync_task.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_cron_sync_task.yaml new file mode 100644 index 00000000..93fed995 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_cron_sync_task.yaml @@ -0,0 +1,181 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000034", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [{"id": "D1", + "mobileLogin": "d1@email.com", "openStart": false, "loadBeforeDeparture": true, + "noReload": true, "globalCapacity": 9999, "useAllCapacities": false, "startX": + 0.0, "startY": 0.0, "endX": 0.0, "endY": 0.0, "workStartTime": "17:07:00", + "fixedLoadingDuration": "00:00:00", "travelPenalty": 0.0, "workPenalty": 0.0}, + {"id": "D2", "mobileLogin": "d2@email.com", "openStart": false, + "loadBeforeDeparture": true, "noReload": true, "globalCapacity": 9999, + "useAllCapacities": false, "startX": 0.0, "startY": 0.0, "endX": 0.0, "endY": + 0.0, "workStartTime": "17:07:00", "fixedLoadingDuration": "00:00:00", + "travelPenalty": 0.0, "workPenalty": 0.0}], "options": {"vehicleCode": + "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "1596" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:07:44 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909264.826.989.170065|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=826AF2B934B2058F5B4FBAE8ABF1008F; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/status?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","optimizeStatus":"TERMINATED","startTime":1676909264236,"initialCost":1,"currentCost":1,"initialDriveDistance":0,"currentDriveDistance":0,"initialDriveTime":0,"currentDriveTime":0,"initialDriveCost":0,"currentDriveCost":0,"initialWorkTime":360,"currentWorkTime":360,"initialWorkCost":1,"currentWorkCost":1,"initialOverWorkTime":0,"currentOverWorkTime":0,"initialOverWorkCost":0,"currentOverWorkCost":0,"initialRestTime":0,"currentRestTime":0,"initialNightsCost":0,"currentNightsCost":0,"initialCourierCost":0,"currentCourierCost":0,"initialDeliveryCost":0,"currentDeliveryCost":0,"initialFixedCost":0,"currentFixedCost":0,"initialPickUpQuantity":0.0,"currentPickUpQuantity":0.0,"initialDeliveredQuantity":0.0,"currentDeliveredQuantity":0.0,"initialUnplannedVisits":0,"currentUnplannedVisits":0,"initialPlannedVisits":0,"currentPlannedVisits":0,"currentVisitsNb":2,"initialLateTime":1020,"currentLateTime":1020,"initialWaitTime":0,"currentWaitTime":0,"mileageChartRemainingTime":0,"initialCo2":0.0,"currentCo2":0.0,"initialOpenTourNumber":1,"currentOpenTourNumber":1,"subOptimNb":0,"subOptimWaitingNb":0,"subOptimRunningNb":0,"subOptimFinishedNb":0,"subOptimErrorNb":0,"subOptimAbortedNb":0,"simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","positionInQueue":0}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.021.7494.826478|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5235D2F23978EC9579570082D065639E; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/result?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: + '{"message":null,"status":"OK","plannedOrders":[{"resourceId":"D2","dayId":"1","stopId":"Start","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":1,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"Reload + (dep_1)","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":4,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"31","stopPosition":1,"stopY":0.0,"stopX":0.0,"stopType":0,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:03","stopStatus":1,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"9","stopPosition":2,"stopY":0.0,"stopX":0.0,"stopType":0,"stopDriveTime":"00:00","stopStartTime":"17:10","stopDuration":"00:03","stopStatus":1,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"End","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":2,"stopDriveTime":"00:00","stopStartTime":"17:13","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0}],"unplannedOrders":[],"warnings":[{"objectType":"R","id":"D1","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"},{"objectType":"R","id":"D2","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"}],"taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w","simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","inputData":null}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.405.988.265846|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5740BF321B3908F767D7E5AAA120BD3C; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ko.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ko.yaml new file mode 100644 index 00000000..3525d9bd --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ko.yaml @@ -0,0 +1,181 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000034", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [{"id": "D1", + "mobileLogin": "d1@email.com", "openStart": false, "loadBeforeDeparture": true, + "noReload": true, "globalCapacity": 9999, "useAllCapacities": false, "startX": + 0.0, "startY": 0.0, "endX": 0.0, "endY": 0.0, "workStartTime": "17:07:00", + "fixedLoadingDuration": "00:00:00", "travelPenalty": 0.0, "workPenalty": 0.0}, + {"id": "D2", "mobileLogin": "d2@email.com", "openStart": false, + "loadBeforeDeparture": true, "noReload": true, "globalCapacity": 9999, + "useAllCapacities": false, "startX": 0.0, "startY": 0.0, "endX": 0.0, "endY": + 0.0, "workStartTime": "17:07:00", "fixedLoadingDuration": "00:00:00", + "travelPenalty": 0.0, "workPenalty": 0.0}], "options": {"vehicleCode": + "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "1596" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:07:44 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909264.826.989.170065|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=826AF2B934B2058F5B4FBAE8ABF1008F; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/status?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","optimizeStatus":"TERMINATED","startTime":1676909264236,"initialCost":1,"currentCost":1,"initialDriveDistance":0,"currentDriveDistance":0,"initialDriveTime":0,"currentDriveTime":0,"initialDriveCost":0,"currentDriveCost":0,"initialWorkTime":360,"currentWorkTime":360,"initialWorkCost":1,"currentWorkCost":1,"initialOverWorkTime":0,"currentOverWorkTime":0,"initialOverWorkCost":0,"currentOverWorkCost":0,"initialRestTime":0,"currentRestTime":0,"initialNightsCost":0,"currentNightsCost":0,"initialCourierCost":0,"currentCourierCost":0,"initialDeliveryCost":0,"currentDeliveryCost":0,"initialFixedCost":0,"currentFixedCost":0,"initialPickUpQuantity":0.0,"currentPickUpQuantity":0.0,"initialDeliveredQuantity":0.0,"currentDeliveredQuantity":0.0,"initialUnplannedVisits":0,"currentUnplannedVisits":0,"initialPlannedVisits":0,"currentPlannedVisits":0,"currentVisitsNb":2,"initialLateTime":1020,"currentLateTime":1020,"initialWaitTime":0,"currentWaitTime":0,"mileageChartRemainingTime":0,"initialCo2":0.0,"currentCo2":0.0,"initialOpenTourNumber":1,"currentOpenTourNumber":1,"subOptimNb":0,"subOptimWaitingNb":0,"subOptimRunningNb":0,"subOptimFinishedNb":0,"subOptimErrorNb":0,"subOptimAbortedNb":0,"simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","positionInQueue":0}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.021.7494.826478|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5235D2F23978EC9579570082D065639E; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/result?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: + '{"message":null,"status":"fail","plannedOrders":[{"resourceId":"D2","dayId":"1","stopId":"Start","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":1,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"Reload + (dep_1)","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":4,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"9","stopPosition":2,"stopY":0.0,"stopX":0.0,"stopType":0,"stopDriveTime":"00:00","stopStartTime":"17:10","stopDuration":"00:03","stopStatus":1,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"End","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":2,"stopDriveTime":"00:00","stopStartTime":"17:13","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0}],"unplannedOrders":[],"warnings":[{"objectType":"R","id":"D1","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"},{"objectType":"R","id":"D2","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"}],"taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w","simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","inputData":null}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.405.988.265846|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5740BF321B3908F767D7E5AAA120BD3C; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ok.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ok.yaml new file mode 100644 index 00000000..93fed995 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_get_result_ok.yaml @@ -0,0 +1,181 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000034", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [{"id": "D1", + "mobileLogin": "d1@email.com", "openStart": false, "loadBeforeDeparture": true, + "noReload": true, "globalCapacity": 9999, "useAllCapacities": false, "startX": + 0.0, "startY": 0.0, "endX": 0.0, "endY": 0.0, "workStartTime": "17:07:00", + "fixedLoadingDuration": "00:00:00", "travelPenalty": 0.0, "workPenalty": 0.0}, + {"id": "D2", "mobileLogin": "d2@email.com", "openStart": false, + "loadBeforeDeparture": true, "noReload": true, "globalCapacity": 9999, + "useAllCapacities": false, "startX": 0.0, "startY": 0.0, "endX": 0.0, "endY": + 0.0, "workStartTime": "17:07:00", "fixedLoadingDuration": "00:00:00", + "travelPenalty": 0.0, "workPenalty": 0.0}], "options": {"vehicleCode": + "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "1596" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:07:44 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909264.826.989.170065|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=826AF2B934B2058F5B4FBAE8ABF1008F; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/status?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","optimizeStatus":"TERMINATED","startTime":1676909264236,"initialCost":1,"currentCost":1,"initialDriveDistance":0,"currentDriveDistance":0,"initialDriveTime":0,"currentDriveTime":0,"initialDriveCost":0,"currentDriveCost":0,"initialWorkTime":360,"currentWorkTime":360,"initialWorkCost":1,"currentWorkCost":1,"initialOverWorkTime":0,"currentOverWorkTime":0,"initialOverWorkCost":0,"currentOverWorkCost":0,"initialRestTime":0,"currentRestTime":0,"initialNightsCost":0,"currentNightsCost":0,"initialCourierCost":0,"currentCourierCost":0,"initialDeliveryCost":0,"currentDeliveryCost":0,"initialFixedCost":0,"currentFixedCost":0,"initialPickUpQuantity":0.0,"currentPickUpQuantity":0.0,"initialDeliveredQuantity":0.0,"currentDeliveredQuantity":0.0,"initialUnplannedVisits":0,"currentUnplannedVisits":0,"initialPlannedVisits":0,"currentPlannedVisits":0,"currentVisitsNb":2,"initialLateTime":1020,"currentLateTime":1020,"initialWaitTime":0,"currentWaitTime":0,"mileageChartRemainingTime":0,"initialCo2":0.0,"currentCo2":0.0,"initialOpenTourNumber":1,"currentOpenTourNumber":1,"subOptimNb":0,"subOptimWaitingNb":0,"subOptimRunningNb":0,"subOptimFinishedNb":0,"subOptimErrorNb":0,"subOptimAbortedNb":0,"simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","positionInQueue":0}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.021.7494.826478|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5235D2F23978EC9579570082D065639E; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.28.2 + method: GET + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/result?taskId=7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w&tsCloudApiKey=fake_api_key + response: + body: + string: + '{"message":null,"status":"OK","plannedOrders":[{"resourceId":"D2","dayId":"1","stopId":"Start","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":1,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"Reload + (dep_1)","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":4,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"31","stopPosition":1,"stopY":0.0,"stopX":0.0,"stopType":0,"stopDriveTime":"00:00","stopStartTime":"17:07","stopDuration":"00:03","stopStatus":1,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"9","stopPosition":2,"stopY":0.0,"stopX":0.0,"stopType":0,"stopDriveTime":"00:00","stopStartTime":"17:10","stopDuration":"00:03","stopStatus":1,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0},{"resourceId":"D2","dayId":"1","stopId":"End","stopPosition":0,"stopY":0.0,"stopX":0.0,"stopType":2,"stopDriveTime":"00:00","stopStartTime":"17:13","stopDuration":"00:00","stopStatus":0,"stopDriveDistance":0,"stopElapsedDistance":0,"placeName":null,"placeAddress":null,"aggregationInfo":null,"walkGoupRootId":null,"stopDriveSpeed":0.0}],"unplannedOrders":[],"warnings":[{"objectType":"R","id":"D1","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"},{"objectType":"R","id":"D2","constraint":29,"constraintName":"WORKPENALTY","value":"0.0","message":"Journey + cost and hourly cost can not be set both to zero: default values will be + applied.","messageId":6,"i18nMessageCode":"missingMandatoryPenalties"}],"taskId":"7FFFFE79906B4288VjokZIK5TJSH0PH1Mlw77w","simulationId":"7FFFFE79906B42C27iTIDFHlQT2UHxnPr7yFzg","inputData":null}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Mon, 20 Feb 2023 16:08:45 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676909326.405.988.265846|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=5740BF321B3908F767D7E5AAA120BD3C; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_send_ok_with_resources.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_send_ok_with_resources.yaml new file mode 100644 index 00000000..be0e3833 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_send_ok_with_resources.yaml @@ -0,0 +1,77 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000002", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [{"id": "D1", + "mobileLogin": "d1@email.com", "openStart": false, "loadBeforeDeparture": true, + "noReload": true, "globalCapacity": 9999, "useAllCapacities": false, "startX": + 0.0, "startY": 0.0, "endX": 0.0, "endY": 0.0, "workStartTime": "02:29:00", + "fixedLoadingDuration": "00:00:00", "travelPenalty": 0.0, "workPenalty": 0.0}, + {"id": "D2", "mobileLogin": "d2@email.com", "openStart": false, + "loadBeforeDeparture": true, "noReload": true, "globalCapacity": 9999, + "useAllCapacities": false, "startX": 0.0, "startY": 0.0, "endX": 0.0, "endY": + 0.0, "workStartTime": "02:29:00", "fixedLoadingDuration": "00:00:00", + "travelPenalty": 0.0, "workPenalty": 0.0}], "options": {"vehicleCode": + "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "823" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79952E95FD4ZD-9FWZQBuyh4qWhv2qpg"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 19 Feb 2023 17:55:54 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676829354.859.986.156482|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=DC6EFC59CB8654CD4DF5934D7A44349E; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_fail_connexion.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_fail_connexion.yaml new file mode 100644 index 00000000..1c1c84a0 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_fail_connexion.yaml @@ -0,0 +1,66 @@ +interactions: + - request: + body: + '{"simulationName": "TST/00000027", "countryCode": "US", "beginDate": + "2023-02-20", "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": + "dep_1"}], "orders": [{"customerId": false, "fixedVisitDuration": "00:03:00", + "id": 9, "label": "Wood Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, + "y": 0.0, "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock + CA 95380\nUnited States"}, "timeWindows": [{"beginTime": "08:00", "endTime": + "17:00"}]}, {"customerId": false, "fixedVisitDuration": "00:03:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [], "options": + {"vehicleCode": "deliveryIntermediateVehicle", "maxOptimDuration": "00:00:00", + "useForbiddenTransitAreas": false}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "946" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: "" + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Length: + - "0" + Date: + - Mon, 20 Feb 2023 01:52:37 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676857958.202.7540.245719|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=D228CB41D4409DF0A176463FC1EA30FA; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + vary: + - origin + status: + code: 403 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_no_order.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_no_order.yaml new file mode 100644 index 00000000..d3f28a50 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_no_order.yaml @@ -0,0 +1,58 @@ +interactions: + - request: + body: + '{"simulationName": 52, "countryCode": "BE", "beginDate": "2023-02-18", + "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": "dep_1"}], "orders": + [], "resources": [], "options": {}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "186" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":"no orders found","status":"ERROR","taskId":null}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sat, 18 Feb 2023 19:46:29 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676749589.966.989.624966|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=570C65F0964B9520C1151CCE00149A54; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_ok_without_resources.yaml b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_ok_without_resources.yaml new file mode 100644 index 00000000..a76f7527 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/cassettes/TestShipmentAdvicePlannerToursolver.test_task_send_request_ok_without_resources.yaml @@ -0,0 +1,66 @@ +interactions: + - request: + body: + '{"simulationName": 21, "countryCode": "BE", "beginDate": "2023-02-19", + "language": "en_US", "depots": [{"x": 0.0, "y": 0.0, "id": "dep_1"}], "orders": + [{"customerId": false, "fixedVisitDuration": "00:00:00", "id": 9, "label": "Wood + Corner", "phone": "(623)-853-7197", "type": 0, "x": 0.0, "y": 0.0, + "customDataMap": {"address": "Wood Corner\n1839 Arbor Way\n\nTurlock CA + 95380\nUnited States"}, "timeWindows": [{"beginTime": "10:00", "endTime": + "12:00"}]}, {"customerId": false, "fixedVisitDuration": "00:00:00", "id": 31, + "label": "Gemini Furniture, Oscar Morgan", "phone": "(561)-239-1744", "type": 0, + "x": 0.0, "y": 0.0, "customDataMap": {"address": "Gemini Furniture\n317 + Fairchild Dr\n\nFairfield CA 94535\nUnited States"}, "timeWindows": + [{"beginTime": "08:00", "endTime": "17:00"}]}], "resources": [], "options": {}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - "823" + Content-Type: + - application/json + User-Agent: + - python-requests/2.28.2 + method: POST + uri: https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/optimize?tsCloudApiKey=fake_api_key + response: + body: + string: '{"message":null,"status":"OK","taskId":"7FFFFE79952E95FD4ZD-9FWZQBuyh4qWhv2qpg"}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Accept,Upgrade-Insecure-Requests,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization + Access-Control-Allow-Methods: + - PUT, GET, POST, DELETE, PATCH, OPTIONS + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - "*,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + Access-Control-Max-Age: + - "1728000" + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 19 Feb 2023 17:55:54 GMT + Set-Cookie: + - INGRESSTSPRODCOOKIE=1676829354.859.986.156482|526296c4243d90e279c23661c06665b2; + Path=/ToursolverCloud; Secure; HttpOnly; SameSite=None + - JSESSIONID=DC6EFC59CB8654CD4DF5934D7A44349E; Path=/ToursolverCloud; + HttpOnly; SameSite=None; Secure + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Transfer-Encoding: + - chunked + vary: + - origin + status: + code: 200 + message: "" +version: 1 diff --git a/shipment_advice_planner_toursolver/tests/common.py b/shipment_advice_planner_toursolver/tests/common.py new file mode 100644 index 00000000..b0f7b1ff --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/common.py @@ -0,0 +1,37 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo.tests.common import Form, TransactionCase + + +class TestShipmentAdvicePlannerToursolverCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.pickings = cls.env["stock.picking"].search([]) + cls.context = { + "active_ids": cls.pickings.ids, + "active_model": "stock.picking", + } + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.resource_1 = cls.env.ref( + "shipment_advice_planner_toursolver.toursolver_resource_r1_demo" + ) + cls.resource_2 = cls.env.ref( + "shipment_advice_planner_toursolver.toursolver_resource_r2_demo" + ) + + def setUp(self): + super().setUp() + self.wizard_form = Form( + self.env["shipment.advice.planner"].with_context(**self.context) + ) + self.wizard_form.warehouse_id = self.warehouse + + def _create_task(self): + self.wizard_form.shipment_planning_method = "toursolver" + wizard = self.wizard_form.save() + wizard.delivery_resource_ids = self.resource_1 | self.resource_2 + wizard.button_plan_shipments() + return wizard.picking_to_plan_ids.toursolver_task_id diff --git a/shipment_advice_planner_toursolver/tests/test_picking_can_be_planned.py b/shipment_advice_planner_toursolver/tests/test_picking_can_be_planned.py new file mode 100644 index 00000000..82f70929 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/test_picking_can_be_planned.py @@ -0,0 +1,25 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.shipment_advice_planner.tests.test_picking_can_be_planned import ( + TestPickingCanBePlanned as TestPickingCanBePlannedBase, +) + +from .common import TestShipmentAdvicePlannerToursolverCommon + + +class TestPickingCanBePlanned( + TestPickingCanBePlannedBase, TestShipmentAdvicePlannerToursolverCommon +): + def test_01(self): + picking = self.pickings.filtered(lambda p: p.state == "assigned")[0] + self.assert_picking_can_be_planned_in_shipment_advice(picking) + self._create_task() + self.assertTrue(picking.toursolver_task_id) + self.assert_can_not_be_planned_in_shipment_advice(picking) + picking.toursolver_task_id.state = "done" + self.assert_picking_can_be_planned_in_shipment_advice(picking) + picking.toursolver_task_id.state = "cancelled" + self.assert_picking_can_be_planned_in_shipment_advice(picking) + picking.toursolver_task_id.state = "draft" + self.assert_can_not_be_planned_in_shipment_advice(picking) diff --git a/shipment_advice_planner_toursolver/tests/test_shipment_advice_planner_toursolver.py b/shipment_advice_planner_toursolver/tests/test_shipment_advice_planner_toursolver.py new file mode 100644 index 00000000..3e966f20 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/test_shipment_advice_planner_toursolver.py @@ -0,0 +1,147 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time +from vcr_unittest import VCRTestCase + +from odoo.tools import mute_logger + +from .common import TestShipmentAdvicePlannerToursolverCommon + + +class TestShipmentAdvicePlannerToursolver( + VCRTestCase, TestShipmentAdvicePlannerToursolverCommon +): + def test_query_url(self): + expected_query_url = ( + "https://geoservices.geoconcept.com/ToursolverCloud/api/ts/toursolver/" + "test?param1=param_1¶m2=param_2&tsCloudApiKey=fake_api_key" + ) + task = self._create_task() + query_url = task._toursolver_query_url( + action="test", param1="param_1", param2="param_2" + ) + self.assertEqual(query_url, expected_query_url) + + def test_toursolver_task_creation(self): + self.wizard_form.shipment_planning_method = "toursolver" + wizard = self.wizard_form.save() + self.assertFalse(wizard.picking_to_plan_ids.toursolver_task_id) + wizard.button_plan_shipments() + task = wizard.picking_to_plan_ids.toursolver_task_id + self.assertTrue(task) + self.assertEqual(len(wizard.picking_to_plan_ids), 9) + self.assertEqual(len(task.picking_ids), 9) + + @mute_logger("TourSolver Connexion") + def test_task_send_request_fail_connexion(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "error") + self.assertIn("403 Client Error", task.toursolver_error_message) + + @mute_logger("TourSolver Connexion") + def test_task_send_request_no_order(self): + task = self._create_task() + task.picking_ids = False + task.button_send_request() + self.assertEqual(task.state, "error") + self.assertEqual(task.toursolver_error_message, "no orders found") + + def test_task_send_request_ok_without_resources(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "in_progress") + self.assertFalse(task.toursolver_error_message) + self.assertEqual(task.task_id, "7FFFFE79952E95FD4ZD-9FWZQBuyh4qWhv2qpg") + + def test_send_ok_with_resources(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "in_progress") + self.assertFalse(task.toursolver_error_message) + self.assertEqual(task.task_id, "7FFFFE79952E95FD4ZD-9FWZQBuyh4qWhv2qpg") + + @freeze_time("2023-02-15 10:30:00") + def test_resource_properties(self): + self.assertDictEqual( + self.resource_1.with_context( + tz="Europe/Brussels" + )._get_resource_properties(), + { + "globalCapacity": 9999, + "id": "D1", + "loadBeforeDeparture": True, + "mobileLogin": "d1@email.com", + "noReload": True, + "openStart": False, + "useAllCapacities": False, + "workPenalty": 0.0, + "workStartTime": "11:30:00", + "travelPenalty": 0.0, + "fixedLoadingDuration": "00:00:00", + }, + ) + + def test_check_status(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "in_progress") + self.assertFalse(task.toursolver_error_message) + task.button_check_status() + self.assertEqual(task.state, "success") + + def test_get_result_ok(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "in_progress") + self.assertFalse(task.toursolver_error_message) + task.button_check_status() + self.assertEqual(task.state, "success") + task.button_get_result() + self.assertEqual(task.state, "done") + shipment = task.shipment_advice_ids + self.assertEqual(shipment.toursolver_resource_id, self.resource_2) + planned_picking = shipment.planned_picking_ids + planned_partners = shipment.planned_picking_ids.mapped("partner_id") + self.assertEqual( + set(planned_picking.mapped("toursolver_shipment_advice_rank")), + {1, 2}, + ) + first_stop = planned_picking.filtered( + lambda p: p.toursolver_shipment_advice_rank == 1 + ).partner_id + second_stop = planned_picking.filtered( + lambda p: p.toursolver_shipment_advice_rank == 2 + ).partner_id + self.assertEqual(first_stop, planned_partners[1]) + self.assertEqual(second_stop, planned_partners[0]) + + def test_get_result_ko(self): + task = self._create_task() + task.button_send_request() + self.assertEqual(task.state, "in_progress") + task.button_check_status() + self.assertEqual(task.state, "success") + task.button_get_result() + self.assertEqual(task.toursolver_status, "failed") + self.assertEqual(task.state, "error") + self.assertEqual( + task.toursolver_error_message, + "The following partners are not found into the optimization result: Oscar Morgan", + ) + self.assertFalse(task.shipment_advice_ids) + + def test_cron_sync_task(self): + task = self._create_task() + self.env[task._name]._cron_sync_task() + self.assertEqual(task.state, "in_progress") + self.env[task._name]._cron_sync_task() + self.assertEqual(task.state, "done") + self.assertEqual( + task.shipment_advice_ids.toursolver_resource_id, self.resource_2 + ) + + def test_backend_definition_creation(self): + backend = self.env["toursolver.backend"].create({"name": "backend"}) + self.assertTrue(backend.definition_id) diff --git a/shipment_advice_planner_toursolver/tests/test_toursolver_delivery_window.py b/shipment_advice_planner_toursolver/tests/test_toursolver_delivery_window.py new file mode 100644 index 00000000..62aba4d0 --- /dev/null +++ b/shipment_advice_planner_toursolver/tests/test_toursolver_delivery_window.py @@ -0,0 +1,192 @@ +# Copyright 2020 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase + + +class TestToursolverDeliveryWindow(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.delivery_window_model = cls.env["toursolver.delivery.window"] + cls.partner_model = cls.env["res.partner"] + cls.partner_1 = cls.partner_model.create({"name": "partner 1"}) + cls.partner_2 = cls.partner_model.create({"name": "patner 2"}) + cls.monday = cls.env.ref("base_time_window.time_weekday_monday") + cls.sunday = cls.env.ref("base_time_window.time_weekday_sunday") + + def test_00(self): + """ + Data: + + A partner without delivery window + Test Case: + Add a delivery window + Expected result: + A delivery window is created for the partner + """ + + self.assertFalse(self.partner_1.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.toursolver_delivery_window_ids) + delivery_window = self.partner_1.toursolver_delivery_window_ids + self.assertEqual(delivery_window.time_window_start, 10.0) + self.assertEqual(delivery_window.time_window_end, 12.0) + self.assertEqual(delivery_window.time_window_weekday_ids, self.monday) + + def test_01(self): + """ + Data: + + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 unlink the partner + Expected result: + 1 A delivery window is created for the partner + 2 The delivery window is removed + """ + partner_id = self.partner_1.id + self.assertFalse(self.partner_1.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.toursolver_delivery_window_ids) + delivery_window = self.delivery_window_model.search( + [("partner_id", "=", partner_id)] + ) + self.assertTrue(delivery_window) + self.partner_1.unlink() + self.assertFalse(delivery_window.exists()) + + def test_02(self): + """ + Data: + + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 Add a second delivery window that overlaps the first one (same day) + Expected result: + 1 A delivery window is created for the partner + 2 ValidationError is raised + """ + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + with self.assertRaises(ValidationError): + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 11.0, + "time_window_end": 13.0, + "time_window_weekday_ids": [ + (4, self.monday.id), + (4, self.sunday.id), + ], + } + ) + + def test_03(self): + """ + Data: + + A partner without delivery window + Test Case: + 1 Add a delivery window + 2 Add a second delivery window that overlaps the first one (another day) + Expected result: + 1 A delivery window is created for the partner + 2 A second delivery window is created for the partner + """ + self.assertFalse(self.partner_1.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 11.0, + "time_window_end": 13.0, + "time_window_weekday_ids": [(4, self.sunday.id)], + } + ) + self.assertEqual(len(self.partner_1.toursolver_delivery_window_ids), 2) + + def test_04(self): + """ + Data: + + Partner 1 without delivery window + Partner 2 without delivery window + Test Case: + 1 Add a delivery window to partner 1 + 2 Add the same delivery window to partner 2 + Expected result: + 1 A delivery window is created for the partner 1 + 1 A delivery window is created for the partner 2 + """ + self.assertFalse(self.partner_1.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_1.toursolver_delivery_window_ids) + self.assertFalse(self.partner_2.toursolver_delivery_window_ids) + self.delivery_window_model.create( + { + "partner_id": self.partner_2.id, + "time_window_start": 10.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) + self.assertTrue(self.partner_2.toursolver_delivery_window_ids) + + def test_05(self): + """ + Data: + + Partner 1 without delivery window + Test Case: + Add a delivery window to partner 1 with time_window_end > time_window_start + Expected result: + ValidationError is raised + """ + with self.assertRaises(ValidationError): + self.delivery_window_model.create( + { + "partner_id": self.partner_1.id, + "time_window_start": 14.0, + "time_window_end": 12.0, + "time_window_weekday_ids": [(4, self.monday.id)], + } + ) diff --git a/shipment_advice_planner_toursolver/views/res_config_settings.xml b/shipment_advice_planner_toursolver/views/res_config_settings.xml new file mode 100644 index 00000000..2f0a3bf4 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/res_config_settings.xml @@ -0,0 +1,39 @@ + + + + + + res.config.settings + + + +

Shipment advices planning by geo-optimization (TourSolver)

+
+
+
+
+
+
+
+
+
+ + + +
diff --git a/shipment_advice_planner_toursolver/views/res_partner.xml b/shipment_advice_planner_toursolver/views/res_partner.xml new file mode 100644 index 00000000..3b449c41 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/res_partner.xml @@ -0,0 +1,21 @@ + + + + + res.partner + + + + + + + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/views/stock_picking.xml b/shipment_advice_planner_toursolver/views/stock_picking.xml new file mode 100644 index 00000000..8d81ce90 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/stock_picking.xml @@ -0,0 +1,19 @@ + + + + + + stock.picking + + + + + + + + + + + + diff --git a/shipment_advice_planner_toursolver/views/toursolver_backend.xml b/shipment_advice_planner_toursolver/views/toursolver_backend.xml new file mode 100644 index 00000000..d4bc7ed9 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/toursolver_backend.xml @@ -0,0 +1,109 @@ + + + + + + toursolver.backend + +
+ + + + + +
+
+
+ + + + toursolver.backend + + + + + + + + + TourSolver + toursolver.backend + tree,form + [] + {} + + + + Backend + + + + + +
diff --git a/shipment_advice_planner_toursolver/views/toursolver_delivery_window.xml b/shipment_advice_planner_toursolver/views/toursolver_delivery_window.xml new file mode 100644 index 00000000..5fbc398c --- /dev/null +++ b/shipment_advice_planner_toursolver/views/toursolver_delivery_window.xml @@ -0,0 +1,45 @@ + + + + + toursolver.delivery.window + + + + + + + + + + + + toursolver.delivery.window + + + + + + + + + + + Partner Delivery Windows + toursolver.delivery.window + tree + [] + {} + + + Partner Delivery Windows + + + + + diff --git a/shipment_advice_planner_toursolver/views/toursolver_resource.xml b/shipment_advice_planner_toursolver/views/toursolver_resource.xml new file mode 100644 index 00000000..d32d3615 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/toursolver_resource.xml @@ -0,0 +1,93 @@ + + + + + + toursolver.resource + +
+ + + +
+
+
+ + + toursolver.resource + + + + + + + + + toursolver.resource + + + + + + + + + Resources + toursolver.resource + tree,form + [] + {} + + + + Resources + + + + + +
diff --git a/shipment_advice_planner_toursolver/views/toursolver_task.xml b/shipment_advice_planner_toursolver/views/toursolver_task.xml new file mode 100644 index 00000000..a600a294 --- /dev/null +++ b/shipment_advice_planner_toursolver/views/toursolver_task.xml @@ -0,0 +1,132 @@ + + + + + + toursolver.task + +
+
+
+ + + +
+
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + toursolver.task + + + + + + + + + + + + Tasks + toursolver.task + tree,form + [] + {} + + + + Tasks + + + + + +
diff --git a/shipment_advice_planner_toursolver/wizards/__init__.py b/shipment_advice_planner_toursolver/wizards/__init__.py new file mode 100644 index 00000000..f1f4bd57 --- /dev/null +++ b/shipment_advice_planner_toursolver/wizards/__init__.py @@ -0,0 +1 @@ +from . import shipment_advice_planner diff --git a/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.py b/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.py new file mode 100644 index 00000000..499230e0 --- /dev/null +++ b/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.py @@ -0,0 +1,68 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command, fields, models + + +class ShipmentAdvicePlanner(models.TransientModel): + + _inherit = "shipment.advice.planner" + + shipment_planning_method = fields.Selection( + selection_add=[("toursolver", "TourSolver")], + ondelete={"toursolver": "cascade"}, + ) + delivery_resource_ids = fields.Many2many( + comodel_name="toursolver.resource", + string="Delivery resources", + help="delivery resources to be considered in geo-optimazation", + ) + toursolver_resource_id = fields.Many2one( + comodel_name="toursolver.resource", + string="Toursolver Resource", + readonly=True, + help="TourSolver resource to be propgated to the shipment advice in simple" + "planning method", + ) + toursolver_task_id = fields.Many2one( + comodel_name="toursolver.task", string="Toursolver Task", readonly=True + ) + + def _prepare_shipment_advice_common_vals(self, picking_type): + res = super()._prepare_shipment_advice_common_vals(picking_type) + res.update( + { + "toursolver_resource_id": self.toursolver_resource_id.id, + "toursolver_task_id": self.toursolver_task_id.id, + } + ) + return res + + def _plan_shipments_for_method(self): + self.ensure_one() + if self.shipment_planning_method != "toursolver": + return super()._plan_shipments_for_method() + for ( + picking_type, + pickings_to_plan, + ) in self._get_picking_to_plan_by_picking_type().items(): + self._init_toursolver_task(picking_type, pickings_to_plan) + return self.env["shipment.advice"] + + def _prepare_toursolver_task_vals(self, picking_type, pickings_to_plan): + task_model = self.env["toursolver.task"] + backend = task_model._get_default_toursolver_backend() + return { + "toursolver_backend_id": backend.id, + "warehouse_id": picking_type.warehouse_id.id, + "dock_id": self.dock_id.id, + "picking_ids": [Command.set(pickings_to_plan.ids)], + "delivery_resource_ids": [Command.set(self.delivery_resource_ids.ids)], + } + + def _init_toursolver_task(self, picking_type, pickings_to_plan): + task_model = self.env["toursolver.task"] + # create access is not given to any group + return task_model.sudo().create( + self._prepare_toursolver_task_vals(picking_type, pickings_to_plan) + ) diff --git a/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.xml b/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.xml new file mode 100644 index 00000000..519c1ca1 --- /dev/null +++ b/shipment_advice_planner_toursolver/wizards/shipment_advice_planner.xml @@ -0,0 +1,27 @@ + + + + + + shipment.advice.planner + + + + + + + + + + + + + diff --git a/shipment_advice_planner_toursolver_queue_job/README.rst b/shipment_advice_planner_toursolver_queue_job/README.rst new file mode 100644 index 00000000..c57335fb --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/README.rst @@ -0,0 +1,77 @@ +============================================ +Shipment Advice Planner Toursolver Queue Job +============================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:77444cad6012c2f458324f4e9b5e491d8438cb09bf44397a8aa40c87c001bcdb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fstock--logistics--transport-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-transport/tree/16.0/shipment_advice_planner_toursolver_queue_job + :alt: OCA/stock-logistics-transport +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-transport-16-0/stock-logistics-transport-16-0-shipment_advice_planner_toursolver_queue_job + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-transport&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module makes TourSolver tasks of shipment planning process runs in queue jobs. + +**Table of contents** + +.. contents:: + :local: + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon +* Souheil Bejaoui + +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/stock-logistics-transport `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/shipment_advice_planner_toursolver_queue_job/__init__.py b/shipment_advice_planner_toursolver_queue_job/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/shipment_advice_planner_toursolver_queue_job/__manifest__.py b/shipment_advice_planner_toursolver_queue_job/__manifest__.py new file mode 100644 index 00000000..c4fa396c --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Shipment Advice Planner Toursolver Queue Job", + "summary": """Run TourSolver queries in queue jobs""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-transport", + "depends": ["shipment_advice_planner_toursolver", "queue_job", "web_notify"], + "data": ["data/queue_job_channel.xml", "data/queue_job_function.xml"], + "demo": [], +} diff --git a/shipment_advice_planner_toursolver_queue_job/data/queue_job_channel.xml b/shipment_advice_planner_toursolver_queue_job/data/queue_job_channel.xml new file mode 100644 index 00000000..c5f70be4 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/data/queue_job_channel.xml @@ -0,0 +1,12 @@ + + + + + + TourSolver + + + + + diff --git a/shipment_advice_planner_toursolver_queue_job/data/queue_job_function.xml b/shipment_advice_planner_toursolver_queue_job/data/queue_job_function.xml new file mode 100644 index 00000000..b77ef76c --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/data/queue_job_function.xml @@ -0,0 +1,33 @@ + + + + + + + _toursolver_send_request + + + + + + _toursolver_check_status + + + + + + _toursolver_get_result + + + + diff --git a/shipment_advice_planner_toursolver_queue_job/models/__init__.py b/shipment_advice_planner_toursolver_queue_job/models/__init__.py new file mode 100644 index 00000000..055fc019 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/models/__init__.py @@ -0,0 +1 @@ +from . import toursolver_task diff --git a/shipment_advice_planner_toursolver_queue_job/models/toursolver_task.py b/shipment_advice_planner_toursolver_queue_job/models/toursolver_task.py new file mode 100644 index 00000000..a7de22b7 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/models/toursolver_task.py @@ -0,0 +1,62 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError + +from odoo.addons.queue_job.exception import RetryableJobError + + +class ToursolverTask(models.Model): + + _inherit = "toursolver.task" + + def _toursolver_process(self): + self.ensure_one() + send_request_job = self.delayable()._toursolver_send_request() + check_status_job = self.delayable()._toursolver_check_status() + get_result_job = self.delayable()._toursolver_get_result() + send_request_job.on_done(check_status_job.on_done(get_result_job)).delay() + self.env.user.notify_info( + message=_( + "TourSolver task '%(task)s' is being processed in background." + " You will be notify once it's done" + ) + % dict(task=self.name), + sticky=False, + ) + + def _toursolver_check_status(self): + res = super()._toursolver_check_status() + if not self.task_id: + raise UserError(_("TourSolver taskID is null")) + if self.state == "in_progress": + raise RetryableJobError( + "The result is not ready yet", seconds=5, ignore_retry=True + ) + return res + + def _toursolver_get_result(self): + res = super()._toursolver_get_result() + if self.state == "done": + self.create_uid.notify_success( + message=_( + "TourSolver task '%(task)s' process finished with success." + " Shipment advices are created." + ) + % dict(task=self.name), + sticky=False, + ) + return res + + def _toursolver_notify_error(self, error_msg): + res = super()._toursolver_notify_error(error_msg) + self.create_uid.notify_danger( + message=_( + "TourSolver task '%(task)s' process failed." + " Please check the task for more details." + ) + % dict(task=self.name), + sticky=False, + ) + return res diff --git a/shipment_advice_planner_toursolver_queue_job/readme/CONTRIBUTORS.rst b/shipment_advice_planner_toursolver_queue_job/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..7bbe49c9 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Laurent Mignon +* Souheil Bejaoui diff --git a/shipment_advice_planner_toursolver_queue_job/readme/DESCRIPTION.rst b/shipment_advice_planner_toursolver_queue_job/readme/DESCRIPTION.rst new file mode 100644 index 00000000..792d15fe --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module makes TourSolver tasks of shipment planning process runs in queue jobs. diff --git a/shipment_advice_planner_toursolver_queue_job/static/description/icon.png b/shipment_advice_planner_toursolver_queue_job/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/shipment_advice_planner_toursolver_queue_job/static/description/icon.png differ diff --git a/shipment_advice_planner_toursolver_queue_job/static/description/index.html b/shipment_advice_planner_toursolver_queue_job/static/description/index.html new file mode 100644 index 00000000..9923dd82 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/static/description/index.html @@ -0,0 +1,422 @@ + + + + + + +Shipment Advice Planner Toursolver Queue Job + + + +
+

Shipment Advice Planner Toursolver Queue Job

+ + +

Beta License: AGPL-3 OCA/stock-logistics-transport Translate me on Weblate Try me on Runboat

+

This module makes TourSolver tasks of shipment planning process runs in queue jobs.

+

Table of contents

+ +
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

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.

+

This module is part of the OCA/stock-logistics-transport project on GitHub.

+

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

+
+
+
+ + diff --git a/shipment_advice_planner_toursolver_queue_job/wizards/__init__.py b/shipment_advice_planner_toursolver_queue_job/wizards/__init__.py new file mode 100644 index 00000000..f1f4bd57 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/wizards/__init__.py @@ -0,0 +1 @@ +from . import shipment_advice_planner diff --git a/shipment_advice_planner_toursolver_queue_job/wizards/shipment_advice_planner.py b/shipment_advice_planner_toursolver_queue_job/wizards/shipment_advice_planner.py new file mode 100644 index 00000000..b4a645d6 --- /dev/null +++ b/shipment_advice_planner_toursolver_queue_job/wizards/shipment_advice_planner.py @@ -0,0 +1,14 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ShipmentAdvicePlanner(models.TransientModel): + + _inherit = "shipment.advice.planner" + + def _init_toursolver_task(self, warehouse, pickings_to_plan): + task = super()._init_toursolver_task(warehouse, pickings_to_plan) + task._toursolver_process() + return task diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..4aadb0af --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +vcrpy-unittest