Merge PR #2102 into 16.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2024-07-12 15:41:30 +00:00
35 changed files with 2729 additions and 0 deletions

View File

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

View File

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

110
stock_vlm_mgmt/README.rst Normal file
View File

@@ -0,0 +1,110 @@
===============================
Vertical Lift Module management
===============================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:16cc58033099be982092f3cf06236fc4a8ab693fcfa4ff09ff5555062362ce43
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/16.0/stock_vlm_mgmt
: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-16-0/stock-logistics-warehouse-16-0-stock_vlm_mgmt
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-warehouse&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds basic a management system for Vertical Lift Modules. It's thought as
a simpler alternative attemp to stock_vertical_lift and all the dependencies that
come with it.
**Table of contents**
.. contents::
:local:
Known issues / Roadmap
======================
* Launch the tasks in batches so we don't have to send them to the VLM one by one. In
the case of Kardex, we'll be dealing with the connection limitations. If we send a
list of tasks, right now we're closing the connection once we receive a response (Kardex).
We need to keep listening until all the ids are received, but that locks our thread...
We also need to respond to operation issues on every task, like full trays, changes
of quantity, etc... and we can't lock many threads as we could put down the instance.
Something along the lines of queue_job maybe would need to push to the bus the tasks
updates (hard?) Or maybe the kardex proxy from c2c where we use a controller to send
the tasks? This should rely on js, if we want a proper UX.
* Confirming the VLM tasks after the picking is confirmed makes not much sense, but
we're dealing with the quants limitations. Anyway we shouldn't allow to leave
operations on halt and after a real VLM task is done, the picking should be validated.
What to do with the non existing quants (inputs)... maybe we could leave the vlm task
pending assignation, so when we finally validate the picking we just have to perform
the proper links.
* Not a requiste right now, but we could need to support batch pickings. Let's deal
with the basics for now anyway.
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 to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vlm_mgmt%0Aversion:%2016.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
~~~~~~~
* Tecnativa
Contributors
~~~~~~~~~~~~
* `Tecnativa <https://www.tecnativa.com>`_:
* David Vidal
* Pedro M. Baeza
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.
.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px
:target: https://github.com/chienandalu
:alt: chienandalu
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-chienandalu|
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_vlm_mgmt>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -0,0 +1,30 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Vertical Lift Module management",
"summary": "Light self contained alternative for VLM integrations",
"version": "16.0.1.0.0",
"author": "Tecnativa, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"maintainers": ["chienandalu"],
"license": "AGPL-3",
"category": "Stock",
"depends": ["stock", "base_sparse_field"],
"data": [
"security/ir.model.access.csv",
"views/stock_location_vlm_tray_views.xml",
"views/stock_location_views.xml",
"views/stock_picking_views.xml",
"views/stock_quant_views.xml",
"views/stock_vlm_task_views.xml",
"views/stock_quant_vlm_views.xml",
"views/stock_location_tray_type_views.xml",
"wizards/stock_vlm_task_action_views.xml",
],
"assets": {
"web.assets_backend": [
"stock_vlm_mgmt/static/src/scss/stock_vlm_mgmt.scss",
"stock_vlm_mgmt/static/src/js/**/*",
],
},
}

View File

@@ -0,0 +1,8 @@
from . import vlm_tray_cell_position_mixin
from . import stock_location
from . import stock_location_vlm_tray
from . import stock_location_vlm_tray_type
from . import stock_move_line
from . import stock_picking
from . import stock_quant
from . import stock_vlm_task

View File

@@ -0,0 +1,144 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, fields, models
from odoo.exceptions import UserError
class StockLocation(models.Model):
_inherit = "stock.location"
is_vlm = fields.Boolean()
vlm_vendor = fields.Selection(
selection=[
("test", "Test"),
],
)
vlm_address = fields.Char(
help=(
"An VLM normally will be behind some propietary proxy that handles several "
"VLMs at once, so we need to set which one corresponds to this location"
)
)
vlm_hostname = fields.Char()
vlm_port = fields.Char()
vlm_removal_strategy = fields.Selection(
selection=[
("fifo", "FIFO"),
("lifo", "LIFO"),
("optimal", "Less carrier movements"),
],
default="fifo",
)
vlm_tray_ids = fields.One2many(
comodel_name="stock.location.vlm.tray", inverse_name="location_id"
)
vlm_sequence_id = fields.Many2one(comodel_name="ir.sequence")
vlm_user = fields.Char()
vlm_password = fields.Char()
def _prepare_vlm_request(self, **kw) -> dict:
return {
"task_type": kw.get("task_type", "release"),
"task_id": self.vlm_sequence_id.next_by_id(),
"address": self.vlm_address,
"carrier": kw.get("carrier", "0"),
"pos_x": kw.get("pos_x", "0"),
"pos_y": kw.get("pos_y", "0"),
"qty": str(kw.get("qty", "0")),
"info1": kw.get("info1", "") or "",
"info2": kw.get("info2", "") or "",
"info3": kw.get("info3", "") or "",
"info4": kw.get("info4", "") or "",
}
def send_vlm_request(self, data: dict, **options) -> dict:
"""Send request to the vendor methods. The vendor connector should deal with
the connection issues transforming the response into these standard codes:
-1: Connection refused
-2: The request is issued but the response is lost
-3: Timeout in the request
-4: VLM Hardware issues
"""
self.ensure_one()
if not hasattr(self, "_%s_vlm_connector" % self.vlm_vendor):
raise UserError(_("No implemented request connector for this vendor!"))
timeout = (
self.env["ir.config_parameter"].sudo().get_param("stock_vlm_mgmt.timeout")
)
vlm_connector = getattr(self, "_%s_vlm_connector" % self.vlm_vendor)()(
self.vlm_hostname,
self.vlm_port,
timeout=timeout,
user=self.vlm_user,
password=self.vlm_password,
**options
)
response = vlm_connector.request_operation(data)
response_code = response.get("code", "")
# These negative codes are issued by the connector and they're common connection
# problems.
if response_code == "-1":
raise UserError(
_("The connection was refused by the VLM and couldn't be stablished.")
)
elif response_code == "-2":
raise UserError(
_(
"The command response has been lost for unknown reasons. Did you "
"perform the operation on the VLM? Make sure there aren't "
"unconsistencies with the recorded data"
)
)
elif response_code == "-3":
raise UserError(
_("The task couldn't be performed due to a timeout in the request"),
)
elif response_code == "-4":
raise UserError(
_(
"The task couldn't be performed. Try again or check the vertical "
"lift module for hardware issues"
)
)
elif response_code == "-5":
raise UserError(_("The task was cancelled by the VLM"))
return response
def action_release_vlm_trays(self):
"""Send to the VLM a special command that releases all the trays"""
data = self._prepare_vlm_request(
task_type="release",
info1=_(
"%(user)s has requested a release of the trays from Odoo",
user=self.env.user.name,
),
)
self.send_vlm_request(data)
def action_view_vlm_tray(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"stock_vlm_mgmt.location_vlm_tray_action"
)
action["domain"] = [("id", "in", self.vlm_tray_ids.ids)]
action["context"] = dict(self.env.context, default_location_id=self.id)
return action
def action_view_vlm_quants(self):
action = self.env["ir.actions.act_window"]._for_xml_id(
"stock_vlm_mgmt.location_quant_vlm_action"
)
action["domain"] = [("location_id", "=", self.id)]
action["context"] = dict(
self.env.context,
vlm_inventory_mode=True,
)
view_id = self.env.ref("stock_vlm_mgmt.view_stock_quant_inventory_tree").id
action.update(
{
"view_mode": "tree",
"views": [
[view_id, "tree"] for view in action["views"] if view[1] == "tree"
],
}
)
return action

View File

