From c11b2a4f4904b507b44f9db5e7df721492f3fd6a Mon Sep 17 00:00:00 2001 From: david Date: Fri, 20 Aug 2021 15:07:36 +0200 Subject: [PATCH 01/28] [ADD] delivery_schenker: New module TT31005 --- delivery_schenker/README.rst | 170 ++++++ delivery_schenker/__init__.py | 1 + delivery_schenker/__manifest__.py | 20 + .../api/fat_bookingWebServiceV1_1.wsdl | 186 ++++++ .../api/prod_bookingWebServiceV1_1.wsdl | 186 ++++++ .../data/delivery_schenker_data.xml | 120 ++++ delivery_schenker/models/__init__.py | 3 + delivery_schenker/models/delivery_carrier.py | 530 ++++++++++++++++++ delivery_schenker/models/product_packaging.py | 14 + delivery_schenker/models/schenker_request.py | 190 +++++++ delivery_schenker/readme/CONFIGURE.rst | 10 + delivery_schenker/readme/CONTRIBUTORS.rst | 3 + delivery_schenker/readme/DESCRIPTION.rst | 2 + delivery_schenker/readme/INSTALL.rst | 5 + delivery_schenker/readme/ROADMAP.rst | 22 + delivery_schenker/readme/USAGE.rst | 41 ++ delivery_schenker/static/description/icon.png | Bin 0 -> 3740 bytes delivery_schenker/static/description/icon.svg | 528 +++++++++++++++++ .../static/description/index.html | 529 +++++++++++++++++ delivery_schenker/tests/__init__.py | 0 .../views/delivery_schenker_view.xml | 78 +++ .../views/stock_picking_views.xml | 20 + 22 files changed, 2658 insertions(+) create mode 100644 delivery_schenker/README.rst create mode 100644 delivery_schenker/__init__.py create mode 100644 delivery_schenker/__manifest__.py create mode 100644 delivery_schenker/api/fat_bookingWebServiceV1_1.wsdl create mode 100644 delivery_schenker/api/prod_bookingWebServiceV1_1.wsdl create mode 100644 delivery_schenker/data/delivery_schenker_data.xml create mode 100644 delivery_schenker/models/__init__.py create mode 100644 delivery_schenker/models/delivery_carrier.py create mode 100644 delivery_schenker/models/product_packaging.py create mode 100644 delivery_schenker/models/schenker_request.py create mode 100644 delivery_schenker/readme/CONFIGURE.rst create mode 100644 delivery_schenker/readme/CONTRIBUTORS.rst create mode 100644 delivery_schenker/readme/DESCRIPTION.rst create mode 100644 delivery_schenker/readme/INSTALL.rst create mode 100644 delivery_schenker/readme/ROADMAP.rst create mode 100644 delivery_schenker/readme/USAGE.rst create mode 100644 delivery_schenker/static/description/icon.png create mode 100644 delivery_schenker/static/description/icon.svg create mode 100644 delivery_schenker/static/description/index.html create mode 100644 delivery_schenker/tests/__init__.py create mode 100644 delivery_schenker/views/delivery_schenker_view.xml create mode 100644 delivery_schenker/views/stock_picking_views.xml diff --git a/delivery_schenker/README.rst b/delivery_schenker/README.rst new file mode 100644 index 0000000000..a4155ec4ec --- /dev/null +++ b/delivery_schenker/README.rst @@ -0,0 +1,170 @@ +================= +Delivery Schenker +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdelivery--carrier-lightgray.png?logo=github + :target: https://github.com/OCA/delivery-carrier/tree/13.0/delivery_schenker + :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-13-0/delivery-carrier-13-0-delivery_schenker + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/99/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module links the `DB Schenker `_ booking and tracking +APIs with Odoo delivery system. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module depends on the `zeep` python library and the OCA/delivery-carrier +`delivery_package_number` and `delivery_state` modules. + +The Schenker API doesn't provide delivery rating methods, so OCA's +`delivery_price_method` is advised in order to use this carrier in a sales workflow. + +Configuration +============= + +To configure a Schenker delivery method: + +#. Go to *Inventory > Configuration > Delivery > Shipping methods* and create a new one. +#. Choose *DB Schenker* as provider. +#. Configure the service parameters according to your contract considerations. +#. Choose a delivery product and a default packaging. This is mandatory for the booking + request as it needs the packaging code. + +To make tests, set the carrier environment to test from the smart button. Don't forget +to set it to production once you're ready to use the delivery method! + +Usage +===== + +These are the operations possible with this module: + +Place shipping bookings +~~~~~~~~~~~~~~~~~~~~~~~ + +#. When the picking is validated, the shipping will be booked at Schenker. +#. With the response, we'll receive the delivery tracking number and the pdf label in a + chatter message and it will be kept as attachment to the document. +#. You can manage packages number either with the proper Odoo workflows or with the + package number field available in the *Additional Info* tab. You'll get as many + labels as declared packages. + +Cancel bookings +~~~~~~~~~~~~~~~ + +#. As in other carriers, we can cancel the shipping after the picking is done. To do + so, go to *Additional Info* tab and click on the *Cancel* action on the side of the + tracking number. +#. We can generate a new shipping if necessary. + +Get labels +~~~~~~~~~~ + +#. If by chance we delete the generated labels, we can obtain them again hitting the + *Schenker Label* buttons in the header of the picking form. + +Tracking +~~~~~~~~ + +#. The module is integrated with `delivery_state` to be able to get the tracking info + directly from the DB Schenker API. +#. To do so, go to a picking shipped with Schenker. In the *Additional Info* tab you'll + find an action button to *Update tracking state* so the state will be updated from + the Schenker API. + +Debugging +~~~~~~~~~ + +The API calls and responses are tracked in two special fields in the picking that can +be viewed by technical users. You can also log them in as `ir.logging` records setting +the carrier debug on from the smart button. + +Known issues / Roadmap +====================== + +* There's no dummy access key to test API calls so no tests can be performed. +* The test booking and shipping APIs databases aren't connected so it isn't possible to + perform trackings on test mode. +* Only land shipping is implemented, although the module is prepared for extend to + air and ocean just considering the mandatory request fields for those methods. + Some additional adaptations could be needed (e.g.: origin and destination airport, + port) anyway. +* Only volume is supported as a measure unit and with the limitations of Odoo itself. To + enjoy a full fledged volume support, install and configure the OCA’s + `stock_quant_package_dimension` module and its dependencies. The connector is ready to + make use of their volume computations. +* It’d be needed to extend the method to support Schenker measure units such as loading + pieces or pallet space. +* Some more booking features aren’t yet supported although can be extended in the + future. Some of those, although the complete list would be really extensive: + + * Dangerous goods. + * Driver pre-advise. + * Transport temperature. + * Customs clearance. + * Cargo insurance. + * Cash on delivery. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * David Vidal + +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_schenker/__init__.py b/delivery_schenker/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/delivery_schenker/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_schenker/__manifest__.py b/delivery_schenker/__manifest__.py new file mode 100644 index 0000000000..fa9df94f58 --- /dev/null +++ b/delivery_schenker/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Delivery Schenker", + "summary": "Delivery Carrier implementation for DB Schenker API", + "version": "13.0.1.0.0", + "category": "Stock", + "website": "https://github.com/OCA/delivery-carrier", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["delivery_package_number", "delivery_state"], + "external_dependencies": {"python": ["zeep"]}, + "data": [ + "views/delivery_schenker_view.xml", + "views/stock_picking_views.xml", + "data/delivery_schenker_data.xml", + ], +} diff --git a/delivery_schenker/api/fat_bookingWebServiceV1_1.wsdl b/delivery_schenker/api/fat_bookingWebServiceV1_1.wsdl new file mode 100644 index 0000000000..79053f4181 --- /dev/null +++ b/delivery_schenker/api/fat_bookingWebServiceV1_1.wsdl @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Booking Web Service to create Land, Ocean and Air bookings + +Booking Web Service operation to create Land bookings + + + + + + + + +Booking Web Service operation to create Air bookings + + + + + + + + +Booking Web Service operation to create Ocean LCL bookings + + + + + + + + +Booking Web Service operation to create Ocean FCL bookings + + + + + + + + +Booking Web Service operation to get a barcode based on a booking id + + + + + + + + +Booking Web Service operation to cancel a booking by booking id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery_schenker/api/prod_bookingWebServiceV1_1.wsdl b/delivery_schenker/api/prod_bookingWebServiceV1_1.wsdl new file mode 100644 index 0000000000..3f7ccc7a02 --- /dev/null +++ b/delivery_schenker/api/prod_bookingWebServiceV1_1.wsdl @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Booking Web Service to create Land, Ocean and Air bookings + +Booking Web Service operation to create Land bookings + + + + + + + + +Booking Web Service operation to create Air bookings + + + + + + + + +Booking Web Service operation to create Ocean LCL bookings + + + + + + + + +Booking Web Service operation to create Ocean FCL bookings + + + + + + + + +Booking Web Service operation to get a barcode based on a booking id + + + + + + + + +Booking Web Service operation to cancel a booking by booking id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery_schenker/data/delivery_schenker_data.xml b/delivery_schenker/data/delivery_schenker_data.xml new file mode 100644 index 0000000000..cd9379a5da --- /dev/null +++ b/delivery_schenker/data/delivery_schenker_data.xml @@ -0,0 +1,120 @@ + + + + + CI + (Schenker) Canister + schenker + + + CT + (Schenker) Carton + schenker + + + CS + (Schenker) Case + schenker + + + CO + (Schenker) Colli + schenker + + + CH + (Schenker) Crate + schenker + + + GP + (Schenker) Skeleton box pallet + schenker + + + NE + (Schenker) Unpacked Skid + schenker + + + BG + (Schenker) Bag + schenker + + + BL + (Schenker) Bale + schenker + + + DR + (Schenker) Barrel + schenker + + + BX + (Schenker) Box + schenker + + + BY + (Schenker) Bundle + schenker + + + TR + (Schenker) Drum + schenker + + + EP + (Schenker) Europallet + schenker + + + FR + (Schenker) Frame + schenker + + + HO + (Schenker) Hobbock + schenker + + + OP + (Schenker) One-way pallet + schenker + + + PK + (Schenker) Package + schenker + + + XP + (Schenker) Pallet + schenker + + + PZ + (Schenker) Pipe + schenker + + + RO + (Schenker) Roll + schenker + + + SK + (Schenker) Sack + schenker + + + ZZ + (Schenker) Other + schenker + + + diff --git a/delivery_schenker/models/__init__.py b/delivery_schenker/models/__init__.py new file mode 100644 index 0000000000..5915e82b1c --- /dev/null +++ b/delivery_schenker/models/__init__.py @@ -0,0 +1,3 @@ +from . import delivery_carrier +from . import product_packaging +from . import schenker_request diff --git a/delivery_schenker/models/delivery_carrier.py b/delivery_schenker/models/delivery_carrier.py new file mode 100644 index 0000000000..ecac6bc950 --- /dev/null +++ b/delivery_schenker/models/delivery_carrier.py @@ -0,0 +1,530 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from lxml import etree + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from .schenker_request import SchenkerRequest + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + delivery_type = fields.Selection(selection_add=[("schenker", "DB Schenker")]) + schenker_access_key = fields.Char(string="Access Key", help="Schenker Access Key") + schenker_group_id = fields.Char(string="Group") + schenker_user = fields.Char(string="User") + schenker_booking_type = fields.Selection( + selection=[ + ("land", "Land"), + ("air", "Air"), + ("ocean_fcl", "Ocean FCL"), + ("ocean_lcl", "Ocean LCL"), + ], + default="land", + string="Booking Type", + help="Choose Scnecker booking type. Only land is currently suported", + ) + schenker_barcode_format = fields.Selection( + selection=[("A4", "A4"), ("A6", "A6")], default="A6", string="Barcode Format", + ) + schenker_barcode_mail = fields.Char( + string="Barcode Copy Email", + help="Optional: send a barcode copy to this email address", + ) + schenker_barcode_a4_start_pos = fields.Integer( + string="Barcode Start Position", + default=1, + help="For A4 format you can define the starting position", + ) + schenker_barcode_a4_separated = fields.Boolean( + string="Barcode Separated", + default=False, + help="For A4 define if the labels shall be printed on separate pages", + ) + schenker_incoterm_id = fields.Many2one( + comodel_name="account.incoterms", + string="Default Incoterm", + help="It will be overriden by the sale order one if it's specified.", + ) + schenker_service_type = fields.Selection( + string="Service Type", + help="Defines service type: D2D, D2P, P2D, P2P, D2A, A2D, A2A. Depending on " + "the Transport mode the service will be validated. For instance if the " + "transport mode is AIR, the service type P2P (PortToPort)", + selection=[ + ("D2D", "Door-to-door"), + ("D2P", "Door-to-port"), + ("P2D", "Port-to-door"), + ("D2A", "Door-to-airport"), + ("A2D", "Airport-to-door"), + ("A2A", "Aiport-to-airport"), + ], + ) + schenker_service_land = fields.Selection( + string="Land service", + help="Land shipping product options. Depending on your customer account, some " + "services could not be available", + selection=[ + ("CON", "DBSchenkerconcepts"), + ("DIR", "DBSchenkerdirects"), + ("LPA", "DBSchenkerparcel Logistics Parcel"), + ("PAL", "DBSchenkerpallets"), + ("PRI", "DBSchenkerprivpark"), + ("auc0", "austroexpress PUNKT 10"), + ("auc2", "austroexpress PUNKT 12"), + ("auc8", "austroexpress PUNKT 8"), + ("aucc", "austroexpress PUNKT 17"), + ("auco", "austrocargo"), + ("ecsp", "SCHENKERsystem-plus"), + ("ect1", "DB SCHENKERspeed 10"), + ("ect2", "DB SCHENKERspeed 12"), + ("sch2", "DB SCHENKERtop 12"), + ("schs", "DB SCHENKERsystem international"), + ("sysd", "DB SCHENKERsystem domestic"), + ("scht", "DB SCHENKERtop"), + ("schx", "DB SCHENKERsystem fix"), + ("ecpa", "DB SCHENKERparcel"), + ("ect8", "DB SCHENKERspeed 8"), + ("ectn", "DB SCHENKERspeed"), + ("40", "DB SCHENKERsystem classic"), + ("41", "DB SCHENKERsystem speed"), + ("42", "DB SCHENKERsystem fixday"), + ("43", "DBSchenker System"), + ("44", "DBSchenker System Premium"), + ("71", "DB SCHENKERdirect"), + ], + ) + schenker_service_air = fields.Selection( + string="Air service", + help="Air shipping product options. Depending on your customer account, some " + "services could not be available", + selection=[ + ("f", "DB SCHENKERjetcargo first"), + ("s", "DB SCHENKERjetcargo special"), + ("b", "DB SCHENKERjetcargo business"), + ("e", "DB SCHENKERjetcargo economy"), + ("eagd", "DB SCHENKERjetexpress gold"), + ("easv", "DB SCHENKERjetexpress silver"), + ], + ) + schenker_indoor_delivery = fields.Boolean( + string="Indoor Delivery", help="Defines if indoor delivery is required", + ) + schenker_express = fields.Boolean( + string="Express", help="Defines if shipment is express", + ) + schenker_food_related = fields.Boolean( + string="Food Related", help="Defines if shipment is food related", + ) + schenker_heated_transport = fields.Boolean( + string="Heated Transport", + help="Defines if shipment is required heated transport", + ) + schenker_home_delivery = fields.Boolean( + string="Home Delivery", help="Defines if shipment is required home delivery", + ) + schenker_own_pickup = fields.Boolean( + string="Own Pickup", help="Defines if shipment is required own pickup", + ) + schenker_pharmaceuticals = fields.Boolean( + string="Pharmaceuticals", help="Defines if shipment is pharmaceutical", + ) + schenker_measure_unit = fields.Selection( + string="Measure Unit", + help="The proper request will be formed accordingly from the picking", + selection=[ + ("VOLUME", "Volume"), + ("LOADING_METERS", "Loading meters"), + ("PIECES", "Pieces"), + ("PALLET_SPACE", "Pallet space"), + ], + default="VOLUME", + ) + schenker_default_packaging_id = fields.Many2one( + comodel_name="product.packaging", + string="Default Package Type", + domain=[("package_carrier_type", "=", "schenker")], + help="If not delivery package or the package doesn't have defined the packaging" + "it will default to this type", + ) + + def _get_schenker_credentials(self): + """Access key is mandatory for every request while group and user are + optional""" + credentials = { + "prod": self.prod_environment, + "access_key": self.schenker_access_key, + } + if self.schenker_group_id: + credentials["group_id"] = self.schenker_group_id + if self.schenker_user: + credentials["user"] = self.schenker_user + return credentials + + @api.model + def _schenker_log_request(self, schenker_request, picking): + """Helper to write raw request/response to the current picking. If debug + is active in the carrier, those will be logged in the ir.logging as well""" + schenker_last_request = schenker_last_response = False + try: + schenker_last_request = etree.tostring( + schenker_request.history.last_sent["envelope"], + encoding="UTF-8", + pretty_print=True, + ) + schenker_last_response = etree.tostring( + schenker_request.history.last_received["envelope"], + encoding="UTF-8", + pretty_print=True, + ) + # Don't fail hard on this. Sometimes zeep could not be able to keep history + except Exception: + return + # Debug must be active in the carrier + self.log_xml(schenker_last_request, "schenker_request") + self.log_xml(schenker_last_response, "schenker_response") + + def _prepare_schenker_barcode(self): + """Always request the barcode label when generating the booking. We can choose + between two formats: A6 and A4, where an starting position can be set""" + vals = { + "barcodeRequest": self.schenker_barcode_format, + } + if self.schenker_barcode_mail: + vals["barcodeRequestEmail"] = self.schenker_barcode_mail + if self.schenker_barcode_format == "A6": + return vals + # This options only can be informed when the label format is A4 + vals.update( + { + "start_pos": self.schenker_barcode_a4_start_pos, + "separated": self.schenker_barcode_a4_separated, + } + ) + return vals + + def _prepare_schenker_address( + self, + partner, + address_type="CONSIGNEE", + location_type="PHYSICAL", + person_type="COMPANY", + ): + """Generic for any address type. The address from the one receiving the goods. + Keep in mind that every country could have their own mandatory fields rules, + so the request could fail if those fields aren't filled on the contact. An + informative error should raise though. + :param res.partner record + :returns dicts with shipping address formated for Scheneket API + """ + vals = { + "type": address_type, + "name1": partner.name, + "locationType": location_type, # POSTAL or PHYSICAL + "personType": person_type, # PERSON OR COMPANY + "street": partner.street, + "postalCode": partner.zip, + "city": partner.city, + "stateCode": partner.state_id.code, + "stateName": partner.state_id.name, + "countryCode": partner.country_id.code, + "preferredLanguage": self.env["res.lang"]._lang_get(partner.lang).iso_code, + } + # Optional stuff. The API doesn't like falsy or empty request fields + if partner.email: + vals["email"] = partner.email + if partner.mobile: + vals["mobilePhone"] = partner.mobile + if partner.phone: + vals["phone"] = partner.phone + if partner.street2: + vals["street2"] = partner.street2 + return vals + + def _schenker_shipping_address(self, picking): + """Each booking should have at least 2 addresses of types: SHIPPER and CONSIGNEE + Other options are: PICKUP, DELIVERY, NOTIFY, INVOICE and could be hooked to this + method to include them in the booking request. + :param picking record + :returns list of dicts with shipping addresses formated for Scheneket API + """ + shipper_address = ( + picking.picking_type_id.warehouse_id.partner_id + or picking.company_id.partner_id + ) + consignee_address = picking.partner_id + return [ + self._prepare_schenker_address(shipper_address, "SHIPPER"), + self._prepare_schenker_address(consignee_address), + ] + + def _schenker_shipping_product(self): + """Gets the proper shipping product according to the shipping type + :returns string with shipping product code + """ + type_mapping = { + "air": self.schenker_service_air, + "land": self.schenker_service_land, + "ocean_fcl": "fcl", + "ocean_lcl": "lcl", + } + return type_mapping[self.schenker_booking_type] + + def _schenker_metric_system(self): + """ + :returns string with schenker metric system (METRIC or IMPERIAL) + """ + get_param = self.env["ir.config_parameter"].sudo().get_param + product_weight_in_lbs_param = get_param("product.weight_in_lbs", "0") + return "IMPERIAL" if product_weight_in_lbs_param == "1" else "METRIC" + + def _schenker_pickup_dates(self, picking): + """Convert picking dates for schenker api. We're taking the whole delivery + day as picking windows, although a more complex solution could be provided. + :param picking record with picking to send + :returns dict values with the picking dates in iso format + """ + date_from = fields.Datetime.context_timestamp( + self, picking.date_done.replace(hour=0, minute=0, second=0) + ).isoformat() + date_to = fields.Datetime.context_timestamp( + self, picking.date_done.replace(hour=23, minute=59, second=59) + ).isoformat() + return { + "pickUpDateFrom": date_from, + "pickUpDateTo": date_to, + } + + def _schenker_shipping_information_package(self, picking, package): + weight = package.shipping_weight or package.weight + # Volume calculations can be unfolded with stock_quant_package_dimension + if hasattr(package, "volume"): + volume = round(package.volume, 2) + else: + volume = sum([q.quantity * q.product_id.volume for q in package.quant_ids]) + return { + # Dangerous goods is not supported + "dgr": False, + "cargoDesc": picking.name + " / " + package.name, + "grossWeight": round(weight, 2), + # Default to 1 if no volume informed + "volume": volume, + "packageType": ( + package.packaging_id.shipper_package_code + or self.schenker_default_packaging_id.shipper_package_code + ), + "stackable": ( + package.packaging_id.schenker_stackable + or self.schenker_default_packaging_id.schenker_stackable + ), + "pieces": 1, + } + + def _schenker_shipping_information(self, picking): + """When we don't use delivery packages, we'll deliver everything in one single + shipping info. Otherwise, we'll get the info for each package. + :param picking record with picking to deliver + :returns list of dicts with delivery packages shipping info + """ + if picking.package_level_ids and picking.package_ids: + return [ + self._schenker_shipping_information_package(picking, package) + for package in picking.package_ids + ] + weight = picking.shipping_weight or picking.weight + # Obviously products should be well configured. This parameter is mandatory. + volume = sum( + [ + ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id) + * ml.product_id.volume + for ml in picking.move_line_ids + ] + ) + return [ + { + # Dangerous goods is not supported + "dgr": False, + "cargoDesc": picking.name, + # For a more complex solution use packaging properly + "grossWeight": round(weight / picking.number_of_packages, 2), + "volume": round(volume, 2) or 0.01, + "packageType": self.schenker_default_packaging_id.shipper_package_code, + "stackable": self.schenker_default_packaging_id.schenker_stackable, + "pieces": picking.number_of_packages, + } + ] + + def _schenker_measures(self, picking): + """Only volume is supported as a pallet calculations structure should be + provided to use the other API options. This hook can be used to communicate + with the API in the future + :param picking record with picking to deliver + :returns dict values for the proper unit key and value + """ + if self.schenker_measure_unit == "VOLUME": + return { + "measureUnitVolume": round(picking.volume, 2) or 0.01, + } + return {} + + def _prepare_schenker_shipping(self, picking): + """Convert picking values for schenker api + :param picking record with picking to send + :returns dict values for the connector + """ + self.ensure_one() + # We'll compose the request via some diferenced parts, like label settings, + # address options, incoterms and so. There are lots of thing to take into + # account to acomplish a properly formed request. + vals = {} + vals.update(self._prepare_schenker_barcode()) + vals.update( + { + "address": self._schenker_shipping_address(picking), + "incoterm": ( + picking.sale_id.incoterm.code or self.schenker_incoterm_id.code + ), + # A maximum of 35 characters is supported + "incotermLocation": picking.partner_id.display_name[:35], + "productCode": self._schenker_shipping_product(), + "measurementType": self._schenker_metric_system(), + "grossWeight": round(picking.shipping_weight, 2), + "shippingInformation": { + "shipmentPosition": self._schenker_shipping_information(picking), + "grossWeight": round(picking.shipping_weight, 2), + "volume": round(picking.volume, 2) or 0.01, + }, + "measureUnit": self.schenker_measure_unit, + # Customs Clearance not supported for now as it needs a full customs + # implementation + "customsClearance": False, + # Defines a business scenario where the Schenker customer sends a + # booking request in the name of his ordering party + "neutralShipping": False, + "pickupDates": self._schenker_pickup_dates(picking), + # Not supported for the moment + "specialCargo": False, + "specialCargoDescription": False, + "serviceType": self.schenker_service_type, + "indoorDelivery": self.schenker_indoor_delivery, + "express": self.schenker_express, + "foodRelated": self.schenker_food_related, + "heatedTransport": self.schenker_heated_transport, + "homeDelivery": self.schenker_home_delivery, + "ownPickup": self.schenker_own_pickup, + "pharmaceuticals": self.schenker_pharmaceuticals, + } + ) + vals.update(self._schenker_measures(picking)) + return vals + + def schenker_send_shipping(self, pickings): + """Send booking request to Schenker + :param pickings: A recordset of pickings + :return list: A list of dictionaries although in practice it's + called one by one and only the first item in the dict is taken. Due + to this design, we have to inject vals in the context to be able to + add them to the message. + """ + schenker_request = SchenkerRequest(**self._get_schenker_credentials()) + result = [] + for picking in pickings: + vals = self._prepare_schenker_shipping(picking) + vals.update({"tracking_number": False, "exact_price": 0}) + try: + response = schenker_request._send_shipping( + vals, self.schenker_booking_type + ) + except Exception as e: + raise (e) + finally: + self._schenker_log_request(schenker_request, picking) + if not response: + result.append(vals) + continue + vals["tracking_number"] = response.get("booking_id") + # We post an extra message in the chatter with the barcode and the + # label because there's clean way to override the one sent by core. + body = _("Schenker Shipping barcode document") + attachment = [] + if response.get("barcode"): + attachment = [ + ( + "schenker_label_{}.pdf".format(response.get("booking_id")), + response.get("barcode"), + ) + ] + picking.message_post(body=body, attachments=attachment) + result.append(vals) + return result + + def schenker_cancel_shipment(self, pickings): + """Cancel the expedition + :param pickings - stock.picking recordset + :returns pdf file + """ + schenker_request = SchenkerRequest(**self._get_schenker_credentials()) + for picking in pickings.filtered("carrier_tracking_ref"): + try: + schenker_request._cancel_shipment(picking.carrier_tracking_ref) + except Exception as e: + raise (e) + finally: + self._schenker_log_request(schenker_request, picking) + return True + + def schenker_get_label(self, reference, reference_type): + """Generate label for picking + :param picking - stock.picking record + :returns pdf file + """ + self.ensure_one() + if not reference: + return False + schenker_request = SchenkerRequest(**self._get_schenker_credentials()) + label = schenker_request._shipping_label(reference, reference_type) + if not label: + return False + return label + + def schenker_get_tracking_link(self, picking): + """Provide tracking link for the customer""" + return ( + "https://eschenker.dbschenker.com/app/tracking-public/?refNumber=%s" + % picking.carrier_tracking_ref + ) + + def schenker_tracking_state_update(self, picking): + """Tracking state update""" + # TODO: To be implemented + return + + def schenker_rate_shipment(self, order): + """There's no public API so another price method should be used.""" + raise NotImplementedError( + _( + "Schenker API doesn't provide methods to compute delivery " + "rates, so you should relay on another price method instead or " + "override this one in your custom code." + ) + ) + + # UX Control over not implemented features. + + @api.onchange("schenker_booking_type") + def onchange_schenker_booking_type(self): + """Avoid by UX that the user could choose another shipping method. In + the future, this can be removed as long as those method have the proper + support""" + if self.schenker_booking_type != "land": + raise UserError(_("Only land shipping is currently supported")) + + @api.onchange("schenker_measure_unit") + def onchange_schenker_measure_unit(self): + """Avoid by UX that the user could choose another measure unit. Proper pallet + calculation structure should be provided to use the other API options. A hook + method is provided though.""" + if self.schenker_measure_unit != "VOLUME": + raise UserError(_("Only volume is currently supported")) diff --git a/delivery_schenker/models/product_packaging.py b/delivery_schenker/models/product_packaging.py new file mode 100644 index 0000000000..c41b202a11 --- /dev/null +++ b/delivery_schenker/models/product_packaging.py @@ -0,0 +1,14 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ProductPackaging(models.Model): + _inherit = "product.packaging" + + package_carrier_type = fields.Selection( + selection_add=[("schenker", "DB Schenker")], + ) + schenker_stackable = fields.Boolean( + string="Stackable", help="Define if the package is stackable by default", + ) diff --git a/delivery_schenker/models/schenker_request.py b/delivery_schenker/models/schenker_request.py new file mode 100644 index 0000000000..3aca07c780 --- /dev/null +++ b/delivery_schenker/models/schenker_request.py @@ -0,0 +1,190 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import binascii +import logging +from xml.etree import ElementTree as ET + +from zeep import Client, Settings +from zeep.exceptions import Fault +from zeep.plugins import HistoryPlugin + +from odoo import _ +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +SCHENKER_API_URL = { + "test": "https://eschenker-fat.dbschenker.com", + "prod": "https://eschenker.dbschenker.com", +} +SCHENKER_API_SERVICE = { + "booking": "/webservice/bookingWebServiceV1_1?wsdl", + "tracking": "/webservice/bookingWebServiceV1_1?wsdl", +} + + +class SchenkerRequest: + """Interface between Schenker SOAP API and Odoo recordset + Abstract Schenker API Operations to connect them with Odoo + + Not all the features are implemented, but could be easily extended with + the provided API. We leave the operations empty for future. + """ + + def __init__( + self, access_key=None, group_id=None, user=None, prod=False, service="booking" + ): + self.access_key = access_key or "" + self.group_id = group_id or "" + self.user = user or "" + self.service = service + api_env = "prod" if prod else "test" + self.history = HistoryPlugin(maxlen=10) + settings = Settings(strict=False, xml_huge_tree=True) + self.client = Client( + wsdl=SCHENKER_API_URL[api_env] + SCHENKER_API_SERVICE[service], + settings=settings, + plugins=[self.history], + ) + + def _process_reply(self, service, vals=None): + """Schenker API returns error petitions as server exceptions wich makes zeep to + raise a Fault exception as well. To catch the error info we need to make a + raw_response request and the extract the error codes from the response.""" + try: + response = service(vals) + except Fault as e: + with self.client.settings(raw_response=True): + response = service(vals) + try: + root = ET.fromstring(response.text) + error_text = next(root.iter("faultstring")).text + error_message = next(root.iter("message")).text + error_code = next(root.iter("code")).text + raise ValidationError( + _( + "Error in the request to the Schenker API. This is the " + "thrown message:\n\n" + "[%s]\n" + "%s - %s" % (error_text, error_code, error_message) + ) + ) + except ValidationError: + raise + # If we can't get the proper exception, fallback to the first + # exception error traceback + except Exception: + raise Fault(e) + return response + + # Booking API methods + + def _shipping_type_method(self, method): + """Map shipping method with API method. Note that currently only land + is supported. Default to land to ensure a method is provided. + :params string with shipping method + :returns string with the mapped key value for the proper method + """ + method_map = { + "land": "getBookingRequestLand", + "air": "getBookingRequestAir", + "ocean_fcl": "getBookingRequestOceanFCL", + "ocean_lcl": "getBookingRequestOceanLCL", + } + return method_map.get("method", "getBookingRequestLand") + + def _shipping_api_credentials(self): + """Each API has a different credentials SOAP declaration""" + credentials = {"applicationArea": {"accessKey": self.access_key}} + if self.user: + credentials["applicationArea"]["userId"] = self.user + if self.group_id: + credentials["applicationArea"]["groupId"] = self.group_id + return credentials + + def _scheneker_shipping_api_wrapper(self, method=False): + """Aside from a different API method, each one has its own wrapper""" + booking_wrapper_map = { + "land": "bookingLand", + "air": "bookingAir", + "ocean_fcl": "bookingOceanFCL", + "ocean_lcl": "bookingOceanLCL", + } + return booking_wrapper_map.get(method, "land") + + def _send_shipping(self, picking_vals, method=False): + """Create new shipment + :params vals dict of needed values + :returns dict with Schenker response containing the shipping code and label + """ + vals = self._shipping_api_credentials() + method_wrapper = self._scheneker_shipping_api_wrapper(method) + vals[method_wrapper] = picking_vals + # From the Schenker docs: + # Defines if booking shall be submitted. If false, the booking can be edited + # in the frontend and MUST be submitted manually. + vals[method_wrapper].update({"submitBooking": True}) + response = self._process_reply( + self.client.service[self._shipping_type_method(method)], vals + ) + return { + "booking_id": response.bookingId, + "barcode": response.barcodeDocument, + } + + def _shipping_label(self, reference_list=None): + """Get shipping label for the given ref + :param list reference -- shipping reference list + :returns: base64 with pdf labels + """ + reference_list = reference_list or [] + vals = self._shipping_api_credentials() + vals.update({"barcodeRequest": [{"booking_id": ref} for ref in reference_list]}) + label = self._process_reply( + self.client.service.getBookingRequestLand, vals + ).document + return label and binascii.a2b_base64(label) + + def _cancel_shipment(self, reference=False): + """Cancel de expedition for the given ref + :param str reference -- booking reference string + :returns: bool True if success + """ + vals = self._shipping_api_credentials() + vals.update({"cancelRequest": {"bookingId": reference}}) + response = self._process_reply( + self.client.service.getBookingCancelRequest, vals + ) + # TODO: Inspect typical response as we don't want to return a zeep object. + # Anyway, it's going to fail if the booking can't be cancelled. So either we + # receive an exception error or the booking is cancelled. + return bool(response) + + # Tracking API methods + + def _tracking_api_credentials(self): + """Each API has a different credentials SOAP declaration""" + credentials = {"accessKey": self.access_key, "in": {}} + if self.user: + credentials["in"].setdefault("applicationArea", {}) + credentials["in"]["applicationArea"]["userId"] = self.user + if self.group_id: + credentials["in"].setdefault("applicationArea", {}) + credentials["in"]["applicationArea"]["groupId"] = self.group_id + return credentials + + def _get_shipment_details(self, reference=False, reference_type="BID"): + vals = self._shipping_api_credentials() + vals["in"]["referenceNumber"] = reference + response = self._process_reply( + self.client.service.getPublicServiceShipmentDetails, vals + ) + return response + + def _get_tracking_states(self, reference=False): + # TODO: Test Shipping API isn't connected to Test Booking API + if not reference: + return {} + # response = self._get_shipment_details(reference) + # It should come from ShipmentInfo.ShipmentBasicInfo.StatusEventList + return {} diff --git a/delivery_schenker/readme/CONFIGURE.rst b/delivery_schenker/readme/CONFIGURE.rst new file mode 100644 index 0000000000..7778ddc6d6 --- /dev/null +++ b/delivery_schenker/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +To configure a Schenker delivery method: + +#. Go to *Inventory > Configuration > Delivery > Shipping methods* and create a new one. +#. Choose *DB Schenker* as provider. +#. Configure the service parameters according to your contract considerations. +#. Choose a delivery product and a default packaging. This is mandatory for the booking + request as it needs the packaging code. + +To make tests, set the carrier environment to test from the smart button. Don't forget +to set it to production once you're ready to use the delivery method! diff --git a/delivery_schenker/readme/CONTRIBUTORS.rst b/delivery_schenker/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..94b6ba9536 --- /dev/null +++ b/delivery_schenker/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Tecnativa `_: + + * David Vidal diff --git a/delivery_schenker/readme/DESCRIPTION.rst b/delivery_schenker/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..03b9b125f3 --- /dev/null +++ b/delivery_schenker/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module links the `DB Schenker `_ booking and tracking +APIs with Odoo delivery system. diff --git a/delivery_schenker/readme/INSTALL.rst b/delivery_schenker/readme/INSTALL.rst new file mode 100644 index 0000000000..5652116f35 --- /dev/null +++ b/delivery_schenker/readme/INSTALL.rst @@ -0,0 +1,5 @@ +This module depends on the `zeep` python library and the OCA/delivery-carrier +`delivery_package_number` and `delivery_state` modules. + +The Schenker API doesn't provide delivery rating methods, so OCA's +`delivery_price_method` is advised in order to use this carrier in a sales workflow. diff --git a/delivery_schenker/readme/ROADMAP.rst b/delivery_schenker/readme/ROADMAP.rst new file mode 100644 index 0000000000..cfd14730ec --- /dev/null +++ b/delivery_schenker/readme/ROADMAP.rst @@ -0,0 +1,22 @@ +* There's no dummy access key to test API calls so no tests can be performed. +* The test booking and shipping APIs databases aren't connected so it isn't possible to + perform trackings on test mode. +* Only land shipping is implemented, although the module is prepared for extend to + air and ocean just considering the mandatory request fields for those methods. + Some additional adaptations could be needed (e.g.: origin and destination airport, + port) anyway. +* Only volume is supported as a measure unit and with the limitations of Odoo itself. To + enjoy a full fledged volume support, install and configure the OCA’s + `stock_quant_package_dimension` module and its dependencies. The connector is ready to + make use of their volume computations. +* It’d be needed to extend the method to support Schenker measure units such as loading + pieces or pallet space. +* Some more booking features aren’t yet supported although can be extended in the + future. Some of those, although the complete list would be really extensive: + + * Dangerous goods. + * Driver pre-advise. + * Transport temperature. + * Customs clearance. + * Cargo insurance. + * Cash on delivery. diff --git a/delivery_schenker/readme/USAGE.rst b/delivery_schenker/readme/USAGE.rst new file mode 100644 index 0000000000..cffac6611a --- /dev/null +++ b/delivery_schenker/readme/USAGE.rst @@ -0,0 +1,41 @@ +These are the operations possible with this module: + +Place shipping bookings +~~~~~~~~~~~~~~~~~~~~~~~ + +#. When the picking is validated, the shipping will be booked at Schenker. +#. With the response, we'll receive the delivery tracking number and the pdf label in a + chatter message and it will be kept as attachment to the document. +#. You can manage packages number either with the proper Odoo workflows or with the + package number field available in the *Additional Info* tab. You'll get as many + labels as declared packages. + +Cancel bookings +~~~~~~~~~~~~~~~ + +#. As in other carriers, we can cancel the shipping after the picking is done. To do + so, go to *Additional Info* tab and click on the *Cancel* action on the side of the + tracking number. +#. We can generate a new shipping if necessary. + +Get labels +~~~~~~~~~~ + +#. If by chance we delete the generated labels, we can obtain them again hitting the + *Schenker Label* buttons in the header of the picking form. + +Tracking +~~~~~~~~ + +#. The module is integrated with `delivery_state` to be able to get the tracking info + directly from the DB Schenker API. +#. To do so, go to a picking shipped with Schenker. In the *Additional Info* tab you'll + find an action button to *Update tracking state* so the state will be updated from + the Schenker API. + +Debugging +~~~~~~~~~ + +The API calls and responses are tracked in two special fields in the picking that can +be viewed by technical users. You can also log them in as `ir.logging` records setting +the carrier debug on from the smart button. diff --git a/delivery_schenker/static/description/icon.png b/delivery_schenker/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e3b8b0ff9771f63ddea1e37bd173ff823d86a7a9 GIT binary patch literal 3740 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 fw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery_schenker/static/description/index.html b/delivery_schenker/static/description/index.html new file mode 100644 index 0000000000..085f3ed7ff --- /dev/null +++ b/delivery_schenker/static/description/index.html @@ -0,0 +1,529 @@ + + + + + + +Delivery Schenker + + + +
+

