mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
cubiscan
|
||||
mock
|
||||
1
setup/stock_cubiscan/odoo/addons/stock_cubiscan
Symbolic link
1
setup/stock_cubiscan/odoo/addons/stock_cubiscan
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../stock_cubiscan
|
||||
6
setup/stock_cubiscan/setup.py
Normal file
6
setup/stock_cubiscan/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
2
stock_cubiscan/__init__.py
Normal file
2
stock_cubiscan/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
27
stock_cubiscan/__manifest__.py
Normal file
27
stock_cubiscan/__manifest__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Stock Cubiscan",
|
||||
"summary": "Implement inteface with Cubiscan devices for packaging",
|
||||
"version": "13.0.1.0.0",
|
||||
"category": "Warehouse",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"barcodes",
|
||||
"stock",
|
||||
"web_tree_dynamic_colored_field",
|
||||
"product_packaging_dimension",
|
||||
"product_packaging_type_required",
|
||||
],
|
||||
"external_dependencies": {"python": ["cubiscan"]},
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"data": [
|
||||
"views/assets.xml",
|
||||
"views/cubiscan_view.xml",
|
||||
"wizard/cubiscan_wizard.xml",
|
||||
"security/ir.model.access.csv",
|
||||
],
|
||||
"installable": True,
|
||||
"development_status": "Alpha",
|
||||
}
|
||||
2
stock_cubiscan/models/__init__.py
Normal file
2
stock_cubiscan/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import stock
|
||||
from . import cubiscan
|
||||
98
stock_cubiscan/models/cubiscan.py
Normal file
98
stock_cubiscan/models/cubiscan.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from cubiscan.cubiscan import CubiScan
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class CubiscanDevice(models.Model):
|
||||
_name = "cubiscan.device"
|
||||
_description = "Cubiscan Device"
|
||||
_order = "warehouse_id, name"
|
||||
|
||||
name = fields.Char("Name", required=True)
|
||||
device_address = fields.Char("Device IP Address", required=True)
|
||||
port = fields.Integer("Port", required=True)
|
||||
timeout = fields.Integer(
|
||||
"Timeout", help="Timeout in seconds", required=True, default=30
|
||||
)
|
||||
warehouse_id = fields.Many2one("stock.warehouse", "Warehouse")
|
||||
state = fields.Selection(
|
||||
[("not_ready", "Not Ready"), ("ready", "Ready")],
|
||||
default="not_ready",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
@api.constrains("device_address", "port")
|
||||
def _check_connection_infos(self):
|
||||
self.ensure_one()
|
||||
if not 1 <= self.port <= 65535:
|
||||
raise ValidationError(_("Port must be in range 1-65535"))
|
||||
|
||||
def open_wizard(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": _("CubiScan Wizard"),
|
||||
"res_model": "cubiscan.wizard",
|
||||
"type": "ir.actions.act_window",
|
||||
"view_id": False,
|
||||
"view_mode": "form",
|
||||
"context": {"default_device_id": self.id},
|
||||
"target": "fullscreen",
|
||||
"flags": {
|
||||
"withControlPanel": False,
|
||||
"form_view_initial_mode": "edit",
|
||||
"no_breadcrumbs": True,
|
||||
},
|
||||
}
|
||||
|
||||
def _get_interface_client_args(self):
|
||||
"""Prepare the arguments to instanciate the CubiScan client
|
||||
|
||||
Can be overriden to change the parameters.
|
||||
|
||||
Example, adding a ssl certificate::
|
||||
|
||||
args, kwargs = super()._get_interface_client_args()
|
||||
ctx = SSL.create_default_context()
|
||||
ctx.load_cert_chain("/usr/lib/ssl/certs/my_cert.pem")
|
||||
kwargs['ssl'] = ctx
|
||||
return args, kwargs
|
||||
|
||||
Returns a 2 items tuple with: (args, kwargs) where args
|
||||
is a list and kwargs a dict.
|
||||
"""
|
||||
return ([self.device_address, self.port, self.timeout], {})
|
||||
|
||||
def _get_interface(self):
|
||||
"""Return the CubiScan client
|
||||
|
||||
Can be overrided to customize the way it is instanciated
|
||||
"""
|
||||
self.ensure_one()
|
||||
args, kwargs = self._get_interface_client_args()
|
||||
return CubiScan(*args, **kwargs)
|
||||
|
||||
def test_device(self):
|
||||
"""Check connection with the Cubiscan device"""
|
||||
for device in self:
|
||||
res = device._get_interface().test()
|
||||
if res and "error" not in res and device.state == "not_ready":
|
||||
device.state = "ready"
|
||||
elif res and "error" in res and device.state == "ready":
|
||||
device.state = "not_ready"
|
||||
|
||||
def get_measure(self):
|
||||
"""Return a measure from the Cubiscan device"""
|
||||
self.ensure_one()
|
||||
if self.state != "ready":
|
||||
raise UserError(
|
||||
_(
|
||||
"Device is not ready. Please use the 'Test'"
|
||||
" button before using the device."
|
||||
)
|
||||
)
|
||||
return self._get_interface().measure()
|
||||
26
stock_cubiscan/models/stock.py
Normal file
26
stock_cubiscan/models/stock.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = "stock.warehouse"
|
||||
|
||||
cubiscan_device_ids = fields.One2many(
|
||||
"cubiscan.device", "warehouse_id", string="Cubiscan Devices"
|
||||
)
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = "product.packaging"
|
||||
# FIXME: move this constraint in product_packaging_type
|
||||
# https://github.com/OCA/product-attribute/tree/13.0/product_packaging_type
|
||||
_sql_constraints = [
|
||||
(
|
||||
"product_packaging_type_unique",
|
||||
"unique (product_id, packaging_type_id)",
|
||||
"It is forbidden to have different packagings "
|
||||
"with the same type for a given product.",
|
||||
)
|
||||
]
|
||||
4
stock_cubiscan/readme/CONFIGURE.rst
Normal file
4
stock_cubiscan/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
The first step is to configure the Packaging Types (Pallet, Box, ...) in Inventory > Configuration > Product Packaging Types.
|
||||
|
||||
Configure the Cubiscan device in Inventory > Configuration > Cubiscan Devices.
|
||||
Use the "Test Device" to check the connection with the hardware.
|
||||
1
stock_cubiscan/readme/CONTRIBUTORS.rst
Normal file
1
stock_cubiscan/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Patrick Tombez <patrick.tombez@camptocamp.com>
|
||||
5
stock_cubiscan/readme/DESCRIPTION.rst
Normal file
5
stock_cubiscan/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
Cubiscan_ are dimensioners for cubing and weighing in warehouses.
|
||||
This module implements the communication with the dimensioners as well
|
||||
as a screen to measure and weight packaging of the products.
|
||||
|
||||
.. _Cubiscan: https://cubiscan.com/
|
||||
3
stock_cubiscan/readme/ROADMAP.rst
Normal file
3
stock_cubiscan/readme/ROADMAP.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
* The UI could get some improvements
|
||||
* Being able to open the Cubiscan screen from a product would be nice
|
||||
* The wizard should allow to set weight and size for a single unit in addition to the packaging
|
||||
5
stock_cubiscan/readme/USAGE.rst
Normal file
5
stock_cubiscan/readme/USAGE.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
Use the "Wizard" button on a Cubiscan device to open the screen and take
|
||||
measurements.
|
||||
|
||||
For developers: a script in the directory ``scripts/cubiscan_stub.py`` allows
|
||||
to simulate a Cubiscan server and send random measurements.
|
||||
57
stock_cubiscan/scripts/cubiscan_stub.py
Executable file
57
stock_cubiscan/scripts/cubiscan_stub.py
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/python3
|
||||
# pylint: disable=print-used,attribute-deprecated
|
||||
"""Stub a Cubiscan server
|
||||
|
||||
Allow testing the connection to Cubiscan from Odoo
|
||||
without real hardware.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_cubiscan(reader, writer):
|
||||
message = yield from reader.readline()
|
||||
addr = writer.get_extra_info("peername")
|
||||
|
||||
print("Received {!r} from {!r}".format(message, addr))
|
||||
# print("Expecting {!r} from {!r}".format(message, addr))
|
||||
print("{!r}".format(message == b"\x02M\x03\r\n"))
|
||||
if message == b"\x02M\x03\r\n":
|
||||
length = random.uniform(0, 1000)
|
||||
width = random.uniform(0, 1000)
|
||||
height = random.uniform(0, 1000)
|
||||
weight = random.uniform(0, 10000)
|
||||
answer = (
|
||||
b"\x02MAH123456,L%05.1f,W%05.1f,H%05.1f,M,K%06.1f,D%06.1f,M,F0000,I\x03\r\n"
|
||||
% (length, width, height, weight, weight)
|
||||
)
|
||||
else:
|
||||
answer = b"\x02\x03\r\n"
|
||||
print("Send: {!r}".format(answer))
|
||||
writer.write(answer)
|
||||
yield from writer.drain()
|
||||
|
||||
|
||||
def main():
|
||||
loop = asyncio.get_event_loop()
|
||||
coro = asyncio.start_server(handle_cubiscan, "0.0.0.0", 9876, loop=loop)
|
||||
server = loop.run_until_complete(coro)
|
||||
|
||||
# Serve requests until Ctrl+C is pressed
|
||||
addr = server.sockets[0].getsockname()
|
||||
print("Serving on {}".format(addr))
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# Close the server
|
||||
server.close()
|
||||
loop.run_until_complete(server.wait_closed())
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
stock_cubiscan/security/ir.model.access.csv
Normal file
3
stock_cubiscan/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_cubiscan_device_inventory_manager,cubiscan.device.inventory.manager,stock_cubiscan.model_cubiscan_device,stock.group_stock_manager,1,1,1,1
|
||||
access_cubiscan_device_inventory_user,cubiscan.device.inventory.user,stock_cubiscan.model_cubiscan_device,stock.group_stock_user,1,0,0,0
|
||||
|
33
stock_cubiscan/static/src/scss/cubiscan_wizard.scss
Normal file
33
stock_cubiscan/static/src/scss/cubiscan_wizard.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
.o_web_client.o_fullscreen {
|
||||
.o_form_view.cubiscan_wizard {
|
||||
font-size: 16px;
|
||||
|
||||
@include media-breakpoint-up(x1) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 1em;
|
||||
padding: 1em;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.o_data_cell:not(.o_list_button) {
|
||||
padding: 0.75em;
|
||||
font-size: 1.5em;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.o_field_many2one input.o_input {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.o_form_statusbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
stock_cubiscan/tests/__init__.py
Normal file
2
stock_cubiscan/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import test_cubiscan
|
||||
from . import test_cubiscan_wizard
|
||||
33
stock_cubiscan/tests/test_cubiscan.py
Normal file
33
stock_cubiscan/tests/test_cubiscan.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from cubiscan.cubiscan import CubiScan
|
||||
from mock import patch
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestCubiscan(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.device_obj = cls.env["cubiscan.device"]
|
||||
|
||||
def test_constraints(self):
|
||||
vals = {"name": "Test Device"}
|
||||
|
||||
# Wrong port
|
||||
vals.update({"device_address": "10.10.0.42", "port": -42})
|
||||
with self.assertRaises(ValidationError):
|
||||
self.device_obj.create(vals)
|
||||
|
||||
def test_device_test(self):
|
||||
vals = {"name": "Test Device", "device_address": "10.10.0.42", "port": 5982}
|
||||
device = self.device_obj.create(vals)
|
||||
self.assertEquals(device.state, "not_ready")
|
||||
|
||||
with patch.object(CubiScan, "_make_request") as mocked:
|
||||
mocked.return_value = {"identifier": 42}
|
||||
device.test_device()
|
||||
|
||||
self.assertEquals(device.state, "ready")
|
||||
122
stock_cubiscan/tests/test_cubiscan_wizard.py
Normal file
122
stock_cubiscan/tests/test_cubiscan_wizard.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from cubiscan.cubiscan import CubiScan
|
||||
from mock import patch
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestCubiscanWizard(SavepointCase):
|
||||
@staticmethod
|
||||
def get_measure_result(length, width, height, weight):
|
||||
return {
|
||||
"origin": "1",
|
||||
"location": "dev001",
|
||||
"length": (length, None),
|
||||
"width": (width, None),
|
||||
"height": (height, None),
|
||||
"space_metric": True,
|
||||
"weight": (weight, None),
|
||||
"dim_weight": (weight, None),
|
||||
"weight_metric": True,
|
||||
"factor": 1,
|
||||
"intl_unit": True,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||
|
||||
cls.device_obj = cls.env["cubiscan.device"]
|
||||
cls.cs_wizard = cls.env["cubiscan.wizard"]
|
||||
PackType = cls.env["product.packaging.type"]
|
||||
pack_type_data = [
|
||||
("unit", 2, 0, 0),
|
||||
("internal", 3, 1, 0),
|
||||
("retail", 10, 1, 1),
|
||||
("transport", 20, 1, 1),
|
||||
("pallet", 30, 1, 1),
|
||||
]
|
||||
for name, seq, gtin, req in pack_type_data:
|
||||
PackType.create(
|
||||
{
|
||||
"name": name,
|
||||
"code": name.upper(),
|
||||
"sequence": seq,
|
||||
"has_gtin": gtin,
|
||||
"required": req,
|
||||
}
|
||||
)
|
||||
|
||||
cls.device = cls.device_obj.create(
|
||||
{
|
||||
"name": "Test Device",
|
||||
"device_address": "192.168.21.42",
|
||||
"port": 4242,
|
||||
"state": "ready",
|
||||
}
|
||||
)
|
||||
|
||||
cls.wizard = cls.cs_wizard.create({"device_id": cls.device.id})
|
||||
|
||||
cls.product_1 = cls.env.ref("product.product_product_6")
|
||||
cls.product_2 = cls.env.ref("product.product_product_7")
|
||||
|
||||
cls.product_1.barcode = "424242"
|
||||
PackType.cron_check_create_required_packaging()
|
||||
|
||||
def test_product_onchange(self):
|
||||
self.wizard.product_id = self.product_1.id
|
||||
|
||||
self.assertEqual(len(self.wizard.line_ids), 0)
|
||||
self.wizard.onchange_product_id()
|
||||
self.assertEqual(len(self.wizard.line_ids), 6)
|
||||
|
||||
def test_product_onchange_barcode(self):
|
||||
self.assertFalse(self.wizard.product_id)
|
||||
self.assertFalse(self.wizard.line_ids)
|
||||
|
||||
self.wizard.on_barcode_scanned("424242")
|
||||
|
||||
self.assertEqual(self.wizard.product_id, self.product_1)
|
||||
self.assertEqual(len(self.wizard.line_ids), 6)
|
||||
|
||||
def test_cubiscan_measures(self):
|
||||
self.wizard.product_id = self.product_1.id
|
||||
self.wizard.onchange_product_id()
|
||||
|
||||
with patch.object(CubiScan, "_make_request") as request:
|
||||
for idx, line in enumerate(self.wizard.line_ids):
|
||||
request.return_value = TestCubiscanWizard.get_measure_result(
|
||||
2 ** idx, 1, 1, 2 ** idx
|
||||
)
|
||||
line.cubiscan_measure()
|
||||
self.assertEqual(
|
||||
line.read(["lngth", "width", "height", "max_weight", "volume"])[0],
|
||||
{
|
||||
"id": line.id,
|
||||
"lngth": (2 ** idx) * 1000,
|
||||
"width": 1000,
|
||||
"height": 1000,
|
||||
"max_weight": 2.0 ** idx,
|
||||
"volume": 2.0 ** idx,
|
||||
},
|
||||
)
|
||||
|
||||
self.wizard.action_save()
|
||||
|
||||
packagings = self.product_1.packaging_ids.sorted()
|
||||
self.assertEqual(len(packagings), 6)
|
||||
for idx, packaging in enumerate(packagings):
|
||||
self.assertEqual(
|
||||
packaging.read(["lngth", "width", "height", "max_weight", "volume"])[0],
|
||||
{
|
||||
"id": packaging.id,
|
||||
"lngth": (2 ** idx) * 1000,
|
||||
"width": 1000,
|
||||
"height": 1000,
|
||||
"max_weight": 2.0 ** idx,
|
||||
"volume": 2.0 ** idx,
|
||||
},
|
||||
)
|
||||
16
stock_cubiscan/views/assets.xml
Normal file
16
stock_cubiscan/views/assets.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<template
|
||||
id="cubiscan_assets"
|
||||
name="cubiscan.assets"
|
||||
inherit_id="web.assets_backend"
|
||||
>
|
||||
<xpath expr="." position="inside">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/scss"
|
||||
href="/stock_cubiscan/static/src/scss/cubiscan_wizard.scss"
|
||||
/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
67
stock_cubiscan/views/cubiscan_view.xml
Normal file
67
stock_cubiscan/views/cubiscan_view.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="view_cubiscan_device_form">
|
||||
<field name="name">cubiscan.device.form</field>
|
||||
<field name="model">cubiscan.device</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button
|
||||
type="object"
|
||||
name="open_wizard"
|
||||
string="Wizard"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="test_device"
|
||||
string="Test Device"
|
||||
attrs="{'invisible': [('id','=',False)]}"
|
||||
/>
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="id" invisible="1" />
|
||||
<group>
|
||||
<group name="cubiscan">
|
||||
<field name="name" />
|
||||
<field name="device_address" />
|
||||
<field name="port" />
|
||||
<field name="timeout" />
|
||||
<field name="warehouse_id" />
|
||||
</group>
|
||||
<group />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="view_cubiscan_device_tree">
|
||||
<field name="name">cubiscan.device.tree</field>
|
||||
<field name="model">cubiscan.device</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
decoration-warning="state == 'not_ready'"
|
||||
decoration-success="state == 'ready'"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="device_address" />
|
||||
<field name="port" />
|
||||
<field name="timeout" />
|
||||
<field name="warehouse_id" />
|
||||
<field name="state" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_cubiscan_device_form" model="ir.actions.act_window">
|
||||
<field name="name">CubiScan Devices</field>
|
||||
<field name="res_model">cubiscan.device</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem
|
||||
action="action_cubiscan_device_form"
|
||||
id="menu_action_cubiscan_device_form"
|
||||
parent="stock.menu_warehouse_config"
|
||||
sequence="2"
|
||||
/>
|
||||
</odoo>
|
||||
1
stock_cubiscan/wizard/__init__.py
Normal file
1
stock_cubiscan/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import cubiscan_wizard
|
||||
177
stock_cubiscan/wizard/cubiscan_wizard.py
Normal file
177
stock_cubiscan/wizard/cubiscan_wizard.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class CubiscanWizard(models.TransientModel):
|
||||
"""This wizard is used to show a screen showing Cubiscan information
|
||||
|
||||
It is opened in a headless view (no breadcrumb, no menus, fullscreen).
|
||||
"""
|
||||
|
||||
_name = "cubiscan.wizard"
|
||||
_inherit = "barcodes.barcode_events_mixin"
|
||||
_description = "Cubiscan Wizard"
|
||||
_rec_name = "device_id"
|
||||
|
||||
device_id = fields.Many2one("cubiscan.device", readonly=True)
|
||||
product_id = fields.Many2one("product.product", domain=[("type", "=", "product")])
|
||||
line_ids = fields.One2many("cubiscan.wizard.line", "wizard_id")
|
||||
|
||||
@api.onchange("product_id")
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
to_create = []
|
||||
packaging_types = self.env["product.packaging.type"].search([])
|
||||
for seq, pack_type in enumerate(packaging_types):
|
||||
pack = self.env["product.packaging"].search(
|
||||
[
|
||||
("product_id", "=", self.product_id.id),
|
||||
("packaging_type_id", "=", pack_type.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
vals = {
|
||||
"wizard_id": self.id,
|
||||
"sequence": seq + 1,
|
||||
"name": pack_type.name,
|
||||
"qty": 0,
|
||||
"max_weight": 0,
|
||||
"lngth": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"barcode": False,
|
||||
"packaging_type_id": pack_type.id,
|
||||
}
|
||||
if pack:
|
||||
vals.update(
|
||||
{
|
||||
"qty": pack.qty,
|
||||
"max_weight": pack.max_weight,
|
||||
"lngth": pack.lngth,
|
||||
"width": pack.width,
|
||||
"height": pack.height,
|
||||
"barcode": pack.barcode,
|
||||
"packaging_id": pack.id,
|
||||
"packaging_type_id": pack_type.id,
|
||||
}
|
||||
)
|
||||
to_create.append(vals)
|
||||
recs = self.env["cubiscan.wizard.line"].create(to_create)
|
||||
self.line_ids = recs
|
||||
else:
|
||||
self.line_ids = [(5, 0, 0)]
|
||||
|
||||
def action_reopen_fullscreen(self):
|
||||
# Action to reopen wizard in fullscreen (e.g. after page refresh)
|
||||
self.ensure_one()
|
||||
res = self.device_id.open_wizard()
|
||||
res["res_id"] = self.id
|
||||
return res
|
||||
|
||||
def action_search_barcode(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "cubiscan.wizard.barcode",
|
||||
"view_mode": "form",
|
||||
"name": _("Barcode"),
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
prod = self.env["product.product"].search([("barcode", "=", barcode)])
|
||||
self.product_id = prod
|
||||
self.onchange_product_id()
|
||||
|
||||
def action_save(self):
|
||||
self.ensure_one()
|
||||
actions = []
|
||||
for line in self.line_ids:
|
||||
vals = {
|
||||
"name": line.name,
|
||||
"qty": line.qty,
|
||||
"max_weight": line.max_weight,
|
||||
"lngth": line.lngth,
|
||||
"width": line.width,
|
||||
"height": line.height,
|
||||
"barcode": line.barcode,
|
||||
"packaging_type_id": line.packaging_type_id.id,
|
||||
}
|
||||
pack = line.packaging_id
|
||||
if pack:
|
||||
actions.append((1, pack.id, vals))
|
||||
else:
|
||||
actions.append((0, 0, vals))
|
||||
self.product_id.packaging_ids = actions
|
||||
# reload lines
|
||||
self.onchange_product_id()
|
||||
|
||||
def action_close(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref("stock_cubiscan.action_cubiscan_device_form").read()[0]
|
||||
action.update(
|
||||
{
|
||||
"res_id": self.device_id.id,
|
||||
"target": "main",
|
||||
"views": [
|
||||
(
|
||||
self.env.ref("stock_cubiscan.view_cubiscan_device_form").id,
|
||||
"form",
|
||||
)
|
||||
],
|
||||
"flags": {"headless": False, "clear_breadcrumbs": True},
|
||||
}
|
||||
)
|
||||
return action
|
||||
|
||||
|
||||
class CubiscanWizardLine(models.TransientModel):
|
||||
_name = "cubiscan.wizard.line"
|
||||
_description = "Cubiscan Wizard Line"
|
||||
_order = "sequence"
|
||||
|
||||
wizard_id = fields.Many2one("cubiscan.wizard")
|
||||
sequence = fields.Integer()
|
||||
name = fields.Char("Packaging", readonly=True)
|
||||
qty = fields.Float("Quantity")
|
||||
max_weight = fields.Float("Weight (kg)", readonly=True)
|
||||
# this is not a typo:
|
||||
# https://github.com/odoo/odoo/issues/41353#issuecomment-568037415
|
||||
lngth = fields.Integer("Length (mm)", readonly=True)
|
||||
width = fields.Integer("Width (mm)", readonly=True)
|
||||
height = fields.Integer("Height (mm)", readonly=True)
|
||||
volume = fields.Float(
|
||||
"Volume (m³)",
|
||||
digits=(8, 4),
|
||||
compute="_compute_volume",
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
barcode = fields.Char("GTIN")
|
||||
packaging_id = fields.Many2one(
|
||||
"product.packaging", string="Packaging (rel)", readonly=True
|
||||
)
|
||||
packaging_type_id = fields.Many2one(
|
||||
"product.packaging.type", readonly=True, required=True
|
||||
)
|
||||
required = fields.Boolean(related="packaging_type_id.required", readonly=True)
|
||||
|
||||
@api.depends("lngth", "width", "height")
|
||||
def _compute_volume(self):
|
||||
for line in self:
|
||||
line.volume = (line.lngth * line.width * line.height) / 1000.0 ** 3
|
||||
|
||||
def cubiscan_measure(self):
|
||||
self.ensure_one()
|
||||
measures = self.wizard_id.device_id.get_measure()
|
||||
# measures are a tuple of 2 slots (measure, precision error),
|
||||
# we only care about the measure for now
|
||||
measures = {
|
||||
"lngth": int(measures["length"][0] * 1000),
|
||||
"width": int(measures["width"][0] * 1000),
|
||||
"height": int(measures["height"][0] * 1000),
|
||||
"max_weight": measures["weight"][0],
|
||||
}
|
||||
self.write(measures)
|
||||
92
stock_cubiscan/wizard/cubiscan_wizard.xml
Normal file
92
stock_cubiscan/wizard/cubiscan_wizard.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="view_cubiscan_wizard" model="ir.ui.view">
|
||||
<field name="name">cubiscan.wizard.form</field>
|
||||
<field name="model">cubiscan.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form class="cubiscan_wizard">
|
||||
<header>
|
||||
<button
|
||||
name="action_reopen_fullscreen"
|
||||
string="Fullscreen"
|
||||
type="object"
|
||||
/>
|
||||
</header>
|
||||
<group>
|
||||
<group>
|
||||
<label for="device_id" />
|
||||
<field
|
||||
name="device_id"
|
||||
nolabel="1"
|
||||
options="{'no_open': True, 'no_create_edit': True}"
|
||||
/>
|
||||
<label for="product_id" />
|
||||
<field
|
||||
name="product_id"
|
||||
nolabel="1"
|
||||
options="{'no_open': True, 'no_create_edit': True}"
|
||||
/>
|
||||
<field
|
||||
name="_barcode_scanned"
|
||||
widget="barcode_handler"
|
||||
invisible="1"
|
||||
/>
|
||||
</group>
|
||||
<group />
|
||||
</group>
|
||||
<separator />
|
||||
<field name="line_ids">
|
||||
<tree editable="bottom" create="0" delete="0">
|
||||
<field name="sequence" invisible="1" />
|
||||
<field name="required" invisible="1" />
|
||||
<field name="name" />
|
||||
<field name="qty" />
|
||||
<field
|
||||
name="max_weight"
|
||||
options="{'bg_color': 'lightcoral: max_weight == 0.0 and required'}"
|
||||
/>
|
||||
<field
|
||||
name="lngth"
|
||||
options="{'bg_color': 'lightcoral: lngth == 0.0 and required'}"
|
||||
/>
|
||||
<field
|
||||
name="width"
|
||||
options="{'bg_color': 'lightcoral: width == 0.0 and required'}"
|
||||
/>
|
||||
<field
|
||||
name="height"
|
||||
options="{'bg_color': 'lightcoral: height == 0.0 and required'}"
|
||||
/>
|
||||
<field
|
||||
name="volume"
|
||||
options="{'bg_color': 'lightcoral: volume == 0.0 and required'}"
|
||||
/>
|
||||
<button
|
||||
name="cubiscan_measure"
|
||||
type="object"
|
||||
string="CubiScan"
|
||||
class="btn btn-warning"
|
||||
/>
|
||||
<field name="barcode" />
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button
|
||||
name="action_save"
|
||||
type="object"
|
||||
icon="fa-check"
|
||||
class="btn btn-primary"
|
||||
string="Save"
|
||||
/>
|
||||
<button
|
||||
name="action_close"
|
||||
type="object"
|
||||
icon="fa-times"
|
||||
class="btn btn-danger"
|
||||
string="Close"
|
||||
/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user