mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
Add stock_cubiscan
This commit is contained in:
committed by
Guewen Baconnier
parent
faa9e562ff
commit
76eccd3a40
2
stock_cubiscan/__init__.py
Normal file
2
stock_cubiscan/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
24
stock_cubiscan/__manifest__.py
Normal file
24
stock_cubiscan/__manifest__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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': '12.0.1.0.0',
|
||||
'category': 'Stock',
|
||||
'author': 'Camptocamp',
|
||||
'license': 'AGPL-3',
|
||||
'depends': [
|
||||
'barcodes',
|
||||
'stock',
|
||||
'web_tree_dynamic_colored_field'
|
||||
],
|
||||
'website': 'http://www.camptocamp.com',
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'views/cubiscan_view.xml',
|
||||
'views/product_packaging_views.xml',
|
||||
'wizard/cubiscan_wizard.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
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
|
||||
88
stock_cubiscan/models/cubiscan.py
Normal file
88
stock_cubiscan/models/cubiscan.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from ipaddress import ip_address
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@api.multi
|
||||
@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')
|
||||
|
||||
try:
|
||||
ip_address(self.device_address)
|
||||
except ValueError:
|
||||
raise ValidationError('Device IP Address is not valid')
|
||||
|
||||
@api.multi
|
||||
def copy(self, default=None):
|
||||
if not default:
|
||||
default = dict()
|
||||
default['state'] = 'not_ready'
|
||||
return super().copy(default)
|
||||
|
||||
@api.multi
|
||||
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',
|
||||
'view_type': 'form',
|
||||
'context': {'default_device_id': self.id},
|
||||
'target': 'fullscreen',
|
||||
'flags': {
|
||||
'headless': True,
|
||||
'form_view_initial_mode': 'edit',
|
||||
'no_breadcrumbs': True,
|
||||
},
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def _get_interface(self):
|
||||
self.ensure_one()
|
||||
return CubiScan(self.device_address, self.port, self.timeout)
|
||||
|
||||
@api.multi
|
||||
def test_device(self):
|
||||
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'
|
||||
|
||||
@api.multi
|
||||
def get_measure(self):
|
||||
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()
|
||||
36
stock_cubiscan/models/stock.py
Normal file
36
stock_cubiscan/models/stock.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2019 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, 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"
|
||||
|
||||
# TODO move these in an addon. Warning:
|
||||
# * 'delivery' defines the same fields and add them in the 'Delivery
|
||||
# Packages' view
|
||||
# * our put-away modules (wms/stock_putaway_storage_type_strategy) will
|
||||
# need these fields as well
|
||||
max_weight = fields.Float()
|
||||
length = fields.Integer()
|
||||
width = fields.Integer()
|
||||
height = fields.Integer()
|
||||
volume = fields.Float(
|
||||
compute='_compute_volume', readonly=True, store=False
|
||||
)
|
||||
|
||||
@api.depends('length', 'width', 'height')
|
||||
def _compute_volume(self):
|
||||
for pack in self:
|
||||
pack.volume = (
|
||||
pack.length * pack.width * pack.height
|
||||
) / 1000.0 ** 3
|
||||
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
|
||||
41
stock_cubiscan/tests/test_cubiscan.py
Normal file
41
stock_cubiscan/tests/test_cubiscan.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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)
|
||||
|
||||
# Wrong IP
|
||||
vals.update({'device_address': '999.261.42.42', 'port': 5982})
|
||||
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')
|
||||
110
stock_cubiscan/tests/test_cubiscan_wizard.py
Normal file
110
stock_cubiscan/tests/test_cubiscan_wizard.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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']
|
||||
|
||||
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'
|
||||
).product_tmpl_id
|
||||
cls.product_2 = cls.env.ref(
|
||||
'product.product_product_7'
|
||||
).product_tmpl_id
|
||||
|
||||
cls.product_1.barcode = '424242'
|
||||
|
||||
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), 5)
|
||||
|
||||
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), 5)
|
||||
|
||||
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(
|
||||
['length', 'width', 'height', 'max_weight', 'volume']
|
||||
)[0],
|
||||
{
|
||||
'id': line.id,
|
||||
'length': (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
|
||||
self.assertEqual(len(packagings), 5)
|
||||
for idx, packaging in enumerate(packagings):
|
||||
self.assertEqual(
|
||||
packaging.read(
|
||||
['length', 'width', 'height', 'max_weight', 'volume']
|
||||
)[0],
|
||||
{
|
||||
'id': packaging.id,
|
||||
'length': (2 ** idx) * 1000,
|
||||
'width': 1000,
|
||||
'height': 1000,
|
||||
'max_weight': 2.0 ** idx,
|
||||
'volume': 2.0 ** idx,
|
||||
},
|
||||
)
|
||||
8
stock_cubiscan/views/assets.xml
Normal file
8
stock_cubiscan/views/assets.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
55
stock_cubiscan/views/cubiscan_view.xml
Normal file
55
stock_cubiscan/views/cubiscan_view.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?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_type">form</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>
|
||||
24
stock_cubiscan/views/product_packaging_views.xml
Normal file
24
stock_cubiscan/views/product_packaging_views.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- TODO the fields have to be moved to a standalone module
|
||||
See the comment in models/stock.py
|
||||
-->
|
||||
<record id="product_packaging_form_view" model="ir.ui.view">
|
||||
<field name="name">product.packaging.form.view</field>
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="inherit_id" ref="product.product_packaging_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="qty" position="after">
|
||||
<group name="size">
|
||||
<field name="height"/>
|
||||
<field name="width"/>
|
||||
<field name="length"/>
|
||||
<field name="volume"/>
|
||||
<field name="max_weight"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
1
stock_cubiscan/wizard/__init__.py
Normal file
1
stock_cubiscan/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import cubiscan_wizard
|
||||
159
stock_cubiscan/wizard/cubiscan_wizard.py
Normal file
159
stock_cubiscan/wizard/cubiscan_wizard.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# 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):
|
||||
_name = 'cubiscan.wizard'
|
||||
_inherit = 'barcodes.barcode_events_mixin'
|
||||
_description = 'Cubiscan Wizard'
|
||||
_rec_name = 'device_id'
|
||||
|
||||
PACKAGING_UNITS = ['Unit', 'kfVE', 'DhVE', 'KrVE', 'PAL']
|
||||
|
||||
device_id = fields.Many2one('cubiscan.device', readonly=True)
|
||||
product_id = fields.Many2one('product.template')
|
||||
line_ids = fields.One2many('cubiscan.wizard.line', 'wizard_id')
|
||||
|
||||
@api.onchange('product_id')
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
to_create = []
|
||||
for seq, name in enumerate(self.PACKAGING_UNITS):
|
||||
pack = self.product_id.packaging_ids.filtered(
|
||||
lambda rec: rec.name == name
|
||||
)
|
||||
vals = {
|
||||
'wizard_id': self.id,
|
||||
'sequence': seq + 1,
|
||||
'name': name,
|
||||
}
|
||||
if pack:
|
||||
vals.update(
|
||||
{
|
||||
'qty': pack.qty,
|
||||
'max_weight': pack.max_weight,
|
||||
'length': pack.length,
|
||||
'width': pack.width,
|
||||
'height': pack.height,
|
||||
'barcode': pack.barcode,
|
||||
}
|
||||
)
|
||||
to_create.append(vals)
|
||||
recs = self.env['cubiscan.wizard.line'].create(to_create)
|
||||
self.line_ids = [(6, 0, recs.ids)]
|
||||
else:
|
||||
self.line_ids = [(5, 0, 0)]
|
||||
|
||||
@api.multi
|
||||
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",
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def on_barcode_scanned(self, barcode):
|
||||
self.ensure_one()
|
||||
prod = self.env['product.template'].search([('barcode', '=', barcode)])
|
||||
self.product_id = prod
|
||||
self.onchange_product_id()
|
||||
|
||||
@api.multi
|
||||
def action_save(self):
|
||||
self.ensure_one()
|
||||
actions = []
|
||||
for line in self.line_ids:
|
||||
vals = {
|
||||
'sequence': line.sequence,
|
||||
'name': line.name,
|
||||
'qty': line.qty,
|
||||
'max_weight': line.max_weight,
|
||||
'length': line.length,
|
||||
'width': line.width,
|
||||
'height': line.height,
|
||||
'barcode': line.barcode,
|
||||
}
|
||||
pack = self.product_id.packaging_ids.filtered(
|
||||
lambda rec: rec.name == line.name
|
||||
)
|
||||
if pack:
|
||||
actions.append((1, pack.id, vals))
|
||||
else:
|
||||
actions.append((0, 0, vals))
|
||||
self.product_id.packaging_ids = actions
|
||||
|
||||
@api.multi
|
||||
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)
|
||||
length = fields.Integer("Length (mm)", readonly=True)
|
||||
width = fields.Integer("Width (mm)", readonly=True)
|
||||
height = fields.Integer("Height (mm)", readonly=True)
|
||||
volume = fields.Float(
|
||||
"Volume (m3)", compute='_compute_volume', readonly=True, store=False
|
||||
)
|
||||
barcode = fields.Char("GTIN")
|
||||
|
||||
@api.depends('length', 'width', 'height')
|
||||
def _compute_volume(self):
|
||||
for line in self:
|
||||
line.volume = (
|
||||
line.length * line.width * line.height
|
||||
) / 1000.0 ** 3
|
||||
|
||||
@api.multi
|
||||
def cubiscan_measure(self):
|
||||
self.ensure_one()
|
||||
measures = self.wizard_id.device_id.get_measure()
|
||||
measures = {
|
||||
k: (
|
||||
v[0] if k in ['length', 'width', 'height', 'weight'] else False
|
||||
)
|
||||
for k, v in measures.items()
|
||||
}
|
||||
weight = measures.pop('weight')
|
||||
measures = {k: int(v * 1000) for k, v in measures.items()}
|
||||
measures['max_weight'] = weight
|
||||
self.write(measures)
|
||||
40
stock_cubiscan/wizard/cubiscan_wizard.xml
Normal file
40
stock_cubiscan/wizard/cubiscan_wizard.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
<field name="product_id" nolabel="1" options="{'no_open': True, 'create': False, 'create_edit': False}" />
|
||||
<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="name" />
|
||||
<field name="qty" />
|
||||
<field name="max_weight" options="{'bg_color': 'lightcoral: max_weight == 0.0'}" />
|
||||
<field name="length" options="{'bg_color': 'lightcoral: length == 0.0'}" />
|
||||
<field name="width" options="{'bg_color': 'lightcoral: width == 0.0'}" />
|
||||
<field name="height" options="{'bg_color': 'lightcoral: height == 0.0'}" />
|
||||
<field name="volume" options="{'bg_color': 'lightcoral: volume == 0.0'}" />
|
||||
<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