Merge PR #1079 into 14.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2021-09-14 10:37:18 +00:00
32 changed files with 2464 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../../../stock_location_tray

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@@ -0,0 +1,126 @@
==============
Location Trays
==============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/14.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-14-0/stock-logistics-warehouse-14-0-stock_location_tray
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/153/14.0
:alt: Try me on Runbot
|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
.. figure:: https://raw.githubusercontent.com/OCA/stock-logistics-warehouse/14.0/stock_location_tray/static/description/location-tray.png
:alt: Location Tray
:width: 600 px
**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 <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_location_tray%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Phuc Tran Thanh <phuc@trobz.com>
Other credits
~~~~~~~~~~~~~
The development 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 <https://github.com/OCA/stock-logistics-warehouse/tree/14.0/stock_location_tray>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,21 @@
# 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": "14.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": [
"views/stock_location_views.xml",
"views/stock_location_tray_type_views.xml",
"views/stock_location_tray_templates.xml",
"views/stock_move_line_views.xml",
"security/ir.model.access.csv",
],
"installable": True,
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="stock_location_tray_demo" model="stock.location">
<field name="name">Tray</field>
<field name="barcode">TRAY</field>
<field name="location_id" ref="stock.stock_location_stock" />
<field name="tray_type_id" ref="stock_location_tray_type_small_8x" />
<field name="usage">internal</field>
</record>
<!-- When the trays are created, they will create their 'cell' locations.
This method will add xmlids on them to be able to reference them in
other demo data and tests.
-->
<function model="stock.location" name="_create_tray_xmlids">
<function
eval="[[('cell_in_tray_type_id', '!=', False)]]"
model="stock.location"
name="search"
/>
<value>stock_location_tray</value>
</function>
</odoo>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record model="stock.location.tray.type" id="stock_location_tray_type_small_32x">
<field name="name">Small 32x</field>
<field name="code">B10804</field>
<field name="rows">4</field>
<field name="cols">8</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_small_16x">
<field name="name">Small 16x</field>
<field name="code">B20802</field>
<field name="rows">2</field>
<field name="cols">8</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_small_8x">
<field name="name">Small 8x</field>
<field name="code">B20402</field>
<field name="rows">2</field>
<field name="cols">4</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_small_16x_2">
<field name="name">Small 16x</field>
<field name="code">B40802</field>
<field name="rows">2</field>
<field name="cols">8</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_small_16x_3">
<field name="name">Small 16x</field>
<field name="code">B30404</field>
<field name="rows">4</field>
<field name="cols">4</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_large_32x">
<field name="name">Large 32x</field>
<field name="code">B20804</field>
<field name="rows">4</field>
<field name="cols">8</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_large_16x">
<field name="name">Large 16x</field>
<field name="code">B30802</field>
<field name="rows">2</field>
<field name="cols">8</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_large_8x">
<field name="name">Large 8x</field>
<field name="code">B30402</field>
<field name="rows">2</field>
<field name="cols">4</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_large_4x">
<field name="name">Large 4x</field>
<field name="code">B30401</field>
<field name="rows">1</field>
<field name="cols">4</field>
</record>
<record model="stock.location.tray.type" id="stock_location_tray_type_large_16x_2">
<field name="name">Large 16x</field>
<field name="code">B30404</field>
<field name="rows">4</field>
<field name="cols">4</field>
</record>
</odoo>

View File

@@ -0,0 +1,319 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_location_tray
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.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_tray_type__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_tray_type__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_tray_type____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
#: model_terms:ir.ui.view,arch_db:stock_location_tray.view_stock_location_tray_type_tree
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
#: code:addons/stock_location_tray/models/stock_location.py:0
#, python-format
msgid "Tray locations cannot be archived when they contain products."
msgstr ""
#. module: stock_location_tray
#: code:addons/stock_location_tray/models/stock_location.py:0
#, python-format
msgid "Trays cannot be modified when they contain products."
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 ""

View File

@@ -0,0 +1,3 @@
from . import stock_location
from . import stock_location_tray_type
from . import stock_move_line

View File