@@ -0,0 +1,89 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.osv import expression
class StockLocationVlmTray(models.Model):
_name = "stock.location.vlm.tray"
_description = "Individual trays in a Vertical Lift Module"
name = fields.Char()
location_id = fields.Many2one(
comodel_name="stock.location", domain=[("is_vlm", "=", True)]
)
tray_type_id = fields.Many2one(
comodel_name="stock.location.vlm.tray.type", required=True
)
tray_matrix = fields.Serialized(compute="_compute_tray_matrix")
is_full = fields.Boolean()
@api.depends("tray_type_id")
def _compute_tray_matrix(self):
"""Render empty and allocated cells"""
# TODO: Unify computes and optimize query to do it in one shot
for tray in self:
cell_not_empty = self.env["stock.quant.vlm"].search_read(
[("tray_id", "=", tray.id)], ["pos_x", "pos_y", "tray_id"]
)
tray_matrix = {
"selected": [],
"cells": tray.tray_type_id._generate_cells_matrix(),
"first_empty_cell": [],
}
for position in cell_not_empty:
# Let's be gentle with positioning errors.
try:
tray_matrix["cells"][position["pos_y"]][position["pos_x"]] = 1
# pylint: disable=except-pass
except IndexError:
pass
for row, cells in enumerate(tray_matrix["cells"]):
if 0 not in cells:
continue
tray_matrix["first_empty_cell"] = [cells.index(0), row]
break
tray.tray_matrix = tray_matrix
def action_tray_content(self, pos_x=None, pos_y=None):
"""See the vlm quants belonging to the tray"""
self.ensure_one()
domain = [("tray_id", "=", self.id)]
if (pos_x is not None) and (pos_y is not None):
domain = expression.AND(
[domain, [("pos_x", "=", pos_x), ("pos_y", "=", pos_y)]]
)
vlm_quant = self.env["stock.quant.vlm"].search(domain)
action = self.env["ir.actions.act_window"]._for_xml_id(
"stock_vlm_mgmt.location_quant_vlm_action"
)
self.env.ref("stock_vlm_mgmt.view_location_form")
action["domain"] = [("id", "in", vlm_quant.ids)]
action["context"] = dict(
self.env.context,
default_tray_id=self.id,
default_location_id=self.location_id.id,
vlm_inventory_mode=True,
)
view_id = self.env.ref("stock_vlm_mgmt.view_stock_quant_inventory_tree").id
action.update(
{
"view_mode": "tree",
"views": [
[view_id, "tree"] for view in action["views"] if view[1] == "tree"
],
}
)
return action
def action_tray_call(self):
"""Send to the VLM a special command that calls this tray"""
data = self.location_id._prepare_vlm_request(
task_type="count",
carrier=self.name,
info1=_(
"%(user)s has requested a release of the trays from Odoo",
user=self.env.user.name,
),
)
self.location_id.with_context(vlm_tray_call=True).send_vlm_request(data)

View File

@@ -0,0 +1,53 @@
# Copyright 2019 Camptocamp SA
# Copyright 2024 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class StockLocationVlmTrayType(models.Model):
_name = "stock.location.vlm.tray.type"
_description = "VLM Tray configuration"
_rec_names_search = ["name", "code"]
active = fields.Boolean(default=True)
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")
tray_matrix = fields.Serialized(compute="_compute_tray_matrix")
@api.depends("width", "cols")
def _compute_width_per_cell(self):
for record in self:
width = record.width
if not width:
record.width_per_cell = 0.0
continue
record.width_per_cell = width / record.cols
@api.depends("depth", "rows")
def _compute_depth_per_cell(self):
for record in self:
depth = record.depth
if not depth:
record.depth_per_cell = 0.0
continue
record.depth_per_cell = depth / record.rows
@api.depends("rows", "cols")
def _compute_tray_matrix(self):
for record in self:
# As we only want to show the disposition of
# the tray, we generate a "full" tray, we'll
# see all the boxes on the web widget.
# (0 means empty, 1 means used)
cells = self._generate_cells_matrix(default_state=1)
record.tray_matrix = {"selected": [], "cells": cells}
def _generate_cells_matrix(self, default_state=0):
return [[default_state] * self.cols for __ in range(self.rows)]

View File

@@ -0,0 +1,152 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from collections import defaultdict
from odoo import api, fields, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
has_vlm_operation = fields.Boolean(compute="_compute_has_vlm_operation")
vlm_pending_quantity = fields.Float(
help="Quantity pending on VLM Operations",
default=0.0,
digits="Product Unit of Measure",
copy=False,
store=True,
readonly=False,
compute="_compute_vlm_pending_quantity",
)
@api.depends("location_id", "location_dest_id")
def _compute_has_vlm_operation(self):
self.has_vlm_operation = False
if self.env.context.get("skip_vlm_task"):
return
self.filtered(
lambda x: x.location_id.is_vlm or x.location_dest_id.is_vlm
).has_vlm_operation = True
@api.depends("qty_done")
def _compute_vlm_pending_quantity(self):
"""Pair the reported quantities to the done ones"""
# TODO: Maybe get rid of these and rely on operation task once they're created
self.vlm_pending_quantity = 0
for line in self.filtered("has_vlm_operation"):
line.vlm_pending_quantity = line.qty_done
def _prepare_vlm_put_task(self, key):
"""Over the recordset of detailed operations return a list of tasks to
perform"""
product, location = key
quants = self.env["stock.quant"].search(
[
("product_id", "=", product.id),
("location_id", "=", location.id),
]
)
# Get free spots
# TODO: Sort them according to their strategy
existing_quants = quants.vlm_quant_ids.filtered(lambda x: not x.tray_id.is_full)
location_trays = location.vlm_tray_ids.filtered(lambda x: not x.is_full)
# This is our demand for this product we'll try to fulfill it in one shot.
# Anyway we could use other trays if one gets full
quantity_fulfilled = sum(self.mapped("qty_done"))
task_vlm_quant = fields.first(existing_quants)
trays = location_trays - existing_quants.tray_id
first_tray = task_vlm_quant.tray_id or fields.first(trays)
if task_vlm_quant:
pos_x = task_vlm_quant.pos_x
pos_y = task_vlm_quant.pos_y
else:
pos_x, pos_y = first_tray.tray_matrix.get("first_empty_cell")
return {
"quantity_pending": quantity_fulfilled,
"product_id": product.id,
"state": "pending",
"vlm_quant_id": task_vlm_quant.id,
"location_id": first_tray.location_id.id,
"reference": ",".join(self.mapped("reference")),
"tray_id": first_tray.id,
"pos_x": pos_x,
"pos_y": pos_y,
"move_line_ids": [(6, 0, self.ids)],
"task_type": "put",
}
def _prepare_vlm_get_tasks(self, key):
"""Over the recordset of detailed operations return a list of tasks to
perform"""
product, location = key
quants = self.env["stock.quant"].search(
[
("product_id", "=", product.id),
("location_id", "=", location.id),
]
)
# TODO: Sort them according to their strategy
# This is our demand for this product we'll try to fulfill it in one shot.
# Anyway we could use other trays if one gets full
pending_quantity = sum(self.mapped("qty_done"))
tasks = []
for vlm_quant in quants.vlm_quant_ids:
qty = min(pending_quantity, vlm_quant.quantity)
tasks.append(
{
"quantity_pending": qty,
"product_id": product.id,
"state": "pending",
"vlm_quant_id": vlm_quant.id,
"tray_id": vlm_quant.tray_id.id,
"location_id": vlm_quant.location_id.id,
"quant_id": vlm_quant.quant_id.id,
"pos_x": vlm_quant.pos_x,
"pos_y": vlm_quant.pos_y,
"move_line_ids": [(6, 0, self.ids)],
"task_type": "get",
"reference": ",".join(self.mapped("reference")),
}
)
pending_quantity -= qty
if pending_quantity <= 0:
break
return tasks
def _action_done(self):
"""Once we've completed our ml operations we need to complete them in the VLM.
For that, we'll prepare the needed operations for each move line. Each move line
could have several operations and one operation could correspond to several move
lines. Some simple example:
- A move line from vlm1 to vlm2 will split into two operations: one to get the
products from vlm1 and another to put the products into vlm2.
- If we've got several move lines for the same quant, we don't want to do an
operation for each one. We'll try to merge them into a single operation so we
minimize tray calls.
"""
res = super()._action_done()
if self.env.context.get("skip_vlm_task"):
return res
sml = self.exists()
# 1. Prepare put operations
put_in_vlm = sml.filtered(lambda x: x.qty_done and x.location_dest_id.is_vlm)
# Let's group lines by common features. For this first design we don't care
# about lot, owner, etc. Maybe in the feature this is relevant
put_dict = defaultdict(self.browse)
for line in put_in_vlm:
put_dict[(line.product_id, line.location_dest_id)] += line
put_task_values = []
for key, lines in put_dict.items():
put_task_values.append(lines._prepare_vlm_put_task(key))
self.env["stock.vlm.task"].create(put_task_values)
# 2. Prepare get operations
get_from_vlm = sml.filtered(lambda x: x.qty_done and x.location_id.is_vlm)
# TODO: Again we group lines by common features, merge into single method
get_dict = defaultdict(self.browse)
for line in get_from_vlm:
get_dict[(line.product_id, line.location_id)] += line
get_task_values = []
for key, lines in get_dict.items():
get_task_values += lines._prepare_vlm_get_tasks(key)
self.env["stock.vlm.task"].create(get_task_values)
return res

