Files
stock-logistics-warehouse/stock_location_tray/models/stock_location.py
2021-08-17 16:15:20 +07:00

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)