Add stock_vertical_lift module

This commit is contained in:
Guewen Baconnier
2019-06-11 16:50:48 +02:00
parent 3eb8429ef8
commit 311fb24447
42 changed files with 2447 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View 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.

View File

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

View File

@@ -0,0 +1,34 @@
# 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': '12.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
],
'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/vertical_lift_shuttle_views.xml',
'views/stock_vertical_lift_templates.xml',
'views/shuttle_screen_templates.xml',
'security/ir.model.access.csv',
],
'installable': True,
'development_status': 'Alpha',
}

View File

@@ -0,0 +1,32 @@
<?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="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"/>
<field name="property_stock_inventory" ref="stock.location_inventory"/>
</record>
<record id="product_recovery_socks" model="product.product">
<field name="default_code">RS300</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"/>
<field name="property_stock_inventory" ref="stock.location_inventory"/>
</record>
</odoo>

View File

@@ -0,0 +1,20 @@
<?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_validate">
<function eval="[[('state','=','draft'),('id', '=', ref('stock_vertical_lift.stock_inventory_vertical_lift_0'))]]" model="stock.inventory" name="search"/>
</function>
</odoo>

View File

@@ -0,0 +1,116 @@
<?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"/>
<field name="company_id"></field>
</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>

View File

@@ -0,0 +1,30 @@
<?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>
</odoo>

View File

@@ -0,0 +1,22 @@
<?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>

View 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

View 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

View File

@@ -0,0 +1,4 @@
from . import vertical_lift_shuttle
from . import stock_location
from . import stock_move
from . import stock_quant

View File

@@ -0,0 +1,40 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, 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,
)
@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)
if kind:
location.vertical_lift_kind = kind

View File

@@ -0,0 +1,23 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class StockMove(models.Model):
_inherit = 'stock.move'
@api.multi
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.shuttle. 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, becaus the state of the lines is a related
# to this one).
self.env['vertical.lift.shuttle'].invalidate_cache(
['number_of_ops', 'number_of_ops_all']
)
return result

View File

@@ -0,0 +1,16 @@
# 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.shuttle. But we know that when the quantity
# of quant changes, we can invalidate the field on the shuttles.
self.env['vertical.lift.shuttle'].invalidate_cache(['tray_qty'])
return result

View File

