Skip to content

Commit

Permalink
Merge PR OCA#826 into 13.0
Browse files Browse the repository at this point in the history
Signed-off-by simahawk
  • Loading branch information
OCA-git-bot committed Mar 30, 2020
2 parents bdf80a3 + c2479ad commit 3b24ef5
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 0 deletions.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cubiscan
mock
1 change: 1 addition & 0 deletions setup/stock_cubiscan/odoo/addons/stock_cubiscan
6 changes: 6 additions & 0 deletions setup/stock_cubiscan/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
2 changes: 2 additions & 0 deletions stock_cubiscan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
27 changes: 27 additions & 0 deletions stock_cubiscan/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions stock_cubiscan/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import stock
from . import cubiscan
98 changes: 98 additions & 0 deletions stock_cubiscan/models/cubiscan.py
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions stock_cubiscan/models/stock.py
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions stock_cubiscan/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions stock_cubiscan/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Patrick Tombez <patrick.tombez@camptocamp.com>
5 changes: 5 additions & 0 deletions stock_cubiscan/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions stock_cubiscan/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions stock_cubiscan/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -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 changes: 57 additions & 0 deletions stock_cubiscan/scripts/cubiscan_stub.py
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions stock_cubiscan/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions stock_cubiscan/static/src/scss/cubiscan_wizard.scss
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions stock_cubiscan/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import test_cubiscan
from . import test_cubiscan_wizard
33 changes: 33 additions & 0 deletions stock_cubiscan/tests/test_cubiscan.py
Original file line number Diff line number Diff line change
@@ -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")
Loading

0 comments on commit 3b24ef5

Please sign in to comment.