View File

@@ -0,0 +1,85 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class StockPicking(models.Model):
_inherit = "stock.picking"
vlm_move_line_ids = fields.Many2many(
comodel_name="stock.move.line", compute="_compute_vlm_move_line_ids"
)
vlm_pending_move_line_ids = fields.Many2many(
comodel_name="stock.move.line", compute="_compute_vlm_move_line_ids"
)
has_vlm_operations = fields.Boolean(compute="_compute_has_vlm_operations")
has_vlm_pending_operations = fields.Boolean(
compute="_compute_has_vlm_pending_operations"
)
vlm_task_ids = fields.Many2many(
comodel_name="stock.vlm.task",
compute="_compute_vlm_task_ids",
)
has_pending_vlm_tasks = fields.Boolean(compute="_compute_has_pending_vlm_tasks")
@api.depends("location_id", "location_dest_id")
def _compute_vlm_move_line_ids(self):
for pick in self:
pick.vlm_move_line_ids = pick.move_line_ids.filtered(
lambda x: x.location_id.is_vlm or x.location_dest_id.is_vlm
)
pick.vlm_pending_move_line_ids = pick.vlm_move_line_ids.filtered(
"vlm_pending_quantity"
)
@api.depends("vlm_move_line_ids")
def _compute_has_vlm_operations(self):
for pick in self:
pick.has_vlm_operations = bool(pick.vlm_move_line_ids)
@api.depends("vlm_pending_move_line_ids")
def _compute_has_vlm_pending_operations(self):
"""We need to complete the quantities in the VLM"""
self.has_vlm_pending_operations = False
for pick in self.filtered(lambda x: x.state == "done"):
pick.has_vlm_pending_operations = bool(pick.vlm_pending_move_line_ids)
@api.depends("state")
def _compute_vlm_task_ids(self):
self.vlm_task_ids = False
for pick in self.filtered(lambda x: x.state == "done"):
pick.vlm_task_ids = self.env["stock.vlm.task"].search(
[
("move_line_ids", "in", pick.move_line_ids.ids),
]
)
def _compute_has_pending_vlm_tasks(self):
"""Helper to show the task action button"""
self.has_pending_vlm_tasks = False
for pick in self:
pick.has_pending_vlm_tasks = bool(
pick.vlm_task_ids.filtered(lambda x: x.state != "done")
)
def action_do_vlm_tasks(self):
"""Perform the VLM pending tasks"""
return self.vlm_task_ids.filtered(lambda x: x.state != "done").action_do_tasks()
def action_open_vlm_task(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_vlm_mgmt.vlm_task_action"
)
# Let's pre-sort the task so we always first get and then put. i.e.: when we're
# moving goods from one VLM to another
get_tasks = self.vlm_task_ids.filtered(lambda x: x.task_type == "get").sorted(
lambda x: (x.location_id, x.tray_id, x.product_id)
)
put_tasks = self.vlm_task_ids.filtered(lambda x: x.task_type == "put").sorted(
lambda x: (x.location_id, x.tray_id, x.product_id)
)
vlm_tasks = get_tasks + put_tasks
action["name"] = f"SVM tasks for {self.name}"
action["domain"] = [("id", "in", vlm_tasks.ids)]
action["context"] = dict(self.env.context, search_default_pending=1)
return action

View File

@@ -0,0 +1,119 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools.float_utils import float_compare, float_round
class StockQuant(models.Model):
_inherit = "stock.quant"
vlm_quant_ids = fields.One2many(
comodel_name="stock.quant.vlm", inverse_name="quant_id"
)
def action_view_in_vlm_structure(self):
"""Open the VLM structure filtering by this product to locate it easily"""
action = self.location_id.action_view_vlm_quants()
action["domain"] = expression.AND(
[action["domain"], [("product_id", "=", self.product_id.id)]]
)
return action
class StockQuantVlm(models.Model):
_name = "stock.quant.vlm"
_inherit = ["vlm.tray.cell.position.mixin"]
_description = "Vertical Lift Module structure inside the quant"
_rec_name = "product_id"
quant_id = fields.Many2one(comodel_name="stock.quant")
location_id = fields.Many2one(comodel_name="stock.location")
product_id = fields.Many2one(comodel_name="product.product", required=True)
tray_id = fields.Many2one(comodel_name="stock.location.vlm.tray")
tray_type_id = fields.Many2one(related="tray_id.tray_type_id")
quantity = fields.Float()
@api.model
def _is_inventory_mode(self):
"""As in stock.quant inventory mode, we have an special context to trigger
the update of the linked quant. This way we can make partial stocks tray
by tray just setting the difference (positive or negative in the quant)"""
return self.env.context.get("vlm_inventory_mode") and self.user_has_groups(
"stock.group_stock_manager"
)
def _set_inventory_quantity(self, inventory_quantity, skip_diff=None):
"""Update the related quant quantity according to the part of the counted
quant or tray"""
if not self._is_inventory_mode():
return
for vlm_quant in self.filtered(lambda x: x.product_id.type == "product"):
rounding = vlm_quant.product_id.uom_id.rounding
# TODO: Support lots and other quant stuff
quant = vlm_quant.quant_id or vlm_quant.quant_id.search(
[
("product_id", "=", vlm_quant.product_id.id),
("location_id", "=", vlm_quant.location_id.id),
],
limit=1,
)
quant_quantity = quant.quantity
diff = float_round(
inventory_quantity - vlm_quant.quantity, precision_rounding=rounding
)
diff_float_compared = float_compare(diff, 0, precision_rounding=rounding)
# When we create a new record, the diff would be 0
if skip_diff:
diff = diff_float_compared = inventory_quantity
# Update the related quant or create a brand new one
if diff_float_compared == 0:
continue
quant_quantity += diff
# We want to skip the VLM tasks as we're setting the inventory on hand
quant = vlm_quant.quant_id.with_context(
inventory_mode=True, skip_vlm_task=True
)
if quant:
quant.inventory_quantity = quant_quantity
quant._apply_inventory()
else:
vlm_quant.quant_id = quant.create(
{
"product_id": self.product_id.id,
"location_id": self.location_id.id,
"inventory_quantity": quant_quantity,
}
)
vlm_quant.quant_id._apply_inventory()
def write(self, vals):
if self._is_inventory_mode() and "quantity" in vals:
self._set_inventory_quantity(vals["quantity"])
if "product_id" in vals:
raise UserError(_("You can't change the product"))
return super().write(vals)
@api.model
def create(self, vals):
vlm_quant = super().create(vals)
if self._is_inventory_mode():
vlm_quant._set_inventory_quantity(vals["quantity"], skip_diff=True)
return vlm_quant
def unlink(self):
# We need to trigger the quantities update whenever this happens
for vlm_quant in self:
vlm_quant.with_context(vlm_inventory_mode=True).quantity = 0
return super().unlink()
def action_task_detail(self):
return {
"view_mode": "form",
"res_model": "stock.quant.vlm",
"res_id": self.id,
"type": "ir.actions.act_window",
"context": self._context,
"target": "new",
}

View File

