mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
282 lines
11 KiB
Python
282 lines
11 KiB
Python
# 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
|
|
|
|
@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_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
|
|
|
|
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)
|
|
|
|
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
|
|
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)
|