@@ -0,0 +1,246 @@
# 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", readonly=True
)
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.ref("stock.action_location_form").read()[0]
action.update(
{
"res_id": location.id,
"view_mode": "form",
"view_type": "form",
"view_id": view.id,
"views": [(view.id, "form")],
}
)
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(
_("Location %s has sub-locations, it cannot be converted to a tray.")
% (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(StockLocation, location).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 means 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,
}
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 = []
def has_ref(xmlid):
ModelData = self.env["ir.model.data"]
__, 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
tray_external = self.env["ir.model.data"].browse(
self.env["ir.model.data"]._get_id(module, tray_name)
)
cell_external_id = "{}_x{}y{}".format(
tray_name, location.posx, location.posy
)
cell_xmlid = "{}.{}".format(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,
}
)
self.env["ir.model.data"].create(xmlids_to_create)

View File

@@ -0,0 +1,113 @@
# 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.osv import expression
from odoo.addons.base_sparse_field.models.fields import Serialized
class StockLocationTrayType(models.Model):
_name = "stock.location.tray.type"
_description = "Stock Location Tray Type"
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}
@api.model
def _name_search(
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
):
args = args or []
domain = []
if name:
domain = ["|", ("name", operator, name), ("code", operator, name)]
return self._search(
expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid
)
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:
if record.active:
continue
if record.location_ids:
location_bullets = [
" - {}".format(location.display_name)
for location in record.location_ids
]
raise exceptions.ValidationError(
_(
"The tray type {} is used by the following locations "
"and cannot be archived:\n\n{}"
).format(record.name, "\n".join(location_bullets))
)
@api.constrains("rows", "cols")
def _location_check_rows_cols(self):
for record in self:
if record.location_ids:
location_bullets = [
" - {}".format(location.display_name)
for location in record.location_ids
]
raise exceptions.ValidationError(
_(
"The tray type {} is used by the following locations, "
"it's size cannot be changed:\n\n{}"
).format(record.name, "\n".join(location_bullets))
)
def open_locations(self):
action = self.env.ref("stock.action_location_form").read()[0]
action["domain"] = [("tray_type_id", "in", self.ids)]
if len(self.ids) == 1:
action["context"] = {"default_tray_type_id": self.id}
return action

View File

@@ -0,0 +1,53 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, 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"
)
@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
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 = _("Source Tray")
context["show_source_tray"] = True
else:
name = _("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")

View File

@@ -0,0 +1,25 @@
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.

View File

@@ -0,0 +1,2 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
* Phuc Tran Thanh <phuc@trobz.com>

View File

@@ -0,0 +1,3 @@
The development of this module has been financially supported by:
* Camptocamp

View File

@@ -0,0 +1,8 @@
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
.. figure:: ../static/description/location-tray.png
:alt: Location Tray
:width: 600 px

View File