@@ -0,0 +1,192 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from concurrent.futures import ThreadPoolExecutor, as_completed
from odoo import api, fields, models
from odoo.tools import float_compare, float_is_zero
class VlmOperationTask(models.Model):
_name = "stock.vlm.task"
_inherit = ["vlm.tray.cell.position.mixin"]
_description = "Vertical Lift Module task"
display_name = fields.Char(compute="_compute_display_name")
vlm_quant_id = fields.Many2one(comodel_name="stock.quant.vlm")
quant_id = fields.Many2one(comodel_name="stock.quant")
move_line_ids = fields.Many2many(comodel_name="stock.move.line")
product_id = fields.Many2one(comodel_name="product.product")
location_id = fields.Many2one(comodel_name="stock.location")
tray_id = fields.Many2one(comodel_name="stock.location.vlm.tray")
tray_type_id = fields.Many2one(related="tray_id.tray_type_id")
splitted_vlm_task_id = fields.Many2one(comodel_name="stock.vlm.task")
quantity_pending = fields.Float(digits="Product Unit of Measure")
quantity_done = fields.Float(digits="Product Unit of Measure")
reference = fields.Char(default="")
state = fields.Selection(
selection=[
("pending", "Pending"),
("waiting", "Waiting"),
("done", "Done"),
],
)
task_type = fields.Selection(
selection=[
("put", "Put"),
("get", "Get"),
("count", "Count"),
],
)
skipped = fields.Boolean()
@api.depends("task_type", "quantity_pending", "reference", "location_id", "tray_id")
def _compute_display_name(self):
"""E.g.: WH/003: Put 80.0 Units of [RS200] Running socks in KARDEX ⇨ tray 31"""
for task in self:
qty = task.quantity_pending if task.state != "done" else task.quantity_done
task.display_name = (
f"{task.reference}: "
f"{task.task_type.capitalize()} {qty} "
f"{task.product_id.uom_id.name} of {task.product_id.display_name} "
f"{'in' if task.task_type == 'put' else 'from'} "
f"{task.location_id.name} ⇨ TRAY {task.tray_id.name}"
)
def action_command_task_threaded(self):
# TODO: Not working, fails on environment or cursor things. Not usable
with api.Environment.manage(), self.pool.cursor() as new_cr:
self = self.with_env(self.env(cr=new_cr))
with ThreadPoolExecutor() as executor:
futures = []
for task in self:
futures.append(executor.submit(task.action_command_task))
for future in as_completed(futures):
yield future.result()
def action_command_task(self):
"""Send to the VLM then needed operations"""
self.state = "waiting"
pos_x, pos_y = self.tray_cell_center_position()
response = self.location_id.send_vlm_request(
self.location_id._prepare_vlm_request(
task_type=self.task_type,
carrier=self.tray_id.name,
pos_x=pos_x,
pos_y=pos_y,
qty=self.quantity_pending,
info1=self.reference,
info2=self.product_id.display_name,
)
)
# The only relevant thing we get in the response is the quantity
return self._post_command(float(response.get("qty", "0")))
def _post_command(self, quantity_done):
"""What to do depending on the VLM response"""
quantity_compare = float_compare(
quantity_done,
self.quantity_pending,
precision_rounding=self.product_id.uom_id.rounding,
)
# There could be several cases:
# A) The done quantity is equal to the task quantity: task is done
if quantity_compare == 0:
self._set_done(quantity_done)
# Set the pending quantity of the mls to 0
self.move_line_ids.vlm_pending_quantity = 0
self._update_quantities()
return ("done", self)
# B) The done quantity is lower than the task quantity:
# - Was the tray full?: propose another task
# - TODO: The quantity couldn't be fulfilled: the picking was wrong
elif quantity_compare < 0:
# Return it as it is
if float_is_zero(quantity_done, self.product_id.uom_id.rounding):
return ("zero_quantity", self)
# Add an extra task for the remaining quantity and launch the wizard
# so the user decides where to put them
new_task = self._action_split(quantity_done)
self._update_quantities()
# Set the new task position and command it to the VLM
return ("split", new_task)
# C) Then done quantity is higher:
# - TODO: Was the picking was wrong?
elif quantity_compare > 0:
# Show the issue to the user
if not self.env.context.get("skip_vlm_mismatch"):
return (
"mismatch_greater",
self.with_context(vlm_task_action_warning="mismatch_greater"),
)
# TODO: Just confirm the qtys? Something else to do? It isnt't going
# to match with the quant!
self._set_done(quantity_done)
self._update_quantities()
def _set_done(self, quantity_done=None):
quantity_done = quantity_done or self.quantity_pending
self.quantity_pending = 0
self.state = "done"
self.quantity_done = quantity_done
def _action_skip(self):
"""The task isn't done (it can be done later) or be finished manually"""
self.skipped = True
self.state = "done"
def _action_split(self, quantity_done):
"""Divide a partially done task"""
new_task = self.copy(
{
"quantity_pending": self.quantity_pending - quantity_done,
"splitted_vlm_task_id": self.id,
"state": "pending",
}
)
self._set_done(quantity_done)
return new_task
def _update_quantities(self):
if self.vlm_quant_id:
if self.task_type == "put":
self.vlm_quant_id.quantity += self.quantity_done
elif self.task_type == "get":
self.vlm_quant_id.quantity -= self.quantity_done
elif self.task_type == "put":
# For lots we should refine the search from the linked move lines
quant = self.env["stock.quant"].search(
[
("location_id", "=", self.location_id.id),
("product_id", "=", self.product_id.id),
],
limit=1,
)
vlm_quant = self.env["stock.quant.vlm"].create(
{
"quant_id": quant.id,
"product_id": self.product_id.id,
"location_id": self.location_id.id,
"tray_id": self.tray_id.id,
"pos_x": self.pos_x,
"pos_y": self.pos_y,
"quantity": self.quantity_done,
}
)
self.quant_id = quant
self.vlm_quant_id = vlm_quant
def action_do_tasks(self):
"""Perform the tasks sequentially"""
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_vlm_mgmt.action_vlm_task_action"
)
tasks = self.filtered(lambda x: x.state != "done" or x.skipped)
if not tasks:
return
action["name"] = f"VLM task {1} of {len(self.ids)}"
action["context"] = dict(
self.env.context,
default_vlm_task_id=tasks.ids[0],
default_vlm_task_ids=tasks.ids,
)
return action

View File

@@ -0,0 +1,90 @@
# Copyright 2019 Camptocamp SA
# Copyright 2024 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class VlmTrayCellPositionMixin(models.AbstractModel):
_name = "vlm.tray.cell.position.mixin"
_description = "Tray position helpers"
tray_id = fields.Many2one(comodel_name="stock.location.vlm.tray")
tray_type_id = fields.Many2one(comodel_name="stock.location.vlm.tray.type")
tray_matrix = fields.Serialized(compute="_compute_tray_matrix")
pos_x = fields.Integer(compute="_compute_pos", readonly=False, store=True)
pos_y = fields.Integer(compute="_compute_pos", readonly=False, store=True)
human_pos_x = fields.Integer(
string="X",
compute="_compute_human_pos_x",
inverse="_inverse_human_pos_x",
readonly=False,
)
human_pos_y = fields.Integer(
string="Y",
compute="_compute_human_pos_y",
inverse="_inverse_human_pos_y",
readonly=False,
)
@api.depends("pos_x")
def _compute_human_pos_x(self):
for record in self:
record.human_pos_x = record.pos_x + 1
@api.depends("pos_y")
def _compute_human_pos_y(self):
for record in self:
record.human_pos_y = record.pos_y + 1
@api.depends("tray_matrix")
def _compute_pos(self):
for record in self:
if not record.tray_matrix["selected"]:
continue
record.update(
{
"pos_x": record.tray_matrix["selected"][0],
"pos_y": record.tray_matrix["selected"][1],
}
)
@api.depends("pos_x", "pos_y", "tray_type_id", "tray_id")
def _compute_tray_matrix(self):
self.tray_matrix = {}
for record in self.filtered("tray_type_id"):
cell_not_empty = self.env["stock.quant.vlm"].search_read(
[("tray_id", "=", record.tray_id.id)], ["pos_x", "pos_y"]
)
tray_matrix = {
"selected": [record.pos_x, record.pos_y],
"cells": record.tray_type_id._generate_cells_matrix(),
"first_empty_cell": False,
}
for position in cell_not_empty:
# Let's be gentle with positioning errors.
try:
tray_matrix["cells"][position["pos_y"]][position["pos_x"]] = 1
# pylint: disable=except-pass
except IndexError:
pass
for row, cells in enumerate(tray_matrix["cells"]):
if 0 not in cells:
continue
tray_matrix["first_empty_cell"] = [cells.index(0), row]
break
record.tray_matrix = tray_matrix
def tray_cell_center_position(self, pos_x=None, pos_y=None):
"""Center position in mm of a cell. Used to position the laser pointer.
@return {tuple} millimeters from bottom-left corner (left, bottom)
"""
if not self.tray_type_id:
return 0, 0
pos_x = pos_x or self.pos_x
pos_y = pos_y or self.pos_y
cell_width = self.tray_type_id.width_per_cell
cell_depth = self.tray_type_id.depth_per_cell
# pos_x and pos_y start at one, we want to count from 0
from_left = pos_x * cell_width + (cell_width / 2)
from_bottom = pos_y * cell_depth + (cell_depth / 2)
return int(from_left), int(from_bottom)

View File

@@ -0,0 +1,4 @@
* `Tecnativa <https://www.tecnativa.com>`_:
* David Vidal
* Pedro M. Baeza

View File

@@ -0,0 +1,3 @@
This module adds basic a management system for Vertical Lift Modules. It's thought as
a simpler alternative attemp to stock_vertical_lift and all the dependencies that
come with it.

