diff --git a/delivery_carrier_label_gls/README.rst b/delivery_carrier_label_gls/README.rst new file mode 100644 index 0000000000..8562bea750 --- /dev/null +++ b/delivery_carrier_label_gls/README.rst @@ -0,0 +1,116 @@ +========================== +Delivery Carrier Label GLS +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b3ed1d0fb921f626dd8f18712140d92f994236498ca931940d5e98cfecd79441 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdelivery--carrier-lightgray.png?logo=github + :target: https://github.com/OCA/delivery-carrier/tree/14.0/delivery_carrier_label_gls + :alt: OCA/delivery-carrier +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/delivery-carrier-14-0/delivery-carrier-14-0-delivery_carrier_label_gls + :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/delivery-carrier&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Integration with GLS shipping webservices. +Documentation: +http://gls-shipit.gls-group.eu/webservices/2_8_11/doxygen/WS-REST-API/index.html + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To start using GLS, you need to configure two types of settings in +*Inventory - Configuration- Delivery* or *Inventory - Configuration - Settings* +which leads to the right section in inventory global settings. +First you have the *Carrier Account* where you find account number +and password then you also have *Shipping Methods* with other GLS +parameters to configure such as contact ID, urls and return address. +These 2 types of settings use **"GLS"** as delivery type. +The contact ID corresponds to the sender which needs to be a contact in the +GLS database. This determines the default return address, as well as the billing. +You can also configure the tracking url that is used for each carrier. + +For client integration tests you need to fill your credentials in the tests/common.py. + +Usage +===== + +Create the packages on GLS which returns a tracking ID. +If there is any kind of mistake (address, weight), +it is possible to cancel it as long as it has not been scanned yet. +If the package is not cancelled, it is invoiced even if it never ships. + +When sending a picking, all products should be put in one or multiple packages. +These packages need to have a GLS packaging either Parcel, Express, or Freight. +These are already pre-configured in the module data. + +The end of day report should be printed when the delivery takes place. +At the last resort, this function should be called at the end of the day. +If it had already run, it would have no impact. +In case the delivery is delayed, the report should simply be kept for the +next day, and provided with the next report if there is one. + +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 +* Akretion + +Contributors +~~~~~~~~~~~~ + +* David Beal @ Akretion +* Nans Lefebvre +* Laurent Mignon +* Hughes Damry + +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/delivery-carrier `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/delivery_carrier_label_gls/__init__.py b/delivery_carrier_label_gls/__init__.py new file mode 100644 index 0000000000..9846d442c8 --- /dev/null +++ b/delivery_carrier_label_gls/__init__.py @@ -0,0 +1,3 @@ +from .hooks import post_init_hook +from . import models +from . import wizards diff --git a/delivery_carrier_label_gls/__manifest__.py b/delivery_carrier_label_gls/__manifest__.py new file mode 100644 index 0000000000..f4ac7a848e --- /dev/null +++ b/delivery_carrier_label_gls/__manifest__.py @@ -0,0 +1,37 @@ +# © 2015 David BEAL @ Akretion +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Delivery Carrier Label GLS", + "version": "14.0.1.0.0", + "author": "Acsone,Akretion,Odoo Community Association (OCA)", + "maintener": "Akretion", + "category": "Warehouse", + "summary": "GLS carrier label printing", + "depends": ["base_delivery_carrier_label", "delivery"], + "website": "https://github.com/OCA/delivery-carrier", + "data": [ + "security/groups.xml", + "security/ir.model.access.csv", + "data/ir_cron.xml", + "data/product_product.xml", + "data/carrier_account.xml", + "data/delivery_carrier.xml", + "data/product_packaging.xml", + "views/res_config_settings.xml", + "views/delivery_carrier.xml", + "views/delivery_report_gls.xml", + "views/sale_order.xml", + "views/stock.xml", + "views/carrier_account.xml", + "wizards/delivery_report_gls_wizard.xml", + "report/report_delivery_report_gls.xml", + "report/delivery_report_gls_view.xml", + ], + "license": "AGPL-3", + "installable": True, + "auto_install": False, + "application": False, + "post_init_hook": "post_init_hook", +} diff --git a/delivery_carrier_label_gls/data/carrier_account.xml b/delivery_carrier_label_gls/data/carrier_account.xml new file mode 100644 index 0000000000..4ae281d88a --- /dev/null +++ b/delivery_carrier_label_gls/data/carrier_account.xml @@ -0,0 +1,17 @@ + + + + GLS + gls + CHANGE ME + CHANGE ME + + + carrier_account_id + + + + diff --git a/delivery_carrier_label_gls/data/delivery_carrier.xml b/delivery_carrier_label_gls/data/delivery_carrier.xml new file mode 100644 index 0000000000..edaffaa2fb --- /dev/null +++ b/delivery_carrier_label_gls/data/delivery_carrier.xml @@ -0,0 +1,24 @@ + + + + GLS + gls + GLS + + + CHANGE ME + https://shipit-wbm-test01.gls-group.eu:8443/backend/rs/ + https://shipit-wbm-test01.gls-group.eu:8443/backend/rs/ + https://gls-group.eu/EU/en/parcel-tracking/match=%s + pdf + + diff --git a/delivery_carrier_label_gls/data/ir_cron.xml b/delivery_carrier_label_gls/data/ir_cron.xml new file mode 100644 index 0000000000..12b21904d6 --- /dev/null +++ b/delivery_carrier_label_gls/data/ir_cron.xml @@ -0,0 +1,18 @@ + + + + GLS: End of Day Report + + + 1 + days + -1 + + + model._cron_end_of_day_report() + + + diff --git a/delivery_carrier_label_gls/data/product_packaging.xml b/delivery_carrier_label_gls/data/product_packaging.xml new file mode 100644 index 0000000000..1cc9f4bb86 --- /dev/null +++ b/delivery_carrier_label_gls/data/product_packaging.xml @@ -0,0 +1,23 @@ + + + + + GLS: Parcel + gls + PARCEL + 40 + + + GLS: Express + gls + EXPRESS + 40 + + + GLS: Freight + gls + FREIGHT + 40 + + diff --git a/delivery_carrier_label_gls/data/product_product.xml b/delivery_carrier_label_gls/data/product_product.xml new file mode 100644 index 0000000000..b47ccdd318 --- /dev/null +++ b/delivery_carrier_label_gls/data/product_product.xml @@ -0,0 +1,16 @@ + + + + + SHIP_GLS + service + + Shipping cost - GLS + + + diff --git a/delivery_carrier_label_gls/hooks.py b/delivery_carrier_label_gls/hooks.py new file mode 100644 index 0000000000..b7b878ec27 --- /dev/null +++ b/delivery_carrier_label_gls/hooks.py @@ -0,0 +1,11 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import SUPERUSER_ID, api + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + rcs = env["res.config.settings"].create({"company_id": env.company.id}) + rcs.group_stock_tracking_lot = True + rcs.execute() diff --git a/delivery_carrier_label_gls/i18n/delivery_carrier_label_gls.pot b/delivery_carrier_label_gls/i18n/delivery_carrier_label_gls.pot new file mode 100644 index 0000000000..0e9e286989 --- /dev/null +++ b/delivery_carrier_label_gls/i18n/delivery_carrier_label_gls.pot @@ -0,0 +1,543 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_carrier_label_gls +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-16 16:53+0000\n" +"PO-Revision-Date: 2023-01-16 16:53+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: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Connector" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "City" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Country" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Date and time of reception:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Driver's name:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Internal Reference" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Name" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Number of Packages" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Package Reference" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Parcels" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Partner:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Signature:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Total Weight (kg)" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Tracking" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Vehicle registration plate:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Weight" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "Cancel" +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_quant_package.py:0 +#, python-format +msgid "Cannot open tracking URL for this carrier." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__carrier_id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__carrier_id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_package_type__package_carrier_type +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__carrier_id +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_search_view +msgid "Carrier" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_carrier_account_search +msgid "Carrier Account" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_quant_package_form +msgid "Carrier Tracking" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__carrier_ids +msgid "Carriers" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "Compute shipping costs and ship with GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_res_partner +msgid "Contact" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_contact_id +msgid "Contact id for GLS International transportation (T8914)" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:product.product,name:delivery_carrier_label_gls.product_product_gls +#: model:product.template,name:delivery_carrier_label_gls.product_product_gls_product_template +msgid "Coûts de livraison - GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__create_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__create_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__create_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__create_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__d_200 +msgid "D200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__datamax +msgid "Datamax" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__date +msgid "Date" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__display_name +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__display_name +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "Documentation" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_report_gls +msgid "End of the day GLS report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_sale_order__gls_parcel_shop +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_stock_picking__gls_parcel_shop +msgid "Fill this for a delivery to a ParcelShop." +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_picking.py:0 +#, python-format +msgid "For GLS every operation should be put in a pack." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:delivery.carrier,name:delivery_carrier_label_gls.delivery_carrier_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__delivery_type__gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__stock_package_type__package_carrier_type__gls +msgid "GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_client_gls +msgid "GLS API client" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Carrier Account" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "GLS Contact ID:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.report,name:delivery_carrier_label_gls.report_delivery_report_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__report_id +msgid "GLS Delivery Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "GLS Delivery Report:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__gls_package_ref +msgid "GLS Identifier" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_picking__gls_package_ref +msgid "GLS Package Identifiers" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_sale_order__gls_parcel_shop +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_picking__gls_parcel_shop +msgid "GLS Parcel Shop Identifier" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:res.groups,name:delivery_carrier_label_gls.group_gls_report_manager +msgid "GLS Report Manager" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Shipping Methods" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_delivery_carrier_form +msgid "GLS configuration" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.act_window,name:delivery_carrier_label_gls.delivery_report_gls_act_window +#: model:ir.ui.menu,name:delivery_carrier_label_gls.delivery_report_gls_menu +msgid "GLS: Delivery Reports" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.server,name:delivery_carrier_label_gls.cron_end_of_day_report_ir_actions_server +#: model:ir.cron,cron_name:delivery_carrier_label_gls.cron_end_of_day_report +#: model:ir.cron,name:delivery_carrier_label_gls.cron_end_of_day_report +msgid "GLS: End of Day Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.act_window,name:delivery_carrier_label_gls.delivery_report_gls_wizard_act_window +#: model:ir.ui.menu,name:delivery_carrier_label_gls.delivery_report_gls_wizard_menu +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "GLS: Get End of Day Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url_tracking +msgid "Gls Url Tracking" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_search_view +msgid "Group By" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__id +msgid "ID" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_return_partner_id +msgid "If set, this partner's address will be used on the return label." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__intermec +msgid "Intermec" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_contact_id +msgid "International" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_label_template +msgid "Label Template" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_label_format +msgid "Label format" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls____last_update +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls____last_update +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__write_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__write_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__write_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__write_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py:0 +#, python-format +msgid "Only GLS supports delivery reports." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__pdf +msgid "PDF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i +msgid "PF4I" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i_200 +msgid "PF4I200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i_300 +msgid "PF4I300" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_8_d_200 +msgid "PF8D200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_quant_package +msgid "Packages" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__gls_picking_id +msgid "Picking used in GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__package_ids +msgid "Pickings" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__delivery_type +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_sale_order__delivery_type +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_carrier_account_search +msgid "Provider" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_form_view +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__report_datetime +msgid "Report Date and Time" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_return_partner_id +msgid "Return Address" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_url_tracking +msgid "Root URL for parcel tracking. Needs a %s for the tracking reference." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url +msgid "Service Url" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_shipping_label +msgid "Shipping Label" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_carrier +msgid "Shipping Methods" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_package_type +msgid "Stock package type" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__t_200_bf +msgid "T200BF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__t_300_bf +msgid "T300BF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url_test +msgid "Test Service Url" +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py:0 +#, python-format +msgid "There are no new shipments." +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_client_gls.py:0 +#, python-format +msgid "This is not a GLS client." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__toshiba +msgid "Toshiba" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_quant_package_form +msgid "Track your parcel in carrier information system" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:product.product,uom_name:delivery_carrier_label_gls.product_product_gls +#: model:product.template,uom_name:delivery_carrier_label_gls.product_product_gls_product_template +msgid "Units" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_report_gls_wizard +msgid "Wizard to get the End of Day Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_quant_package.py:0 +#, python-format +msgid "You cannot cancel a pack that wasn't sent to GLS." +msgstr "" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_client_gls.py:0 +#, python-format +msgid "Your GLS Contact ID is not configured." +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__zpl_200 +msgid "ZPL200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__zpl_300 +msgid "ZPL300" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__zebra +msgid "Zebra" +msgstr "" diff --git a/delivery_carrier_label_gls/i18n/fr.po b/delivery_carrier_label_gls/i18n/fr.po new file mode 100644 index 0000000000..45e5ba04b6 --- /dev/null +++ b/delivery_carrier_label_gls/i18n/fr.po @@ -0,0 +1,543 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_carrier_label_gls +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-16 16:53+0000\n" +"PO-Revision-Date: 2023-01-16 16:53+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: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Connector" +msgstr "Connecteur GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "City" +msgstr "Ville" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Country" +msgstr "Pays" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Date and time of reception:" +msgstr "Date et heure de réception:" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Driver's name:" +msgstr "Nom du chauffeur:" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Internal Reference" +msgstr "Réference interne" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Name" +msgstr "Nom" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Number of Packages" +msgstr "Nombre de paquets" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Package Reference" +msgstr "Référence de paquet" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Parcels" +msgstr "Colis" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Partner:" +msgstr "Partenaire:" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Signature:" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Total Weight (kg)" +msgstr "Poids total (kg)" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Tracking" +msgstr "Traçabilité" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Vehicle registration plate:" +msgstr "Plaque d'immatriculation:" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "Weight" +msgstr "Poids" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "Cancel" +msgstr "Annuler" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_quant_package.py:0 +#, python-format +msgid "Cannot open tracking URL for this carrier." +msgstr "Impossible d'ouvrir l'URL de tracking pour ce transporteur." + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__carrier_id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__carrier_id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_package_type__package_carrier_type +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__carrier_id +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_search_view +msgid "Carrier" +msgstr "Transporteur" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_carrier_account_search +msgid "Carrier Account" +msgstr "Compte du transporteur" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_quant_package_form +msgid "Carrier Tracking" +msgstr "Traçabilité du transporteur" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__carrier_ids +msgid "Carriers" +msgstr "Transporteurs" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "Compute shipping costs and ship with GLS" +msgstr "Calculer les coûts de livraison et livrer avec GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_res_partner +msgid "Contact" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_contact_id +msgid "Contact id for GLS International transportation (T8914)" +msgstr "Identifiant pour le transport international chez GLS (T8914)" + +#. module: delivery_carrier_label_gls +#: model:product.product,name:delivery_carrier_label_gls.product_product_gls +#: model:product.template,name:delivery_carrier_label_gls.product_product_gls_product_template +msgid "Coûts de livraison - GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__create_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__create_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__create_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__create_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__d_200 +msgid "D200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__datamax +msgid "Datamax" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__date +msgid "Date" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__display_name +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__display_name +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "Documentation" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_report_gls +msgid "End of the day GLS report" +msgstr "Rapport GLS de fin de journée" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_sale_order__gls_parcel_shop +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_stock_picking__gls_parcel_shop +msgid "Fill this for a delivery to a ParcelShop." +msgstr "Complétez ceci pour une livraison chez un Parcel Shop" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_picking.py:0 +#, python-format +msgid "For GLS every operation should be put in a pack." +msgstr "Pour GLS, chaque opération doit être mise en paquet." + +#. module: delivery_carrier_label_gls +#: model:delivery.carrier,name:delivery_carrier_label_gls.delivery_carrier_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__delivery_type__gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__stock_package_type__package_carrier_type__gls +msgid "GLS" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_client_gls +msgid "GLS API client" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Carrier Account" +msgstr "Compte transporteur GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "GLS Contact ID:" +msgstr "Identifiant GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.report,name:delivery_carrier_label_gls.report_delivery_report_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__report_id +msgid "GLS Delivery Report" +msgstr "Rapport de livraison GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.report_delivery_report_gls_document +msgid "GLS Delivery Report:" +msgstr "Rapport de livraison GLS:" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__gls_package_ref +msgid "GLS Identifier" +msgstr "Référence GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_picking__gls_package_ref +msgid "GLS Package Identifiers" +msgstr "Identifiants de paquet GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_sale_order__gls_parcel_shop +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_picking__gls_parcel_shop +msgid "GLS Parcel Shop Identifier" +msgstr "Identifiant de colis GLS" + +#. module: delivery_carrier_label_gls +#: model:res.groups,name:delivery_carrier_label_gls.group_gls_report_manager +msgid "GLS Report Manager" +msgstr "Gestionnaire de rapport GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.res_config_settings_view_form_stock +msgid "GLS Shipping Methods" +msgstr "Modes de livraison GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_delivery_carrier_form +msgid "GLS configuration" +msgstr "Configuration GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.act_window,name:delivery_carrier_label_gls.delivery_report_gls_act_window +#: model:ir.ui.menu,name:delivery_carrier_label_gls.delivery_report_gls_menu +msgid "GLS: Delivery Reports" +msgstr "GLS: Rapports de livraison" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.server,name:delivery_carrier_label_gls.cron_end_of_day_report_ir_actions_server +#: model:ir.cron,cron_name:delivery_carrier_label_gls.cron_end_of_day_report +#: model:ir.cron,name:delivery_carrier_label_gls.cron_end_of_day_report +msgid "GLS: End of Day Report" +msgstr "GLS: Rapport de fin de journée" + +#. module: delivery_carrier_label_gls +#: model:ir.actions.act_window,name:delivery_carrier_label_gls.delivery_report_gls_wizard_act_window +#: model:ir.ui.menu,name:delivery_carrier_label_gls.delivery_report_gls_wizard_menu +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "GLS: Get End of Day Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url_tracking +msgid "Gls Url Tracking" +msgstr "URL de tracking GLS" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_search_view +msgid "Group By" +msgstr "Groupé par" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__id +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__id +msgid "ID" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_return_partner_id +msgid "If set, this partner's address will be used on the return label." +msgstr "Si indiqué, l'adresse de ce partenaire sera utilisée sur l'étiquette de retour." + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__intermec +msgid "Intermec" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_contact_id +msgid "International" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_label_template +msgid "Label Template" +msgstr "Gabarit d'étiquette" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_label_format +msgid "Label format" +msgstr "Format d'étiquette" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls____last_update +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls____last_update +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__write_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__write_uid +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_client_gls__write_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__write_date +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls_wizard__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py:0 +#, python-format +msgid "Only GLS supports delivery reports." +msgstr "Seul GLS supporte les rapports de livraison." + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__pdf +msgid "PDF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i +msgid "PF4I" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i_200 +msgid "PF4I200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_4_i_300 +msgid "PF4I300" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__pf_8_d_200 +msgid "PF8D200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_quant_package +msgid "Packages" +msgstr "Paquets" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_stock_quant_package__gls_picking_id +msgid "Picking used in GLS" +msgstr "Transfert utilisé par GLS" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__package_ids +msgid "Pickings" +msgstr "Transferts" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__delivery_type +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_sale_order__delivery_type +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_carrier_account_search +msgid "Provider" +msgstr "Fournisseur" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_form_view +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.delivery_report_gls_wizard_form_view +msgid "Report" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_report_gls__report_datetime +msgid "Report Date and Time" +msgstr "Date et heure de rapport" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_return_partner_id +msgid "Return Address" +msgstr "Adresse de retour" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,help:delivery_carrier_label_gls.field_delivery_carrier__gls_url_tracking +msgid "Root URL for parcel tracking. Needs a %s for the tracking reference." +msgstr "URL de base pour la traçabilité des colis. Nécessite une %s pour la référence de tracking." + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_sale_order +msgid "Sales Order" +msgstr "Bons de commande" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url +msgid "Service Url" +msgstr "URL du service" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_shipping_label +msgid "Shipping Label" +msgstr "Étiquette de livraison" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_carrier +msgid "Shipping Methods" +msgstr "Modes de livraison" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_package_type +msgid "Stock package type" +msgstr "Type de paquet" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__t_200_bf +msgid "T200BF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__t_300_bf +msgid "T300BF" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields,field_description:delivery_carrier_label_gls.field_delivery_carrier__gls_url_test +msgid "Test Service Url" +msgstr "URL du service de test" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py:0 +#, python-format +msgid "There are no new shipments." +msgstr "Il n'y a pas d'expédition." + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_client_gls.py:0 +#, python-format +msgid "This is not a GLS client." +msgstr "Ce n'est pas un client GLS." + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__toshiba +msgid "Toshiba" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model_terms:ir.ui.view,arch_db:delivery_carrier_label_gls.view_quant_package_form +msgid "Track your parcel in carrier information system" +msgstr "Traquez votre colis dans le système du transporteur" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: delivery_carrier_label_gls +#: model:product.product,uom_name:delivery_carrier_label_gls.product_product_gls +#: model:product.template,uom_name:delivery_carrier_label_gls.product_product_gls_product_template +msgid "Units" +msgstr "Unités" + +#. module: delivery_carrier_label_gls +#: model:ir.model,name:delivery_carrier_label_gls.model_delivery_report_gls_wizard +msgid "Wizard to get the End of Day Report" +msgstr "Assistant d'obtention du rapport de fin de journée" + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/models/stock_quant_package.py:0 +#, python-format +msgid "You cannot cancel a pack that wasn't sent to GLS." +msgstr "Vous ne pouvez pas annuler un paquet qui n'a pas été envoyé par GLS." + +#. module: delivery_carrier_label_gls +#. odoo-python +#: code:addons/delivery_carrier_label_gls/wizards/delivery_client_gls.py:0 +#, python-format +msgid "Your GLS Contact ID is not configured." +msgstr "Votre identifiant GLS n'est pas configuré." + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__zpl_200 +msgid "ZPL200" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_template__zpl_300 +msgid "ZPL300" +msgstr "" + +#. module: delivery_carrier_label_gls +#: model:ir.model.fields.selection,name:delivery_carrier_label_gls.selection__delivery_carrier__gls_label_format__zebra +msgid "Zebra" +msgstr "" diff --git a/delivery_carrier_label_gls/migrations/16.0.1.0.0/post-migrate.py b/delivery_carrier_label_gls/migrations/16.0.1.0.0/post-migrate.py new file mode 100644 index 0000000000..8b53fe2e33 --- /dev/null +++ b/delivery_carrier_label_gls/migrations/16.0.1.0.0/post-migrate.py @@ -0,0 +1,45 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging + +from openupgradelib import openupgrade + +from odoo.tools import sql + +_logger = logging.getLogger(__name__) + + +def _update_from_res_company_to_delivery_carrier(env): + _logger.info("update from res_company to delivery_carrier") + company_id = env.ref("base.main_company").id + if sql.column_exists(env.cr, "res_company", "gls_contact_id"): + # update value in delivery.carrier from res_company gls_contact_id + env.cr.execute( + f"""UPDATE delivery_carrier + SET gls_contact_id = + (SELECT gls_contact_id FROM res_company WHERE id={company_id}) + WHERE delivery_type like 'gls'""" + ) + # remove gls_contact_id from res_company + env.cr.execute("ALTER TABLE res_company DROP COLUMN gls_contact_id") + + +def _update_from_delivery_carrier_to_carrier_account(env): + _logger.info("update value in carrier.account from delivery.carrier") + for column in ("gls_login", "gls_password"): + if sql.column_exists(env.cr, "delivery.carrier", column): + # update column value in carrier_account + env.cr.execute( + f"""UPDATE carrier_account + SET {column} = + (SELECT {column} FROM delivery_carrier WHERE delivery_type like 'gls') + WHERE delivery_type like 'gls'""" + ) + # remove column from delivery.carrier + env.cr.execute(f"ALTER TABLE delivery_carrier DROP COLUMN {column}") + + +@openupgrade.migrate() +def migrate(env, version): + _update_from_res_company_to_delivery_carrier(env) + _update_from_delivery_carrier_to_carrier_account(env) diff --git a/delivery_carrier_label_gls/models/__init__.py b/delivery_carrier_label_gls/models/__init__.py new file mode 100644 index 0000000000..3befdf2bf2 --- /dev/null +++ b/delivery_carrier_label_gls/models/__init__.py @@ -0,0 +1,8 @@ +from . import delivery_carrier +from . import delivery_report_gls +from . import product_packaging +from . import res_partner +from . import sale_order +from . import shipping_label +from . import stock_picking +from . import stock_quant_package diff --git a/delivery_carrier_label_gls/models/delivery_carrier.py b/delivery_carrier_label_gls/models/delivery_carrier.py new file mode 100644 index 0000000000..131e1abe1f --- /dev/null +++ b/delivery_carrier_label_gls/models/delivery_carrier.py @@ -0,0 +1,161 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import json + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + delivery_type = fields.Selection( + selection_add=[("gls", "GLS")], + ondelete={ + "gls": lambda recs: recs.write({"delivery_type": "fixed", "fixed_price": 0}) + }, + ) + + # Backport + carrier_account_id = fields.Many2one( + "carrier.account", + string="Account", + company_dependent=True, + domain="[('delivery_type', '=', delivery_type)]", + ) + + gls_contact_id = fields.Char( + string="International", + size=10, + help="Contact id for GLS International transportation (T8914)", + ) + gls_url = fields.Char(string="Service Url") + gls_url_test = fields.Char(string="Test Service Url") + gls_url_tracking = fields.Char( + help="Root URL for parcel tracking. Needs a %s for the tracking reference." + ) + gls_label_format = fields.Selection( + string="Label format", + selection=[ + ("pdf", "PDF"), + ("zebra", "Zebra"), + ("intermec", "Intermec"), + ("datamax", "Datamax"), + ("toshiba", "Toshiba"), + ], + default="pdf", + ) + gls_label_template = fields.Selection( + string="Label Template", + selection=[ + ("D_200", "D200"), + ("PF_4_I", "PF4I"), + ("PF_4_I_200", "PF4I200"), + ("PF_4_I_300", "PF4I300"), + ("PF_8_D_200", "PF8D200"), + ("T_200_BF", "T200BF"), + ("T_300_BF", "T300BF"), + ("ZPL_200", "ZPL200"), + ("ZPL_300", "ZPL300"), + ], + default=False, + ) + gls_return_partner_id = fields.Many2one( + "res.partner", + string="Return Address", + help="If set, this partner's address will be used on the return label.", + ) + + @api.constrains("gls_return_partner_id") + def _check_gls_return_partner_id(self): + records_to_check = self.filtered( + lambda c: c.delivery_type == "gls" and c.gls_return_partner_id + ) + for carrier in records_to_check: + carrier.gls_return_partner_id._gls_prepare_address() + + @api.constrains( + "delivery_type", + "gls_contact_id", + "gls_url_tracking", + "gls_label_format", + "carrier_account_id", + ) + def _check_gls_fields(self): + gls_field_names = [ + "gls_contact_id", + "gls_url_tracking", + "gls_label_format", + "carrier_account_id", + ] + gls_records = self.filtered(lambda c: c.delivery_type == "gls") + for rec in gls_records: + for field_name in gls_field_names: + value = rec[field_name] + if not value: + field = rec._fields[field_name] + description_string = field._description_string(self.env) + raise ValidationError( + _( + f"The GLS field '{description_string}' is required for " + f"carrier {rec.name}" + ) + ) + + for rec in self - gls_records: + if any( + rec[f] + for f in gls_field_names + if f not in ("carrier_account_id", "gls_label_format") + ): + raise ValidationError( + _(f"Incorrect GLS parameters set on carrier {rec.name}.") + ) + + @api.constrains("delivery_type", "prod_environment", "gls_url", "gls_url_test") + def _check_gls_url(self): + for rec in self.filtered(lambda c: c.delivery_type == "gls"): + if not rec.prod_environment and not rec.gls_url_test: + raise ValidationError( + _("The GLS field 'Test Service Url' is required in test mode") + ) + if rec.prod_environment and not rec.gls_url: + raise ValidationError( + _("The GLS field 'Service Url' is required in non test mode") + ) + + def gls_send_shipping(self, picking): + tracking_number = picking.gls_send_shipping(self) + return [{"exact_price": False, "tracking_number": tracking_number}] + + @api.model + def gls_tracking_url(self, tracking_number): + return self.gls_url_tracking % tracking_number + + def gls_get_tracking_link(self, pickings): + links = [] + for picking in pickings: + for link in picking.mapped("package_ids.parcel_tracking"): + if link: + links.append(self.gls_tracking_url(link)) + # Prevent wrong json string coercion (' instead of ") in case of multiple links + if len(links) == 1: + return links[0] + return json.dumps(links) + + def gls_cancel_shipment(self, pickings): + if pickings.package_ids.report_id: + msg = _("Packages cannot be canceled after the End of Day report.") + raise ValidationError(msg) + for picking in pickings: + picking.gls_cancel_shipment() + + def _get_gls_client(self): + """Return a GLS connection client using this carrier configuration.""" + # the client checks all parameters, so nothing needed here. + self.ensure_one() + return self.env["delivery.client.gls"].create({"carrier_id": self.id}) diff --git a/delivery_carrier_label_gls/models/delivery_report_gls.py b/delivery_carrier_label_gls/models/delivery_report_gls.py new file mode 100644 index 0000000000..3e2cb2dee8 --- /dev/null +++ b/delivery_carrier_label_gls/models/delivery_report_gls.py @@ -0,0 +1,28 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class DeliveryReport(models.Model): + _name = "delivery.report.gls" + _description = "End of the day GLS report" + + report_datetime = fields.Datetime( + string="Report Date and Time", required=True, readonly=True + ) + carrier_id = fields.Many2one( + "delivery.carrier", string="Carrier", required=True, readonly=True + ) + package_ids = fields.One2many( + "stock.quant.package", + "report_id", + "Packages", + readonly=True, + ) + + @api.depends("carrier_id", "report_datetime") + def _compute_display_name(self): + for record in self: + carrier_name = record.carrier_id.name or "GLS" + record.display_name = "{}/{}".format(carrier_name, record.report_datetime) diff --git a/delivery_carrier_label_gls/models/product_packaging.py b/delivery_carrier_label_gls/models/product_packaging.py new file mode 100644 index 0000000000..a32be3211a --- /dev/null +++ b/delivery_carrier_label_gls/models/product_packaging.py @@ -0,0 +1,13 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductPackaging(models.Model): + _inherit = "product.packaging" + + package_carrier_type = fields.Selection( + selection_add=[("gls", "GLS")], + ondelete={"gls": lambda recs: recs.write({"package_carrier_type": "none"})}, + ) diff --git a/delivery_carrier_label_gls/models/res_partner.py b/delivery_carrier_label_gls/models/res_partner.py new file mode 100644 index 0000000000..b15b1c2a46 --- /dev/null +++ b/delivery_carrier_label_gls/models/res_partner.py @@ -0,0 +1,86 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import re +from collections import defaultdict + +from odoo import _, models +from odoo.exceptions import ValidationError + +GLS_MAX_LENGTHS = defaultdict(lambda: 40) +GLS_MAX_LENGTHS.update(eMail=80, ZIPCode=10, CountryCode=2) + + +def keep_alphanumeric(s): + return re.sub(r"[\W_]", "", s) + + +def keep_digits(s): + return re.sub("[^0-9]", "", s) + + +def len_is(n): + return lambda s: len(s) == n + + +country_normalize_validate = { + "BE": (keep_digits, len_is(4)), + "LU": (keep_digits, len_is(4)), + "FR": (keep_digits, len_is(5)), + "DE": (keep_digits, len_is(5)), +} +COUNTRY_NORMALIZE_VALIDATE = defaultdict(lambda: (keep_alphanumeric, lambda s: bool(s))) +COUNTRY_NORMALIZE_VALIDATE.update(country_normalize_validate) + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _gls_prepare_address(self): + self.ensure_one() + address_payload = {} + mapping = { + "name": "Name1", + "street": "Street", + "street2": "StreetNumber", + "city": "City", + "email": "eMail", + "zip": "ZIPCode", + "phone": "FixedLinePhonenumber", + "mobile": "MobilePhoneNumber", + "country_id.code": "CountryCode", + "state_id.name": "Province", + } + mapping_optional = {"phone", "mobile", "street2", "state_id.name"} + for key in mapping: + if "." in key: + value = self.mapped(key) + value = value[0] if value else value + else: + value = self[key] + if not value and key not in mapping_optional: + msg = _(f"Missing required parameter {key} on partner {self.name}") + raise ValidationError(msg) + if value: + gls_key = mapping[key] + address_payload[gls_key] = value[: GLS_MAX_LENGTHS[gls_key]] + if address_payload["CountryCode"] == "MC": # for GLS Monaco is France + address_payload["CountryCode"] = "FR" # read commit for explanation + address_payload["ZIPCode"] = self._get_iso_zip( + validate_raises=True, country_code=address_payload["CountryCode"] + ) + return address_payload + + def _get_iso_zip(self, validate_raises=False, country_code=None): + """GLS does not support common ways to format the zip, and will raise. + Typically in Luxembourg they are written L-4280 or in certain countries + they might add some space or dash for readability. + """ + self.ensure_one() + country_code = country_code or self.country_id.code + normalize, validate = COUNTRY_NORMALIZE_VALIDATE[country_code] + iso_zip = normalize(self.zip) + if validate_raises and not validate(iso_zip): + msg = _(f"Not a valid ZIP code for country {country_code}: {self.zip}") + raise ValidationError(msg) + return iso_zip diff --git a/delivery_carrier_label_gls/models/sale_order.py b/delivery_carrier_label_gls/models/sale_order.py new file mode 100644 index 0000000000..28ef0297ad --- /dev/null +++ b/delivery_carrier_label_gls/models/sale_order.py @@ -0,0 +1,21 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + gls_parcel_shop = fields.Char( + "GLS Parcel Shop Identifier", + help="Fill this for a delivery to a ParcelShop.", + ) + delivery_type = fields.Selection(related="carrier_id.delivery_type", readonly=True) + + def action_confirm(self): + res = super().action_confirm() + for so in self: + if so.gls_parcel_shop: + so.picking_ids.update({"gls_parcel_shop": so.gls_parcel_shop}) + return res diff --git a/delivery_carrier_label_gls/models/shipping_label.py b/delivery_carrier_label_gls/models/shipping_label.py new file mode 100644 index 0000000000..befe51bbbc --- /dev/null +++ b/delivery_carrier_label_gls/models/shipping_label.py @@ -0,0 +1,19 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class ShippingLabel(models.Model): + + _inherit = "shipping.label" + + @api.model + def _get_file_type_selection(self): + """Adds GLS supported file types.""" + res = super()._get_file_type_selection() + existing_formats = {t[0] for t in res} + gls_formats = self.env["delivery.carrier"]._fields["gls_label_format"].selection + for key, name in gls_formats: + if key not in existing_formats: + res.append((key, name)) + return res diff --git a/delivery_carrier_label_gls/models/stock_picking.py b/delivery_carrier_label_gls/models/stock_picking.py new file mode 100644 index 0000000000..fad0794eed --- /dev/null +++ b/delivery_carrier_label_gls/models/stock_picking.py @@ -0,0 +1,65 @@ +# Copyright 2021 ACSONE SA/NV +# © 2015 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +def joincat(s1, s2): + """Simply append a ref to a string, or get the string itself if s1 is falsy.""" + return s2 if not s1 else ",".join((s1, s2)) + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + gls_parcel_shop = fields.Char( + "GLS Parcel Shop Identifier", + help="Fill this for a delivery to a ParcelShop.", + ) + gls_package_ref = fields.Char("GLS Package Identifiers", readonly=True, copy=False) + + def _check_is_everything_packaged(self): + for picking in self: + if not all(o.result_package_id for o in picking.move_lines.move_line_ids): + msg = _("For GLS every operation should be put in a pack.") + raise ValidationError(msg) + + def button_validate(self): + """Check that each GLS picking has been completely sent.""" + gls_pickings = self.filtered(lambda p: p.delivery_type == "gls") + gls_pickings._check_is_everything_packaged() + return super().button_validate() + + def gls_send_shipping(self, delivery_carrier=False): + for package in self.move_lines.move_line_ids.result_package_id: + if not package.parcel_tracking: + self.gls_send_shipping_package(package) + return self.carrier_tracking_ref + + def gls_send_shipping_package(self, package): + self.ensure_one() + package.carrier_id = self.carrier_id + package.gls_picking_id = self + package.gls_send_shipping_package() + self.gls_package_ref = joincat(self.gls_package_ref, package.gls_package_ref) + tracking = package.parcel_tracking + self.carrier_tracking_ref = joincat(self.carrier_tracking_ref, tracking) + return self.carrier_tracking_ref + + def generate_labels(self, package_ids=None): + """Alias to gls_send_shipping, for compatibility with base_delivery.""" + return self.gls_send_shipping() + + def gls_cancel_shipment(self): + for package in self.move_line_ids.result_package_id: + if package.parcel_tracking: + package.gls_cancel_shipment() + + self.carrier_tracking_ref = False + self.gls_package_ref = False diff --git a/delivery_carrier_label_gls/models/stock_quant_package.py b/delivery_carrier_label_gls/models/stock_quant_package.py new file mode 100644 index 0000000000..b0b9a08d79 --- /dev/null +++ b/delivery_carrier_label_gls/models/stock_quant_package.py @@ -0,0 +1,123 @@ +# Copyright 2021 ACSONE SA/NV +# © 2015 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class StockQuantPackage(models.Model): + _inherit = "stock.quant.package" + + carrier_id = fields.Many2one("delivery.carrier") + report_id = fields.Many2one( + "delivery.report.gls", "GLS Delivery Report", readonly=True, copy=False + ) + gls_package_ref = fields.Char("GLS Identifier", readonly=True, copy=False) + gls_picking_id = fields.Many2one( + comodel_name="stock.picking", + string="Picking used in GLS", + readonly=True, + copy=False, + ) + + def gls_cancel_shipment(self): + self.ensure_one() + if not self.parcel_tracking: + raise ValidationError( + _("You cannot cancel a pack that wasn't sent to GLS.") + ) + client = self.carrier_id._get_gls_client() + client.cancel_parcel(self.parcel_tracking) + self.parcel_tracking = False + self.gls_package_ref = False + label = self.env["shipping.label"].search([("package_id", "=", self.id)]) + label.attachment_id.unlink() + label.unlink() + + def _get_carrier_tracking_url(self): + url = None + if self.carrier_id.delivery_type == "gls" and self.parcel_tracking: + url = self.carrier_id.gls_tracking_url(self.parcel_tracking) + return url + + def open_tracking_url(self): + self.ensure_one() + url = self._get_carrier_tracking_url() + if not url: + raise UserError(_("Cannot open tracking URL for this carrier.")) + return {"type": "ir.actions.act_url", "url": url, "target": "new"} + + def gls_validate_package(self): + self.ensure_one() + package_name = self.name or self.id + allowed_product_types = ["PARCEL", "EXPRESS", "FREIGHT"] + if self.packaging_id.shipper_package_code not in allowed_product_types: + msg = _( + f"The GLS package code for package {package_name} should be " + f"in {allowed_product_types}." + ) + raise ValidationError(msg) + if not self.gls_picking_id: + msg = _(f"The GLS picking is missing on package {package_name}.") + raise ValidationError(msg) + if not self.shipping_weight: + msg = _(f"The shipping weight is missing on package {package_name}.") + raise ValidationError(msg) + + def _gls_prepare_shipment(self): + weight = max(self.shipping_weight, 0.1) # GLS API requirement + reference = (self.gls_picking_id.name or f"PICKING{self.id}")[:40] + partner = self.gls_picking_id.partner_id + return { + "Product": self.packaging_id.shipper_package_code, + "Consignee": {"Address": partner._gls_prepare_address()}, + "ShipmentUnit": [{"Weight": "{:05.2f}".format(weight)}], + "ShipmentReference": [reference], + "Service": self._gls_prepare_package_service(), + } + + def gls_send_shipping_package(self): + self.ensure_one() + self.gls_validate_package() + client = self.carrier_id._get_gls_client() + response = client.create_parcel({"Shipment": self._gls_prepare_shipment()}) + + assert len(response["CreatedShipment"]["ParcelData"]) == 1 # :-/ + parcel_data = response["CreatedShipment"]["ParcelData"][0] + tracking = parcel_data["TrackID"] + # avoid paying for lost API calls + self.env.cr.postrollback.add(lambda: client.cancel_parcel(tracking)) + self.parcel_tracking = tracking + self.gls_package_ref = parcel_data["ParcelNumber"] + label_content = response["CreatedShipment"]["PrintData"] + self._gls_label_package(label_content) + + @api.model + def _gls_prepare_package_service(self): + if not self.gls_picking_id.gls_parcel_shop: + service = [{"Service": {"ServiceName": "service_flexdelivery"}}] + else: + details = { + "ServiceName": "service_shopdelivery", + "ParcelShopID": self.gls_picking_id.gls_parcel_shop, + } + service = [{"ShopDelivery": details}] + return service + + @api.model + def _gls_label_package(self, label_data): + self.ensure_one() + extension = "pdf" if self.carrier_id.gls_label_format == "pdf" else "txt" + file_type = self.carrier_id.gls_label_format + name = (self.name or f"PACKAGE{self.id}") + "." + extension + vals_label = { + "package_id": self.id, + "datas": label_data[0]["Data"], + "res_id": self.gls_picking_id.id, + "res_model": self.gls_picking_id._name, + "file_type": file_type, + "type": "binary", + "name": name, + } + return self.env["shipping.label"].create(vals_label) diff --git a/delivery_carrier_label_gls/readme/CONFIGURE.rst b/delivery_carrier_label_gls/readme/CONFIGURE.rst new file mode 100644 index 0000000000..e04a57bfa2 --- /dev/null +++ b/delivery_carrier_label_gls/readme/CONFIGURE.rst @@ -0,0 +1,12 @@ +To start using GLS, you need to configure two types of settings in +*Inventory - Configuration- Delivery* or *Inventory - Configuration - Settings* +which leads to the right section in inventory global settings. +First you have the *Carrier Account* where you find account number +and password then you also have *Shipping Methods* with other GLS +parameters to configure such as contact ID, urls and return address. +These 2 types of settings use **"GLS"** as delivery type. +The contact ID corresponds to the sender which needs to be a contact in the +GLS database. This determines the default return address, as well as the billing. +You can also configure the tracking url that is used for each carrier. + +For client integration tests you need to fill your credentials in the tests/common.py. diff --git a/delivery_carrier_label_gls/readme/CONTRIBUTORS.rst b/delivery_carrier_label_gls/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..b692337099 --- /dev/null +++ b/delivery_carrier_label_gls/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* David Beal @ Akretion +* Nans Lefebvre +* Laurent Mignon +* Hughes Damry +* Florian Mounier diff --git a/delivery_carrier_label_gls/readme/DESCRIPTION.rst b/delivery_carrier_label_gls/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..d9d0ef0b04 --- /dev/null +++ b/delivery_carrier_label_gls/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +Integration with GLS shipping webservices. +Documentation: +http://gls-shipit.gls-group.eu/webservices/2_8_11/doxygen/WS-REST-API/index.html diff --git a/delivery_carrier_label_gls/readme/USAGE.rst b/delivery_carrier_label_gls/readme/USAGE.rst new file mode 100644 index 0000000000..4c012254c3 --- /dev/null +++ b/delivery_carrier_label_gls/readme/USAGE.rst @@ -0,0 +1,14 @@ +Create the packages on GLS which returns a tracking ID. +If there is any kind of mistake (address, weight), +it is possible to cancel it as long as it has not been scanned yet. +If the package is not cancelled, it is invoiced even if it never ships. + +When sending a picking, all products should be put in one or multiple packages. +These packages need to have a GLS packaging either Parcel, Express, or Freight. +These are already pre-configured in the module data. + +The end of day report should be printed when the delivery takes place. +At the last resort, this function should be called at the end of the day. +If it had already run, it would have no impact. +In case the delivery is delayed, the report should simply be kept for the +next day, and provided with the next report if there is one. diff --git a/delivery_carrier_label_gls/report/delivery_report_gls_view.xml b/delivery_carrier_label_gls/report/delivery_report_gls_view.xml new file mode 100644 index 0000000000..55dba02107 --- /dev/null +++ b/delivery_carrier_label_gls/report/delivery_report_gls_view.xml @@ -0,0 +1,19 @@ + + + + + delivery.report.gls + GLS Delivery Report + qweb-pdf + delivery_carrier_label_gls.report_delivery_report_gls_document + delivery_carrier_label_gls.report_delivery_report_gls + 'GLS Delivery Report' + + report + + + diff --git a/delivery_carrier_label_gls/report/report_delivery_report_gls.xml b/delivery_carrier_label_gls/report/report_delivery_report_gls.xml new file mode 100644 index 0000000000..e60a7cb9ec --- /dev/null +++ b/delivery_carrier_label_gls/report/report_delivery_report_gls.xml @@ -0,0 +1,133 @@ + + + + + + diff --git a/delivery_carrier_label_gls/security/groups.xml b/delivery_carrier_label_gls/security/groups.xml new file mode 100644 index 0000000000..cc96eea5a4 --- /dev/null +++ b/delivery_carrier_label_gls/security/groups.xml @@ -0,0 +1,11 @@ + + + + + GLS Report Manager + + + + + diff --git a/delivery_carrier_label_gls/security/ir.model.access.csv b/delivery_carrier_label_gls/security/ir.model.access.csv new file mode 100644 index 0000000000..de7b57bd63 --- /dev/null +++ b/delivery_carrier_label_gls/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_delivery_report_gls_usr,access_delivery_report_gls_usr,model_delivery_report_gls,base.group_user,1,0,0,0 +access_delivery_report_gls_mgr,access_delivery_report_gls_mgr,model_delivery_report_gls,group_gls_report_manager,1,1,1,1 +access_delivery_report_wizard_gls_usr,access_delivery_report_wizard_gls_usr,model_delivery_report_gls_wizard,base.group_user,1,0,0,0 +access_delivery_report_wizard_gls_mgr,access_delivery_report_wizard_gls_mgr,model_delivery_report_gls_wizard,group_gls_report_manager,1,1,1,1 +access_delivery_client_gls,access_delivery_client_gls,delivery_carrier_label_gls.model_delivery_client_gls,base.group_user,1,1,1,1 diff --git a/delivery_carrier_label_gls/static/description/icon.png b/delivery_carrier_label_gls/static/description/icon.png new file mode 100644 index 0000000000..ab59199497 Binary files /dev/null and b/delivery_carrier_label_gls/static/description/icon.png differ diff --git a/delivery_carrier_label_gls/static/description/index.html b/delivery_carrier_label_gls/static/description/index.html new file mode 100644 index 0000000000..b20681a615 --- /dev/null +++ b/delivery_carrier_label_gls/static/description/index.html @@ -0,0 +1,458 @@ + + + + + + +Delivery Carrier Label GLS + + + +
+

