diff --git a/delivery_estimated_package_quantity_by_weight/README.rst b/delivery_estimated_package_quantity_by_weight/README.rst new file mode 100644 index 0000000000..e293948f17 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/README.rst @@ -0,0 +1,104 @@ +============================================= +Delivery Estimated Package Quantity By Weight +============================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a35ba2d1b127f3f76257bb0405764911e2b224b554865a756261c6f221cbe3de + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/delivery_estimated_package_quantity_by_weight + :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-18-0/delivery-carrier-18-0-delivery_estimated_package_quantity_by_weight + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module computes the amount of packages a picking out should have +depending on the weight of the products and the limit fixed by the +carrier. It's fully independent of the delivery_package_number module. + +A warning is given if the number of packages for the picking out is +above what is considered as the theoretical number of packages for this +picking and the chosen carrier. The goal is to minimize the number of +packages billed by the carrier. + +The chosen strategy for the theoretical number of packages is as follow: + +- Split the product_weights into as many items as we have +- Try to fit the heaviest product with the lightest. If it does not work + then the heaviest should have a box for itself +- While the weight of products does not exceed the limit, continue + adding products in the same package + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Here are some limitations of the module: + +- Product packagings are not supported +- Multiple package types having different max weight for a shipping + method are not supported + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Lindsay Marion +- Hughes Damry +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +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_estimated_package_quantity_by_weight/__init__.py b/delivery_estimated_package_quantity_by_weight/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_estimated_package_quantity_by_weight/__manifest__.py b/delivery_estimated_package_quantity_by_weight/__manifest__.py new file mode 100644 index 0000000000..a7ae0ba2d4 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Delivery Estimated Package Quantity By Weight", + "summary": """ + Compute the amount of packages a picking out should have depending on the + weight of the products and the limit fixed by the carrier""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "website": "https://github.com/OCA/delivery-carrier", + "author": "ACSONE SA/NV, Odoo Community Association (OCA)", + "depends": ["stock_delivery"], + "data": ["views/delivery_carrier_views.xml", "views/stock_picking_views.xml"], + "installable": True, +} diff --git a/delivery_estimated_package_quantity_by_weight/i18n/delivery_estimated_package_quantity_by_weight.pot b/delivery_estimated_package_quantity_by_weight/i18n/delivery_estimated_package_quantity_by_weight.pot new file mode 100644 index 0000000000..0eebeecd2c --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/i18n/delivery_estimated_package_quantity_by_weight.pot @@ -0,0 +1,64 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_estimated_package_quantity_by_weight +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__maximum_weight_per_package +msgid "Maximum weight per package" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "Number of packages" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__number_of_packages_done +msgid "Number of packages in a picking out" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_visible +msgid "Number of packages visible" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_delivery_carrier +msgid "Shipping Methods" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "The number of packages outranged the adviced number of packages" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__theoretical_number_of_packages +msgid "Theoretical number of packages in a picking out" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_outranged +msgid "Too many packages compared to the theoretical number" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__weight_uom_name +msgid "Weight unit of measure label" +msgstr "" diff --git a/delivery_estimated_package_quantity_by_weight/i18n/es.po b/delivery_estimated_package_quantity_by_weight/i18n/es.po new file mode 100644 index 0000000000..1ae42f68dd --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/i18n/es.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_estimated_package_quantity_by_weight +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-08-03 15:09+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__maximum_weight_per_package +msgid "Maximum weight per package" +msgstr "Peso máximo por paquete" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "Number of packages" +msgstr "Número de paquetes" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__number_of_packages_done +msgid "Number of packages in a picking out" +msgstr "Número de paquetes en una recogida" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_visible +msgid "Number of packages visible" +msgstr "Número de paquetes visibles" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_delivery_carrier +msgid "Shipping Methods" +msgstr "Métodos de envío" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "The number of packages outranged the adviced number of packages" +msgstr "El número de paquetes superó el aconsejado" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__theoretical_number_of_packages +msgid "Theoretical number of packages in a picking out" +msgstr "Número teórico de paquetes en una recogida" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_outranged +msgid "Too many packages compared to the theoretical number" +msgstr "Demasiados paquetes en comparación con el número teórico" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_stock_picking +msgid "Transfer" +msgstr "Transferencia" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__weight_uom_name +msgid "Weight unit of measure label" +msgstr "Etiqueta de unidad de medida de peso" diff --git a/delivery_estimated_package_quantity_by_weight/i18n/fr_BE.po b/delivery_estimated_package_quantity_by_weight/i18n/fr_BE.po new file mode 100644 index 0000000000..a37343aa97 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/i18n/fr_BE.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_estimated_package_quantity_by_weight +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-03-13 13:22+0000\n" +"PO-Revision-Date: 2023-03-13 13:22+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__maximum_weight_per_package +msgid "Maximum weight per package" +msgstr "Poids maximum par colis" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "Number of packages" +msgstr "Le nombre de colis est visible" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__number_of_packages_done +msgid "Number of packages in a picking out" +msgstr "Nombre de colis recommandé" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_visible +msgid "Number of packages visible" +msgstr "Le nombre de colis est visible" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_delivery_carrier +msgid "Shipping Methods" +msgstr "Modes de livraison" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "The number of packages outranged the adviced number of packages" +msgstr "Le nombre de colis dépasse le nombre de colis recommandé" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__theoretical_number_of_packages +msgid "Theoretical number of packages in a picking out" +msgstr "Nombre de colis recommandé" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_outranged +msgid "Too many packages compared to the theoretical number" +msgstr "Trop de colis en comparaison du nombre de colis recommandé" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_stock_picking +msgid "Transfer" +msgstr "Transfert" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__weight_uom_name +msgid "Weight unit of measure label" +msgstr "" diff --git a/delivery_estimated_package_quantity_by_weight/i18n/it.po b/delivery_estimated_package_quantity_by_weight/i18n/it.po new file mode 100644 index 0000000000..dd58742122 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/i18n/it.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * delivery_estimated_package_quantity_by_weight +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-07 12:35+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__maximum_weight_per_package +msgid "Maximum weight per package" +msgstr "Peso massimo per collo" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "Number of packages" +msgstr "Numero di colli" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__number_of_packages_done +msgid "Number of packages in a picking out" +msgstr "Numero di colli in un prelievo di uscita" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_visible +msgid "Number of packages visible" +msgstr "Numero di colli visibi" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_delivery_carrier +msgid "Shipping Methods" +msgstr "Metodi di spedizione" + +#. module: delivery_estimated_package_quantity_by_weight +#: model_terms:ir.ui.view,arch_db:delivery_estimated_package_quantity_by_weight.stock_picking_form_view +msgid "The number of packages outranged the adviced number of packages" +msgstr "Il numero di colli ha superato il numero di colli consigliato" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__theoretical_number_of_packages +msgid "Theoretical number of packages in a picking out" +msgstr "Numero teorico di colli in un prelievo di uscita" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_stock_picking__is_number_of_packages_outranged +msgid "Too many packages compared to the theoretical number" +msgstr "Troppi colli rispetto al numero teorico" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model,name:delivery_estimated_package_quantity_by_weight.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" + +#. module: delivery_estimated_package_quantity_by_weight +#: model:ir.model.fields,field_description:delivery_estimated_package_quantity_by_weight.field_delivery_carrier__weight_uom_name +msgid "Weight unit of measure label" +msgstr "Etichetta unità di misura del peso" diff --git a/delivery_estimated_package_quantity_by_weight/models/__init__.py b/delivery_estimated_package_quantity_by_weight/models/__init__.py new file mode 100644 index 0000000000..66690e69a9 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_picking +from . import delivery_carrier diff --git a/delivery_estimated_package_quantity_by_weight/models/delivery_carrier.py b/delivery_estimated_package_quantity_by_weight/models/delivery_carrier.py new file mode 100644 index 0000000000..6fdd47dfbc --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/models/delivery_carrier.py @@ -0,0 +1,20 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + maximum_weight_per_package = fields.Float(string="Maximum weight per package") + weight_uom_name = fields.Char( + string="Weight unit of measure label", compute="_compute_weight_uom_name" + ) + + def _compute_weight_uom_name(self): + weight_uom_name = self.env[ + "product.template" + ]._get_weight_uom_name_from_ir_config_parameter() + for rec in self: + rec.weight_uom_name = weight_uom_name diff --git a/delivery_estimated_package_quantity_by_weight/models/stock_picking.py b/delivery_estimated_package_quantity_by_weight/models/stock_picking.py new file mode 100644 index 0000000000..704e7b8045 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/models/stock_picking.py @@ -0,0 +1,110 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + theoretical_number_of_packages = fields.Integer( + "Theoretical number of packages in a picking out", + compute="_compute_theoretical_number_of_packages", + ) + number_of_packages_done = fields.Integer( + "Number of packages in a picking out", + compute="_compute_number_of_packages_done", + ) + is_number_of_packages_visible = fields.Boolean( + "Number of packages visible", + compute="_compute_is_number_of_packages_visible", + ) + is_number_of_packages_outranged = fields.Boolean( + "Too many packages compared to the theoretical number", + compute="_compute_is_number_of_packages_outranged", + ) + + @api.depends( + "picking_type_code", "carrier_id", "carrier_id.maximum_weight_per_package" + ) + def _compute_is_number_of_packages_visible(self): + for rec in self: + if ( + rec.picking_type_code == "outgoing" + and rec.carrier_id.maximum_weight_per_package + ): + rec.is_number_of_packages_visible = True + else: + rec.is_number_of_packages_visible = False + + @api.depends("is_number_of_packages_visible", "move_ids") + def _compute_theoretical_number_of_packages(self): + for rec in self: + if rec.is_number_of_packages_visible: + products_weights = rec.move_ids.mapped("product_id.weight") + number_of_items = rec.move_ids.mapped("product_uom_qty") + rec.theoretical_number_of_packages = rec._number_of_packages( + products_weights, + number_of_items, + rec.carrier_id.maximum_weight_per_package, + ) + else: + rec.theoretical_number_of_packages = False + + @api.depends( + "is_number_of_packages_visible", + "move_line_ids", + "move_line_ids.result_package_id", + ) + def _compute_number_of_packages_done(self): + for rec in self: + if rec.is_number_of_packages_visible: + rec.number_of_packages_done = len( + rec.mapped("move_line_ids.result_package_id") + ) + else: + rec.number_of_packages_done = False + + @api.depends("theoretical_number_of_packages", "number_of_packages_done") + def _compute_is_number_of_packages_outranged(self): + for rec in self: + if rec.is_number_of_packages_visible: + rec.is_number_of_packages_outranged = ( + rec.number_of_packages_done > rec.theoretical_number_of_packages + ) + else: + rec.is_number_of_packages_outranged = False + + def _number_of_packages( + self, products_weights, number_of_items, maximum_weight_per_package + ): + # Split the product_weights into as many items as we haves + products_weights_list = [] + for weight, number in zip(products_weights, number_of_items, strict=False): + for _i in range(int(number)): + products_weights_list.append(weight) + + products_weights_list.sort() + + i = 0 + weight = 0 + j = len(products_weights_list) - 1 + theoretical_number_of_packages = 0 + while i <= j: + theoretical_number_of_packages += 1 + # Try to fit the heaviest product with the lightest. + # If it does not work, then the heaviest should have + # a box to itself + weight = products_weights_list[i] + products_weights_list[j] + while weight <= maximum_weight_per_package: + i += 1 + if i < j: + # While the weight of products does not exceed the limit, + # continue adding products in the same package + weight += products_weights_list[i] + else: + break + + j -= 1 + + return theoretical_number_of_packages diff --git a/delivery_estimated_package_quantity_by_weight/pyproject.toml b/delivery_estimated_package_quantity_by_weight/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/delivery_estimated_package_quantity_by_weight/readme/CONTRIBUTORS.md b/delivery_estimated_package_quantity_by_weight/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..bebf3901dd --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Lindsay Marion \<\> +- Hughes Damry \<\> +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/delivery_estimated_package_quantity_by_weight/readme/DESCRIPTION.md b/delivery_estimated_package_quantity_by_weight/readme/DESCRIPTION.md new file mode 100644 index 0000000000..155873c746 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +This module computes the amount of packages a picking out should have +depending on the weight of the products and the limit fixed by the +carrier. It's fully independent of the delivery_package_number module. + +A warning is given if the number of packages for the picking out is +above what is considered as the theoretical number of packages for this +picking and the chosen carrier. The goal is to minimize the number of +packages billed by the carrier. + +The chosen strategy for the theoretical number of packages is as follow: +- Split the product_weights into as many items as we have +- Try to fit the heaviest product with the lightest. If it does not work + then the heaviest should have a box for itself +- While the weight of products does not exceed the limit, continue + adding products in the same package diff --git a/delivery_estimated_package_quantity_by_weight/readme/USAGE.md b/delivery_estimated_package_quantity_by_weight/readme/USAGE.md new file mode 100644 index 0000000000..b4a35226f0 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/readme/USAGE.md @@ -0,0 +1,4 @@ +Here are some limitations of the module: +- Product packagings are not supported +- Multiple package types having different max weight for a shipping + method are not supported diff --git a/delivery_estimated_package_quantity_by_weight/static/description/icon.png b/delivery_estimated_package_quantity_by_weight/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/delivery_estimated_package_quantity_by_weight/static/description/icon.png differ diff --git a/delivery_estimated_package_quantity_by_weight/static/description/index.html b/delivery_estimated_package_quantity_by_weight/static/description/index.html new file mode 100644 index 0000000000..2b0f44fee0 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/static/description/index.html @@ -0,0 +1,452 @@ + + + + + +Delivery Estimated Package Quantity By Weight + + + +
+