View File

@@ -0,0 +1,17 @@
* Launch the tasks in batches so we don't have to send them to the VLM one by one. In
the case of Kardex, we'll be dealing with the connection limitations. If we send a
list of tasks, right now we're closing the connection once we receive a response (Kardex).
We need to keep listening until all the ids are received, but that locks our thread...
We also need to respond to operation issues on every task, like full trays, changes
of quantity, etc... and we can't lock many threads as we could put down the instance.
Something along the lines of queue_job maybe would need to push to the bus the tasks
updates (hard?) Or maybe the kardex proxy from c2c where we use a controller to send
the tasks? This should rely on js, if we want a proper UX.
* Confirming the VLM tasks after the picking is confirmed makes not much sense, but
we're dealing with the quants limitations. Anyway we shouldn't allow to leave
operations on halt and after a real VLM task is done, the picking should be validated.
What to do with the non existing quants (inputs)... maybe we could leave the vlm task
pending assignation, so when we finally validate the picking we just have to perform
the proper links.
* Not a requiste right now, but we could need to support batch pickings. Let's deal
with the basics for now anyway.

View File

@@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_quant_vlm,access_stock_quant_vlm,model_stock_quant_vlm,stock.group_stock_user,1,1,1,1
access_stock_location_vlm_tray_user,access_stock_location_vlm_tray,model_stock_location_vlm_tray,stock.group_stock_user,1,0,0,0
access_stock_location_vlm_tray_manager,access_stock_location_vlm_tray,model_stock_location_vlm_tray,stock.group_stock_manager,1,1,1,1
access_vlm_task,access_vlm_task,model_stock_vlm_task,stock.group_stock_user,1,1,1,1
access_vlm_task_action,access_vlm_task_action,model_stock_vlm_task_action,stock.group_stock_user,1,1,1,1
access_stock_location_vlm_tray_type,access_stock_location_vlm_tray_type,model_stock_location_vlm_tray_type,stock.group_stock_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_quant_vlm access_stock_quant_vlm model_stock_quant_vlm stock.group_stock_user 1 1 1 1
3 access_stock_location_vlm_tray_user access_stock_location_vlm_tray model_stock_location_vlm_tray stock.group_stock_user 1 0 0 0
4 access_stock_location_vlm_tray_manager access_stock_location_vlm_tray model_stock_location_vlm_tray stock.group_stock_manager 1 1 1 1
5 access_vlm_task access_vlm_task model_stock_vlm_task stock.group_stock_user 1 1 1 1
6 access_vlm_task_action access_vlm_task_action model_stock_vlm_task_action stock.group_stock_user 1 1 1 1
7 access_stock_location_vlm_tray_type access_stock_location_vlm_tray_type model_stock_location_vlm_tray_type stock.group_stock_user 1 1 1 1

View File

@@ -0,0 +1,451 @@
<!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: https://docutils.sourceforge.io/" />
<title>Vertical Lift Module management</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/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="vertical-lift-module-management">
<h1 class="title">Vertical Lift Module management</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:16cc58033099be982092f3cf06236fc4a8ab693fcfa4ff09ff5555062362ce43
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" 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 image-reference" 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 image-reference" href="https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_vlm_mgmt"><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 image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-16-0/stock-logistics-warehouse-16-0-stock_vlm_mgmt"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-warehouse&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds basic a management system for Vertical Lift Modules. Its thought as
a simpler alternative attemp to stock_vertical_lift and all the dependencies that
come with it.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-1">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-1">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Launch the tasks in batches so we dont have to send them to the VLM one by one. In
the case of Kardex, well be dealing with the connection limitations. If we send a
list of tasks, right now were closing the connection once we receive a response (Kardex).
We need to keep listening until all the ids are received, but that locks our thread…
We also need to respond to operation issues on every task, like full trays, changes
of quantity, etc… and we cant lock many threads as we could put down the instance.
Something along the lines of queue_job maybe would need to push to the bus the tasks
updates (hard?) Or maybe the kardex proxy from c2c where we use a controller to send
the tasks? This should rely on js, if we want a proper UX.</li>
<li>Confirming the VLM tasks after the picking is confirmed makes not much sense, but
were dealing with the quants limitations. Anyway we shouldnt allow to leave
operations on halt and after a real VLM task is done, the picking should be validated.
What to do with the non existing quants (inputs)… maybe we could leave the vlm task
pending assignation, so when we finally validate the picking we just have to perform
the proper links.</li>
<li>Not a requiste right now, but we could need to support batch pickings. Lets deal
with the basics for now anyway.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">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 to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vlm_mgmt%0Aversion:%2016.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="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>David Vidal</li>
<li>Pedro M. Baeza</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/chienandalu"><img alt="chienandalu" src="https://github.com/chienandalu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_vlm_mgmt">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>

View File

@@ -0,0 +1,62 @@
/** @odoo-module **/
/* Copyright 2024 Tecnativa - David Vidal
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).*/
import {Component, useState} from "@odoo/owl";
import {_lt} from "@web/core/l10n/translation";
import {registry} from "@web/core/registry";
import {standardFieldProps} from "@web/views/fields/standard_field_props";
import {useService} from "@web/core/utils/hooks";
export class LocationTrayMatrixField extends Component {
setup() {
this.state = useState(this.props.value);
this.orm = useService("orm");
this.action = useService("action");
}
/**
*
* @param {Event} event
* @returns {Object} Odoo action
*/
async onClickCell(event) {
const coordinates = event.currentTarget.dataset.coordinates
.split(",")
.map((x) => {
return parseInt(x, 10);
});
if (this.props.click_action) {
const action = this.orm.call(
this.props.record.resModel,
this.props.click_action,
[[this.props.record.data.id]],
{pos_x: coordinates[0], pos_y: coordinates[1]}
);
return this.action.doAction(action);
}
// This is for responsiveness
this.state.selected = coordinates;
// And here we propagate the changes to the field
this.props.value.selected = coordinates;
this.props.update(this.props.value);
}
}
LocationTrayMatrixField.template = "stock_vlm_mgmt.location_tray_matrix";
LocationTrayMatrixField.props = {
...standardFieldProps,
click_action: {
type: String,
optional: true,
},
};
LocationTrayMatrixField.extractProps = ({attrs}) => {
if ("click_action" in attrs.options) {
return {click_action: attrs.options.click_action};
}
};
LocationTrayMatrixField.displayName = _lt("Tray storage layout");
LocationTrayMatrixField.supportedTypes = ["serialized"];
registry.category("fields").add("location_tray_matrix", LocationTrayMatrixField);

View File

