Add stock_location_tray

Extracted from stock_vertical_lift
(https://github.com/OCA/stock-logistics-warehouse/pull/633)

Add tray types to stock locations, automatically generates
sub-locations. Present them nicely with a custom widget.
This commit is contained in:
Guewen Baconnier
2019-09-19 11:34:24 +02:00
committed by Hai Lang
parent d7250dacd1
commit 4ad48463c2
23 changed files with 1757 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
==============
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/12.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-12-0/stock-logistics-warehouse-12-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/12.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/12.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.
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:%2012.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>
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/12.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,26 @@
# 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': '12.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',
'security/ir.model.access.csv',
],
'installable': True,
}

View File

@@ -0,0 +1,21 @@
<?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,74 @@
<?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,2 @@
from . import stock_location
from . import stock_location_tray_type

View File

@@ -0,0 +1,259 @@
# 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/2/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:
# we skip the others only for performance
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):
continue
location.tray_matrix = {
"selected": location._tray_cell_coords(),
"cells": location._tray_cell_matrix(),
}
@api.multi
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)
)
@api.multi
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
@api.constrains("active")
def _tray_check_active(self):
for record in self:
if record.active:
continue
# We cannot disable any cell of a tray (entire tray)
# if at least one of the cell contains stock.
# We cannot disable a tray, a shuffle or a view if
# at least one of their tray contain stock.
if record.cell_in_tray_type_id:
parent = record.location_id
else:
parent = record
# Add the record to the search: as it has been set inactive, it
# will not be found by the search.
locs = self.search([("id", "child_of", parent.id)]) | record
if any(
(loc.tray_type_id or loc.cell_in_tray_type_id)
and loc.tray_cell_contains_stock
for loc in locs
):
raise exceptions.ValidationError(
_(
"Tray locations cannot be archived when "
"they contain products."
)
)
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))
@api.multi
def _update_tray_sublocations(self):
values = []
for location in self:
tray_type = location.tray_type_id
try:
location.child_ids.write({"active": False})
except exceptions.ValidationError:
# trap this check (_tray_check_active) to display a
# contextual error message
raise exceptions.UserError(
_(
"Trays cannot be modified when "
"they contain products."
)
)
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)
@api.multi
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.
"""
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
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 self.env.ref(cell_xmlid, raise_if_not_found=False):
self.env["ir.model.data"].create(
{
"name": cell_external_id,
"module": module,
"model": self._name,
"res_id": location.id,
"noupdate": tray_external.noupdate,
}
)

View File

@@ -0,0 +1,89 @@
# 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)
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("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)]
tray_ids = self._search(
expression.AND([domain, args]),
limit=limit,
access_rights_uid=name_get_uid,
)
return self.browse(tray_ids).name_get()
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))
)
@api.multi
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,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 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>

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,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

View File

@@ -0,0 +1,458 @@
<?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 0.14: 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/12.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-12-0/stock-logistics-warehouse-12-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/12.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/12.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="#bug-tracker" id="id5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id9">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="bug-tracker">
<h1><a class="toc-backref" href="#id5">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:%2012.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="#id6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id8">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>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id9">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/12.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,249 @@
odoo.define('stock_location_tray.tray', function (require) {
"use strict";
var core = require('web.core');
var KanbanRecord = require('web.KanbanRecord');
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, {});
this.clickAction = 'clickAction' in (options || {}) ? options.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.
*
*/
_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.
*
*/
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.find('canvas').context;
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.
*
*/
_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, selectedY;
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 w = ((canvas.width - padding * cols) / cols);
var h = ((canvas.height - padding * rows) / 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

View File

@@ -0,0 +1,39 @@
# 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.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 _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,170 @@
# 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.env["stock.location"].create(
{
"name": "Tray Z",
"location_id": self.stock_location.id,
"usage": "internal",
"tray_type_id": tray_type.id,
}
)
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
},
)
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_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 = "cannot be archived"
while location:
with self.assertRaisesRegex(exceptions.ValidationError, message):
location.active = False
# restore state for the next test loop
location.active = True
location = location.location_id
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 = "cannot be modified when they contain products"
with self.assertRaisesRegex(exceptions.UserError, message):
self.tray_location.tray_type_id = tray_type

View File

@@ -0,0 +1,72 @@
# 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': '🐵',
'usage': 'internal',
'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
},
)
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.*{}.*'.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
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.*{}.*'.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

View File

@@ -0,0 +1,11 @@
<?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"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,85 @@
<?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"
/>
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
</button>
</div>
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<group>
<group string="Tray Configuration" name="settings">
<field name="code"/>
<field name="rows"/>
<field name="cols"/>
</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_type">form</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>
<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,48 @@
<?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="localization" 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'}"
/>
</group>
</group>
<field name="posx" position="attributes">
<attribute name="attrs">{'readonly': [('cell_in_tray_type_id', '!=', False)]}</attribute>
</field>
<field name="posy" position="attributes">
<attribute name="attrs">{'readonly': [('cell_in_tray_type_id', '!=', False)]}</attribute>
</field>
<field name="posz" position="attributes">
<attribute name="attrs">{'readonly': [('cell_in_tray_type_id', '!=', False)]}</attribute>
</field>
</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>