diff --git a/stock_location_tray/README.rst b/stock_location_tray/README.rst new file mode 100644 index 000000000000..9254e09ddba1 --- /dev/null +++ b/stock_location_tray/README.rst @@ -0,0 +1,132 @@ +============== +Location Trays +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a243a579cbc68148c86fd438f3c644d478501f3a4f20511630cf19fe7d789481 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/18.0/stock_location_tray + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-18-0/stock-logistics-warehouse-18-0-stock_location_tray + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-warehouse&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add an optional Tray Type on Stock Locations. A tray type defines a +number of columns and rows. A location with a tray type becomes a tray, +and sub-locations are automatically created according to the columns and +rows of the tray type + +|image1| + +.. |image1| image:: https://raw.githubusercontent.com/OCA/stock-logistics-warehouse/18.0/stock_location_tray/static/description/location-tray.png + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +General +------- + +In Inventory Settings, you must have: + + - Storage Locations + +Tray types +---------- + +Tray types can be configured in the Inventory settings. A tray type +defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols \* m rows. + +Locations +--------- + +The tray type can be configured in Stock Locations. + +The tray type of a tray can be changed as long as none of its cell +contains products. When changed, it archives the cells and creates new +ones as configured on the new tray type. + +The matrix widget on Tray locations can be clicked to reach a +sub-location. Blue squares represent the locations that contain goods. + +Known issues / Roadmap +====================== + +The buttons on operations opens a view with the tray matrix to show +operators where to pick/put goods. The issue is that Odoo allows only +one modal popup to be open at a time. The tray matrix replaces the +operations window. We have to find a way to prevent this. The tray +matrix could be displayed through a tooltip maybe, if we find how to +render a widget in a tooltip. + +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 + +Contributors +------------ + +- Guewen Baconnier +- Phuc Tran Thanh +- Do Anh Duy + +Other credits +------------- + +The development and migration of this module has been financially +supported by: + +- Camptocamp + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_location_tray/__init__.py b/stock_location_tray/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_location_tray/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_location_tray/__manifest__.py b/stock_location_tray/__manifest__.py new file mode 100644 index 000000000000..15eb1ddb13e8 --- /dev/null +++ b/stock_location_tray/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Location Trays", + "summary": "Organize a location as a matrix of cells", + "version": "18.0.1.0.0", + "category": "Stock", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["stock", "base_sparse_field"], + "website": "https://github.com/OCA/stock-logistics-warehouse", + "demo": ["demo/stock_location_tray_type_demo.xml", "demo/stock_location_demo.xml"], + "data": [ + "security/ir.model.access.csv", + "views/stock_location_views.xml", + "views/stock_location_tray_type_views.xml", + "views/stock_move_line_views.xml", + ], + "assets": { + "web.assets_backend": [ + "stock_location_tray/static/src/scss/stock_location_tray.scss", + "stock_location_tray/static/src/js/backend/stock_location_tray.esm.js", + "stock_location_tray/static/src/js/backend/stock_location_tray.xml", + ], + }, + "installable": True, +} diff --git a/stock_location_tray/demo/stock_location_demo.xml b/stock_location_tray/demo/stock_location_demo.xml new file mode 100644 index 000000000000..fe130035997b --- /dev/null +++ b/stock_location_tray/demo/stock_location_demo.xml @@ -0,0 +1,22 @@ + + + + Tray + TRAY + + + internal + + + + + stock_location_tray + + diff --git a/stock_location_tray/demo/stock_location_tray_type_demo.xml b/stock_location_tray/demo/stock_location_tray_type_demo.xml new file mode 100644 index 000000000000..71aa9d18af3d --- /dev/null +++ b/stock_location_tray/demo/stock_location_tray_type_demo.xml @@ -0,0 +1,63 @@ + + + + Small 32x + B10804 + 4 + 8 + + + Small 16x + B20802 + 2 + 8 + + + Small 8x + B20402 + 2 + 4 + + + Small 16x + B40802 + 2 + 8 + + + Small 16x + B30404 + 4 + 4 + + + Large 32x + B20804 + 4 + 8 + + + Large 16x + B30802 + 2 + 8 + + + Large 8x + B30402 + 2 + 4 + + + Large 4x + B30401 + 1 + 4 + + + Large 16x + B30404 + 4 + 4 + + diff --git a/stock_location_tray/i18n/it.po b/stock_location_tray/i18n/it.po new file mode 100644 index 000000000000..e54913984f35 --- /dev/null +++ b/stock_location_tray/i18n/it.po @@ -0,0 +1,313 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_location_tray +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__active +msgid "Active" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.actions.act_window,help:stock_location_tray.action_stock_location_tray_type +msgid "Add a Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_search +msgid "Archived" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__cell_in_tray_type_id +msgid "Cell Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_matrix +msgid "Cells" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location__cell_name_format +msgid "" +"Cells sub-locations generated in a tray will be named after this format. " +"Replacement fields between curly braces are used to inject positions. {x}, " +"{y}, and {z} will be replaced by their corresponding position. Complex " +"formatting (such as padding, ...) can be done using the format specification" +" at https://docs.python.org/3/library/string.html#formatstrings" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_tray +msgid "Close" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__code +msgid "Code" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__cols +msgid "Cols" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__create_date +msgid "Created on" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.actions.act_window,help:stock_location_tray.action_stock_location_tray_type +msgid "" +"Define the number of rows and cols on a tray, depending of the boxes\n" +"size." +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__depth +msgid "Depth" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__depth_per_cell +msgid "Depth Per Cell" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__depth +msgid "Depth of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__tray_dest_matrix +msgid "Destination Cell" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_move_line.py:0 +#, python-format +msgid "Destination Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__display_name +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__display_name +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Disposition" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__height +msgid "Height" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__height +msgid "Height of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__id +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__id +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__id +msgid "ID" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_location +msgid "Inventory Locations" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location____last_update +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type____last_update +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__location_ids +msgid "Location" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location.py:0 +#, python-format +msgid "Location %s has sub-locations, it cannot be converted to a tray." +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_search +msgid "Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.actions.act_window,name:stock_location_tray.action_stock_location_tray_type +#: model:ir.ui.menu,name:stock_location_tray.menu_stock_location_tray_type +msgid "Location Tray Types" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Locations" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__name +msgid "Name" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__cell_name_format +msgid "Name Format for Cells" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__rows +msgid "Rows" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_operation_tree +msgid "Show Destination Tray" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_operation_tree +msgid "Show Source Tray" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Sizes" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__tray_source_matrix +msgid "Source Cell" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_move_line.py:0 +#, python-format +msgid "Source Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_location_tray_type +msgid "Stock Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location_tray_type.py:0 +#, python-format +msgid "" +"The tray type {} is used by the following locations and cannot be archived:\n" +"\n" +"{}" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location_tray_type.py:0 +#, python-format +msgid "" +"The tray type {} is used by the following locations, it's size cannot be changed:\n" +"\n" +"{}" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_location_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_tray +msgid "Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_cell_contains_stock +msgid "Tray Cell Contains Stock" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Tray Configuration" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__tray_matrix +msgid "Tray Matrix" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_type_id +msgid "Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location__tray_cell_contains_stock +msgid "Used to know if a cell of a Tray location is empty." +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__width +msgid "Width" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__width_per_cell +msgid "Width Per Cell" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__width +msgid "Width of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "mm /" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "mm per cell" +msgstr "" diff --git a/stock_location_tray/i18n/stock_location_tray.pot b/stock_location_tray/i18n/stock_location_tray.pot new file mode 100644 index 000000000000..a2f780799dfb --- /dev/null +++ b/stock_location_tray/i18n/stock_location_tray.pot @@ -0,0 +1,312 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_location_tray +# +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_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__active +msgid "Active" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.actions.act_window,help:stock_location_tray.action_stock_location_tray_type +msgid "Add a Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_search +msgid "Archived" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__cell_in_tray_type_id +msgid "Cell Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_matrix +msgid "Cells" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location__cell_name_format +msgid "" +"Cells sub-locations generated in a tray will be named after this format. " +"Replacement fields between curly braces are used to inject positions. {x}, " +"{y}, and {z} will be replaced by their corresponding position. Complex " +"formatting (such as padding, ...) can be done using the format specification" +" at https://docs.python.org/3/library/string.html#formatstrings" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_tray +msgid "Close" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__code +msgid "Code" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__cols +msgid "Cols" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__create_date +msgid "Created on" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.actions.act_window,help:stock_location_tray.action_stock_location_tray_type +msgid "" +"Define the number of rows and cols on a tray, depending of the boxes\n" +"size." +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__depth +msgid "Depth" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__depth_per_cell +msgid "Depth Per Cell" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__depth +msgid "Depth of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__tray_dest_matrix +msgid "Destination Cell" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_move_line.py:0 +#, python-format +msgid "Destination Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__display_name +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__display_name +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Disposition" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__height +msgid "Height" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__height +msgid "Height of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__id +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__id +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__id +msgid "ID" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_location +msgid "Inventory Locations" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location____last_update +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type____last_update +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__location_ids +msgid "Location" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location.py:0 +#, python-format +msgid "Location %s has sub-locations, it cannot be converted to a tray." +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_search +msgid "Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.actions.act_window,name:stock_location_tray.action_stock_location_tray_type +#: model:ir.ui.menu,name:stock_location_tray.menu_stock_location_tray_type +msgid "Location Tray Types" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Locations" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__name +msgid "Name" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__cell_name_format +msgid "Name Format for Cells" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__rows +msgid "Rows" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_operation_tree +msgid "Show Destination Tray" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_operation_tree +msgid "Show Source Tray" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Sizes" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_move_line__tray_source_matrix +msgid "Source Cell" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_move_line.py:0 +#, python-format +msgid "Source Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model,name:stock_location_tray.model_stock_location_tray_type +msgid "Stock Location Tray Type" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location_tray_type.py:0 +#, python-format +msgid "" +"The tray type {} is used by the following locations and cannot be archived:\n" +"\n" +"{}" +msgstr "" + +#. module: stock_location_tray +#: code:addons/stock_location_tray/models/stock_location_tray_type.py:0 +#, python-format +msgid "" +"The tray type {} is used by the following locations, it's size cannot be changed:\n" +"\n" +"{}" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_location_form +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_move_line_tray +msgid "Tray" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_cell_contains_stock +msgid "Tray Cell Contains Stock" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "Tray Configuration" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__tray_matrix +msgid "Tray Matrix" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location__tray_type_id +msgid "Tray Type" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location__tray_cell_contains_stock +msgid "Used to know if a cell of a Tray location is empty." +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__width +msgid "Width" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,field_description:stock_location_tray.field_stock_location_tray_type__width_per_cell +msgid "Width Per Cell" +msgstr "" + +#. module: stock_location_tray +#: model:ir.model.fields,help:stock_location_tray.field_stock_location_tray_type__width +msgid "Width of the tray in mm" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "mm /" +msgstr "" + +#. module: stock_location_tray +#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_form +msgid "mm per cell" +msgstr "" diff --git a/stock_location_tray/models/__init__.py b/stock_location_tray/models/__init__.py new file mode 100644 index 000000000000..5a84dcfd5757 --- /dev/null +++ b/stock_location_tray/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_location +from . import stock_location_tray_type +from . import stock_move_line diff --git a/stock_location_tray/models/stock_location.py b/stock_location_tray/models/stock_location.py new file mode 100644 index 000000000000..45f48cf78dbf --- /dev/null +++ b/stock_location_tray/models/stock_location.py @@ -0,0 +1,250 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from collections import defaultdict + +from odoo import api, exceptions, fields, models + +from odoo.addons.base_sparse_field.models.fields import Serialized + + +class StockLocation(models.Model): + _inherit = "stock.location" + + tray_type_id = fields.Many2one( + comodel_name="stock.location.tray.type", ondelete="restrict" + ) + cell_in_tray_type_id = fields.Many2one( + string="Cell Tray Type", related="location_id.tray_type_id" + ) + tray_cell_contains_stock = fields.Boolean( + compute="_compute_tray_cell_contains_stock", + help="Used to know if a cell of a Tray location is empty.", + ) + tray_matrix = Serialized(string="Cells", compute="_compute_tray_matrix") + cell_name_format = fields.Char( + string="Name Format for Cells", + default=lambda self: self._default_cell_name_format(), + help="Cells sub-locations generated in a tray will be named" + " after this format. Replacement fields between curly braces are used" + " to inject positions. {x}, {y}, and {z} will be replaced by their" + " corresponding position. Complex formatting (such as padding, ...)" + " can be done using the format specification at " + " https://docs.python.org/3/library/string.html#formatstrings", + ) + + def _default_cell_name_format(self): + return "x{x:0>2}y{y:0>2}" + + @api.depends("quant_ids.quantity") + def _compute_tray_cell_contains_stock(self): + for location in self: + if not location.cell_in_tray_type_id: + # Not a tray cell so the value is irrelevant, + # best to skip them for performance. + location.tray_cell_contains_stock = False + continue + quants = location.quant_ids.filtered(lambda r: r.quantity > 0) + location.tray_cell_contains_stock = bool(quants) + + @api.depends("quant_ids.quantity", "tray_type_id", "location_id.tray_type_id") + def _compute_tray_matrix(self): + for location in self: + if not (location.tray_type_id or location.cell_in_tray_type_id): + location.tray_matrix = {} + continue + location.tray_matrix = location._tray_matrix_for_widget() + + def _tray_matrix_for_widget(self): + selected = self._tray_cell_coords() + cells = self._tray_cell_matrix() + return { + # x, y: position of the selected cell + "selected": selected, + # 0 is empty, 1 is not + "cells": cells, + } + + def action_tray_matrix_click(self, coordX, coordY): + self.ensure_one() + if self.cell_in_tray_type_id: + tray = self.location_id + else: + tray = self + location = self.search( + [ + ("id", "child_of", tray.ids), + # we receive positions counting from 0 but they are stored + # in the "human" format starting from 1 + ("posx", "=", coordX + 1), + ("posy", "=", coordY + 1), + ] + ) + location.ensure_one() + view = self.env.ref("stock.view_location_form") + action = self.env["ir.actions.act_window"]._for_xml_id( + "stock.action_location_form" + ) + action.update( + { + "res_id": location.id, + "view_mode": "form", + "view_type": "form", + "view_id": view.id, + "views": [(view.id, "form")], + "target": "new", + } + ) + return action + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._update_tray_sublocations() + return records + + def _check_before_add_tray_type(self): + if not self.tray_type_id and self.child_ids: + raise exceptions.UserError( + self.env._( + "Location %(display_name)s has sub-locations, " + "it cannot be converted to a tray.", + display_name=self.display_name, + ) + ) + + def write(self, vals): + for location in self: + trays_to_update = False + if "tray_type_id" in vals: + location._check_before_add_tray_type() + new_tray_type_id = vals.get("tray_type_id") + trays_to_update = location.tray_type_id.id != new_tray_type_id + # short-circuit this check if we already know that we have to + # update trays + if not trays_to_update and "cell_name_format" in vals: + new_format = vals.get("cell_name_format") + trays_to_update = location.cell_name_format != new_format + super().write(vals) + if trays_to_update: + self._update_tray_sublocations() + elif "posz" in vals and location.tray_type_id: + # On initial generation (when tray_to_update is true), + # the sublocations are already generated with the pos z. + location.child_ids.write({"posz": vals["posz"]}) + return True + + def tray_cell_center_position(self): + """Return the center position in mm of a cell + + The returned position is a tuple with the number of millimeters + from the bottom-left corner. Tuple: (left, bottom) + """ + if not self.cell_in_tray_type_id: + return 0, 0 + posx = self.posx + posy = self.posy + cell_width = self.cell_in_tray_type_id.width_per_cell + cell_depth = self.cell_in_tray_type_id.depth_per_cell + # posx and posy start at one, we want to count from 0 + from_left = (posx - 1) * cell_width + (cell_width / 2) + from_bottom = (posy - 1) * cell_depth + (cell_depth / 2) + return from_left, from_bottom + + def _tray_cell_coords(self): + if not self.cell_in_tray_type_id: + return [] + return [self.posx - 1, self.posy - 1] + + def _tray_cell_matrix(self): + assert self.tray_type_id or self.cell_in_tray_type_id + if self.tray_type_id: + location = self + else: # cell + location = self.location_id + cells = location.tray_type_id._generate_cells_matrix() + for cell in location.child_ids: + if cell.tray_cell_contains_stock: + # 1 mean used + cells[cell.posy - 1][cell.posx - 1] = 1 + return cells + + def _format_tray_sublocation_name(self, x, y, z): + template = self.cell_name_format or self._default_cell_name_format() + # using format_map allows to have missing replacement strings + return template.format_map(defaultdict(str, x=x, y=y, z=z)) + + def _update_tray_sublocations(self): + values = [] + for location in self: + tray_type = location.tray_type_id + location.child_ids.write({"active": False}) + if not tray_type: + continue + + # create accepts several records now + posz = location.posz or 0 + for row in range(1, tray_type.rows + 1): + for col in range(1, tray_type.cols + 1): + cell_name = location._format_tray_sublocation_name(col, row, posz) + subloc_values = { + "name": cell_name, + "posx": col, + "posy": row, + "posz": posz, + "location_id": location.id, + "company_id": location.company_id.id, + "tray_type_id": False, + } + values.append(subloc_values) + if values: + self.create(values) + + def _create_tray_xmlids(self, module): + """Create external IDs for generated cells + + If the tray location has one. Used for the demo/test data. It will not + handle properly changing the tray format as the former cells will keep + the original xmlid built on x and y, the new ones will not be able to + use them. As these xmlids are meant for the demo data and the tests, + it is not a problem and should not be used for other purposes. + + Called from stock_location_tray/demo/stock_location_demo.xml. + """ + xmlids_to_create = [] + ModelData = self.env["ir.model.data"] + + def has_ref(xmlid): + __, res_id = ModelData._xmlid_to_res_model_res_id(xmlid) + return bool(res_id) + + for location in self: + if not location.cell_in_tray_type_id: + continue + tray = location.location_id + tray_external_id = tray.get_external_id().get(tray.id) + if not tray_external_id: + continue + # This will never happen as both name and module are required + # in ir.model.data + # if "." not in tray_external_id: + # continue + namespace, tray_name = tray_external_id.split(".") + if module != namespace: + continue + xmlid = f"{module}.{tray_name}" + tray_external = ModelData.browse(ModelData._xmlid_lookup(xmlid)[1]) + cell_external_id = f"{tray_name}_x{location.posx}y{location.posy}" + cell_xmlid = f"{module}.{cell_external_id}" + if not has_ref(cell_xmlid): + xmlids_to_create.append( + { + "name": cell_external_id, + "module": module, + "model": self._name, + "res_id": location.id, + "noupdate": tray_external.noupdate, + } + ) + + ModelData.create(xmlids_to_create) diff --git a/stock_location_tray/models/stock_location_tray_type.py b/stock_location_tray/models/stock_location_tray_type.py new file mode 100644 index 000000000000..4b0ff14d40ab --- /dev/null +++ b/stock_location_tray/models/stock_location_tray_type.py @@ -0,0 +1,101 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, exceptions, fields, models + +from odoo.addons.base_sparse_field.models.fields import Serialized + + +class StockLocationTrayType(models.Model): + _name = "stock.location.tray.type" + _description = "Stock Location Tray Type" + _rec_names_search = ["name", "code"] + + name = fields.Char(required=True) + code = fields.Char(required=True) + rows = fields.Integer(required=True) + cols = fields.Integer(required=True) + + width = fields.Integer(help="Width of the tray in mm") + depth = fields.Integer(help="Depth of the tray in mm") + height = fields.Integer(help="Height of the tray in mm") + + width_per_cell = fields.Float(compute="_compute_width_per_cell") + depth_per_cell = fields.Float(compute="_compute_depth_per_cell") + + active = fields.Boolean(default=True) + tray_matrix = Serialized(compute="_compute_tray_matrix") + location_ids = fields.One2many( + comodel_name="stock.location", inverse_name="tray_type_id" + ) + + @api.depends("width", "cols") + def _compute_width_per_cell(self): + for record in self: + width = record.width + if not width: + record.width_per_cell = 0.0 + continue + record.width_per_cell = width / record.cols + + @api.depends("depth", "rows") + def _compute_depth_per_cell(self): + for record in self: + depth = record.depth + if not depth: + record.depth_per_cell = 0.0 + continue + record.depth_per_cell = depth / record.rows + + @api.depends("rows", "cols") + def _compute_tray_matrix(self): + for record in self: + # As we only want to show the disposition of + # the tray, we generate a "full" tray, we'll + # see all the boxes on the web widget. + # (0 means empty, 1 means used) + cells = self._generate_cells_matrix(default_state=1) + record.tray_matrix = {"selected": [], "cells": cells} + + def _generate_cells_matrix(self, default_state=0): + return [[default_state] * self.cols for __ in range(self.rows)] + + @api.constrains("active") + def _location_check_active(self): + for record in self.filtered(lambda r: not r.active and r.location_ids): + location_bullets = [ + f" - {location.display_name}" for location in record.location_ids + ] + raise exceptions.ValidationError( + self.env._( + "The tray type %(name)s is used by the following locations " + "and cannot be archived:\n\n%(location_bullets)s", + name=record.name, + location_bullets="\n".join(location_bullets), + ) + ) + + @api.constrains("rows", "cols") + def _location_check_rows_cols(self): + for record in self: + if record.location_ids: + location_bullets = [ + f" - {location.display_name}" for location in record.location_ids + ] + raise exceptions.ValidationError( + self.env._( + "The tray type %(name)s is used by the following locations, " + "it's size cannot be changed:\n\n%(location_bullets)s", + name=record.name, + location_bullets="\n".join(location_bullets), + ) + ) + + def open_locations(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "stock.action_location_form" + ) + action["domain"] = [("tray_type_id", "in", self.ids)] + if len(self.ids) == 1: + action["context"] = {"default_tray_type_id": self.id} + return action diff --git a/stock_location_tray/models/stock_move_line.py b/stock_location_tray/models/stock_move_line.py new file mode 100644 index 000000000000..aa923e486eb7 --- /dev/null +++ b/stock_location_tray/models/stock_move_line.py @@ -0,0 +1,64 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +from odoo.addons.base_sparse_field.models.fields import Serialized + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + tray_source_matrix = Serialized( + string="Source Cell", compute="_compute_tray_matrix" + ) + tray_dest_matrix = Serialized( + string="Destination Cell", compute="_compute_tray_matrix" + ) + show_tray_source_location = fields.Boolean(compute="_compute_show_tray_location") + show_tray_destination_location = fields.Boolean( + compute="_compute_show_tray_location" + ) + + @api.depends("location_id", "location_dest_id") + def _compute_tray_matrix(self): + for record in self: + record.tray_source_matrix = record.location_id.tray_matrix + record.tray_dest_matrix = record.location_dest_id.tray_matrix + + @api.depends("move_id.picking_type_id.code") + def _compute_show_tray_location(self): + for line in self: + picking_type_code = line.move_id.picking_type_id.code + line.show_tray_source_location = picking_type_code != "incoming" + line.show_tray_destination_location = picking_type_code != "outgoing" + + def _action_show_tray(self, location_from): + assert location_from in ("source", "dest") + self.ensure_one() + view = self.env.ref("stock_location_tray.view_stock_move_line_tray") + context = self.env.context.copy() + if location_from == "source": + name = self.env._("Source Tray") + context["show_source_tray"] = True + else: + name = self.env._("Destination Tray") + context["show_dest_tray"] = True + return { + "name": name, + "type": "ir.actions.act_window", + "view_type": "form", + "view_mode": "form", + "res_model": "stock.move.line", + "views": [(view.id, "form")], + "view_id": view.id, + "target": "new", + "res_id": self.id, + "context": context, + } + + def action_show_source_tray(self): + return self._action_show_tray("source") + + def action_show_dest_tray(self): + return self._action_show_tray("dest") diff --git a/stock_location_tray/pyproject.toml b/stock_location_tray/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/stock_location_tray/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/stock_location_tray/readme/CONFIGURE.md b/stock_location_tray/readme/CONFIGURE.md new file mode 100644 index 000000000000..604733af5979 --- /dev/null +++ b/stock_location_tray/readme/CONFIGURE.md @@ -0,0 +1,22 @@ +## General + +In Inventory Settings, you must have: + +> - Storage Locations + +## Tray types + +Tray types can be configured in the Inventory settings. A tray type +defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols \* m rows. + +## Locations + +The tray type can be configured in Stock Locations. + +The tray type of a tray can be changed as long as none of its cell +contains products. When changed, it archives the cells and creates new +ones as configured on the new tray type. + +The matrix widget on Tray locations can be clicked to reach a +sub-location. Blue squares represent the locations that contain goods. diff --git a/stock_location_tray/readme/CONTRIBUTORS.md b/stock_location_tray/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..1e96451d4cbb --- /dev/null +++ b/stock_location_tray/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Guewen Baconnier \<\> +- Phuc Tran Thanh \<\> +- Do Anh Duy \<\> diff --git a/stock_location_tray/readme/CREDITS.md b/stock_location_tray/readme/CREDITS.md new file mode 100644 index 000000000000..c2d2a1e6b83c --- /dev/null +++ b/stock_location_tray/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development and migration of this module has been financially supported by: + +- Camptocamp diff --git a/stock_location_tray/readme/DESCRIPTION.md b/stock_location_tray/readme/DESCRIPTION.md new file mode 100644 index 000000000000..8efd6d7e229e --- /dev/null +++ b/stock_location_tray/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +Add an optional Tray Type on Stock Locations. A tray type defines a +number of columns and rows. A location with a tray type becomes a tray, +and sub-locations are automatically created according to the columns and +rows of the tray type + +![](../static/description/location-tray.png) diff --git a/stock_location_tray/readme/ROADMAP.md b/stock_location_tray/readme/ROADMAP.md new file mode 100644 index 000000000000..d70df7092e8d --- /dev/null +++ b/stock_location_tray/readme/ROADMAP.md @@ -0,0 +1,6 @@ +The buttons on operations opens a view with the tray matrix to show +operators where to pick/put goods. The issue is that Odoo allows only +one modal popup to be open at a time. The tray matrix replaces the +operations window. We have to find a way to prevent this. The tray +matrix could be displayed through a tooltip maybe, if we find how to +render a widget in a tooltip. diff --git a/stock_location_tray/security/ir.model.access.csv b/stock_location_tray/security/ir.model.access.csv new file mode 100644 index 000000000000..1836f4588f95 --- /dev/null +++ b/stock_location_tray/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_location_tray_type_stock_user,access_stock_location_tray_type stock user,model_stock_location_tray_type,stock.group_stock_user,1,0,0,0 +access_stock_location_tray_type_manager,access_stock_location_tray_type stock manager,model_stock_location_tray_type,stock.group_stock_manager,1,1,1,1 diff --git a/stock_location_tray/static/description/icon.png b/stock_location_tray/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/stock_location_tray/static/description/icon.png differ diff --git a/stock_location_tray/static/description/index.html b/stock_location_tray/static/description/index.html new file mode 100644 index 000000000000..796477b33f51 --- /dev/null +++ b/stock_location_tray/static/description/index.html @@ -0,0 +1,481 @@ + + + + + +Location Trays + + + +
+

