diff --git a/setup/stock_storage_type/odoo/addons/stock_storage_type b/setup/stock_storage_type/odoo/addons/stock_storage_type new file mode 120000 index 00000000000..fa235fde206 --- /dev/null +++ b/setup/stock_storage_type/odoo/addons/stock_storage_type @@ -0,0 +1 @@ +../../../../stock_storage_type \ No newline at end of file diff --git a/setup/stock_storage_type/setup.py b/setup/stock_storage_type/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_storage_type/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_storage_type/README.rst b/stock_storage_type/README.rst new file mode 100644 index 00000000000..f38ee27a312 --- /dev/null +++ b/stock_storage_type/README.rst @@ -0,0 +1,180 @@ +================== +Stock Storage Type +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9d8a507406a64928d2e380ecb28e8148fa11681dfad57c1d8c94810c1661e2a4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fwms-lightgray.png?logo=github + :target: https://github.com/OCA/wms/tree/16.0/stock_storage_type + :alt: OCA/wms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/wms-16-0/wms-16-0-stock_storage_type + :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/wms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module introduces two new models in order to manage stock moves with + packages according to the packaging and stock location properties. + +* Stock package storage type (`stock.package.storage.type`) + + This model is linked to product.packaging and defines the type of storage + related to a specific packaging. + +* Stock location storage type (`stock.location.storage.type`) + + This models is linked to stock.location and defines the types of storage + that are allowed for a specific location. + +Therefore a Stock location storage type can include different Stock package +storage type in order to validate the destination of a move with package into a +stock location. +Moreover Stock location storage type can include product, size or lot +restrictions for the stock locations it's defined on, so that a move with +package will only be allowed if it doesn't violate the restrictions defined +(cf stock_location_storage_type_strategy). + +Moreover, this module implements "storage type put-away strategy" in order to compute a +put-away location using storage types. + +The standard put-away strategy is applied *before* the storage type put-away +strategy as the former relies on product or product category and the latter +relies on stock packages. + +In other words, when a move is assigned, Odoo standard put-away strategy will be +applied to compute a new destination on the stock move lines, according to the +product. +After this first "put-away computation", the "storage type" put-away strategy +is applied, if the reserved quant is linked to a package defining a package +storage type. + +Storage locations linked to the package storage are processed sequentially, if +said storage location is a child of the move line's destination location (i.e +either the put-away location or the move's destination location). +For each location, their packs storage strategy is applied as well as the +restrictions defined on the stock location storage types. +If no suitable location is found, the next location in the sequence will be +searched and so on. + +For the packs putaway strategy "none", the location is considered as is. For +the "ordered children" strategy, children locations are sorted by first by max +height which is a physical constraint to respect, then pack putaway sequence +which allow to favor for example some level or corridor, and finally by name. + +At the end, if found location is not the same as the original destination location, +the putaway strategies are applied (e.g.: A "none" pack putaway strategy is set on +computed location and a putaway rule exists on that one). + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +Currently, the module supports only strategies applied on packages (``stock.quant.package``). +For implementations that do not use packages, it would be possible to add +compatibility with product packaging. + +The information needed from a package are: + +* the storage type, to know which strategy is applied +* the dimensions and weight, to apply constraints + +If we want to support product packaging, we would need to: + +* guess the product packaging of a move line based on the product and quantities + (multiple of a packaging quantity, for instance 8000 would be a pallet if the pallet + has 2000 units, 1900 would be Box if the Box has 100 units) +* from the product packaging, we know the storage type and dimensions + +Everywhere the module is using ``package_id``, we would have to check this: + +* use the package if a package is set +* else, use the computed packaging + +About Unit of Measures: + +In v13, there is an assumption of height to be expressed in mm and weight in kg. +In v14, packaging can be expressed in differents units. Explicit fields are introduced +like max_weight_in_kg in order make simple and efficient computations. + + +Limitation +========== + +If the locations structure is using views intensively in order to separate +storage types kindly (not mixing them), Odoo standard method to get putaway +strategy is returning the first child if a move location destination is a view. + +This is not convenient if we want to set specific strategies on that view. So, +we override standard process by returning the view itself (if no putaway is set). + +This can lead to a change on standard behavior as people will need to change manually +the location destination for pickings with views as default destination. + +Idea: maybe adding a field on view locations to say 'this is a view but don't +apply standard child location selection' could help filtering view candidates. + +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 +~~~~~~~ + +* Camptocamp +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Akim Juillerat +* Guewen Baconnier +* Raphaël Reverdy +* Jacques-Etienne Baudoux +* Laurent Mignon +* Fernando La Chica - GreenICe +* Denis Roussel + +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/wms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_storage_type/__init__.py b/stock_storage_type/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_storage_type/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_storage_type/__manifest__.py b/stock_storage_type/__manifest__.py new file mode 100644 index 00000000000..34e99aa9653 --- /dev/null +++ b/stock_storage_type/__manifest__.py @@ -0,0 +1,41 @@ +# Copyright 2019-2021 Camptocamp SA +# Copyright 2019-2021 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Stock Storage Type", + "summary": "Manage packages and locations storage types", + "version": "16.0.1.0.1", + "development_status": "Beta", + "category": "Warehouse Management", + "website": "https://github.com/OCA/wms", + "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "stock_move_line_reserved_quant", + "stock_putaway_hook", + "stock_quant_package_dimension", + "stock_storage_category_capacity_name", + ], + "data": [ + "security/ir.model.access.csv", + "views/product_template.xml", + "views/stock_location.xml", + "views/stock_storage_category.xml", + "views/stock_storage_category_capacity.xml", + "views/stock_package_level.xml", + "views/stock_package_type.xml", + "views/stock_storage_location_sequence.xml", + "views/stock_storage_location_sequence_cond.xml", + "views/storage_type_menus.xml", + ], + "demo": [ + "demo/stock_package_type.xml", + "demo/stock_storage_category.xml", + "demo/stock_storage_category_capacity.xml", + "demo/product_packaging.xml", + "demo/stock_location.xml", + "demo/stock_storage_location_sequence.xml", + ], +} diff --git a/stock_storage_type/demo/product_packaging.xml b/stock_storage_type/demo/product_packaging.xml new file mode 100644 index 00000000000..f65427ed342 --- /dev/null +++ b/stock_storage_type/demo/product_packaging.xml @@ -0,0 +1,24 @@ + + + + Single bag + 1 + + + + 4 units cardbox + 4 + + + + + 48 units pallet + 48 + + + 1200 + 800 + 1500 + 60 + + diff --git a/stock_storage_type/demo/stock_location.xml b/stock_storage_type/demo/stock_location.xml new file mode 100644 index 00000000000..44c2efc9480 --- /dev/null +++ b/stock_storage_type/demo/stock_location.xml @@ -0,0 +1,107 @@ + + + + Cardboxes storage area + + + ordered_locations + + + Bin 1 + + + + Bin 2 + + + + Bin 3 + + + + Bin 4 + + + + Pallets storage area + + + ordered_locations + + + Pallets Bin 1 + + + + Pallets Bin 2 + + + + Pallets Bin 3 + + + + Pallets Bin 4 + + + + Pallets reserve storage area + + + ordered_locations + + + Pallets Reserve Bin 1 + + + + Pallets Reserve Bin 2 + + + + Pallets Reserve Bin 3 + + + + Pallets Reserve Bin 4 + + + + Cardboxes reserve storage area + + + ordered_locations + + + Cardboxes Reserve Bin 1 + + + + Cardboxes Reserve Bin 2 + + + + Cardboxes Reserve Bin 3 + + + + Cardboxes Reserve Bin 4 + + + diff --git a/stock_storage_type/demo/stock_package_type.xml b/stock_storage_type/demo/stock_package_type.xml new file mode 100644 index 00000000000..2951f7766e2 --- /dev/null +++ b/stock_storage_type/demo/stock_package_type.xml @@ -0,0 +1,12 @@ + + + + Pallets + + + Pallets UK + + + Cardboxes + + diff --git a/stock_storage_type/demo/stock_storage_category.xml b/stock_storage_type/demo/stock_storage_category.xml new file mode 100644 index 00000000000..174b1166c17 --- /dev/null +++ b/stock_storage_type/demo/stock_storage_category.xml @@ -0,0 +1,11 @@ + + + + + Pallets + + + Cardboxes + + diff --git a/stock_storage_type/demo/stock_storage_category_capacity.xml b/stock_storage_type/demo/stock_storage_category_capacity.xml new file mode 100644 index 00000000000..5a3a536582f --- /dev/null +++ b/stock_storage_type/demo/stock_storage_category_capacity.xml @@ -0,0 +1,39 @@ + + + + + + empty + + + 1 + + + + + + 1 + + + + + + 1 + + diff --git a/stock_storage_type/demo/stock_storage_location_sequence.xml b/stock_storage_type/demo/stock_storage_location_sequence.xml new file mode 100644 index 00000000000..be6cee1cce6 --- /dev/null +++ b/stock_storage_type/demo/stock_storage_location_sequence.xml @@ -0,0 +1,47 @@ + + + + + 10 + + + + + 20 + + + + + 10 + + + + + 20 + + + diff --git a/stock_storage_type/i18n/es_AR.po b/stock_storage_type/i18n/es_AR.po new file mode 100644 index 00000000000..1d6d5ef5509 --- /dev/null +++ b/stock_storage_type/i18n/es_AR.po @@ -0,0 +1,903 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_storage_type +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2022-06-29 20:05+0000\n" +"Last-Translator: Ignacio Buioli \n" +"Language-Team: none\n" +"Language: es_AR\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.3.2\n" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence.py:0 +#, python-format +msgid "" +" * %s (WARNING: restrictions are active on " +"location storage types matching this package storage type)" +msgstr "" +" * %s (ADVERTENCIA: las restricciones están " +"activas en los tipos de almacenamiento de ubicación que coinciden con este " +"tipo de almacenamiento de paquetes)" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence.py:0 +#, python-format +msgid "" +" * %s (WARNING: no suitable location matching " +"storage type)" +msgstr "" +" * %s (ADVERTENCIA: no hay una ubicación " +"adecuada que coincida con el tipo de almacenamiento)" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "" +"The \"Put-Away sequence\" must be defined in " +"order to put away packages using this package storage type (%s)." +msgstr "" +"La \"Secuencia de almacenamiento\" debe " +"definirse para guardar paquetes utilizando este tipo de almacenamiento de " +"paquetes (%s)." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active +msgid "Active" +msgstr "Activo" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__allowed_location_dest_domain +msgid "Allowed Destinations Domain" +msgstr "Dominios de Destinos Permitidos" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__allowed_location_storage_type_ids +msgid "Allowed Location Storage Type" +msgstr "Tipo de Almacenamiento de Ubicación Permitida" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__location_storage_type_ids +msgid "Allowed locations storage types" +msgstr "Tipos de almacenamiento de ubicaciones permitidas" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__package_storage_type_ids +msgid "Allowed packages storage types" +msgstr "Tipos de almacenamiento de paquetes permitidos" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_type_form_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_location_storage_type_search_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_package_storage_type_search_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view +msgid "Archived" +msgstr "Archivado" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__barcode +msgid "Barcode" +msgstr "Código de Barras" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet +msgid "Code Snippet" +msgstr "Snippet de Código" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs +msgid "Code Snippet Docs" +msgstr "Documentación de Snippet de Código" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type +msgid "Condition Type" +msgstr "Tipo de Condición" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#, python-format +msgid "Condition type is set to `Code`: you must provide a piece of code" +msgstr "" +"Tipo de Condición configurado como `Código`: debe proveer una porción de " +"código" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "Conditions" +msgstr "Condiciones" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Content restrictions" +msgstr "Restricciones de contenido" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_product_product__product_package_storage_type_id +#: model:ir.model.fields,help:stock_storage_type.field_product_template__product_package_storage_type_id +msgid "" +"Defines a 'default' package storage type for this product to be applied on " +"packages without product packagings for put-away computations." +msgstr "" +"Define un tipo de almacenamiento de paquete 'predeterminado' para que este " +"producto se aplique en paquetes sin embalajes de producto para cálculos de " +"ubicación." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__length_uom_id +msgid "Dimensions Units of Measure" +msgstr "Dimensiones Unidades de Medida" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__do_not_mix_lots +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__do_not_mix_lots +msgid "Do Not Mix Lots" +msgstr "No Mezcla Lotes" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__do_not_mix_products +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__do_not_mix_products +msgid "Do Not Mix Products" +msgstr "No Mezcla Productos" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code +msgid "Execute code" +msgstr "Ejecutar código" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__has_restrictions +msgid "Has Restrictions" +msgstr "Tiene Restricciones" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__height_in_m +msgid "Height in m" +msgstr "Altura en m" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_package_storage_type__height_required +msgid "Height is mandatory for packages configured with this storage type." +msgstr "" +"La Altura es obligatoria para paquetes configurados con este tipo de " +"almacenamiento." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__height_required +msgid "Height required for packages" +msgstr "Altura requerida para paquetes" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__id +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id +msgid "ID" +msgstr "ID" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__do_not_mix_lots +msgid "" +"If checked, moves to the destination location will only be allowed if the " +"location contains product of the same lot." +msgstr "" +"Si está marcado, los traslados a la ubicación de destino solo se permitirán " +"si la ubicación contiene productos del mismo lote." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__do_not_mix_products +msgid "" +"If checked, moves to the destination location will only be allowed if the " +"location contains the same product." +msgstr "" +"Si está marcado, los movimientos a la ubicación de destino solo se " +"permitirán si la ubicación contiene el mismo producto." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__only_empty +msgid "" +"If checked, moves to the destination location will only be allowed if there " +"are not any existing quant nor planned move on this location" +msgstr "" +"Si está marcado, los movimientos a la ubicación de destino solo se " +"permitirán si no hay ninguna cantidad existente ni un movimiento planificado " +"en esta ubicación" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_height +msgid "" +"If defined, moves to the destination location will only be allowed if the " +"packaging height is lower than this maximum." +msgstr "" +"Si se define, los movimientos a la ubicación de destino solo se permitirán " +"si la altura del embalaje es menor que este máximo." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_weight +msgid "" +"If defined, moves to the destination location will only be allowed if the " +"packaging wight is lower than this maximum." +msgstr "" +"Si se define, los traslados a la ubicación de destino solo se permitirán si " +"el peso del embalaje es inferior a este máximo." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids +msgid "In Move" +msgstr "En Movimiento" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_line_ids +msgid "In Move Line" +msgstr "En Línea de Movimiento" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location +msgid "Inventory Locations" +msgstr "Ubicaciones de Inventario" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__leaf_child_location_ids +msgid "Leaf Child Location" +msgstr "Ubicación de Hoja Hija" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__leaf_location_ids +msgid "Leaf Location" +msgstr "Ubicación Hoja" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__length_uom_name +msgid "Length unit of measure label" +msgstr "Etiqueta de Unidad de Medida de Longitud" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__location_ids +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_id +msgid "Location" +msgstr "Ubicación" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_is_empty +msgid "Location Is Empty" +msgstr "La Ubicación está Vacía" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location_package_storage_type_rel +msgid "Location Package storage type relation" +msgstr "Ubicación Relación del tipo de almacenamiento del paquete" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_storage_type_ids +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__location_storage_type_id +msgid "Location Storage Type" +msgstr "Tipo de Ubicación de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.location_storage_type_action +#: model:ir.ui.menu,name:stock_storage_type.menu_location_storage_type_action +msgid "Location Storage Types" +msgstr "Tipos de Ubicación de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_will_contain_lot_ids +msgid "Location Will Contain Lot" +msgstr "La Ubicación Contendrá Lote" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_will_contain_product_ids +msgid "Location Will Contain Product" +msgstr "La Ubicación Contendrá Producto" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location_storage_type +msgid "Location storage type" +msgstr "Tipo de ubicación de almacenamiento" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s defines max height of %s but the package is bigger: " +"%s." +msgstr "" +"El tipo de ubicación de almacenamiento %s define una altura máxima de %s " +"pero el paquete es más grande: %s." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s defines max weight of %s but the package is " +"heavier: %s." +msgstr "" +"El tipo de ubicación de almacenamiento %s define un peso máximo de %s pero " +"el paquete es más pesado: %s." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'do not mix lots' but there are other " +"lots in location." +msgstr "" +"El tipo de ubicación de almacenamiento %s está marcado como 'no mezcla " +"lotes' pero hay otros lotes en la ubicación." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'do not mix products' but there are " +"other products in location." +msgstr "" +"El tipo de ubicación de almacenamiento %s está marcado como 'no mezcla " +"productos' pero hay otros productos en la ubicación." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'only empty' with other quants in " +"location." +msgstr "" +"El tipo de ubicación de almacenamiento %s está marcado como 'solo vacío' con " +"otras cantidades en la ubicación." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_storage_type_ids +msgid "" +"Location storage types defined here will be applied on all the children " +"locations that do not define their own location storage types." +msgstr "" +"Los tipos de ubicación de almacenamiento definidos aquí se aplicarán a todas " +"las ubicaciones secundarias que no definen sus propios tipos de ubicación de " +"almacenamiento." + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Locations" +msgstr "Ubicaciones" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_package_storage_type__location_storage_type_ids +msgid "Locations storage types that can accept such a package storage type." +msgstr "" +"Los tipos de ubicación de almacenamiento que puede aceptar dicho tipo de " +"almacenamiento de paquetes." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__allowed_location_storage_type_ids +msgid "" +"Locations storage types that this location can accept. (If no location " +"storage types are defined on this specific location, the location storage " +"types of the parent location are applied)." +msgstr "" +"Tipos de ubicaciones de almacenamiento que esta ubicación puede aceptar. (Si " +"no se definen tipos de ubicación de almacenamiento en esta ubicación " +"específica, se aplican los tipos de ubicación de almacenamiento de la " +"ubicación principal)." + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__max_height_in_m +msgid "Max height (m)" +msgstr "Altura máxima (m)" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__max_height +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_height +msgid "Max height (mm)" +msgstr "Altura máxima (mm)" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_height_in_m +msgid "Max height in m" +msgstr "Altura máxima en metros" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_weight +msgid "Max weight (kg)" +msgstr "Peso máximo (kg)" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_weight_in_kg +msgid "Max weight in kg" +msgstr "Peso máximo en kg" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name +msgid "Name" +msgstr "Nombre" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_location__pack_putaway_strategy__none +msgid "None" +msgstr "Ninguno" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__only_empty +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__only_empty +msgid "Only Empty" +msgstr "Solo Vacío" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_location__pack_putaway_strategy__ordered_locations +msgid "Ordered Children Locations" +msgstr "Ubicaciones Hijas Ordenadas" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__out_move_line_ids +msgid "Out Move Line" +msgstr "Línea de Movimiento de Salida" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__pack_putaway_sequence +msgid "Pack Putaway Sequence" +msgstr "Secuencia de Paquete de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__pack_weight_in_kg +msgid "Pack weight in kg" +msgstr "Peso del paquete en kg" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Package %s is not allowed into location %s, because there isn't any location " +"storage type that allows package storage type %s into it:\n" +"\n" +"%s" +msgstr "" +"El paquete %s no está permitido en la ubicación %s, porque no hay ningún " +"tipo de ubicación de almacenamiento que permita el tipo de almacenamiento de " +"paquetes %s en él:\n" +"\n" +"%s" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__package_storage_type_id +msgid "Package Storage Type" +msgstr "Tipo de Paquete de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.package_storage_type_action +#: model:ir.ui.menu,name:stock_storage_type.menu_package_storage_type_action +msgid "Package Storage Types" +msgstr "Tipos de Paquetes de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_package_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_product__product_package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__product_package_storage_type_id +msgid "Package storage type" +msgstr "Tipo de paquete de almacenamiento" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "Package storage type %s is not allowed into Location %s" +msgstr "" +"El tipo de paquete de almacenamiento %s no está permitido en la Ubicación %s" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__package_storage_type_id +msgid "" +"Package storage type for put-away computation. Get value automatically from " +"the packaging if set, or from the product ifthe package contains only a " +"single product." +msgstr "" +"Tipo de paquetes de almacenamiento para el cálculo de ubicación. Obtenga " +"valor automáticamente del paquete si está configurado, o del producto si el " +"paquete contiene solo un producto." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__package_storage_type_ids +msgid "" +"Package storage types that are allowed on locations where this location " +"storage type is defined." +msgstr "" +"Tipos de paquetes de almacenamiento que están permitidos en ubicaciones " +"donde se define este tipo de ubicación de almacenamiento." + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_quant_package +msgid "Packages" +msgstr "Paquetes" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__pack_putaway_strategy +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy +msgid "Packs Put-Away Strategy" +msgstr "Estrategia de Colocación de Paquetes" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_product_packaging +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__product_packaging_ids +msgid "Product Packaging" +msgstr "Empaquetado del Producto" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_product_template +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__storage_location_sequence_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_type_form_view +msgid "Put-Away sequence" +msgstr "Secuencia de Colocación" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "Put-away sequence" +msgstr "Secuencia de colocación" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_quant +msgid "Quants" +msgstr "Cantidades" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_level_tree_view_picking_inherit +msgid "Recompute Putaway" +msgstr "Recomputar Colocación" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence +msgid "Sequence of locations to put-away the package storage type" +msgstr "" +"Secuencia de ubicaciones para guardar el tipo del paquete de almacenamiento" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view +msgid "Show locations" +msgstr "Mostrar ubicaciones" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Size restrictions" +msgstr "Restricciones de tamaño" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de Inventario" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_package_level +msgid "Stock Package Level" +msgstr "Nivel de Paquete de Existencias" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond +msgid "Stock Storage Location Sequence Condition" +msgstr "Condición de Secuencia de Ubicación de Almacenamiento de Stock" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_location_sequence_cond_act_window +msgid "Stock Storage Location Sequence Conditions" +msgstr "Condiciones de Secuencia de Ubicación de Almacenamiento de Stock" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_location_sequence_cond_name +msgid "Stock storage location sequence condition name must be unique" +msgstr "" +"El nombre de la condición de secuencia de ubicación de almacenamiento de " +"stock debe ser único" + +#. module: stock_storage_type +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_location_sequence_cond_menu +msgid "Storage Location Sequence Conditions" +msgstr "Condiciones de Secuencia de Ubicación de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__storage_type_message +msgid "Storage Type Message" +msgstr "Mensaje de Tipo de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.ui.menu,name:stock_storage_type.storage_type_menu +msgid "Storage Types" +msgstr "Tipos de Almacenamiento" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.location_package_storage_type_rel_action +#: model:ir.ui.menu,name:stock_storage_type.menu_storage_type_mapping_action +msgid "Storage Types Mapping" +msgstr "Asignación de Tipos de Almacenamiento" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view +msgid "Storage locations" +msgstr "Ubicaciones de almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__storage_location_sequence_ids +msgid "Storage locations sequences" +msgstr "Secuencias de ubicaciones de almacenamiento" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_height_in_m +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_weight_in_kg +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__height_in_m +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__pack_weight_in_kg +msgid "Technical field, to speed up comparaisons" +msgstr "Campo técnico, para acelerar comparaciones" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant_package.py:0 +#, python-format +msgid "The height is mandatory on package {}." +msgstr "La altura es obligatoria en el paquete {}." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__max_height +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__max_height_in_m +msgid "The max height supported among allowed location storage types." +msgstr "" +"La altura máxima soportada entre los tipos de ubicación de almacenamiento " +"permitidos." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_product_packaging__package_storage_type_id +msgid "" +"The package storage type will be set on stock packages using this product " +"packaging, in order to compute its putaway." +msgstr "" +"El tipo del paquete de almacenamiento se establecerá en los paquetes de " +"existencias que utilicen el empaquetado de este producto para calcular su " +"entrada en inventario." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__pack_putaway_strategy +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy +msgid "" +"This defines the storage strategy to use when packs are put away in this " +"location.\n" +"None: when a pack is moved to this location, it will not be put away any " +"further.\n" +"Ordered Children Locations: when a pack is moved to this location, a " +"suitable location will be searched in its children locations according to " +"the restrictions defined on their respective location storage types." +msgstr "" +"Esto define la estrategia de almacenamiento que se utilizará cuando los " +"paquetes se guarden en esta ubicación.\n" +"Ninguno: cuando un paquete se mueve a esta ubicación, no se guardará más.\n" +"Ubicaciones Hijas Ordenadas: cuando un paquete se mueve a esta ubicación, se " +"buscará una ubicación adecuada en sus ubicaciones hijas de acuerdo con las " +"restricciones definidas en sus respectivos tipos de almacenamiento de " +"ubicación." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__length_uom_id +msgid "UoM for height" +msgstr "UdM para la altura" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__weight_uom_id +msgid "Weight Unit of Measure" +msgstr "Unidad de Medida del Peso" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__weight_uom_id +msgid "Weight Units of Measure" +msgstr "Unidades de Medida del Peso" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__weight_uom_name +msgid "Weight unit of measure label" +msgstr "Etiqueta de la unidad de medida del peso" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "" +"When a package with storage type %s is put away, the strategy will look for " +"an allowed location in the following locations:

%s

Note: this happens as long as these locations are children of the " +"stock move destination location or as long as these locations are " +"children of the destination location after the (product or category) put-" +"away is applied." +msgstr "" +"Cuando se guarda un paquete con el tipo de almacenamiento %s, la estrategia " +"buscará una ubicación permitida en las siguientes ubicaciones:

%s " +"

Nota: esto sucede siempre que estas ubicaciones sean " +"hijas de la ubicación de destino del movimiento de inventario o siempre " +"que estas ubicaciones sean hijas de la ubicación de destino después de que " +"se aplique la ubicación de (producto o categoría)." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_location_storage_type.py:0 +#, python-format +msgid "" +"You cannot set 'Do not mix lots' or 'Do not mix products' with 'Only empty' " +"constraint." +msgstr "" +"No se puede configurar 'No mezcla lotes' o 'No mezcla prodictos' con la " +"restricción 'Solo vacío'." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_location_storage_type.py:0 +#, python-format +msgid "" +"You cannot set 'Do not mix lots' without setting 'Do not mix products' " +"constraint." +msgstr "" +"No se puede configurar 'No Mezcla Lotes' sin configurar la restricción 'No " +"Mezcla Productos'." + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#, python-format +msgid "code_snippet should return boolean value into `result` variable." +msgstr "code_snippet debe devolver un valor booleano en la variable `result`." + +#. module: stock_storage_type +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_48_pallet +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_4_cardbox +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_single_bag +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_cardboxes +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_pallets +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_pallets_uk +msgid "kg" +msgstr "kg" + +#. module: stock_storage_type +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_48_pallet +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_4_cardbox +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_single_bag +msgid "m³" +msgstr "m³" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_is_empty +msgid "" +"technical field: True if the location is empty and there is no pending " +"incoming products in the location. Computed only if the location needs to " +"check for emptiness (has an \"only empty\" location storage type)." +msgstr "" +"campo técnico: Es Verdadero si la ubicación está vacía y no hay productos " +"entrantes pendientes en la ubicación. Se calcula solo si la ubicación " +"necesita verificar si está vacía (tiene un tipo de ubicación de " +"alamacenamiento como \"solo vacío\")." + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_location_ids +msgid "technical field: all the leaves locations" +msgstr "campo técnico: todas las ubicaciones hojas" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_child_location_ids +msgid "technical field: all the leaves sub-locations" +msgstr "campo técnico: todas las sub-ubicaciones hojas" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_will_contain_product_ids +msgid "" +"technical field: list of products in the location, either now or in pending " +"operations" +msgstr "" +"campo técnico: lista de productos en la ubicación, ya sea ahora o en " +"operaciones pendientes" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_will_contain_lot_ids +msgid "" +"technical field: list of stock.production.lots in the location, either now " +"or in pending operations" +msgstr "" +"campo técnico: lista de stock.production.lots en la ubicación, ya sea ahora " +"o en operaciones pendientes" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__in_move_line_ids +msgid "technical field: the pending incoming stock.move.lines in the location" +msgstr "campo técnico: stock.move.lines de entrada pendientes en la ubicación" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__in_move_ids +msgid "technical field: the pending incoming stock.moves in the location" +msgstr "campo técnico: stock.moves de entrada pendientes en la ubicación" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__out_move_line_ids +msgid "technical field: the pending outgoing stock.move.lines in the location" +msgstr "campo técnico: stock.move.lines de salida pendientes en la ubicación" diff --git a/stock_storage_type/i18n/stock_storage_type.pot b/stock_storage_type/i18n/stock_storage_type.pot new file mode 100644 index 00000000000..29912fa605c --- /dev/null +++ b/stock_storage_type/i18n/stock_storage_type.pot @@ -0,0 +1,806 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_storage_type +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.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: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence.py:0 +#, python-format +msgid "" +" * %s (WARNING: restrictions are active on " +"location storage types matching this package storage type)" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence.py:0 +#, python-format +msgid "" +" * %s (WARNING: no suitable location matching " +"storage type)" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "" +"The \"Put-Away sequence\" must be defined in " +"order to put away packages using this package storage type (%s)." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__active +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__active +msgid "Active" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__allowed_location_dest_domain +msgid "Allowed Destinations Domain" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__allowed_location_storage_type_ids +msgid "Allowed Location Storage Type" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__location_storage_type_ids +msgid "Allowed locations storage types" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__package_storage_type_ids +msgid "Allowed packages storage types" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_type_form_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_location_storage_type_search_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_package_storage_type_search_view +#: model_terms:ir.ui.view,arch_db:stock_storage_type.stock_storage_location_sequence_cond_form_view +msgid "Archived" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__barcode +msgid "Barcode" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet +msgid "Code Snippet" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__code_snippet_docs +msgid "Code Snippet Docs" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__condition_type +msgid "Condition Type" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#, python-format +msgid "Condition type is set to `Code`: you must provide a piece of code" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_sequence_cond_ids +msgid "Conditions" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Content restrictions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__create_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__create_date +msgid "Created on" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_product_product__product_package_storage_type_id +#: model:ir.model.fields,help:stock_storage_type.field_product_template__product_package_storage_type_id +msgid "" +"Defines a 'default' package storage type for this product to be applied on " +"packages without product packagings for put-away computations." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__length_uom_id +msgid "Dimensions Units of Measure" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__display_name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__do_not_mix_lots +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__do_not_mix_lots +msgid "Do Not Mix Lots" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__do_not_mix_products +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__do_not_mix_products +msgid "Do Not Mix Products" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_storage_location_sequence_cond__condition_type__code +msgid "Execute code" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__has_restrictions +msgid "Has Restrictions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__height_in_m +msgid "Height in m" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_package_storage_type__height_required +msgid "Height is mandatory for packages configured with this storage type." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__height_required +msgid "Height required for packages" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__id +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__id +msgid "ID" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__do_not_mix_lots +msgid "" +"If checked, moves to the destination location will only be allowed if the " +"location contains product of the same lot." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__do_not_mix_products +msgid "" +"If checked, moves to the destination location will only be allowed if the " +"location contains the same product." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__only_empty +msgid "" +"If checked, moves to the destination location will only be allowed if there " +"are not any existing quant nor planned move on this location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_height +msgid "" +"If defined, moves to the destination location will only be allowed if the " +"packaging height is lower than this maximum." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_weight +msgid "" +"If defined, moves to the destination location will only be allowed if the " +"packaging wight is lower than this maximum." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_ids +msgid "In Move" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__in_move_line_ids +msgid "In Move Line" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location +msgid "Inventory Locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_move____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_level____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence____last_update +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_uid +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__write_date +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__leaf_child_location_ids +msgid "Leaf Child Location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__leaf_location_ids +msgid "Leaf Location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__length_uom_name +msgid "Length unit of measure label" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__location_ids +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_id +msgid "Location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_is_empty +msgid "Location Is Empty" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location_package_storage_type_rel +msgid "Location Package storage type relation" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_storage_type_ids +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__location_storage_type_id +msgid "Location Storage Type" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.location_storage_type_action +#: model:ir.ui.menu,name:stock_storage_type.menu_location_storage_type_action +msgid "Location Storage Types" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_will_contain_lot_ids +msgid "Location Will Contain Lot" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__location_will_contain_product_ids +msgid "Location Will Contain Product" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_location_storage_type +msgid "Location storage type" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s defines max height of %s but the package is bigger:" +" %s." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s defines max weight of %s but the package is " +"heavier: %s." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'do not mix lots' but there are other " +"lots in location." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'do not mix products' but there are " +"other products in location." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Location storage type %s is flagged 'only empty' with other quants in " +"location." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_storage_type_ids +msgid "" +"Location storage types defined here will be applied on all the children " +"locations that do not define their own location storage types." +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_package_storage_type__location_storage_type_ids +msgid "Locations storage types that can accept such a package storage type." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__allowed_location_storage_type_ids +msgid "" +"Locations storage types that this location can accept. (If no location " +"storage types are defined on this specific location, the location storage " +"types of the parent location are applied)." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__max_height_in_m +msgid "Max height (m)" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__max_height +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_height +msgid "Max height (mm)" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_height_in_m +msgid "Max height in m" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_weight +msgid "Max weight (kg)" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__max_weight_in_kg +msgid "Max weight in kg" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__name +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence_cond__name +msgid "Name" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_location__pack_putaway_strategy__none +msgid "None" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__only_empty +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__only_empty +msgid "Only Empty" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields.selection,name:stock_storage_type.selection__stock_location__pack_putaway_strategy__ordered_locations +msgid "Ordered Children Locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__out_move_line_ids +msgid "Out Move Line" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__pack_putaway_sequence +msgid "Pack Putaway Sequence" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__pack_weight_in_kg +msgid "Pack weight in kg" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "" +"Package %s is not allowed into location %s, because there isn't any location storage type that allows package storage type %s into it:\n" +"\n" +"%s" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_packaging__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_package_storage_type_rel__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_quant_package__package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__package_storage_type_id +msgid "Package Storage Type" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.package_storage_type_action +#: model:ir.ui.menu,name:stock_storage_type.menu_package_storage_type_action +msgid "Package Storage Types" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_package_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_product_product__product_package_storage_type_id +#: model:ir.model.fields,field_description:stock_storage_type.field_product_template__product_package_storage_type_id +msgid "Package storage type" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant.py:0 +#, python-format +msgid "Package storage type %s is not allowed into Location %s" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__package_storage_type_id +msgid "" +"Package storage type for put-away computation. Get value automatically from " +"the packaging if set, or from the product ifthe package contains only a " +"single product." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__package_storage_type_ids +msgid "" +"Package storage types that are allowed on locations where this location " +"storage type is defined." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_quant_package +msgid "Packages" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__pack_putaway_strategy +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy +msgid "Packs Put-Away Strategy" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_product_packaging +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__product_packaging_ids +msgid "Product Packaging" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_product_template +msgid "Product Template" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__storage_location_sequence_ids +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_type_form_view +msgid "Put-Away sequence" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "Put-away sequence" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_level_tree_view_picking_inherit +msgid "Recompute Putaway" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_storage_location_sequence__sequence +msgid "Sequence" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence +msgid "Sequence of locations to put-away the package storage type" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view +msgid "Show locations" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.location_storage_type_form_view +msgid "Size restrictions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_package_level +msgid "Stock Package Level" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model,name:stock_storage_type.model_stock_storage_location_sequence_cond +msgid "Stock Storage Location Sequence Condition" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.stock_storage_location_sequence_cond_act_window +msgid "Stock Storage Location Sequence Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.constraint,message:stock_storage_type.constraint_stock_storage_location_sequence_cond_name +msgid "Stock storage location sequence condition name must be unique" +msgstr "" + +#. module: stock_storage_type +#: model:ir.ui.menu,name:stock_storage_type.stock_storage_location_sequence_cond_menu +msgid "Storage Location Sequence Conditions" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_package_storage_type__storage_type_message +msgid "Storage Type Message" +msgstr "" + +#. module: stock_storage_type +#: model:ir.ui.menu,name:stock_storage_type.storage_type_menu +msgid "Storage Types" +msgstr "" + +#. module: stock_storage_type +#: model:ir.actions.act_window,name:stock_storage_type.location_package_storage_type_rel_action +#: model:ir.ui.menu,name:stock_storage_type.menu_storage_type_mapping_action +msgid "Storage Types Mapping" +msgstr "" + +#. module: stock_storage_type +#: model_terms:ir.ui.view,arch_db:stock_storage_type.package_storage_location_tree_view +msgid "Storage locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location__storage_location_sequence_ids +msgid "Storage locations sequences" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_height_in_m +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__max_weight_in_kg +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__height_in_m +#: model:ir.model.fields,help:stock_storage_type.field_stock_quant_package__pack_weight_in_kg +msgid "Technical field, to speed up comparaisons" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_quant_package.py:0 +#, python-format +msgid "The height is mandatory on package {}." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__max_height +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__max_height_in_m +msgid "The max height supported among allowed location storage types." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_product_packaging__package_storage_type_id +msgid "" +"The package storage type will be set on stock packages using this product " +"packaging, in order to compute its putaway." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__pack_putaway_strategy +#: model:ir.model.fields,help:stock_storage_type.field_stock_storage_location_sequence__location_putaway_strategy +msgid "" +"This defines the storage strategy to use when packs are put away in this location.\n" +"None: when a pack is moved to this location, it will not be put away any further.\n" +"Ordered Children Locations: when a pack is moved to this location, a suitable location will be searched in its children locations according to the restrictions defined on their respective location storage types." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__length_uom_id +msgid "UoM for height" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location_storage_type__weight_uom_id +msgid "Weight Unit of Measure" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__weight_uom_id +msgid "Weight Units of Measure" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,field_description:stock_storage_type.field_stock_location_storage_type__weight_uom_name +msgid "Weight unit of measure label" +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_package_storage_type.py:0 +#, python-format +msgid "" +"When a package with storage type %s is put away, the strategy will look for " +"an allowed location in the following locations:

%s " +"

Note: this happens as long as these locations are " +"children of the stock move destination location or as long as these " +"locations are children of the destination location after the (product or " +"category) put-away is applied." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_location_storage_type.py:0 +#, python-format +msgid "" +"You cannot set 'Do not mix lots' or 'Do not mix products' with 'Only empty' " +"constraint." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_location_storage_type.py:0 +#, python-format +msgid "" +"You cannot set 'Do not mix lots' without setting 'Do not mix products' " +"constraint." +msgstr "" + +#. module: stock_storage_type +#: code:addons/stock_storage_type/models/stock_storage_location_sequence_cond.py:0 +#, python-format +msgid "code_snippet should return boolean value into `result` variable." +msgstr "" + +#. module: stock_storage_type +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_48_pallet +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_4_cardbox +#: model:product.packaging,weight_uom_name:stock_storage_type.product_product_9_packaging_single_bag +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_cardboxes +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_pallets +#: model:stock.location.storage.type,weight_uom_name:stock_storage_type.location_storage_type_pallets_uk +msgid "kg" +msgstr "" + +#. module: stock_storage_type +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_48_pallet +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_4_cardbox +#: model:product.packaging,volume_uom_name:stock_storage_type.product_product_9_packaging_single_bag +msgid "m³" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_is_empty +msgid "" +"technical field: True if the location is empty and there is no pending " +"incoming products in the location. Computed only if the location needs to " +"check for emptiness (has an \"only empty\" location storage type)." +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_location_ids +msgid "technical field: all the leaves locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__leaf_child_location_ids +msgid "technical field: all the leaves sub-locations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_will_contain_product_ids +msgid "" +"technical field: list of products in the location, either now or in pending " +"operations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__location_will_contain_lot_ids +msgid "" +"technical field: list of stock.production.lots in the location, either now " +"or in pending operations" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__in_move_line_ids +msgid "technical field: the pending incoming stock.move.lines in the location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__in_move_ids +msgid "technical field: the pending incoming stock.moves in the location" +msgstr "" + +#. module: stock_storage_type +#: model:ir.model.fields,help:stock_storage_type.field_stock_location__out_move_line_ids +msgid "technical field: the pending outgoing stock.move.lines in the location" +msgstr "" diff --git a/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py b/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py new file mode 100644 index 00000000000..532acba534a --- /dev/null +++ b/stock_storage_type/migrations/16.0.1.0.1/post-migrate.py @@ -0,0 +1,104 @@ +# Copyright 2022 ACSONE SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from openupgradelib import openupgrade + + +def _move_product_package_type(env): + """ + stock.package.storage.type has been merged with product.package.type + which is in core now. + """ + # Update fields values + query = """ + UPDATE stock_package_type + SET height_required = ( + SELECT height_required FROM stock_package_storage_type + WHERE id = old_storage_type_id) + """ + openupgrade.logged_query(env.cr, query) + # Update ids on product template + query = """ + UPDATE product_template + SET package_type_id = ( + SELECT id FROM stock_package_type + WHERE old_storage_type_id = product_package_storage_type_id + ) + WHERE product_package_storage_type_id IS NOT NULL + """ + openupgrade.logged_query(env.cr, query) + # Update ids on product packaging + query = """ + UPDATE product_packaging + SET package_type_id = ( + SELECT id FROM stock_package_type + WHERE old_storage_type_id = package_storage_type_id + ) + WHERE package_storage_type_id IS NOT NULL + """ + openupgrade.logged_query(env.cr, query) + + +def _move_location_storage_type(env): + """ + Update location storage type values => capacities + """ + # Update fields values + query = """ + UPDATE stock_storage_category_capacity sscc + SET allow_new_product = ( + SELECT + (CASE + WHEN do_not_mix_lots = true THEN 'same_lot' + WHEN do_not_mix_products = true + AND do_not_mix_lots = false THEN 'same' + WHEN only_empty = True THEN 'empty' + ELSE 'mixed' + END) + FROM stock_location_storage_type + WHERE id = old_location_storage_type_id) + FROM stock_location_storage_type slst + WHERE slst.id = sscc.old_location_storage_type_id + """ + openupgrade.logged_query(env.cr, query) + + +def _update_location_sequence(env): + query = """ + UPDATE stock_storage_location_sequence ssls + SET package_type_id = spt.id + FROM stock_package_type spt + WHERE spt.old_storage_type_id = ssls.package_storage_type_id + """ + openupgrade.logged_query(env.cr, query) + + +def _update_quant_package(env): + query = """ + UPDATE stock_quant_package sqp + SET package_type_id = spt.id + FROM stock_package_type spt + WHERE spt.old_storage_type_id = sqp.package_storage_type_id + """ + openupgrade.logged_query(env.cr, query) + + +def _update_product_template(env): + query = """ + ALTER TABLE product_template + DROP CONSTRAINT product_template_product_package_storage_type_id_fkey; + UPDATE product_template pt + SET product_package_storage_type_id = spt.id + FROM stock_package_type spt + WHERE spt.old_storage_type_id = pt.product_package_storage_type_id; + """ + openupgrade.logged_query(env.cr, query) + + +@openupgrade.migrate() +def migrate(env, version): + _move_product_package_type(env) + _move_location_storage_type(env) + _update_location_sequence(env) + _update_quant_package(env) + _update_product_template(env) diff --git a/stock_storage_type/migrations/16.0.1.0.1/pre-migrate.py b/stock_storage_type/migrations/16.0.1.0.1/pre-migrate.py new file mode 100644 index 00000000000..230572b90c3 --- /dev/null +++ b/stock_storage_type/migrations/16.0.1.0.1/pre-migrate.py @@ -0,0 +1,159 @@ +# Copyright 2022 ACSONE SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from openupgradelib import openupgrade + + +def _move_product_package_type(env): + """ + stock.package.storage.type has been merged with product.package.type + which is in core now. + """ + query = "ALTER TABLE stock_package_type ADD COLUMN old_storage_type_id integer" + openupgrade.logged_query(env.cr, query) + # Insert data from old model to new + query = """ + INSERT INTO stock_package_type (name, old_storage_type_id) + (SELECT name, id FROM stock_package_storage_type sspt) + RETURNING id, old_storage_type_id + """ + openupgrade.logged_query(env.cr, query) + openupgrade.merge_models( + env.cr, + "stock.package.storage.type", + "stock.package.type", + "old_storage_type_id", + ) + # Update possible xml ids + query = """ + UPDATE ir_model_data + SET model = 'stock.package.type' + WHERE model = 'stock.package.storage.type' + """ + openupgrade.logged_query(env.cr, query) + + +def _move_location_storage_type(env): + """ + stock.storage.location.type has been merged with stock.storage.category.capacity + which is in core now. + """ + query = """ + ALTER TABLE stock_storage_category_capacity + ADD COLUMN old_location_storage_type_id integer + """ + openupgrade.logged_query(env.cr, query) + + _create_categories(env) + + openupgrade.merge_models( + env.cr, + "stock.location.storage.type", + "stock.storage.category.capacity", + "old_location_storage_type_id", + ) + # Update possible xml ids + query = """ + UPDATE ir_model_data + SET model = 'stock.storage.category.capacity' + WHERE model = 'stock.location.storage.type' + """ + openupgrade.logged_query(env.cr, query) + + +def _create_categories(env): + """ + Iterate on locations that have the field location_storage_type_ids + filled in and create a storage category for each combination. + Don't duplicate capacities that have the same: + - category + - package type + """ + query = """ + SELECT DISTINCT(location_id) FROM stock_location_location_storage_type_rel; + """ + openupgrade.logged_query(env.cr, query) + result = env.cr.fetchall() + ids = [r[0] for r in result] + existing_location_type_ids = {} + for location_id in ids: + query = """ + SELECT id, name FROM stock_location_storage_type + WHERE id in ( + SELECT location_storage_type_id + FROM stock_location_location_storage_type_rel + WHERE location_id = %s + ) + """ + openupgrade.logged_query(env.cr, query, (location_id,)) + result = env.cr.fetchall() + location_type_ids = {res[0] for res in result} + name = "/".join([res[1] for res in result]) + category_id = None + for category, existing_location_type in existing_location_type_ids.items(): + if all( + location_type_id in existing_location_type + for location_type_id in location_type_ids + ): + # All ids are found in the existing category + category_id = category + break + if category_id is None: + query = """ + INSERT INTO stock_storage_category (name, allow_new_product) + VALUES (%s, 'mixed') + RETURNING id + """ + openupgrade.logged_query(env.cr, query, (name,)) + result = env.cr.fetchone() + category_id = result[0] + + # Update the location by setting the category + query = """ + UPDATE stock_location + SET storage_category_id = %s + WHERE id = %s + AND storage_category_id IS NULL; + """ + openupgrade.logged_query( + env.cr, + query, + ( + category_id, + location_id, + ), + ) + + # Get all the linked package storage types (former package_storage_type_ids) + query = """ + SELECT spt.id, rel.location_storage_type_id + FROM stock_location_package_storage_type_rel rel + JOIN stock_package_type spt + ON spt.old_storage_type_id = rel.package_storage_type_id + WHERE location_storage_type_id IN %s + """ + openupgrade.logged_query(env.cr, query, (tuple(list(location_type_ids)),)) + results = env.cr.fetchall() + for result in results: + query = """ + INSERT INTO stock_storage_category_capacity + (storage_category_id, quantity, package_type_id, old_location_storage_type_id) + VALUES (%s, 1, %s, %s) + ON CONFLICT (storage_category_id, package_type_id) DO NOTHING + """ + openupgrade.logged_query( + env.cr, + query, + ( + category_id, + result[0], + result[1], + ), + ) + existing_location_type_ids[category_id] = location_type_ids + + +@openupgrade.migrate() +def migrate(env, version): + _move_product_package_type(env) + _move_location_storage_type(env) diff --git a/stock_storage_type/models/__init__.py b/stock_storage_type/models/__init__.py new file mode 100644 index 00000000000..ef4b4162a17 --- /dev/null +++ b/stock_storage_type/models/__init__.py @@ -0,0 +1,12 @@ +from . import ( + product_template, + stock_location, + stock_package_level, + stock_package_type, + stock_quant, + stock_quant_package, + stock_storage_category, + stock_storage_category_capacity, + stock_storage_location_sequence, + stock_storage_location_sequence_cond, +) diff --git a/stock_storage_type/models/product_template.py b/stock_storage_type/models/product_template.py new file mode 100644 index 00000000000..157bc310752 --- /dev/null +++ b/stock_storage_type/models/product_template.py @@ -0,0 +1,16 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import fields, models + + +class ProductTemplate(models.Model): + + _inherit = "product.template" + + package_type_id = fields.Many2one( + "stock.package.type", + string="Package type", + help="Defines a 'default' package type for this product to be " + "applied on packages without product packagings and on put-away " + "computation based on package type for product not in a package", + ) diff --git a/stock_storage_type/models/stock_location.py b/stock_storage_type/models/stock_location.py new file mode 100644 index 00000000000..7a8a07844da --- /dev/null +++ b/stock_storage_type/models/stock_location.py @@ -0,0 +1,735 @@ +# Copyright 2019-2021 Camptocamp SA +# Copyright 2019-2021 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import logging + +from psycopg2 import sql + +from odoo import api, fields, models +from odoo.fields import Command +from odoo.tools import float_compare + +_logger = logging.getLogger(__name__) + + +class StockLocation(models.Model): + + _inherit = "stock.location" + + computed_storage_category_id = fields.Many2one( + comodel_name="stock.storage.category", + string="Computed Storage Category", + compute="_compute_computed_storage_category_id", + store=True, + recursive=True, + help="This represents the Storage Category that will be used. It depends either " + "on the category set on the location or on one of its parent.", + ) + computed_storage_capacity_ids = fields.One2many( + related="computed_storage_category_id.capacity_ids", + ) + pack_putaway_strategy = fields.Selection( + selection=[ + ("none", "None"), + ("ordered_locations", "Ordered Children Locations"), + ], + required=True, + default="none", + string="Put-Away Strategy", + help="This defines the storage strategy based on package type to use when " + "a product or package is put away in this location.\n" + "None: when moved to this location, it will not be put" + " away any further.\n" + "Ordered Children Locations: when moved to this " + "location, a suitable location will be searched in its children " + "locations according to the restrictions defined on their " + "respective location storage types.", + ) + package_type_putaway_sequence = fields.Integer( + string="Putaway Sequence", + help="Allow to sort the valid locations by sequence for the storage " + "strategy based on package type", + ) + storage_location_sequence_ids = fields.One2many( + "stock.storage.location.sequence", + "location_id", + string="Storage locations sequences", + ) + location_is_empty = fields.Boolean( + compute="_compute_location_is_empty", + store=True, + help="technical field: True if the location is empty " + "and there is no pending incoming products in the location. " + " Computed only if the location needs to check for emptiness " + '(has an "only empty" location storage type).', + recursive=True, + ) + # TODO: Maybe renaming these fields as there are already such fields + # in core but without domains. Something like 'pending_in_move_ids' + in_move_ids = fields.One2many( + "stock.move", + "location_dest_id", + domain=[ + ("state", "in", ("waiting", "confirmed", "partially_available", "assigned")) + ], + help="technical field: the pending incoming stock.moves in the location", + ) + + in_move_line_ids = fields.One2many( + "stock.move.line", + "location_dest_id", + domain=[ + ("state", "in", ("waiting", "confirmed", "partially_available", "assigned")) + ], + help="technical field: the pending incoming " + "stock.move.lines in the location", + ) + out_move_line_ids = fields.One2many( + "stock.move.line", + "location_id", + domain=[ + ("state", "in", ("waiting", "confirmed", "partially_available", "assigned")) + ], + help="technical field: the pending outgoing " + "stock.move.lines in the location", + ) + location_will_contain_lot_ids = fields.Many2many( + "stock.lot", + store=True, + compute="_compute_location_will_contain_lot_ids", + help="technical field: list of stock.lots in " + "the location, either now or in pending operations", + ) + location_will_contain_product_ids = fields.Many2many( + "product.product", + store=True, + compute="_compute_location_will_contain_product_ids", + help="technical field: list of products in " + "the location, either now or in pending operations", + ) + + leaf_location_ids = fields.Many2many( + "stock.location", + compute="_compute_leaf_location_ids", + recursive=True, + help="technical field: all the leaves locations", + ) + leaf_child_location_ids = fields.Many2many( + "stock.location", + compute="_compute_leaf_location_ids", + recursive=True, + help="technical field: all the leaves sub-locations", + ) + max_height = fields.Float( + related="computed_storage_category_id.max_height", + store=True, + recursive=True, + ) + max_height_in_m = fields.Float( + related="computed_storage_category_id.max_height_in_m", + store=True, + recursive=True, + ) + + do_not_mix_products = fields.Boolean( + compute="_compute_do_not_mix_products", store=True, recursive=True + ) + do_not_mix_lots = fields.Boolean( + compute="_compute_do_not_mix_lots", store=True, recursive=True + ) + only_empty = fields.Boolean( + compute="_compute_only_empty", store=True, recursive=True + ) + + @api.depends( + "usage", + "computed_storage_category_id.allow_new_product", + "computed_storage_category_id.capacity_ids.allow_new_product", + ) + def _compute_do_not_mix_lots(self): + """ + This computes the value that says if the location cannot have mixed lots from: + - its own Storage Category value + - one of its Storage Capacities value + """ + for rec in self: + rec.do_not_mix_lots = rec.usage == "internal" and ( + any( + storage_type.allow_new_product == "same_lot" + for storage_type in rec.computed_storage_category_id.capacity_ids + ) + or rec.computed_storage_category_id.allow_new_product == "same_lot" + ) + + @api.depends( + "usage", + "computed_storage_category_id.allow_new_product", + "computed_storage_category_id.capacity_ids.allow_new_product", + ) + def _compute_only_empty(self): + """ + This computes the value that says if the location cannot have mixed lots from: + - its own Storage Category value + - one of its Storage Capacities value + """ + for rec in self: + rec.only_empty = rec.usage == "internal" and ( + any( + storage_type.allow_new_product == "empty" + for storage_type in rec.computed_storage_category_id.capacity_ids + ) + or rec.computed_storage_category_id.allow_new_product == "empty" + ) + + @api.depends( + "usage", + "computed_storage_category_id.allow_new_product", + "computed_storage_category_id.capacity_ids.allow_new_product", + ) + def _compute_do_not_mix_products(self): + """ + This computes the value that says if the location cannot have mixed lots from: + - its own Storage Category value + - one of its Storage Capacities value + """ + for rec in self: + rec.do_not_mix_products = rec.usage == "internal" and ( + any( + storage_type.allow_new_product in ("same", "same_lot") + for storage_type in rec.computed_storage_category_id.capacity_ids + ) + or rec.computed_storage_category_id.allow_new_product + in ("same", "same_lot") + ) + + @api.depends( + "location_id", "storage_category_id", "location_id.computed_storage_category_id" + ) + def _compute_computed_storage_category_id(self): + """ + This computes the Storage Category depending on: + - its own Storage Category + - or one of its parent (along the parent path) Storage Category + """ + for location in self: + if location.storage_category_id: + location.computed_storage_category_id = location.storage_category_id + else: + parent = location.location_id + location.computed_storage_category_id = parent.storage_category_id + + @api.depends("child_ids.leaf_location_ids", "child_ids.active") + def _compute_leaf_location_ids(self): + """Compute all children leaf locations. Current location is excluded (not a child)""" + query = """ + SELECT parent.id, ARRAY_AGG(sub.id) AS leaves + FROM stock_location parent + INNER JOIN stock_location sub + ON sub.parent_path LIKE parent.parent_path || '%%' + AND sub.id != parent.id + AND sub.active + LEFT JOIN stock_location subsub + ON subsub.location_id = sub.id + AND subsub.active + WHERE + -- exclude any location which has children so we keep only leaves + subsub.id IS NULL + AND parent.id in %s + GROUP BY parent.id; + """ + self.env.cr.execute(query, (tuple(self.ids),)) + rows = dict(self.env.cr.fetchall()) + for loc in self: + leave_ids = rows.get(loc.id) + if not leave_ids: + loc.leaf_location_ids = loc + loc.leaf_child_location_ids = False + continue + leaves = self.search([("id", "in", leave_ids)]) + loc.leaf_location_ids = leaves + loc.leaf_child_location_ids = leaves + + def _should_compute_will_contain_product_ids(self): + return self.do_not_mix_products + + def _should_compute_will_contain_lot_ids(self): + return self.do_not_mix_lots + + def _should_compute_location_is_empty(self): + return self.only_empty + + @api.depends( + "quant_ids", + "in_move_ids", + "in_move_line_ids", + "do_not_mix_products", + ) + def _compute_location_will_contain_product_ids(self): + for rec in self: + if not rec._should_compute_will_contain_product_ids(): + no_product = self.env["product.product"].browse() + rec.location_will_contain_product_ids = no_product + else: + products = ( + rec.mapped("quant_ids.product_id") + | rec.mapped("in_move_ids.product_id") + | rec.mapped("in_move_line_ids.product_id") + ) + rec.location_will_contain_product_ids = products + + @api.depends( + "quant_ids", + "in_move_line_ids", + "do_not_mix_lots", + ) + def _compute_location_will_contain_lot_ids(self): + for rec in self: + if not rec._should_compute_will_contain_lot_ids(): + no_lot = self.env["stock.lot"].browse() + rec.location_will_contain_lot_ids = no_lot + else: + lots = rec.mapped("quant_ids.lot_id") | rec.mapped( + "in_move_line_ids.lot_id" + ) + rec.location_will_contain_lot_ids = lots + + @api.depends( + "quant_ids.quantity", + "out_move_line_ids.qty_done", + "in_move_ids", + "in_move_line_ids", + "only_empty", + ) + def _compute_location_is_empty(self): + for rec in self: + # No restriction should apply on customer/supplier/... + # locations and we don't need to compute is empty + # if there is no limit on the location + if not rec._should_compute_location_is_empty(): + # avoid write if not required + if not rec.location_is_empty: + rec.location_is_empty = True + continue + # we do want to keep a write here even if the value is the same + # to enforce concurrent transaction safety: 2 moves taking + # quantities in a location have to be executed sequentially + # or the location could remain "not empty" + if ( + sum(rec.quant_ids.mapped("quantity")) + - sum(rec.out_move_line_ids.mapped("qty_done")) + > 0 + or rec.in_move_ids + or rec.in_move_line_ids + ): + rec.location_is_empty = False + else: + rec.location_is_empty = True + + # method provided by "stock_putaway_hook" + def _putaway_strategy_finalizer( + self, + putaway_location, + product, + quantity=0, + package=None, + packaging=None, + additional_qty=None, + ): + putaway_location = super()._putaway_strategy_finalizer( + putaway_location, product, quantity, package, packaging, additional_qty + ) + if package: + # If package provided, the product is not set (in the get_putaway_strategy() method) + product = package.single_product_id or product + return self._get_package_type_putaway_strategy( + putaway_location, package, product, quantity + ) + + def _get_package_type(self, package, product): + # Returns the package type either from the package, either from the product + package_type = self.env["stock.package.type"].browse() + if package: + package_type = package.package_type_id + _logger.debug( + "Computing putaway for package %s of package type %s" + % (package, package_type) + ) + elif product.package_type_id: + # Get default package type on product if defined + package_type = product.package_type_id + _logger.debug( + "Computing putaway for product %s of package type %s" + % (product, product.package_type_id) + ) + return package_type + + def _get_package_type_putaway_strategy( + self, putaway_location, package, product, quantity + ): + package_type = self._get_package_type(package, product) + # exclude_sml_ids are passed into the context during the get_putaway_strategy + # call. + stock_move_line_ids = self.env.context.get("exclude_sml_ids", []) + stock_move_lines = self.env["stock.move.line"].browse(stock_move_line_ids) + quants = ( + package.quant_ids + if package + else stock_move_lines.mapped("reserved_quant_id") + ) + if not package_type: + # Fallback on standard one + return putaway_location + # TODO: Remove this and use only putaway_location as always filled in + dest_location = putaway_location or self + _logger.debug("putaway location: %s", dest_location.name) + package_locations = self.env["stock.storage.location.sequence"].search( + [ + ("package_type_id", "=", package_type.id), + ("location_id", "child_of", dest_location.ids), + ] + ) + if not package_locations: + return dest_location + + for package_sequence in package_locations: + if not package_sequence.can_be_applied(putaway_location, quants, product): + continue + pref_loc = package_sequence.location_id + storage_locations = pref_loc.get_storage_locations(products=product) + _logger.debug("Storage locations selected: %s" % storage_locations) + allowed_location = storage_locations.select_first_allowed_location( + package_type, quants, product + ) + if allowed_location: + _logger.debug( + "Applied putaway strategy to location %s" + % allowed_location.complete_name + ) + # Reapply putaway strategy if particular rules have been put on product level + # Check if the allowed location is not self to avoid recursive computations + if allowed_location != self: + final_location = allowed_location._get_putaway_strategy( + product, quantity, package + ) + return final_location + return allowed_location + _logger.debug( + "Could not find a valid putaway location, fallback to %s" + % putaway_location.complete_name + ) + return putaway_location + + def get_storage_locations(self, products=None): + # TODO support multiple products? cf ABC + self.ensure_one() + locations = self.browse() + if self.pack_putaway_strategy == "none": + locations = self + return locations + else: + products = products or self.env["product.product"] + locations = self._get_sorted_leaf_child_locations(products) + return locations + + def _get_sorted_leaf_locations_orderby(self, products): + """Return SQL orderby clause and params for sorting locations + + First, locations are ordered by max height, knowing that a max height of 0 + means "no limit" and as such it should be among the last locations. + Then, they are ordered by a sequence and name. + """ + self.env["stock.location"].flush_model( + ["max_height", "package_type_putaway_sequence", "name"] + ) + orderby = [] + if self.pack_putaway_strategy == "ordered_locations": + orderby = [ + "CASE WHEN max_height > 0 THEN max_height ELSE 'Infinity' END", + "package_type_putaway_sequence", + "name", + "id", + ] + return ", ".join(orderby), [] + + def _get_sorted_leaf_child_locations(self, products): + """Return sorted leaf sub-locations + + The locations are candidate locations that will be evaluated one per + one in order to find the first available location. They must be leaf + locations where we can actually put goods. + """ + if not self.leaf_child_location_ids: + return self.leaf_child_location_ids + query = self._where_calc([("id", "in", self.leaf_child_location_ids.ids)]) + _, where_clause, where_params = query.get_sql() + orderby_clause, orderby_params = self._get_sorted_leaf_locations_orderby( + products + ) + query = sql.SQL( + "SELECT id FROM {table} WHERE {where} ORDER BY {orderby}" + ).format( + table=sql.Identifier(self._table), + where=sql.SQL(where_clause), + orderby=sql.SQL(orderby_clause), + ) + self._cr.execute(query, where_params + orderby_params) + location_ids = [x[0] for x in self.env.cr.fetchall()] + return self.env["stock.location"].browse(location_ids) + + def select_first_allowed_location(self, package_type, quants, products): + allowed = self.select_allowed_locations(package_type, quants, products, limit=1) + return allowed + + def _domain_location_storage_type_constraints(self, package_type, quants, products): + """Compute the domain for the location storage type which match the package + storage type + + This method also checks the "capacity" constraints (height and weight) + """ + # There can be multiple location storage types for a given + # location, so we need to filter on the ones relative to the package + # we consider. + Capacity = self.env["stock.storage.category.capacity"] + compatible_location_storage_types = Capacity.search( + [("computed_location_ids", "in", self.ids)] + ) + + pertinent_loc_storagetype_domain = [ + ("id", "in", compatible_location_storage_types.ids), + ("package_type_id", "=", package_type.id), + ] + if quants.package_id.height: + pertinent_loc_storagetype_domain += [ + "|", + ("storage_category_id.max_height_in_m", "=", 0), + ( + "storage_category_id.max_height_in_m", + ">=", + quants.package_id.height_in_m, + ), + ] + package_weight_kg = ( + quants.package_id.pack_weight_in_kg + or quants.package_id.estimated_pack_weight_kg + ) + if package_weight_kg: + pertinent_loc_storagetype_domain += [ + "|", + ("storage_category_id.max_weight_in_kg", "=", 0), + ("storage_category_id.max_weight_in_kg", ">=", package_weight_kg), + ] + _logger.debug( + "pertinent storage type domain: %s", pertinent_loc_storagetype_domain + ) + return pertinent_loc_storagetype_domain + + def _allowed_locations_for_location_storage_types( + self, location_storage_types, quants, products + ): + valid_location_ids = set() + for loc_storage_type in location_storage_types: + location_domain = loc_storage_type._domain_location_storage_type( + self, quants, products + ) + _logger.debug("pertinent location domain: %s", location_domain) + locations = self.search(location_domain) + valid_location_ids |= set(locations.ids) + return self.browse(valid_location_ids) + + def _select_final_valid_putaway_locations(self, limit=None): + """Return the valid locations using the provided limit + + ``self`` contains locations already ordered and contains + only valid locations. + This method can be used as a hook to add or remove valid + locations based on other properties. Pay attention to + keep the order. + """ + return self[:limit] + + def select_allowed_locations(self, package_type, quants, products, limit=None): + """Filter allowed locations for a storage type + + ``self`` contains locations already ordered according to the + putaway strategy, so beware of the return that must keep the + same order + """ + # We have package who may be placed in a stock.location + # + # 1. On the stock.location there are location_storage_type and on the + # packages there are package_storage_type. Between both, there's a m2m + # who says which package ST can be placed in which location ST + # + # 2. On a location_ST there are some additional restrictions: a - + # capacity (volume / height / weight) and b - properties (boolean + # flags: only empty, don't mix lots, don't mix products) + Capacity = self.env["stock.storage.category.capacity"] + _logger.debug( + "select allowed location for package storage type %s (q=%s, p=%s)", + package_type.name, + quants, + products.mapped("name"), + ) + # 1: filter locations on compatible storage type + compatible_locations = self.search( + [ + ("id", "in", self.ids), + ( + "computed_storage_category_id.capacity_ids", + "in", + package_type.storage_category_capacity_ids.ids, + ), + ] + ) + pertinent_loc_s_t_domain = ( + compatible_locations._domain_location_storage_type_constraints( + package_type, quants, products + ) + ) + + pertinent_loc_storage_types = Capacity.search(pertinent_loc_s_t_domain) + + # now loop over the pertinent location storage types (there should be + # few of them) and check for properties to find suitable locations + valid_locations = ( + compatible_locations._allowed_locations_for_location_storage_types( + pertinent_loc_storage_types, quants, products + ) + ) + + valid_locations = self._order_allowed_locations(valid_locations) + valid_locations = valid_locations._select_final_valid_putaway_locations( + limit=limit + ) + + _logger.debug( + "select allowed location for package storage" + " type %s (q=%s, p=%s) found %d locations", + package_type.name, + quants, + products.mapped("name"), + len(valid_locations), + ) + return valid_locations + + def _order_allowed_locations(self, valid_locations): + """Return the ordered list of valid_locations + + By default the order should be the same as self. However, if the + valid_locations list contains locations configured to not mix products, + we must give priority to locations that already contains products + (the ones with less qty first) + """ + valid_no_mix = valid_locations.filtered("do_not_mix_products") + loc_ordered_by_qty = [] + if valid_no_mix: + StockQuant = self.env["stock.quant"] + domain_quant = [("location_id", "in", valid_no_mix.ids)] + loc_ordered_by_qty = [ + item["location_id"][0] + for item in StockQuant.read_group( + domain_quant, + ["location_id", "quantity"], + ["location_id"], + orderby="quantity", + ) + if (float_compare(item["quantity"], 0, precision_digits=2) > 0) + ] + valid_location_ids = set(valid_locations.ids) - set(loc_ordered_by_qty) + ordered_valid_location_ids = loc_ordered_by_qty + [ + id_ for id_ in self.ids if id_ in valid_location_ids + ] + valid_locations = self.browse(ordered_valid_location_ids) + return valid_locations + + @api.depends_context("fixed_child_internal_location") + def _compute_child_internal_location_ids(self): + """ + This will override the child selection by setting self as + the only child. + + TODO: Maybe adding a field on view location in order to compute this + without context changing + """ + if self.env.context.get("fixed_child_internal_location"): + internal_location_id = self.env.context.get("fixed_child_internal_location") + internal_location = self.browse(internal_location_id) + if internal_location_id: + self.update( + { + "child_internal_location_ids": [ + Command.set(internal_location.ids) + ] + } + ) + else: + return super()._compute_child_internal_location_ids() + + def _get_stock_storage_type_putaway_rules( + self, product, package=None, packaging=None + ): + """ + We have retrieved the code from stock module in order to get + the evaluated putaway rules on this location in order to determine + if we should return self or super(). + """ + self = self._check_access_putaway() + products = self.env.context.get("products", self.env["product.product"]) + products |= product + # find package type on package or packaging + package_type = self.env["stock.package.type"] + if package: + package_type = package.package_type_id + elif packaging: + package_type = packaging.package_type_id + + categ = ( + products.categ_id + if len(products.categ_id) == 1 + else self.env["product.category"] + ) + categs = categ + while categ.parent_id: + categ = categ.parent_id + categs |= categ + + putaway_rules = self.putaway_rule_ids.filtered( + lambda rule: (not rule.product_id or rule.product_id in products) + and (not rule.category_id or rule.category_id in categs) + and (not rule.package_type_ids or package_type in rule.package_type_ids) + ) + return putaway_rules + + def _get_putaway_strategy( + self, product, quantity=0, package=None, packaging=None, additional_qty=None + ): + """ + As standard Odoo method will return the first real child of a view, + this is not convenient as if a storage sequence is set on that view, + it won't be applied. + + So, we check if no putaway rule is set on the view, then set the id of + the view to context to bypass the child_internal_location_ids field. + """ + if self.usage == "view": + putaway_rules = self._get_stock_storage_type_putaway_rules( + product=product, package=package, packaging=packaging + ) + if not putaway_rules: + self_fixed_child = self.with_context( + fixed_child_internal_location=self.id + ) + return super(StockLocation, self_fixed_child)._get_putaway_strategy( + product, + quantity=quantity, + package=package, + packaging=packaging, + additional_qty=additional_qty, + ) + return super()._get_putaway_strategy( + product, + quantity=quantity, + package=package, + packaging=packaging, + additional_qty=additional_qty, + ) diff --git a/stock_storage_type/models/stock_package_level.py b/stock_storage_type/models/stock_package_level.py new file mode 100644 index 00000000000..c7d66d88c17 --- /dev/null +++ b/stock_storage_type/models/stock_package_level.py @@ -0,0 +1,100 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class StockPackageLevel(models.Model): + + _inherit = "stock.package_level" + + # We use a domain with the module 'web_domain_field', because if we use a + # many2many with a domain in the view, the onchange updating the many2many + # client side blocks the browser for several seconds if we have thousands + # of locations. + allowed_location_dest_ids = fields.Many2many( + comodel_name="stock.location", + string="Allowed Destinations", + compute="_compute_allowed_location_dest_ids", + ) + + @api.depends( + "package_id", + "package_id.package_type_id", + "package_id.package_type_id.storage_category_capacity_ids", + "package_id.package_type_id.storage_location_sequence_ids", + "package_id.package_type_id.storage_location_sequence_ids.location_id", + "package_id.package_type_id.storage_location_sequence_ids.location_id.leaf_location_ids", # noqa + # Dependency on quant_ids managed by cache invalidation on create/write + "picking_id", + "picking_id.location_dest_id", + "picking_id.package_level_ids.location_dest_id", + ) + def _compute_allowed_location_dest_ids(self): + """ + We compute here a recordset that will be used in a domain like + [("id", "in", allowed_location_dest_ids)] + """ + for pack_level in self: + picking_child_location_dest_ids = self.env["stock.location"].search( + [("id", "child_of", pack_level.picking_id.location_dest_id.id)] + ) + # For outgoing type, we don't set the location dest so avoid + # computing the domain + if ( + pack_level.package_id.package_type_id + and pack_level.picking_type_code != "outgoing" + ): + allowed_locations = pack_level._get_allowed_location_dest_ids() + # TODO check if intersect is needed as we use picking dest loc + # in _get_allowed_location_dest_ids + intersect_locations = ( + allowed_locations & picking_child_location_dest_ids + ) + # Add the pack_level actual location_dest since it is actually + # excluded by the check on incoming stock moves + intersect_locations |= pack_level.location_dest_id + pack_level.allowed_location_dest_ids = intersect_locations + elif isinstance(pack_level.id, models.NewId): + pack_level.allowed_location_dest_ids = ( + pack_level.picking_id.location_dest_id + ) + else: + pack_level.allowed_location_dest_ids = picking_child_location_dest_ids + + def _get_allowed_location_dest_ids(self): + package_locations = self.env["stock.storage.location.sequence"].search( + [ + ( + "package_type_id", + "=", + self.package_id.package_type_id.id, + ), + ("location_id", "child_of", self.picking_id.location_dest_id.id), + ] + ) + all_allowed_locations = set() + products = self.mapped("move_line_ids.product_id") + for pack_loc in package_locations: + pref_loc = pack_loc.location_id + storage_locations = pref_loc.get_storage_locations(products=products) + allowed_locations = storage_locations.select_allowed_locations( + self.package_id.package_type_id, + self.package_id.quant_ids, + products, + ) + all_allowed_locations.update(allowed_locations.ids) + return self.env["stock.location"].browse(all_allowed_locations) + + def recompute_pack_putaway(self): + for level in self: + if not level.package_id.quant_ids: + continue + level.location_dest_id = ( + level.location_dest_id._get_package_type_putaway_strategy( + level.location_dest_id, + level.package_id, + level.mapped("move_line_ids.product_id"), + 1.0, + ) + ) diff --git a/stock_storage_type/models/stock_package_type.py b/stock_storage_type/models/stock_package_type.py new file mode 100644 index 00000000000..ab93b299c33 --- /dev/null +++ b/stock_storage_type/models/stock_package_type.py @@ -0,0 +1,71 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, fields, models + + +class StockPackageType(models.Model): + + _inherit = "stock.package.type" + + product_packaging_ids = fields.One2many("product.packaging", "package_type_id") + storage_location_sequence_ids = fields.One2many( + "stock.storage.location.sequence", + "package_type_id", + string="Put-Away sequence", + ) + storage_type_message = fields.Html(compute="_compute_storage_type_message") + height_required = fields.Boolean( + string="Height required for packages", + help=("Height is mandatory for packages configured with this storage type."), + default=False, + ) + barcode = fields.Char(copy=False) + # TODO: Check if this is convenient with the constraint on barcode field + # in core module + active = fields.Boolean(default=True) + + @api.depends("storage_location_sequence_ids") + def _compute_storage_type_message(self): + for package_type in self: + storage_locations = package_type.storage_location_sequence_ids + if storage_locations: + formatted_storage_locations_msgs = [] + last = False + for sl in storage_locations: + # check if we're on the last element + if sl == storage_locations[-1]: + last = True + formatted_storage_locations_msgs.append( + sl._format_package_storage_type_message(last=last) + ) + msg = _( + "When a package with storage type {name} is put away, the " + "strategy will look for an allowed location in the " + "following locations:

" + "{message}

" + "Note: this happens as long as these locations " + "are children of the stock move destination location " + "or as long as these locations are children of the " + "destination location after the (product or category) " + "put-away is applied." + ).format( + name=package_type.name, + message="
".join(formatted_storage_locations_msgs), + ) + else: + msg = _( + 'The "Put-Away sequence" ' + "must be defined in order to put away packages using " + "this package storage type ({storage})." + ).format(storage=package_type.name) + package_type.storage_type_message = msg + + def action_view_storage_locations(self): + return { + "name": _("Put-away sequence"), + "type": "ir.actions.act_window", + "res_model": "stock.storage.location.sequence", + "view_mode": "list", + "domain": [("package_type_id", "=", self.id)], + "context": {"default_package_type_id": self.id}, + } diff --git a/stock_storage_type/models/stock_quant.py b/stock_storage_type/models/stock_quant.py new file mode 100644 index 00000000000..ce08ee4ef23 --- /dev/null +++ b/stock_storage_type/models/stock_quant.py @@ -0,0 +1,155 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, models +from odoo.exceptions import ValidationError + + +class StockQuant(models.Model): + + _inherit = "stock.quant" + + @api.constrains("package_id", "location_id", "lot_id", "product_id") + def _check_storage_capacities(self): + """ + Check if at least one storage capacity allows the package type + into the quant's location + """ + for quant in self: + location = quant.location_id + package_type = quant.package_id.package_type_id + storage_capacities = location.computed_storage_category_id.capacity_ids + if not quant.package_id or not package_type or not storage_capacities: + continue + allowed_capacities = storage_capacities.filtered( + lambda capacity: package_type == capacity.package_type_id + ) + if not allowed_capacities: + raise ValidationError( + _( + "Package type {storage} is not allowed into " + "Location {location}" + ).format(storage=package_type.name, location=location.name) + ) + allowed = False + package_weight_kg = ( + quant.package_id.pack_weight_in_kg + or quant.package_id.estimated_pack_weight_kg + ) + package_quants = quant.package_id.mapped("quant_ids") + package_products = package_quants.mapped("product_id") + package_lots = package_quants.mapped("lot_id") + other_quants_in_location = self.search( + [ + ("location_id", "=", location.id), + ("id", "not in", package_quants.ids), + ("quantity", ">", 0), + ] + ) + products_in_location = other_quants_in_location.mapped("product_id") + lots_in_location = other_quants_in_location.mapped("lot_id") + capacity_fails = [] + for capacity in allowed_capacities: + # Check content constraints + if capacity.allow_new_product == "empty" and other_quants_in_location: + capacity_fails.append( + _( + "Storage Capacity {storage_capacity} is flagged " + "'only empty'" + " with other quants in location." + ).format(storage_capacity=capacity.display_name) + ) + continue + if capacity.allow_new_product == "same" and ( + len(package_products) > 1 + or len(products_in_location) >= 1 + and package_products != products_in_location + ): + capacity_fails.append( + _( + "Storage Capacity {storage_capacity} is flagged 'do not mix" + " products' but there are other products in " + "location." + ).format(storage_capacity=capacity.display_name) + ) + continue + if capacity.allow_new_product == "same_lot" and ( + len(package_lots) > 1 + or len(lots_in_location) >= 1 + and package_lots != lots_in_location + ): + capacity_fails.append( + _( + "Storage Capacity {storage_capacity} is flagged 'do not mix" + " lots' but there are other lots in " + "location." + ).format(storage_capacity=capacity.display_name) + ) + continue + # Check size constraint + if ( + capacity.storage_category_id.max_height_in_m + and quant.package_id.height_in_m + > capacity.storage_category_id.max_height_in_m + ): + capacity_fails.append( + _( + "Storage Category {storage_category} defines " + "max height of {max_h} but the package is bigger: " + "{height}." + ).format( + storage_category=capacity.storage_category_id.display_name, + max_h=capacity.storage_category_id.max_height_in_m, + height=quant.package_id.height_in_m, + ) + ) + continue + if ( + capacity.storage_category_id.max_weight_in_kg + and package_weight_kg + > capacity.storage_category_id.max_weight_in_kg + ): + capacity_fails.append( + _( + "Storage Category {storage_category} defines " + "max weight of {max_w} but the package is heavier: " + "{weight_kg}." + ).format( + storage_category=capacity.storage_category_id.display_name, + max_w=capacity.storage_category_id.max_weight_in_kg, + weight_kg=package_weight_kg, + ) + ) + continue + # If we get here, it means there is a location storage type + # allowing the package into the location + allowed = True + break + if not allowed: + raise ValidationError( + _( + "Package {package} is not allowed into location {location}," + " because there isn't any storage capacity that allows" + " package type {type} into it:\n\n{fails}" + ).format( + package=quant.package_id.name, + location=location.complete_name, + type=package_type.name, + fails="\n".join(capacity_fails), + ) + ) + + def write(self, vals): + res = super().write(vals) + self._invalidate_package_level_allowed_location_dest_ids() + return res + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + self._invalidate_package_level_allowed_location_dest_ids() + return res + + def _invalidate_package_level_allowed_location_dest_ids(self): + self.env["stock.package_level"].invalidate_model( + fnames=["allowed_location_dest_ids"] + ) diff --git a/stock_storage_type/models/stock_quant_package.py b/stock_storage_type/models/stock_quant_package.py new file mode 100644 index 00000000000..3967e8ef59a --- /dev/null +++ b/stock_storage_type/models/stock_quant_package.py @@ -0,0 +1,93 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockQuantPackage(models.Model): + + _inherit = "stock.quant.package" + + pack_weight_in_kg = fields.Float( + help="Technical field, to speed up comparaisons", + compute="_compute_pack_weight_in_kg", + store=True, + ) + height_in_m = fields.Float( + help="Technical field, to speed up comparaisons", + compute="_compute_height_in_m", + store=True, + ) + + @api.depends("pack_weight", "weight_uom_id") + def _compute_pack_weight_in_kg(self): + uom_kg = self.env.ref("uom.product_uom_kgm") + for package in self: + package.pack_weight_in_kg = package.weight_uom_id._compute_quantity( + qty=package.pack_weight, + to_unit=uom_kg, + round=False, + ) + + @api.depends("height", "length_uom_id") + def _compute_height_in_m(self): + uom_meters = self.env.ref("uom.product_uom_meter") + for package in self: + package.height_in_m = package.length_uom_id._compute_quantity( + qty=package.height, + to_unit=uom_meters, + round=False, + ) + + @api.constrains("height", "package_type_id", "product_packaging_id") + def _check_package_type_height_required(self): + for package in self: + if package.package_type_id.height_required and not package.height: + raise ValidationError( + _("The height is mandatory on package {}.").format(package.name) + ) + + def auto_assign_packaging(self): + res = super().auto_assign_packaging() + for package in self: + if not package.package_type_id: + # if no storage type could be set by auto assign, + # fallback on the default product's storage type (if any) + package._sync_package_type_from_single_product() + return res + + @api.model_create_multi + def create(self, vals): + records = super().create(vals) + records._sync_package_type_from_packaging() + return records + + def write(self, vals): + result = super().write(vals) + if vals.get("product_packaging_id"): + self._sync_package_type_from_packaging() + return result + + def _sync_package_type_from_packaging(self): + for package in self: + if package.package_type_id: + # Do not set package storage type for delivery packages + # to not trigger constraint like height requirement + # (we are delivering them, not storing them) + continue + package_type = package.product_packaging_id.package_type_id + if not package_type: + continue + package.package_type_id = package_type + + def _sync_package_type_from_single_product(self): + for package in self: + if package.package_type_id: + # Do not set package type for delivery packages + # to not trigger constraint like height requirement + # (we are delivering them, not storing them) + continue + package_type = package.single_product_id.package_type_id + if not package_type: + continue + package.package_type_id = package_type diff --git a/stock_storage_type/models/stock_storage_category.py b/stock_storage_type/models/stock_storage_category.py new file mode 100644 index 00000000000..63b794c38f5 --- /dev/null +++ b/stock_storage_type/models/stock_storage_category.py @@ -0,0 +1,94 @@ +# Copyright 2022 ACSONE SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, fields, models + + +class StockStorageCategory(models.Model): + + _inherit = "stock.storage.category" + + allow_new_product = fields.Selection( + selection_add=[("same_lot", "If lots are all the same")], + ondelete={"same_lot": "cascade"}, + ) + + computed_location_ids = fields.One2many( + comodel_name="stock.location", inverse_name="computed_storage_category_id" + ) + + # TODO: Move these fields in another module ? + max_height = fields.Float( + string="Max height (mm)", + help="The max height supported for this storage category.", + ) + + max_height_in_m = fields.Float( + help="Technical field, to speed up comparaisons", + compute="_compute_max_height_in_m", + store=True, + ) + weight_uom_id = fields.Many2one( + # Same as product.packing + "uom.uom", + string="Weight Units of Measure", + domain=lambda self: [ + ("category_id", "=", self.env.ref("uom.product_uom_categ_kgm").id) + ], + help="Weight Unit of Measure", + compute=False, + default=lambda self: self.env[ + "product.template" + ]._get_weight_uom_id_from_ir_config_parameter(), + ) + weight_uom_name = fields.Char( + # Same as product.packing + string="Weight unit of measure label", + related="weight_uom_id.name", + readonly=True, + ) + max_weight_in_kg = fields.Float( + help="Technical field, to speed up comparaisons", + compute="_compute_max_weight_in_kg", + store=True, + ) + + length_uom_id = fields.Many2one( + # Same as product.packing + "uom.uom", + "Dimensions Units of Measure", + domain=lambda self: [ + ("category_id", "=", self.env.ref("uom.uom_categ_length").id) + ], + help="UoM for height", + default=lambda self: self.env[ + "product.template" + ]._get_length_uom_id_from_ir_config_parameter(), + ) + + _sql_constraints = [ + ( + "positive_max_height", + "CHECK(max_height >= 0)", + "Max height should be a positive number.", + ), + ] + + @api.depends("max_height", "length_uom_id") + def _compute_max_height_in_m(self): + uom_m = self.env.ref("uom.product_uom_meter") + for slst in self: + slst.max_height_in_m = slst.length_uom_id._compute_quantity( + qty=slst.max_height, + to_unit=uom_m, + round=False, + ) + + @api.depends("max_weight") + def _compute_max_weight_in_kg(self): + uom_kg = self.env.ref("uom.product_uom_kgm") + for slst in self: + slst.max_weight_in_kg = slst.weight_uom_id._compute_quantity( + qty=slst.max_weight, + to_unit=uom_kg, + round=False, + ) diff --git a/stock_storage_type/models/stock_storage_category_capacity.py b/stock_storage_type/models/stock_storage_category_capacity.py new file mode 100644 index 00000000000..e007d398bf1 --- /dev/null +++ b/stock_storage_type/models/stock_storage_category_capacity.py @@ -0,0 +1,111 @@ +# Copyright 2022 ACSONE SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, fields, models + + +class StorageCategoryProductCapacity(models.Model): + + _inherit = "stock.storage.category.capacity" + + allow_new_product = fields.Selection( + selection=[ + ("empty", "If the location is empty"), + ("same", "If all products are same"), + ("mixed", "Allow mixed products"), + ("same_lot", "If all lots are the same"), + ], + default="mixed", + required=True, + ) + computed_location_ids = fields.One2many( + comodel_name="stock.location", + related="storage_category_id.computed_location_ids", + ) + has_restrictions = fields.Boolean( + compute="_compute_has_restrictions", + help="Technical: This is used to check if we need to display warning message", + ) + + @api.model + def _get_display_name_attributes(self): + """ + Adds the storage capacity attributes to compose the display name + """ + attributes = super()._get_display_name_attributes() + value = self._fields["allow_new_product"].convert_to_export( + self.allow_new_product, self + ) + attributes.append(_("Allow New Product: ") + value) + return attributes + + @api.model + def _compute_display_name_depends(self): + depends = super()._compute_display_name_depends() + depends.append("allow_new_product") + return depends + + @api.depends( + "allow_new_product", + "storage_category_id.max_height", + "storage_category_id.max_weight", + ) + def _compute_has_restrictions(self): + """ + A storage capacity has restrictions when it: + - does not accept mixed products + - or does not accept mixed lots + - or do have a maximum height set on its category + - or do have a maximum weight set on its category + """ + for capacity in self: + capacity.has_restrictions = any( + [ + capacity.allow_new_product != "mixed", + capacity.storage_category_id.max_height, + capacity.storage_category_id.max_weight, + ] + ) + + def _get_product_location_domain(self, products): + """ + Helper to get products location domain + """ + return [ + "|", + # Ideally, we would like a domain which is a strict comparison: + # if we do not mix products, we should be able to filter on == + # product.id. Here, if we can create a move for product B and + # set it's destination in a location already used by product A, + # then all the new moves for product B will be allowed in the + # location. + ("location_will_contain_product_ids", "in", products.ids), + ("location_will_contain_product_ids", "=", False), + ] + + def _domain_location_storage_type(self, candidate_locations, quants, products): + """ + Compute a domain which applies the constraint of the + Stock Storage Category Capacities to select locations among candidate + locations. + """ + self.ensure_one() + location_domain = [ + ("id", "in", candidate_locations.ids), + ("computed_storage_category_id.capacity_ids", "in", self.ids), + ] + # Build the domain using the 'allow_new_product' field + if self.allow_new_product == "empty": + location_domain.append(("location_is_empty", "=", True)) + elif self.allow_new_product == "same": + location_domain += self._get_product_location_domain(products) + elif self.allow_new_product == "same_lot": + lots = quants.mapped("lot_id") + # As same lot should filter also on same product + location_domain += self._get_product_location_domain(products) + location_domain += [ + "|", + # same comment as for the products + ("location_will_contain_lot_ids", "in", lots.ids), + ("location_will_contain_lot_ids", "=", False), + ] + return location_domain diff --git a/stock_storage_type/models/stock_storage_location_sequence.py b/stock_storage_type/models/stock_storage_location_sequence.py new file mode 100644 index 00000000000..f789317c1f8 --- /dev/null +++ b/stock_storage_type/models/stock_storage_location_sequence.py @@ -0,0 +1,91 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, fields, models + + +class StockStorageLocationSequence(models.Model): + + _name = "stock.storage.location.sequence" + _description = "Sequence of locations to put-away the package storage type" + _order = "sequence" + + package_type_id = fields.Many2one("stock.package.type", required=True) + sequence = fields.Integer(required=True) + location_id = fields.Many2one( + "stock.location", + required=True, + ) + location_putaway_strategy = fields.Selection( + related="location_id.pack_putaway_strategy" + ) + location_sequence_cond_ids = fields.Many2many( + string="Conditions", + comodel_name="stock.storage.location.sequence.cond", + relation="stock_location_sequence_cond_rel", + ) + + def _format_package_storage_type_message(self, last=False): + self.ensure_one() + # TODO improve ugly code + type_matching_locations = self.location_id.get_storage_locations().filtered( + lambda l: self.package_type_id + in l.computed_storage_category_id.capacity_ids.mapped("package_type_id") + ) + if type_matching_locations: + # Get the selection description + pack_storage_strat = None + pack_storage_strat_selection = self.location_id._fields[ + "pack_putaway_strategy" + ]._description_selection(self.env) + for strat in pack_storage_strat_selection: + if strat[0] == self.location_id.pack_putaway_strategy: + pack_storage_strat = strat[1] + break + msg = ' * {} ({})'.format( + self.location_id.name, + pack_storage_strat, + ) + if last: + # If last, we want to check if restrictions are defined on + # location storage types accepting this package storage type + # TODO improve ugly code + capacities = type_matching_locations.mapped( + "computed_storage_category_id.capacity_ids" + ).filtered( + lambda lst, package_type=self.package_type_id: package_type + == lst.package_type_id + and not lst.has_restrictions + ) + if not capacities: + msg = _( + ' * {location} (WARNING: ' + "restrictions are active on location storage types " + "matching this package storage type)" + ).format(location=self.location_id.name) + else: + msg = _( + ' * {location} ' + "(WARNING: no suitable location matching storage type)" + ).format(location=self.location_id.name) + return msg + + def button_show_locations(self): + xmlid = "stock.action_location_form" + action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) + action["domain"] = [ + ("parent_path", "=ilike", "{}%".format(self.location_id.parent_path)), + ( + "computed_storage_capacity_ids", + "in", + self.package_type_id.storage_category_capacity_ids.ids, + ), + ] + return action + + def can_be_applied(self, putaway_location, quant, product): + """Check if conditions are met.""" + self.ensure_one() + for cond in self.location_sequence_cond_ids: + if not cond.evaluate(self, putaway_location, quant, product): + return False + return True diff --git a/stock_storage_type/models/stock_storage_location_sequence_cond.py b/stock_storage_type/models/stock_storage_location_sequence_cond.py new file mode 100644 index 00000000000..04a2cf1c662 --- /dev/null +++ b/stock_storage_type/models/stock_storage_location_sequence_cond.py @@ -0,0 +1,147 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +import textwrap + +from odoo import _, api, exceptions, fields, models +from odoo.tools import safe_eval + +_logger = logging.getLogger(__name__) + + +class StockStorageLocationSequenceCond(models.Model): + + _name = "stock.storage.location.sequence.cond" + _description = "Stock Storage Location Sequence Condition" + + name = fields.Char(required=True) + + condition_type = fields.Selection( + selection=[("code", "Execute code")], default="code", required=True + ) + code_snippet = fields.Text(required=True) + code_snippet_docs = fields.Text( + compute="_compute_code_snippet_docs", + default=lambda self: self._default_code_snippet_docs(), + ) + + active = fields.Boolean( + default=True, + ) + + _sql_constraints = [ + ( + "name", + "EXCLUDE (name WITH =) WHERE (active = True)", + "Stock storage location sequence condition name must be unique", + ) + ] + + def _compute_code_snippet_docs(self): + for rec in self: + rec.code_snippet_docs = textwrap.dedent(rec._default_code_snippet_docs()) + + @api.constrains("condition_type", "code_snippet") + def _check_condition_type_code(self): + for rec in self.filtered(lambda c: c.condition_type == "code"): + if not rec._code_snippet_valued(): + raise exceptions.UserError( + _( + "Condition type is set to `Code`: you must provide a piece of code" + ) + ) + + def _default_code_snippet_docs(self): + return """ + Available vars: + * storage_location_sequence + * condition + * putaway_location + * quant + * product + * env + * datetime + * dateutil + * time + * user + * exceptions + + Must initialize a boolean 'result' variable set to True when condition is met + + """ + + def _get_code_snippet_eval_context( + self, storage_location_sequence, putaway_location, quant, product + ): + """Prepare the context used when evaluating python code + :returns: dict -- evaluation context given to safe_eval + """ + self.ensure_one() + return { + "env": self.env, + "user": self.env.user, + "condition": self, + "putaway_location": putaway_location, + "quant": quant, + "product": product, + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, + "time": safe_eval.time, + "storage_location_sequence": storage_location_sequence, + "exceptions": safe_eval.wrap_module( + exceptions, ["UserError", "ValidationError"] + ), + } + + def _exec_code(self, storage_location_sequence, putaway_location, quant, product): + self.ensure_one() + if not self._code_snippet_valued(): + return False + eval_ctx = self._get_code_snippet_eval_context( + storage_location_sequence, putaway_location, quant, product + ) + snippet = self.code_snippet + safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True) + result = eval_ctx.get("result") + if not isinstance(result, bool): + raise exceptions.UserError( + _("code_snippet should return boolean value into `result` variable.") + ) + if not result: + _logger.debug( + "Condition %s not met:\n" + "* putaway sequence: %s\n" + "* putaway location: %s\n" + "* quant: %s\n" + "* product: %s\n" + % ( + self.name, + storage_location_sequence.id, + putaway_location.name, + quant.id, + product.display_name, + ) + ) + return result + + def _code_snippet_valued(self): + self.ensure_one() + snippet = self.code_snippet or "" + return bool( + [ + not line.startswith("#") + for line in (snippet.splitlines()) + if line.strip("") + ] + ) + + def evaluate(self, storage_location_sequence, putaway_location, quant, product): + self.ensure_one() + if self.condition_type == "code": + return self._exec_code( + storage_location_sequence, putaway_location, quant, product + ) + condition_type = self.condition_type + raise exceptions.UserError( + _(f"Not able to evaluate condition of type {condition_type}") + ) diff --git a/stock_storage_type/readme/CONFIGURATION.rst b/stock_storage_type/readme/CONFIGURATION.rst new file mode 100644 index 00000000000..00a0c3f399c --- /dev/null +++ b/stock_storage_type/readme/CONFIGURATION.rst @@ -0,0 +1,31 @@ +Got to "Inventory > Settings > Storage Types", to define Package Storage Types +and Location Storage Types. + +Package Storage Type can be defined on Product Packaging form view from the +product form view. + +Location Storage Type can be added to any stock location and will be computed +automatically as Allowed Locations Storage Types on said stock location's +children location. + + +- Pack put-away strategy + +On stock locations, you can define a "Pack put-away strategy" as "Ordered bins", +so that any move, having this locations as its destination, will be put-away +on a children location, according to the restrictions from storage types. + +- Put-away sequence + +For any package storage types, you must define a Put-away sequence (i.e. stock +location to search) where such a package is allowed to be put-away. Locations +will be processed sequentially and the first one having an allowed child +location (according to restrictions) will be used to put away. + +A good practice here, is to set a location accepting this storage type without +any restriction as the last location in the sequence, to act as a fallback +if no other location could be found before. + +If a location with a 'none' strategy is set in the sequence and matches with the +move line's destination, it will stop evaluating the next locations in the +sequence. diff --git a/stock_storage_type/readme/CONTRIBUTORS.rst b/stock_storage_type/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..168bba9f427 --- /dev/null +++ b/stock_storage_type/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Akim Juillerat +* Guewen Baconnier +* Raphaël Reverdy +* Jacques-Etienne Baudoux +* Laurent Mignon +* Fernando La Chica - GreenICe +* Denis Roussel diff --git a/stock_storage_type/readme/DESCRIPTION.rst b/stock_storage_type/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..bd8d800bb2a --- /dev/null +++ b/stock_storage_type/readme/DESCRIPTION.rst @@ -0,0 +1,51 @@ +This module introduces two new models in order to manage stock moves with + packages according to the packaging and stock location properties. + +* Stock package storage type (`stock.package.storage.type`) + + This model is linked to product.packaging and defines the type of storage + related to a specific packaging. + +* Stock location storage type (`stock.location.storage.type`) + + This models is linked to stock.location and defines the types of storage + that are allowed for a specific location. + +Therefore a Stock location storage type can include different Stock package +storage type in order to validate the destination of a move with package into a +stock location. +Moreover Stock location storage type can include product, size or lot +restrictions for the stock locations it's defined on, so that a move with +package will only be allowed if it doesn't violate the restrictions defined +(cf stock_location_storage_type_strategy). + +Moreover, this module implements "storage type put-away strategy" in order to compute a +put-away location using storage types. + +The standard put-away strategy is applied *before* the storage type put-away +strategy as the former relies on product or product category and the latter +relies on stock packages. + +In other words, when a move is assigned, Odoo standard put-away strategy will be +applied to compute a new destination on the stock move lines, according to the +product. +After this first "put-away computation", the "storage type" put-away strategy +is applied, if the reserved quant is linked to a package defining a package +storage type. + +Storage locations linked to the package storage are processed sequentially, if +said storage location is a child of the move line's destination location (i.e +either the put-away location or the move's destination location). +For each location, their packs storage strategy is applied as well as the +restrictions defined on the stock location storage types. +If no suitable location is found, the next location in the sequence will be +searched and so on. + +For the packs putaway strategy "none", the location is considered as is. For +the "ordered children" strategy, children locations are sorted by first by max +height which is a physical constraint to respect, then pack putaway sequence +which allow to favor for example some level or corridor, and finally by name. + +At the end, if found location is not the same as the original destination location, +the putaway strategies are applied (e.g.: A "none" pack putaway strategy is set on +computed location and a putaway rule exists on that one). diff --git a/stock_storage_type/readme/ROADMAP.rst b/stock_storage_type/readme/ROADMAP.rst new file mode 100644 index 00000000000..9816b1c35f7 --- /dev/null +++ b/stock_storage_type/readme/ROADMAP.rst @@ -0,0 +1,43 @@ +Currently, the module supports only strategies applied on packages (``stock.quant.package``). +For implementations that do not use packages, it would be possible to add +compatibility with product packaging. + +The information needed from a package are: + +* the storage type, to know which strategy is applied +* the dimensions and weight, to apply constraints + +If we want to support product packaging, we would need to: + +* guess the product packaging of a move line based on the product and quantities + (multiple of a packaging quantity, for instance 8000 would be a pallet if the pallet + has 2000 units, 1900 would be Box if the Box has 100 units) +* from the product packaging, we know the storage type and dimensions + +Everywhere the module is using ``package_id``, we would have to check this: + +* use the package if a package is set +* else, use the computed packaging + +About Unit of Measures: + +In v13, there is an assumption of height to be expressed in mm and weight in kg. +In v14, packaging can be expressed in differents units. Explicit fields are introduced +like max_weight_in_kg in order make simple and efficient computations. + + +Limitation +========== + +If the locations structure is using views intensively in order to separate +storage types kindly (not mixing them), Odoo standard method to get putaway +strategy is returning the first child if a move location destination is a view. + +This is not convenient if we want to set specific strategies on that view. So, +we override standard process by returning the view itself (if no putaway is set). + +This can lead to a change on standard behavior as people will need to change manually +the location destination for pickings with views as default destination. + +Idea: maybe adding a field on view locations to say 'this is a view but don't +apply standard child location selection' could help filtering view candidates. diff --git a/stock_storage_type/security/ir.model.access.csv b/stock_storage_type/security/ir.model.access.csv new file mode 100644 index 00000000000..ed6a03817a1 --- /dev/null +++ b/stock_storage_type/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_storage_location_sequence_user,access_stock_storage_location_sequence_user,model_stock_storage_location_sequence,base.group_user,1,0,0,0 +access_stock_storage_location_sequence_manager,access_stock_storage_location_sequence_manager,model_stock_storage_location_sequence,stock.group_stock_manager,1,1,1,1 +access_stock_storage_location_sequence_cond_user,access_stock_storage_location_sequence_cond_user,model_stock_storage_location_sequence_cond,base.group_user,1,0,0,0 +access_stock_storage_location_sequence_cond_manager,access_stock_storage_location_sequence_cond_manager,model_stock_storage_location_sequence_cond,stock.group_stock_manager,1,1,1,1 diff --git a/stock_storage_type/static/description/icon.png b/stock_storage_type/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/stock_storage_type/static/description/icon.png differ diff --git a/stock_storage_type/static/description/index.html b/stock_storage_type/static/description/index.html new file mode 100644 index 00000000000..e2231f6fc0e --- /dev/null +++ b/stock_storage_type/static/description/index.html @@ -0,0 +1,514 @@ + + + + + + +Stock Storage Type + + + +
+

Stock Storage Type

+ + +

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

+
+
This module introduces two new models in order to manage stock moves with
+
packages according to the packaging and stock location properties.
+
+
    +
  • Stock package storage type (stock.package.storage.type)

    +

    This model is linked to product.packaging and defines the type of storage +related to a specific packaging.

    +
  • +
  • Stock location storage type (stock.location.storage.type)

    +

    This models is linked to stock.location and defines the types of storage +that are allowed for a specific location.

    +
  • +
+

Therefore a Stock location storage type can include different Stock package +storage type in order to validate the destination of a move with package into a +stock location. +Moreover Stock location storage type can include product, size or lot +restrictions for the stock locations it’s defined on, so that a move with +package will only be allowed if it doesn’t violate the restrictions defined +(cf stock_location_storage_type_strategy).

+

Moreover, this module implements “storage type put-away strategy” in order to compute a +put-away location using storage types.

+

The standard put-away strategy is applied before the storage type put-away +strategy as the former relies on product or product category and the latter +relies on stock packages.

+

In other words, when a move is assigned, Odoo standard put-away strategy will be +applied to compute a new destination on the stock move lines, according to the +product. +After this first “put-away computation”, the “storage type” put-away strategy +is applied, if the reserved quant is linked to a package defining a package +storage type.

+

Storage locations linked to the package storage are processed sequentially, if +said storage location is a child of the move line’s destination location (i.e +either the put-away location or the move’s destination location). +For each location, their packs storage strategy is applied as well as the +restrictions defined on the stock location storage types. +If no suitable location is found, the next location in the sequence will be +searched and so on.

+

For the packs putaway strategy “none”, the location is considered as is. For +the “ordered children” strategy, children locations are sorted by first by max +height which is a physical constraint to respect, then pack putaway sequence +which allow to favor for example some level or corridor, and finally by name.

+

At the end, if found location is not the same as the original destination location, +the putaway strategies are applied (e.g.: A “none” pack putaway strategy is set on +computed location and a putaway rule exists on that one).

+

Table of contents

+ +
+

Known issues / Roadmap

+

Currently, the module supports only strategies applied on packages (stock.quant.package). +For implementations that do not use packages, it would be possible to add +compatibility with product packaging.

+

The information needed from a package are:

+
    +
  • the storage type, to know which strategy is applied
  • +
  • the dimensions and weight, to apply constraints
  • +
+

If we want to support product packaging, we would need to:

+
    +
  • guess the product packaging of a move line based on the product and quantities +(multiple of a packaging quantity, for instance 8000 would be a pallet if the pallet +has 2000 units, 1900 would be Box if the Box has 100 units)
  • +
  • from the product packaging, we know the storage type and dimensions
  • +
+

Everywhere the module is using package_id, we would have to check this:

+
    +
  • use the package if a package is set
  • +
  • else, use the computed packaging
  • +
+

About Unit of Measures:

+

In v13, there is an assumption of height to be expressed in mm and weight in kg. +In v14, packaging can be expressed in differents units. Explicit fields are introduced +like max_weight_in_kg in order make simple and efficient computations.

+
+
+

Limitation

+

If the locations structure is using views intensively in order to separate +storage types kindly (not mixing them), Odoo standard method to get putaway +strategy is returning the first child if a move location destination is a view.

+

This is not convenient if we want to set specific strategies on that view. So, +we override standard process by returning the view itself (if no putaway is set).

+

This can lead to a change on standard behavior as people will need to change manually +the location destination for pickings with views as default destination.

+

Idea: maybe adding a field on view locations to say ‘this is a view but don’t +apply standard child location selection’ could help filtering view candidates.

+
+
+

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

+
    +
  • Camptocamp
  • +
  • BCIM
  • +
+
+
+

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

+

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

+
+
+
+ + diff --git a/stock_storage_type/tests/__init__.py b/stock_storage_type/tests/__init__.py new file mode 100644 index 00000000000..06655c55be3 --- /dev/null +++ b/stock_storage_type/tests/__init__.py @@ -0,0 +1,9 @@ +from . import ( + test_auto_assign_storage_type, + test_package_height_required, + test_package_type_message, + test_stock_location, + test_storage_type, + test_storage_type_move, + test_storage_type_putaway_strategy, +) diff --git a/stock_storage_type/tests/common.py b/stock_storage_type/tests/common.py new file mode 100644 index 00000000000..5083cb3cb4c --- /dev/null +++ b/stock_storage_type/tests/common.py @@ -0,0 +1,143 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.tests import TransactionCase + + +class TestStorageTypeCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + ref = cls.env.ref + cls.warehouse = ref("stock.warehouse0") + # set two steps reception on warehouse + cls.warehouse.reception_steps = "two_steps" + + cls.suppliers_location = ref("stock.stock_location_suppliers") + cls.input_location = ref("stock.stock_location_company") + cls.stock_location = ref("stock.stock_location_stock") + + cls.cardboxes_location = ref("stock_storage_type.stock_location_cardboxes") + cls.pallets_location = ref("stock_storage_type.stock_location_pallets") + cls.pallets_reserve_location = ref( + "stock_storage_type.stock_location_pallets_reserve" + ) + cls.areas = ( + cls.cardboxes_location | cls.pallets_location | cls.pallets_reserve_location + ) + cls.location_sequence_pallet = ref( + "stock_storage_type.stock_package_storage_location_pallets" + ) + + cls.cardboxes_bin_1_location = ref( + "stock_storage_type.stock_location_cardboxes_bin_1" + ) + cls.cardboxes_bin_2_location = ref( + "stock_storage_type.stock_location_cardboxes_bin_2" + ) + cls.cardboxes_bin_3_location = ref( + "stock_storage_type.stock_location_cardboxes_bin_3" + ) + cls.cardboxes_bin_4_location = ref( + "stock_storage_type.stock_location_cardboxes_bin_4" + ) + cls.env["stock.location"]._parent_store_compute() + cls.pallets_bin_1_location = ref( + "stock_storage_type.stock_location_pallets_bin_1" + ) + cls.pallets_bin_2_location = ref( + "stock_storage_type.stock_location_pallets_bin_2" + ) + cls.pallets_bin_3_location = ref( + "stock_storage_type.stock_location_pallets_bin_3" + ) + cls.pallets_bin_4_location = ref( + "stock_storage_type.stock_location_pallets_bin_4" + ) + + cls.receipts_picking_type = ref("stock.picking_type_in") + cls.internal_picking_type = ref("stock.picking_type_internal") + + cls.product = ref("product.product_product_9") + cls.product2 = cls.env["product.product"].create( + {"name": "Product B", "type": "product"} + ) + cls.product3 = cls.env["product.product"].create( + {"name": "Product C", "type": "product"} + ) + cls.product_lot = ref("stock.product_cable_management_box") + + cls.cardboxes_package_storage_type = ref( + "stock_storage_type.package_storage_type_cardboxes" + ) + cls.pallets_package_storage_type = ref( + "stock_storage_type.package_storage_type_pallets" + ) + cls.cardboxes_location_storage_type = ref( + "stock_storage_type.location_storage_type_cardboxes" + ) + cls.pallets_location_storage_type = ref( + "stock_storage_type.location_storage_type_pallets" + ) + + cls.product_cardbox_product_packaging = ref( + "stock_storage_type." "product_product_9_packaging_4_cardbox" + ) + cls.product_pallet_product_packaging = ref( + "stock_storage_type." "product_product_9_packaging_48_pallet" + ) + cls.pallet_pack_type = ref("stock_storage_type." "package_storage_type_pallets") + cls.product_lot_cardbox_product_packaging = cls.env["product.packaging"].create( + { + "name": "5 units cardbox", + "qty": 5, + "product_id": cls.product_lot.id, + "package_type_id": cls.cardboxes_package_storage_type.id, + } + ) + cls.product_lot_pallets_product_packaging = cls.env["product.packaging"].create( + { + "name": "20 units pallet", + "qty": 20, + "product_id": cls.product_lot.id, + "package_type_id": cls.pallets_package_storage_type.id, + } + ) + cls.internal_picking_type.write({"show_entire_packs": True}) + # show_reserved must be set here because it changes the behaviour of + # put_in_pack operation: + # if show_reserved: qty_done must be set on stock.picking.move_line_ids + # if not show_reserved: qty_done must be set on + # stock.picking.move_line_nosuggest_ids + cls.receipts_picking_type.write( + {"show_entire_packs": True, "show_reserved": True} + ) + + @classmethod + def _update_qty_in_location( + cls, location, product, quantity, package=None, lot=None + ): + quants = cls.env["stock.quant"]._gather( + product, location, lot_id=lot, package_id=package, strict=True + ) + # this method adds the quantity to the current quantity, so remove it + quantity -= sum(quants.mapped("quantity")) + cls.env["stock.quant"]._update_available_quantity( + product, location, quantity, package_id=package, lot_id=lot + ) + + @classmethod + def _create_single_move(cls, product): + picking_type = cls.warehouse.int_type_id + move_vals = { + "name": product.name, + "picking_type_id": picking_type.id, + "product_id": product.id, + "product_uom_qty": 2.0, + "product_uom": product.uom_id.id, + "location_id": cls.input_location.id, + "location_dest_id": picking_type.default_location_dest_id.id, + "state": "confirmed", + "procure_method": "make_to_stock", + } + return cls.env["stock.move"].create(move_vals) diff --git a/stock_storage_type/tests/test_auto_assign_storage_type.py b/stock_storage_type/tests/test_auto_assign_storage_type.py new file mode 100644 index 00000000000..bd83034e14a --- /dev/null +++ b/stock_storage_type/tests/test_auto_assign_storage_type.py @@ -0,0 +1,89 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from .common import TestStorageTypeCommon + + +class TestAutoAssignStorageType(TestStorageTypeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_packaging = cls.product_lot_pallets_product_packaging + cls.package_storage_type = cls.product_packaging.package_type_id + + # Create a new package type for auto assignation + vals = { + "name": "Auto Assigned Package Type", + } + cls.auto_assigned_package_type = cls.env["stock.package.type"].create(vals) + + def test_auto_assign_package_storage_type_without_packaging_id(self): + """Packages without `packaging_id` are internal packages and they + are intended to be stored in the warehouse. + On such packages storage type is automatically defined. + """ + package = self.env["stock.quant.package"].create( + {"name": "TEST", "product_packaging_id": self.product_packaging.id} + ) + self.assertEqual(package.package_type_id, self.package_storage_type) + + def test_auto_assign_packaging(self): + """ + Test the auto assignation for package type from + the default package type on the product + + - Set the default package type on the product + - Create a move and validate it + - The package type should be set on package + """ + + # Set a default package on the product + self.product.package_type_id = self.auto_assigned_package_type + + confirmed_move = self._create_single_move(self.product) + confirmed_move.location_dest_id = self.pallets_bin_1_location + confirmed_move._assign_picking() + self._update_qty_in_location( + confirmed_move.location_id, + confirmed_move.product_id, + confirmed_move.product_qty, + ) + confirmed_move._action_assign() + picking = confirmed_move.picking_id + picking.action_confirm() + picking.move_line_ids.qty_done = 10.0 + first_package = picking.action_put_in_pack() + + picking.button_validate() + + self.assertEqual(self.auto_assigned_package_type, first_package.package_type_id) + + def test_auto_assign_no_packaging(self): + """ + Test the non auto assignation for package type from + the default package type on the product + + - Unset the default package type on the product + - Create a move and validate it + - The package type should not be set on package + """ + + # Set a default package on the product + self.product.package_type_id = False + + confirmed_move = self._create_single_move(self.product) + confirmed_move.location_dest_id = self.pallets_bin_1_location + confirmed_move._assign_picking() + self._update_qty_in_location( + confirmed_move.location_id, + confirmed_move.product_id, + confirmed_move.product_qty, + ) + confirmed_move._action_assign() + picking = confirmed_move.picking_id + picking.action_confirm() + picking.move_line_ids.qty_done = 10.0 + first_package = picking.action_put_in_pack() + + picking.button_validate() + + self.assertFalse(first_package.package_type_id) diff --git a/stock_storage_type/tests/test_package_height_required.py b/stock_storage_type/tests/test_package_height_required.py new file mode 100644 index 00000000000..bda19fc7292 --- /dev/null +++ b/stock_storage_type/tests/test_package_height_required.py @@ -0,0 +1,21 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo.exceptions import ValidationError + +from .common import TestStorageTypeCommon + + +class TestStorageTypeMove(TestStorageTypeCommon): + def test_package_storage_type_height_required(self): + packaging = self.product_lot_pallets_product_packaging + storage_type = packaging.package_type_id + # Without 'height_required' + self.env["stock.quant.package"].create( + {"name": "TEST1", "product_packaging_id": packaging.id} + ) + # With 'height_required' + storage_type.height_required = True + with self.assertRaises(ValidationError): + self.env["stock.quant.package"].create( + {"name": "TEST2", "product_packaging_id": packaging.id} + ) diff --git a/stock_storage_type/tests/test_package_type_message.py b/stock_storage_type/tests/test_package_type_message.py new file mode 100644 index 00000000000..7a77d490d86 --- /dev/null +++ b/stock_storage_type/tests/test_package_type_message.py @@ -0,0 +1,34 @@ +# Copyright 2022 ACSONE SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.tests import TransactionCase + + +class TestStorageType(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.pallets_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_pallets" + ) + cls.pallets_uk_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_pallets_uk" + ) + cls.cardboxes_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_cardboxes" + ) + cls.cardboxes_stock = cls.env.ref("stock_storage_type.stock_location_cardboxes") + cls.cardboxes_bin_1 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_1" + ) + cls.cardboxes_bin_2 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_2" + ) + cls.cardboxes_bin_3 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_3" + ) + cls.cardboxes_bin_4 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_4" + ) diff --git a/stock_storage_type/tests/test_stock_location.py b/stock_storage_type/tests/test_stock_location.py new file mode 100644 index 00000000000..ba46ef1781c --- /dev/null +++ b/stock_storage_type/tests/test_stock_location.py @@ -0,0 +1,218 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from .common import TestStorageTypeCommon + + +class TestStockLocation(TestStorageTypeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + ref = cls.env.ref + cls.pallets_reserve_bin_1_location = ref( + "stock_storage_type.stock_location_pallets_reserve_bin_1" + ) + cls.pallets_reserve_bin_2_location = ref( + "stock_storage_type.stock_location_pallets_reserve_bin_2" + ) + cls.pallets_reserve_bin_3_location = ref( + "stock_storage_type.stock_location_pallets_reserve_bin_3" + ) + cls.pallets_reserve_bin_4_location = ref( + "stock_storage_type.stock_location_pallets_reserve_bin_4" + ) + + def test_get_ordered_leaf_locations(self): + sublocation = self.stock_location.copy( + { + "name": "Sub-location", + "pack_putaway_strategy": "ordered_locations", + "location_id": self.stock_location.id, + } + ) + self.areas.write({"location_id": sublocation.id}) + + # Test with the same max_height on all related storage types (0 here) + ordered_locations = sublocation.get_storage_locations(self.product) + self.assertEqual( + ordered_locations.ids, + ( + self.cardboxes_bin_1_location + | self.cardboxes_bin_2_location + | self.cardboxes_bin_3_location + | self.cardboxes_bin_4_location + | self.pallets_bin_1_location + | self.pallets_bin_2_location + | self.pallets_bin_3_location + | self.pallets_bin_4_location + | self.pallets_reserve_bin_1_location + | self.pallets_reserve_bin_2_location + | self.pallets_reserve_bin_3_location + | self.pallets_reserve_bin_4_location + ).ids, + ) + # Set the max_height on pallets storage type higher than the others + self.pallets_location_storage_type.storage_category_id.max_height = 2 + self.cardboxes_location_storage_type.storage_category_id.max_height = 1 + ordered_locations = sublocation.get_storage_locations(self.product) + self.assertEqual( + ordered_locations.ids, + ( + self.cardboxes_bin_1_location + | self.cardboxes_bin_2_location + | self.cardboxes_bin_3_location + | self.cardboxes_bin_4_location + | self.pallets_bin_1_location + | self.pallets_bin_2_location + | self.pallets_bin_3_location + | self.pallets_bin_4_location + | self.pallets_reserve_bin_1_location + | self.pallets_reserve_bin_2_location + | self.pallets_reserve_bin_3_location + | self.pallets_reserve_bin_4_location + ).ids, + ) + # Set the max_height on cardboxes storage type higher than the others + self.pallets_location_storage_type.storage_category_id.max_height = 1 + self.cardboxes_location_storage_type.storage_category_id.max_height = 2 + ordered_locations = sublocation.get_storage_locations(self.product) + self.assertEqual( + ordered_locations.ids, + ( + self.pallets_bin_1_location + | self.pallets_bin_2_location + | self.pallets_bin_3_location + | self.pallets_bin_4_location + | self.pallets_reserve_bin_1_location + | self.pallets_reserve_bin_2_location + | self.pallets_reserve_bin_3_location + | self.pallets_reserve_bin_4_location + | self.cardboxes_bin_1_location + | self.cardboxes_bin_2_location + | self.cardboxes_bin_3_location + | self.cardboxes_bin_4_location + ).ids, + ) + + def test_will_contain_product_ids(self): + location = self.pallets_bin_1_location + location.computed_storage_category_id.allow_new_product = "same" + + self._update_qty_in_location(location, self.product, 10) + self.assertEqual(location.location_will_contain_product_ids, self.product) + + # the moves and move lines created are not really valid, but we don't care, it's + # only to have "in_move_ids" and "in_move_line_ids" on the location + self.env["stock.move"].create( + { + "name": "test", + "product_id": self.product2.id, + "location_id": self.stock_location.id, + "location_dest_id": location.id, + "product_uom": self.product2.uom_id.id, + "product_uom_qty": 10, + "state": "waiting", + } + ) + self.assertEqual( + location.location_will_contain_product_ids, self.product | self.product2 + ) + + ml_move = self.env["stock.move"].create( + { + "name": "test", + "product_id": self.product3.id, + "location_id": self.stock_location.id, + "location_dest_id": location.location_id.id, + "product_uom": self.product2.uom_id.id, + "product_uom_qty": 10, + "state": "waiting", + } + ) + self.env["stock.move.line"].create( + { + "product_id": self.product3.id, + "location_id": self.stock_location.id, + "location_dest_id": location.id, + "product_uom_id": self.product3.uom_id.id, + "reserved_uom_qty": 10, + "move_id": ml_move.id, + "company_id": self.env.company.id, + } + ) + self.assertEqual( + location.location_will_contain_product_ids, + self.product | self.product2 | self.product3, + ) + + location.computed_storage_category_id.allow_new_product = "mixed" + self.assertEqual( + location.location_will_contain_product_ids, + self.env["product.product"].browse(), + ) + + def test_will_contain_lot_ids(self): + location = self.pallets_bin_1_location + location.computed_storage_category_id.allow_new_product = "same_lot" + lot_values = {"product_id": self.product.id, "company_id": self.env.company.id} + lot1 = self.env["stock.lot"].create(lot_values) + lot2 = self.env["stock.lot"].create(lot_values) + + self._update_qty_in_location(location, self.product, 10, lot=lot1) + self.assertEqual(location.location_will_contain_lot_ids, lot1) + + # the moves and move lines created are not really valid, but we don't care, it's + # only to have "in_move_ids" and "in_move_line_ids" on the location + ml_move = self.env["stock.move"].create( + { + "name": "test", + "product_id": self.product.id, + "location_id": self.stock_location.id, + "location_dest_id": location.location_id.id, + "product_uom": self.product2.uom_id.id, + "product_uom_qty": 10, + "state": "waiting", + } + ) + self.env["stock.move.line"].create( + { + "product_id": self.product.id, + "lot_id": lot2.id, + "location_id": self.stock_location.id, + "location_dest_id": location.id, + "product_uom_id": self.product.uom_id.id, + "reserved_uom_qty": 10, + "move_id": ml_move.id, + "company_id": self.env.company.id, + } + ) + self.assertEqual(location.location_will_contain_lot_ids, lot1 | lot2) + + location.computed_storage_category_id.allow_new_product = "mixed" + self.assertEqual( + location.location_will_contain_lot_ids, + self.env["stock.lot"].browse(), + ) + + def test_location_is_empty_non_internal(self): + location = self.env.ref("stock.stock_location_customers") + # we always consider an non-internal location empty, the put-away + # rules do not apply and we can add as many quants as we want + self.assertTrue(location.location_is_empty) + self._update_qty_in_location(location, self.product, 10) + self.assertTrue(location.location_is_empty) + + def test_location_is_empty(self): + location = self.pallets_reserve_bin_1_location + self.assertTrue(location.only_empty) + self.assertTrue(location.location_is_empty) + self._update_qty_in_location(location, self.product, 10) + self.assertFalse(location.location_is_empty) + + # When the location has no "only_empty" storage type, we don't + # care about if it is empty or not, we keep it as True so we + # can always put things inside. Not computing it prevents + # useless race conditions on concurrent writes. + location.computed_storage_category_id.capacity_ids.filtered( + lambda c: c.allow_new_product == "empty" + ).allow_new_product = "mixed" + self.assertTrue(location.location_is_empty) diff --git a/stock_storage_type/tests/test_storage_type.py b/stock_storage_type/tests/test_storage_type.py new file mode 100644 index 00000000000..a7747c71eda --- /dev/null +++ b/stock_storage_type/tests/test_storage_type.py @@ -0,0 +1,193 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.tests import TransactionCase + + +class TestStorageType(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.location_sequence_pallet = cls.env.ref( + "stock_storage_type.stock_package_storage_location_pallets" + ) + + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.pallets_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_pallets" + ) + cls.pallets_uk_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_pallets_uk" + ) + cls.cardboxes_location_storage_type = cls.env.ref( + "stock_storage_type.location_storage_type_cardboxes" + ) + cls.cardboxes_stock = cls.env.ref("stock_storage_type.stock_location_cardboxes") + cls.cardboxes_bin_1 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_1" + ) + cls.cardboxes_bin_2 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_2" + ) + cls.cardboxes_bin_3 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_3" + ) + cls.cardboxes_bin_4 = cls.env.ref( + "stock_storage_type.stock_location_cardboxes_bin_4" + ) + + def test_location_allowed_storage_types(self): + # As cardboxes location storage type is defined on parent stock + # location_storage_type_ids + self.assertEqual( + self.cardboxes_stock.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + # It is what's allowed on the parent stock + self.assertEqual( + self.cardboxes_stock.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + # and also what's allowed on the children + self.assertEqual( + self.cardboxes_bin_1.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + self.assertEqual( + self.cardboxes_bin_2.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + self.assertEqual( + self.cardboxes_bin_3.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + special_cardbox = self.env["stock.storage.category"].create( + { + "name": "Special Cardboxes", + } + ) + # If I change on a child, it will only be applied on this child + special_cardboxes = self.cardboxes_location_storage_type.copy( + {"storage_category_id": special_cardbox.id} + ) + self.cardboxes_bin_1.storage_category_id = special_cardbox + self.assertEqual( + self.cardboxes_bin_1.computed_storage_category_id.capacity_ids, + special_cardboxes, + ) + # and not on his parent nor siblings + self.assertEqual( + self.cardboxes_stock.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + self.assertEqual( + self.cardboxes_bin_2.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + self.assertEqual( + self.cardboxes_bin_3.computed_storage_category_id.capacity_ids, + self.cardboxes_location_storage_type, + ) + # If I create a child bin on cardboxes bin 1, it will use the first + # parent's storage type + bin_1_child = self.env["stock.location"].create( + {"name": "Carboxes bin 1 child", "location_id": self.cardboxes_bin_1.id} + ) + self.assertEqual( + bin_1_child.computed_storage_category_id.capacity_ids, special_cardboxes + ) + + def test_location_leaf_locations(self): + cardboxes_leaves = self.env["stock.location"].search( + [("id", "child_of", self.cardboxes_stock.id), ("child_ids", "=", False)] + ) + + self.assertEqual(self.cardboxes_stock.leaf_location_ids, cardboxes_leaves) + all_stock_leaves = self.env["stock.location"].search( + [("id", "child_of", self.stock_location.id), ("child_ids", "=", False)] + ) + self.assertEqual(self.stock_location.leaf_location_ids, all_stock_leaves) + + def test_location_leaf_locations_on_leaf(self): + self.cardboxes_bin_4.active = False + self.assertEqual( + self.cardboxes_stock.leaf_location_ids, + self.cardboxes_bin_1 | self.cardboxes_bin_2 | self.cardboxes_bin_3, + ) + + def test_location_max_height(self): + self.pallets_location_storage_type.storage_category_id.max_height = 2 + self.cardboxes_location_storage_type.storage_category_id.max_height = 0 + category_id = self.pallets_location_storage_type.storage_category_id.id + test_location = self.env["stock.location"].create( + { + "name": "TEST", + "storage_category_id": category_id, + } + ) + # Should be the max height of pallets storage category (2) + self.assertEqual(test_location.max_height, 2) + self.cardboxes_location_storage_type.storage_category_id.max_height = 1 + test_location.storage_category_id = ( + self.cardboxes_location_storage_type.storage_category_id + ) + # Should be the max height of cardboxes storage category (2) + self.assertEqual(test_location.max_height, 1) + + def test_storage_type_max_height_in_meters(self): + # Set the 'max_height' as meters and check that 'max_height_in_m' is equal + uom_meter = self.env.ref("uom.product_uom_meter") + self.pallets_location_storage_type.storage_category_id.length_uom_id = uom_meter + self.pallets_location_storage_type.storage_category_id.max_height = 100 + self.assertEqual( + self.pallets_location_storage_type.storage_category_id.max_height_in_m, 100 + ) + # Then set the UoM to centimeters and check that max_height_in_m is + # reduced by a factor 100 + uom_cm = self.env.ref("uom.product_uom_cm") + self.pallets_location_storage_type.storage_category_id.length_uom_id = uom_cm + self.assertEqual( + self.pallets_location_storage_type.storage_category_id.max_height_in_m, 1 + ) + + def test_archive_package_storage_type(self): + target = self.env.ref("stock_storage_type.package_storage_type_pallets") + all_package_storage_types = self.env["stock.package.type"].search([]) + self.assertIn(target, all_package_storage_types) + target.active = False + all_package_storage_types = self.env["stock.package.type"].search([]) + self.assertNotIn(target, all_package_storage_types) + + def test_package_message(self): + """ + Test for the message displayed on Stock Package Type forms + """ + pallets = self.env.ref("stock_storage_type.package_storage_type_pallets") + message = "When a package with storage type Pallets is put away, the " + message += "strategy will look for an allowed location in the " + message += "following locations:" + self.assertIn(message, pallets.storage_type_message) + + message = ( + "Pallets reserve storage area (WARNING: restrictions are active on " + "location storage types matching this package storage type)" + ) + + self.assertIn(message, pallets.storage_type_message) + + def test_sequence_to_location_menu(self): + action = self.location_sequence_pallet.button_show_locations() + self.assertIn( + ( + "computed_storage_capacity_ids", + "in", + self.location_sequence_pallet.package_type_id.storage_category_capacity_ids.ids, + ), + action["domain"], + ) + + def test_storage_capacity_display(self): + self.assertEqual( + self.cardboxes_stock.computed_storage_category_id.capacity_ids.display_name, + "Cardboxes x 1.0 (Package: Cardboxes - Allow New Product: Allow mixed products)", + ) diff --git a/stock_storage_type/tests/test_storage_type_move.py b/stock_storage_type/tests/test_storage_type_move.py new file mode 100644 index 00000000000..35326c92aa3 --- /dev/null +++ b/stock_storage_type/tests/test_storage_type_move.py @@ -0,0 +1,468 @@ +# Copyright 2020 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo.exceptions import ValidationError +from odoo.tools.safe_eval import const_eval + +from .common import TestStorageTypeCommon + + +class TestStorageTypeMove(TestStorageTypeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.areas.write({"pack_putaway_strategy": "ordered_locations"}) + + def assert_package_level_domain(self, json_domain, expected_locations): + domain = const_eval(json_domain) + self.assertEqual(len(domain), 1) + self.assertEqual(domain[0][0], "id") + self.assertEqual(domain[0][1], "in") + self.assertEqual(sorted(domain[0][2]), sorted(expected_locations.ids)) + + def _test_confirmed_move(self, product=None): + confirmed_move = self._create_single_move(product or self.product) + confirmed_move.location_dest_id = self.pallets_bin_1_location.id + move_to_assign = self._create_single_move(self.product) + (confirmed_move | move_to_assign)._assign_picking() + package = self.env["stock.quant.package"].create( + # {"product_packaging_id": self.product_pallet_product_packaging.id} + {"package_type_id": self.pallet_pack_type.id} + ) + self._update_qty_in_location( + move_to_assign.location_id, + move_to_assign.product_id, + move_to_assign.product_qty, + package=package, + ) + move_to_assign._action_assign() + return move_to_assign + + def test_not_only_empty_confirmed_move(self): + self.pallets_location_storage_type.write({"allow_new_product": "mixed"}) + move = self._test_confirmed_move() + self.assertEqual( + move.move_line_ids.location_dest_id, self.pallets_bin_1_location + ) + + def test_only_empty_confirmed_move(self): + self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + move = self._test_confirmed_move() + self.assertNotEqual( + move.move_line_ids.location_dest_id, self.pallets_bin_1_location + ) + + def test_do_not_mix_products_confirmed_move_ok(self): + self.pallets_location_storage_type.write({"allow_new_product": "same"}) + move = self._test_confirmed_move() + self.assertEqual( + move.move_line_ids.location_dest_id, self.pallets_bin_1_location + ) + + def test_do_not_mix_products_confirmed_move_nok(self): + self.pallets_location_storage_type.write({"allow_new_product": "same"}) + move_other_product = self._test_confirmed_move( + self.env.ref("product.product_product_10") + ) + self.assertNotEqual( + move_other_product.move_line_ids.location_dest_id, + self.pallets_bin_1_location, + ) + + def test_package_level_location_dest_domain_only_empty(self): + # Set pallets location type as only empty + self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 96.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 48.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_pallet_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + # First move line goes into pallets bin 1 + # Second move line goes into pallets bin 2 + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.pallets_bin_1_location | self.pallets_bin_2_location, + ) + self.assertEqual( + int_picking.package_level_ids.mapped("location_dest_id"), + self.pallets_bin_1_location | self.pallets_bin_2_location, + ) + package_type_locations = int_picking.package_level_ids.mapped( + "package_id.package_type_id." "storage_location_sequence_ids.location_id" + ) + possible_locations = self.env["stock.location"].search( + [ + ( + "computed_storage_category_id.capacity_ids", + "in", + int_picking.package_level_ids.mapped( + "package_id.package_type_id.storage_category_capacity_ids" + ).ids, + ), + ("location_id", "child_of", int_picking.location_dest_id.id), + ("id", "in", package_type_locations.mapped("leaf_location_ids").ids), + ] + ) + only_empty_possible_locations = possible_locations.filtered( + lambda l: not l.quant_ids + ) + + for level in int_picking.package_level_ids: + self.assertEqual( + level.allowed_location_dest_ids, + only_empty_possible_locations + - ( + # remove the destination of other levels but keep the current one + int_picking.package_level_ids.mapped("location_dest_id") + ) + | level.location_dest_id, + ) + + # Update qty in a bin to ensure it's not in possible locations anymore + self.env["stock.quant"]._update_available_quantity( + self.product, self.pallets_bin_3_location, 1.0 + ) + only_empty_possible_locations_2 = possible_locations.filtered( + lambda l: not l.quant_ids + ) + self.assertEqual( + only_empty_possible_locations, + only_empty_possible_locations_2 | self.pallets_bin_3_location, + ) + + for level in int_picking.package_level_ids: + self.assertEqual( + level.allowed_location_dest_ids, + only_empty_possible_locations_2 + - ( + # remove the destination of other levels but keep the current one + int_picking.package_level_ids.mapped("location_dest_id") + ) + | level.location_dest_id, + ) + + # Creating a new possible location must be reflected in domain + pallets_bin_4_location = self.env["stock.location"].create( + {"name": "Pallets bin 4", "location_id": self.pallets_location.id} + ) + + for level in int_picking.package_level_ids: + self.assertEqual( + level.allowed_location_dest_ids, + (only_empty_possible_locations_2 | pallets_bin_4_location) + - ( + # remove the destination of other levels but keep the current one + int_picking.package_level_ids.mapped("location_dest_id") + ) + | level.location_dest_id, + ) + + def test_package_level_location_dest_domain_mixed(self): + # Mark picking to allow creation and use of existing lots in order + # to register two times the same lot in different packages + self.receipts_picking_type.use_existing_lots = True + self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 52.0, + "product_uom": self.product.uom_id.id, + }, + ), + ( + 0, + 0, + { + "name": self.product_lot.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product_lot.id, + "product_uom_qty": 15.0, + "product_uom": self.product.uom_id.id, + }, + ), + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ).qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + # Put in pack again + product_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product and not ml.result_package_id + ) + product_ml_without_package.qty_done = 4.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_cardbox_product_packaging + # Create lots to be used on move lines + lot_a0001 = self.env["stock.lot"].create( + { + "name": "A0001", + "product_id": self.product_lot.id, + "company_id": self.env.user.company_id.id, + } + ) + lot_a0002 = self.env["stock.lot"].create( + { + "name": "A0002", + "product_id": self.product_lot.id, + "company_id": self.env.user.company_id.id, + } + ) + # Put in pack lot product + in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_lot + ).write({"qty_done": 5.0, "lot_id": lot_a0001.id}) + third_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + third_pack.product_packaging_id = self.product_lot_cardbox_product_packaging + # Put in pack lot product again + product_lot_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_lot and not ml.result_package_id + ) + product_lot_ml_without_package.write({"qty_done": 5.0, "lot_id": lot_a0002.id}) + fourth_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + fourth_pack.product_packaging_id = self.product_lot_cardbox_product_packaging + # Put in pack lot product again ... again (to have two times same lot) + product_lot_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_lot and not ml.result_package_id + ) + product_lot_ml_without_package.write({"qty_done": 5.0, "lot_id": lot_a0002.id}) + fifth_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + fifth_pack.product_packaging_id = self.product_lot_cardbox_product_packaging + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + + def _get_possible_locations(package_level): + storage_type = package_level.package_id.package_type_id + package_type_locations = storage_type.storage_location_sequence_ids.mapped( + "location_id.leaf_location_ids" + ) + possible_locations = self.env["stock.location"].search( + [ + ( + "computed_storage_category_id.capacity_ids", + "in", + storage_type.storage_category_capacity_ids.ids, + ), + ( + "location_id", + "child_of", + package_level.picking_id.location_dest_id.id, + ), + ("id", "in", package_type_locations.ids), + ] + ) + return ( + possible_locations + - package_level.picking_id.package_level_ids.mapped("location_dest_id") + | pack_level.location_dest_id + ) + + def _levels_for(packages): + return self.env["stock.package_level"].search( + [ + ("package_id", "in", packages.ids), + ("picking_id", "=", int_picking.id), + ] + ) + + first_level = _levels_for(first_package) + self.assertEqual(len(first_level), 1) + # Pallet into pallets bin + self.assertEqual(first_level.location_dest_id, self.pallets_bin_1_location) + + second_level = _levels_for(second_pack) + # Cardbox into cardbox bin + self.assertEqual(len(second_level), 1) + self.assertEqual(second_level.location_dest_id, self.cardboxes_bin_1_location) + + third_level = _levels_for(third_pack) + + # Cardbox with different product go into different cardbox location + self.assertEqual(len(third_level), 1) + self.assertEqual(third_level.location_dest_id, self.cardboxes_bin_3_location) + + fourth_fifth_levels = _levels_for(fourth_pack | fifth_pack) + # Cardbox with same product but different lot go into different + # cardbox location + # Cardbox with same product same lot go into same location + self.assertEqual(len(fourth_fifth_levels), 2) + self.assertEqual( + fourth_fifth_levels.location_dest_id, self.cardboxes_bin_2_location + ) + + for pack_level in ( + first_level | second_level | third_level | fourth_fifth_levels + ): + # Check domain + self.assertEqual( + pack_level.allowed_location_dest_ids, + _get_possible_locations(pack_level), + ) + + # Set the quantities done in order to avoid immediate transfer wizard + for move_line in pack_level.move_line_ids: + move_line.qty_done = move_line.reserved_qty + + second_level.location_dest_id = third_level.location_dest_id + with self.assertRaises(ValidationError): + int_picking.button_validate() + + def test_stock_move_no_package(self): + """ + Create a stock move for a product with lot restriction + Don't put it in a package + Check that lot restriction is well applied + """ + # Constrain Cardbox Capacity to accept same lots only + self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + # Set a quantity in cardbox bin 2 to make sure constraint is applied + self.env["stock.quant"]._update_available_quantity( + self.env.ref("product.product_product_10"), + self.cardboxes_bin_2_location, + 1.0, + ) + + # As we don't put in pack in this flow, we need to set a default + # package type on the product level in order to get the specialized putaway + # to be applied + self.product_lot.package_type_id = self.cardboxes_package_storage_type + + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 8.0, + "product_uom": self.product.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ( + 0, + 0, + { + "name": self.product_lot.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product_lot.id, + "product_uom_qty": 10.0, + "product_uom": self.product_lot.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ], + } + ) + # Mark as todo + in_picking.action_confirm() + + # Fill in the lots during the incoming process + product_lot_ml = in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_lot + ) + product_lot_ml.write({"qty_done": 5.0, "lot_name": "A0001"}) + product_lot_ml.copy({"qty_done": 3.0, "lot_name": "A0002"}) + + product_ml = in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ) + + product_ml.write({"qty_done": 8.0}) + + in_picking._action_done() + + int_picking = in_picking.move_ids.mapped("move_dest_ids.picking_id") + + lot_lines = int_picking.move_line_ids.filtered( + lambda line: line.product_id == self.product_lot + ) + destination_ids = lot_lines.mapped("location_dest_id.id") + # Check if the destinations are all different + self.assertAlmostEqual( + list(set(destination_ids)), + destination_ids, + ) + + lot_ids = lot_lines.mapped("lot_id.id") + # Check if the lots are all different + self.assertAlmostEqual( + list(set(lot_ids)), + lot_ids, + ) diff --git a/stock_storage_type/tests/test_storage_type_putaway_strategy.py b/stock_storage_type/tests/test_storage_type_putaway_strategy.py new file mode 100644 index 00000000000..2d0e97acebc --- /dev/null +++ b/stock_storage_type/tests/test_storage_type_putaway_strategy.py @@ -0,0 +1,810 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from .common import TestStorageTypeCommon + + +class TestPutawayStorageTypeStrategy(TestStorageTypeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.areas.write({"pack_putaway_strategy": "ordered_locations"}) + + def test_storage_strategy_ordered_locations_cardboxes(self): + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 8.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 4.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_cardbox_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 4.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_cardbox_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() # TODO drop ? + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.cardboxes_bin_1_location, + ) + # Archive all leaf locations. Ensure that the intermediate location is + # not returned as a valid leaf location and that the next location in + # the sequence (reserve) is selected + int_picking.do_unreserve() + cardboxes_stock = self.env.ref("stock_storage_type.stock_location_cardboxes") + cardboxes_stock.child_ids.write({"active": False}) + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + reserve_cardbox = self.env.ref( + "stock_storage_type.stock_location_cardboxes_reserve_bin_1" + ) + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), reserve_cardbox + ) + + def test_storage_strategy_only_empty_ordered_locations_pallets(self): + # Set pallets location type as only empty + self.pallets_location_storage_type.write({"allow_new_product": "empty"}) + # Set a quantity in pallet bin 2 to make sure constraint is applied + self.env["stock.quant"]._update_available_quantity( + self.product, self.pallets_bin_2_location, 1.0 + ) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 96.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 48.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_pallet_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + # First move line goes into pallets bin 1 + # Second move line goes into pallets bin 3 as bin 1 is planned for + # first move line and bin 2 is already used + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.pallets_bin_1_location | self.pallets_bin_3_location, + ) + + def test_storage_strategy_max_weight_ordered_locations_pallets(self): + # Add a category for max_weight 50 + category_50 = self.env["stock.storage.category"].create( + {"name": "Pallets max 50 kg", "max_weight": 50} + ) + + # Define new pallets location type with a max weight on bin 2 + light_location_storage_type = self.pallets_location_storage_type.copy( + {"allow_new_product": "empty", "storage_category_id": category_50.id} + ) + self.pallets_bin_2_location.write({"storage_category_id": category_50.id}) + self.assertEqual( + self.pallets_bin_2_location.storage_category_id.capacity_ids, + light_location_storage_type, + ) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 96.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + first_package.onchange_product_packaging_id() + self.assertEqual(first_package.pack_weight, 60) + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 48.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_pallet_product_packaging + second_pack.onchange_product_packaging_id() + self.assertEqual(second_pack.pack_weight, 60) + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + # First move line goes into pallets bin 1 + # Second move line goes into pallets bin 3 as bin 1 is planned for + # first move line and bin 2 is already used + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.pallets_bin_1_location | self.pallets_bin_3_location, + ) + + def test_storage_strategy_no_products_lots_mix_ordered_locations_cardboxes(self): + self.cardboxes_location_storage_type.write({"allow_new_product": "same_lot"}) + # Set a quantity in cardbox bin 2 to make sure constraint is applied + self.env["stock.quant"]._update_available_quantity( + self.env.ref("product.product_product_10"), + self.cardboxes_bin_2_location, + 1.0, + ) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 8.0, + "product_uom": self.product.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ( + 0, + 0, + { + "name": self.product_lot.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product_lot.id, + "product_uom_qty": 10.0, + "product_uom": self.product_lot.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack product + in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ).qty_done = 4.0 + product_first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_first_package.product_packaging_id = ( + self.product_cardbox_product_packaging + ) + # Put in pack product again + product_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product + ) + product_ml_without_package.qty_done = 4.0 + product_second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_second_pack.product_packaging_id = ( + self.product_cardbox_product_packaging + ) + + # Put in pack product lot + product_lot_ml = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product_lot + ) + product_lot_ml.write({"qty_done": 5.0, "lot_name": "A0001"}) + product_lot_first_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_lot_first_pack.product_packaging_id = ( + self.product_lot_cardbox_product_packaging + ) + # Put in pack product lot again + product_lot_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product_lot + ) + product_lot_ml_without_package.write({"qty_done": 5.0, "lot_name": "A0002"}) + product_lot_second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_lot_second_pack.product_packaging_id = ( + self.product_lot_cardbox_product_packaging + ) + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.mapped("move_dest_ids.picking_id") + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + product_mls = int_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ) + self.assertEqual( + product_mls.mapped("location_dest_id"), self.cardboxes_bin_1_location + ) + product_lot_mls = int_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product_lot + ) + self.assertEqual( + product_lot_mls.mapped("location_dest_id"), + self.cardboxes_bin_3_location | self.cardboxes_bin_4_location, + ) + + def test_storage_strategy_none_in_sequence(self): + """When a location has a strategy 'none' in sequence, stop + + We can use it to stop computing the put-away when the destination + location match, for instance to use a setup with a sequence: + + 1. Stock: None + 2. Stock/Cardboxes Reserve: ordered locations + + If a move is created with destination 'Cardboxes Reserve', the put-away + rule stops at the rule 1. so the move stays in 'Cardboxes Reserve. + Then, the destination is changed to 'Stock/Cardboxes Reserve' and + a recomputation is done, the put-away for Bin 1 is applied. + + """ + move = self._create_single_move(self.product) + # move.location_dest_id = self.cardboxes_location.id + move._assign_picking() + package = self.env["stock.quant.package"].create( + {"product_packaging_id": self.product_lot_cardbox_product_packaging.id} + ) + self._update_qty_in_location( + move.location_id, move.product_id, move.product_qty, package=package + ) + + # configure a new sequence with none in the parent location + self.cardboxes_package_storage_type.storage_location_sequence_ids.unlink() + self.warehouse.lot_stock_id.pack_putaway_strategy = "none" + self.warehouse.lot_stock_id.storage_category_id = ( + self.cardboxes_location_storage_type.storage_category_id + ) + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.warehouse.lot_stock_id.id, + "sequence": 1, + } + ) + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.cardboxes_location.id, + "sequence": 2, + } + ) + + move._action_assign() + move_line = move.move_line_ids + package_level = move_line.package_level_id + + self.assertEqual( + package_level.location_dest_id, + self.warehouse.lot_stock_id, + "the move line's destination must stay in Stock as we have" + " a 'none' strategy on it and it is in the sequence", + ) + + package_level.location_dest_id = self.cardboxes_location + # if we reapply the strategy, it should now apply the ordered + # location of the cardbox location + package_level.recompute_pack_putaway() + + self.assertTrue( + package_level.location_dest_id in self.cardboxes_location.child_ids + ) + + def test_storage_strategy_do_not_mix_products_reuse_location(self): + """Location with restriction 'do_not_mix_products' should have priority + + When locations are configured with 'do_not_mix_products' the strategy + must give priority to location that already contains the product + (less qty first). + """ + StockLocation = self.env["stock.location"] + self.cardboxes_location_storage_type.write({"allow_new_product": "same"}) + product = self.product + packaging = self.product_cardbox_product_packaging + dest_location = self.cardboxes_location + package = self.env["stock.quant.package"].create( + {"name": "TEST1", "product_packaging_id": packaging.id} + ) + self.env["stock.quant"].create( + { + "product_id": product.id, + "package_id": package.id, + "location_id": self.input_location.id, + } + ) + + location = StockLocation._get_package_type_putaway_strategy( + dest_location, package, product, 1.0 + ) + + # No location with given product -> the first bin should be returned + self.assertEqual(location, self.cardboxes_bin_1_location) + + # Set a quantity in cardbox bin 4 to trigger the priority on the + # location that already contains the product + self.env["stock.quant"]._update_available_quantity( + product, + self.cardboxes_bin_3_location, + 10.0, + ) + location = StockLocation._get_package_type_putaway_strategy( + dest_location, package, product, 1.0 + ) + self.assertEqual(location, self.cardboxes_bin_3_location) + + # Set less quantity on bin 4. Since it's the location with less quantity + # that should have priority + self.env["stock.quant"]._update_available_quantity( + product, + self.cardboxes_bin_4_location, + 1.0, + ) + location = StockLocation._get_package_type_putaway_strategy( + dest_location, package, product, 1.0 + ) + self.assertEqual(location, self.cardboxes_bin_4_location) + + def test_storage_strategy_none_in_sequence_to_fixes(self): + """When a location has a strategy 'none' in sequence, stop + + We can use it to stop computing the put-away when the destination + location match, In such a case, if the location match and a putaway + rule is defined on the product for this destination location, + the location destination will be the location from the putaway rule. + + This is very usefull to support fixed location putaway + + Ex: + product putaway: + * in: Cardboxes + * out: cardboxes_bin_4_location + + sequence: + 1. Stock/Cardboxes: None + + If a move is created with destination "Stock", the put-away rule stops + at sequence 1. Since a put away exists on the product for 'Cardboxes', + the product putaway is applied and the final destination is + 'cardboxes_bin_4_location' + + """ + move = self._create_single_move(self.product) + move._assign_picking() + self.assertEqual(move.location_dest_id, self.stock_location) + package = self.env["stock.quant.package"].create( + {"product_packaging_id": self.product_lot_cardbox_product_packaging.id} + ) + self._update_qty_in_location( + move.location_id, move.product_id, move.product_qty, package=package + ) + + # configure a new sequence with none in the parent location + self.cardboxes_package_storage_type.storage_location_sequence_ids.unlink() + self.cardboxes_location.pack_putaway_strategy = "none" + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.cardboxes_location.id, + "sequence": 1, + } + ) + + # create a put away rule on the product from cardboxes to bin 4 + self.env["stock.putaway.rule"].create( + { + "location_in_id": self.cardboxes_location.id, + "location_out_id": self.cardboxes_bin_4_location.id, + "product_id": self.product.id, + } + ) + + move._action_assign() + move_line = move.move_line_ids + package_level = move_line.package_level_id + + self.assertEqual( + package_level.location_dest_id, + self.cardboxes_bin_4_location, + ) + + def test_storage_strategy_sequence_condition(self): + """If a condition is not met on storage location sequence, it's ignored""" + move = self._create_single_move(self.product) + move._assign_picking() + original_location_dest = move.location_dest_id + package = self.env["stock.quant.package"].create( + {"product_packaging_id": self.product_lot_cardbox_product_packaging.id} + ) + self._update_qty_in_location( + move.location_id, move.product_id, move.product_qty, package=package + ) + + # configure a new sequence with none in the parent location + self.cardboxes_package_storage_type.storage_location_sequence_ids.unlink() + self.warehouse.lot_stock_id.pack_putaway_strategy = "none" + self.warehouse.lot_stock_id.storage_category_id = ( + self.cardboxes_location_storage_type.storage_category_id + ) + condition = self.env["stock.storage.location.sequence.cond"].create( + {"name": "Always False", "code_snippet": "result = False"} + ) + self.none_sequence = self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.warehouse.lot_stock_id.id, + "sequence": 1, + "location_sequence_cond_ids": [(6, 0, condition.ids)], + } + ) + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.cardboxes_location.id, + "sequence": 2, + } + ) + + move._action_assign() + move_line = move.move_line_ids + package_level = move_line.package_level_id + + self.assertIn( + package_level.location_dest_id, + self.cardboxes_location.child_ids, + "the move line's destination must go into the cardbox location" + " since the the first sequence is ignored due to the False" + " condition on it", + ) + + # if we update the condition to always be True, reset the + # location_dest on the package_level and reapply the put away strategy + # the move line's destination must be in Stock as we have a 'none' + # strategy the first putaway sequence + condition.code_snippet = "result = True" + package_level.location_dest_id = original_location_dest.id + package_level.recompute_pack_putaway() + + self.assertEqual( + package_level.location_dest_id, + self.warehouse.lot_stock_id, + "the move line's destination must stay in Stock as we have" + " a 'none' strategy on it and it is in the sequence", + ) + + package_level.location_dest_id = self.cardboxes_location + # if we reapply the strategy, it should now apply the ordered + # location of the cardbox location + package_level.recompute_pack_putaway() + + self.assertTrue( + package_level.location_dest_id in self.cardboxes_location.child_ids + ) + + def test_default_product_package_type(self): + # We try to move a product that is not contained in a package + # but has a default package type + # Destination location should become Pallet location + move = self._create_single_move(self.product) + move._assign_picking() + original_location_dest = move.location_dest_id + # Change default product package type + move.product_id.package_type_id = self.pallets_package_storage_type + self._update_qty_in_location( + move.location_id, move.product_id, move.product_qty + ) + move._action_assign() + move_line = move.move_line_ids + + self.assertEqual(move_line.location_dest_id, self.pallets_bin_1_location) + + self.assertNotEqual( + original_location_dest, + move_line.location_dest_id, + ) + + def test_storage_strategy_ordered_locations_cardboxes_with_new_leaf_putaway(self): + """ + In this scenario, we check that a storage strategy is well applied + but, then, check that a standard putaway rule has been applied too. + + Storage rule applied: for Cardboxes + Putaway rule: From Carboxes bin location 1 to Cardbox leaf 1 + + Location Structure: + + Stock + -- Cardbox Bin + ----- Cardbox leaf + """ + + # Create the fixed location + self.fix_location = self.env["stock.location"].create( + { + "name": "Cardbox 1 Fixed", + "location_id": self.cardboxes_bin_1_location.id, + } + ) + + # Create the putaway rule + self.env["stock.putaway.rule"].create( + { + "product_id": self.product.id, + "location_in_id": self.cardboxes_bin_1_location.id, + "location_out_id": self.fix_location.id, + } + ) + self.cardboxes_bin_1_location.pack_putaway_strategy = "none" + + # Create the sequence for Bin 1 + + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.cardboxes_bin_1_location.id, + "sequence": 1, + } + ) + + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 8.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 4.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_cardbox_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 4.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_cardbox_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() # TODO drop ? + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.fix_location, + ) + + def test_storage_strategy_with_view(self): + """ + Create a new locations structure: + - Stock + - Food (View) + - Food A (View) + - Pallet 1 + - Pallet 2 + - Food B (View) + - Cardbox 1 + - Cardbox 2 + + A storage sequence strategy is set on Food (View) to + go to Cardboxes (Food B) for package type cardboxes + + A putaway rule is set on Food B to go to Cardbox2 for + product + + Check the product goes well to Cardbox2 + """ + self.food_view = self.env["stock.location"].create( + { + "name": "Food View", + "location_id": self.warehouse.lot_stock_id.id, + "usage": "view", + } + ) + + self.food_pallets = self.env["stock.location"].create( + { + "name": "Food A", + "location_id": self.food_view.id, + "usage": "view", + } + ) + + self.food_pallet_1 = self.env["stock.location"].create( + { + "name": "Food Pallet 1", + "location_id": self.food_pallets.id, + } + ) + self.food_pallet_2 = self.env["stock.location"].create( + { + "name": "Food Pallet 2", + "location_id": self.food_pallets.id, + } + ) + + self.food_cardboxes = self.env["stock.location"].create( + { + "name": "Food B", + "location_id": self.food_view.id, + "storage_category_id": self.env.ref( + "stock_storage_type.storage_category_cardboxes" + ).id, + "usage": "view", + } + ) + + self.food_cardbox_1 = self.env["stock.location"].create( + { + "name": "Food Cardbox 1", + "location_id": self.food_cardboxes.id, + } + ) + self.food_cardbox_2 = self.env["stock.location"].create( + { + "name": "Food Cardbox 2", + "location_id": self.food_cardboxes.id, + } + ) + + self.env["stock.putaway.rule"].create( + { + "product_id": self.product.id, + "location_in_id": self.food_cardboxes.id, + "location_out_id": self.food_cardbox_2.id, + } + ) + + self.food_view.pack_putaway_strategy = "none" + + self.env["stock.storage.location.sequence"].create( + { + "package_type_id": self.cardboxes_package_storage_type.id, + "location_id": self.food_cardboxes.id, + "sequence": 1, + } + ) + + move = self._create_single_move(self.product) + move.location_dest_id = self.food_view + move._assign_picking() + package = self.env["stock.quant.package"].create( + {"product_packaging_id": self.product_lot_cardbox_product_packaging.id} + ) + self._update_qty_in_location( + move.location_id, move.product_id, move.product_qty, package=package + ) + + move._action_assign() + move_line = move.move_line_ids + package_level = move_line.package_level_id + + self.assertEqual( + package_level.location_dest_id, + self.food_cardbox_2, + "the move line's destination must stay in Stock as we have" + " a 'none' strategy on it and it is in the sequence", + ) diff --git a/stock_storage_type/views/product_template.xml b/stock_storage_type/views/product_template.xml new file mode 100644 index 00000000000..fdc622c5279 --- /dev/null +++ b/stock_storage_type/views/product_template.xml @@ -0,0 +1,19 @@ + + + + product.template_procurement.inherit + product.template + + + + + + + + diff --git a/stock_storage_type/views/stock_location.xml b/stock_storage_type/views/stock_location.xml new file mode 100644 index 00000000000..2bcb3ba94c7 --- /dev/null +++ b/stock_storage_type/views/stock_location.xml @@ -0,0 +1,33 @@ + + + + stock.location.form.inherit (in stock_storage_type) + stock.location + + + + + + + + + + + + + diff --git a/stock_storage_type/views/stock_package_level.xml b/stock_storage_type/views/stock_package_level.xml new file mode 100644 index 00000000000..7c5b8116e1e --- /dev/null +++ b/stock_storage_type/views/stock_package_level.xml @@ -0,0 +1,43 @@ + + + + Package Level Inherit (in stock_storage_type) + stock.package_level + + + + + + + [("id", "in", allowed_location_dest_ids)] + + + + + Package Level Tree Picking Inherit (in stock_storage_type) + stock.package_level + + + + + + + [("id", "in", allowed_location_dest_ids)] + + +