@@ -0,0 +1,412 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, exceptions, fields, models
from odoo.addons.base_sparse_field.models.fields import Serialized
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
)
current_move_line_id = fields.Many2one(comodel_name='stock.move.line')
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',
)
operation_descr = fields.Char(
string="Operation",
default="Scan New Destination Location",
readonly=True,
)
# tray information (will come from stock.location or a new tray model)
tray_location_id = fields.Many2one(
comodel_name='stock.location',
compute='_compute_tray_matrix',
string='Tray Location',
)
tray_name = fields.Char(compute='_compute_tray_matrix', string='Tray Name')
tray_type_id = fields.Many2one(
comodel_name='stock.location.tray.type',
compute='_compute_tray_matrix',
string='Tray Type',
)
tray_type_code = fields.Char(
compute='_compute_tray_matrix', string='Tray Code'
)
tray_x = fields.Integer(string='X', compute='_compute_tray_matrix')
tray_y = fields.Integer(string='Y', compute='_compute_tray_matrix')
tray_matrix = Serialized(string='Cells', compute='_compute_tray_matrix')
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=True,
)
# TODO add a glue addon with product_expiry to add the field
_barcode_scanned = fields.Char(
"Barcode Scanned",
help="Value of the last barcode scanned.",
store=False,
)
def on_barcode_scanned(self, barcode):
self.ensure_one()
# FIXME notify_info is only for the demo
self.env.user.notify_info('Scanned barcode: {}'.format(barcode))
method = 'on_barcode_scanned_{}'.format(self.mode)
getattr(self, method)(barcode)
def on_barcode_scanned_pick(self, barcode):
location = self.env['stock.location'].search(
[('barcode', '=', barcode)]
)
if location:
self.current_move_line_id.location_dest_id = location
self.operation_descr = _('Save')
else:
self.env.user.notify_warning(
_('No location found for barcode {}').format(barcode)
)
def on_barcode_scanned_put(self, barcode):
pass
def on_barcode_scanned_inventory(self, barcode):
pass
@api.model
def _selection_hardware(self):
return [('simulation', 'Simulation')]
@api.depends('current_move_line_id.product_id.packaging_ids')
def _compute_product_packagings(self):
for record in self:
if not record.current_move_line_id:
continue
product = record.current_move_line_id.product_id
values = {
'packagings': [
{
'name': pkg.name,
'qty': pkg.qty,
'unit': product.uom_id.name,
}
for pkg in product.packaging_ids
]
}
content = self.env['ir.qweb'].render(
'stock_vertical_lift.packagings', values
)
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):
continue
product = record.current_move_line_id.product_id
quants = self.env['stock.quant'].search(
[
('location_id', '=', record.tray_location_id.id),
('product_id', '=', product.id),
]
)
record.tray_qty = sum(quants.mapped('quantity'))
@api.depends()
def _compute_tray_matrix(self):
for record in self:
modes = {
'pick': 'location_id',
'put': 'location_dest_id',
# TODO what to do for inventory?
'inventory': 'location_id',
}
location = record.current_move_line_id[modes[record.mode]]
tray_type = location.location_id.tray_type_id
selected = []
cells = []
if location:
selected = location._tray_cell_coords()
cells = location._tray_cell_matrix()
# 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 = {
# x, y: position of the selected cell
'selected': selected,
# 0 is empty, 1 is not
'cells': cells,
}
def _domain_move_lines_to_do(self):
domain = [
# TODO check state
('state', '=', 'assigned')
]
domain_extensions = {
'pick': [('location_id', 'child_of', self.location_id.id)],
# TODO ensure that we cannot have the same ml in 2 shuttles (cannot
# happen with 'pick' as they are in the shuttle's location)
'put': [('location_dest_id', 'child_of', self.location_id.id)],
# TODO
'inventory': [('id', '=', 0)],
}
return domain + domain_extensions[self.mode]
def _domain_move_lines_to_do_all(self):
domain = [
# TODO check state
('state', '=', 'assigned')
]
# TODO search only in the view being a parent of shuttle's location
shuttle_locations = self.env['stock.location'].search(
[('vertical_lift_kind', '=', 'view')]
)
domain_extensions = {
'pick': [('location_id', 'child_of', shuttle_locations.ids)],
'put': [('location_dest_id', 'child_of', shuttle_locations.ids)],
# TODO
'inventory': [('id', '=', 0)],
}
return domain + domain_extensions[self.mode]
def count_move_lines_to_do(self):
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):
self.ensure_one()
return self.env['stock.move.line'].search_count(
self._domain_move_lines_to_do_all()
)
def button_release(self):
if self.current_move_line_id:
self._hardware_switch_off_laser_pointer()
self._hardware_close_tray()
self.select_next_move_line()
if not self.current_move_line_id:
# sorry not sorry
return {
'effect': {
'fadeout': 'slow',
'message': _('Congrats, you cleared the queue!'),
'img_url': '/web/static/src/img/smile.svg',
'type': 'rainbow_man',
}
}
def process_current_pick(self):
# test code, TODO the smart one
# (scan of barcode increments qty, save calls action_done?)
line = self.current_move_line_id
if line.state != 'done':
line.qty_done = line.product_qty
line.move_id._action_done()
def process_current_put(self):
raise exceptions.UserError(_('Put workflow not implemented'))
def process_current_inventory(self):
raise exceptions.UserError(_('Inventory workflow not implemented'))
def button_save(self):
if not (self and self.current_move_line_id):
return
self.ensure_one()
method = 'process_current_{}'.format(self.mode)
getattr(self, method)()
self.operation_descr = _('Release')
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
# TODO use a state machine to define next steps and
# description?
descr = (
_('Scan New Destination Location')
if next_move_line
else _('No operations')
)
self.operation_descr = descr
if next_move_line:
self._hardware_switch_on_laser_pointer()
self._hardware_open_tray()
def action_open_screen(self):
self.select_next_move_line()
self.ensure_one()
screen_xmlid = (
'stock_vertical_lift.vertical_lift_shuttle_view_form_screen'
)
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'views': [[self.env.ref(screen_xmlid).id, 'form']],
'res_id': self.id,
'target': 'fullscreen',
'flags': {
'headless': True,
'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_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.select_next_move_line()
def switch_put(self):
self.mode = 'put'
self.select_next_move_line()
def switch_inventory(self):
self.mode = 'inventory'
self.select_next_move_line()
def _hardware_switch_on_laser_pointer(self):
if self.hardware == 'simulation':
self.env.user.notify_info(
message=_('Laser pointer on x{} y{}').format(
self.tray_x, self.tray_y
),
title=_('Lift Simulation'),
)
def _hardware_switch_off_laser_pointer(self):
if self.hardware == 'simulation':
self.env.user.notify_info(
message=_('Switch off laser pointer'),
title=_('Lift Simulation'),
)
def _hardware_open_tray(self):
if self.hardware == 'simulation':
self.env.user.notify_info(
message=_('Opening tray {}').format(self.tray_name),
title=_('Lift Simulation'),
)
def _hardware_close_tray(self):
if self.hardware == 'simulation':
self.env.user.notify_info(
message=_('Closing tray {}').format(self.tray_name),
title=_('Lift Simulation'),
)
class VerticalLiftShuttleManualBarcode(models.TransientModel):
_name = 'vertical.lift.shuttle.manual.barcode'
_description = 'Action to input a barcode'
barcode = fields.Char(string="Barcode")
@api.multi
def button_save(self):
shuttle_id = self.env.context.get('active_id')
shuttle = self.env['vertical.lift.shuttle'].browse(shuttle_id).exists()
if not shuttle:
return
if self.barcode:
shuttle.on_barcode_scanned(self.barcode)

View File

@@ -0,0 +1,39 @@
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.

View File

@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>

View 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.

View File

@@ -0,0 +1,3 @@
* Complete Pick screen and workflow (currently enough for a demo, not for production)
* Implement Put-away screen and workflow
* Implement Inventory screen and workflow

View File

@@ -0,0 +1,3 @@
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_vertical_lift_shuttle_stock_user access_vertical_lift_shuttle stock user model_vertical_lift_shuttle stock.group_stock_user 1 0 0 0
3 access_vertical_lift_shuttle_manager access_vertical_lift_shuttle stock manager model_vertical_lift_shuttle stock.group_stock_manager 1 1 1 1

View 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 &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</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>

View File

@@ -0,0 +1,55 @@
odoo.define('stock_vertical_lift.vertical_lift', function (require) {
"use strict";
var core = require('web.core');
var KanbanRecord = require('web.KanbanRecord');
var basicFields = require('web.basic_fields');
var field_registry = require('web.field_registry');
var FieldInteger = basicFields.FieldInteger;
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);
}
},
});
var ExitButton = FieldInteger.extend({
tagName: 'button',
className: 'btn btn-danger btn-block btn-lg o_shuttle_exit',
events: {
'click': '_onClick',
},
_render: function () {
this.$el.text(this.string);
},
_onClick: function () {
// the only reason to have this field widget is to be able
// to inject clear_breadcrumbs in the action:
// it will revert back to a normal - non-headless - view
this.do_action('stock_vertical_lift.vertical_lift_shuttle_action', {
clear_breadcrumbs: true,
});
},
});
field_registry.add('vlift_shuttle_exit_button', ExitButton);
return {
ExitButton: ExitButton,
};
});