Delivery Estimated Package Quantity By Weight

+ + +

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

+

This module computes the amount of packages a picking out should have +depending on the weight of the products and the limit fixed by the +carrier. It’s fully independent of the delivery_package_number module.

+

A warning is given if the number of packages for the picking out is +above what is considered as the theoretical number of packages for this +picking and the chosen carrier. The goal is to minimize the number of +packages billed by the carrier.

+

The chosen strategy for the theoretical number of packages is as follow:

+
    +
  • Split the product_weights into as many items as we have
  • +
  • Try to fit the heaviest product with the lightest. If it does not work +then the heaviest should have a box for itself
  • +
  • While the weight of products does not exceed the limit, continue +adding products in the same package
  • +
+

Table of contents

+ +
+

Usage

+

Here are some limitations of the module:

+
    +
  • Product packagings are not supported
  • +
  • Multiple package types having different max weight for a shipping +method are not supported
  • +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

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

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/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_estimated_package_quantity_by_weight/tests/__init__.py b/delivery_estimated_package_quantity_by_weight/tests/__init__.py new file mode 100644 index 0000000000..438bdb918c --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_picking diff --git a/delivery_estimated_package_quantity_by_weight/tests/test_stock_picking.py b/delivery_estimated_package_quantity_by_weight/tests/test_stock_picking.py new file mode 100644 index 0000000000..37c24e72e1 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/tests/test_stock_picking.py @@ -0,0 +1,465 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests.common import TransactionCase + + +class TestStockPicking(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product_carrier = cls.env["product.product"].create( + { + "name": "Product Carrier", + "sale_ok": False, + "type": "service", + } + ) + + cls.partner = cls.env["res.partner"].create( + {"name": "Unittest partner", "ref": "12344566777878"} + ) + + cls.delivery_carrier = cls.env["delivery.carrier"].create( + { + "name": "Unittest delivery carrier", + "maximum_weight_per_package": 37, + "product_id": cls.product_carrier.id, + } + ) + + cls.package_type = cls.env["stock.package.type"].create( + { + "name": "Pack Type 1", + } + ) + + cls.warehouse_1 = cls.env["stock.warehouse"].create( + { + "name": "Base Warehouse", + "reception_steps": "one_step", + "delivery_steps": "pick_ship", + "code": "BWH", + } + ) + + cls.product1 = cls.env["product.product"].create( + { + "name": "Product 1", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0001", + "default_code": "12341", + "weight": 25, + } + ) + + cls.product2 = cls.env["product.product"].create( + { + "name": "Product 2", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0002", + "default_code": "12342", + "weight": 30, + } + ) + + cls.product3 = cls.env["product.product"].create( + { + "name": "Product 3", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0003", + "default_code": "12343", + "weight": 30, + } + ) + + cls.product4 = cls.env["product.product"].create( + { + "name": "Product 4", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0004", + "default_code": "12344", + "weight": 0.3, + } + ) + + cls.product5 = cls.env["product.product"].create( + { + "name": "Product 5", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0005", + "default_code": "12345", + "weight": 3, + } + ) + + cls.product6 = cls.env["product.product"].create( + { + "name": "Product 6", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0006", + "default_code": "12346", + "weight": 8, + } + ) + + cls.product7 = cls.env["product.product"].create( + { + "name": "Product 7", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0007", + "default_code": "12347", + "weight": 0.6, + } + ) + + cls.product8 = cls.env["product.product"].create( + { + "name": "Product 8", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0008", + "default_code": "12348", + "weight": 2, + } + ) + + cls.product9 = cls.env["product.product"].create( + { + "name": "Product 9", + "sale_ok": True, + "type": "consu", + "is_storable": True, + "list_price": 10, + "barcode": "XXX0009", + "default_code": "12349", + "weight": 12, + } + ) + + cls.products = [ + cls.product1, + cls.product2, + cls.product3, + cls.product4, + cls.product5, + cls.product6, + cls.product7, + cls.product8, + cls.product9, + ] + + cls.picking_type_out = cls.env.ref("stock.picking_type_out") + cls.picking_type_out.default_location_src_id = cls.warehouse_1.lot_stock_id.id + cls.picking_type_out.default_location_dest_id = cls.env.ref( + "stock.stock_location_customers" + ).id + + cls.stock_location = cls.warehouse_1.lot_stock_id + cls.customer_location = cls.env.ref("stock.stock_location_customers") + + cls.so = cls._confirm_sale_order( + partner=cls.partner, products=cls.products, carrier=cls.delivery_carrier + ) + + @classmethod + def _confirm_sale_order(cls, partner=None, products=None, qty=10, carrier=None): + Sale = cls.env["sale.order"] + lines = [ + Command.create( + { + "name": p.name, + "product_id": p.id, + "product_uom_qty": qty, + "product_uom": p.uom_id.id, + "price_unit": 1, + }, + ) + for p in products + ] + so_values = { + "partner_id": partner.id, + "warehouse_id": cls.warehouse_1.id, + "order_line": lines, + } + if carrier: + so_values["carrier_id"] = carrier.id + + so = Sale.create(so_values) + so.action_confirm() + + # Set carrier and picking type on pickings + for picking in so.picking_ids: + picking.carrier_id = carrier + picking.picking_type_code = "outgoing" + + return so + + def test_all_products(self): + """ + Data: + All the products are in the SO, some are heavy, others light + Test case: + Check the number of packages in the shipping. + Each box should not exceed 37 kg + Expected result: + 34 packages + """ + ship = self.so.picking_ids.filtered(lambda p: p.picking_type_code == "outgoing") + ship.carrier_id = self.delivery_carrier + ship.picking_type_code = "outgoing" + ship.invalidate_recordset() + ship.is_number_of_packages_visible = True + ship._compute_theoretical_number_of_packages() + + self.assertEqual(ship.theoretical_number_of_packages, 34) + + def test_light_products(self): + """ + Data: + Only light products are considered here + Test case: + Check the number of packages in the shipping. + Each box should not exceed 37 kg + Expected result: + 1 package is enough + """ + products = [ + self.product4, + self.product5, + self.product6, + self.product7, + self.product8, + self.product9, + ] + so = self._confirm_sale_order( + partner=self.partner, + products=products, + qty=1, + carrier=self.delivery_carrier, + ) + + ship = so.picking_ids.filtered(lambda p: p.picking_type_code == "outgoing") + ship.invalidate_recordset() + ship.is_number_of_packages_visible = True + ship._compute_theoretical_number_of_packages() + + self.assertEqual(ship.theoretical_number_of_packages, 1) + + def test_one_product(self): + """ + Data: + Only one product is considered here + Test case: + Check the number of packages in the shipping. + Each box should not exceed 37 kg + Expected result: + 1 package is enough + """ + so = self._confirm_sale_order( + partner=self.partner, + products=[self.product4], + qty=1, + carrier=self.delivery_carrier, + ) + + ship = so.picking_ids.filtered(lambda p: p.picking_type_code == "outgoing") + ship.invalidate_recordset() + ship.is_number_of_packages_visible = True + ship._compute_theoretical_number_of_packages() + + self.assertEqual(ship.theoretical_number_of_packages, 1) + + def test_heavy_products(self): + """ + Data: + Only heavy products are considered here + Test case: + Check the number of packages in the shipping. + Each box should not exceed 30 kg + Expected result: + 30 packages are needed, one by product + """ + products = [self.product1, self.product2, self.product3] + so = self._confirm_sale_order( + partner=self.partner, + products=products, + qty=10, + carrier=self.delivery_carrier, + ) + + ship = so.picking_ids.filtered(lambda p: p.picking_type_code == "outgoing") + ship.invalidate_recordset() + ship.is_number_of_packages_visible = True + ship._compute_theoretical_number_of_packages() + + self.assertEqual(ship.theoretical_number_of_packages, 30) + + def test_put_in_pack(self): + """Test putting products in packages and validating picking""" + picking = self.env["stock.picking"].create( + { + "picking_type_id": self.picking_type_out.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + "carrier_id": self.delivery_carrier.id, + "picking_type_code": "outgoing", + } + ) + + self.env["stock.move"].create( + { + "name": self.product9.name, + "product_id": self.product9.id, + "product_uom_qty": 2, + "product_uom": self.product9.uom_id.id, + "picking_id": picking.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + } + ) + + picking.action_confirm() + self.assertTrue(picking.is_number_of_packages_visible) + self.assertEqual(picking.theoretical_number_of_packages, 1) + self.assertEqual(picking.number_of_packages_done, 0) + self.assertFalse(picking.is_number_of_packages_outranged) + + # First put in pack with quantity 1 + picking.move_ids.quantity = 1 + pack_action = picking.action_put_in_pack() + pack_action_ctx = pack_action["context"] + pack_wiz = ( + self.env["choose.delivery.package"] + .with_context(**pack_action_ctx) + .create({"delivery_package_type_id": self.package_type.id}) + ) + pack_wiz.action_put_in_pack() + self.assertEqual(picking.number_of_packages_done, 1) + self.assertFalse(picking.is_number_of_packages_outranged) + + # Second put in pack with quantity 2 + picking.move_ids.quantity = 2 + pack_action = picking.action_put_in_pack() + pack_action_ctx = pack_action["context"] + pack_wiz = ( + self.env["choose.delivery.package"] + .with_context(**pack_action_ctx) + .create({"delivery_package_type_id": self.package_type.id}) + ) + pack_wiz.action_put_in_pack() + self.assertEqual(picking.number_of_packages_done, 2) + self.assertTrue(picking.is_number_of_packages_outranged) + + # Validate picking + picking.button_validate() + self.assertEqual(picking.state, "done") + + def test_number_of_packages_edge_cases(self): + """Test edge cases for package number computations""" + picking = self.env["stock.picking"].create( + { + "picking_type_id": self.picking_type_out.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + } + ) + picking._compute_is_number_of_packages_visible() + self.assertFalse(picking.is_number_of_packages_visible) + + # No carrier maximum weight + picking.carrier_id = self.delivery_carrier + self.delivery_carrier.maximum_weight_per_package = 0 + picking._compute_is_number_of_packages_visible() + self.assertFalse(picking.is_number_of_packages_visible) + + # No moves + picking._compute_theoretical_number_of_packages() + self.assertFalse(picking.theoretical_number_of_packages) + + # No move lines + picking._compute_number_of_packages_done() + self.assertFalse(picking.number_of_packages_done) + + # Number of packages not visible + picking._compute_is_number_of_packages_outranged() + self.assertFalse(picking.is_number_of_packages_outranged) + + def test_packages_with_no_weight(self): + """ + Test package computations with products having no weight + """ + # Create a product with no weight + product_no_weight = self.env["product.product"].create( + { + "name": "No Weight Product", + "type": "consu", + "weight": 0.0, + } + ) + + # Create picking with proper carrier_id reference + picking = self.env["stock.picking"].create( + { + "picking_type_id": self.picking_type_out.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + "carrier_id": self.delivery_carrier.id, # Pass the ID instead of record + } + ) + + # Set picking type code after creation + picking.picking_type_id.code = "outgoing" + + # Add move with the no-weight product + self.env["stock.move"].create( + { + "name": product_no_weight.name, + "product_id": product_no_weight.id, + "product_uom_qty": 1, + "product_uom": product_no_weight.uom_id.id, + "picking_id": picking.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + } + ) + + picking.action_confirm() + self.delivery_carrier.maximum_weight_per_package = 37 + + # Force recompute all required fields + picking.invalidate_recordset() + picking._compute_is_number_of_packages_visible() + picking._compute_theoretical_number_of_packages() + picking._compute_number_of_packages_done() + picking._compute_is_number_of_packages_outranged() + + self.assertTrue(picking.is_number_of_packages_visible) + self.assertEqual(picking.theoretical_number_of_packages, 1) + self.assertEqual(picking.number_of_packages_done, 0) + self.assertFalse(picking.is_number_of_packages_outranged) diff --git a/delivery_estimated_package_quantity_by_weight/views/delivery_carrier_views.xml b/delivery_estimated_package_quantity_by_weight/views/delivery_carrier_views.xml new file mode 100644 index 0000000000..53c6ba418c --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/views/delivery_carrier_views.xml @@ -0,0 +1,33 @@ + + + + + delivery.carrier.form (in delivery_number_package_theoretical) + delivery.carrier + + + + + + + + delivery.carrier.tree (in delivery_number_package_theoretical) + delivery.carrier + + + + + + + + diff --git a/delivery_estimated_package_quantity_by_weight/views/stock_picking_views.xml b/delivery_estimated_package_quantity_by_weight/views/stock_picking_views.xml new file mode 100644 index 0000000000..42c53ed422 --- /dev/null +++ b/delivery_estimated_package_quantity_by_weight/views/stock_picking_views.xml @@ -0,0 +1,52 @@ + + + + + stock.picking.form (in delivery_number_package_theoretical) + stock.picking + + +
+ + +
+ + +

+

+
+

+ + / +

+
+
+
+
+