Delivery Carrier Label GLS

+ + +

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

+

Integration with GLS shipping webservices. +Documentation: +http://gls-shipit.gls-group.eu/webservices/2_8_11/doxygen/WS-REST-API/index.html

+

Table of contents

+ +
+

Configuration

+

To start using GLS, you need to configure two types of settings in +Inventory - Configuration- Delivery or Inventory - Configuration - Settings +which leads to the right section in inventory global settings. +First you have the Carrier Account where you find account number +and password then you also have Shipping Methods with other GLS +parameters to configure such as contact ID, urls and return address. +These 2 types of settings use “GLS” as delivery type. +The contact ID corresponds to the sender which needs to be a contact in the +GLS database. This determines the default return address, as well as the billing. +You can also configure the tracking url that is used for each carrier.

+

For client integration tests you need to fill your credentials in the tests/common.py.

+
+
+

Usage

+

Create the packages on GLS which returns a tracking ID. +If there is any kind of mistake (address, weight), +it is possible to cancel it as long as it has not been scanned yet. +If the package is not cancelled, it is invoiced even if it never ships.

+

When sending a picking, all products should be put in one or multiple packages. +These packages need to have a GLS packaging either Parcel, Express, or Freight. +These are already pre-configured in the module data.

+

The end of day report should be printed when the delivery takes place. +At the last resort, this function should be called at the end of the day. +If it had already run, it would have no impact. +In case the delivery is delayed, the report should simply be kept for the +next day, and provided with the next report if there is one.