@@ -0,0 +1,5 @@
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.

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,476 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Location Trays</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="location-trays">
<h1 class="title">Location Trays</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/14.0/stock_location_tray"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-14-0/stock-logistics-warehouse-14-0-stock_location_tray"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/153/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>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</p>
<div class="figure">
<img alt="Location Tray" src="https://raw.githubusercontent.com/OCA/stock-logistics-warehouse/14.0/stock_location_tray/static/description/location-tray.png" style="width: 600px;" />
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a><ul>
<li><a class="reference internal" href="#general" id="id2">General</a></li>
<li><a class="reference internal" href="#tray-types" id="id3">Tray types</a></li>
<li><a class="reference internal" href="#locations" id="id4">Locations</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id5">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id9">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="id10">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="id11">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<div class="section" id="general">
<h2><a class="toc-backref" href="#id2">General</a></h2>
<p>In Inventory Settings, you must have:</p>
<blockquote>
<ul class="simple">
<li>Storage Locations</li>
</ul>
</blockquote>
</div>
<div class="section" id="tray-types">
<h2><a class="toc-backref" href="#id3">Tray types</a></h2>
<p>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.</p>
</div>
<div class="section" id="locations">
<h2><a class="toc-backref" href="#id4">Locations</a></h2>
<p>The tray type can be configured in Stock Locations.</p>
<p>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.</p>
<p>The matrix widget on Tray locations can be clicked to reach a sub-location.
Blue squares represent the locations that contain goods.</p>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id5">Known issues / Roadmap</a></h1>
<p>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.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_location_tray%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id8">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id9">Contributors</a></h2>
<ul class="simple">
<li>Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
<li>Phuc Tran Thanh &lt;<a class="reference external" href="mailto:phuc&#64;trobz.com">phuc&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#id10">Other credits</a></h2>
<p>The development of this module has been financially supported by:</p>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id11">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/14.0/stock_location_tray">OCA/stock-logistics-warehouse</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,269 @@
odoo.define("stock_location_tray.tray", function (require) {
"use strict";
var basicFields = require("web.basic_fields");
var field_registry = require("web.field_registry");
var DebouncedField = basicFields.DebouncedField;
/**
* 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:
*
* <field name="tray_matrix"
* widget="location_tray_matrix"
* options="{'click_action': 'action_tray_matrix_click'}"
* />
*
*/
var LocationTrayMatrixField = DebouncedField.extend({
className: "o_field_location_tray_matrix",
tagName: "canvas",
supportedFieldTypes: ["serialized"],
events: {
click: "_onClick",
},
cellColorEmpty: "#ffffff",
cellColorNotEmpty: "#4e6bfd",
selectedColor: "#08f46b",
selectedLineWidth: 5,
globalAlpha: 0.8,
cellPadding: 2,
init: function (parent, name, record, options) {
this._super.apply(this, arguments);
this.nodeOptions = _.defaults(this.nodeOptions, {});
if ("clickAction" in (options || {})) {
this.clickAction = options.clickAction;
} else {
this.clickAction = this.nodeOptions.click_action;
}
},
isSet: function () {
if (Object.keys(this.value).length === 0) {
return false;
}
if (this.value.cells.length === 0) {
return false;
}
return this._super.apply(this, arguments);
},
start: function () {
// Setup resize events to redraw the canvas
this._resizeDebounce = this._resizeDebounce.bind(this);
this._resizePromise = null;
$(window).on("resize", this._resizeDebounce);
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.clickAction) {
self.$el.css("cursor", "pointer");
}
// _super calls _render(), but the function
// resizeCanvasToDisplaySize would resize the canvas
// to 0 because the actual canvas would still be unknown.
// Call again _render() here but through a setTimeout to
// let the js renderer thread catch up.
self._ready = true;
return self._resizeDebounce();
});
},
_onClick: function (ev) {
if (!this.isSet()) {
return;
}
if (!this.clickAction) {
return;
}
var width = this.canvas.width,
height = this.canvas.height,
rect = this.canvas.getBoundingClientRect();
var clickX = ev.clientX - rect.left,
clickY = ev.clientY - rect.top;
var cells = this.value.cells,
cols = cells[0].length,
rows = cells.length;
// We remove 1 to start counting from 0
var coordX = Math.ceil((clickX * cols) / width) - 1,
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
if (coordX >= cols) {
coordX = cols - 1;
}
if (coordY >= rows) {
coordY = 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);
var self = this;
this._rpc({
model: this.model,
method: this.clickAction,
args: [[this.res_id], coordX, coordY],
}).then(function (action) {
self.trigger_up("do_action", {action: action});
});
},
/**
* Debounce the rendering on resize.
* It is useless to render on each resize event.
*
*/
_resizeDebounce: function () {
clearTimeout(this._resizePromise);
var self = this;
this._resizePromise = setTimeout(function () {
self._render();
}, 20);
},
destroy: function () {
$(window).off("resize", this._resizeDebounce);
this._super.apply(this, arguments);
},
/**
* Render the widget only when it is in the DOM.
* We need the width and height of the widget to draw the canvas.
*
* @returns {Promise}
*/
_render: function () {
if (this._ready) {
return this._renderInDOM();
}
return $.when();
},
/**
* 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 {jQueryElement} canvas - the DOM canvas to draw
* @returns {Boolean}
*/
resizeCanvasToDisplaySize: function (canvas) {
// Look up the size the canvas is being displayed
var width = canvas.clientWidth;
var height = canvas.clientHeight;
// If it's resolution does not match change it
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
return true;
}
return false;
},
/**
* Resize the canvas, clear it and redraw the cells
* Should be called only if the canvas is already in DOM
*
*/
_renderInDOM: function () {
this.canvas = this.$el[0];
var canvas = this.canvas;
var ctx = canvas.getContext("2d");
this.resizeCanvasToDisplaySize(ctx.canvas);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
if (this.isSet()) {
var selected = this.value.selected || [];
var cells = this.value.cells;
this._drawMatrix(canvas, ctx, cells, selected);
}
},
/**
* Draw the cells in the canvas.
*
* @param {jQueryElement} canvas - the DOM canvas to draw
* @param {Object} ctx - the canvas 2d context
* @param {List} cells - A 2-dimensional list of cells
* @param {List} selected - A list containing the position (x,y) of the
* selected cell (can be empty if no cell is selected)
*/
_drawMatrix: function (canvas, ctx, cells, selected) {
var colors = {
0: this.cellColorEmpty,
1: this.cellColorNotEmpty,
};
var cols = cells[0].length;
var rows = cells.length;
var selectedX = null,
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);
}
var padding = this.cellPadding;
var padding_width = padding * cols;
var padding_height = padding * rows;
var w = (canvas.width - padding_width) / cols;
var 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
var reversed_cells = cells.slice().reverse();
for (var y = 0; y < rows; y++) {
for (var x = 0; x < cols; x++) {
ctx.fillStyle = colors[reversed_cells[y][x]];
var fillWidth = w;
var 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();
},
});
field_registry.add("location_tray_matrix", LocationTrayMatrixField);
return {
LocationTrayMatrixField: LocationTrayMatrixField,
};
});