Delivery Schenker

+ + +

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

+

This module links the DB Schenker booking and tracking +APIs with Odoo delivery system.

+

Table of contents

+ +
+

Installation

+

This module depends on the zeep python library and the OCA/delivery-carrier +delivery_package_number and delivery_state modules.

+

The Schenker API doesn’t provide delivery rating methods, so OCA’s +delivery_price_method is advised in order to use this carrier in a sales workflow.

+
+
+

Configuration

+

To configure a Schenker delivery method:

+
    +
  1. Go to Inventory > Configuration > Delivery > Shipping methods and create a new one.
  2. +
  3. Choose DB Schenker as provider.
  4. +
  5. Configure the service parameters according to your contract considerations.
  6. +
  7. Choose a delivery product and a default packaging. This is mandatory for the booking +request as it needs the packaging code.
  8. +
+

To make tests, set the carrier environment to test from the smart button. Don’t forget +to set it to production once you’re ready to use the delivery method!

+
+
+

Usage

+

These are the operations possible with this module:

+
+

Place shipping bookings

+
    +
  1. When the picking is validated, the shipping will be booked at Schenker.
  2. +
  3. With the response, we’ll receive the delivery tracking number and the pdf label in a +chatter message and it will be kept as attachment to the document.
  4. +
  5. You can manage packages number either with the proper Odoo workflows or with the +package number field available in the Additional Info tab. You’ll get as many +labels as declared packages.
  6. +