+
+
+

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
  • +
  • Akretion
  • +
+
+
+

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/delivery-carrier project on GitHub.

+

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

+
+
+
+ + diff --git a/delivery_carrier_label_gls/static/img/gls.jpg b/delivery_carrier_label_gls/static/img/gls.jpg new file mode 100644 index 0000000000..90fa43fd83 Binary files /dev/null and b/delivery_carrier_label_gls/static/img/gls.jpg differ diff --git a/delivery_carrier_label_gls/tests/__init__.py b/delivery_carrier_label_gls/tests/__init__.py new file mode 100644 index 0000000000..61d806088f --- /dev/null +++ b/delivery_carrier_label_gls/tests/__init__.py @@ -0,0 +1,5 @@ +from . import common +from . import test_partner +from . import test_gls_client +from . import test_flow +from . import test_carrier_constraints diff --git a/delivery_carrier_label_gls/tests/common.py b/delivery_carrier_label_gls/tests/common.py new file mode 100644 index 0000000000..9195f532ed --- /dev/null +++ b/delivery_carrier_label_gls/tests/common.py @@ -0,0 +1,194 @@ +# Copyright 2021 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from contextlib import contextmanager +from unittest import mock + +from odoo.tests.common import SavepointCase + + +class TestGLS(SavepointCase): + @classmethod + def _get_gls_carrier_vals(cls): + return { + "name": "Test GLS", + "company_id": cls.company.id, + "delivery_type": "gls", + "prod_environment": False, + "carrier_account_id": cls.gls_carrier_account.id, + "gls_contact_id": "CONTACTID", # fill it for client integration test + "product_id": cls.gls_product.id, + "gls_url_test": "https://shipit-wbm-test01.gls-group.eu:8443/backend/rs/", + "gls_url_tracking": "https://gls-group.eu/EU/en/parcel-tracking/match=%s", + } + + @classmethod + def _get_gls_carrier_account_vals(cls): + return { + "name": "GLS test", + "company_id": cls.company.id, + "delivery_type": "gls", + "account": "GLS Account", # fill it for client integration test + "password": "GLS password", # fill it for client integration test + } + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.user.company_id + vals_gls_product = { + "default_code": "Code ship GLS", + "type": "service", + "sale_ok": False, + "name": "Name ship GLS", + } + cls.gls_product = cls.env["product.product"].create(vals_gls_product) + vals_gls_carrier_account = cls._get_gls_carrier_account_vals() + cls.gls_carrier_account = cls.env["carrier.account"].create( + vals_gls_carrier_account + ) + vals_gls_carrier = cls._get_gls_carrier_vals() + cls.gls_carrier = cls.env["delivery.carrier"].create(vals_gls_carrier) + cls.gls_client = cls.env["delivery.client.gls"].create( + {"carrier_id": cls.gls_carrier.id} + ) + vals_product = {"name": "product", "type": "product", "weight": 0.5} + cls.product = cls.env["product.product"].create(vals_product) + vals_partner = { + "name": "partner", + "city": "Ramillies", + "zip": "1367", + "email": "rd@odoo.con", + "street": "9, rue des bourlottes", + "country_id": cls.env.ref("base.be").id, + } + cls.partner = cls.env["res.partner"].create(vals_partner) + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.warehouse = cls.env["stock.warehouse"].search( + [("lot_stock_id", "=", cls.stock_location.id)], limit=1 + ) + cls.customer_location = cls.env.ref("stock.stock_location_customers") + cls.gls_parcel_shop = "0560005537" + vals_sale_order = { + "partner_id": cls.partner.id, + "gls_parcel_shop": cls.gls_parcel_shop, + "carrier_id": cls.gls_carrier.id, + } + cls.sale_order = cls.env["sale.order"].create(vals_sale_order) + vals_order_line = { + "name": "Line Description", + "order_id": cls.sale_order.id, + "product_id": cls.product.id, + } + cls.order_line = cls.env["sale.order.line"].create(vals_order_line) + + +@contextmanager +def mock_gls_client(mock_client=None): + mock_client = mock_client or MockGlsClient() + mock_path_prefix = "odoo.addons.delivery_carrier_label_gls.models" + mock_path_class = "delivery_carrier.DeliveryCarrier._get_gls_client" + mock_path = ".".join((mock_path_prefix, mock_path_class)) + with mock.patch(mock_path, return_value=mock_client) as mocked: + yield mocked, mock_client + + +class MockGlsClient(object): + def __init__(self): + self.report_counter = 0 + + def cancel_parcel(self, parcel_tracking): + return {"TrackID": parcel_tracking, "result": "CANCELLATION_PENDING"} + + def create_parcel(self, shipment_payload): + return { + "CreatedShipment": { + "CustomerID": "0560002709", + "GDPR": [ + "Information about Data Protection in GLS Group can be found at", + "gls-group.eu/dataprotection", + ], + "ParcelData": [ + { + "Barcodes": { + "Primary1D": "716649153647", + "Primary1DPrint": True, + "Primary2D": "ABE7100BE7311...", + "Secondary2D": "A|Dagbladen...", + }, + "HandlingInformation": "SHD S", + "ParcelNumber": "716649153647", + "RoutingInfo": { + "FinalLocationCode": "BE7311", + "HubLocation": "B73", + "InboundSortingFlag": "2", + "LastRoutingDate": "2021-06-12", + "Tour": "1101", + }, + "ServiceArea": { + "Service": [ + { + "Header": "ShopDeliveryService", + "Information": [ + { + "Name": "ConsigneeName", + "Value": "partner", + }, + {"Name": "Phone", "Value": ""}, + ], + } + ] + }, + "TrackID": "ZMZE0SRO", + } + ], + "PickupLocation": "BE7100", + "PrintData": [{"Data": "JVeryLongString==", "LabelFormat": "PDF"}], + "ShipmentReference": ["WH/OUT/00032"], + } + } + + def get_end_of_day_report(self, date=False): + self.report_counter += 1 + result_payload = { + "Shipments": [ + { + "Consignee": { + "Address": { + "City": "Ramillies", + "CountryCode": "BE", + "Name1": "partner", + "Street": "9, rue des bourlottes", + "ZIPCode": "1367", + "eMail": "rd@odoo.con", + } + }, + "Product": "PARCEL", + "Service": [{"Service": {"ServiceName": "service_shopdelivery"}}], + "ShipmentUnit": [ + { + "ParcelNumber": "716649153647", + "TrackID": "ZMZE0SRO", + "Weight": "0.5", + } + ], + "Shipper": { + "AlternativeShipperAddress": { + "City": "Drogenbos", + "CountryCode": "BE", + "FixedLinePhonenumber": "0032022222222", + "MobilePhoneNumber": "0032022222222", + "Name1": "Adress", + "Name2": "Depot 71", + "Street": "StreetName", + "StreetNumber": "42", + "ZIPCode": "4280", + "eMail": "email@example.com", + }, + "ContactID": "CONTACTID", + }, + "ShippingDate": "2021-06-14", + } + ] + } + return {} if self.report_counter > 1 else result_payload diff --git a/delivery_carrier_label_gls/tests/test_carrier_constraints.py b/delivery_carrier_label_gls/tests/test_carrier_constraints.py new file mode 100644 index 0000000000..c6a7661bd5 --- /dev/null +++ b/delivery_carrier_label_gls/tests/test_carrier_constraints.py @@ -0,0 +1,51 @@ +# Copyright 2021 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from .common import TestGLS + +gls_required_fields = [ + "gls_contact_id", + "gls_url_tracking", + "gls_url_test", +] + + +class TestCarrierConstraints(TestGLS): + def test_missing_field(self): + for field in gls_required_fields: + values = self._get_gls_carrier_vals() + values.pop(field) + with self.assertRaises(ValidationError): + self.env["delivery.carrier"].create(values) + # check that GLS label format can't be empty (there is a default on create) + values = self._get_gls_carrier_vals() + values["gls_label_format"] = False + with self.assertRaises(ValidationError): + self.env["delivery.carrier"].create(values) + + def test_url_prod(self): + values = self._get_gls_carrier_vals() + values["prod_environment"] = True # gls_url is already missing + with self.assertRaises(ValidationError): + self.env["delivery.carrier"].create(values) + + def test_cannot_modify_with_gls_parameters(self): + values = self._get_gls_carrier_vals() + gls_carrier = self.env["delivery.carrier"].create(values) + vals_fixed = {"delivery_type": "fixed"} + with self.assertRaises(ValidationError): + gls_carrier.write(vals_fixed) + + def test_cannot_create_with_gls_parameters(self): + values = self._get_gls_carrier_vals() + values["delivery_type"] = "fixed" + with self.assertRaises(ValidationError): + self.env["delivery.carrier"].create(values) + + # however this works, as expected + product = self.env["product.product"].create({"name": "brol"}) + self.env["delivery.carrier"].create( + {"name": "notGLS", "product_id": product.id} + ) diff --git a/delivery_carrier_label_gls/tests/test_flow.py b/delivery_carrier_label_gls/tests/test_flow.py new file mode 100644 index 0000000000..92f9ebd9ac --- /dev/null +++ b/delivery_carrier_label_gls/tests/test_flow.py @@ -0,0 +1,91 @@ +# Copyright 2021 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import UserError, ValidationError + +from .common import TestGLS, mock_gls_client + + +class TestGlsFlow(TestGLS): + def test_flow(self): + """We test the complete flow, since actions depend on each other. + To call cancel, you need to call create before; same with the report. + """ + self.env["stock.quant"]._update_available_quantity( + self.product, self.stock_location, 20.0 + ) + # when + self.order_line.product_uom_qty = 5 + self.sale_order.action_confirm() + # then + picking = self.sale_order.picking_ids + self.assertEqual(picking.carrier_id, self.gls_carrier) + self.assertEqual(picking.gls_parcel_shop, self.gls_parcel_shop) + self.assertEqual(picking.gls_package_ref, False) + + # given + # picking.force_assign() + + # everything should be put in a package before! + with self.assertRaises(UserError): + picking.button_validate() + + # given + # pack_operation = picking.pack_operation_ids + # pack_operation.qty_done = pack_operation.product_qty + picking.move_lines.write({"quantity_done": 5}) + # when + pack_action = picking.action_put_in_pack() + pack_action_ctx = pack_action["context"] + pack_action_model = pack_action["res_model"] + + # then: we land on "package details" action ('choose.delivery.package' wizard) + # and we instanciate it + self.assertEqual(pack_action["name"], "Package Details") + self.assertEqual(pack_action_model, "choose.delivery.package") + pack_wiz = ( + self.env["choose.delivery.package"] + .with_context(**pack_action_ctx) + .create({}) + ) + + # given + parcel_xmlid = "delivery_carrier_label_gls.product_packaging_gls_parcel" + packaging_parcel = self.env.ref(parcel_xmlid) + pack_wiz.delivery_packaging_id = packaging_parcel + self.assertNotEqual(pack_wiz.shipping_weight, 0.0) + pack_wiz.action_put_in_pack() + + # when + with mock_gls_client(): + picking.button_validate() + package = picking.package_ids[0] + # then: following is true since we have only one package + self.assertEqual(package.parcel_tracking, picking.carrier_tracking_ref) + self.assertEqual(package.gls_package_ref, picking.gls_package_ref) + + domain = [("res_model", "=", "stock.picking"), ("res_id", "=", picking.id)] + label = self.env["shipping.label"].search(domain) + self.assertEqual(len(label), 1, "There should be one label for this picking.") + + # optional: we cancel and resend the package, that should cancel itself out + with mock_gls_client(): + package.gls_cancel_shipment() + package.gls_send_shipping_package() + + # when + wizard_report = self.env["delivery.report.gls.wizard"].create({}) + # then + self.assertTrue(self.gls_carrier in wizard_report.carrier_ids) + + # given + wizard_report.carrier_ids = self.gls_carrier + # when + with mock_gls_client(): # two report calls within the same mock! + reports = wizard_report._get_end_of_day_report() + # then + self.assertTrue(package in reports.package_ids) + + # this time it will raise since there are no new packages + with self.assertRaises(ValidationError): + wizard_report.get_end_of_day_report() diff --git a/delivery_carrier_label_gls/tests/test_gls_client.py b/delivery_carrier_label_gls/tests/test_gls_client.py new file mode 100644 index 0000000000..c460b03c99 --- /dev/null +++ b/delivery_carrier_label_gls/tests/test_gls_client.py @@ -0,0 +1,120 @@ +# Copyright 2021 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import unittest + +from odoo.exceptions import ValidationError + +from .common import TestGLS + + +# to run these tests, you need to put your GLS credentials into the carrier account +# and shipping method. +@unittest.skip("gls_client") +class TestGlsClient(TestGLS): + @property + def create_payload(self): + return { + "Shipment": { + "Consignee": { + "Address": { + "Name1": "DALEMANS, Bert Courtyn", + "Street": "42, MEERSBLOEM MELDEN", + "CountryCode": "BE", + "ZIPCode": "9700", + "City": "OUDENAARDE", + "ContactPerson": "John", + "eMail": "bart.bart@xyz.be", + "FixedLinePhonenumber": "0495", + } + }, + "ShipmentUnit": [{"Weight": 5}], + "Product": "PARCEL", + }, + } + + def test_bad_request_allowed_services(self): + bad_payload = { + "Source": { + "CountryCode": "LX", # nonexistent countryCode! + "ZIPCode": "9800", + }, + "Destination": {"CountryCode": "BE", "ZIPCode": "1620"}, + } + with self.assertRaises(ValidationError): + self.gls_client.getAllowedServices(bad_payload) + + def test_get_allowed_services(self): + payload = { + "Source": {"CountryCode": "BE", "ZIPCode": "1367"}, + "Destination": {"CountryCode": "BE", "ZIPCode": "1620"}, + } + response = self.gls_client.getAllowedServices(payload) + + self.assertTrue({"ProductName": "PARCEL"} in response["AllowedServices"]) + # sample_response = {'AllowedServices': [{'ProductName': 'PARCEL'}, + # {'ProductName': 'EXPRESS'}, + # {'ServiceName': 'service_Saturday'}, + # {'ServiceName': 'service_pickandship'}, + # {'ServiceName': 'service_shopdelivery'}, + # {'ServiceName': 'service_0900'}, + # {'ServiceName': 'service_saturday_1200'}, + # {'ServiceName': 'service_easystart'}, + # {'ServiceName': 'service_cash'}, + # {'ServiceName': 'service_flexdelivery'}, + # {'ServiceName': 'service_pickandreturn'}, + # {'ServiceName': 'service_1200'}, + # {'ServiceName': 'service_shopreturn'}, + # {'ServiceName': 'service_1000'}]} + + def test_flow(self): + # firstly we create then cancel a package + response_create_cancel = self.gls_client.create_parcel(self.create_payload) + parcel_data = response_create_cancel["CreatedShipment"]["ParcelData"][0] + tracking_cancel = parcel_data["TrackID"] + self.assertEqual( + len(response_create_cancel["CreatedShipment"]["ParcelData"]), 1 + ) + self.assertTrue(len(tracking_cancel)) + self.assertTrue("PrintData" in response_create_cancel["CreatedShipment"]) + response_cancel = self.gls_client.cancel_parcel(tracking_cancel) + self.assertEqual(response_cancel["TrackID"], tracking_cancel) + cancellation_result = ["CANCELLED", "CANCELLATION_PENDING"] + self.assertTrue(response_cancel["result"] in cancellation_result) + + # secondly we create a package then get a report + response_create = self.gls_client.create_parcel(self.create_payload) + tracking = response_create["CreatedShipment"]["ParcelData"][0]["TrackID"] + report = self.gls_client.get_end_of_day_report() + # WARNING if you called create_parcel before, [0] won't work + self.assertEqual(len(report["Shipments"]), 1) + # the parcel that we canceled does not appear in the report + tracking_response = report["Shipments"][0]["ShipmentUnit"][0]["TrackID"] + self.assertEqual(tracking_response, tracking) + # the next call does not return parcels for which we already have a report + empty_report = self.gls_client.get_end_of_day_report() + self.assertEqual(empty_report, {}) + + # cannot cancel once the report is done + with self.assertRaises(ValidationError): + self.gls_client.cancel_parcel(tracking) + + def test_create_wrong_shipment(self): + """Check that with a wrong parameter, we get an exception.""" + bad_payload = self.create_payload + bad_payload["Shipment"]["Consignee"]["CountryCode"] = "LX" + + with self.assertRaises(ValidationError): + self.gls_client.validate_parcel(bad_payload) + with self.assertRaises(ValidationError): + self.gls_client.create_parcel(bad_payload) + + def test_wrong_cancel(self): + parcel_tracking = "doesnotexist" + with self.assertRaises(ValidationError): + self.gls_client.cancel_parcel(parcel_tracking) + + def test_get_parcel_shops(self): + belgium = self.env.ref("base.be") + response = self.gls_client.getParcelShopsByCountryCode(belgium.code) + self.assertTrue(len([p["ParcelShopID"] for p in response["ParcelShop"]])) diff --git a/delivery_carrier_label_gls/tests/test_partner.py b/delivery_carrier_label_gls/tests/test_partner.py new file mode 100644 index 0000000000..e39a0ca632 --- /dev/null +++ b/delivery_carrier_label_gls/tests/test_partner.py @@ -0,0 +1,18 @@ +# Copyright 2021 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from .common import TestGLS + + +class TestPartner(TestGLS): + def test_belgian_zip(self): + """Check that we have a string formatting parameter""" + expected = "1367" + self.partner.zip = "B-1367" + iso_zip = self.partner._get_iso_zip(validate_raises=True) + self.assertEqual(iso_zip, expected) + self.partner.zip = "B-136" + with self.assertRaises(ValidationError): + self.partner._get_iso_zip(validate_raises=True) diff --git a/delivery_carrier_label_gls/views/carrier_account.xml b/delivery_carrier_label_gls/views/carrier_account.xml new file mode 100644 index 0000000000..93d7e60a9e --- /dev/null +++ b/delivery_carrier_label_gls/views/carrier_account.xml @@ -0,0 +1,12 @@ + + + + carrier.account.search + carrier.account + + + + + + + diff --git a/delivery_carrier_label_gls/views/delivery_carrier.xml b/delivery_carrier_label_gls/views/delivery_carrier.xml new file mode 100644 index 0000000000..9e92fc1659 --- /dev/null +++ b/delivery_carrier_label_gls/views/delivery_carrier.xml @@ -0,0 +1,47 @@ + + + + delivery.carrier.form + delivery.carrier + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery_carrier_label_gls/views/delivery_report_gls.xml b/delivery_carrier_label_gls/views/delivery_report_gls.xml new file mode 100644 index 0000000000..c9bf01fbc9 --- /dev/null +++ b/delivery_carrier_label_gls/views/delivery_report_gls.xml @@ -0,0 +1,63 @@ + + + + GLS: Delivery Reports + delivery.report.gls + +
+ + + + + + + + + + + +
+
+
+ + delivery.report.gls.tree + delivery.report.gls + + + + + + + + delivery.report.gls.search + delivery.report.gls + + + + + + + + + + + + GLS: Delivery Reports + delivery.report.gls + tree,form + [] + {} + + +
diff --git a/delivery_carrier_label_gls/views/res_config_settings.xml b/delivery_carrier_label_gls/views/res_config_settings.xml new file mode 100644 index 0000000000..a0132e9e2d --- /dev/null +++ b/delivery_carrier_label_gls/views/res_config_settings.xml @@ -0,0 +1,51 @@ + + + + res.config.settings.view.form (in delivery_carrier_label_gls) + res.config.settings + + + + + + + + diff --git a/delivery_carrier_label_gls/views/sale_order.xml b/delivery_carrier_label_gls/views/sale_order.xml new file mode 100644 index 0000000000..5c6f3adf0a --- /dev/null +++ b/delivery_carrier_label_gls/views/sale_order.xml @@ -0,0 +1,17 @@ + + + + sale.order.form.delivery_gls + sale.order + + + + + + + + + diff --git a/delivery_carrier_label_gls/views/stock.xml b/delivery_carrier_label_gls/views/stock.xml new file mode 100644 index 0000000000..ab310dba24 --- /dev/null +++ b/delivery_carrier_label_gls/views/stock.xml @@ -0,0 +1,34 @@ + + + + stock.picking + + + + + + + + + + stock.quant.package + + + + + + diff --git a/delivery_carrier_label_gls/wizards/__init__.py b/delivery_carrier_label_gls/wizards/__init__.py new file mode 100644 index 0000000000..1b6a01cf5d --- /dev/null +++ b/delivery_carrier_label_gls/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import delivery_client_gls +from . import delivery_report_gls_wizard diff --git a/delivery_carrier_label_gls/wizards/delivery_client_gls.py b/delivery_carrier_label_gls/wizards/delivery_client_gls.py new file mode 100644 index 0000000000..ec557ffebd --- /dev/null +++ b/delivery_carrier_label_gls/wizards/delivery_client_gls.py @@ -0,0 +1,117 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# pylint: disable=unbalanced-tuple-unpacking + +import logging + +import requests + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +HEADERS = { # a GLS service constant. + "Accept": "application/glsVersion1+json, application/json", + "Content-Type": "application/glsVersion1+json", +} + + +class DeliveryClientGls(models.TransientModel): + _name = "delivery.client.gls" + _description = "GLS API client" + + carrier_id = fields.Many2one("delivery.carrier", "Carrier", required=True) + + @api.constrains("carrier_id") + def _constrain_carrier(self): + for record in self: + if record.carrier_id.delivery_type != "gls": + raise ValidationError(_("This is not a GLS client.")) + + def _get_contact_id(self): + self.ensure_one() + contact_id = self.carrier_id.gls_contact_id + if not contact_id: + raise ValidationError(_("Your GLS Contact ID is not configured.")) + return contact_id + + def _get_gls_connections_parameters(self): + self.ensure_one() + test_mode = not self.carrier_id.prod_environment + url_key = "gls_url_test" if test_mode else "gls_url" + if not self.carrier_id[url_key]: + raise ValidationError( + _(f"The {url_key} is missing in the delivery configuration.") + ) + return ( + self.carrier_id[url_key], + self.carrier_id.carrier_account_id.account, + self.carrier_id.carrier_account_id.password, + ) + + def get_end_of_day_report(self, date=False): + date = date or fields.Date.today() + params = {"date": fields.Date.to_string(date)} + return self._post("shipments/endofday", params=params) + + def validate_parcel(self, payload): + payload["Shipment"]["Product"] = "PARCEL" + payload["Shipment"]["Shipper"] = {"ContactID": self._get_contact_id()} + return self._post("shipments/validate", json=payload) + + def create_parcel(self, payload): + payload["Shipment"]["Shipper"] = {"ContactID": self._get_contact_id()} + if self.carrier_id.gls_return_partner_id: + address = self.carrier_id.gls_return_partner_id._gls_prepare_address() + payload["Shipment"]["Shipper"]["AlternativeShipperAddress"] = address + payload["PrintingOptions"] = { + "ReturnLabels": { + "TemplateSet": self.carrier_id.gls_label_template or "NONE", + "LabelFormat": self.carrier_id.gls_label_format.upper(), + } + } + try: + response = self._post("shipments", json=payload) + except ValidationError as e: + if "ARTICLES_VALID_FOR_COUNTRY" in e.args[0]: + payload["Shipment"].pop( + "Service" + ) # let GLS use default available service + response = self._post("shipments", json=payload) + else: + raise + return response + + def cancel_parcel(self, parcel_tracking): + end_url = "shipments/cancel/%s" % parcel_tracking + response = self._post(end_url) + if response["result"] not in ["CANCELLED", "CANCELLATION_PENDING"]: + raise ValidationError(_("This package cannot be cancelled anymore.")) + return response + + def getAllowedServices(self, payload): + return self._post("shipments/allowedservices", json=payload) + + def getParcelShopsByCountryCode(self, country_code): + return self._get("parcelshop/country/%s" % country_code) + + def _get(self, url_endpoint, **kwargs): + return self._request(requests.get, url_endpoint, **kwargs) + + def _post(self, url_endpoint, **kwargs): + return self._request(requests.post, url_endpoint, **kwargs) + + def _request(self, verb, url_endpoint, **kwargs): + root_url, login, password = self._get_gls_connections_parameters() + url = root_url + url_endpoint if url_endpoint else root_url + _logger.info("GLS request: %s" % kwargs) + response = verb(url, auth=(login, password), headers=HEADERS, **kwargs) + log = (response.status_code, response.headers, response.content) + _logger.info("GLS response:\ncode: %s\nheaders: %s\ncontent: %s" % log) + if not response.ok: + error = _("GLS: cannot perform this operation. Original error: %s") + msg = response.headers.get("message") or response.text + message = " ".join((str(response.status_code), msg or _("Unknown error."))) + raise ValidationError(error % message) + return response.json() diff --git a/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py b/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py new file mode 100644 index 0000000000..31939a7e5f --- /dev/null +++ b/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.py @@ -0,0 +1,87 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class DeliveryReportGls(models.TransientModel): + _name = "delivery.report.gls.wizard" + _description = "Wizard to get the End of Day Report" + + @api.model + def _default_carriers(self): + return self.env["delivery.carrier"].search([("delivery_type", "=", "gls")]) + + carrier_ids = fields.Many2many( + comodel_name="delivery.carrier", + string="Carriers", + default=_default_carriers, + domain=[("delivery_type", "=", "gls")], + ) + date = fields.Date(default=False) + + @api.constrains + def _constrain_carrier_id(self): + for record in self: + if any(carrier.delivery_type != "gls" for carrier in record.carrier_ids): + raise ValidationError(_("Only GLS supports delivery reports.")) + + @api.model + def _cron_end_of_day_report(self): + return self.create({})._get_end_of_day_report() + + def get_end_of_day_report(self): + self.ensure_one() + reports = self._get_end_of_day_report() + if not reports: + raise ValidationError(_("There are no new shipments.")) + action_xmlid = "delivery_carrier_label_gls.delivery_report_gls_act_window" + window_action = self.env.ref(action_xmlid).read()[0] + window_action["target"] = "current" + window_action["view_mode"] = "form" if len(reports) == 1 else "tree" + if len(reports) == 1: + window_action["res_id"] = reports.id + window_action["views"] = [(False, "form")] + else: + window_action["views"] = [(False, "tree"), (False, "form")] + window_action["domain"] = [("id", "in", reports.ids)] + return window_action + + def _get_end_of_day_report(self): + reports = self.env["delivery.report.gls"].browse() + for carrier in self.carrier_ids: + # TODO: actually process each login only once. + reports |= self._get_carrier_end_of_day_report(carrier) + return reports + + def _get_carrier_end_of_day_report(self, delivery_carrier): + # there could be multiple reports in one call, + # since carrier is login/Contact ID, + # but this returns all contact IDs for the login... + # TODO: rollback or any other error: leave a trace other than the logs + reports = self.env["delivery.report.gls"].browse() + now = fields.Datetime.now() # TODO: if date is today, use now instead + date = self.date or now + report_api = delivery_carrier._get_gls_client().get_end_of_day_report(date=date) + packages_api = report_api.get("Shipments", []) + if packages_api: + track_ids = [] + for shipment in packages_api: + for unit in shipment["ShipmentUnit"]: + track_ids.append(unit["TrackID"]) + domain_packages = [("parcel_tracking", "in", track_ids)] + packages = self.env["stock.quant.package"].search(domain_packages) + if len(packages) != len(packages_api): + _logger.exception("End of day report: wrong number of packages!") + for carrier in packages.carrier_id: + vals_report = {"report_datetime": date or now, "carrier_id": carrier.id} + report = self.env["delivery.report.gls"].create(vals_report) + report_pkgs = packages.filtered(lambda p, c=carrier: p.carrier_id == c) + report_pkgs.write({"report_id": report.id}) + reports |= report + return reports diff --git a/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.xml b/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.xml new file mode 100644 index 0000000000..2933b516fb --- /dev/null +++ b/delivery_carrier_label_gls/wizards/delivery_report_gls_wizard.xml @@ -0,0 +1,40 @@ + + + + GLS: Get End of Day Report + delivery.report.gls.wizard + +
+ + + + +
+
+
+
+
+ + GLS: Get End of Day Report + delivery.report.gls.wizard + form + new + [] + {} + + +
diff --git a/setup/delivery_carrier_label_gls/odoo/addons/delivery_carrier_label_gls b/setup/delivery_carrier_label_gls/odoo/addons/delivery_carrier_label_gls new file mode 120000 index 0000000000..f59ce09d16 --- /dev/null +++ b/setup/delivery_carrier_label_gls/odoo/addons/delivery_carrier_label_gls @@ -0,0 +1 @@ +../../../../delivery_carrier_label_gls \ No newline at end of file diff --git a/setup/delivery_carrier_label_gls/setup.py b/setup/delivery_carrier_label_gls/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/delivery_carrier_label_gls/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)