View File

@@ -0,0 +1,124 @@
.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: 2.0em;
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_shuttle_exit {
text-align: center;
}
}
.o_vlift_shuttle_manual_barcode {
.o_field_char {
padding: 1em;
font-size: 2em;
}
.btn {
padding: 1em;
font-size: 2em;
text-transform: uppercase;
}
}
}

View File

@@ -0,0 +1,2 @@
from . import test_location
from . import test_vertical_lift_shuttle

View File

@@ -0,0 +1,49 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
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.vertical_lift_loc = cls.env.ref(
'stock_vertical_lift.stock_location_vertical_lift'
)
@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,
},
)
],
}
)

View File

@@ -0,0 +1,40 @@
# 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')

View File

@@ -0,0 +1,252 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import unittest
from odoo import _, exceptions
from .common import VerticalLiftCase
class TestVerticalLiftTrayType(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.current_move_line_id, self.out_move_line)
def test_switch_put(self):
self.shuttle.switch_put()
self.assertEqual(self.shuttle.mode, 'put')
# TODO check that we have an incoming move when switching
self.assertEqual(
self.shuttle.current_move_line_id,
self.env['stock.move.line'].browse(),
)
def test_switch_inventory(self):
self.shuttle.switch_inventory()
self.assertEqual(self.shuttle.mode, 'inventory')
# TODO check that we have what we should (what?)
self.assertEqual(
self.shuttle.current_move_line_id,
self.env['stock.move.line'].browse(),
)
def test_pick_action_open_screen(self):
self.shuttle.switch_pick()
action = self.shuttle.action_open_screen()
self.assertTrue(self.shuttle.current_move_line_id)
self.assertEqual(action['type'], 'ir.actions.act_window')
self.assertEqual(action['res_model'], 'vertical.lift.shuttle')
self.assertEqual(action['res_id'], self.shuttle.id)
def test_pick_select_next_move_line(self):
self.shuttle.switch_pick()
self.shuttle.select_next_move_line()
self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line)
self.assertEqual(
self.shuttle.operation_descr,
_('Scan New Destination Location')
)
def test_pick_save(self):
self.shuttle.switch_pick()
self.shuttle.current_move_line_id = self.out_move_line
self.shuttle.button_save()
self.assertEqual(
self.shuttle.current_move_line_id.state,
'done'
)
self.assertEqual(self.shuttle.operation_descr, _('Release'))
def test_pick_related_fields(self):
self.shuttle.switch_pick()
ml = self.shuttle.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(self.shuttle.tray_location_id, ml.location_id)
self.assertEqual(
self.shuttle.tray_name,
# parent = tray
ml.location_id.location_id.name,
)
self.assertEqual(
self.shuttle.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(
self.shuttle.tray_type_code,
ml.location_id.location_id.tray_type_id.code,
)
self.assertEqual(self.shuttle.tray_x, ml.location_id.posx)
self.assertEqual(self.shuttle.tray_y, ml.location_id.posy)
# Move line related fields
self.assertEqual(self.shuttle.picking_id, ml.picking_id)
self.assertEqual(self.shuttle.picking_origin, ml.picking_id.origin)
self.assertEqual(
self.shuttle.picking_partner_id, ml.picking_id.partner_id
)
self.assertEqual(self.shuttle.product_id, ml.product_id)
self.assertEqual(self.shuttle.product_uom_id, ml.product_uom_id)
self.assertEqual(self.shuttle.product_uom_qty, ml.product_uom_qty)
self.assertEqual(self.shuttle.qty_done, ml.qty_done)
self.assertEqual(self.shuttle.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
shuttle2 = self.env.ref(
'stock_vertical_lift.stock_vertical_lift_demo_shuttle_2'
)
self.assertEqual(shuttle1.number_of_ops, 4)
self.assertEqual(shuttle2.number_of_ops, 2)
self.assertEqual(shuttle1.number_of_ops_all, 6)
self.assertEqual(shuttle2.number_of_ops_all, 6)
# Process a line, should change the numbers.
shuttle1.select_next_move_line()
shuttle1.process_current_pick()
self.assertEqual(shuttle1.number_of_ops, 3)
self.assertEqual(shuttle2.number_of_ops, 2)
self.assertEqual(shuttle1.number_of_ops_all, 5)
self.assertEqual(shuttle2.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(shuttle1.number_of_ops, 3)
self.assertEqual(shuttle2.number_of_ops, 3)
self.assertEqual(shuttle1.number_of_ops_all, 6)
self.assertEqual(shuttle2.number_of_ops_all, 6)
@unittest.skip('Not implemented')
def test_put_count_move_lines(self):
pass
@unittest.skip('Not implemented')
def test_inventory_count_move_lines(self):
pass
@unittest.skip('Not implemented')
def test_on_barcode_scanned(self):
# test to implement when the code is implemented
pass
def test_button_release(self):
# for the test, we'll consider our last line has been delivered
self.out_move_line.qty_done = self.out_move_line.product_qty
self.out_move_line.move_id._action_done()
# release, no further operation in queue
result = self.shuttle.button_release()
self.assertFalse(self.shuttle.current_move_line_id)
self.assertEqual(self.shuttle.operation_descr, _('No operations'))
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_process_current_pick(self):
self.shuttle.switch_pick()
self.shuttle.current_move_line_id = self.out_move_line
qty_to_process = self.out_move_line.product_qty
self.shuttle.process_current_pick()
self.assertEqual(self.out_move_line.state, 'done')
self.assertEqual(self.out_move_line.qty_done, qty_to_process)
def test_process_current_put(self):
# test to implement when the code is implemented
with self.assertRaises(exceptions.UserError):
self.shuttle.process_current_put()
def test_process_current_inventory(self):
# test to implement when the code is implemented
with self.assertRaises(exceptions.UserError):
self.shuttle.process_current_inventory()
def test_matrix(self):
self.shuttle.switch_pick()
self.shuttle.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(
self.shuttle.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
self.shuttle.current_move_line_id = self.out_move_line
self._update_quantity_in_cell(cell, self.out_move_line.product_id, 50)
self.assertEqual(self.shuttle.tray_qty, 50)
self._update_quantity_in_cell(cell, self.out_move_line.product_id, -20)
self.assertEqual(self.shuttle.tray_qty, 30)

View File

@@ -0,0 +1,17 @@
<odoo>
<template id="packagings">
<ul class="o_vlift_packaging list-unstyled">
<t t-foreach="packagings" 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="packaging['unit']"/>
</li>
</t>
</ul>
</template>
</odoo>

View File

@@ -0,0 +1,35 @@
<?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">
<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>
<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>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,11 @@
<?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"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="vertical_lift_shuttle_view_form_screen" model="ir.ui.view">
<field name="name">vertical.lift.shuttle.view.form.screen</field>
<field name="model">vertical.lift.shuttle</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<form string="Operations" delete="0" create="0" class="o_vlift_shuttle">
<!-- <field name="product_id" invisible="1"/> -->
<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"/> /
<field name="number_of_ops_all" readonly="1"/>
</div>
</div>
<div class="o_shuttle_actions">
<div class="o_shuttle_content">
<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"/>
<!-- will react on barcode 'O-BTN.release -->
<button name="button_release"
type="object"
string="Release"
class="btn-primary"
barcode_trigger="release"/>
</div>
</div>
</div>
<div class="o_shuttle_operation bg-primary jumbotron jumbotron-fluid">
<div class="container">
<field name="operation_descr"/>
</div>
</div>
<div class="o_shuttle_data" attrs="{'invisible': [('current_move_line_id', '=', False)]}">
<!-- 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"
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">
<group col="1">
<field name="tray_name"/>
<field name="tray_type_code"/>
<field name="tray_y"/>
<field name="tray_x"/>
<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>
</div>
<field name="_barcode_scanned" widget="barcode_handler"/>
</form>
</field>
</record>
<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">
<field name="id" string="Back to settings"
widget="vlift_shuttle_exit_button" readonly="1"/>
</div>
</div>
<footer></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_manual_barcode">
<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></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="Operations">
<group>
<field name="name"/>
<field name="mode"/>
<field name="location_id"/>
<field name="hardware"/>
<!-- move line visible and editable for prototyping purposes, to
remove later -->
<field name="current_move_line_id" groups="base.group_no_one"/>
</group>
</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_type">form</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>

View File

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

View File

@@ -0,0 +1,17 @@
# 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': '12.0.1.0.0',
'category': 'Stock',
'author': 'Camptocamp, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'depends': [
'stock_vertical_lift',
],
'website': 'https://www.camptocamp.com',
'data': [],
'installable': True,
'development_status': 'Alpha',
}

View File

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

View File

@@ -0,0 +1,14 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
class VerticalLiftShuttle(models.Model):
_inherit = 'vertical.lift.shuttle'
@api.model
def _selection_hardware(self):
values = super()._selection_hardware()
values += [('kardex', 'Kardex')]
return values

View File

@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>

View File

@@ -0,0 +1,2 @@
Add support for Kardex Remstar vertical lifts to the Vertical Lift
module.

View File

@@ -0,0 +1 @@
* Add support of the hardware