mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -1,3 +1,5 @@
|
||||
product-attribute
|
||||
server-env
|
||||
server-ux
|
||||
web
|
||||
wms
|
||||
|
||||
1
setup/stock_vertical_lift/odoo/addons/stock_vertical_lift
Symbolic link
1
setup/stock_vertical_lift/odoo/addons/stock_vertical_lift
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../stock_vertical_lift
|
||||
6
setup/stock_vertical_lift/setup.py
Normal file
6
setup/stock_vertical_lift/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
../../../../stock_vertical_lift_kardex
|
||||
6
setup/stock_vertical_lift_kardex/setup.py
Normal file
6
setup/stock_vertical_lift_kardex/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
../../../../stock_vertical_lift_packaging_type
|
||||
6
setup/stock_vertical_lift_packaging_type/setup.py
Normal file
6
setup/stock_vertical_lift_packaging_type/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
../../../../stock_vertical_lift_server_env
|
||||
6
setup/stock_vertical_lift_server_env/setup.py
Normal file
6
setup/stock_vertical_lift_server_env/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
../../../../stock_vertical_lift_storage_type
|
||||
6
setup/stock_vertical_lift_storage_type/setup.py
Normal file
6
setup/stock_vertical_lift_storage_type/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
@@ -5,6 +5,7 @@
|
||||
<field name="sequence">1</field>
|
||||
<field name="location_id" ref="stock.stock_location_stock" />
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
<field name="active" eval="False" />
|
||||
</record>
|
||||
<record id="stock_reserve_rule_removal_1_demo" model="stock.reserve.rule.removal">
|
||||
<field name="rule_id" ref="stock_reserve_rule_1_demo" />
|
||||
|
||||
@@ -25,8 +25,8 @@ A product: Funky Socks
|
||||
Scenario:
|
||||
|
||||
* Activate Storage Locations and Multi-Warehouses
|
||||
* You can open Inventory > Configuration > Stock Reservation Rules to see the
|
||||
rules
|
||||
* You can open Inventory > Configuration > Stock Reservation Rules to activate
|
||||
and see the rules (by default in demo, the rules are created inactive)
|
||||
* Open Transfer: Outgoing shipment (reservation rules demo 1)
|
||||
* Check availability: it has 150 units, as it will not empty Zone A, it will not
|
||||
take products there, it should take 100 in B and 50 in C (following the rules
|
||||
|
||||
@@ -17,6 +17,8 @@ class TestReserveRule(common.SavepointCase):
|
||||
"code": "WHTEST",
|
||||
}
|
||||
)
|
||||
cls.rule = cls.env.ref("stock_reserve_rule.stock_reserve_rule_1_demo")
|
||||
cls.rule.active = True
|
||||
|
||||
cls.customer_loc = cls.env.ref("stock.stock_location_customers")
|
||||
|
||||
|
||||
134
stock_vertical_lift/README.rst
Normal file
134
stock_vertical_lift/README.rst
Normal file
@@ -0,0 +1,134 @@
|
||||
=============
|
||||
Vertical Lift
|
||||
=============
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Alpha
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_vertical_lift
|
||||
:alt: OCA/stock-logistics-warehouse
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-12-0/stock-logistics-warehouse-12-0-stock_vertical_lift
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||
:target: https://runbot.odoo-community.org/runbot/153/12.0
|
||||
:alt: Try me on Runbot
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Add configuration and dedicated screens to work with Vertical Lift
|
||||
systems (such as Kardex Remstar, Modula, ...). Drivers for controlling
|
||||
the lifts physically must be added by additional addons.
|
||||
|
||||
.. IMPORTANT::
|
||||
This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
`More details on development status <https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/oca_module_lifecycle_development_status.rst>`_
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
General
|
||||
~~~~~~~
|
||||
|
||||
In Inventory Settings, you must have:
|
||||
|
||||
* Storage Locations
|
||||
* Multi-Warehouses
|
||||
* Multi-Step Routes
|
||||
|
||||
Locations
|
||||
~~~~~~~~~
|
||||
|
||||
Additional configuration parameters are added in Locations:
|
||||
|
||||
* Sub-locations of a location with the "Is a Vertical Lift View Location"
|
||||
activated are considered as "Shuttles". A shuttle is a vertical lift shelf.
|
||||
* Sub-locations of shuttles are considered as "Trays", which is a tier of a
|
||||
shuttle. When a tray is created, a tray type must be selected. When saved, the
|
||||
tray location will automatically create as many sub-locations - called
|
||||
"Cells" - as the tray type contains.
|
||||
* The tray type of a tray can be changed as long as none of its cell contains
|
||||
products. When changed, it archives the cells and creates new ones as
|
||||
configured on the new tray type.
|
||||
|
||||
Tray types
|
||||
~~~~~~~~~~
|
||||
|
||||
Tray types can be configured in the Inventory settings.
|
||||
A tray type defines how much cells a tray can hold. It is a square or rectangle
|
||||
matrix of n cols * m rows.
|
||||
|
||||
Vertical Lift Shuttles
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created
|
||||
in Odoo for each physical shuttle. Depending of the subsidiary addons installed
|
||||
(eg. Kardex), different options may be required (host address, ...). The base
|
||||
addon only includes shuttles of kind "simulation" which will not send orders to
|
||||
the hardware.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Extract the tray types and matrix widget in a module, they can be used
|
||||
alone without vertical lift
|
||||
* Consider merging the 'vertical_lift_kind' with the kind added by
|
||||
stock_location_zone
|
||||
* Complete Pick screen and workflow (currently enough for a demo, not for production)
|
||||
* Implement Put-away screen and workflow
|
||||
* Implement Inventory screen and workflow
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vertical_lift%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Camptocamp
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_vertical_lift>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
2
stock_vertical_lift/__init__.py
Normal file
2
stock_vertical_lift/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
41
stock_vertical_lift/__manifest__.py
Normal file
41
stock_vertical_lift/__manifest__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Vertical Lift",
|
||||
"summary": "Provides the core for integration with Vertical Lifts",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Stock",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"stock",
|
||||
"barcodes",
|
||||
"base_sparse_field",
|
||||
"stock_location_tray", # OCA/stock-logistics-warehouse
|
||||
"web_notify", # OCA/web
|
||||
"web_ir_actions_act_view_reload", # OCA/web
|
||||
],
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"demo": [
|
||||
"demo/stock_location_demo.xml",
|
||||
"demo/vertical_lift_shuttle_demo.xml",
|
||||
"demo/product_demo.xml",
|
||||
"demo/stock_inventory_demo.xml",
|
||||
"demo/stock_picking_demo.xml",
|
||||
],
|
||||
"data": [
|
||||
"views/stock_location_views.xml",
|
||||
"views/stock_move_line_views.xml",
|
||||
"views/vertical_lift_shuttle_views.xml",
|
||||
"views/vertical_lift_operation_base_views.xml",
|
||||
"views/vertical_lift_operation_pick_views.xml",
|
||||
"views/vertical_lift_operation_put_views.xml",
|
||||
"views/vertical_lift_operation_inventory_views.xml",
|
||||
"views/stock_vertical_lift_templates.xml",
|
||||
"views/shuttle_screen_templates.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/ir_sequence.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
1
stock_vertical_lift/controllers/__init__.py
Normal file
1
stock_vertical_lift/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
22
stock_vertical_lift/controllers/main.py
Normal file
22
stock_vertical_lift/controllers/main.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VerticalLiftController(http.Controller):
|
||||
@http.route(["/vertical-lift"], type="http", auth="public", csrf=False)
|
||||
def vertical_lift(self, answer, secret):
|
||||
if secret == os.environ.get("VERTICAL_LIFT_SECRET", ""):
|
||||
rec = request.env["vertical.lift.command"].sudo().record_answer(answer)
|
||||
return str(rec.id)
|
||||
else:
|
||||
_logger.error(
|
||||
"secret mismatch: %r != %r",
|
||||
secret,
|
||||
os.environ.get("VERTICAL_LIFT_SECRET", ""),
|
||||
)
|
||||
raise http.AuthenticationError()
|
||||
10
stock_vertical_lift/data/ir_sequence.xml
Normal file
10
stock_vertical_lift/data/ir_sequence.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record model="ir.sequence" id="sequence_kardex_command">
|
||||
<field name="name">Vertical Lift Commands</field>
|
||||
<field name="code">vertical.lift.command</field>
|
||||
<field name="prefix">L</field>
|
||||
<field name="padding">6</field>
|
||||
<field name="company_id" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
||||
29
stock_vertical_lift/demo/product_demo.xml
Normal file
29
stock_vertical_lift/demo/product_demo.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="product_running_socks" model="product.product">
|
||||
<field name="default_code">RS200</field>
|
||||
<field name="barcode">4491673293664</field>
|
||||
<field name="name">Running Socks</field>
|
||||
<field name="type">product</field>
|
||||
<field name="categ_id" ref="product.product_category_6" />
|
||||
<field name="lst_price">30.0</field>
|
||||
<field name="standard_price">20.0</field>
|
||||
<field name="weight">1.0</field>
|
||||
<field name="tracking">none</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
</record>
|
||||
<record id="product_recovery_socks" model="product.product">
|
||||
<field name="default_code">RS300</field>
|
||||
<field name="barcode">2779891103531</field>
|
||||
<field name="name">Recovery Socks</field>
|
||||
<field name="type">product</field>
|
||||
<field name="categ_id" ref="product.product_category_6" />
|
||||
<field name="lst_price">30.0</field>
|
||||
<field name="standard_price">20.0</field>
|
||||
<field name="weight">1.0</field>
|
||||
<field name="tracking">none</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
</record>
|
||||
</odoo>
|
||||
30
stock_vertical_lift/demo/stock_inventory_demo.xml
Normal file
30
stock_vertical_lift/demo/stock_inventory_demo.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="stock_inventory_vertical_lift_0" model="stock.inventory">
|
||||
<field name="name">Starting Vertical Lift Inventory</field>
|
||||
</record>
|
||||
<record id="stock_inventory_vertical_lift_line_1" model="stock.inventory.line">
|
||||
<field name="product_id" ref="product_running_socks" />
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="inventory_id" ref="stock_inventory_vertical_lift_0" />
|
||||
<field name="product_qty">30.0</field>
|
||||
<field
|
||||
name="location_id"
|
||||
ref="stock_location_vertical_lift_demo_tray_1b_x3y2"
|
||||
/>
|
||||
</record>
|
||||
<function model="stock.inventory" name="_action_start">
|
||||
<function
|
||||
eval="[[('state','=','draft'),('id', '=', ref('stock_vertical_lift.stock_inventory_vertical_lift_0'))]]"
|
||||
model="stock.inventory"
|
||||
name="search"
|
||||
/>
|
||||
</function>
|
||||
<function model="stock.inventory" name="action_validate">
|
||||
<function
|
||||
eval="[[('state','=','confirm'),('id', '=', ref('stock_vertical_lift.stock_inventory_vertical_lift_0'))]]"
|
||||
model="stock.inventory"
|
||||
name="search"
|
||||
/>
|
||||
</function>
|
||||
</odoo>
|
||||
131
stock_vertical_lift/demo/stock_location_demo.xml
Normal file
131
stock_vertical_lift/demo/stock_location_demo.xml
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="stock_location_vertical_lift" model="stock.location">
|
||||
<field name="name">Vertical Lift</field>
|
||||
<!-- Beware, must not be view, even if we must not have stock here.
|
||||
Because stock moves targeting views are not included in the
|
||||
inventory at date, and we want to be able to use "Vertical Lift View"
|
||||
as destination (the final destination will be a "cell" in a Shuttle).
|
||||
-->
|
||||
<field name="location_id" ref="stock.stock_location_stock" />
|
||||
<field name="usage">internal</field>
|
||||
<field name="vertical_lift_location" eval="True" />
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_shuttle_1" model="stock.location">
|
||||
<field name="name">Shuttle 1</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift" />
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_1a" model="stock.location">
|
||||
<field name="name">Tray 1A</field>
|
||||
<field name="barcode">T1A</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_1" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_small_8x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_1b" model="stock.location">
|
||||
<field name="name">Tray 1B</field>
|
||||
<field name="barcode">T1B</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_1" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_16x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_1c" model="stock.location">
|
||||
<field name="name">Tray 1C</field>
|
||||
<field name="barcode">T1C</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_1" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_32x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_shuttle_2" model="stock.location">
|
||||
<field name="name">Shuttle 2</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift" />
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_2a" model="stock.location">
|
||||
<field name="name">Tray 2A</field>
|
||||
<field name="barcode">T2A</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_2" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_8x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_2b" model="stock.location">
|
||||
<field name="name">Tray 2B</field>
|
||||
<field name="barcode">T2B</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_2" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_4x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_2c" model="stock.location">
|
||||
<field name="name">Tray 2C</field>
|
||||
<field name="barcode">T2C</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_2" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_16x_2"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_2d" model="stock.location">
|
||||
<field name="name">Tray 2D</field>
|
||||
<field name="barcode">T2D</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_2" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_small_8x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_shuttle_3" model="stock.location">
|
||||
<field name="name">Shuttle 3</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift" />
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_3a" model="stock.location">
|
||||
<field name="name">Tray 3A</field>
|
||||
<field name="barcode">T3A</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_3" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_small_32x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<record id="stock_location_vertical_lift_demo_tray_3b" model="stock.location">
|
||||
<field name="name">Tray 3B</field>
|
||||
<field name="barcode">T3B</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_3" />
|
||||
<field
|
||||
name="tray_type_id"
|
||||
ref="stock_location_tray.stock_location_tray_type_large_32x"
|
||||
/>
|
||||
<field name="usage">internal</field>
|
||||
</record>
|
||||
<!-- When the trays are created, they will create their 'cell' locations.
|
||||
This method will add xmlids on them to be able to reference them in
|
||||
other demo data and tests.
|
||||
-->
|
||||
<function model="stock.location" name="_create_tray_xmlids">
|
||||
<function
|
||||
eval="[[('cell_in_tray_type_id', '!=', False)]]"
|
||||
model="stock.location"
|
||||
name="search"
|
||||
/>
|
||||
<value>stock_vertical_lift</value>
|
||||
</function>
|
||||
</odoo>
|
||||
63
stock_vertical_lift/demo/stock_picking_demo.xml
Normal file
63
stock_vertical_lift/demo/stock_picking_demo.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="stock_picking_out_demo_vertical_lift_1" model="stock.picking">
|
||||
<field name="picking_type_id" ref="stock.picking_type_out" />
|
||||
<field name="origin">Outgoing shipment from Vertical Lift (demo)</field>
|
||||
<field name="partner_id" ref="base.res_partner_1" />
|
||||
<field name="date" eval="DateTime.today()" />
|
||||
<field name="location_id" ref="stock.stock_location_stock" />
|
||||
<field name="location_dest_id" ref="stock.stock_location_customers" />
|
||||
<field
|
||||
name="move_lines"
|
||||
model="stock.move"
|
||||
eval="[(0, 0, {
|
||||
'name': obj().env.ref('stock_vertical_lift.product_running_socks').name,
|
||||
'product_id': ref('stock_vertical_lift.product_running_socks'),
|
||||
'product_uom': ref('uom.product_uom_unit'),
|
||||
'product_uom_qty': 15.0,
|
||||
'picking_type_id': ref('stock.picking_type_out'),
|
||||
'location_id': ref('stock.stock_location_stock'),
|
||||
'location_dest_id': ref('stock.stock_location_customers'),
|
||||
})]"
|
||||
/>
|
||||
</record>
|
||||
<function model="stock.picking" name="action_confirm">
|
||||
<value
|
||||
model="stock.picking"
|
||||
eval="[obj().env.ref('stock_vertical_lift.stock_picking_out_demo_vertical_lift_1').id]"
|
||||
/>
|
||||
</function>
|
||||
<function model="stock.picking" name="action_assign">
|
||||
<value
|
||||
model="stock.picking"
|
||||
eval="[obj().env.ref('stock_vertical_lift.stock_picking_out_demo_vertical_lift_1').id]"
|
||||
/>
|
||||
</function>
|
||||
<record id="stock_picking_in_demo_vertical_lift_1" model="stock.picking">
|
||||
<field name="picking_type_id" ref="stock.picking_type_in" />
|
||||
<field name="origin">Incoming shipment from Vertical Lift (demo)</field>
|
||||
<field name="partner_id" ref="base.res_partner_1" />
|
||||
<field name="date" eval="DateTime.today()" />
|
||||
<field name="location_id" ref="stock.stock_location_suppliers" />
|
||||
<field name="location_dest_id" ref="stock.stock_location_stock" />
|
||||
<field
|
||||
name="move_lines"
|
||||
model="stock.move"
|
||||
eval="[(0, 0, {
|
||||
'name': obj().env.ref('stock_vertical_lift.product_running_socks').name,
|
||||
'product_id': ref('stock_vertical_lift.product_running_socks'),
|
||||
'product_uom': ref('uom.product_uom_unit'),
|
||||
'product_uom_qty': 15.0,
|
||||
'picking_type_id': ref('stock.picking_type_in'),
|
||||
'location_id': ref('stock.stock_location_suppliers'),
|
||||
'location_dest_id': ref('stock.stock_location_stock'),
|
||||
})]"
|
||||
/>
|
||||
</record>
|
||||
<function model="stock.picking" name="action_confirm">
|
||||
<value
|
||||
model="stock.picking"
|
||||
eval="[obj().env.ref('stock_vertical_lift.stock_picking_in_demo_vertical_lift_1').id]"
|
||||
/>
|
||||
</function>
|
||||
</odoo>
|
||||
18
stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml
Normal file
18
stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="stock_vertical_lift_demo_shuttle_1" model="vertical.lift.shuttle">
|
||||
<field name="name">Shuttle 1</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_1" />
|
||||
<field name="mode">pick</field>
|
||||
</record>
|
||||
<record id="stock_vertical_lift_demo_shuttle_2" model="vertical.lift.shuttle">
|
||||
<field name="name">Shuttle 2</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_2" />
|
||||
<field name="mode">pick</field>
|
||||
</record>
|
||||
<record id="stock_vertical_lift_demo_shuttle_3" model="vertical.lift.shuttle">
|
||||
<field name="name">Shuttle 3</field>
|
||||
<field name="location_id" ref="stock_location_vertical_lift_demo_shuttle_3" />
|
||||
<field name="mode">pick</field>
|
||||
</record>
|
||||
</odoo>
|
||||
55
stock_vertical_lift/images/O-BTN.release.svg
Normal file
55
stock_vertical_lift/images/O-BTN.release.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" width="356" height="81.587" viewBox="0 0 356 81.587">
|
||||
<desc></desc>
|
||||
<rect x="0" y="0" width="356" height="81.587" style="fill:#ffffff;" />
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="2" y1="0" x2="2" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="7" y1="0" x2="7" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="13" y1="0" x2="13" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="23" y1="0" x2="23" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="33" y1="0" x2="33" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="40" y1="0" x2="40" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="45" y1="0" x2="45" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="52" y1="0" x2="52" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="59" y1="0" x2="59" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="67" y1="0" x2="67" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="75" y1="0" x2="75" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="80" y1="0" x2="80" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="90" y1="0" x2="90" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="97" y1="0" x2="97" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="107" y1="0" x2="107" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="111" y1="0" x2="111" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="117" y1="0" x2="117" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="128" y1="0" x2="128" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="133" y1="0" x2="133" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="140" y1="0" x2="140" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="149" y1="0" x2="149" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="155" y1="0" x2="155" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="161" y1="0" x2="161" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:8;" x1="170" y1="0" x2="170" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="177" y1="0" x2="177" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="182" y1="0" x2="182" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="189" y1="0" x2="189" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="200" y1="0" x2="200" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="207" y1="0" x2="207" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="211" y1="0" x2="211" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="221" y1="0" x2="221" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="226" y1="0" x2="226" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="233" y1="0" x2="233" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="243" y1="0" x2="243" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="249" y1="0" x2="249" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="254" y1="0" x2="254" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="265" y1="0" x2="265" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:8;" x1="272" y1="0" x2="272" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="281" y1="0" x2="281" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="287" y1="0" x2="287" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="292" y1="0" x2="292" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="299" y1="0" x2="299" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="309" y1="0" x2="309" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="318" y1="0" x2="318" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="327" y1="0" x2="327" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="332" y1="0" x2="332" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="343" y1="0" x2="343" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="349" y1="0" x2="349" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="354" y1="0" x2="354" y2="60"/>
|
||||
<text x="178" y="77.271" text-anchor="middle" style="font-family: Tahoma; font-size: 20px; stroke: none; fill:#000000;">O-BTN.release</text></svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
46
stock_vertical_lift/images/O-BTN.save.svg
Normal file
46
stock_vertical_lift/images/O-BTN.save.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" width="290" height="81.587" viewBox="0 0 290 81.587">
|
||||
<desc></desc>
|
||||
<rect x="0" y="0" width="290" height="81.587" style="fill:#ffffff;" />
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="2" y1="0" x2="2" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="7" y1="0" x2="7" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="13" y1="0" x2="13" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="23" y1="0" x2="23" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="33" y1="0" x2="33" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="40" y1="0" x2="40" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="45" y1="0" x2="45" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="52" y1="0" x2="52" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="59" y1="0" x2="59" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="67" y1="0" x2="67" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="75" y1="0" x2="75" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="80" y1="0" x2="80" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="90" y1="0" x2="90" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="97" y1="0" x2="97" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="107" y1="0" x2="107" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="111" y1="0" x2="111" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="117" y1="0" x2="117" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="128" y1="0" x2="128" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="133" y1="0" x2="133" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="140" y1="0" x2="140" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="149" y1="0" x2="149" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="155" y1="0" x2="155" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:8;" x1="162" y1="0" x2="162" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="171" y1="0" x2="171" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="177" y1="0" x2="177" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="183" y1="0" x2="183" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="188" y1="0" x2="188" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:8;" x1="202" y1="0" x2="202" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="209" y1="0" x2="209" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="215" y1="0" x2="215" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="221" y1="0" x2="221" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="226" y1="0" x2="226" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="233" y1="0" x2="233" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="243" y1="0" x2="243" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="248" y1="0" x2="248" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="259" y1="0" x2="259" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="266" y1="0" x2="266" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:6;" x1="277" y1="0" x2="277" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:2;" x1="283" y1="0" x2="283" y2="60"/>
|
||||
<line fill="none" style="stroke:#000000; stroke-width:4;" x1="288" y1="0" x2="288" y2="60"/>
|
||||
<text x="145" y="77.271" text-anchor="middle" style="font-family: Tahoma; font-size: 20px; stroke: none; fill:#000000;">O-BTN.save</text></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
11
stock_vertical_lift/models/__init__.py
Normal file
11
stock_vertical_lift/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from . import vertical_lift_shuttle
|
||||
from . import vertical_lift_operation_base
|
||||
from . import vertical_lift_operation_pick
|
||||
from . import vertical_lift_operation_put
|
||||
from . import vertical_lift_operation_inventory
|
||||
from . import stock_inventory
|
||||
from . import stock_location
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_quant
|
||||
from . import vertical_lift_command
|
||||
27
stock_vertical_lift/models/stock_inventory.py
Normal file
27
stock_vertical_lift/models/stock_inventory.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class InventoryLine(models.Model):
|
||||
_inherit = "stock.inventory.line"
|
||||
|
||||
vertical_lift_done = fields.Boolean(default=False)
|
||||
# Field used to sort lines by tray on the inventory scan screen, so entire
|
||||
# trays are processed one after the other
|
||||
vertical_lift_tray_id = fields.Many2one(
|
||||
comodel_name="stock.location",
|
||||
compute="_compute_vertical_lift_tray_id",
|
||||
readonly=True,
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("location_id.vertical_lift_kind")
|
||||
def _compute_vertical_lift_tray_id(self):
|
||||
for line in self:
|
||||
if line.location_id.vertical_lift_kind == "cell":
|
||||
# The parent of the cell is the tray.
|
||||
line.vertical_lift_tray_id = line.location_id.location_id
|
||||
else:
|
||||
line.vertical_lift_tray_id = False
|
||||
159
stock_vertical_lift/models/stock_location.py
Normal file
159
stock_vertical_lift/models/stock_location.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
_inherit = "stock.location"
|
||||
|
||||
vertical_lift_location = fields.Boolean(
|
||||
"Is a Vertical Lift View Location?",
|
||||
default=False,
|
||||
help="Check this box to use it as the view for Vertical" " Lift Shuttles.",
|
||||
)
|
||||
vertical_lift_kind = fields.Selection(
|
||||
selection=[
|
||||
("view", "View"),
|
||||
("shuttle", "Shuttle"),
|
||||
("tray", "Tray"),
|
||||
("cell", "Cell"),
|
||||
],
|
||||
compute="_compute_vertical_lift_kind",
|
||||
store=True,
|
||||
)
|
||||
|
||||
# This is a one2one in practice, but this one is not really interesting.
|
||||
# It's there only to be in the depends of 'vertical_lift_shuttle_id', which
|
||||
# give the unique shuttle for any location in the tree (whether it's a
|
||||
# shuttle, a tray or a cell)
|
||||
inverse_vertical_lift_shuttle_ids = fields.One2many(
|
||||
comodel_name="vertical.lift.shuttle", inverse_name="location_id", readonly=True
|
||||
)
|
||||
# compute the unique shuttle for any shuttle, tray or cell location, by
|
||||
# going through the parents
|
||||
vertical_lift_shuttle_id = fields.Many2one(
|
||||
comodel_name="vertical.lift.shuttle",
|
||||
compute="_compute_vertical_lift_shuttle_id",
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"location_id", "location_id.vertical_lift_kind", "vertical_lift_location"
|
||||
)
|
||||
def _compute_vertical_lift_kind(self):
|
||||
tree = {"view": "shuttle", "shuttle": "tray", "tray": "cell"}
|
||||
for location in self:
|
||||
if location.vertical_lift_location:
|
||||
location.vertical_lift_kind = "view"
|
||||
continue
|
||||
kind = tree.get(location.location_id.vertical_lift_kind, False)
|
||||
location.vertical_lift_kind = kind
|
||||
|
||||
@api.depends(
|
||||
"inverse_vertical_lift_shuttle_ids", "location_id.vertical_lift_shuttle_id"
|
||||
)
|
||||
def _compute_vertical_lift_shuttle_id(self):
|
||||
for location in self:
|
||||
if location.inverse_vertical_lift_shuttle_ids:
|
||||
# we have a unique constraint on the other side
|
||||
assert len(location.inverse_vertical_lift_shuttle_ids) == 1
|
||||
shuttle = location.inverse_vertical_lift_shuttle_ids
|
||||
else:
|
||||
shuttle = location.location_id.vertical_lift_shuttle_id
|
||||
location.vertical_lift_shuttle_id = shuttle
|
||||
|
||||
def _hardware_vertical_lift_fetch_tray(self, cell_location=None):
|
||||
payload = self._hardware_vertical_lift_fetch_tray_payload(cell_location)
|
||||
return self.vertical_lift_shuttle_id._hardware_send_message(payload)
|
||||
|
||||
def _hardware_vertical_lift_fetch_tray_payload(self, cell_location=None):
|
||||
"""Prepare "fetch" message to be sent to the vertical lift hardware
|
||||
|
||||
Private method, this is where the implementation actually happens.
|
||||
Addons can add their instructions based on the hardware used for
|
||||
this location.
|
||||
|
||||
The hardware used for a location can be found in:
|
||||
|
||||
``self.vertical_lift_shuttle_id.hardware``
|
||||
|
||||
Each addon can implement its own mechanism depending of this value
|
||||
and must call ``super``.
|
||||
|
||||
The method must send the command to the vertical lift to fetch / open
|
||||
the tray. If a ``cell_location`` is passed and if the hardware supports
|
||||
a way to show a cell (such as a laser pointer), it should send this
|
||||
command as well.
|
||||
|
||||
Useful information that could be needed for the drivers:
|
||||
|
||||
* Any field of `self` (name, barcode, ...) which is the current tray.
|
||||
* Any field of `cell_location` (name, barcode, ...) which is the cell
|
||||
in the tray.
|
||||
* ``self.vertical_lift_shuttle_id`` is the current Shuttle, where we
|
||||
find details about the hardware, the current mode (pick, put, ...).
|
||||
* ``self.tray_type_id`` is the kind of tray.
|
||||
* ``self.tray_type_id.width_per_cell`` and
|
||||
``self.tray_type_id.depth_per_cell`` return the size of a cell in mm.
|
||||
* ``cell_location.posx`` and ``posy`` are the coordinate from the
|
||||
bottom-left of the tray.
|
||||
* ``cell_location.tray_cell_center_position()`` returns the central
|
||||
position of the cell in mm from the bottom-left of a tray. (distance
|
||||
from left, distance from bottom). Can be used for instance for
|
||||
highlighting the cell using a laser pointer.
|
||||
|
||||
Returns a message in bytes, that will be sent through
|
||||
``VerticalLiftShuttle._hardware_send_message()``.
|
||||
"""
|
||||
if self.vertical_lift_shuttle_id.hardware == "simulation":
|
||||
message = _("Opening tray {}.").format(self.name)
|
||||
if cell_location:
|
||||
from_left, from_bottom = cell_location.tray_cell_center_position()
|
||||
message += _("<br/>Laser pointer on x{} y{} ({}mm, {}mm)").format(
|
||||
cell_location.posx, cell_location.posy, from_left, from_bottom
|
||||
)
|
||||
return message.encode("utf-8")
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def fetch_vertical_lift_tray(self, cell_location=None):
|
||||
"""Send instructions to the vertical lift hardware to fetch a tray
|
||||
|
||||
Public method to use for:
|
||||
* fetch the vertical lift tray and presenting it to the operator
|
||||
(physically)
|
||||
* direct the laser pointer to the cell's location if set
|
||||
|
||||
Depending on the hardware, the laser pointer may not be implemented.
|
||||
|
||||
The actual implementation of the method goes in the private method
|
||||
``_hardware_vertical_lift_fetch_tray()``.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.vertical_lift_kind == "cell":
|
||||
if cell_location:
|
||||
raise ValueError(
|
||||
"cell_location cannot be set when the location is a cell."
|
||||
)
|
||||
tray = self.location_id
|
||||
tray.fetch_vertical_lift_tray(cell_location=self)
|
||||
elif self.vertical_lift_kind == "tray":
|
||||
self._hardware_vertical_lift_fetch_tray(cell_location=cell_location)
|
||||
else:
|
||||
raise exceptions.UserError(
|
||||
_("Cannot fetch a vertical lift tray on location %s") % (self.name,)
|
||||
)
|
||||
return True
|
||||
|
||||
def button_fetch_vertical_lift_tray(self):
|
||||
self.ensure_one()
|
||||
if self.vertical_lift_kind in ("cell", "tray"):
|
||||
self.fetch_vertical_lift_tray()
|
||||
return True
|
||||
|
||||
def button_release_vertical_lift_tray(self):
|
||||
self.ensure_one()
|
||||
if self.vertical_lift_kind:
|
||||
self.vertical_lift_shuttle_id.release_vertical_lift_tray()
|
||||
return True
|
||||
22
stock_vertical_lift/models/stock_move.py
Normal file
22
stock_vertical_lift/models/stock_move.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
if "state" in vals:
|
||||
# We cannot have fields to depends on to invalidate these computed
|
||||
# fields on vertical.lift.operation.*. But we know that when the
|
||||
# state of any move line changes, we can invalidate them as the
|
||||
# count of assigned move lines may change (and we track this in
|
||||
# stock.move, not stock.move.line, because the state of the lines
|
||||
# is a related to this one).
|
||||
models = ("vertical.lift.operation.pick", "vertical.lift.operation.put")
|
||||
for model in models:
|
||||
self.env[model].invalidate_cache(["number_of_ops", "number_of_ops_all"])
|
||||
return result
|
||||
21
stock_vertical_lift/models/stock_move_line.py
Normal file
21
stock_vertical_lift/models/stock_move_line.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
def fetch_vertical_lift_tray_source(self):
|
||||
self.ensure_one()
|
||||
self.location_id.fetch_vertical_lift_tray()
|
||||
# We reload mainly because otherwise, it would close
|
||||
# the popup. This action is provided by the OCA module
|
||||
# web_ir_actions_act_view_reload
|
||||
return {"type": "ir.actions.act_view_reload"}
|
||||
|
||||
def fetch_vertical_lift_tray_dest(self):
|
||||
self.ensure_one()
|
||||
self.location_dest_id.fetch_vertical_lift_tray()
|
||||
return {"type": "ir.actions.act_view_reload"}
|
||||
18
stock_vertical_lift/models/stock_quant.py
Normal file
18
stock_vertical_lift/models/stock_quant.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = "stock.quant"
|
||||
|
||||
def _update_available_quantity(self, *args, **kwargs):
|
||||
result = super()._update_available_quantity(*args, **kwargs)
|
||||
# We cannot have fields to depends on to invalidate this computed
|
||||
# fields on vertical.lift.operation.* models. But we know that when the
|
||||
# quantity of quant changes, we can invalidate the field
|
||||
models = ("vertical.lift.operation.pick", "vertical.lift.operation.put")
|
||||
for model in models:
|
||||
self.env[model].invalidate_cache(["tray_qty"])
|
||||
return result
|
||||
50
stock_vertical_lift/models/vertical_lift_command.py
Normal file
50
stock_vertical_lift/models/vertical_lift_command.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import logging
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VerticalLiftCommand(models.Model):
|
||||
_name = "vertical.lift.command"
|
||||
_order = "shuttle_id, name desc"
|
||||
_description = "commands sent to the shuttle"
|
||||
|
||||
def _default_name(self):
|
||||
return self.env["ir.sequence"].next_by_code("vertical.lift.command")
|
||||
|
||||
name = fields.Char(
|
||||
"Name", default=lambda s: s._default_name(), required=True, index=True
|
||||
)
|
||||
command = fields.Char(required=True)
|
||||
answer = fields.Char()
|
||||
error = fields.Char()
|
||||
shuttle_id = fields.Many2one("vertical.lift.shuttle", required=True)
|
||||
|
||||
def record_answer(self, answer):
|
||||
name = self._get_key(answer)
|
||||
record = self.search([("name", "=", name)], limit=1)
|
||||
if not record:
|
||||
_logger.error("unable to match answer to a command: %r", answer)
|
||||
raise exceptions.UserError(_("Unknown record %s") % name)
|
||||
record.answer = answer
|
||||
record.shuttle_id._hardware_response_callback(record)
|
||||
return record
|
||||
|
||||
def _get_key(self, answer):
|
||||
key = answer.split("|")[1:2]
|
||||
if key:
|
||||
return key[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if "name" not in values:
|
||||
name = self._get_key(values.get("command"))
|
||||
if name:
|
||||
values["name"] = name
|
||||
return super().create(vals_list)
|
||||
427
stock_vertical_lift/models/vertical_lift_operation_base.py
Normal file
427
stock_vertical_lift/models/vertical_lift_operation_base.py
Normal file
@@ -0,0 +1,427 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
from odoo.addons.base_sparse_field.models.fields import Serialized
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VerticalLiftOperationBase(models.AbstractModel):
|
||||
"""Base model for shuttle operations (pick, put, inventory)"""
|
||||
|
||||
_name = "vertical.lift.operation.base"
|
||||
_inherit = "barcodes.barcode_events_mixin"
|
||||
_description = "Vertical Lift Operation - Base"
|
||||
|
||||
name = fields.Char(related="shuttle_id.name", readonly=True)
|
||||
shuttle_id = fields.Many2one(
|
||||
comodel_name="vertical.lift.shuttle", required=True, readonly=True
|
||||
)
|
||||
location_id = fields.Many2one(related="shuttle_id.location_id", readonly=True)
|
||||
number_of_ops = fields.Integer(
|
||||
compute="_compute_number_of_ops", string="Number of Operations"
|
||||
)
|
||||
number_of_ops_all = fields.Integer(
|
||||
compute="_compute_number_of_ops_all",
|
||||
string="Number of Operations in all shuttles",
|
||||
)
|
||||
mode = fields.Selection(related="shuttle_id.mode", readonly=True)
|
||||
|
||||
state = fields.Selection(
|
||||
selection=lambda self: self._selection_states(),
|
||||
default=lambda self: self._initial_state,
|
||||
)
|
||||
_initial_state = None # to define in sub-classes
|
||||
|
||||
# if there is an action and it's returning True, the transition is done,
|
||||
# otherwise not
|
||||
Transition = namedtuple("Transition", "current_state next_state action direct_eval")
|
||||
# default values to None
|
||||
Transition.__new__.__defaults__ = (None,) * len(Transition._fields)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"shuttle_id_unique",
|
||||
"UNIQUE(shuttle_id)",
|
||||
"One pick can be run at a time for a shuttle.",
|
||||
)
|
||||
]
|
||||
|
||||
def _selection_states(self):
|
||||
return []
|
||||
|
||||
def _transitions(self):
|
||||
"""Define the transitions between the states
|
||||
|
||||
To set in sub-classes.
|
||||
It is a tuple of a ``Transition`` instances, evaluated in order.
|
||||
A transition has a source step, a destination step, a function and a
|
||||
flag ``direct_eval``.
|
||||
When the function returns True, the transition is applied, otherwise,
|
||||
the next transition matching the current step is evaluated.
|
||||
When a transition has no function, it is always applied.
|
||||
The flag ``direct_eval`` indicates that the workflow should directly
|
||||
evaluates again the transitions to reach the next step. It allows to
|
||||
use "virtual" steps that will never be kept for users but be used as
|
||||
router.
|
||||
|
||||
The initial state must be defined in the attribute ``_initial_state``.
|
||||
|
||||
The transition from a step to another are triggered by a call to
|
||||
``next_step()``. This method is called in several places:
|
||||
|
||||
* ``reset_steps()`` (called when the screen opens)
|
||||
* ``button_save()``, generally used to post the move
|
||||
* ``button_release()``, generally used to go to the next line
|
||||
* ``on_barcode_scanned()``, the calls to ``next_step()`` are to
|
||||
implement in sub-classed if the scanned barcode leads to the next
|
||||
step
|
||||
|
||||
Example of workflow described below:
|
||||
|
||||
::
|
||||
_initial_state = "noop"
|
||||
|
||||
def _selection_states(self):
|
||||
return [
|
||||
("noop", "No operations"),
|
||||
("scan_destination", "Scan New Destination Location"),
|
||||
("save", "Put goods in tray and save"),
|
||||
("release", "Release"),
|
||||
]
|
||||
|
||||
def _transitions(self):
|
||||
return (
|
||||
self.Transition(
|
||||
"noop",
|
||||
"scan_destination",
|
||||
lambda self: self.select_next_move_line()
|
||||
),
|
||||
self.Transition("scan_destination", "save"),
|
||||
self.Transition("save", "release"),
|
||||
self.Transition(
|
||||
"release",
|
||||
"scan_destination",
|
||||
lambda self: self.select_next_move_line()
|
||||
),
|
||||
self.Transition("release", "noop"),
|
||||
)
|
||||
|
||||
When we arrive on the screen, the ``on_screen_open`` methods resets the
|
||||
steps (``reset_steps()``). It ensures the current step is ``noop`` and
|
||||
directly tries to reach the next step (call to ``next_step()``).
|
||||
|
||||
It tries to go from ``noop`` to ``scan_destination``, calling
|
||||
``self.select_next_move_line()``. If the method finds a line, it
|
||||
returns True and the transition is applied, otherwise, the step stays
|
||||
``noop``.
|
||||
|
||||
The transitions from ``scan_destination`` and ``save`` and from
|
||||
``save`` and ``release`` are always applied when ``next_step()`` is
|
||||
called (``scan_destination`` → ``save`` from ``on_barcode_scanned``
|
||||
when a destination was found, ``save`` → ``release`` from the save
|
||||
button).
|
||||
|
||||
When ``button_release()`` is called, it calls ``next_step()`` which
|
||||
first evaluates ``self.select_next_move_line()``: if a move line remains, it
|
||||
goes to ``scan_destination``, otherwise to ``noop``.
|
||||
|
||||
"""
|
||||
return ()
|
||||
|
||||
def step(self):
|
||||
return self.state
|
||||
|
||||
def next_step(self, direct_eval=False):
|
||||
current_state = self.state
|
||||
for transition in self._transitions():
|
||||
if direct_eval and not transition.direct_eval:
|
||||
continue
|
||||
if transition.current_state != current_state:
|
||||
continue
|
||||
if not transition.action or transition.action(self):
|
||||
_logger.debug(
|
||||
"Transition %s → %s",
|
||||
transition.current_state,
|
||||
transition.next_state,
|
||||
)
|
||||
self.state = transition.next_state
|
||||
break
|
||||
# reevaluate the transitions if we have a new state with direct_eval transitions
|
||||
if self.state != current_state and any(
|
||||
transition.direct_eval
|
||||
for transition in self._transitions()
|
||||
if transition.current_state == self.state
|
||||
):
|
||||
self.next_step(direct_eval=True)
|
||||
|
||||
def reset_steps(self):
|
||||
if not self._initial_state:
|
||||
raise NotImplementedError("_initial_state must be defined")
|
||||
self.state = self._initial_state
|
||||
self.next_step()
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
# to implement in sub-classes
|
||||
|
||||
def on_screen_open(self):
|
||||
"""Called when the screen is opened"""
|
||||
self.reset_steps()
|
||||
|
||||
def onchange(self, values, field_name, field_onchange):
|
||||
if field_name == "_barcode_scanned":
|
||||
# _barcode_scanner is implemented (in the barcodes module) as an
|
||||
# onchange, which is really annoying when we want it to act as a
|
||||
# normal button and actually have side effect in the database
|
||||
# (update line, go to the next step, ...). This override shorts the
|
||||
# onchange call and calls the scanner method as a normal method.
|
||||
self.on_barcode_scanned(values["_barcode_scanned"])
|
||||
# We can't know which fields on_barcode_scanned changed, refresh
|
||||
# everything.
|
||||
return {"value": self.read()[0]}
|
||||
else:
|
||||
return super().onchange(values, field_name, field_onchange)
|
||||
|
||||
@api.depends()
|
||||
def _compute_number_of_ops(self):
|
||||
for record in self:
|
||||
record.number_of_ops = 0
|
||||
|
||||
@api.depends()
|
||||
def _compute_number_of_ops_all(self):
|
||||
for record in self:
|
||||
record.number_of_ops_all = 0
|
||||
|
||||
def action_open_screen(self):
|
||||
return self.shuttle_id.action_open_screen()
|
||||
|
||||
def action_menu(self):
|
||||
return self.shuttle_id.action_menu()
|
||||
|
||||
def action_manual_barcode(self):
|
||||
return self.shuttle_id.action_manual_barcode()
|
||||
|
||||
def process_current(self):
|
||||
"""Process the action (pick, put, ...)
|
||||
|
||||
To implement in sub-classes
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def button_save(self):
|
||||
"""Confirm the operation (set move to done, ...)"""
|
||||
self.ensure_one()
|
||||
if not self.step() == "save":
|
||||
return
|
||||
self.next_step()
|
||||
|
||||
def button_release(self):
|
||||
"""Release the operation, go to the next"""
|
||||
self.ensure_one()
|
||||
if not self.step() == "release":
|
||||
return
|
||||
self.next_step()
|
||||
|
||||
def _render_product_packagings(self, product):
|
||||
if not product:
|
||||
return ""
|
||||
return self.env["ir.qweb"].render(
|
||||
"stock_vertical_lift.packagings",
|
||||
self._prepare_values_for_product_packaging(product),
|
||||
)
|
||||
|
||||
def _prepare_values_for_product_packaging(self, product):
|
||||
return {"product": product}
|
||||
|
||||
def _get_tray_qty(self, product, location):
|
||||
quants = self.env["stock.quant"].search(
|
||||
[("location_id", "=", location.id), ("product_id", "=", product.id)]
|
||||
)
|
||||
return sum(quants.mapped("quantity"))
|
||||
|
||||
def _rainbow_man(self, message=None):
|
||||
if not message:
|
||||
message = _("Congrats, you cleared the queue!")
|
||||
return {
|
||||
"effect": {
|
||||
"fadeout": "slow",
|
||||
"message": message,
|
||||
"img_url": "/web/static/src/img/smile.svg",
|
||||
"type": "rainbow_man",
|
||||
}
|
||||
}
|
||||
|
||||
def _send_notification_refresh(self):
|
||||
"""Send a refresh notification
|
||||
|
||||
Generally, you want to call the method
|
||||
_send_notification_refresh() on VerticalLiftShuttle so you
|
||||
don't need to know the id of the current operation.
|
||||
|
||||
Other notifications can be implemented, they have to be
|
||||
added in static/src/js/vertical_lift.js and the message
|
||||
must contain an "action" and "params".
|
||||
"""
|
||||
self.ensure_one()
|
||||
channel = "notify_vertical_lift_screen"
|
||||
bus_message = {
|
||||
"action": "refresh",
|
||||
"params": {"model": self._name, "id": self.id},
|
||||
}
|
||||
self.env["bus.bus"].sendone(channel, bus_message)
|
||||
|
||||
|
||||
class VerticalLiftOperationTransfer(models.AbstractModel):
|
||||
"""Base model for shuttle pick and put operations"""
|
||||
|
||||
_name = "vertical.lift.operation.transfer"
|
||||
_inherit = "vertical.lift.operation.base"
|
||||
_description = "Vertical Lift Operation - Transfer"
|
||||
|
||||
current_move_line_id = fields.Many2one(
|
||||
comodel_name="stock.move.line", readonly=True
|
||||
)
|
||||
|
||||
tray_location_id = fields.Many2one(
|
||||
comodel_name="stock.location",
|
||||
compute="_compute_tray_data",
|
||||
string="Tray Location",
|
||||
)
|
||||
tray_name = fields.Char(compute="_compute_tray_data", string="Tray Name")
|
||||
tray_type_id = fields.Many2one(
|
||||
comodel_name="stock.location.tray.type",
|
||||
compute="_compute_tray_data",
|
||||
string="Tray Type",
|
||||
)
|
||||
tray_type_code = fields.Char(compute="_compute_tray_data", string="Tray Code")
|
||||
tray_x = fields.Integer(string="X", compute="_compute_tray_data")
|
||||
tray_y = fields.Integer(string="Y", compute="_compute_tray_data")
|
||||
tray_matrix = Serialized(string="Cells", compute="_compute_tray_data")
|
||||
tray_qty = fields.Float(string="Stock Quantity", compute="_compute_tray_qty")
|
||||
|
||||
# current operation information
|
||||
picking_id = fields.Many2one(
|
||||
related="current_move_line_id.picking_id", readonly=True
|
||||
)
|
||||
picking_origin = fields.Char(
|
||||
related="current_move_line_id.picking_id.origin", readonly=True
|
||||
)
|
||||
picking_partner_id = fields.Many2one(
|
||||
related="current_move_line_id.picking_id.partner_id", readonly=True
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
related="current_move_line_id.product_id", readonly=True
|
||||
)
|
||||
product_uom_id = fields.Many2one(
|
||||
related="current_move_line_id.product_uom_id", readonly=True
|
||||
)
|
||||
product_uom_qty = fields.Float(
|
||||
related="current_move_line_id.product_uom_qty", readonly=True
|
||||
)
|
||||
product_packagings = fields.Html(
|
||||
string="Packaging", compute="_compute_product_packagings"
|
||||
)
|
||||
qty_done = fields.Float(related="current_move_line_id.qty_done", readonly=True)
|
||||
lot_id = fields.Many2one(related="current_move_line_id.lot_id", readonly=True)
|
||||
location_dest_id = fields.Many2one(
|
||||
string="Destination",
|
||||
related="current_move_line_id.location_dest_id",
|
||||
readonly=False,
|
||||
)
|
||||
# TODO add a glue addon with product_expiry to add the field
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
self.env.user.notify_info(
|
||||
"Scanned barcode: {}. Not implemented.".format(barcode)
|
||||
)
|
||||
|
||||
@api.depends("current_move_line_id.product_id.packaging_ids")
|
||||
def _compute_product_packagings(self):
|
||||
for record in self:
|
||||
product = record.current_move_line_id.product_id
|
||||
if not product:
|
||||
record.product_packagings = ""
|
||||
continue
|
||||
content = self._render_product_packagings(product)
|
||||
record.product_packagings = content
|
||||
|
||||
@api.depends()
|
||||
def _compute_number_of_ops(self):
|
||||
for record in self:
|
||||
record.number_of_ops = record.count_move_lines_to_do()
|
||||
|
||||
@api.depends()
|
||||
def _compute_number_of_ops_all(self):
|
||||
for record in self:
|
||||
record.number_of_ops_all = record.count_move_lines_to_do_all()
|
||||
|
||||
@api.depends("tray_location_id", "current_move_line_id.product_id")
|
||||
def _compute_tray_qty(self):
|
||||
for record in self:
|
||||
if not (record.tray_location_id and record.current_move_line_id):
|
||||
record.tray_qty = 0.0
|
||||
continue
|
||||
product = record.current_move_line_id.product_id
|
||||
location = record.tray_location_id
|
||||
record.tray_qty = self._get_tray_qty(product, location)
|
||||
|
||||
@api.depends("current_move_line_id")
|
||||
def _compute_tray_data(self):
|
||||
for record in self:
|
||||
modes = {"pick": "location_id", "put": "location_dest_id"}
|
||||
location = record.current_move_line_id[modes[record.mode]]
|
||||
tray_type = location.location_id.tray_type_id
|
||||
# this is the current cell
|
||||
record.tray_location_id = location.id
|
||||
# name of the tray where the cell is
|
||||
record.tray_name = location.location_id.name
|
||||
record.tray_type_id = tray_type.id
|
||||
record.tray_type_code = tray_type.code
|
||||
record.tray_x = location.posx
|
||||
record.tray_y = location.posy
|
||||
record.tray_matrix = location.tray_matrix
|
||||
|
||||
def _domain_move_lines_to_do(self):
|
||||
# to implement in sub-classes
|
||||
return [("id", "=", 0)]
|
||||
|
||||
def _domain_move_lines_to_do_all(self):
|
||||
# to implement in sub-classes
|
||||
return [("id", "=", 0)]
|
||||
|
||||
def count_move_lines_to_do(self):
|
||||
"""Count move lines to process in current shuttles"""
|
||||
self.ensure_one()
|
||||
return self.env["stock.move.line"].search_count(self._domain_move_lines_to_do())
|
||||
|
||||
def count_move_lines_to_do_all(self):
|
||||
"""Count move lines to process in all shuttles"""
|
||||
self.ensure_one()
|
||||
return self.env["stock.move.line"].search_count(
|
||||
self._domain_move_lines_to_do_all()
|
||||
)
|
||||
|
||||
def process_current(self):
|
||||
line = self.current_move_line_id
|
||||
if line.state in ("assigned", "partially_available"):
|
||||
line.qty_done = line.product_qty
|
||||
line.move_id._action_done()
|
||||
return True
|
||||
|
||||
def fetch_tray(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def reset_steps(self):
|
||||
self.clear_current_move_line()
|
||||
super().reset_steps()
|
||||
|
||||
def clear_current_move_line(self):
|
||||
self.current_move_line_id = False
|
||||
return True
|
||||
266
stock_vertical_lift/models/vertical_lift_operation_inventory.py
Normal file
266
stock_vertical_lift/models/vertical_lift_operation_inventory.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
|
||||
from odoo.addons.base_sparse_field.models.fields import Serialized
|
||||
|
||||
# TODO handle autofocus + easy way to validate for the input field
|
||||
|
||||
|
||||
class VerticalLiftOperationInventory(models.Model):
|
||||
_name = "vertical.lift.operation.inventory"
|
||||
_inherit = "vertical.lift.operation.base"
|
||||
_description = "Vertical Lift Operation Inventory"
|
||||
|
||||
_initial_state = "noop"
|
||||
|
||||
def _selection_states(self):
|
||||
return [
|
||||
("noop", "No inventory in progress"),
|
||||
("quantity", "Inventory, please enter the amount"),
|
||||
("confirm_wrong_quantity", "The quantity does not match, are you sure?"),
|
||||
# save is never visible, but save and go to the next or noop directly
|
||||
("save", "Save"),
|
||||
# no need for release and save button here?
|
||||
# ("release", "Release"),
|
||||
]
|
||||
|
||||
def _transitions(self):
|
||||
return (
|
||||
self.Transition(
|
||||
"noop",
|
||||
"quantity",
|
||||
# transition only if inventory lines are found
|
||||
lambda self: self.select_next_inventory_line(),
|
||||
),
|
||||
self.Transition(
|
||||
"quantity", "save", lambda self: self._has_identical_quantity(),
|
||||
),
|
||||
self.Transition(
|
||||
"quantity",
|
||||
"confirm_wrong_quantity",
|
||||
lambda self: self._start_confirm_wrong_quantity(),
|
||||
),
|
||||
self.Transition(
|
||||
"confirm_wrong_quantity",
|
||||
"save",
|
||||
lambda self: self.quantity_input == self.last_quantity_input,
|
||||
),
|
||||
# if the confirmation of the quantity is different, cycle back to
|
||||
# the 'quantity' step
|
||||
self.Transition(
|
||||
"confirm_wrong_quantity",
|
||||
"quantity",
|
||||
lambda self: self._go_back_to_quantity_input(),
|
||||
),
|
||||
# go to quantity if we have lines in queue, otherwise, go to noop
|
||||
self.Transition(
|
||||
"save",
|
||||
"quantity",
|
||||
lambda self: self.process_current()
|
||||
and self.select_next_inventory_line(),
|
||||
# when we reach 'save', this transition is directly
|
||||
# evaluated
|
||||
direct_eval=True,
|
||||
),
|
||||
self.Transition(
|
||||
"save",
|
||||
"noop",
|
||||
lambda self: self.process_current()
|
||||
and self.clear_current_inventory_line(),
|
||||
# when we reach 'save', this transition is directly
|
||||
# evaluated
|
||||
direct_eval=True,
|
||||
),
|
||||
)
|
||||
|
||||
current_inventory_line_id = fields.Many2one(
|
||||
comodel_name="stock.inventory.line", readonly=True
|
||||
)
|
||||
|
||||
quantity_input = fields.Float()
|
||||
# if the quantity is wrong, user has to write 2 times
|
||||
# the same quantity to really confirm it's correct
|
||||
last_quantity_input = fields.Float()
|
||||
|
||||
tray_location_id = fields.Many2one(
|
||||
comodel_name="stock.location",
|
||||
compute="_compute_tray_data",
|
||||
string="Tray Location",
|
||||
)
|
||||
tray_name = fields.Char(compute="_compute_tray_data", string="Tray Name")
|
||||
tray_type_id = fields.Many2one(
|
||||
comodel_name="stock.location.tray.type",
|
||||
compute="_compute_tray_data",
|
||||
string="Tray Type",
|
||||
)
|
||||
tray_type_code = fields.Char(compute="_compute_tray_data", string="Tray Code")
|
||||
tray_x = fields.Integer(string="X", compute="_compute_tray_data")
|
||||
tray_y = fields.Integer(string="Y", compute="_compute_tray_data")
|
||||
tray_matrix = Serialized(string="Cells", compute="_compute_tray_data")
|
||||
tray_qty = fields.Float(string="Stock Quantity", compute="_compute_tray_qty")
|
||||
|
||||
# current operation information
|
||||
inventory_id = fields.Many2one(
|
||||
related="current_inventory_line_id.inventory_id", readonly=True
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
related="current_inventory_line_id.product_id", readonly=True
|
||||
)
|
||||
product_uom_id = fields.Many2one(
|
||||
related="current_inventory_line_id.product_uom_id", readonly=True
|
||||
)
|
||||
product_qty = fields.Float(
|
||||
related="current_inventory_line_id.product_qty", readonly=True
|
||||
)
|
||||
product_packagings = fields.Html(
|
||||
string="Packaging", compute="_compute_product_packagings"
|
||||
)
|
||||
package_id = fields.Many2one(
|
||||
related="current_inventory_line_id.package_id", readonly=True
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
related="current_inventory_line_id.prod_lot_id", readonly=True
|
||||
)
|
||||
|
||||
@api.depends("current_inventory_line_id")
|
||||
def _compute_tray_data(self):
|
||||
for record in self:
|
||||
location = record.current_inventory_line_id.location_id
|
||||
tray_type = location.location_id.tray_type_id
|
||||
# this is the current cell
|
||||
record.tray_location_id = location.id
|
||||
# name of the tray where the cell is
|
||||
record.tray_name = location.location_id.name
|
||||
record.tray_type_id = tray_type.id
|
||||
record.tray_type_code = tray_type.code
|
||||
record.tray_x = location.posx
|
||||
record.tray_y = location.posy
|
||||
record.tray_matrix = location.tray_matrix
|
||||
|
||||
@api.depends("current_inventory_line_id.product_id.packaging_ids")
|
||||
def _compute_product_packagings(self):
|
||||
for record in self:
|
||||
product = record.current_inventory_line_id.product_id
|
||||
if not product:
|
||||
record.product_packagings = ""
|
||||
continue
|
||||
content = self._render_product_packagings(product)
|
||||
record.product_packagings = content
|
||||
|
||||
@api.depends("tray_location_id", "current_inventory_line_id.product_id")
|
||||
def _compute_tray_qty(self):
|
||||
for record in self:
|
||||
if not (record.tray_location_id and record.current_inventory_line_id):
|
||||
record.tray_qty = 0.0
|
||||
continue
|
||||
product = record.current_inventory_line_id.product_id
|
||||
location = record.tray_location_id
|
||||
record.tray_qty = self._get_tray_qty(product, location)
|
||||
|
||||
def _compute_number_of_ops(self):
|
||||
for record in self:
|
||||
line_model = self.env["stock.inventory.line"]
|
||||
record.number_of_ops = line_model.search_count(
|
||||
self._domain_inventory_lines_to_do()
|
||||
)
|
||||
|
||||
def _compute_number_of_ops_all(self):
|
||||
for record in self:
|
||||
line_model = self.env["stock.inventory.line"]
|
||||
record.number_of_ops_all = line_model.search_count(
|
||||
self._domain_inventory_lines_to_do_all()
|
||||
)
|
||||
|
||||
def _domain_inventory_lines_to_do(self):
|
||||
return [
|
||||
("location_id", "child_of", self.location_id.id),
|
||||
("state", "=", "confirm"),
|
||||
("vertical_lift_done", "=", False),
|
||||
]
|
||||
|
||||
def _domain_inventory_lines_to_do_all(self):
|
||||
shuttle_locations = self.env["stock.location"].search(
|
||||
[("vertical_lift_kind", "=", "view")]
|
||||
)
|
||||
return [
|
||||
("location_id", "child_of", shuttle_locations.ids),
|
||||
("state", "=", "confirm"),
|
||||
("vertical_lift_done", "=", False),
|
||||
]
|
||||
|
||||
def reset_steps(self):
|
||||
self.clear_current_inventory_line()
|
||||
super().reset_steps()
|
||||
|
||||
def _has_identical_quantity(self):
|
||||
line = self.current_inventory_line_id
|
||||
return (
|
||||
float_compare(
|
||||
line.theoretical_qty,
|
||||
self.quantity_input,
|
||||
precision_rounding=line.product_uom_id.rounding,
|
||||
)
|
||||
== 0
|
||||
)
|
||||
|
||||
def _start_confirm_wrong_quantity(self):
|
||||
self.last_quantity_input = self.quantity_input
|
||||
self.quantity_input = 0.0
|
||||
return True
|
||||
|
||||
def _go_back_to_quantity_input(self):
|
||||
self.last_quantity_input = self.quantity_input
|
||||
self.quantity_input = 0.0
|
||||
return True
|
||||
|
||||
def clear_current_inventory_line(self):
|
||||
self.write(
|
||||
{
|
||||
"quantity_input": 0.0,
|
||||
"last_quantity_input": 0.0,
|
||||
"current_inventory_line_id": False,
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
def fetch_tray(self):
|
||||
location = self.current_inventory_line_id.location_id
|
||||
location.fetch_vertical_lift_tray()
|
||||
|
||||
def select_next_inventory_line(self):
|
||||
self.ensure_one()
|
||||
next_line = self.env["stock.inventory.line"].search(
|
||||
self._domain_inventory_lines_to_do(),
|
||||
limit=1,
|
||||
order="vertical_lift_tray_id, location_id, id",
|
||||
)
|
||||
self.current_inventory_line_id = next_line
|
||||
if next_line:
|
||||
self.fetch_tray()
|
||||
return bool(next_line)
|
||||
|
||||
def process_current(self):
|
||||
line = self.current_inventory_line_id
|
||||
if not line.vertical_lift_done:
|
||||
line.vertical_lift_done = True
|
||||
if self.quantity_input != line.product_qty:
|
||||
line.product_qty = self.quantity_input
|
||||
inventory = line.inventory_id
|
||||
if all(line.vertical_lift_done for line in inventory.line_ids):
|
||||
inventory.action_validate()
|
||||
self.quantity_input = self.last_quantity_input = 0.0
|
||||
return True
|
||||
|
||||
def button_save(self):
|
||||
self.ensure_one()
|
||||
if not self.step() in ("quantity", "confirm_wrong_quantity"):
|
||||
return
|
||||
self.next_step()
|
||||
if self.step() == "noop":
|
||||
# close the tray once everything is inventoried
|
||||
self.shuttle_id.release_vertical_lift_tray()
|
||||
# sorry not sorry
|
||||
return self._rainbow_man()
|
||||
90
stock_vertical_lift/models/vertical_lift_operation_pick.py
Normal file
90
stock_vertical_lift/models/vertical_lift_operation_pick.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, models
|
||||
|
||||
|
||||
class VerticalLiftOperationPick(models.Model):
|
||||
_name = "vertical.lift.operation.pick"
|
||||
_inherit = "vertical.lift.operation.transfer"
|
||||
_description = "Vertical Lift Operation Pick"
|
||||
|
||||
_initial_state = "noop"
|
||||
|
||||
def _selection_states(self):
|
||||
return [
|
||||
("noop", "No operations"),
|
||||
("scan_destination", "Scan New Destination Location"),
|
||||
("save", "Pick goods and save"),
|
||||
("release", "Release"),
|
||||
]
|
||||
|
||||
def _transitions(self):
|
||||
return (
|
||||
self.Transition(
|
||||
"noop", "scan_destination", lambda self: self.select_next_move_line()
|
||||
),
|
||||
self.Transition("scan_destination", "save"),
|
||||
self.Transition("save", "release", lambda self: self.process_current()),
|
||||
# go to scan_destination if we have lines in queue, otherwise, go to noop
|
||||
self.Transition(
|
||||
"release", "scan_destination", lambda self: self.select_next_move_line()
|
||||
),
|
||||
self.Transition("release", "noop"),
|
||||
)
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
if not self.current_move_line_id or self.current_move_line_id.state == "done":
|
||||
return
|
||||
if self.step() == "scan_destination":
|
||||
location = self.env["stock.location"].search([("barcode", "=", barcode)])
|
||||
if location:
|
||||
self.location_dest_id = location
|
||||
self.next_step()
|
||||
else:
|
||||
self.env.user.notify_warning(
|
||||
_("No location found for barcode {}").format(barcode)
|
||||
)
|
||||
|
||||
def _domain_move_lines_to_do(self):
|
||||
domain = [
|
||||
("state", "in", ("assigned", "partially_available")),
|
||||
("location_id", "child_of", self.location_id.id),
|
||||
]
|
||||
return domain
|
||||
|
||||
def _domain_move_lines_to_do_all(self):
|
||||
shuttle_locations = self.env["stock.location"].search(
|
||||
[("vertical_lift_kind", "=", "view")]
|
||||
)
|
||||
domain = [
|
||||
("state", "in", ("assigned", "partially_available")),
|
||||
("location_id", "child_of", shuttle_locations.ids),
|
||||
]
|
||||
return domain
|
||||
|
||||
def fetch_tray(self):
|
||||
self.current_move_line_id.fetch_vertical_lift_tray_source()
|
||||
|
||||
def select_next_move_line(self):
|
||||
self.ensure_one()
|
||||
next_move_line = self.env["stock.move.line"].search(
|
||||
self._domain_move_lines_to_do(), limit=1
|
||||
)
|
||||
self.current_move_line_id = next_move_line
|
||||
if next_move_line:
|
||||
self.fetch_tray()
|
||||
return True
|
||||
return False
|
||||
|
||||
def button_release(self):
|
||||
"""Release the operation, go to the next"""
|
||||
super().button_release()
|
||||
if self.step() == "noop":
|
||||
# we don't need to release (close) the tray until we have reached
|
||||
# the last line: the release is implicit when a next line is
|
||||
# fetched
|
||||
self.shuttle_id.release_vertical_lift_tray()
|
||||
# sorry not sorry
|
||||
return self._rainbow_man()
|
||||
181
stock_vertical_lift/models/vertical_lift_operation_put.py
Normal file
181
stock_vertical_lift/models/vertical_lift_operation_put.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, fields, models
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
class VerticalLiftOperationPut(models.Model):
|
||||
_name = "vertical.lift.operation.put"
|
||||
_inherit = "vertical.lift.operation.transfer"
|
||||
_description = "Vertical Lift Operation Put"
|
||||
|
||||
_initial_state = "scan_source"
|
||||
|
||||
def _selection_states(self):
|
||||
return [
|
||||
("scan_source", "Scan a package, product or lot to put-away"),
|
||||
("scan_tray_type", "Scan Tray Type"),
|
||||
("save", "Put goods in tray and save"),
|
||||
("release", "Release"),
|
||||
]
|
||||
|
||||
def _transitions(self):
|
||||
return (
|
||||
self.Transition(
|
||||
"scan_source",
|
||||
"scan_tray_type",
|
||||
# transition only if a move line has been selected
|
||||
# (by on_barcode_scanner)
|
||||
lambda self: self.current_move_line_id,
|
||||
),
|
||||
self.Transition("scan_tray_type", "save"),
|
||||
self.Transition("save", "release", lambda self: self.process_current()),
|
||||
self.Transition(
|
||||
"release", "scan_source", lambda self: self.clear_current_move_line()
|
||||
),
|
||||
)
|
||||
|
||||
def _domain_move_lines_to_do(self):
|
||||
domain = [
|
||||
("state", "in", ("assigned", "partially_available")),
|
||||
("location_dest_id", "child_of", self.location_id.id),
|
||||
]
|
||||
return domain
|
||||
|
||||
def _domain_move_lines_to_do_all(self):
|
||||
shuttle_locations = self.env["stock.location"].search(
|
||||
[("vertical_lift_kind", "=", "view")]
|
||||
)
|
||||
domain = [
|
||||
("state", "in", ("assigned", "partially_available")),
|
||||
("location_dest_id", "child_of", shuttle_locations.ids),
|
||||
]
|
||||
return domain
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
if self.step() == "scan_source":
|
||||
self._scan_source_action(barcode)
|
||||
elif self.step() in ("scan_tray_type", "save"):
|
||||
# note: we must be able to scan a different tray type when we are
|
||||
# in the save step too, in case we couldn't put it in the first one
|
||||
# for some reason.
|
||||
self._scan_tray_type_action(barcode)
|
||||
|
||||
def _scan_source_action(self, barcode):
|
||||
line = self._find_move_line(barcode)
|
||||
if line:
|
||||
self.current_move_line_id = line
|
||||
self.next_step()
|
||||
else:
|
||||
self.env.user.notify_warning(
|
||||
_("No move line found for barcode {}").format(barcode)
|
||||
)
|
||||
|
||||
def _scan_tray_type_action(self, barcode):
|
||||
tray_type = self._find_tray_type(barcode)
|
||||
if tray_type:
|
||||
if self._assign_available_cell(tray_type):
|
||||
self.fetch_tray()
|
||||
if self.step() == "scan_tray_type":
|
||||
# when we are in "save" step, stay here
|
||||
self.next_step()
|
||||
else:
|
||||
self.env.user.notify_warning(
|
||||
_('No free space for tray type "{}" in this shuttle.').format(
|
||||
tray_type.display_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.env.user.notify_warning(
|
||||
_("No tray type found for barcode {}").format(barcode)
|
||||
)
|
||||
|
||||
def _find_tray_type(self, barcode):
|
||||
return self.env["stock.location.tray.type"].search(
|
||||
[("code", "=", barcode)], limit=1
|
||||
)
|
||||
|
||||
def _find_move_line(self, barcode):
|
||||
package = self.env["stock.quant.package"].search([("name", "=", barcode)])
|
||||
if package:
|
||||
return self._find_move_line_for_package(package)
|
||||
|
||||
lot = self.env["stock.production.lot"].search([("name", "=", barcode)])
|
||||
if lot:
|
||||
return self._find_move_line_for_lot(package)
|
||||
|
||||
product = self.env["product.product"].search([("barcode", "=", barcode)])
|
||||
if not product:
|
||||
packaging = self.env["product.packaging"].search(
|
||||
[("product_id", "!=", False), ("barcode", "=", barcode)]
|
||||
)
|
||||
product = packaging.product_id
|
||||
if product:
|
||||
return self._find_move_line_for_product(product)
|
||||
|
||||
def _find_move_line_for_package(self, package):
|
||||
domain = AND(
|
||||
[self._domain_move_lines_to_do_all(), [("package_id", "in", package.ids)]]
|
||||
)
|
||||
return self.env["stock.move.line"].search(domain, limit=1)
|
||||
|
||||
def _find_move_line_for_lot(self, lot):
|
||||
domain = AND(
|
||||
[
|
||||
self._domain_move_lines_to_do_all(),
|
||||
[
|
||||
("lot_id", "=", lot.id),
|
||||
# if the lot is in a package, the package must be scanned
|
||||
("package_id", "=", False),
|
||||
],
|
||||
]
|
||||
)
|
||||
return self.env["stock.move.line"].search(domain, limit=1)
|
||||
|
||||
def _find_move_line_for_product(self, product):
|
||||
domain = AND(
|
||||
[
|
||||
self._domain_move_lines_to_do_all(),
|
||||
[
|
||||
("product_id", "=", product.id),
|
||||
# if the lot is in a package, the package must be scanned
|
||||
("package_id", "=", False),
|
||||
],
|
||||
]
|
||||
)
|
||||
return self.env["stock.move.line"].search(domain, limit=1)
|
||||
|
||||
def _check_tray_type(self, barcode):
|
||||
location = self.current_move_line_id.location_dest_id
|
||||
tray_type = location.cell_in_tray_type_id
|
||||
return barcode == tray_type.code
|
||||
|
||||
def _assign_available_cell(self, tray_type):
|
||||
locations = self.env["stock.location"].search(
|
||||
[
|
||||
("id", "child_of", self.location_id.id),
|
||||
("cell_in_tray_type_id", "=", tray_type.id),
|
||||
]
|
||||
)
|
||||
location = fields.first(
|
||||
locations.filtered(lambda loc: not loc.tray_cell_contains_stock)
|
||||
)
|
||||
if location:
|
||||
self.current_move_line_id.location_dest_id = location
|
||||
self.current_move_line_id.package_level_id.location_dest_id = location
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_tray(self):
|
||||
self.current_move_line_id.fetch_vertical_lift_tray_dest()
|
||||
|
||||
def button_release(self):
|
||||
# release (close) the tray each time, because for put-away, we
|
||||
# never know if the operator will scan another line or not
|
||||
self.shuttle_id.release_vertical_lift_tray()
|
||||
super().button_release()
|
||||
if self.count_move_lines_to_do_all() == 0:
|
||||
# sorry not sorry
|
||||
return self._rainbow_man()
|
||||
277
stock_vertical_lift/models/vertical_lift_shuttle.py
Normal file
277
stock_vertical_lift/models/vertical_lift_shuttle.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import logging
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VerticalLiftShuttle(models.Model):
|
||||
_name = "vertical.lift.shuttle"
|
||||
_inherit = "barcodes.barcode_events_mixin"
|
||||
_description = "Vertical Lift Shuttle"
|
||||
|
||||
name = fields.Char()
|
||||
mode = fields.Selection(
|
||||
[("pick", "Pick"), ("put", "Put"), ("inventory", "Inventory")],
|
||||
default="pick",
|
||||
required=True,
|
||||
)
|
||||
location_id = fields.Many2one(
|
||||
comodel_name="stock.location",
|
||||
required=True,
|
||||
domain="[('vertical_lift_kind', '=', 'shuttle')]",
|
||||
ondelete="restrict",
|
||||
help="The Shuttle source location for Pick operations "
|
||||
"and destination location for Put operations.",
|
||||
)
|
||||
hardware = fields.Selection(
|
||||
selection="_selection_hardware", default="simulation", required=True
|
||||
)
|
||||
server = fields.Char(help="hostname or IP address of the server")
|
||||
port = fields.Integer(
|
||||
help="network port of the server on which to send the message"
|
||||
)
|
||||
use_tls = fields.Boolean(
|
||||
help="set this if the server expects TLS wrapped communication"
|
||||
)
|
||||
command_ids = fields.One2many(
|
||||
"vertical.lift.command", "shuttle_id", string="Hardware commands"
|
||||
)
|
||||
_sql_constraints = [
|
||||
(
|
||||
"location_id_unique",
|
||||
"UNIQUE(location_id)",
|
||||
"You cannot have two shuttles using the same location.",
|
||||
)
|
||||
]
|
||||
|
||||
def _selection_hardware(self):
|
||||
return [("simulation", "Simulation")]
|
||||
|
||||
@property
|
||||
def _model_for_mode(self):
|
||||
return {
|
||||
"pick": "vertical.lift.operation.pick",
|
||||
"put": "vertical.lift.operation.put",
|
||||
"inventory": "vertical.lift.operation.inventory",
|
||||
}
|
||||
|
||||
@property
|
||||
def _screen_view_for_mode(self):
|
||||
return {
|
||||
"pick": ("stock_vertical_lift." "vertical_lift_operation_pick_screen_view"),
|
||||
"put": ("stock_vertical_lift." "vertical_lift_operation_put_screen_view"),
|
||||
"inventory": (
|
||||
"stock_vertical_lift." "vertical_lift_operation_inventory_screen_view"
|
||||
),
|
||||
}
|
||||
|
||||
def _hardware_send_message(self, payload):
|
||||
"""default implementation for message sending
|
||||
|
||||
If in hardware is 'simulation' then display a simple message.
|
||||
Otherwise defaults to connecting to server:port using a TCP socket
|
||||
(optionnally wrapped with TLS) and sending the payload.
|
||||
|
||||
:param payload: a bytes object containing the payload
|
||||
|
||||
"""
|
||||
self.ensure_one()
|
||||
_logger.info("send %r", payload)
|
||||
command_values = {"shuttle_id": self.id, "command": payload.decode()}
|
||||
|
||||
self.env["vertical.lift.command"].sudo().create(command_values)
|
||||
if self.hardware == "simulation":
|
||||
self.env.user.notify_info(message=payload, title=_("Lift Simulation"))
|
||||
return True
|
||||
else:
|
||||
conn = self._hardware_get_server_connection()
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
size = conn.send(payload[offset:])
|
||||
offset += size
|
||||
if offset >= len(payload) or not size:
|
||||
break
|
||||
finally:
|
||||
self._hardware_release_server_connection(conn)
|
||||
|
||||
def _hardware_response_callback(self, command):
|
||||
"""should be called when a response is received from the hardware
|
||||
|
||||
:param response: a string
|
||||
"""
|
||||
success = self._check_server_response(command)
|
||||
self._send_notification_refresh(success)
|
||||
|
||||
def _check_server_response(self, command):
|
||||
"""Use this to check if the response is a success or a failure
|
||||
|
||||
:param payload: the payload sent
|
||||
:param response: the response received
|
||||
:return: True if the response is a succes, False otherwise
|
||||
"""
|
||||
return True
|
||||
|
||||
def _hardware_release_server_connection(self, conn):
|
||||
conn.close()
|
||||
|
||||
def _hardware_get_server_connection(self):
|
||||
"""This implementation will yield a new connection to the server
|
||||
and close() it when exiting the context.
|
||||
Override to match the communication protocol of your hardware"""
|
||||
conn = socket.create_connection((self.server, self.port))
|
||||
if self.use_tls:
|
||||
ctx = ssl.create_default_context()
|
||||
self._hardware_update_tls_context(ctx)
|
||||
conntls = ctx.wrap_socket(conn, server_hostname=self.server)
|
||||
return conntls
|
||||
else:
|
||||
return conn
|
||||
|
||||
def _hardware_update_tls_context(self, context):
|
||||
"""Update the TLS context, e.g. to add a client certificate.
|
||||
|
||||
This method does nothing, override to match your communication
|
||||
protocol."""
|
||||
pass
|
||||
|
||||
def _operation_for_mode(self):
|
||||
model = self._model_for_mode[self.mode]
|
||||
record = self.env[model].search([("shuttle_id", "=", self.id)])
|
||||
if not record:
|
||||
record = self.env[model].create({"shuttle_id": self.id})
|
||||
return record
|
||||
|
||||
def action_open_screen(self):
|
||||
self.ensure_one()
|
||||
assert self.mode in ("pick", "put", "inventory")
|
||||
screen_xmlid = self._screen_view_for_mode[self.mode]
|
||||
operation = self._operation_for_mode()
|
||||
operation.on_screen_open()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": operation._name,
|
||||
"views": [[self.env.ref(screen_xmlid).id, "form"]],
|
||||
"res_id": operation.id,
|
||||
"target": "fullscreen",
|
||||
"flags": {
|
||||
"withControlPanel": False,
|
||||
"form_view_initial_mode": "edit",
|
||||
"no_breadcrumbs": True,
|
||||
},
|
||||
}
|
||||
|
||||
def action_menu(self):
|
||||
menu_xmlid = "stock_vertical_lift.vertical_lift_shuttle_form_menu"
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "vertical.lift.shuttle",
|
||||
"views": [[self.env.ref(menu_xmlid).id, "form"]],
|
||||
"name": _("Menu"),
|
||||
"target": "new",
|
||||
"res_id": self.id,
|
||||
}
|
||||
|
||||
def action_back_to_settings(self):
|
||||
self.release_vertical_lift_tray()
|
||||
action_xmlid = "stock_vertical_lift.vertical_lift_shuttle_action"
|
||||
action = self.env.ref(action_xmlid).read()[0]
|
||||
action["target"] = "main"
|
||||
return action
|
||||
|
||||
def action_manual_barcode(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "vertical.lift.shuttle.manual.barcode",
|
||||
"view_mode": "form",
|
||||
"name": _("Barcode"),
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
# TODO: should the mode be changed on all the shuttles at the same time?
|
||||
def switch_pick(self):
|
||||
self.mode = "pick"
|
||||
self.release_vertical_lift_tray()
|
||||
return self.action_open_screen()
|
||||
|
||||
def switch_put(self):
|
||||
self.mode = "put"
|
||||
self.release_vertical_lift_tray()
|
||||
return self.action_open_screen()
|
||||
|
||||
def switch_inventory(self):
|
||||
self.mode = "inventory"
|
||||
self.release_vertical_lift_tray()
|
||||
return self.action_open_screen()
|
||||
|
||||
def _hardware_vertical_lift_release_tray_payload(self):
|
||||
"""Prepare "release" message to be sent to the vertical lift hardware
|
||||
|
||||
Private method, this is where the implementation actually happens.
|
||||
Addons can add their instructions based on the hardware used for
|
||||
this location.
|
||||
|
||||
The hardware used for a location can be found in:
|
||||
|
||||
``self.hardware``
|
||||
|
||||
Each addon can implement its own mechanism depending of this value
|
||||
and must call ``super``.
|
||||
|
||||
The method must send the command to the vertical lift to release (close)
|
||||
the tray.
|
||||
|
||||
Returns a message in bytes, that will be sent through
|
||||
``VerticalLiftShuttle._hardware_send_message()``.
|
||||
"""
|
||||
if self.hardware == "simulation":
|
||||
message = _("Releasing tray")
|
||||
return message.encode("utf-8")
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def release_vertical_lift_tray(self):
|
||||
"""Send instructions to the vertical lift hardware to close trays
|
||||
|
||||
The actual implementation of the method goes in the private method
|
||||
``_hardware_vertical_lift_release_tray()``.
|
||||
"""
|
||||
self.ensure_one()
|
||||
payload = self._hardware_vertical_lift_release_tray_payload()
|
||||
return self._hardware_send_message(payload)
|
||||
|
||||
def _send_notification_refresh(self, success):
|
||||
"""Send a refresh notification to the current opened screen
|
||||
|
||||
The form controller on the front-end side will instantaneously
|
||||
refresh the form with the latest committed data.
|
||||
|
||||
It can be used for instance after a vertical lift hardware
|
||||
event occurred to inform the user on their screen.
|
||||
|
||||
The method is private only to prevent xml/rpc calls to
|
||||
interact with the screen.
|
||||
"""
|
||||
# XXX do we want to do something special in the notification?
|
||||
self._operation_for_mode()._send_notification_refresh()
|
||||
|
||||
|
||||
class VerticalLiftShuttleManualBarcode(models.TransientModel):
|
||||
_name = "vertical.lift.shuttle.manual.barcode"
|
||||
_description = "Action to input a barcode"
|
||||
|
||||
barcode = fields.Char(string="Barcode")
|
||||
|
||||
def button_save(self):
|
||||
active_id = self.env.context.get("active_id")
|
||||
model = self.env.context.get("active_model")
|
||||
record = self.env[model].browse(active_id).exists()
|
||||
if not record:
|
||||
return
|
||||
if self.barcode:
|
||||
record.on_barcode_scanned(self.barcode)
|
||||
48
stock_vertical_lift/readme/CONFIGURE.rst
Normal file
48
stock_vertical_lift/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
General
|
||||
~~~~~~~
|
||||
|
||||
In Inventory Settings, you must have:
|
||||
|
||||
* Storage Locations
|
||||
* Multi-Warehouses
|
||||
* Multi-Step Routes
|
||||
|
||||
Locations
|
||||
~~~~~~~~~
|
||||
|
||||
Additional configuration parameters are added in Locations:
|
||||
|
||||
* Sub-locations of a location with the "Is a Vertical Lift View Location"
|
||||
activated are considered as "Shuttles". A shuttle is a vertical lift shelf.
|
||||
* Sub-locations of shuttles are considered as "Trays", which is a tier of a
|
||||
shuttle. When a tray is created, a tray type must be selected. When saved, the
|
||||
tray location will automatically create as many sub-locations - called
|
||||
"Cells" - as the tray type contains.
|
||||
* The tray type of a tray can be changed as long as none of its cell contains
|
||||
products. When changed, it archives the cells and creates new ones as
|
||||
configured on the new tray type.
|
||||
|
||||
Tray types
|
||||
~~~~~~~~~~
|
||||
|
||||
Tray types can be configured in the Inventory settings.
|
||||
A tray type defines how much cells a tray can hold. It is a square or rectangle
|
||||
matrix of n cols * m rows.
|
||||
|
||||
Vertical Lift Shuttles
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created
|
||||
in Odoo for each physical shuttle. Depending of the subsidiary addons installed
|
||||
(eg. Kardex), different options may be required (host address, ...). The base
|
||||
addon only includes shuttles of kind "simulation" which will not send orders to
|
||||
the hardware.
|
||||
|
||||
Put-away configuration
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to use put-away in the vertical lift, the Receipts must have the
|
||||
vertical lift view as destination. E.g. create put-away rules on the products
|
||||
so when they arrive in WH/Stock, they are stored in WH/Stock/Vertical Lift. On
|
||||
the put-away screen, when scanning the tray type to store, the destination will
|
||||
be updated with an available cell of the same tray type in the current shuttle.
|
||||
1
stock_vertical_lift/readme/CONTRIBUTORS.rst
Normal file
1
stock_vertical_lift/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
3
stock_vertical_lift/readme/DESCRIPTION.rst
Normal file
3
stock_vertical_lift/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
Add configuration and dedicated screens to work with Vertical Lift
|
||||
systems (such as Kardex Remstar, Modula, ...). Drivers for controlling
|
||||
the lifts physically must be added by additional addons.
|
||||
8
stock_vertical_lift/readme/ROADMAP.rst
Normal file
8
stock_vertical_lift/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
* Complete screen workflows (currently enough for a demo, not for production)
|
||||
* Inventory: find a way to have a nice autofocus for quantity, still compatible
|
||||
with barcode scanner (Odoo disables the autofocus when using barcode, which
|
||||
makes sense)
|
||||
* Put-away: handle packages
|
||||
* Handle "multi-shuttle" put-away
|
||||
* Create glue module for product_expiry
|
||||
* Challenge the save + release buttons and workflow
|
||||
7
stock_vertical_lift/security/ir.model.access.csv
Normal file
7
stock_vertical_lift/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_vertical_lift_shuttle_stock_user,access_vertical_lift_shuttle stock user,model_vertical_lift_shuttle,stock.group_stock_user,1,0,0,0
|
||||
access_vertical_lift_shuttle_manager,access_vertical_lift_shuttle stock manager,model_vertical_lift_shuttle,stock.group_stock_manager,1,1,1,1
|
||||
access_vertical_lift_operation_pick_stock_user,access_vertical_lift_operation_pick stock user,model_vertical_lift_operation_pick,stock.group_stock_user,1,1,1,1
|
||||
access_vertical_lift_operation_put_stock_user,access_vertical_lift_operation_put stock user,model_vertical_lift_operation_put,stock.group_stock_user,1,1,1,1
|
||||
access_vertical_lift_operation_inventory_stock_user,access_vertical_lift_operation_inventory stock user,model_vertical_lift_operation_inventory,stock.group_stock_user,1,1,1,1
|
||||
access_vertical_lift_command,vertical_lift_command,model_vertical_lift_command,base.group_user,1,0,0,0
|
||||
|
490
stock_vertical_lift/static/description/index.html
Normal file
490
stock_vertical_lift/static/description/index.html
Normal file
@@ -0,0 +1,490 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
|
||||
<title>Vertical Lift</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="vertical-lift">
|
||||
<h1 class="title">Vertical Lift</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_vertical_lift"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-12-0/stock-logistics-warehouse-12-0-stock_vertical_lift"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/153/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||
<p>Add configuration and dedicated screens to work with Vertical Lift
|
||||
systems (such as Kardex Remstar, Modula, …). Drivers for controlling
|
||||
the lifts physically must be added by additional addons.</p>
|
||||
<div class="admonition important">
|
||||
<p class="first admonition-title">Important</p>
|
||||
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
<a class="reference external" href="https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/oca_module_lifecycle_development_status.rst">More details on development status</a></p>
|
||||
</div>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a><ul>
|
||||
<li><a class="reference internal" href="#general" id="id2">General</a></li>
|
||||
<li><a class="reference internal" href="#locations" id="id3">Locations</a></li>
|
||||
<li><a class="reference internal" href="#tray-types" id="id4">Tray types</a></li>
|
||||
<li><a class="reference internal" href="#vertical-lift-shuttles" id="id5">Vertical Lift Shuttles</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="id6">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id7">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id8">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id9">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id10">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id11">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
|
||||
<div class="section" id="general">
|
||||
<h2><a class="toc-backref" href="#id2">General</a></h2>
|
||||
<p>In Inventory Settings, you must have:</p>
|
||||
<blockquote>
|
||||
<ul class="simple">
|
||||
<li>Storage Locations</li>
|
||||
<li>Multi-Warehouses</li>
|
||||
<li>Multi-Step Routes</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="section" id="locations">
|
||||
<h2><a class="toc-backref" href="#id3">Locations</a></h2>
|
||||
<p>Additional configuration parameters are added in Locations:</p>
|
||||
<ul class="simple">
|
||||
<li>Sub-locations of a location with the “Is a Vertical Lift View Location”
|
||||
activated are considered as “Shuttles”. A shuttle is a vertical lift shelf.</li>
|
||||
<li>Sub-locations of shuttles are considered as “Trays”, which is a tier of a
|
||||
shuttle. When a tray is created, a tray type must be selected. When saved, the
|
||||
tray location will automatically create as many sub-locations - called
|
||||
“Cells” - as the tray type contains.</li>
|
||||
<li>The tray type of a tray can be changed as long as none of its cell contains
|
||||
products. When changed, it archives the cells and creates new ones as
|
||||
configured on the new tray type.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="tray-types">
|
||||
<h2><a class="toc-backref" href="#id4">Tray types</a></h2>
|
||||
<p>Tray types can be configured in the Inventory settings.
|
||||
A tray type defines how much cells a tray can hold. It is a square or rectangle
|
||||
matrix of n cols * m rows.</p>
|
||||
</div>
|
||||
<div class="section" id="vertical-lift-shuttles">
|
||||
<h2><a class="toc-backref" href="#id5">Vertical Lift Shuttles</a></h2>
|
||||
<p>The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created
|
||||
in Odoo for each physical shuttle. Depending of the subsidiary addons installed
|
||||
(eg. Kardex), different options may be required (host address, …). The base
|
||||
addon only includes shuttles of kind “simulation” which will not send orders to
|
||||
the hardware.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#id6">Known issues / Roadmap</a></h1>
|
||||
<ul class="simple">
|
||||
<li>Extract the tray types and matrix widget in a module, they can be used
|
||||
alone without vertical lift</li>
|
||||
<li>Consider merging the ‘vertical_lift_kind’ with the kind added by
|
||||
stock_location_zone</li>
|
||||
<li>Complete Pick screen and workflow (currently enough for a demo, not for production)</li>
|
||||
<li>Implement Put-away screen and workflow</li>
|
||||
<li>Implement Inventory screen and workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id7">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vertical_lift%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#id8">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id9">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Camptocamp</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id10">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Guewen Baconnier <<a class="reference external" href="mailto:guewen.baconnier@camptocamp.com">guewen.baconnier@camptocamp.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#id11">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_vertical_lift">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>
|
||||
79
stock_vertical_lift/static/src/js/vertical_lift.js
Normal file
79
stock_vertical_lift/static/src/js/vertical_lift.js
Normal file
@@ -0,0 +1,79 @@
|
||||
odoo.define("stock_vertical_lift.vertical_lift", function(require) {
|
||||
"use strict";
|
||||
|
||||
var KanbanRecord = require("web.KanbanRecord");
|
||||
var FormController = require("web.FormController");
|
||||
|
||||
KanbanRecord.include({
|
||||
_openRecord: function() {
|
||||
if (
|
||||
this.modelName === "vertical.lift.shuttle" &&
|
||||
this.$el.hasClass("open_shuttle_screen")
|
||||
) {
|
||||
var self = this;
|
||||
this._rpc({
|
||||
method: "action_open_screen",
|
||||
model: self.modelName,
|
||||
args: [self.id],
|
||||
}).then(function(action) {
|
||||
self.trigger_up("do_action", {action: action});
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
FormController.include({
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
if (this.modelName.startsWith("vertical.lift.operation.")) {
|
||||
this.call("bus_service", "addChannel", "notify_vertical_lift_screen");
|
||||
this.call(
|
||||
"bus_service",
|
||||
"on",
|
||||
"notification",
|
||||
this,
|
||||
this.vlift_bus_notification
|
||||
);
|
||||
this.call("bus_service", "startPolling");
|
||||
}
|
||||
},
|
||||
vlift_bus_notification: function(notifications) {
|
||||
var self = this;
|
||||
_.each(notifications, function(notification) {
|
||||
var channel = notification[0];
|
||||
var message = notification[1];
|
||||
if (channel === "notify_vertical_lift_screen") {
|
||||
switch (message.action) {
|
||||
case "refresh":
|
||||
self.vlift_bus_action_refresh(message.params);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
vlift_bus_action_refresh: function(params) {
|
||||
var selectedIds = this.getSelectedIds();
|
||||
if (!selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
var currentId = selectedIds[0];
|
||||
if (params.id === currentId && params.model === this.modelName) {
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
destroy: function() {
|
||||
if (this.modelName.startsWith("vertical.lift.operation.")) {
|
||||
this.call(
|
||||
"bus_service",
|
||||
"deleteChannel",
|
||||
"notify_vertical_lift_screen"
|
||||
);
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
135
stock_vertical_lift/static/src/scss/vertical_lift.scss
Normal file
135
stock_vertical_lift/static/src/scss/vertical_lift.scss
Normal file
@@ -0,0 +1,135 @@
|
||||
.o_web_client.o_fullscreen {
|
||||
$o-shuttle-padding: $o-horizontal-padding;
|
||||
|
||||
.o_form_view.o_vlift_shuttle {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0;
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 1em;
|
||||
padding: 1em;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.o_shuttle_header {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: $o-shuttle-padding;
|
||||
}
|
||||
|
||||
.o_shuttle_header_content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
font-size: 2em;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
width: 33%;
|
||||
|
||||
&.o_shuttle_header_right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.o_shuttle_actions {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
font-size: 1.2em;
|
||||
padding: $o-shuttle-padding * 0.5;
|
||||
}
|
||||
|
||||
.o_shuttle_operation {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
padding: 0.5em;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.o_shuttle_content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
|
||||
&.o_shuttle_content_right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.o_shuttle_data {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: $o-shuttle-padding * 0.5;
|
||||
|
||||
.o_shuttle_data_content {
|
||||
flex-flow: row nowrap;
|
||||
font-size: 1.2em;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
|
||||
&.o_shuttle_tray {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.o_group {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_location_tray_matrix {
|
||||
width: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_shuttle_highlight {
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_vlift_shuttle_menu {
|
||||
.btn {
|
||||
margin-bottom: $o-shuttle-padding;
|
||||
padding: 1em;
|
||||
font-size: 2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.o_vlift_shuttle_popup {
|
||||
table tr {
|
||||
line-height: 3;
|
||||
font-size: 1.1em;
|
||||
|
||||
.o_list_record_remove {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_char {
|
||||
padding: 1em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 1em;
|
||||
font-size: 2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
footer .btn {
|
||||
padding: 1em;
|
||||
font-size: 2em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
4
stock_vertical_lift/tests/__init__.py
Normal file
4
stock_vertical_lift/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import test_location
|
||||
from . import test_inventory
|
||||
from . import test_pick
|
||||
from . import test_put
|
||||
176
stock_vertical_lift/tests/common.py
Normal file
176
stock_vertical_lift/tests/common.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _
|
||||
|
||||
from odoo.addons.stock_location_tray.tests import common
|
||||
|
||||
|
||||
class VerticalLiftCase(common.LocationTrayTypeCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.shuttle = cls.env.ref(
|
||||
"stock_vertical_lift.stock_vertical_lift_demo_shuttle_1"
|
||||
)
|
||||
cls.product_socks = cls.env.ref("stock_vertical_lift.product_running_socks")
|
||||
cls.product_recovery = cls.env.ref("stock_vertical_lift.product_recovery_socks")
|
||||
cls.vertical_lift_loc = cls.env.ref(
|
||||
"stock_vertical_lift.stock_location_vertical_lift"
|
||||
)
|
||||
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||
cls.customers_location = cls.env.ref("stock.stock_location_customers")
|
||||
cls.location_1a = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a"
|
||||
)
|
||||
cls.location_1a_x1y1 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x1y1"
|
||||
)
|
||||
cls.location_1a_x2y1 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x2y1"
|
||||
)
|
||||
cls.location_1a_x3y1 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y1"
|
||||
)
|
||||
cls.location_1a_x1y2 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x1y2"
|
||||
)
|
||||
cls.location_1b = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1b"
|
||||
)
|
||||
cls.location_1b_x1y1 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1b_x1y1"
|
||||
)
|
||||
cls.location_1b_x1y2 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1b_x1y2"
|
||||
)
|
||||
cls.location_2a = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a"
|
||||
)
|
||||
cls.location_2a_x1y1 = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a_x1y1"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _update_qty_in_location(
|
||||
cls, location, product, quantity, package=None, lot=None
|
||||
):
|
||||
quants = cls.env["stock.quant"]._gather(
|
||||
product, location, lot_id=lot, package_id=package, strict=True
|
||||
)
|
||||
# this method adds the quantity to the current quantity, so remove it
|
||||
quantity -= sum(quants.mapped("quantity"))
|
||||
cls.env["stock.quant"]._update_available_quantity(
|
||||
product, location, quantity, package_id=package, lot_id=lot
|
||||
)
|
||||
|
||||
def _open_screen(self, mode, shuttle=None):
|
||||
getattr(shuttle or self.shuttle, "switch_{}".format(mode))()
|
||||
# opening the screen can do some initialization for the steps
|
||||
action = (shuttle or self.shuttle).action_open_screen()
|
||||
return self.env[action["res_model"]].browse(action["res_id"])
|
||||
|
||||
@classmethod
|
||||
def _create_simple_picking_out(cls, product, quantity):
|
||||
stock_loc = cls.env.ref("stock.stock_location_stock")
|
||||
customer_loc = cls.env.ref("stock.stock_location_customers")
|
||||
picking_type = cls.env.ref("stock.picking_type_out")
|
||||
partner = cls.env.ref("base.res_partner_1")
|
||||
return cls.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": picking_type.id,
|
||||
"partner_id": partner.id,
|
||||
"location_id": stock_loc.id,
|
||||
"location_dest_id": customer_loc.id,
|
||||
"move_lines": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": product.name,
|
||||
"product_id": product.id,
|
||||
"product_uom": product.uom_id.id,
|
||||
"product_uom_qty": quantity,
|
||||
"picking_type_id": picking_type.id,
|
||||
"location_id": stock_loc.id,
|
||||
"location_dest_id": customer_loc.id,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_simple_picking_in(cls, product, quantity, dest_location):
|
||||
supplier_loc = cls.env.ref("stock.stock_location_suppliers")
|
||||
picking_type = cls.env.ref("stock.picking_type_in")
|
||||
partner = cls.env.ref("base.res_partner_1")
|
||||
return cls.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": picking_type.id,
|
||||
"partner_id": partner.id,
|
||||
"location_id": supplier_loc.id,
|
||||
"location_dest_id": dest_location.id,
|
||||
"move_lines": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": product.name,
|
||||
"product_id": product.id,
|
||||
"product_uom": product.uom_id.id,
|
||||
"product_uom_qty": quantity,
|
||||
"picking_type_id": picking_type.id,
|
||||
"location_id": supplier_loc.id,
|
||||
"location_dest_id": dest_location.id,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_inventory(cls, products):
|
||||
"""Create a draft inventory
|
||||
|
||||
Products is a list of tuples (bin location, product).
|
||||
"""
|
||||
values = {
|
||||
"name": "Test Inventory",
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_uom_id": product.uom_id.id,
|
||||
"location_id": location.id,
|
||||
},
|
||||
)
|
||||
for location, product in products
|
||||
],
|
||||
}
|
||||
inventory = cls.env["stock.inventory"].create(values)
|
||||
inventory.action_start()
|
||||
return inventory
|
||||
|
||||
def _test_button_release(self, move_line, expected_state):
|
||||
# for the test, we'll consider our last line has been delivered
|
||||
move_line.qty_done = move_line.product_qty
|
||||
move_line.move_id._action_done()
|
||||
# release, no further operation in queue
|
||||
operation = self.shuttle._operation_for_mode()
|
||||
# the release button can be used only in the state... release
|
||||
operation.state = "release"
|
||||
result = operation.button_release()
|
||||
self.assertEqual(operation.state, expected_state)
|
||||
self.assertFalse(operation.current_move_line_id)
|
||||
expected_result = {
|
||||
"effect": {
|
||||
"fadeout": "slow",
|
||||
"message": _("Congrats, you cleared the queue!"),
|
||||
"img_url": "/web/static/src/img/smile.svg",
|
||||
"type": "rainbow_man",
|
||||
}
|
||||
}
|
||||
self.assertEqual(result, expected_result)
|
||||
115
stock_vertical_lift/tests/test_inventory.py
Normal file
115
stock_vertical_lift/tests/test_inventory.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _
|
||||
|
||||
from .common import VerticalLiftCase
|
||||
|
||||
|
||||
class TestInventory(VerticalLiftCase):
|
||||
def test_switch_inventory(self):
|
||||
self.shuttle.switch_inventory()
|
||||
self.assertEqual(self.shuttle.mode, "inventory")
|
||||
self.assertEqual(
|
||||
self.shuttle._operation_for_mode().current_inventory_line_id,
|
||||
self.env["stock.inventory.line"].browse(),
|
||||
)
|
||||
|
||||
def test_inventory_action_open_screen(self):
|
||||
self.shuttle.switch_inventory()
|
||||
action = self.shuttle.action_open_screen()
|
||||
operation = self.shuttle._operation_for_mode()
|
||||
self.assertEqual(action["type"], "ir.actions.act_window")
|
||||
self.assertEqual(action["res_model"], "vertical.lift.operation.inventory")
|
||||
self.assertEqual(action["res_id"], operation.id)
|
||||
|
||||
def test_inventory_count_ops(self):
|
||||
self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10)
|
||||
self._update_qty_in_location(self.location_1a_x2y1, self.product_recovery, 10)
|
||||
self._create_inventory(
|
||||
[
|
||||
(self.location_1a_x1y1, self.product_socks),
|
||||
(self.location_1a_x2y1, self.product_recovery),
|
||||
]
|
||||
)
|
||||
self._update_qty_in_location(self.location_2a_x1y1, self.product_socks, 10)
|
||||
self._create_inventory([(self.location_2a_x1y1, self.product_socks)])
|
||||
|
||||
operation = self._open_screen("inventory")
|
||||
self.assertEqual(operation.number_of_ops, 2)
|
||||
self.assertEqual(operation.number_of_ops_all, 3)
|
||||
|
||||
def test_process_current_inventory(self):
|
||||
self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10)
|
||||
inventory = self._create_inventory(
|
||||
[(self.location_1a_x1y1, self.product_socks)]
|
||||
)
|
||||
operation = self._open_screen("inventory")
|
||||
self.assertEqual(operation.state, "quantity")
|
||||
self.assertEqual(operation.current_inventory_line_id, inventory.line_ids)
|
||||
# test the happy path, quantity is correct
|
||||
operation.quantity_input = 10.0
|
||||
result = operation.button_save()
|
||||
|
||||
# state is reset
|
||||
# noop because we have no further lines
|
||||
self.assertEqual(operation.state, "noop")
|
||||
self.assertFalse(operation.current_inventory_line_id)
|
||||
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
||||
self.assertEqual(inventory.state, "done")
|
||||
expected_result = {
|
||||
"effect": {
|
||||
"fadeout": "slow",
|
||||
"message": _("Congrats, you cleared the queue!"),
|
||||
"img_url": "/web/static/src/img/smile.svg",
|
||||
"type": "rainbow_man",
|
||||
}
|
||||
}
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_wrong_quantity(self):
|
||||
self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10)
|
||||
inventory = self._create_inventory(
|
||||
[(self.location_1a_x1y1, self.product_socks)]
|
||||
)
|
||||
operation = self._open_screen("inventory")
|
||||
line = operation.current_inventory_line_id
|
||||
self.assertEqual(line, inventory.line_ids)
|
||||
|
||||
operation.quantity_input = 12.0
|
||||
operation.button_save()
|
||||
self.assertEqual(operation.last_quantity_input, 12.0)
|
||||
self.assertEqual(operation.quantity_input, 0.0)
|
||||
self.assertEqual(operation.state, "confirm_wrong_quantity")
|
||||
self.assertEqual(operation.current_inventory_line_id, line)
|
||||
|
||||
# entering the same quantity a second time validates
|
||||
operation.quantity_input = 12.0
|
||||
operation.button_save()
|
||||
self.assertFalse(operation.current_inventory_line_id)
|
||||
|
||||
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
||||
self.assertEqual(inventory.state, "done")
|
||||
|
||||
def test_inventory_next_line(self):
|
||||
self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10)
|
||||
self._update_qty_in_location(self.location_1a_x2y1, self.product_recovery, 10)
|
||||
inventory = self._create_inventory(
|
||||
[
|
||||
(self.location_1a_x1y1, self.product_socks),
|
||||
(self.location_1a_x2y1, self.product_recovery),
|
||||
]
|
||||
)
|
||||
inventory_lines = inventory.line_ids
|
||||
operation = self._open_screen("inventory")
|
||||
operation.quantity_input = 10.0
|
||||
line1 = operation.current_inventory_line_id
|
||||
result = operation.button_save()
|
||||
self.assertFalse(result) # no rainbow man
|
||||
|
||||
# go to next line
|
||||
remaining_line = inventory_lines - line1
|
||||
self.assertEqual(operation.state, "quantity")
|
||||
self.assertEqual(operation.current_inventory_line_id, remaining_line)
|
||||
self.assertEqual(operation.last_quantity_input, 0.0)
|
||||
self.assertEqual(operation.quantity_input, 0.0)
|
||||
37
stock_vertical_lift/tests/test_location.py
Normal file
37
stock_vertical_lift/tests/test_location.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from .common import VerticalLiftCase
|
||||
|
||||
|
||||
class TestVerticalLiftLocation(VerticalLiftCase):
|
||||
def test_vertical_lift_kind(self):
|
||||
# this boolean is what defines a "Vertical Lift View", the upper level
|
||||
# of the tree (View -> Shuttles -> Trays -> Cells)
|
||||
self.assertTrue(self.vertical_lift_loc.vertical_lift_location)
|
||||
self.assertEqual(self.vertical_lift_loc.vertical_lift_kind, "view")
|
||||
|
||||
# check types accross the hierarchy
|
||||
shuttles = self.vertical_lift_loc.child_ids
|
||||
self.assertTrue(
|
||||
all(location.vertical_lift_kind == "shuttle" for location in shuttles)
|
||||
)
|
||||
trays = shuttles.mapped("child_ids")
|
||||
self.assertTrue(
|
||||
all(location.vertical_lift_kind == "tray" for location in trays)
|
||||
)
|
||||
cells = trays.mapped("child_ids")
|
||||
self.assertTrue(
|
||||
all(location.vertical_lift_kind == "cell" for location in cells)
|
||||
)
|
||||
|
||||
def test_create_shuttle(self):
|
||||
# any location created directly under the view is a shuttle
|
||||
shuttle_loc = self.env["stock.location"].create(
|
||||
{
|
||||
"name": "Shuttle 42",
|
||||
"location_id": self.vertical_lift_loc.id,
|
||||
"usage": "internal",
|
||||
}
|
||||
)
|
||||
self.assertEqual(shuttle_loc.vertical_lift_kind, "shuttle")
|
||||
202
stock_vertical_lift/tests/test_pick.py
Normal file
202
stock_vertical_lift/tests/test_pick.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from .common import VerticalLiftCase
|
||||
|
||||
|
||||
class TestPick(VerticalLiftCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.picking_out = cls.env.ref(
|
||||
"stock_vertical_lift.stock_picking_out_demo_vertical_lift_1"
|
||||
)
|
||||
# we have a move line to pick created by demo picking
|
||||
# stock_picking_out_demo_vertical_lift_1
|
||||
cls.out_move_line = cls.picking_out.move_line_ids
|
||||
|
||||
def test_switch_pick(self):
|
||||
self.shuttle.switch_pick()
|
||||
self.assertEqual(self.shuttle.mode, "pick")
|
||||
self.assertEqual(
|
||||
self.shuttle._operation_for_mode().current_move_line_id, self.out_move_line
|
||||
)
|
||||
|
||||
def test_pick_action_open_screen(self):
|
||||
self.shuttle.switch_pick()
|
||||
action = self.shuttle.action_open_screen()
|
||||
operation = self.shuttle._operation_for_mode()
|
||||
self.assertTrue(operation.current_move_line_id)
|
||||
self.assertEqual(action["type"], "ir.actions.act_window")
|
||||
self.assertEqual(action["res_model"], "vertical.lift.operation.pick")
|
||||
self.assertEqual(action["res_id"], operation.id)
|
||||
|
||||
def test_pick_select_next_move_line(self):
|
||||
operation = self._open_screen("pick")
|
||||
operation.select_next_move_line()
|
||||
self.assertEqual(operation.current_move_line_id, self.out_move_line)
|
||||
self.assertEqual(operation.state, "scan_destination")
|
||||
|
||||
def test_pick_save(self):
|
||||
operation = self._open_screen("pick")
|
||||
# assume we already scanned the destination, current state is save
|
||||
operation.state = "save"
|
||||
operation.current_move_line_id = self.out_move_line
|
||||
operation.button_save()
|
||||
self.assertEqual(operation.current_move_line_id.state, "done")
|
||||
self.assertEqual(operation.state, "release")
|
||||
|
||||
def test_pick_related_fields(self):
|
||||
operation = self._open_screen("pick")
|
||||
ml = operation.current_move_line_id = self.out_move_line
|
||||
|
||||
# Trays related fields
|
||||
# For pick, this is the source location, which is the cell where the
|
||||
# product is.
|
||||
self.assertEqual(operation.tray_location_id, ml.location_id)
|
||||
self.assertEqual(
|
||||
operation.tray_name,
|
||||
# parent = tray
|
||||
ml.location_id.location_id.name,
|
||||
)
|
||||
self.assertEqual(
|
||||
operation.tray_type_id,
|
||||
# the tray type is on the parent of the cell (on the tray)
|
||||
ml.location_id.location_id.tray_type_id,
|
||||
)
|
||||
self.assertEqual(
|
||||
operation.tray_type_code, ml.location_id.location_id.tray_type_id.code
|
||||
)
|
||||
self.assertEqual(operation.tray_x, ml.location_id.posx)
|
||||
self.assertEqual(operation.tray_y, ml.location_id.posy)
|
||||
|
||||
# Move line related fields
|
||||
self.assertEqual(operation.picking_id, ml.picking_id)
|
||||
self.assertEqual(operation.picking_origin, ml.picking_id.origin)
|
||||
self.assertEqual(operation.picking_partner_id, ml.picking_id.partner_id)
|
||||
self.assertEqual(operation.product_id, ml.product_id)
|
||||
self.assertEqual(operation.product_uom_id, ml.product_uom_id)
|
||||
self.assertEqual(operation.product_uom_qty, ml.product_uom_qty)
|
||||
self.assertEqual(operation.qty_done, ml.qty_done)
|
||||
self.assertEqual(operation.lot_id, ml.lot_id)
|
||||
|
||||
def test_pick_count_move_lines(self):
|
||||
product1 = self.env.ref("stock_vertical_lift.product_running_socks")
|
||||
product2 = self.env.ref("stock_vertical_lift.product_recovery_socks")
|
||||
# cancel the picking from demo data to start from a clean state
|
||||
self.env.ref(
|
||||
"stock_vertical_lift.stock_picking_out_demo_vertical_lift_1"
|
||||
).action_cancel()
|
||||
|
||||
# ensure that we have stock in some cells, we'll put product1
|
||||
# in the first Shuttle and product2 in the second
|
||||
cell1 = self.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y2"
|
||||
)
|
||||
self._update_quantity_in_cell(cell1, product1, 50)
|
||||
cell2 = self.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a_x1y1"
|
||||
)
|
||||
self._update_quantity_in_cell(cell2, product2, 50)
|
||||
|
||||
# create pickings (we already have an existing one from demo data)
|
||||
pickings = self.env["stock.picking"].browse()
|
||||
pickings |= self._create_simple_picking_out(product1, 1)
|
||||
pickings |= self._create_simple_picking_out(product1, 1)
|
||||
pickings |= self._create_simple_picking_out(product1, 1)
|
||||
pickings |= self._create_simple_picking_out(product1, 1)
|
||||
pickings |= self._create_simple_picking_out(product2, 20)
|
||||
pickings |= self._create_simple_picking_out(product2, 30)
|
||||
# this one should be 'assigned', so should be included in the operation
|
||||
# count
|
||||
unassigned = self._create_simple_picking_out(product2, 1)
|
||||
pickings |= unassigned
|
||||
pickings.action_confirm()
|
||||
# product1 will be taken from the shuttle1, product2 from shuttle2
|
||||
pickings.action_assign()
|
||||
|
||||
shuttle1 = self.shuttle
|
||||
operation1 = shuttle1._operation_for_mode()
|
||||
shuttle2 = self.env.ref(
|
||||
"stock_vertical_lift.stock_vertical_lift_demo_shuttle_2"
|
||||
)
|
||||
operation2 = shuttle2._operation_for_mode()
|
||||
|
||||
self.assertEqual(operation1.number_of_ops, 4)
|
||||
self.assertEqual(operation2.number_of_ops, 2)
|
||||
self.assertEqual(operation1.number_of_ops_all, 6)
|
||||
self.assertEqual(operation2.number_of_ops_all, 6)
|
||||
|
||||
# Process a line, should change the numbers.
|
||||
operation1.select_next_move_line()
|
||||
operation1.process_current()
|
||||
self.assertEqual(operation1.number_of_ops, 3)
|
||||
self.assertEqual(operation2.number_of_ops, 2)
|
||||
self.assertEqual(operation1.number_of_ops_all, 5)
|
||||
self.assertEqual(operation2.number_of_ops_all, 5)
|
||||
|
||||
# add stock and make the last one assigned to check the number is
|
||||
# updated
|
||||
self._update_quantity_in_cell(cell2, product2, 10)
|
||||
unassigned.action_assign()
|
||||
self.assertEqual(operation1.number_of_ops, 3)
|
||||
self.assertEqual(operation2.number_of_ops, 3)
|
||||
self.assertEqual(operation1.number_of_ops_all, 6)
|
||||
self.assertEqual(operation2.number_of_ops_all, 6)
|
||||
|
||||
def test_on_barcode_scanned(self):
|
||||
operation = self._open_screen("pick")
|
||||
self.assertEqual(operation.state, "scan_destination")
|
||||
move_line = operation.current_move_line_id
|
||||
current_destination = move_line.location_dest_id
|
||||
stock_location = self.env.ref("stock.stock_location_stock")
|
||||
self.assertEqual(
|
||||
current_destination, self.env.ref("stock.stock_location_customers")
|
||||
)
|
||||
operation.on_barcode_scanned(stock_location.barcode)
|
||||
self.assertEqual(move_line.location_dest_id, stock_location)
|
||||
self.assertEqual(operation.state, "save")
|
||||
|
||||
def test_button_release(self):
|
||||
self._open_screen("pick")
|
||||
self._test_button_release(self.out_move_line, "noop")
|
||||
|
||||
def test_process_current_pick(self):
|
||||
operation = self._open_screen("pick")
|
||||
operation.current_move_line_id = self.out_move_line
|
||||
qty_to_process = self.out_move_line.product_qty
|
||||
operation.process_current()
|
||||
self.assertEqual(self.out_move_line.state, "done")
|
||||
self.assertEqual(self.out_move_line.qty_done, qty_to_process)
|
||||
|
||||
def test_matrix(self):
|
||||
operation = self._open_screen("pick")
|
||||
operation.current_move_line_id = self.out_move_line
|
||||
location = self.out_move_line.location_id
|
||||
# offset by -1 because the fields are for humans
|
||||
expected_x = location.posx - 1
|
||||
expected_y = location.posy - 1
|
||||
self.assertEqual(
|
||||
operation.tray_matrix,
|
||||
{
|
||||
"selected": [expected_x, expected_y],
|
||||
# fmt: off
|
||||
'cells': [
|
||||
[0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 1, 0, 0, 0, 0, 0],
|
||||
]
|
||||
# fmt: on
|
||||
},
|
||||
)
|
||||
|
||||
def test_tray_qty(self):
|
||||
cell = self.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y2"
|
||||
)
|
||||
self.out_move_line.location_id = cell
|
||||
operation = self.shuttle._operation_for_mode()
|
||||
operation.current_move_line_id = self.out_move_line
|
||||
self._update_quantity_in_cell(cell, self.out_move_line.product_id, 50)
|
||||
self.assertEqual(operation.tray_qty, 50)
|
||||
self._update_quantity_in_cell(cell, self.out_move_line.product_id, -20)
|
||||
self.assertEqual(operation.tray_qty, 30)
|
||||
154
stock_vertical_lift/tests/test_put.py
Normal file
154
stock_vertical_lift/tests/test_put.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from .common import VerticalLiftCase
|
||||
|
||||
|
||||
class TestPut(VerticalLiftCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.picking_in = cls.env.ref(
|
||||
"stock_vertical_lift.stock_picking_in_demo_vertical_lift_1"
|
||||
)
|
||||
cls.picking_in.action_confirm()
|
||||
cls.in_move_line = cls.picking_in.move_line_ids
|
||||
cls.in_move_line.location_dest_id = cls.shuttle.location_id
|
||||
|
||||
def test_put_action_open_screen(self):
|
||||
self.shuttle.switch_put()
|
||||
action = self.shuttle.action_open_screen()
|
||||
operation = self.shuttle._operation_for_mode()
|
||||
self.assertEqual(action["type"], "ir.actions.act_window")
|
||||
self.assertEqual(action["res_model"], "vertical.lift.operation.put")
|
||||
self.assertEqual(action["res_id"], operation.id)
|
||||
|
||||
def test_switch_put(self):
|
||||
self.shuttle.switch_put()
|
||||
self.assertEqual(self.shuttle.mode, "put")
|
||||
self.assertEqual(
|
||||
self.shuttle._operation_for_mode().current_move_line_id,
|
||||
self.env["stock.move.line"].browse(),
|
||||
)
|
||||
|
||||
def test_put_count_move_lines(self):
|
||||
self.picking_in.action_cancel()
|
||||
put1 = self._create_simple_picking_in(
|
||||
self.product_socks, 10, self.location_1a_x1y1
|
||||
)
|
||||
put1.action_confirm()
|
||||
put2 = self._create_simple_picking_in(
|
||||
self.product_recovery, 10, self.vertical_lift_loc
|
||||
)
|
||||
put2.action_confirm()
|
||||
put3 = self._create_simple_picking_in(
|
||||
self.product_recovery, 10, self.vertical_lift_loc
|
||||
)
|
||||
put3.action_confirm()
|
||||
operation = self._open_screen("put")
|
||||
shuttle2 = self.env.ref(
|
||||
"stock_vertical_lift.stock_vertical_lift_demo_shuttle_2"
|
||||
)
|
||||
operation2 = self._open_screen("put", shuttle=shuttle2)
|
||||
|
||||
# we don't really care about the "number_of_ops" for the
|
||||
# put-away, as the move lines are supposed to have the whole
|
||||
# whole shuttle view as destination
|
||||
self.assertEqual(operation.number_of_ops, 1)
|
||||
self.assertEqual(operation.number_of_ops_all, 3)
|
||||
self.assertEqual(operation2.number_of_ops, 0)
|
||||
self.assertEqual(operation2.number_of_ops_all, 3)
|
||||
|
||||
def test_transition_start(self):
|
||||
operation = self._open_screen("put")
|
||||
# we begin with an empty screen, user has to scan a package, product,
|
||||
# or lot
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
|
||||
def test_transition_scan_source_to_scan_tray_type(self):
|
||||
operation = self._open_screen("put")
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
# wrong barcode, nothing happens
|
||||
operation.on_barcode_scanned("foo")
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
# product scanned, move to next step
|
||||
operation.on_barcode_scanned(self.product_socks.barcode)
|
||||
self.assertEqual(operation.state, "scan_tray_type")
|
||||
self.assertEqual(operation.current_move_line_id, self.in_move_line)
|
||||
|
||||
def test_transition_scan_tray_type_to_save(self):
|
||||
operation = self._open_screen("put")
|
||||
# assume we already scanned the product
|
||||
operation.state = "scan_tray_type"
|
||||
operation.current_move_line_id = self.in_move_line
|
||||
# wrong barcode, nothing happens
|
||||
operation.on_barcode_scanned("foo")
|
||||
# tray type scanned, move to next step
|
||||
operation.on_barcode_scanned(self.location_1a.tray_type_id.code)
|
||||
self.assertEqual(operation.state, "save")
|
||||
# a cell has been set
|
||||
self.assertTrue(
|
||||
self.in_move_line.location_dest_id in self.location_1a.child_ids
|
||||
)
|
||||
|
||||
def test_change_tray_type_on_save(self):
|
||||
operation = self._open_screen("put")
|
||||
move_line = self.in_move_line
|
||||
# assume we already scanned the product and the tray type
|
||||
# and the assigned location was location_1a_x1y1
|
||||
operation.current_move_line_id = move_line
|
||||
move_line.location_dest_id = self.location_1a_x1y1
|
||||
operation.state = "save"
|
||||
# we want to use another tray with a different type though,
|
||||
# so we scan again
|
||||
operation.on_barcode_scanned(self.location_1b.tray_type_id.code)
|
||||
self.assertTrue(
|
||||
self.in_move_line.location_dest_id
|
||||
in self.shuttle.location_id.child_ids.child_ids
|
||||
)
|
||||
# we are still in save
|
||||
self.assertEqual(operation.state, "save")
|
||||
# a cell has been set in the other tray
|
||||
self.assertTrue(move_line.location_dest_id in self.location_1b.child_ids)
|
||||
|
||||
def test_transition_scan_tray_type_no_empty_cell(self):
|
||||
operation = self._open_screen("put")
|
||||
# assume we already scanned the product
|
||||
operation.state = "scan_tray_type"
|
||||
operation.current_move_line_id = self.in_move_line
|
||||
# create a tray type without location, which is the same as if all the
|
||||
# locations of a tray type were full
|
||||
new_tray_type = self.env["stock.location.tray.type"].create(
|
||||
{"name": "new tray type", "code": "test", "rows": 1, "cols": 1}
|
||||
)
|
||||
operation.on_barcode_scanned(new_tray_type.code)
|
||||
# should stay the same state
|
||||
self.assertEqual(operation.state, "scan_tray_type")
|
||||
# destination not changed
|
||||
self.assertEqual(self.in_move_line.location_dest_id, self.shuttle.location_id)
|
||||
|
||||
def test_transition_save(self):
|
||||
operation = self._open_screen("put")
|
||||
# first steps of the workflow are done
|
||||
operation.current_move_line_id = self.in_move_line
|
||||
operation.current_move_line_id.location_dest_id = self.location_1a_x1y1
|
||||
operation.state = "save"
|
||||
qty_to_process = self.in_move_line.product_qty
|
||||
operation.button_save()
|
||||
self.assertEqual(self.in_move_line.state, "done")
|
||||
self.assertEqual(self.in_move_line.qty_done, qty_to_process)
|
||||
|
||||
def test_transition_button_release(self):
|
||||
operation = self._open_screen("put")
|
||||
move_line = self.in_move_line
|
||||
# first steps of the workflow are done
|
||||
operation.current_move_line_id = move_line
|
||||
operation.current_move_line_id.location_dest_id = self.location_1a_x1y1
|
||||
# for the test, we'll consider our last line has been delivered
|
||||
move_line.qty_done = move_line.product_qty
|
||||
move_line.move_id._action_done()
|
||||
|
||||
operation = self._open_screen("put")
|
||||
operation.button_release()
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
self.assertFalse(operation.current_move_line_id)
|
||||
23
stock_vertical_lift/views/shuttle_screen_templates.xml
Normal file
23
stock_vertical_lift/views/shuttle_screen_templates.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<odoo>
|
||||
<template id="packagings">
|
||||
<ul class="o_vlift_packaging list-unstyled">
|
||||
<t t-foreach="product.packaging_ids" t-as="packaging">
|
||||
<li>
|
||||
<span>1</span>
|
||||
<span
|
||||
class="packaging_name"
|
||||
itemprop="name"
|
||||
t-esc="packaging.name"
|
||||
/>
|
||||
<span>: </span>
|
||||
<span class="packaging_qty" itemprop="qty" t-esc="packaging.qty" />
|
||||
<span
|
||||
class="packaging_unit"
|
||||
itemprop="unit"
|
||||
t-esc="product.uom_id.name"
|
||||
/>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
</odoo>
|
||||
58
stock_vertical_lift/views/stock_location_views.xml
Normal file
58
stock_vertical_lift/views/stock_location_views.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_location_form" model="ir.ui.view">
|
||||
<field name="name">stock.location.form.vertical.lift</field>
|
||||
<field name="model">stock.location</field>
|
||||
<field name="inherit_id" ref="stock_location_tray.view_location_form" />
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button
|
||||
name="button_fetch_vertical_lift_tray"
|
||||
string="Fetch Shuttle Tray"
|
||||
type="object"
|
||||
groups="stock.group_stock_manager"
|
||||
class="oe_stat_button"
|
||||
icon="fa-hand-paper-o"
|
||||
attrs="{'invisible': [('vertical_lift_kind', 'not in', ('tray', 'cell'))]}"
|
||||
/>
|
||||
<button
|
||||
name="button_release_vertical_lift_tray"
|
||||
string="Release Shuttle Tray"
|
||||
type="object"
|
||||
groups="stock.group_stock_manager"
|
||||
class="oe_stat_button"
|
||||
icon="fa-hand-rock-o"
|
||||
attrs="{'invisible': [('vertical_lift_kind', 'not in', ('tray', 'cell'))]}"
|
||||
/>
|
||||
</div>
|
||||
<field name="return_location" position="after">
|
||||
<field
|
||||
name="vertical_lift_location"
|
||||
attrs="{'invisible': [('vertical_lift_kind', '!=', False), ('vertical_lift_kind', '!=', 'view')]}"
|
||||
/>
|
||||
<field name="vertical_lift_kind" />
|
||||
<field
|
||||
name="vertical_lift_shuttle_id"
|
||||
attrs="{'invisible': [('vertical_lift_kind', 'not in', ('shuttle', 'tray', 'cell'))]}"
|
||||
/>
|
||||
</field>
|
||||
<field name="tray_type_id" position="attributes">
|
||||
<attribute name="attrs">
|
||||
{'invisible': [('cell_in_tray_type_id', '!=', False)],
|
||||
'required': [('vertical_lift_kind', '=', 'tray')]}
|
||||
</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_location_search" model="ir.ui.view">
|
||||
<field name="name">stock.location.search.vertical.lift</field>
|
||||
<field name="model">stock.location</field>
|
||||
<field name="inherit_id" ref="stock_location_tray.view_location_search" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="tray_type_id" position="after">
|
||||
<field name="vertical_lift_kind" />
|
||||
<field name="vertical_lift_shuttle_id" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
35
stock_vertical_lift/views/stock_move_line_views.xml
Normal file
35
stock_vertical_lift/views/stock_move_line_views.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_stock_move_line_operation_tree" model="ir.ui.view">
|
||||
<field name="name">stock.move.line.operations.tree.vertical.lift</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="stock_location_tray.view_stock_move_line_operation_tree"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_show_source_tray" position="before">
|
||||
<button
|
||||
name="fetch_vertical_lift_tray_source"
|
||||
string="Fetch Source Tray"
|
||||
type="object"
|
||||
icon="fa-hand-paper-o"
|
||||
attrs="{'invisible': [('tray_source_matrix', '=', {})]}"
|
||||
invisible="not context.get('show_source_location')"
|
||||
groups="stock.group_stock_multi_locations"
|
||||
/>
|
||||
</button>
|
||||
<button name="action_show_dest_tray" position="before">
|
||||
<button
|
||||
name="fetch_vertical_lift_tray_dest"
|
||||
string="Fetch Destination Tray"
|
||||
type="object"
|
||||
icon="fa-hand-paper-o"
|
||||
attrs="{'invisible': [('tray_dest_matrix', '=', {})]}"
|
||||
invisible="not context.get('show_destination_location')"
|
||||
groups="stock.group_stock_multi_locations"
|
||||
/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
20
stock_vertical_lift/views/stock_vertical_lift_templates.xml
Normal file
20
stock_vertical_lift/views/stock_vertical_lift_templates.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<template
|
||||
id="stock_vertical_lift_assets"
|
||||
name="stock.vertical.lift.assets"
|
||||
inherit_id="web.assets_backend"
|
||||
>
|
||||
<xpath expr="." position="inside">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/scss"
|
||||
href="/stock_vertical_lift/static/src/scss/vertical_lift.scss"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/stock_vertical_lift/static/src/js/vertical_lift.js"
|
||||
/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
210
stock_vertical_lift/views/vertical_lift_operation_base_views.xml
Normal file
210
stock_vertical_lift/views/vertical_lift_operation_base_views.xml
Normal file
@@ -0,0 +1,210 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<!--
|
||||
When we refresh the page on the browser when we are using the
|
||||
"screen" view, odoo loses the information that we want the view
|
||||
to be headless, fullscreen, etc. so it's displayed pretty badly.
|
||||
This view is a work-around: its priority is lower, so it will be
|
||||
picked up by default on loading, and a button allows to re-open
|
||||
the screen view with the proper options.
|
||||
-->
|
||||
<record id="vertical_lift_operation_base_button_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.base.button.view</field>
|
||||
<field name="model">vertical.lift.operation.base</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Operations" delete="0" create="0">
|
||||
<button
|
||||
string="Reopen Screen"
|
||||
name="action_open_screen"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_operation_base_screen_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.base.screen.view</field>
|
||||
<field name="model">vertical.lift.operation.base</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Operations" delete="0" create="0" class="o_vlift_shuttle">
|
||||
<div class="o_shuttle_header">
|
||||
<div class="o_shuttle_header_left o_shuttle_header_content">
|
||||
<field name="name" readonly="1" />
|
||||
</div>
|
||||
<div class="o_shuttle_header_center o_shuttle_header_content">
|
||||
<label for="mode" />
|
||||
<field name="mode" readonly="1" />
|
||||
</div>
|
||||
<div class="o_shuttle_header_right o_shuttle_header_content">
|
||||
<label for="number_of_ops" />
|
||||
<field name="number_of_ops" readonly="1" />
|
||||
<span>/</span>
|
||||
<field name="number_of_ops_all" readonly="1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_shuttle_actions">
|
||||
<div class="o_shuttle_content o_shuttle_content_left">
|
||||
<button
|
||||
name="action_menu"
|
||||
type="object"
|
||||
class="btn-secondary o_shuttle_icon_btn"
|
||||
string=""
|
||||
icon="fa-bars"
|
||||
aria-label="Dropdown menu"
|
||||
title="Dropdown menu"
|
||||
/>
|
||||
<button
|
||||
name="action_manual_barcode"
|
||||
type="object"
|
||||
class="btn-secondary o_shuttle_icon_btn"
|
||||
string=""
|
||||
icon="fa-terminal"
|
||||
aria-label="Barcode Input"
|
||||
title="Barcode Input"
|
||||
/>
|
||||
</div>
|
||||
<div class="o_shuttle_content o_shuttle_content_right">
|
||||
<div>
|
||||
<!-- will react on barcode 'O-BTN.save' -->
|
||||
<button
|
||||
name="button_save"
|
||||
type="object"
|
||||
string="Save"
|
||||
icon="fa-check"
|
||||
class="btn-primary"
|
||||
barcode_trigger="save"
|
||||
attrs="{'invisible': [('state', '!=', 'save')]}"
|
||||
/>
|
||||
<!-- will react on barcode 'O-BTN.release -->
|
||||
<button
|
||||
name="button_release"
|
||||
type="object"
|
||||
string="Release"
|
||||
class="btn-primary"
|
||||
barcode_trigger="release"
|
||||
attrs="{'invisible': [('state', '!=', 'release')]}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_shuttle_operation bg-primary jumbotron jumbotron-fluid">
|
||||
<div class="container">
|
||||
<field name="state" invisible="0" readonly="True" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_shuttle_data">
|
||||
</div>
|
||||
<field name="_barcode_scanned" widget="barcode_handler" />
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_operation_transfer_screen_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.transfer.screen.view</field>
|
||||
<field name="model">vertical.lift.operation.transfer</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_base_screen_view" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('o_shuttle_data')]" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'invisible': [('current_move_line_id', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_shuttle_data')]" position="inside">
|
||||
<!-- on the left of the screen -->
|
||||
<div class="o_shuttle_data_content o_shuttle_move">
|
||||
<div>
|
||||
<group>
|
||||
<field name="current_move_line_id" invisible="1" />
|
||||
<label for="picking_id" />
|
||||
<div>
|
||||
<div>
|
||||
<field
|
||||
name="picking_id"
|
||||
options="{'no_open': True}"
|
||||
class="mr8"
|
||||
/>
|
||||
<span>/</span>
|
||||
<field
|
||||
name="picking_origin"
|
||||
class="oe_inline ml8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label for="picking_partner_id" />
|
||||
<div>
|
||||
<field
|
||||
name="picking_partner_id"
|
||||
options="{'no_open': True}"
|
||||
/>
|
||||
</div>
|
||||
<label for="location_dest_id" />
|
||||
<div>
|
||||
<field
|
||||
name="location_dest_id"
|
||||
class="bg-primary o_shuttle_highlight"
|
||||
readonly="1"
|
||||
options="{'no_open': True}"
|
||||
/>
|
||||
</div>
|
||||
<label for="product_id" />
|
||||
<div colspan="2" class="oe_title">
|
||||
<h1>
|
||||
<field
|
||||
name="product_id"
|
||||
options="{'no_open': True}"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<div colspan="2">
|
||||
<field name="product_packagings" />
|
||||
</div>
|
||||
<field name="lot_id" />
|
||||
<label
|
||||
for="product_uom_qty"
|
||||
string="Quantity"
|
||||
class="ml32"
|
||||
/>
|
||||
<div colspan="2" class="ml32">
|
||||
<h1 class="bg-primary o_shuttle_highlight">
|
||||
<field
|
||||
name="product_uom_qty"
|
||||
class="oe_inline text-white"
|
||||
/>
|
||||
<field
|
||||
name="product_uom_id"
|
||||
options="{'no_open': True}"
|
||||
class="oe_inline ml8 text-white"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- on the right of the screen -->
|
||||
<div
|
||||
class="o_shuttle_data_content o_shuttle_tray"
|
||||
attrs="{'invisible': [('tray_type_id', '=', False)]}"
|
||||
>
|
||||
<group col="1">
|
||||
<field name="tray_type_id" invisible="1" />
|
||||
<field name="tray_name" />
|
||||
<field name="tray_type_code" />
|
||||
<field name="tray_x" />
|
||||
<field name="tray_y" />
|
||||
<label for="tray_qty" />
|
||||
<div colspan="2" class="oe_title">
|
||||
<h1>
|
||||
<field name="tray_qty" />
|
||||
</h1>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<div>
|
||||
<field name="tray_matrix" widget="location_tray_matrix" />
|
||||
</div>
|
||||
</group>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="vertical_lift_operation_inventory_button_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.inventory.button.view</field>
|
||||
<field name="model">vertical.lift.operation.inventory</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_base_button_view" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Inventory Screen</attribute>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_operation_inventory_screen_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.inventory.screen.view</field>
|
||||
<field name="model">vertical.lift.operation.inventory</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_base_screen_view" />
|
||||
<field name="priority">100</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Inventory Screen</attribute>
|
||||
</form>
|
||||
<form position="inside">
|
||||
<field name="state" invisible="1" />
|
||||
</form>
|
||||
<button name="button_save" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'invisible': [('state', 'not in', ('quantity', 'confirm_wrong_quantity'))]}</attribute>
|
||||
</button>
|
||||
<button name="button_release" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</button>
|
||||
<xpath expr="//div[hasclass('o_shuttle_data')]" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'invisible': [('current_inventory_line_id', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_shuttle_data')]" position="inside">
|
||||
<!-- on the left of the screen -->
|
||||
<div class="o_shuttle_data_content o_shuttle_move">
|
||||
<div>
|
||||
<group>
|
||||
<field name="current_inventory_line_id" invisible="1" />
|
||||
<div>
|
||||
<div>
|
||||
<field
|
||||
name="inventory_id"
|
||||
options="{'no_open': True}"
|
||||
class="mr8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label for="product_id" />
|
||||
<div colspan="2" class="oe_title">
|
||||
<h1>
|
||||
<field
|
||||
name="product_id"
|
||||
options="{'no_open': True}"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<div colspan="2">
|
||||
<field name="product_packagings" />
|
||||
</div>
|
||||
<field name="lot_id" />
|
||||
<field name="package_id" />
|
||||
<label
|
||||
for="quantity_input"
|
||||
string="Quantity"
|
||||
class="ml32"
|
||||
/>
|
||||
<div colspan="2" class="ml32">
|
||||
<h1 class="bg-primary o_shuttle_highlight">
|
||||
<field
|
||||
name="quantity_input"
|
||||
default_focus="1"
|
||||
class="oe_inline"
|
||||
attrs="{'readonly': [('state', 'not in', ('quantity', 'confirm_wrong_quantity'))]}"
|
||||
/>
|
||||
<field
|
||||
name="product_uom_id"
|
||||
options="{'no_open': True}"
|
||||
class="oe_inline ml8 text-white"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- on the right of the screen -->
|
||||
<div class="o_shuttle_data_content o_shuttle_tray">
|
||||
<group col="1">
|
||||
<field name="tray_name" />
|
||||
<field name="tray_type_code" />
|
||||
<field name="tray_x" />
|
||||
<field name="tray_y" />
|
||||
</group>
|
||||
<group>
|
||||
<div>
|
||||
<field name="tray_matrix" widget="location_tray_matrix" />
|
||||
</div>
|
||||
</group>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="vertical_lift_operation_pick_button_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.pick.button.view</field>
|
||||
<field name="model">vertical.lift.operation.pick</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_base_button_view" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Pick Screen</attribute>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_operation_pick_screen_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.pick.screen.view</field>
|
||||
<field name="model">vertical.lift.operation.pick</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_transfer_screen_view" />
|
||||
<field name="priority">100</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Pick Screen</attribute>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="vertical_lift_operation_put_button_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.put.button.view</field>
|
||||
<field name="model">vertical.lift.operation.put</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_base_button_view" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Put-Away Screen</attribute>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_operation_put_screen_view" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.operation.put.screen.view</field>
|
||||
<field name="model">vertical.lift.operation.put</field>
|
||||
<field name="inherit_id" ref="vertical_lift_operation_transfer_screen_view" />
|
||||
<field name="priority">100</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form position="attributes">
|
||||
<attribute name="string">Put-Away Screen</attribute>
|
||||
</form>
|
||||
<xpath
|
||||
expr="//div[hasclass('o_shuttle_header_right')]/field[@name='number_of_ops']"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//div[hasclass('o_shuttle_header_right')]/field[@name='number_of_ops']/following-sibling::span"
|
||||
position="replace"
|
||||
>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
216
stock_vertical_lift/views/vertical_lift_shuttle_views.xml
Normal file
216
stock_vertical_lift/views/vertical_lift_shuttle_views.xml
Normal file
@@ -0,0 +1,216 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="vertical_lift_shuttle_form_menu" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.shuttle.view.form.menu</field>
|
||||
<field name="model">vertical.lift.shuttle</field>
|
||||
<field name="priority">100</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Shuttle">
|
||||
<div class="row o_vlift_shuttle_menu">
|
||||
<div class="col-6">
|
||||
<button
|
||||
name="switch_pick"
|
||||
type="object"
|
||||
string="Pick"
|
||||
class="btn-primary btn-block btn btn-lg"
|
||||
/>
|
||||
<button
|
||||
name="switch_put"
|
||||
type="object"
|
||||
string="Put"
|
||||
class="btn-primary btn-block btn btn-lg"
|
||||
/>
|
||||
<button
|
||||
name="switch_inventory"
|
||||
type="object"
|
||||
string="Inventory"
|
||||
class="btn-primary btn-block btn btn-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button
|
||||
name="action_back_to_settings"
|
||||
type="object"
|
||||
string="Back to settings"
|
||||
class="btn-danger btn-block btn-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer />
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_shuttle_manual_barcode_form" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.shuttle.manual.barcode.view.form</field>
|
||||
<field name="model">vertical.lift.shuttle.manual.barcode</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Shuttle">
|
||||
<div class="row o_vlift_shuttle_popup">
|
||||
<div class="col-8">
|
||||
<field name="barcode" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button
|
||||
name="button_save"
|
||||
string="Confirm"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer />
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="vertical_lift_shuttle_view_form" model="ir.ui.view">
|
||||
<field name="name">vertical.lift.shuttle.view.form</field>
|
||||
<field name="model">vertical.lift.shuttle</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Shuttle">
|
||||
<header>
|
||||
<button
|
||||
name="release_vertical_lift_tray"
|
||||
string="Release tray"
|
||||
type="object"
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label class="oe_edit_only" for="name" />
|
||||
<h1>
|
||||
<field name="name" placeholder="Shuttle Name" />
|
||||
</h1>
|
||||
</div>
|
||||
<group name="main">
|
||||
<group name="left">
|
||||
<field name="mode" />
|
||||
<field name="location_id" />
|
||||
<field name="hardware" />
|
||||
</group>
|
||||
<group string="Network" name="network">
|
||||
<field name="server" />
|
||||
<field name="port" />
|
||||
<field name="use_tls" />
|
||||
</group>
|
||||
</group>
|
||||
<group groups="base.group_no_one">
|
||||
<label for="command_ids" />
|
||||
<field name="command_ids">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="command" />
|
||||
<field name="answer" />
|
||||
<field name="error" />
|
||||
<field name="create_date" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="vertical_lift_shuttle_view_kanban">
|
||||
<field name="name">vertical.lift.shuttle.kanban</field>
|
||||
<field name="model">vertical.lift.shuttle</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_dashboard">
|
||||
<field name="name" />
|
||||
<field name="mode" />
|
||||
<!-- <field name="number_of_ops"/> -->
|
||||
<!-- <field name="number_of_ops_all"/> -->
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div
|
||||
class="oe_kanban_global_click o_has_icon open_shuttle_screen"
|
||||
>
|
||||
<div class="o_vlift_kanban_main">
|
||||
<div class="o_kanban_card_content">
|
||||
<div class="o_kanban_primary_left">
|
||||
<div class="o_primary">
|
||||
<strong class="o_kanban_record_title">
|
||||
<span>
|
||||
<t t-esc="record.name.value" />
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
Mode:
|
||||
<field name="mode" />
|
||||
</div>
|
||||
<!-- <div class="col-8"> -->
|
||||
<!-- Operations: -->
|
||||
<!-- <field name="number_of_ops"/> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div class="col-8"> -->
|
||||
<!-- All <t t-esc="record.mode.value"/> Operations: -->
|
||||
<!-- <field name="number_of_ops_all"/> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="o_kanban_card_manage_pane dropdown-menu"
|
||||
groups="stock.group_stock_manager"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="o_kanban_card_manage_section o_kanban_manage_reports"
|
||||
>
|
||||
<div role="menuitem">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="o_kanban_manage_toggle_button o_left"
|
||||
href="#"
|
||||
groups="stock.group_stock_manager"
|
||||
>
|
||||
<i
|
||||
class="fa fa-ellipsis-v"
|
||||
role="img"
|
||||
aria-label="Manage"
|
||||
title="Manage"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="vertical_lift_shuttle_view_tree">
|
||||
<field name="name">vertical.lift.shuttle.tree</field>
|
||||
<field name="model">vertical.lift.shuttle</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Shuttle">
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="vertical_lift_shuttle_action">
|
||||
<field name="name">Vertical Lift Shuttles</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">vertical.lift.shuttle</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="target">current</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Open the Shuttle Interface.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="vertical_lift_shuttle"
|
||||
name="Vertical Lift Shuttles"
|
||||
action="vertical_lift_shuttle_action"
|
||||
parent="stock.menu_stock_warehouse_mgmt"
|
||||
sequence="15"
|
||||
/>
|
||||
</odoo>
|
||||
84
stock_vertical_lift_kardex/README.rst
Normal file
84
stock_vertical_lift_kardex/README.rst
Normal file
@@ -0,0 +1,84 @@
|
||||
======================
|
||||
Vertical Lift - Kardex
|
||||
======================
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Alpha
|
||||
.. |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/13.0/stock_vertical_lift_kardex
|
||||
: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-13-0/stock-logistics-warehouse-13-0-stock_vertical_lift_kardex
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||
:target: https://runbot.odoo-community.org/runbot/153/13.0
|
||||
:alt: Try me on Runbot
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Add support for Kardex Remstar vertical lifts to the Vertical Lift
|
||||
module.
|
||||
|
||||
.. IMPORTANT::
|
||||
This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
`More details on development status <https://odoo-community.org/page/development-status>`_
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Add support of the hardware
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vertical_lift_kardex%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Camptocamp
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_vertical_lift_kardex>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
1
stock_vertical_lift_kardex/__init__.py
Normal file
1
stock_vertical_lift_kardex/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
15
stock_vertical_lift_kardex/__manifest__.py
Normal file
15
stock_vertical_lift_kardex/__manifest__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Vertical Lift - Kardex",
|
||||
"summary": "Integrate with Kardex Remstar Vertical Lifts",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Stock",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": ["stock_vertical_lift", "stock_location_position"],
|
||||
"website": "https://www.camptocamp.com",
|
||||
"data": [],
|
||||
"installable": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
2
stock_vertical_lift_kardex/models/__init__.py
Normal file
2
stock_vertical_lift_kardex/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import stock_location
|
||||
from . import vertical_lift_shuttle
|
||||
98
stock_vertical_lift_kardex/models/stock_location.py
Normal file
98
stock_vertical_lift_kardex/models/stock_location.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, exceptions, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
_inherit = "stock.location"
|
||||
|
||||
def _hardware_kardex_prepare_fetch_payload(self, cell_location=None):
|
||||
if self.level is False:
|
||||
raise exceptions.UserError(
|
||||
_("Shuttle tray %s has no level. " "Please fix the configuration")
|
||||
% self.display_name
|
||||
)
|
||||
shuttle = self.vertical_lift_shuttle_id
|
||||
if cell_location:
|
||||
x, y = cell_location.tray_cell_center_position()
|
||||
if x == 0 and y == 0:
|
||||
raise exceptions.UserError(
|
||||
_(
|
||||
"Cell location %s has no position. "
|
||||
"Check if the dimensions of tray %s "
|
||||
"are properly set in the tray type."
|
||||
)
|
||||
% (cell_location.display_name, self.name)
|
||||
)
|
||||
x, y = int(x), int(y)
|
||||
else:
|
||||
x, y = "", ""
|
||||
subst = {
|
||||
"code": shuttle._kardex_shuttle_code(),
|
||||
"hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"),
|
||||
# hard code the gate for now.
|
||||
# TODO proper handling of multiple gates for 1 lift.
|
||||
"addr": shuttle.name + "-1",
|
||||
"carrier": self.level,
|
||||
"carrierNext": "0",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"boxType": "",
|
||||
"Q": "",
|
||||
"order": "",
|
||||
"part": "",
|
||||
"desc": "",
|
||||
}
|
||||
return shuttle._hardware_kardex_format_template(subst)
|
||||
|
||||
def _hardware_vertical_lift_fetch_tray_payload(self, cell_location=None):
|
||||
"""Prepare "fetch" message to be sent to the vertical lift hardware
|
||||
|
||||
Private method, this is where the implementation actually happens.
|
||||
Addons can add their instructions based on the hardware used for
|
||||
this location.
|
||||
|
||||
The hardware used for a location can be found in:
|
||||
|
||||
``self.vertical_lift_shuttle_id.hardware``
|
||||
|
||||
Each addon can implement its own mechanism depending of this value
|
||||
and must call ``super``.
|
||||
|
||||
The method must send the command to the vertical lift to fetch / open
|
||||
the tray. If a ``cell_location`` is passed and if the hardware supports
|
||||
a way to show a cell (such as a laser pointer), it should send this
|
||||
command as well.
|
||||
|
||||
Useful information that could be needed for the drivers:
|
||||
|
||||
* Any field of `self` (name, barcode, ...) which is the current tray.
|
||||
* Any field of `cell_location` (name, barcode, ...) which is the cell
|
||||
in the tray.
|
||||
* ``self.vertical_lift_shuttle_id`` is the current Shuttle, where we
|
||||
find details about the hardware, the current mode (pick, put, ...).
|
||||
* ``self.tray_type_id`` is the kind of tray.
|
||||
* ``self.tray_type_id.width_per_cell`` and
|
||||
``self.tray_type_id.depth_per_cell`` return the size of a cell in mm.
|
||||
* ``cell_location.posx`` and ``posy`` are the coordinate from the
|
||||
bottom-left of the tray.
|
||||
* ``cell_location.tray_cell_center_position()`` returns the central
|
||||
position of the cell in mm from the bottom-left of a tray. (distance
|
||||
from left, distance from bottom). Can be used for instance for
|
||||
highlighting the cell using a laser pointer.
|
||||
"""
|
||||
if self.vertical_lift_shuttle_id.hardware == "kardex":
|
||||
payload = self._hardware_kardex_prepare_fetch_payload(
|
||||
cell_location=cell_location
|
||||
)
|
||||
_logger.debug("Sending to kardex (fetch): {}", payload)
|
||||
else:
|
||||
payload = super()._hardware_vertical_lift_fetch_tray_payload(
|
||||
cell_location=cell_location
|
||||
)
|
||||
return payload
|
||||
123
stock_vertical_lift_kardex/models/vertical_lift_shuttle.py
Normal file
123
stock_vertical_lift_kardex/models/vertical_lift_shuttle.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
JMIF_STATUS = {
|
||||
0: "success",
|
||||
101: "common error",
|
||||
102: "sequence number invalid",
|
||||
103: "machine busy",
|
||||
104: "timeout",
|
||||
105: "max retry reached",
|
||||
106: "carrier in use or undefined",
|
||||
107: "cancelled",
|
||||
108: "invalid user input data",
|
||||
201: "request accepted and queued",
|
||||
202: "request processing started / request active",
|
||||
203: "carrier arrived, maybe overwritten by code 0",
|
||||
301: "AO occupied with other try on move back (store / put)",
|
||||
302: "AO occupied with other try on fetch (pick)",
|
||||
}
|
||||
|
||||
|
||||
class VerticalLiftShuttle(models.Model):
|
||||
_inherit = "vertical.lift.shuttle"
|
||||
|
||||
def _selection_hardware(self):
|
||||
values = super()._selection_hardware()
|
||||
values += [("kardex", "Kardex")]
|
||||
return values
|
||||
|
||||
_kardex_message_template = (
|
||||
"{code}|{hostId}|{addr}|{carrier}|{carrierNext}|"
|
||||
"{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n"
|
||||
)
|
||||
|
||||
def _hardware_kardex_format_template(self, values):
|
||||
payload = self._kardex_message_template.format(**values)
|
||||
return payload.encode("iso-8859-1", "replace")
|
||||
|
||||
def _kardex_shuttle_code(self):
|
||||
mapping = {"pick": "1", "put": "2", "inventory": "5"}
|
||||
ping = "61"
|
||||
return mapping.get(self.mode, ping)
|
||||
|
||||
def _hardware_kardex_prepare_release_payload(self):
|
||||
subst = {
|
||||
"code": self._kardex_shuttle_code(),
|
||||
"hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"),
|
||||
# hard code the gate for now.
|
||||
"addr": self.name + "-1",
|
||||
"carrier": "0",
|
||||
"carrierNext": "0",
|
||||
"x": "0",
|
||||
"y": "0",
|
||||
"boxType": "",
|
||||
"Q": "",
|
||||
"order": "",
|
||||
"part": "",
|
||||
"desc": "",
|
||||
}
|
||||
return self._hardware_kardex_format_template(subst)
|
||||
|
||||
def _hardware_vertical_lift_release_tray_payload(self):
|
||||
"""Prepare "release" message to be sent to the vertical lift hardware
|
||||
|
||||
Private method, this is where the implementation actually happens.
|
||||
Addons can add their instructions based on the hardware used for
|
||||
this location.
|
||||
|
||||
The hardware used for a location can be found in:
|
||||
|
||||
``self.vertical_lift_shuttle_id.hardware``
|
||||
|
||||
Each addon can implement its own mechanism depending of this value
|
||||
and must call ``super``.
|
||||
|
||||
The method must send the command to the vertical lift to release (close)
|
||||
the tray.
|
||||
|
||||
Returns a message in bytes, that will be sent through
|
||||
``VerticalLiftShuttle._hardware_send_message()``.
|
||||
"""
|
||||
if self.hardware == "kardex":
|
||||
payload = self._hardware_kardex_prepare_release_payload()
|
||||
_logger.debug("Sending to kardex (release): {}", payload)
|
||||
else:
|
||||
payload = super()._hardware_vertical_lift_release_tray_payload()
|
||||
return payload
|
||||
|
||||
def _check_server_response(self, command):
|
||||
response = command.answer
|
||||
code, sep, remaining = response.partition("|")
|
||||
code = int(code)
|
||||
if code == 0:
|
||||
return True
|
||||
elif 1 <= code <= 99:
|
||||
command.error = "interface error %d" % code
|
||||
return False
|
||||
elif code in JMIF_STATUS and code < 200:
|
||||
command.error = "%d: %s" % (code, JMIF_STATUS[code])
|
||||
return False
|
||||
elif code in JMIF_STATUS and code < 300:
|
||||
command.error = "%d: %s" % (code, JMIF_STATUS[code])
|
||||
return True
|
||||
elif code in JMIF_STATUS:
|
||||
command.error = "%d: %s" % (code, JMIF_STATUS[code])
|
||||
elif 501 <= code <= 999:
|
||||
command.error = "%d: %s" % (code, "MM260 Error")
|
||||
elif 1000 <= code <= 32767:
|
||||
command.error = "%d: %s" % (code, "C2000TCP/C3000CGI machine error")
|
||||
elif 0xFF0 <= code == 0xFFF:
|
||||
command.error = "{:x}: {}".format(
|
||||
code, "C3000CGI machine error (global short)"
|
||||
)
|
||||
elif 0xFFF < code:
|
||||
command.error = "{:x}: {}".format(code, "C3000CGI machine error (long)")
|
||||
return False
|
||||
172
stock_vertical_lift_kardex/proxy/kardex-proxy.py
Executable file
172
stock_vertical_lift_kardex/proxy/kardex-proxy.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KardexProxyProtocol(asyncio.Protocol):
|
||||
def __init__(self, loop, queue, args):
|
||||
_logger.info("Proxy created")
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
self.queue = queue
|
||||
self.loop = loop
|
||||
self.args = args
|
||||
|
||||
def connection_made(self, transport):
|
||||
_logger.info("Proxy incoming cnx")
|
||||
self.transport = transport
|
||||
self.buffer = b""
|
||||
|
||||
def data_received(self, data):
|
||||
self.buffer += data
|
||||
_logger.info("Proxy: received %s", data)
|
||||
if len(self.buffer) > 65535:
|
||||
# prevent buffer overflow
|
||||
self.transport.close()
|
||||
|
||||
def eof_received(self):
|
||||
_logger.info("Proxy: received EOF")
|
||||
if self.buffer[-1] != b"\n":
|
||||
# bad format -> close
|
||||
self.transport.close()
|
||||
data = (
|
||||
self.buffer.replace(b"\r\n", b"\n")
|
||||
.replace(b"\n", b"\r\n")
|
||||
.decode("iso-8859-1", "replace")
|
||||
)
|
||||
self.loop.create_task(self.queue.put(data))
|
||||
self.buffer = b""
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
|
||||
|
||||
class KardexClientProtocol(asyncio.Protocol):
|
||||
def __init__(self, loop, queue, args):
|
||||
_logger.info("started kardex client")
|
||||
self.loop = loop
|
||||
self.queue = queue
|
||||
self.args = args
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
_logger.info("connected to kardex server %r", transport)
|
||||
|
||||
async def keepalive(self):
|
||||
while True:
|
||||
t = int(time.time())
|
||||
msg = "61|ping%d|SH1-1|0|0||||||||\r\n" % t
|
||||
await self.send_message(msg)
|
||||
await asyncio.sleep(20)
|
||||
|
||||
async def send_message(self, message):
|
||||
_logger.info("SEND %r", message)
|
||||
message = message.encode("iso-8859-1")
|
||||
self.transport.write(message)
|
||||
|
||||
async def process_queue(self):
|
||||
while True:
|
||||
message = await self.queue.get()
|
||||
await self.send_message(message)
|
||||
|
||||
def data_received(self, data):
|
||||
data = data.replace(b"\0", b"")
|
||||
_logger.info("RECV %s", data)
|
||||
self.buffer += data
|
||||
if b"\r\n" in self.buffer:
|
||||
msg, sep, rem = self.buffer.partition(b"\r\n")
|
||||
self.buffer = rem
|
||||
msg = msg.decode("iso-8859-1", "replace").strip()
|
||||
if msg.startswith("0|ping"):
|
||||
_logger.info("ping ok")
|
||||
else:
|
||||
_logger.info("notify odoo: %s", msg)
|
||||
self.loop.create_task(self.notify_odoo(msg))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.loop.stop()
|
||||
|
||||
async def notify_odoo(self, msg):
|
||||
url = self.args.odoo_url + "/vertical-lift"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
params = {"answer": msg, "secret": self.args.secret}
|
||||
async with session.post(url, data=params) as resp:
|
||||
resp_text = await resp.text()
|
||||
_logger.info("Reponse from Odoo: %s %s", resp.status, resp_text)
|
||||
|
||||
|
||||
def main(args, ssl_context=None):
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
loop = asyncio.get_event_loop()
|
||||
queue = asyncio.Queue(loop=loop)
|
||||
# create the main server
|
||||
coro = loop.create_server(
|
||||
lambda: KardexProxyProtocol(loop, queue, args), host=args.host, port=args.port
|
||||
)
|
||||
loop.run_until_complete(coro)
|
||||
|
||||
# create the connection to the JMIF client
|
||||
if args.kardex_use_tls:
|
||||
if ssl_context is None:
|
||||
ssl_context = ssl.create_default_context()
|
||||
else:
|
||||
ssl_context = None
|
||||
coro = loop.create_connection(
|
||||
lambda: KardexClientProtocol(loop, queue, args),
|
||||
host=args.kardex_host,
|
||||
port=args.kardex_port,
|
||||
ssl=ssl_context,
|
||||
)
|
||||
transport, client = loop.run_until_complete(coro)
|
||||
loop.create_task(client.keepalive())
|
||||
loop.create_task(client.process_queue())
|
||||
loop.run_forever()
|
||||
loop.close()
|
||||
|
||||
|
||||
def make_parser():
|
||||
listen_address = os.environ.get("INTERFACE", "0.0.0.0")
|
||||
listen_port = int(os.environ.get("PORT", "7654"))
|
||||
secret = os.environ.get("ODOO_CALLBACK_SECRET", "")
|
||||
odoo_url = os.environ.get("ODOO_URL", "http://localhost:8069")
|
||||
odoo_db = os.environ.get("ODOO_DB", "odoodb")
|
||||
kardex_host = os.environ.get("KARDEX_HOST", "kardex")
|
||||
kardex_port = int(os.environ.get("KARDEX_PORT", "9600"))
|
||||
kardex_use_tls = (
|
||||
False
|
||||
if os.environ.get("KARDEX_TLS", "") in ("", "0", "False", "FALSE")
|
||||
else True
|
||||
)
|
||||
parser = argparse.ArgumentParser()
|
||||
arguments = [
|
||||
("--host", listen_address, str),
|
||||
("--port", listen_port, int),
|
||||
("--odoo-url", odoo_url, str),
|
||||
("--odoo-db", odoo_db, str),
|
||||
("--secret", secret, str),
|
||||
("--kardex-host", kardex_host, str),
|
||||
("--kardex-port", kardex_port, str),
|
||||
("--kardex-use-tls", kardex_use_tls, bool),
|
||||
]
|
||||
for name, default, type_ in arguments:
|
||||
parser.add_argument(name, default=default, action="store", type=type_)
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = make_parser()
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
1
stock_vertical_lift_kardex/proxy/requirements.txt
Normal file
1
stock_vertical_lift_kardex/proxy/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
aiohttp
|
||||
106
stock_vertical_lift_kardex/proxy/test.py
Normal file
106
stock_vertical_lift_kardex/proxy/test.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# pylint: disable=W8116
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
|
||||
_logger = logging.getLogger("kardex.proxy")
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class KardexProxyProtocol(asyncio.Protocol):
|
||||
def __init__(self, loop, queue):
|
||||
_logger.info("Proxy created")
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
self.queue = queue
|
||||
self.loop = loop
|
||||
|
||||
def connection_made(self, transport):
|
||||
_logger.info("Proxy incoming cnx")
|
||||
self.transport = transport
|
||||
self.buffer = b""
|
||||
|
||||
def data_received(self, data):
|
||||
self.buffer += data
|
||||
_logger.info("Proxy: received %s", data)
|
||||
if len(self.buffer) > 65535:
|
||||
# prevent buffer overflow
|
||||
self.transport.close()
|
||||
|
||||
def eof_received(self):
|
||||
_logger.info("Proxy: received EOF")
|
||||
if self.buffer[-1] != b"\n":
|
||||
# bad format -> close
|
||||
self.transport.close()
|
||||
data = (
|
||||
self.buffer.replace(b"\r\n", b"\n")
|
||||
.replace(b"\n", b"\r\n")
|
||||
.decode("iso-8859-1", "replace")
|
||||
)
|
||||
task = self.loop.create_task(self.queue.put(data))
|
||||
self.buffer = b""
|
||||
print("toto", task)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
|
||||
|
||||
class KardexClientProtocol(asyncio.Protocol):
|
||||
def __init__(self, loop, queue):
|
||||
_logger.info("started kardex client")
|
||||
self.loop = loop
|
||||
self.queue = queue
|
||||
self.transport = None
|
||||
self.buffer = b""
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
_logger.info("connected to kardex server %r", transport)
|
||||
|
||||
async def keepalive(self):
|
||||
while True:
|
||||
t = int(time.time())
|
||||
msg = "61|ping%d|SH1-1|0|0||||||||\r\n" % t
|
||||
await self.send_message(msg)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def send_message(self, message):
|
||||
_logger.info("SEND %s", message)
|
||||
message = message.encode("iso-8859-1").ljust(1024, b"\0")
|
||||
self.transport.write(message)
|
||||
|
||||
async def process_queue(self):
|
||||
while True:
|
||||
message = await self.queue.get()
|
||||
await self.send_message(message)
|
||||
|
||||
def data_received(self, data):
|
||||
data = data.replace(b"\0", b"")
|
||||
_logger.info("RECV %s", data)
|
||||
self.buffer += data
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.loop.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_logger.info("starting")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_debug(1)
|
||||
queue = asyncio.Queue(loop=loop)
|
||||
coro = loop.create_server(
|
||||
lambda: KardexProxyProtocol(loop, queue), port=3000, family=socket.AF_INET
|
||||
)
|
||||
server = loop.run_until_complete(coro)
|
||||
coro = loop.create_connection(
|
||||
lambda: KardexClientProtocol(loop, queue), "localhost", 9600
|
||||
)
|
||||
transport, client = loop.run_until_complete(coro)
|
||||
print("%r" % transport)
|
||||
loop.create_task(client.keepalive())
|
||||
loop.create_task(client.process_queue())
|
||||
_logger.info("run loop")
|
||||
loop.run_forever()
|
||||
loop.close()
|
||||
1
stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst
Normal file
1
stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
2
stock_vertical_lift_kardex/readme/DESCRIPTION.rst
Normal file
2
stock_vertical_lift_kardex/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Add support for Kardex Remstar vertical lifts to the Vertical Lift
|
||||
module.
|
||||
2
stock_vertical_lift_kardex/readme/ROADMAP.rst
Normal file
2
stock_vertical_lift_kardex/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
* Add support of the hardware
|
||||
* handle multiple gates for one lift
|
||||
1
stock_vertical_lift_kardex/requirements.txt
Normal file
1
stock_vertical_lift_kardex/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
aiohttp
|
||||
433
stock_vertical_lift_kardex/static/description/index.html
Normal file
433
stock_vertical_lift_kardex/static/description/index.html
Normal file
@@ -0,0 +1,433 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
|
||||
<title>Vertical Lift - Kardex</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="vertical-lift-kardex">
|
||||
<h1 class="title">Vertical Lift - Kardex</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_vertical_lift_kardex"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_vertical_lift_kardex"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/153/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||
<p>Add support for Kardex Remstar vertical lifts to the Vertical Lift
|
||||
module.</p>
|
||||
<div class="admonition important">
|
||||
<p class="first admonition-title">Important</p>
|
||||
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
|
||||
</div>
|
||||
<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="id1">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#id1">Known issues / Roadmap</a></h1>
|
||||
<ul class="simple">
|
||||
<li>Add support of the hardware</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_vertical_lift_kardex%0Aversion:%2013.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="#id3">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Camptocamp</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Guewen Baconnier <<a class="reference external" href="mailto:guewen.baconnier@camptocamp.com">guewen.baconnier@camptocamp.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_vertical_lift_kardex">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
stock_vertical_lift_packaging_type/__init__.py
Normal file
0
stock_vertical_lift_packaging_type/__init__.py
Normal file
20
stock_vertical_lift_packaging_type/__manifest__.py
Normal file
20
stock_vertical_lift_packaging_type/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Vertical Lift Packaging type",
|
||||
"summary": "Provides integration with Vertical Lifts and packaging types",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Stock",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"stock_vertical_lift",
|
||||
# OCA / product-attribute
|
||||
"product_packaging_type",
|
||||
],
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"data": ["views/shuttle_screen_templates.xml"],
|
||||
"installable": True,
|
||||
"auto_install": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
* Simone Orsi <simahawk@gmail.com>
|
||||
@@ -0,0 +1,5 @@
|
||||
Glue module for `product_packaging_type` (from OCA/product-attribute)
|
||||
and `stock_vertical_lift`.
|
||||
|
||||
If both module are installed this module automatically gets installed
|
||||
and displays the packaging type's full name instead of the packaging's name.
|
||||
@@ -0,0 +1,7 @@
|
||||
<odoo>
|
||||
<template id="packagings" inherit_id="stock_vertical_lift.packagings">
|
||||
<xpath expr="//span[hasclass('packaging_name')]" position="attributes">
|
||||
<attribute name="t-esc">packaging.packaging_type_id.display_name</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
1
stock_vertical_lift_server_env/__init__.py
Normal file
1
stock_vertical_lift_server_env/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
16
stock_vertical_lift_server_env/__manifest__.py
Normal file
16
stock_vertical_lift_server_env/__manifest__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Vertical Lift - Server Environment",
|
||||
"summary": "Server Environment layer for Vertical Lift",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Stock",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": ["stock_vertical_lift", "server_environment"], # OCA/server-env
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"data": [],
|
||||
"installable": True,
|
||||
"auto_install": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
1
stock_vertical_lift_server_env/models/__init__.py
Normal file
1
stock_vertical_lift_server_env/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import vertical_lift_shuttle
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class VerticalLiftShuttle(models.Model):
|
||||
_name = "vertical.lift.shuttle"
|
||||
_inherit = ["vertical.lift.shuttle", "server.env.mixin"]
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
base_fields = super()._server_env_fields
|
||||
sftp_fields = {
|
||||
"hardware": {"compute_default": "_compute_default_hardware"},
|
||||
"server": {},
|
||||
"port": {},
|
||||
"use_tls": {},
|
||||
}
|
||||
sftp_fields.update(base_fields)
|
||||
return sftp_fields
|
||||
|
||||
def _compute_default_hardware(self):
|
||||
self.hardware = "simulation"
|
||||
1
stock_vertical_lift_server_env/readme/CONTRIBUTORS.rst
Normal file
1
stock_vertical_lift_server_env/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
1
stock_vertical_lift_server_env/readme/DESCRIPTION.rst
Normal file
1
stock_vertical_lift_server_env/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1 @@
|
||||
Adds OCA Server Environment features to Stock Vertical Lift.
|
||||
1
stock_vertical_lift_storage_type/__init__.py
Normal file
1
stock_vertical_lift_storage_type/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
stock_vertical_lift_storage_type/__manifest__.py
Normal file
21
stock_vertical_lift_storage_type/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Vertical Lift - Storage Type",
|
||||
"summary": "Compatibility layer for storage types on vertical lifts",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Stock",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"stock_vertical_lift", # OCA/stock-logistics-warehouse
|
||||
"stock_storage_type", # OCA/wms
|
||||
],
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"data": [
|
||||
"views/stock_location_tray_type_views.xml",
|
||||
"views/stock_location_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
4
stock_vertical_lift_storage_type/models/__init__.py
Normal file
4
stock_vertical_lift_storage_type/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import stock_location
|
||||
from . import stock_location_tray_type
|
||||
from . import stock_quant
|
||||
from . import vertical_lift_operation_put
|
||||
50
stock_vertical_lift_storage_type/models/stock_location.py
Normal file
50
stock_vertical_lift_storage_type/models/stock_location.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, exceptions, models
|
||||
|
||||
|
||||
class StockLocation(models.Model):
|
||||
_inherit = "stock.location"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if not self.env.context.get("_sync_tray_type"):
|
||||
for vals in vals_list:
|
||||
if vals.get("tray_type_id") and vals.get("location_storage_type_ids"):
|
||||
raise exceptions.UserError(
|
||||
_(
|
||||
"Error creating '{}': Location storage"
|
||||
" type must be set on the tray type"
|
||||
).format(vals.get("name"))
|
||||
)
|
||||
|
||||
records = super().create(vals_list)
|
||||
records._sync_tray_type_storage_types()
|
||||
return records
|
||||
|
||||
def write(self, values):
|
||||
if not self.env.context.get("_sync_tray_type"):
|
||||
if values.get("location_storage_type_ids"):
|
||||
if values.get("tray_type_id"):
|
||||
has_tray_type = self
|
||||
else:
|
||||
has_tray_type = self.filtered("tray_type_id")
|
||||
if has_tray_type:
|
||||
raise exceptions.UserError(
|
||||
_(
|
||||
"Error updating {}: Location storage"
|
||||
" type must be set on the tray type"
|
||||
).format(", ".join(has_tray_type.mapped("name")))
|
||||
)
|
||||
res = super().write(values)
|
||||
if values.get("tray_type_id"):
|
||||
self._sync_tray_type_storage_types()
|
||||
return res
|
||||
|
||||
def _sync_tray_type_storage_types(self):
|
||||
for location in self.with_context(_sync_tray_type=True):
|
||||
if not location.tray_type_id:
|
||||
continue
|
||||
storage_types = location.tray_type_id.location_storage_type_ids
|
||||
location.write({"location_storage_type_ids": [(6, 0, storage_types.ids)]})
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockLocationTrayType(models.Model):
|
||||
_inherit = "stock.location.tray.type"
|
||||
|
||||
location_storage_type_ids = fields.Many2many(
|
||||
comodel_name="stock.location.storage.type",
|
||||
help="Location storage types applied on the location using " "this tray type.",
|
||||
)
|
||||
|
||||
def write(self, values):
|
||||
res = super().write(values)
|
||||
if values.get("location_storage_type_ids"):
|
||||
self._sync_location_storage_type_ids()
|
||||
return res
|
||||
|
||||
def _sync_location_storage_type_ids(self):
|
||||
for tray_type in self:
|
||||
tray_type.location_ids.with_context(_sync_tray_type=True).write(
|
||||
{
|
||||
"location_storage_type_ids": [
|
||||
(6, 0, tray_type.location_storage_type_ids.ids)
|
||||
]
|
||||
}
|
||||
)
|
||||
18
stock_vertical_lift_storage_type/models/stock_quant.py
Normal file
18
stock_vertical_lift_storage_type/models/stock_quant.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
|
||||
_inherit = "stock.quant"
|
||||
|
||||
def _check_storage_type(self):
|
||||
# disable the checks when placing goods in vertical lift cells:
|
||||
# we still want to benefit of the storage type rules to select
|
||||
# a destination, but we want to allow selecting a different tray
|
||||
# type
|
||||
self = self.filtered(
|
||||
lambda quant: quant.location_id.vertical_lift_kind != "cell"
|
||||
)
|
||||
super()._check_storage_type()
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, models
|
||||
|
||||
|
||||
class VerticalLiftOperationPut(models.Model):
|
||||
_inherit = "vertical.lift.operation.put"
|
||||
|
||||
# In the base module, when a good is scanned for a put-away, the user must
|
||||
# then scan a tray type, which will be used to find an available cell of
|
||||
# this type. When we use storage types (OCA/wms::stock_storage_type), we
|
||||
# shouldn't need to scan a tray type if we already have a storage type,
|
||||
# that could be used to find an available cell location for this type
|
||||
# (applying all the possible restrictions from storage type).
|
||||
|
||||
def _transitions(self):
|
||||
transitions = super()._transitions()
|
||||
updated_transitions = []
|
||||
for transition in transitions:
|
||||
states = (transition.current_state, transition.next_state)
|
||||
if states == ("scan_tray_type", "save"):
|
||||
# insert new transitions just before the normal transition
|
||||
# scanning the tray type, that will bypass it when we have
|
||||
# a storage type
|
||||
updated_transitions.append(
|
||||
self.Transition(
|
||||
"scan_tray_type",
|
||||
"save",
|
||||
lambda self: self._has_storage_type()
|
||||
and self._putaway_with_storage_type(),
|
||||
# this is the trick that makes the transition applies
|
||||
# its function and directly jumps to save
|
||||
direct_eval=True,
|
||||
)
|
||||
)
|
||||
updated_transitions.append(
|
||||
self.Transition(
|
||||
"scan_tray_type",
|
||||
"scan_source",
|
||||
# the transition above returned False because it could
|
||||
# not find a free space, in that case, abort the
|
||||
# put-away for this line in this shuttle
|
||||
lambda self: self._has_storage_type()
|
||||
and self._put_away_with_storage_type_failed()
|
||||
and self.clear_current_move_line(),
|
||||
# this is the trick that makes the transition applies
|
||||
# its function and directly jumps to save
|
||||
direct_eval=True,
|
||||
)
|
||||
)
|
||||
# if none of the 2 transitions above is applied (because
|
||||
# self._has_storage_type() is False), the state remains
|
||||
# `scan_tray_type`, for the base transition doesn't have
|
||||
# `direct_eval=True`
|
||||
updated_transitions.append(transition)
|
||||
|
||||
return tuple(updated_transitions)
|
||||
|
||||
def _has_storage_type_domain(self):
|
||||
move_line = self.current_move_line_id
|
||||
package_storage_type = move_line.package_id.package_storage_type_id
|
||||
# When a put-away is done based on the package's storage type and no
|
||||
# destination is found, we can have 2 reasons:
|
||||
#
|
||||
# 1. No location is available according to the storage types rules,
|
||||
# for instance because they are full
|
||||
# 2. The storage type of the package doesn't have any location
|
||||
# configured in the shuttle's locations
|
||||
#
|
||||
# We want to differentiate the 2 cases and handle them differently.
|
||||
# For 2. we consider the storage type is not meant to be in any
|
||||
# shuttle's tray but the user still tries: act the same way as if we
|
||||
# had no storage type at all, the user has to scan the tray type.
|
||||
# For 1. we try to find a destination location and if none is found,
|
||||
# a notification indicates that the shuttle is full.
|
||||
# In any case, the user should always be able to scan a different
|
||||
# tray type.
|
||||
return [
|
||||
("id", "!=", self.location_id.id),
|
||||
("id", "child_of", self.location_id.id),
|
||||
(
|
||||
"allowed_location_storage_type_ids",
|
||||
"in",
|
||||
package_storage_type.location_storage_type_ids.ids,
|
||||
),
|
||||
]
|
||||
|
||||
def _has_storage_type(self):
|
||||
domain = self._has_storage_type_domain()
|
||||
# we don't care about order and count, only if we have at least one
|
||||
# configured location for the storage type: sorting by id and limit by
|
||||
# 1 will give the best explain plan
|
||||
compatible_locations = self.env["stock.location"].search(
|
||||
domain, limit=1, order="id"
|
||||
)
|
||||
return bool(compatible_locations)
|
||||
|
||||
def _putaway_with_storage_type(self):
|
||||
move_line = self.current_move_line_id
|
||||
# Trigger the put-away application to place it somewhere inside
|
||||
# the current shuttle's location.
|
||||
new_destination = move_line.location_dest_id._get_pack_putaway_strategy(
|
||||
self.location_id, move_line.package_id.quant_ids, move_line.product_id
|
||||
)
|
||||
if new_destination and new_destination.vertical_lift_kind == "cell":
|
||||
move_line.location_dest_id = new_destination
|
||||
move_line.package_level_id.location_dest_id = new_destination
|
||||
self.fetch_tray()
|
||||
return True
|
||||
# no destination found: all the trays for this tray type are full, or rejected
|
||||
# by the location storage type rules
|
||||
return False
|
||||
|
||||
def _put_away_with_storage_type_failed(self):
|
||||
move_line = self.current_move_line_id
|
||||
storage_type = move_line.package_id.package_storage_type_id
|
||||
self.env.user.notify_warning(
|
||||
_("No free space found for storage type '{}' in shuttle '{}'").format(
|
||||
storage_type.name, self.name
|
||||
)
|
||||
)
|
||||
return True
|
||||
1
stock_vertical_lift_storage_type/readme/CONTRIBUTORS.rst
Normal file
1
stock_vertical_lift_storage_type/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
14
stock_vertical_lift_storage_type/readme/DESCRIPTION.rst
Normal file
14
stock_vertical_lift_storage_type/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Compatibility layer between Stock Vertical Lift and Putaway Storage Types (OCA/wms).
|
||||
|
||||
In the vertical lift's Putaway screen, when a good is scanned for a putaway, the
|
||||
user has to scan the tray type of the corresponding size, so an empty place in a
|
||||
matching tray is found. When we use storage types, we should know what tray is
|
||||
compatible with the storage type.
|
||||
|
||||
Changes with this module:
|
||||
|
||||
* The storage types of trays cannot be selected in the locations form, they have
|
||||
to be set in the Tray types.
|
||||
* In the lift put-away screen, when a package has a storage type, the user isn't
|
||||
asked to scan a tray type, instead, the putaway of the Package Storage Type is
|
||||
applied.
|
||||
3
stock_vertical_lift_storage_type/tests/__init__.py
Normal file
3
stock_vertical_lift_storage_type/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import test_put
|
||||
from . import test_stock_location
|
||||
from . import test_tray_type
|
||||
27
stock_vertical_lift_storage_type/tests/common.py
Normal file
27
stock_vertical_lift_storage_type/tests/common.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.addons.stock_vertical_lift.tests.common import VerticalLiftCase
|
||||
|
||||
|
||||
class TrayTypeCommonCase(VerticalLiftCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.TrayType = cls.env["stock.location.tray.type"]
|
||||
cls.location_2b = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2b"
|
||||
)
|
||||
cls.location_2d = cls.env.ref(
|
||||
"stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2d"
|
||||
)
|
||||
LocStorageType = cls.env["stock.location.storage.type"]
|
||||
cls.location_storage_type_buffer = LocStorageType.create(
|
||||
{"name": "VLift Buffer"}
|
||||
)
|
||||
cls.location_storage_type_small_8x = LocStorageType.create(
|
||||
{"name": "Small 8x", "only_empty": True}
|
||||
)
|
||||
cls.storage_types = (
|
||||
cls.location_storage_type_small_8x | cls.location_storage_type_buffer
|
||||
)
|
||||
165
stock_vertical_lift_storage_type/tests/test_put.py
Normal file
165
stock_vertical_lift_storage_type/tests/test_put.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.addons.stock_vertical_lift.tests.common import VerticalLiftCase
|
||||
|
||||
|
||||
class TestPut(VerticalLiftCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.wh = cls.env.ref("stock.warehouse0")
|
||||
cls.wh.wh_input_stock_loc_id.active = True
|
||||
cls.wh.int_type_id.active = True
|
||||
|
||||
# used on the vertical lift top level
|
||||
LocStorageType = cls.env["stock.location.storage.type"]
|
||||
cls.location_storage_type_buffer = LocStorageType.create(
|
||||
{"name": "VLift Buffer"}
|
||||
)
|
||||
cls.location_storage_type_small_8x = LocStorageType.create(
|
||||
{"name": "Small 8x", "only_empty": True}
|
||||
)
|
||||
|
||||
PackageStorageType = cls.env["stock.package.storage.type"]
|
||||
# package storage type used only to putaway a package temporarily in
|
||||
# the vertical lift view location before being put in a shuttle, which
|
||||
# will not be configured in any shuttle's locations, we'll use when
|
||||
# the operator knows that a good has to go in a shuttle, but doesn't
|
||||
# know yet in which tray type: when the first putaway is done, the good
|
||||
# stays in the vertical lift view (above the shuttles), then, when the
|
||||
# user scans the package in a shuttle, they have to scan a tray type.
|
||||
cls.package_storage_type_buffer = PackageStorageType.create(
|
||||
{
|
||||
"name": "VLift Box",
|
||||
"location_storage_type_ids": [
|
||||
(4, cls.location_storage_type_buffer.id),
|
||||
],
|
||||
}
|
||||
)
|
||||
# storage type used for Tray 1A, user won't have to scan a tray type
|
||||
# when this storage type is already set on the package
|
||||
cls.package_storage_type_small_8x = PackageStorageType.create(
|
||||
{
|
||||
"name": "Small 8x",
|
||||
"location_storage_type_ids": [
|
||||
(4, cls.location_storage_type_small_8x.id),
|
||||
(4, cls.location_storage_type_buffer.id),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
cls.location_shuttle1 = cls.shuttle.location_id
|
||||
cls.vertical_lift_loc.location_storage_type_ids = (
|
||||
cls.location_storage_type_buffer
|
||||
)
|
||||
cls.vertical_lift_loc.pack_putaway_strategy = "none"
|
||||
cls.location_shuttle1.location_storage_type_ids = (
|
||||
cls.location_storage_type_small_8x
|
||||
)
|
||||
cls.location_shuttle1.pack_putaway_strategy = "ordered_locations"
|
||||
|
||||
cls.env["stock.storage.location.sequence"].create(
|
||||
{
|
||||
"package_storage_type_id": cls.package_storage_type_small_8x.id,
|
||||
"sequence": 1,
|
||||
"location_id": cls.vertical_lift_loc.id,
|
||||
}
|
||||
)
|
||||
cls.env["stock.storage.location.sequence"].create(
|
||||
{
|
||||
"package_storage_type_id": cls.package_storage_type_small_8x.id,
|
||||
"sequence": 2,
|
||||
"location_id": cls.location_shuttle1.id,
|
||||
}
|
||||
)
|
||||
|
||||
cls.package = cls.env["stock.quant.package"].create({})
|
||||
cls._update_qty_in_location(
|
||||
cls.wh.wh_input_stock_loc_id, cls.product_socks, 10, package=cls.package
|
||||
)
|
||||
|
||||
cls.int_picking = cls._create_simple_picking_int(
|
||||
cls.product_socks, 10, cls.vertical_lift_loc
|
||||
)
|
||||
cls.int_picking.action_confirm()
|
||||
cls.int_picking.action_assign()
|
||||
|
||||
@classmethod
|
||||
def _create_simple_picking_int(cls, product, quantity, dest_location):
|
||||
return cls.env["stock.picking"].create(
|
||||
{
|
||||
"picking_type_id": cls.wh.int_type_id.id,
|
||||
"location_id": cls.wh.wh_input_stock_loc_id.id,
|
||||
"location_dest_id": dest_location.id,
|
||||
"move_lines": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": product.name,
|
||||
"product_id": product.id,
|
||||
"product_uom": product.uom_id.id,
|
||||
"product_uom_qty": quantity,
|
||||
"picking_type_id": cls.wh.int_type_id.id,
|
||||
"location_id": cls.wh.wh_input_stock_loc_id.id,
|
||||
"location_dest_id": dest_location.id,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_storage_type_put_away(self):
|
||||
self.package.package_storage_type_id = self.package_storage_type_small_8x
|
||||
move_line = self.int_picking.move_line_ids
|
||||
self.assertEqual(move_line.location_dest_id, self.vertical_lift_loc)
|
||||
self.assertEqual(
|
||||
move_line.package_level_id.location_dest_id, self.vertical_lift_loc
|
||||
)
|
||||
|
||||
operation = self._open_screen("put")
|
||||
# we begin with an empty screen, user has to scan a package, product,
|
||||
# or lot
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
operation.on_barcode_scanned(self.package.name)
|
||||
|
||||
self.assertEqual(operation.current_move_line_id, move_line)
|
||||
# the dest location was Vertical Lift, it has been change to Vertical
|
||||
# Lift/Shuttle 1, and the computation from there took the first cell
|
||||
# available, we should be the pos x1 and y1 in the tray A.
|
||||
self.assertEqual(move_line.location_dest_id, self.location_1a_x1y1)
|
||||
self.assertEqual(
|
||||
move_line.package_level_id.location_dest_id, self.location_1a_x1y1
|
||||
)
|
||||
|
||||
# the state goes straight to "save", as we don't need to scan the tray type
|
||||
# when a putaway is available
|
||||
self.assertEqual(operation.state, "save")
|
||||
|
||||
def test_storage_type_not_configured(self):
|
||||
# if we do have a storage type, but the storage type is not used in the
|
||||
# shuttle (although it may be used on the lift view location, so we can have an
|
||||
# initial putaway rule that puts the package in the lift view location first),
|
||||
# we have to ask to scan a tray type
|
||||
self.package.package_storage_type_id = self.package_storage_type_buffer
|
||||
move_line = self.int_picking.move_line_ids
|
||||
self.assertEqual(move_line.location_dest_id, self.vertical_lift_loc)
|
||||
self.assertEqual(
|
||||
move_line.package_level_id.location_dest_id, self.vertical_lift_loc
|
||||
)
|
||||
|
||||
operation = self._open_screen("put")
|
||||
# we begin with an empty screen, user has to scan a package, product,
|
||||
# or lot
|
||||
self.assertEqual(operation.state, "scan_source")
|
||||
operation.on_barcode_scanned(self.package.name)
|
||||
|
||||
self.assertEqual(operation.current_move_line_id, move_line)
|
||||
# it stays here, as the put-away has no rule to put the "buffer box" in
|
||||
# any location
|
||||
self.assertEqual(move_line.location_dest_id, self.vertical_lift_loc)
|
||||
|
||||
# and the user has to scan a tray type (so we are back to the normal
|
||||
# flow, tested in stock_vertical_lift)
|
||||
self.assertEqual(operation.state, "scan_tray_type")
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import exceptions
|
||||
|
||||
from .common import TrayTypeCommonCase
|
||||
|
||||
|
||||
class TestTrayTypeLocation(TrayTypeCommonCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.tray_type = cls.env.ref(
|
||||
"stock_location_tray.stock_location_tray_type_small_8x"
|
||||
)
|
||||
cls.tray_type.write(
|
||||
{"location_storage_type_ids": [(6, 0, cls.storage_types.ids)]}
|
||||
)
|
||||
cls.locations = cls.location_2a | cls.location_2b
|
||||
|
||||
def test_location_create_sync(self):
|
||||
locations = self.env["stock.location"].create(
|
||||
[
|
||||
{
|
||||
"name": "tray test 1",
|
||||
"location_id": self.shuttle.location_id.id,
|
||||
"usage": "internal",
|
||||
"tray_type_id": self.tray_type.id,
|
||||
},
|
||||
{
|
||||
"name": "tray test 2",
|
||||
"location_id": self.shuttle.location_id.id,
|
||||
"usage": "internal",
|
||||
"tray_type_id": self.tray_type.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.assertEqual(locations[0].location_storage_type_ids, self.storage_types)
|
||||
self.assertEqual(locations[1].location_storage_type_ids, self.storage_types)
|
||||
|
||||
def test_location_write_sync(self):
|
||||
self.locations.tray_type_id = self.tray_type
|
||||
self.assertEqual(self.location_2a.location_storage_type_ids, self.storage_types)
|
||||
self.assertEqual(self.location_2b.location_storage_type_ids, self.storage_types)
|
||||
|
||||
def test_location_create_error(self):
|
||||
with self.assertRaisesRegex(exceptions.UserError, "Error creating.*"):
|
||||
self.env["stock.location"].create(
|
||||
[
|
||||
{
|
||||
"name": "tray test 1",
|
||||
"location_id": self.shuttle.location_id.id,
|
||||
"usage": "internal",
|
||||
"tray_type_id": self.tray_type.id,
|
||||
"location_storage_type_ids": [
|
||||
(6, 0, self.location_storage_type_buffer.ids)
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "tray test 2",
|
||||
"location_id": self.shuttle.location_id.id,
|
||||
"usage": "internal",
|
||||
"tray_type_id": self.tray_type.id,
|
||||
"location_storage_type_ids": [
|
||||
(6, 0, self.location_storage_type_buffer.ids)
|
||||
],
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def test_location_write_both_fields_error(self):
|
||||
with self.assertRaisesRegex(exceptions.UserError, "Error updating.*"):
|
||||
self.locations.write(
|
||||
{
|
||||
"tray_type_id": self.tray_type.id,
|
||||
"location_storage_type_ids": [
|
||||
(6, 0, self.location_storage_type_buffer.ids)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_location_write_storage_type_error(self):
|
||||
with self.assertRaisesRegex(exceptions.UserError, "Error updating.*"):
|
||||
self.locations.write(
|
||||
{
|
||||
"location_storage_type_ids": [
|
||||
(6, 0, self.location_storage_type_buffer.ids)
|
||||
],
|
||||
}
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user