Location Trays

+ + +

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

+

Add an optional Tray Type on Stock Locations. A tray type defines a +number of columns and rows. A location with a tray type becomes a tray, +and sub-locations are automatically created according to the columns and +rows of the tray type

+

image1

+

Table of contents

+ +
+

Configuration

+
+

General

+

In Inventory Settings, you must have:

+
+
    +
  • Storage Locations
  • +
+
+
+
+

Tray types

+

Tray types can be configured in the Inventory settings. A tray type +defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols * m rows.

+
+
+

Locations

+

The tray type can be configured in Stock Locations.

+

The tray type of a tray can be changed as long as none of its cell +contains products. When changed, it archives the cells and creates new +ones as configured on the new tray type.

+

The matrix widget on Tray locations can be clicked to reach a +sub-location. Blue squares represent the locations that contain goods.

+
+
+
+

Known issues / Roadmap

+

The buttons on operations opens a view with the tray matrix to show +operators where to pick/put goods. The issue is that Odoo allows only +one modal popup to be open at a time. The tray matrix replaces the +operations window. We have to find a way to prevent this. The tray +matrix could be displayed through a tooltip maybe, if we find how to +render a widget in a tooltip.

+
+
+

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

Contributors

+ +
+
+

Other credits

+

The development and migration of this module has been financially +supported by:

+
    +
  • Camptocamp
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

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

+

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

+
+
+
+ + diff --git a/stock_location_tray/static/description/location-tray.png b/stock_location_tray/static/description/location-tray.png new file mode 100644 index 000000000000..a5e1a1303bf3 Binary files /dev/null and b/stock_location_tray/static/description/location-tray.png differ diff --git a/stock_location_tray/static/src/js/backend/stock_location_tray.esm.js b/stock_location_tray/static/src/js/backend/stock_location_tray.esm.js new file mode 100644 index 000000000000..44b828d024e3 --- /dev/null +++ b/stock_location_tray/static/src/js/backend/stock_location_tray.esm.js @@ -0,0 +1,256 @@ +// Copyright 2019 Camptocamp SA +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import {Component, onMounted, onWillDestroy, useEffect, useRef} from "@odoo/owl"; +import {browser} from "@web/core/browser/browser"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useService} from "@web/core/utils/hooks"; + +const {Object, Math} = globalThis; + +export class LocationTrayMatrixField extends Component { + /** + * Shows a canvas with the Tray's cells + * + * An action can be configured which is called when a cell is clicked. + * The action must be an action.multi, it will receive the x and y positions + * of the cell clicked (starting from 0). The action must be configured in + * the options of the field and be on the same model: + * + * + * + **/ + static template = "stock_location_tray.LocationTrayMatrix"; + static props = { + ...standardFieldProps, + clickAction: {type: String, optional: true}, + }; + setup() { + super.setup(); + this.action = useService("action"); + this.orm = useService("orm"); + this.canvasRef = useRef("canvas"); + this._ready = false; + this._resizePromise = null; + this.cellColorEmpty = "#ffffff"; + this.cellColorNotEmpty = "#4e6bfd"; + this.selectedColor = "#08f46b"; + this.selectedLineWidth = 5; + this.globalAlpha = 0.8; + this.cellPadding = 2; + + this._resizeDebounce = this._resizeDebounce.bind(this); + + useEffect( + () => { + if (this._ready) { + this._render(); + } + }, + () => [this.props.record.data[this.props.name]] + ); + + onMounted(() => { + browser.addEventListener("resize", this._resizeDebounce); + this._ready = true; + this._resizeDebounce(); + }); + + onWillDestroy(() => { + browser.removeEventListener("resize", this._resizeDebounce); + }); + } + + get canvas() { + return this.canvasRef.el; + } + + get isSet() { + const value = this.props.record.data[this.props.name]; + return value && Object.keys(value).length > 0 && value.cells?.length > 0; + } + + get clickAction() { + return this.props.options?.click_action; + } + + async _onClick(ev) { + if (!this.isSet || !this.props.clickAction) { + return; + } + + const width = this.canvas.width; + const height = this.canvas.height; + const rect = this.canvas.getBoundingClientRect(); + + const clickX = ev.clientX - rect.left; + const clickY = ev.clientY - rect.top; + + const cells = this.props.record.data[this.props.name].cells; + const cols = cells[0].length; + const rows = cells.length; + + // We remove 1 to start counting from 0 + let coordX = Math.ceil((clickX * cols) / width) - 1; + let coordY = Math.ceil((clickY * rows) / height) - 1; + + // If we click on the last pixel on the bottom or the right + // we would get an offset index + coordX = Math.min(Math.max(coordX, 0), cols - 1); + coordY = Math.min(Math.max(coordY, 0), rows - 1); + + // The coordinate we get when we click is from top, + // but we are looking for the coordinate from the bottom + // to match the user's expectations, invert Y + coordY = Math.abs(coordY - rows + 1); + const action = await this.orm.call( + this.props.record.resModel, + this.props.clickAction, + [[this.props.record.resId], coordX, coordY], + {} + ); + + await this.action.doAction(action); + } + + _resizeDebounce() { + browser.clearTimeout(this._resizePromise); + this._resizePromise = browser.setTimeout(() => this._render(), 20); + } + + /** + * Resize the canvas width and height to the actual size. + * If we don't do that, it will automatically scale to the + * CSS size with blurry squares. + * + * @param {HTMLElement} canvas - the DOM canvas to draw + * @returns {Boolean} + */ + resizeCanvasToDisplaySize(canvas) { + const width = canvas.clientWidth; + const height = canvas.clientHeight; + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; + } + + /** + * Render the widget only when it is in the DOM. + * We need the width and height of the widget to draw the canvas. + */ + _render() { + if (!this._ready || !this.canvas) { + return; + } + + const ctx = this.canvas.getContext("2d"); + this.resizeCanvasToDisplaySize(ctx.canvas); + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.save(); + + if (this.isSet) { + const value = this.props.record.data[this.props.name]; + const selected = value.selected || []; + const cells = value.cells; + this._drawMatrix(this.canvas, ctx, cells, selected); + } + } + + /** + * Draw the cells in the canvas. + * + * @param {HTMLElement} canvas - the DOM canvas to draw + * @param {Object} ctx - the canvas 2d context + * @param {Array} cells - A 2-dimensional list of cells + * @param {Array} selected - A list containing the position (x,y) of the + * selected cell (can be empty if no cell is selected) + */ + _drawMatrix(canvas, ctx, cells, selected) { + const colors = { + 0: this.cellColorEmpty, + 1: this.cellColorNotEmpty, + }; + + const cols = cells[0].length; + const rows = cells.length; + let selectedX = null; + let selectedY = null; + + if (selected.length) { + selectedX = selected[0]; + // We draw top to bottom, but the highlighted cell should + // be a coordinate from bottom to top: reverse the y axis + selectedY = Math.abs(selected[1] - rows + 1); + } + + const padding = this.cellPadding; + const padding_width = padding * cols; + const padding_height = padding * rows; + const w = (canvas.width - padding_width) / cols; + const h = (canvas.height - padding_height) / rows; + + ctx.globalAlpha = this.globalAlpha; + // Again, our matrix is top to bottom (0 is the first line) + // but visually, we want them bottom to top + const reversed_cells = cells.slice().reverse(); + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + ctx.fillStyle = colors[reversed_cells[y][x]]; + let fillWidth = w; + let fillHeight = h; + + // Cheat: remove the padding at bottom and right + // the cells will be a bit larger but not really noticeable + if (x === cols - 1) { + fillWidth += padding; + } + if (y === rows - 1) { + fillHeight += padding; + } + + ctx.fillRect( + x * (w + padding), + y * (h + padding), + fillWidth, + fillHeight + ); + + if (selected && selectedX === x && selectedY === y) { + ctx.globalAlpha = 1.0; + ctx.strokeStyle = this.selectedColor; + ctx.lineWidth = this.selectedLineWidth; + ctx.strokeRect(x * (w + padding), y * (h + padding), w, h); + ctx.globalAlpha = this.globalAlpha; + } + } + } + ctx.restore(); + } +} + +export const locationTrayMatrixField = { + component: LocationTrayMatrixField, + supportedOptions: [ + { + name: "click_action", + type: "string", + }, + ], + supportedTypes: ["serialized"], + extractProps({options}) { + return { + clickAction: options.click_action, + }; + }, +}; + +registry.category("fields").add("location_tray_matrix", locationTrayMatrixField); diff --git a/stock_location_tray/static/src/js/backend/stock_location_tray.xml b/stock_location_tray/static/src/js/backend/stock_location_tray.xml new file mode 100644 index 000000000000..f35d28b8d9b7 --- /dev/null +++ b/stock_location_tray/static/src/js/backend/stock_location_tray.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/stock_location_tray/static/src/scss/stock_location_tray.scss b/stock_location_tray/static/src/scss/stock_location_tray.scss new file mode 100644 index 000000000000..847da8f54cea --- /dev/null +++ b/stock_location_tray/static/src/scss/stock_location_tray.scss @@ -0,0 +1,5 @@ +.o_field_location_tray_matrix canvas { + background-color: #eeeeee; + border: 2px #000000 solid; + width: 100%; +} diff --git a/stock_location_tray/tests/__init__.py b/stock_location_tray/tests/__init__.py new file mode 100644 index 000000000000..fdfe109b6955 --- /dev/null +++ b/stock_location_tray/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_location +from . import test_tray_type +from . import test_stock_move_line diff --git a/stock_location_tray/tests/common.py b/stock_location_tray/tests/common.py new file mode 100644 index 000000000000..821e3437ccf0 --- /dev/null +++ b/stock_location_tray/tests/common.py @@ -0,0 +1,42 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class LocationTrayTypeCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.wh = cls.env.ref("stock.warehouse0") + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.product = cls.env.ref("product.product_delivery_02") + cls.tray_location = cls.env.ref("stock_location_tray.stock_location_tray_demo") + cls.tray_type_small_8x = cls.env.ref( + "stock_location_tray.stock_location_tray_type_small_8x" + ) + cls.tray_type_small_32x = cls.env.ref( + "stock_location_tray.stock_location_tray_type_small_32x" + ) + + def _create_tray_z(self, tray_type=None): + tray_type = tray_type or self.tray_type_small_8x + tray_z = self.env["stock.location"].create( + { + "name": "Tray Z", + "location_id": self.stock_location.id, + "usage": "internal", + "tray_type_id": tray_type.id, + } + ) + return tray_z + + def _cell_for(self, tray, x=1, y=1): + cell = self.env["stock.location"].search( + [("location_id", "=", tray.id), ("posx", "=", x), ("posy", "=", y)] + ) + self.assertEqual(len(cell), 1, f"Cell x{x}y{y} not found for {tray.name}") + return cell + + def _update_quantity_in_cell(self, cell, product, quantity): + self.env["stock.quant"]._update_available_quantity(product, cell, quantity) diff --git a/stock_location_tray/tests/test_location.py b/stock_location_tray/tests/test_location.py new file mode 100644 index 000000000000..68e1a1bcf135 --- /dev/null +++ b/stock_location_tray/tests/test_location.py @@ -0,0 +1,245 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError + +from .common import LocationTrayTypeCase + + +class TestLocation(LocationTrayTypeCase): + def test_create_tray(self): + tray_type = self.tray_type_small_8x + tray_loc = self._create_tray_z(tray_type) + + self.assertEqual(len(tray_loc.child_ids), tray_type.cols * tray_type.rows) # 8 + self.assertTrue( + all( + subloc.cell_in_tray_type_id == tray_type + for subloc in tray_loc.child_ids + ) + ) + + def test_tray_has_stock(self): + cell = self.env.ref("stock_location_tray.stock_location_tray_demo_x3y2") + self.assertFalse(cell.quant_ids) + self.assertFalse(cell.tray_cell_contains_stock) + self._update_quantity_in_cell(cell, self.product, 1) + self.assertTrue(cell.quant_ids) + self.assertTrue(cell.tray_cell_contains_stock) + self._update_quantity_in_cell(cell, self.product, -1) + self.assertTrue(cell.quant_ids) + self.assertFalse(cell.tray_cell_contains_stock) + + def test_matrix_empty_tray(self): + self.assertEqual(self.tray_location.tray_type_id.cols, 4) + self.assertEqual(self.tray_location.tray_type_id.rows, 2) + self.assertEqual( + self.tray_location.tray_matrix, + { + # we show the entire tray, not a cell + "selected": [], + # we have no stock in this location + # fmt: off + "cells": [ + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + # fmt: on + }, + ) + saved_tray_type = self.tray_location.tray_type_id + self.tray_location.tray_type_id = False + self.assertEqual(self.tray_location.tray_matrix, {}) + subloc = self.tray_location.create({"name": "Sub Location"}) + self.tray_location.write({"child_ids": [(6, 0, subloc.id)]}) + message = "Location %s has sub-locations, it cannot be converted to a tray." + with self.assertRaisesRegex( + UserError, message % (self.tray_location.display_name) + ): + self.tray_location.tray_type_id = saved_tray_type + + def test_matrix_stock_tray(self): + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=1, y=1), self.product, 100 + ) + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=2, y=1), self.product, 100 + ) + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=4, y=2), self.product, 100 + ) + self.assertEqual(self.tray_location.tray_type_id.cols, 4) + self.assertEqual(self.tray_location.tray_type_id.rows, 2) + self.assertEqual( + self.tray_location.tray_matrix, + { + # We show the entire tray, not a cell. + "selected": [], + # Note: the coords are stored according to their index in the + # arrays so it is easier to manipulate them. However, we + # display them with the Y axis inverted in the UI to represent + # the view of the operator. + # + # [0, 0, 0, 1], + # [1, 1, 0, 0], + # + # fmt: off + "cells": [ + [1, 1, 0, 0], + [0, 0, 0, 1], + ], + # fmt: on + }, + ) + + def test_action_tray_matrix_click(self): + location_view = self.tray_location.action_tray_matrix_click(2, 1) + self.assertEqual(location_view["res_model"], "stock.location") + cell = self.env.ref("stock_location_tray.stock_location_tray_demo_x3y2") + location_view = cell.action_tray_matrix_click(2, 1) + self.assertEqual(location_view["res_model"], "stock.location") + + def test_matrix_stock_cell(self): + self.tray_location.tray_type_id = self.env.ref( + "stock_location_tray.stock_location_tray_type_large_32x" + ) + cell = self._cell_for(self.tray_location, x=7, y=3) + self._update_quantity_in_cell(cell, self.product, 100) + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=1, y=1), self.product, 100 + ) + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=3, y=2), self.product, 100 + ) + self.assertEqual(self.tray_location.tray_type_id.cols, 8) + self.assertEqual(self.tray_location.tray_type_id.rows, 4) + self.assertEqual( + cell.tray_matrix, + { + # When called on a cell, we expect to have its coords. Worth to + # note: the cell's coordinate are 7 and 3 in the posx and posy + # fields as they make sense for humans. Here, they are offset + # by -1 to have the indexes in the matrix. + "selected": [6, 2], + # fmt: off + "cells": [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + ], + # fmt: on + }, + ) + + def test_check_active_empty(self): + cell = self.env.ref("stock_location_tray.stock_location_tray_demo_x3y2") + self.assertFalse(cell.tray_cell_contains_stock) + # allowed to archive empty cell + cell.active = False + + def test_check_active_not_empty(self): + cell = self.env.ref("stock_location_tray.stock_location_tray_demo_x3y2") + self._update_quantity_in_cell(cell, self.product, 1) + self.assertTrue(cell.tray_cell_contains_stock) + + # we cannot archive an empty cell or any parent + location = cell + message = self.env._( + "You can't disable locations %s because they still contain products.", + location.display_name, + ) + while location: + with self.assertRaisesRegex(UserError, message): + location.active = False + + # restore state for the next test loop + location.active = True + location = location.location_id + if location == self.wh.lot_stock_id: + # we can't disable the Stock location anyway + break + + def test_change_tray_type_when_empty(self): + tray_type = self.tray_type_small_32x + self.tray_location.tray_type_id = tray_type + self.assertEqual( + len(self.tray_location.child_ids), + tray_type.cols * tray_type.rows, # 32 + ) + + def test_change_tray_type_error_when_not_empty(self): + self._update_quantity_in_cell( + self._cell_for(self.tray_location, x=1, y=1), self.product, 1 + ) + tray_type = self.tray_type_small_32x + location_name = self.tray_location.child_ids[0].display_name + message = self.env._( + "You can't disable locations %s because they still contain products.", + location_name, + ) + with self.assertRaisesRegex(UserError, message): + self.tray_location.tray_type_id = tray_type + + def test_location_center_pos(self): + cell = self.env.ref("stock_location_tray.stock_location_tray_demo_x3y2") + tray_type = cell.cell_in_tray_type_id + number_of_x = 4 + number_of_y = 2 + self.assertEqual((number_of_x, number_of_y), (tray_type.cols, tray_type.rows)) + + total_width = 80 + total_depth = 30 + tray_type.width = total_width + tray_type.depth = total_depth + + self.assertEqual( + (total_width / number_of_x, total_depth / number_of_y), + (tray_type.width_per_cell, tray_type.depth_per_cell), + ) + from_left, from_bottom = cell.tray_cell_center_position() + # fmt: off + expected_left = ( + (total_width / number_of_x) # width of a cell + * 2 # we want the center of the cell x3, so we want 2 full cells + + ((total_width / number_of_x) / 2) # + the half of our cell + ) + expected_bottom = ( + (total_depth / number_of_y) # depth of a cell + * 1 # we want the center of the cell y2, so we want 1 full cells + + ((total_depth / number_of_y) / 2) # + the half of our cell + ) + # fmt: on + self.assertEqual(from_left, expected_left) + self.assertEqual(from_bottom, expected_bottom) + + self.assertEqual(self.tray_location.tray_cell_center_position(), (0, 0)) + + def test_cell_name_format_posz(self): + location = self.tray_location + location.cell_name_format = "x{x:0>2}, y{y:0>2}" + self.assertEqual(location.child_ids[0].name, "x01, y01") + location.posz = 1 + self.assertEqual(location.mapped("posz"), [1]) + self.assertEqual( + location.child_ids.mapped("posz"), [1] * len(location.child_ids) + ) + + def test_create_tray_xmlids(self): + module = "stock_location_tray" + tray_z = self._create_tray_z() + # Cover if not location.cell_in_tray_type_id + tray_z._create_tray_xmlids(module) + # Cover if not tray_external_id + tray_z.child_ids._create_tray_xmlids(module) + values = { + "name": "stock_location_tray_z", + "module": module, + "model": tray_z._name, + "res_id": tray_z.id, + "noupdate": 1, + } + self.env["ir.model.data"].create(values) + # Cover if module != namespace + tray_z.child_ids._create_tray_xmlids("no_module") + tray_z.child_ids._create_tray_xmlids(module) diff --git a/stock_location_tray/tests/test_stock_move_line.py b/stock_location_tray/tests/test_stock_move_line.py new file mode 100644 index 000000000000..413a931190c2 --- /dev/null +++ b/stock_location_tray/tests/test_stock_move_line.py @@ -0,0 +1,76 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from .common import LocationTrayTypeCase + + +class TestStockMoveLine(LocationTrayTypeCase): + def create_stock_move(self): + StockMove = self.env["stock.move"] + StockMoveLine = self.env["stock.move.line"] + cell1 = self._cell_for(self.tray_location, x=3, y=1) + self._update_quantity_in_cell(cell1, self.product, 10) + tray_z = self._create_tray_z(self.tray_type_small_32x) + cell2 = self._cell_for(tray_z, x=7, y=3) + move = StockMove.create( + { + "name": "test_in_1", + "location_id": cell1.id, + "location_dest_id": cell2.id, + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "product_uom_qty": 2.0, + } + ) + StockMoveLine.create( + { + "move_id": move.id, + "product_id": move.product_id.id, + "quantity": 1, + "product_uom_id": move.product_uom.id, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + } + ) + return move + + def test_compute_tray_matrix(self): + self.move = self.create_stock_move() + move_line = self.move.move_line_ids[0] + move_line._compute_tray_matrix() + self.assertEqual( + move_line.tray_source_matrix, + { + "selected": [2, 0], + # fmt: off + "cells": [ + [0, 0, 1, 0], + [0, 0, 0, 0], + ], + # fmt: on + }, + ) + self.assertEqual( + move_line.tray_dest_matrix, + { + "selected": [6, 2], + # fmt: off + "cells": [ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + ], + # fmt: on + }, + ) + + def test_action_show_tray(self): + self.move = self.create_stock_move() + move_line = self.move.move_line_ids[0] + view = move_line.action_show_source_tray() + self.assertEqual(view["name"], "Source Tray") + self.assertEqual(view["res_id"], move_line.id) + view = move_line.action_show_dest_tray() + self.assertEqual(view["res_id"], move_line.id) diff --git a/stock_location_tray/tests/test_tray_type.py b/stock_location_tray/tests/test_tray_type.py new file mode 100644 index 000000000000..d359473945f4 --- /dev/null +++ b/stock_location_tray/tests/test_tray_type.py @@ -0,0 +1,92 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError + +from .common import LocationTrayTypeCase + + +class TestLocationTrayType(LocationTrayTypeCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.used_tray_type = cls.env.ref( + "stock_location_tray.stock_location_tray_type_large_16x" + ) + cls.unused_tray_type = cls.env.ref( + "stock_location_tray.stock_location_tray_type_small_16x_3" + ) + + def test_tray_type(self): + # any location created directly under the view is a shuttle + tray_type = self.env["stock.location.tray.type"].create( + {"name": "Test Type", "code": "🐵", "rows": 4, "cols": 6} + ) + self.assertEqual( + tray_type.tray_matrix, + { + "selected": [], # no selection as this is the "model" + # a "full" matrix is generated for display on the UI + # fmt: off + "cells": [ + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + ], + # fmt: on + }, + ) + self.assertEqual(len(tray_type.name_search()), 11) + self.assertEqual(len(tray_type.name_search("Test")), 1) + self.assertEqual(len(tray_type.name_search("🐵")), 1) + self.assertEqual(len(tray_type.name_search("None")), 0) + action = tray_type.open_locations() + self.assertEqual(action["res_model"], "stock.location") + self.assertEqual(action["domain"], [("tray_type_id", "in", tray_type.ids)]) + self.assertEqual(action["context"], {"default_tray_type_id": tray_type.id}) + + def test_check_active(self): + location = self.tray_location + location.tray_type_id = self.used_tray_type + location = self.used_tray_type.location_ids + self.assertTrue(location) + message = f"cannot be archived.\n\n.*{location.name}*" + # we cannot archive used ones + with self.assertRaisesRegex(ValidationError, message): + self.used_tray_type.active = False + # we can archive unused ones + self.unused_tray_type.active = False + # this is to increase test coverage + self.tray_location.tray_type_id = False + + def test_check_cols_rows(self): + location = self.tray_location + location.tray_type_id = self.used_tray_type + location = self.used_tray_type.location_ids + self.assertTrue(location) + message = f"size cannot be changed.\n\n.*{location.name}*" + # we cannot modify size of used ones + with self.assertRaisesRegex(ValidationError, message): + self.used_tray_type.rows = 10 + with self.assertRaisesRegex(ValidationError, message): + self.used_tray_type.cols = 10 + # we can modify size of unused ones + self.unused_tray_type.rows = 10 + self.unused_tray_type.cols = 10 + + def test_width_per_cell(self): + tray_type = self.used_tray_type + tray_type.cols = 10 + tray_type.width = 120 + self.assertEqual(tray_type.width_per_cell, 12) + tray_type.width = 0 + self.assertEqual(tray_type.width_per_cell, 0) + + def test_depth_per_cell(self): + tray_type = self.used_tray_type + tray_type.rows = 10 + tray_type.depth = 120 + self.assertEqual(tray_type.depth_per_cell, 12) + tray_type.depth = 0 + self.assertEqual(tray_type.depth_per_cell, 0) diff --git a/stock_location_tray/views/stock_location_tray_type_views.xml b/stock_location_tray/views/stock_location_tray_type_views.xml new file mode 100644 index 000000000000..5e19d8f1c5f5 --- /dev/null +++ b/stock_location_tray/views/stock_location_tray_type_views.xml @@ -0,0 +1,119 @@ + + + + stock.location.tray.type.form + stock.location.tray.type + +
+
+
+ +
+
+ + stock.location.tray.type.search + stock.location.tray.type + + + + + + + + + + + stock.location.tray.type.list + stock.location.tray.type + + + + + + + + + + + Location Tray Types + stock.location.tray.type + ir.actions.act_window + + + +

+ Add a Location Tray Type +

+

+ Define the number of rows and cols on a tray, depending of the boxes +size. +

+
+
+ +
diff --git a/stock_location_tray/views/stock_location_views.xml b/stock_location_tray/views/stock_location_views.xml new file mode 100644 index 000000000000..5a1b0b163330 --- /dev/null +++ b/stock_location_tray/views/stock_location_views.xml @@ -0,0 +1,52 @@ + + + + stock.location.form.tray.type + stock.location + + + + + + + + + + + + + + + + + stock.location.search.tray.type + stock.location + + + + + + + + + diff --git a/stock_location_tray/views/stock_move_line_views.xml b/stock_location_tray/views/stock_move_line_views.xml new file mode 100644 index 000000000000..fef8f0c93b49 --- /dev/null +++ b/stock_location_tray/views/stock_move_line_views.xml @@ -0,0 +1,56 @@ + + + + stock.move.line.operations.tree.tray.type + stock.move.line + + + + +