+
+
+

Cancel bookings

+
    +
  1. As in other carriers, we can cancel the shipping after the picking is done. To do +so, go to Additional Info tab and click on the Cancel action on the side of the +tracking number.
  2. +
  3. We can generate a new shipping if necessary.
  4. +
+
+
+

Get labels

+
    +
  1. If by chance we delete the generated labels, we can obtain them again hitting the +Schenker Label buttons in the header of the picking form.
  2. +
+
+
+

Tracking

+
    +
  1. The module is integrated with delivery_state to be able to get the tracking info +directly from the DB Schenker API.
  2. +
  3. To do so, go to a picking shipped with Schenker. In the Additional Info tab you’ll +find an action button to Update tracking state so the state will be updated from +the Schenker API.
  4. +
+
+
+

Debugging

+

The API calls and responses are tracked in two special fields in the picking that can +be viewed by technical users. You can also log them in as ir.logging records setting +the carrier debug on from the smart button.

+
+
+
+

Known issues / Roadmap

+
    +
  • There’s no dummy access key to test API calls so no tests can be performed.
  • +
  • The test booking and shipping APIs databases aren’t connected so it isn’t possible to +perform trackings on test mode.
  • +
  • Only land shipping is implemented, although the module is prepared for extend to +air and ocean just considering the mandatory request fields for those methods. +Some additional adaptations could be needed (e.g.: origin and destination airport, +port) anyway.
  • +
  • Only volume is supported as a measure unit and with the limitations of Odoo itself. To +enjoy a full fledged volume support, install and configure the OCA’s +stock_quant_package_dimension module and its dependencies. The connector is ready to +make use of their volume computations.
  • +
  • It’d be needed to extend the method to support Schenker measure units such as loading +pieces or pallet space.
  • +
  • Some more booking features aren’t yet supported although can be extended in the +future. Some of those, although the complete list would be really extensive:
      +
    • Dangerous goods.
    • +
    • Driver pre-advise.
    • +
    • Transport temperature.
    • +
    • Customs clearance.
    • +
    • Cargo insurance.
    • +
    • Cash on delivery.
    • +
    +
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

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_schenker/tests/__init__.py b/delivery_schenker/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/delivery_schenker/views/delivery_schenker_view.xml b/delivery_schenker/views/delivery_schenker_view.xml new file mode 100644 index 0000000000..5c8113a10f --- /dev/null +++ b/delivery_schenker/views/delivery_schenker_view.xml @@ -0,0 +1,78 @@ + + + + + delivery.carrier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery_schenker/views/stock_picking_views.xml b/delivery_schenker/views/stock_picking_views.xml new file mode 100644 index 0000000000..28ac6bb6ff --- /dev/null +++ b/delivery_schenker/views/stock_picking_views.xml @@ -0,0 +1,20 @@ + + + + + stock.picking + + + + +