View File

@@ -0,0 +1,4 @@
.o_field_location_tray_matrix {
background-color: #eeeeee;
border: 2px #000000 solid;
}

View File

@@ -0,0 +1,3 @@
from . import test_location
from . import test_tray_type
from . import test_stock_move_line

View File

@@ -0,0 +1,44 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests import common
class LocationTrayTypeCase(common.SavepointCase):
@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, "Cell x{}y{} not found for {}".format(x, y, tray.name)
)
return cell
def _update_quantity_in_cell(self, cell, product, quantity):
self.env["stock.quant"]._update_available_quantity(product, cell, quantity)

View File

@@ -0,0 +1,237 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import exceptions
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(
exceptions.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 = "You still have some product in locations"
while location:
with self.assertRaisesRegex(exceptions.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
message = "You still have some product in locations"
with self.assertRaisesRegex(exceptions.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)

View File

@@ -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,
"qty_done": 1,
"product_uom_id": move.product_uom.id,
"location_id": move.location_id.id,
"location_dest_id": move.location_dest_id.id,
}
)
self.move = move
def test_compute_tray_matrix(self):
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.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)

View File

@@ -0,0 +1,91 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import exceptions
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("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 = "cannot be archived.\n\n.*{}*".format(location.name)
# we cannot archive used ones
with self.assertRaisesRegex(exceptions.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 = "size cannot be changed.\n\n.*{}*".format(location.name)
# we cannot modify size of used ones
with self.assertRaisesRegex(exceptions.ValidationError, message):
self.used_tray_type.rows = 10
with self.assertRaisesRegex(exceptions.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)

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template
id="stock_location_tray_assets"
name="stock.location.tray.assets"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link
rel="stylesheet"
type="text/scss"
href="/stock_location_tray/static/src/scss/stock_location_tray.scss"
/>
<script
type="text/javascript"
src="/stock_location_tray/static/src/js/stock_location_tray.js"
/>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_location_tray_type_form" model="ir.ui.view">
<field name="name">stock.location.tray.type.form</field>
<field name="model">stock.location.tray.type</field>
<field name="arch" type="xml">
<form string="Location Tray Type">
<div class="oe_button_box" name="button_box">
<button
string="Locations"
class="oe_stat_button"
icon="fa-filter"
name="open_locations"
type="object"
/>
</div>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
<group>
<group string="Tray Configuration" name="settings">
<field name="active" invisible="1" />
<field name="code" />
<field name="rows" />
<field name="cols" />
</group>
<group string="Sizes" name="size">
<label for="width" />
<div>
<field name="width" /> mm /
<field name="width_per_cell" /> mm per cell
</div>
<label for="depth" />
<div>
<field name="depth" /> mm /
<field name="depth_per_cell" /> mm per cell
</div>
<field name="height" />
</group>
<group string="Disposition" name="disposition">
<field
name="tray_matrix"
widget="location_tray_matrix"
nolabel="1"
/>
</group>
</group>
</form>
</field>
</record>
<record id="view_stock_location_tray_type_search" model="ir.ui.view">
<field name="name">stock.location.tray.type.search</field>
<field name="model">stock.location.tray.type</field>
<field name="arch" type="xml">
<search string="Location Tray Type">
<field name="name" />
<field name="code" />
<separator />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
</search>
</field>
</record>
<record id="view_stock_location_tray_type_tree" model="ir.ui.view">
<field name="name">stock.location.tray.type</field>
<field name="model">stock.location.tray.type</field>
<field name="arch" type="xml">
<tree string="Location Tray Type">
<field name="name" />
<field name="code" />
<field name="rows" />
<field name="cols" />
</tree>
</field>
</record>
<record id="action_stock_location_tray_type" model="ir.actions.act_window">
<field name="name">Location Tray Types</field>
<field name="res_model">stock.location.tray.type</field>
<field name="type">ir.actions.act_window</field>
<field name="view_id" ref="view_stock_location_tray_type_tree" />
<field name="search_view_id" ref="view_stock_location_tray_type_search" />
<field name="context" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a Location Tray Type
</p>
<p>
Define the number of rows and cols on a tray, depending of the boxes
size.
</p>
</field>
</record>
<menuitem
action="action_stock_location_tray_type"
id="menu_stock_location_tray_type"
parent="stock.menu_warehouse_config"
sequence="10"
/>
</odoo>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_location_form" model="ir.ui.view">
<field name="name">stock.location.form.tray.type</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form" />
<field name="arch" type="xml">
<group name="additional_info" position="after">
<group string="Tray" name="tray">
<field
name="tray_type_id"
attrs="{'invisible': [('cell_in_tray_type_id', '!=', False)]}"
/>
<field
name="cell_name_format"
attrs="{'invisible': [('tray_type_id', '=', False)]}"
/>
<field
name="cell_in_tray_type_id"
attrs="{'invisible': [('cell_in_tray_type_id', '=', False)]}"
/>
<field
name="tray_matrix"
widget="location_tray_matrix"
attrs="{'invisible': [('tray_type_id', '=', False), ('cell_in_tray_type_id', '=', False)]}"
options="{'click_action': 'action_tray_matrix_click'}"
/>
<field
name="posx"
attrs="{'readonly': [('cell_in_tray_type_id', '!=', False)], 'invisible': ['|', ('cell_in_tray_type_id', '=', False), ('tray_type_id', '=', True)]}"
/>
<field
name="posy"
attrs="{'readonly': [('cell_in_tray_type_id', '!=', False)], 'invisible': ['|', ('cell_in_tray_type_id', '=', False), ('tray_type_id', '=', True)]}"
/>
<field
name="posz"
attrs="{'readonly': [('cell_in_tray_type_id', '!=', False)], 'invisible': ['|', ('cell_in_tray_type_id', '=', False), ('tray_type_id', '=', True)]}"
/>
</group>
</group>
</field>
</record>
<record id="view_location_search" model="ir.ui.view">
<field name="name">stock.location.search.tray.type</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_search" />
<field name="arch" type="xml">
<field name="location_id" position="after">
<separator />
<field name="tray_type_id" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_move_line_operation_tree" model="ir.ui.view">
<field name="name">stock.move.line.operations.tree.tray.type</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree" />
<field name="arch" type="xml">
<field name="location_id" position="after">
<field name="tray_source_matrix" invisible="1" />
<button
name="action_show_source_tray"
string="Show Source Tray"
type="object"
icon="fa-th"
attrs="{'invisible': [('tray_source_matrix', '=', {})]}"
invisible="not context.get('show_source_location')"
groups="stock.group_stock_multi_locations"
/>
</field>
<field name="location_dest_id" position="after">
<field name="tray_dest_matrix" invisible="1" />
<button
name="action_show_dest_tray"
string="Show Destination Tray"
type="object"
icon="fa-th"
attrs="{'invisible': [('tray_dest_matrix', '=', {})]}"
invisible="not context.get('show_destination_location')"
groups="stock.group_stock_multi_locations"
/>
</field>
</field>
</record>
<record id="view_stock_move_line_tray" model="ir.ui.view">
<field name="name">stock.move.line.tray</field>
<field name="model">stock.move.line</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form string="Tray" create="0" edit="0" delete="0">
<group
name="source_tray"
invisible="not context.get('show_source_tray')"
>
<field name="location_id" readonly="1" />
<field name="tray_source_matrix" widget="location_tray_matrix" />
</group>
<group
name="destination_tray"
invisible="not context.get('show_dest_tray')"
>
<field name="location_dest_id" readonly="1" />
<field name="tray_dest_matrix" widget="location_tray_matrix" />
</group>
<footer>
<button string="Close" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>