mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
1
setup/stock_vlm_mgmt/odoo/addons/stock_vlm_mgmt
Symbolic link
1
setup/stock_vlm_mgmt/odoo/addons/stock_vlm_mgmt
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../stock_vlm_mgmt
|
||||
6
setup/stock_vlm_mgmt/setup.py
Normal file
6
setup/stock_vlm_mgmt/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
110
stock_vlm_mgmt/README.rst
Normal file
110
stock_vlm_mgmt/README.rst
Normal 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.
|
||||
2
stock_vlm_mgmt/__init__.py
Normal file
2
stock_vlm_mgmt/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
30
stock_vlm_mgmt/__manifest__.py
Normal file
30
stock_vlm_mgmt/__manifest__.py
Normal 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/**/*",
|
||||
],
|
||||
},
|
||||
}
|
||||
8
stock_vlm_mgmt/models/__init__.py
Normal file
8
stock_vlm_mgmt/models/__init__.py
Normal 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
|
||||
144
stock_vlm_mgmt/models/stock_location.py
Normal file
144
stock_vlm_mgmt/models/stock_location.py
Normal 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
|
||||
89
stock_vlm_mgmt/models/stock_location_vlm_tray.py
Normal file
89
stock_vlm_mgmt/models/stock_location_vlm_tray.py
Normal 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)
|
||||
53
stock_vlm_mgmt/models/stock_location_vlm_tray_type.py
Normal file
53
stock_vlm_mgmt/models/stock_location_vlm_tray_type.py
Normal 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)]
|
||||
152
stock_vlm_mgmt/models/stock_move_line.py
Normal file
152
stock_vlm_mgmt/models/stock_move_line.py
Normal 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
|
||||
85
stock_vlm_mgmt/models/stock_picking.py
Normal file
85
stock_vlm_mgmt/models/stock_picking.py
Normal 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
|
||||
119
stock_vlm_mgmt/models/stock_quant.py
Normal file
119
stock_vlm_mgmt/models/stock_quant.py
Normal 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",
|
||||
}
|
||||
192
stock_vlm_mgmt/models/stock_vlm_task.py
Normal file
192
stock_vlm_mgmt/models/stock_vlm_task.py
Normal 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
|
||||
90
stock_vlm_mgmt/models/vlm_tray_cell_position_mixin.py
Normal file
90
stock_vlm_mgmt/models/vlm_tray_cell_position_mixin.py
Normal 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)
|
||||
4
stock_vlm_mgmt/readme/CONTRIBUTORS.rst
Normal file
4
stock_vlm_mgmt/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
* `Tecnativa <https://www.tecnativa.com>`_:
|
||||
|
||||
* David Vidal
|
||||
* Pedro M. Baeza
|
||||
3
stock_vlm_mgmt/readme/DESCRIPTION.rst
Normal file
3
stock_vlm_mgmt/readme/DESCRIPTION.rst
Normal 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.
|
||||
17
stock_vlm_mgmt/readme/ROADMAP.rst
Normal file
17
stock_vlm_mgmt/readme/ROADMAP.rst
Normal 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.
|
||||
7
stock_vlm_mgmt/security/ir.model.access.csv
Normal file
7
stock_vlm_mgmt/security/ir.model.access.csv
Normal 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
|
||||
|
451
stock_vlm_mgmt/static/description/index.html
Normal file
451
stock_vlm_mgmt/static/description/index.html
Normal 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&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. It’s 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 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.</li>
|
||||
<li>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.</li>
|
||||
<li>Not a requiste right now, but we could need to support batch pickings. Let’s 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>
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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>
|
||||
14
stock_vlm_mgmt/static/src/scss/stock_vlm_mgmt.scss
Normal file
14
stock_vlm_mgmt/static/src/scss/stock_vlm_mgmt.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
stock_vlm_mgmt/views/stock_location_tray_type_views.xml
Normal file
99
stock_vlm_mgmt/views/stock_location_tray_type_views.xml
Normal 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>
|
||||
102
stock_vlm_mgmt/views/stock_location_views.xml
Normal file
102
stock_vlm_mgmt/views/stock_location_views.xml
Normal 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>
|
||||
88
stock_vlm_mgmt/views/stock_location_vlm_tray_views.xml
Normal file
88
stock_vlm_mgmt/views/stock_location_vlm_tray_views.xml
Normal 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>
|
||||
40
stock_vlm_mgmt/views/stock_picking_views.xml
Normal file
40
stock_vlm_mgmt/views/stock_picking_views.xml
Normal 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" />&nbsp;
|
||||
Pending VLM tasks. Proceed to complete them.&nbsp;
|
||||
<button
|
||||
name="action_do_vlm_tasks"
|
||||
string="Start tasks"
|
||||
type="object"
|
||||
class="btn btn-primary btn-lg"
|
||||
/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
54
stock_vlm_mgmt/views/stock_quant_views.xml
Normal file
54
stock_vlm_mgmt/views/stock_quant_views.xml
Normal 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>
|
||||
115
stock_vlm_mgmt/views/stock_quant_vlm_views.xml
Normal file
115
stock_vlm_mgmt/views/stock_quant_vlm_views.xml
Normal 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>
|
||||
142
stock_vlm_mgmt/views/stock_vlm_task_views.xml
Normal file
142
stock_vlm_mgmt/views/stock_vlm_task_views.xml
Normal 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>
|
||||
1
stock_vlm_mgmt/wizards/__init__.py
Normal file
1
stock_vlm_mgmt/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import stock_vlm_task_action
|
||||
186
stock_vlm_mgmt/wizards/stock_vlm_task_action.py
Normal file
186
stock_vlm_mgmt/wizards/stock_vlm_task_action.py
Normal 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")
|
||||
152
stock_vlm_mgmt/wizards/stock_vlm_task_action_views.xml
Normal file
152
stock_vlm_mgmt/wizards/stock_vlm_task_action_views.xml
Normal 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" />&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" />&nbsp;Splitted from&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')]}"
|
||||
/>&nbsp;
|
||||
<i class="fa fa-info-circle" />&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>
|
||||
Reference in New Issue
Block a user