@@ -0,0 +1,24 @@
// Widget in form view
.o_field_location_tray_matrix {
table {
height: 200px;
tr {
td {
min-width: 250px;
}
}
}
}
// Widget in list view
.o_location_tray_matrix_cell {
.o_field_location_tray_matrix {
table {
height: 70px;
tr {
td {
min-width: 25px;
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t t-name="stock_vlm_mgmt.location_tray_matrix" owl="1">
<t
t-set="cell_min_width"
t-value="state.cells.length and (100 / state.cells[0].length) or 0"
/>
<table class="table-responsibe table-bordered text-center bg-light w-100">
<tbody>
<t t-foreach="state.cells" t-as="row" t-key="row_index">
<tr>
<t t-foreach="row" t-as="cell" t-key="cell_index">
<t
t-set="cell_coordinates"
t-value="[cell_index, row_index]"
/>
<t
t-set="is_selected"
t-value="state.selected.toString() === cell_coordinates.toString()"
/>
<td
t-att-data-coordinates="cell_coordinates"
t-attf-class="{{cell ? 'bg-success' : ''}} {{cell ? 'text-success' : 'text-light'}} {{is_selected ? 'border-warning border-3' : ''}}"
t-on-click="onClickCell"
role="button"
>·</td>
</t>
</tr>
</t>
</tbody>
</table>
</t>
</templates>

View File

@@ -0,0 +1,30 @@
/** @odoo-module **/
import {ListController} from "@web/views/list/list_controller";
import {listView} from "@web/views/list/list_view";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
export class VlmRequestListController extends ListController {
setup() {
super.setup();
this.orm = useService("orm");
this.action = useService("action");
}
async onClickVlmRequest() {
const resIds = this.model.root.selection.map((record) => record.resId);
const action = await this.orm.call(
"stock.vlm.task",
"action_do_tasks",
[resIds],
{}
);
this.action.doAction(action);
}
}
registry.category("views").add("stock_vlm_task_action_tree", {
...listView,
Controller: VlmRequestListController,
buttonTemplate: "VlmTaskRequestListView.buttons",
});

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t
t-name="VlmTaskRequestListView.buttons"
t-inherit="web.ListView.Buttons"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//t[contains(@t-if, 'isExportEnable')]" position="before">
<button
t-if="nbSelected"
type="button"
class="btn btn-secondary"
t-on-click="onClickVlmRequest"
>
<i
class="fa fa-rocket"
title="Perform tasks"
role="image"
/> Perform tasks
</button>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,14 @@
// Product image hint in the wizard
.o_form_view {
.vlm_task_image {
float: right;
margin-bottom: 10px;
> img {
max-width: 200px;
max-height: 200px;
margin: auto;
display: block;
}
}
}

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019 Camptocamp SA
Copyright 2024 Tecnativa - David Vidal
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_stock_location_tray_type_form" model="ir.ui.view">
<field name="model">stock.location.vlm.tray.type</field>
<field name="arch" type="xml">
<form string="VLM Tray Type">
<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="Layout" name="tray_layout">
<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="model">stock.location.vlm.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="model">stock.location.vlm.tray.type</field>
<field name="arch" type="xml">
<tree>
<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.vlm.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
id="menu_stock_location_tray_type"
action="action_stock_location_tray_type"
parent="menu_vlm_config"
/>
</odoo>

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_location_form" model="ir.ui.view">
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form" />
<field name="arch" type="xml">
<div class="oe_button_box" position="inside">
<button
string="Trays"
class="oe_stat_button"
icon="fa-inbox"
name="action_view_vlm_tray"
type="object"
attrs="{'invisible': [('is_vlm', '=', False)]}"
/>
<button
string="VLM Structure"
class="oe_stat_button"
icon="fa-table"
name="action_view_vlm_quants"
type="object"
attrs="{'invisible': [('is_vlm', '=', False)]}"
/>
<button
string="Release trays"
class="oe_stat_button"
icon="fa-outdent"
name="action_release_vlm_trays"
type="object"
attrs="{'invisible': [('is_vlm', '=', False)]}"
/>
</div>
<group name="additional_info" position="inside">
<field
name="is_vlm"
attrs="{'invisible': [('usage', 'not in', ('inventory', 'internal'))]}"
/>
<field
name="vlm_vendor"
attrs="{'invisible': [('is_vlm', '=', False)]}"
/>
</group>
<xpath expr="//group[@name='additional_info']/.." position="inside">
<group
string="VLM backend configuration"
name="vlm_config"
attrs="{'invisible': [('is_vlm', '=', False)]}"
>
<field
name="vlm_hostname"
attrs="{'required': [('is_vlm', '=', True)]}"
/>
<field
name="vlm_port"
attrs="{'required': [('is_vlm', '=', True)]}"
/>
<field
name="vlm_address"
attrs="{'required': [('is_vlm', '=', True)]}"
/>
<field name="vlm_user" />
<field name="vlm_password" password="True" />
<field
name="vlm_removal_strategy"
attrs="{'required': [('is_vlm', '=', True)]}"
/>
<field
name="vlm_sequence_id"
attrs="{'required': [('is_vlm', '=', True)]}"
/>
</group>
</xpath>
</field>
</record>
<record id="view_location_search" model="ir.ui.view">
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_search" />
<field name="arch" type="xml">
<separator position="before">
<filter
name="vlm"
string="Vertical Lift Module"
domain="[('is_vlm', '=', True)]"
help="Vertical Lift Module locations"
/>
</separator>
</field>
</record>
<record id="vlm_location_action" model="ir.actions.act_window">
<field name="name">Vertical Lift Modules</field>
<field name="res_model">stock.location</field>
<field name="context">{}</field>
<field name="domain">[('is_vlm', '=', True)]</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="vlm_locations_menu"
parent="stock.menu_warehouse_config"
action="vlm_location_action"
groups="stock.group_stock_manager"
/>
</odoo>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_location_vlm_tray_form" model="ir.ui.view">
<field name="model">stock.location.vlm.tray</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button
string="Call tray"
class="oe_stat_button"
icon="fa-outdent"
name="action_tray_call"
type="object"
/>
<button
string="Tray contents"
class="oe_stat_button"
icon="fa-table"
name="action_tray_content"
type="object"
/>
</div>
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
<group>
<group name="info">
<field name="location_id" />
<field name="tray_type_id" />
<field name="is_full" invisible="1" readonly="1" />
<field
name="tray_matrix"
string="Tray layout"
widget="location_tray_matrix"
options="{'click_action': 'action_tray_content'}"
nolabel="1"
/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_location_vlm_tray_tree" model="ir.ui.view">
<field name="model">stock.location.vlm.tray</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="location_id" />
<field name="tray_type_id" />
<field name="is_full" invisible="1" readonly="1" />
<field name="tray_matrix" widget="location_tray_matrix" nolabel="1" />
<button
string="Call"
class="btn btn-secondary btn-lg"
icon="fa-outdent"
name="action_tray_call"
type="object"
/>
<button
string="Tray inventory"
class="btn btn-primary btn-lg"
icon="fa-table"
name="action_tray_content"
type="object"
/>
</tree>
</field>
</record>
<record id="location_vlm_tray_action" model="ir.actions.act_window">
<field name="name">Vertical Lift Module Trays</field>
<field name="res_model">stock.location.vlm.tray</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_location_vlm_tray_tree" />
</record>
<menuitem
id="menu_vlm_config"
parent="stock.menu_stock_config_settings"
name="Vertical Lift Module"
/>
<menuitem
id="menu_vlm_config_trays"
parent="menu_vlm_config"
action="location_vlm_tray_action"
/>
</odoo>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="svm_operation_picking_form_view" model="ir.ui.view">
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="has_vlm_pending_operations" invisible="1" />
<field name="has_vlm_operations" invisible="1" />
<field name="vlm_task_ids" invisible="1" />
<field name="has_pending_vlm_tasks" invisible="1" />
<button
string="VLM Tasks"
type="object"
name="action_open_vlm_task"
class="oe_stat_button"
icon="fa-rocket"
attrs="{'invisible': [('vlm_task_ids', '=', [])]}"
/>
</xpath>
<xpath expr="//div[hasclass('oe_title')]" position="before">
<div
class="alert alert-warning h4"
role="alert"
attrs="{'invisible': [('has_pending_vlm_tasks', '=', False)]}"
>
<i class="fa fa-rocket" />&amp;nbsp;
Pending VLM tasks. Proceed to complete them.&amp;nbsp;
<button
name="action_do_vlm_tasks"
string="Start tasks"
type="object"
class="btn btn-primary btn-lg"
/>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_quant_tree" model="ir.ui.view">
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.view_stock_quant_tree" />
<field name="arch" type="xml">
<field name="product_id" position="after">
<field name="vlm_quant_ids" invisible="1" />
<button
name="action_view_in_vlm_structure"
string="View in VLM"
type="object"
class="o_replenish_buttons"
icon="fa-table"
attrs="{'invisible': [('vlm_quant_ids', '=', [])]}"
/>
</field>
</field>
</record>
<record id="view_stock_quant_tree_editable" model="ir.ui.view">
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.view_stock_quant_tree_editable" />
<field name="arch" type="xml">
<field name="product_id" position="after">
<field name="vlm_quant_ids" invisible="1" />
<button
name="action_view_in_vlm_structure"
string="View in VLM"
type="object"
class="o_replenish_buttons"
icon="fa-table"
attrs="{'invisible': [('vlm_quant_ids', '=', [])]}"
/>
</field>
</field>
</record>
<record id="view_stock_quant_tree_inventory_editable" model="ir.ui.view">
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.view_stock_quant_tree_inventory_editable" />
<field name="arch" type="xml">
<field name="product_id" position="after">
<field name="vlm_quant_ids" invisible="1" />
<button
name="action_view_in_vlm_structure"
string="View in VLM"
type="object"
class="o_replenish_buttons"
icon="fa-table"
attrs="{'invisible': [('vlm_quant_ids', '=', [])]}"
/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_quant_vlm_form" model="ir.ui.view">
<field name="model">stock.quant.vlm</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group string="Content">
<field name="product_id" readonly="1" force_save="1" />
<field name="quantity" />
</group>
<group string="Reference">
<field name="location_id" readonly="1" force_save="1" />
<field name="quant_id" readonly="1" force_save="1" />
<field
name="tray_id"
domain="[('location_id', '=', location_id)]"
/>
<field name="tray_type_id" invisible="1" />
<field name="pos_x" readonly="1" force_save="1" />
<field name="pos_y" readonly="1" force_save="1" />
</group>
<group>
<field
name="tray_matrix"
widget="location_tray_matrix"
options="{'live_cell_edit': True}"
nolabel="1"
/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_stock_quant_vlm_tree" model="ir.ui.view">
<field name="model">stock.quant.vlm</field>
<field name="arch" type="xml">
<tree>
<field name="tray_id" domain="[('location_id', '=', location_id)]" />
<field name="quant_id" optional="hide" readonly="1" />
<field name="location_id" optional="show" readonly="1" />
<field name="product_id" />
<field name="quantity" />
<field name="pos_x" optional="show" />
<field name="pos_y" optional="show" />
</tree>
</field>
</record>
<record id="location_quant_form_action" model="ir.actions.act_window">
<field name="name">Vertical Lift Module Quants</field>
<field name="res_model">stock.quant.vlm</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="view_stock_quant_inventory_tree" model="ir.ui.view">
<field name="model">stock.quant.vlm</field>
<field name="inherit_id" ref="view_stock_quant_vlm_tree" />
<field name="mode">primary</field>
<field name="arch" type="xml">
<tree position="attributes">
<attribute name="editable">top</attribute>
</tree>
<field name="location_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
<field name="pos_y" position="after">
<button
name="action_task_detail"
string="Details"
type="object"
class="oe_highlight"
/>
</field>
</field>
</record>
<record id="vlm_quant_search_view" model="ir.ui.view">
<field name="model">stock.quant.vlm</field>
<field name="arch" type="xml">
<search>
<field name="product_id" string="Product" />
<field name="location_id" string="VLM" />
<searchpanel>
<field
name="location_id"
string="VLM"
select="multi"
icon="fa-table"
enable_counters="1"
/>
<field
name="tray_id"
string="Trays"
select="multi"
icon="fa-tasks"
enable_counters="1"
/>
</searchpanel>
</search>
</field>
</record>
<record id="location_quant_vlm_action" model="ir.actions.act_window">
<field name="name">Vertical Lift Module Quants</field>
<field name="res_model">stock.quant.vlm</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="vlm_quant_search_view" />
<field name="view_id" ref="view_stock_quant_vlm_tree" />
</record>
<menuitem
id="menu_vlm_quant"
parent="menu_vlm_config"
action="location_quant_vlm_action"
/>
</odoo>

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_vlm_task_form" model="ir.ui.view">
<field name="model">stock.vlm.task</field>
<field name="arch" type="xml">
<form>
<header>
<field
name="state"
widget="statusbar"
clickable="1"
statusbar_visible="pending,waiting,done"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
string="Command"
help="Send task to the VLM"
class="oe_stat_button"
icon="fa-rocket"
name="action_do_tasks"
type="object"
states="pending,waiting"
/>
</div>
<group>
<group>
<field name="reference" />
<field name="task_type" string="Operation" readonly="1" />
<field name="location_id" />
<field name="tray_id" />
<field name="product_id" />
<field name="quantity_pending" string="Pending" />
<field name="quantity_done" string="Done" />
</group>
<group>
<field
name="tray_matrix"
widget="location_tray_matrix"
options="{'live_cell_edit': True}"
nolabel="1"
/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_vlm_task_tree" model="ir.ui.view">
<field name="model">stock.vlm.task</field>
<field name="arch" type="xml">
<tree
decoration-danger="state == 'pending'"
decoration-success="state == 'done'"
js_class="stock_vlm_task_action_tree"
>
<field name="tray_id" />
<field name="location_id" />
<field name="reference" />
<field name="product_id" />
<field name="quantity_pending" string="Pending" />
<field name="quantity_done" string="Done" />
<field name="task_type" string="Operation" />
<field name="state" />
<button
name="action_do_tasks"
title="Do tasks"
type="object"
icon="fa-rocket"
attrs="{'invisible': [('state', '=', 'done')]}"
/>
</tree>
</field>
</record>
<record id="vlm_task_search_view" model="ir.ui.view">
<field name="model">stock.vlm.task</field>
<field name="arch" type="xml">
<search>
<filter
name="pending"
string="Pending tasks"
domain="[('state', 'in', ['pending', 'waiting'])]"
/>
<filter
name="done"
string="Done tasks"
domain="[('state', '=', 'done')]"
/>
<searchpanel>
<field
name="location_id"
string="VLM"
select="multi"
icon="fa-table"
enable_counters="1"
/>
<field
name="task_type"
string="Operation"
select="multi"
icon="fa-rocket"
enable_counters="1"
/>
<field
name="tray_id"
string="Trays"
select="multi"
icon="fa-tasks"
enable_counters="1"
/>
<field
name="product_id"
select="multi"
icon="fa-cubes"
enable_counters="1"
/>
</searchpanel>
</search>
</field>
</record>
<record id="vlm_operation_bulk_task_action" model="ir.actions.server">
<field name="name">Perform VLM Tasks</field>
<field name="model_id" ref="stock_vlm_mgmt.model_stock_vlm_task" />
<field name="binding_model_id" ref="stock_vlm_mgmt.model_stock_vlm_task" />
<field name="state">code</field>
<field name="binding_view_types">list</field>
<field name="code">action = records.action_do_tasks()</field>
</record>
<record id="vlm_task_action" model="ir.actions.act_window">
<field name="name">Vertical Lift Module Tasks</field>
<field name="res_model">stock.vlm.task</field>
<field name="context">{"search_default_pending": 1}</field>
<field name="view_mode">tree,kanban,form</field>
</record>
<menuitem
id="vlm_task_menu"
parent="stock.menu_stock_warehouse_mgmt"
action="vlm_task_action"
groups="stock.group_stock_manager"
/>
</odoo>

View File

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

View File

@@ -0,0 +1,186 @@
# Copyright 2023 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
class VlmOperationTaskAction(models.TransientModel):
_name = "stock.vlm.task.action"
_inherit = ["vlm.tray.cell.position.mixin"]
_description = "Actions to perform on vlm task exceptions"
vlm_task_id = fields.Many2one(
string="Selected task", comodel_name="stock.vlm.task", required=True
)
vlm_task_ids = fields.Many2many(comodel_name="stock.vlm.task")
next_vlm_task_id = fields.Many2one(
comodel_name="stock.vlm.task", compute="_compute_next_vlm_task_id"
)
previous_vlm_task_id = fields.Many2one(
comodel_name="stock.vlm.task", compute="_compute_next_vlm_task_id"
)
splitted_vlm_task_id = fields.Many2one(
comodel_name="stock.vlm.task", related="vlm_task_id.splitted_vlm_task_id"
)
image_512 = fields.Image(related="vlm_task_id.product_id.image_512")
vlm_tasks_total = fields.Integer(compute="_compute_vlm_tasks_total")
vlm_tasks_partial = fields.Integer(compute="_compute_vlm_tasks_total")
vlm_tasks_progress = fields.Integer(compute="_compute_vlm_tasks_total")
location_id = fields.Many2one(
comodel_name="stock.location", related="vlm_task_id.location_id"
)
tray_id = fields.Many2one(comodel_name="stock.location.vlm.tray")
quantity_pending = fields.Float(readonly=True)
quantity_done = fields.Float()
warning = fields.Char(readonly=True)
state = fields.Selection(
selection=[
("pending", "Pending"),
("waiting", "Waiting"),
("done", "Done"),
("skipped", "Skipped"),
("edit", "Edit"),
],
compute="_compute_state",
)
@api.model
def default_get(self, fields):
vals = super().default_get(fields)
task_id = self.env.context.get("default_vlm_task_id")
task = self.env["stock.vlm.task"].browse(task_id)
vals.update(
{
"pos_x": task.pos_x,
"pos_y": task.pos_y,
"quantity_done": task.quantity_done,
"quantity_pending": task.quantity_pending,
"tray_id": task.tray_id.id,
"location_id": task.location_id.id,
"tray_type_id": task.tray_id.tray_type_id.id,
}
)
if not self.env.context.get("default_vlm_task_ids"):
vals.update({"vlm_task_ids": [(6, 0, task.ids)]})
warning = self.env.context.get("vlm_task_action_warning")
if warning == "mismatch_greater":
vals["warning"] = _(
"Quantity mismatch! The quantity reported by the VLM is greater "
"than original demand!. Please check it. If it's ok, you can fix "
"it now and save the task manually."
)
return vals
@api.depends("vlm_task_id", "vlm_task_ids")
def _compute_next_vlm_task_id(self):
self.next_vlm_task_id = False
self.previous_vlm_task_id = False
for wiz in self:
# Value of vlm_tasks_partial is index + 1
current_task_index = wiz.vlm_tasks_partial - 1
if wiz.vlm_tasks_partial < wiz.vlm_tasks_total:
wiz.next_vlm_task_id = wiz.vlm_task_ids[current_task_index + 1]
# It isn't 0
if current_task_index:
wiz.previous_vlm_task_id = wiz.vlm_task_ids[current_task_index - 1]
@api.depends("vlm_task_ids", "vlm_task_ids")
def _compute_vlm_tasks_total(self):
for wiz in self:
wiz.vlm_tasks_total = len(wiz.vlm_task_ids)
wiz.vlm_tasks_partial = wiz.vlm_task_ids.ids.index(wiz.vlm_task_id.id) + 1
wiz.vlm_tasks_progress = (wiz.vlm_tasks_partial / wiz.vlm_tasks_total) * 100
@api.depends("vlm_task_id.state", "vlm_task_id.skipped")
def _compute_state(self):
for wiz in self:
if wiz.vlm_task_id.skipped:
wiz.state = "skipped"
elif self.env.context.get("vlm_wiz_edit_task"):
wiz.state = "edit"
else:
wiz.state = wiz.vlm_task_id.state
def action_command(self):
"""Set task values from the wizard"""
self.vlm_task_id.pos_x = self.pos_x
self.vlm_task_id.pos_y = self.pos_x
self.vlm_task_id.tray_id = self.tray_id
response, task = self.vlm_task_id.action_command_task()
return self._check_post_command_task(response, task)
def action_go_to_task(self, go_to="current"):
"""Go to the next task"""
if go_to == "next":
if not self.next_vlm_task_id:
return {"type": "ir.actions.act_window_close"}
idx = 1
go_to_task = self.next_vlm_task_id
elif go_to == "previous":
if not self.previous_vlm_task_id:
return {"type": "ir.actions.act_window_close"}
idx = -1
go_to_task = self.previous_vlm_task_id
# Current task
else:
idx = 0
go_to_task = self.vlm_task_id
action = self.env["ir.actions.actions"]._for_xml_id(
"stock_vlm_mgmt.action_vlm_task_action"
)
action[
"name"
] = f"VLM task {self.vlm_tasks_partial + idx} of {self.vlm_tasks_total}"
action["context"] = dict(
self.env.context,
default_vlm_task_id=go_to_task.id,
default_vlm_task_ids=self.vlm_task_ids.ids,
default_warning=self.warning,
)
return action
def action_next_task(self):
return self.action_go_to_task(go_to="next")
def action_previous_task(self):
return self.action_go_to_task(go_to="previous")
def action_skip_task(self):
"""Skip task and go to the next one"""
self.vlm_task_id._action_skip()
return self.action_next_task()
def action_manual_set(self):
"""Save the info directly to the task as if it was validated by the VLM"""
task = self.vlm_task_id
if self.pos_x != task.pos_x or self.pos_y != task.pos_y:
task.pos_x = self.pos_x
task.pos_y = self.pos_y
if self.tray_id != task.tray_id:
task.tray_id = self.tray_id
response, task = self.vlm_task_id._post_command(self.quantity_done)
return self._check_post_command_task(response, task)
def _set_warning_message(self, response):
self.warning = False
if response == "zero_quantity":
self.warning = _(
"No quantity was processed. Do you want to put the goods in another "
"position? (you can also skip the task)"
)
elif response == "mismatch_greater":
self.warning = _(
"The quantity reported is greater than the one set in the task!"
)
def _check_post_command_task(self, response, task):
"""Either commanded or manual we want to perform the same tasks"""
# Not all quantites were fulfilled. The task was splitted and we have to insert
# it after our current task and then go to it to complete it.
self._set_warning_message(response)
if response == "split" and task.splitted_vlm_task_id == self.vlm_task_id:
new_tasks_ids = self.vlm_task_ids.ids
insert_index = new_tasks_ids.index(self.vlm_task_id.id) + 1
new_tasks_ids[insert_index:insert_index] = [task.id]
self.vlm_task_ids = self.vlm_task_ids.browse(new_tasks_ids)
return self.action_next_task()
return self.action_go_to_task(go_to="current")

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="vlm_task_action_form_view" model="ir.ui.view">
<field name="model">stock.vlm.task.action</field>
<field name="arch" type="xml">
<form create="false" string="VLM Operation action needed">
<field name="state" invisible="1" />
<sheet>
<field name="next_vlm_task_id" invisible="1" />
<field name="previous_vlm_task_id" invisible="1" />
<field name="splitted_vlm_task_id" invisible="1" />
<widget
name="web_ribbon"
title="Done"
bg_color="bg-success"
attrs="{'invisible': [('state', '!=', 'done')]}"
/>
<widget
name="web_ribbon"
title="Skipped"
bg_color="bg-warning"
attrs="{'invisible': [('state', '!=', 'skipped')]}"
/>
<div
class="alert alert-warning"
role="alert"
attrs="{'invisible': [('warning', '=', False)]}"
>
<i class="fa fa-exclamation-triangle" />&amp;nbsp;
<field name="warning" />
</div>
<div
class="alert alert-info"
role="alert"
attrs="{'invisible': [('splitted_vlm_task_id', '=', False)]}"
>
<i class="fa fa-info-circle" />&amp;nbsp;Splitted from&amp;nbsp;
<field name="splitted_vlm_task_id" widget="selection" />
</div>
<div class="alert alert-info mb-0 h4" role="alert">
<button
name="action_command"
string="Start task"
type="object"
class="btn btn-primary btn-lg"
attrs="{'invisible': [('state', '=', 'done')]}"
/>&amp;nbsp;
<i class="fa fa-info-circle" />&amp;nbsp;
<field name="vlm_task_id" readonly="1" widget="selection" />
</div>
<group>
<group name="quantity">
<field name="quantity_pending" />
<field
name="quantity_done"
attrs="{'readonly': [('state', '=', 'done')]}"
/>
</group>
<group name="tray">
<field name="location_id" widget="selection" />
<field
name="tray_id"
domain="[('location_id', '=', location_id)]"
attrs="{'readonly': [('state', '=', 'done')]}"
widget="selection"
/>
<field name="pos_x" invisible="1" force_save="1" />
<field name="pos_y" invisible="1" force_save="1" />
</group>
</group>
<group>
<group name="position" col="1">
<field
name="tray_matrix"
widget="location_tray_matrix"
options="{'live_cell_edit': True}"
nolabel="1"
attrs="{'readonly': [('state', '=', 'done')]}"
/>
</group>
<group name="image">
<field
nolabel="1"
name="image_512"
widget="image"
class="vlm_task_image"
/>
</group>
</group>
<footer>
<button
name="action_skip_task"
string="Skip"
type="object"
class="btn btn-secondary"
attrs="{'invisible': [('state', 'in', ['done','skipped'])]}"
/>
<button
name="action_manual_set"
string="Manual set"
type="object"
class="btn btn-secondary"
attrs="{'invisible': [('state', '=', 'done')]}"
/>
<!-- Navigation buttons -->
<button
name="action_previous_task"
string="🡸 Previous"
type="object"
class="btn btn-secondary"
attrs="{'invisible': [('previous_vlm_task_id', '=', False)]}"
/>
<button
string="🡸 Previous"
disabled="1"
class="btn btn-secondary"
attrs="{'invisible': [('previous_vlm_task_id', '!=', False)]}"
/>
<button
name="action_next_task"
string="Next 🡺"
type="object"
class="btn btn-primary"
attrs="{'invisible': ['|', ('next_vlm_task_id', '=', False), ('state', 'not in', ['done', 'skipped'])]}"
/>
<button
name="action_next_task"
string="Next 🡺"
type="object"
class="btn btn-secondary"
attrs="{'invisible': ['|', ('next_vlm_task_id', '=', False), ('state', 'in', ['done', 'skipped'])]}"
/>
<button
string="Next 🡺"
disabled="1"
class="btn btn-secondary"
attrs="{'invisible': ['|', ('next_vlm_task_id', '!=', False), ('state', '=', 'done')]}"
/>
</footer>
</sheet>
</form>
</field>
</record>
<record id="action_vlm_task_action" model="ir.actions.act_window">
<field name="res_model">stock.vlm.task.action</field>
<field name="name">VLM Operation Action</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="view_id" ref="vlm_task_action_form_view" />
<field name="target">new</field>
</record>
</odoo>