Merge PR #1121 into 13.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2021-04-21 07:21:45 +00:00
27 changed files with 1336 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
account-analytic
connector
product-attribute
server-env
server-ux

View File

@@ -0,0 +1,112 @@
======================
Stock Measuring Device
======================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_measuring_device
:alt: OCA/stock-logistics-warehouse
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_measuring_device
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/153/13.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
Different manufacturers produce devices which are able to measure and weigh
packages and parcels. Each brand has a different communication protocol. This
module provides an framework to interface such devices with Odoo.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Installation
============
This module by itself does not do anything.
You will need to install a module implementing the communication with your
device. Look for modules with a name starting with stock_measuring_device.
Configuration
=============
The first step is to configure the Packaging Types (Pallet, Box, ...) in Inventory > Configuration > Product Packaging Types.
Configure the measuring device in Inventory > Configuration > Measuring
Devices, don't forget to set the device type, and any other additional
parameters.
Usage
=====
Use the "Wizard" button on a Measuring Device to open the screen and take
measurements.
Known issues / Roadmap
======================
* The UI could get some improvements
* Being able to open the measurement screen from a product would be nice
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_measuring_device%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Patrick Tombez <patrick.tombez@camptocamp.com>
* Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
* Carlos Serra Toro <carlos.serra@camptocamp.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_measuring_device>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,5 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import components
from . import models
from . import wizard

View File

@@ -0,0 +1,34 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
{
"name": "Stock Measuring Device",
"summary": "Implement a common interface for measuring and weighing devices",
"version": "13.0.1.0.0",
"category": "Warehouse",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"depends": [
"component",
"barcodes",
"stock",
"web_tree_dynamic_colored_field",
"product_packaging_dimension",
"product_packaging_type_required",
"product_dimension",
# For the pop-up message to tell the user to refresh.
"web_notify",
"web_ir_actions_act_view_reload",
],
"data": [
"security/ir.model.access.csv",
"data/uom.xml",
"views/assets.xml",
"views/measuring_device_view.xml",
"wizard/measuring_wizard.xml",
"views/menu.xml",
],
"website": "https://github.com/OCA/stock-logistics-warehouse",
"installable": True,
"development_status": "Alpha",
"maintainers": ["gurneyalex"],
}

View File

@@ -0,0 +1,3 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import measuring_device_component

View File

@@ -0,0 +1,46 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import logging
from odoo.addons.component.core import AbstractComponent
_logger = logging.getLogger(__name__)
class MeasuringDevice(AbstractComponent):
_name = "measuring.device.base"
_collection = "measuring.device"
def test_device(self):
"""Test that the device is properly configured.
Override to e.g. test the connection parameter or send a test command.
:return: True on success, False otherwise"""
return True
def preprocess_measures(self, measures):
"""perform required parsing, key mapping and possible unit conversion
:param measures: a dictionary passed to _update_packaging_measures
:return: a dictionary containing values that will be written on the
wizard line
"""
return measures
def post_update_packaging_measures(self, measures, packaging, wizard_line):
"""hook called after the update of the packaging or wizard line update.
This method can be called to send notifications for instance.
:return: None"""
pass
def get_measure(self):
"""Get a measure from the device
the implementation must communicate with the device to trigger a
measure. If this can be done synchronously, call
_update_packaging_measures() to get the update"""
raise NotImplementedError()

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="product_uom_mm" model="uom.uom">
<field name="category_id" ref="uom.uom_categ_length" />
<field name="name">mm</field>
<field name="factor" eval="1000" />
<field name="uom_type">smaller</field>
</record>
</odoo>

View File

@@ -0,0 +1,5 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import measuring_device
from . import product_packaging
from . import stock_warehouse

View File

@@ -0,0 +1,104 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import logging
from odoo import _, fields, models
_logger = logging.getLogger(__name__)
class MeasuringDevice(models.Model):
_name = "measuring.device"
_inherit = "collection.base"
_description = "Measuring and Weighing Device"
_order = "warehouse_id, name"
name = fields.Char("Name", required=True)
warehouse_id = fields.Many2one("stock.warehouse", "Warehouse", required=True)
device_type = fields.Selection(
selection=[],
help="The type of device (e.g. zippcube, cubiscan...) "
"depending on which module are installed.",
)
state = fields.Selection(
[("not_ready", "Not Ready"), ("ready", "Ready")],
default="not_ready",
readonly=True,
copy=False,
)
_sql_constraints = [
(
"name_uniq",
"unique (name)",
"The name of the measuring/weighing device must be unique.",
),
]
def _get_measuring_device(self):
with self.work_on(self._name) as work_ctx:
return work_ctx.component(usage=self.device_type)
def open_wizard(self):
return {
"name": _("Measurement Wizard"),
"res_model": "measuring.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 _is_being_used(self):
self.ensure_one()
return bool(
self.env["product.packaging"].search_count(
[("measuring_device_id", "=", self.id)]
)
)
def _update_packaging_measures(self, measures):
self.ensure_one()
measures = self._get_measuring_device().preprocess_measures(measures)
line_domain = [
("wizard_id.device_id", "=", self.id),
("scan_requested", "=", True),
]
packaging = self.env["product.packaging"]._measuring_device_find_assigned(self)
if packaging:
line_domain += [("packaging_id", "=", packaging.id)]
else:
line_domain += [
("packaging_id", "=", False),
("is_unit_line", "=", True),
]
wizard_line = self.env["measuring.wizard.line"].search(
line_domain, order="write_date DESC", limit=1,
)
if not wizard_line:
_logger.warning("No wizard line found for this measure.")
packaging.write(measures)
else:
measures.update({"scan_requested": False})
wizard_line.write(measures)
self._get_measuring_device().post_update_packaging_measures(
measures, packaging, wizard_line
)
return packaging
def test_device(self):
for rec in self:
success = rec._get_measuring_device().test_device()
if success and rec.state == "not_ready":
rec.state = "ready"
elif not success and rec.state == "ready":
rec.state = "not_ready"

View File

@@ -0,0 +1,49 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import logging
from odoo import _, fields, models
_logger = logging.getLogger(__name__)
class ProductPackaging(models.Model):
_inherit = "product.packaging"
measuring_device_id = fields.Many2one(
"measuring.device",
copy=False,
string="Measuring device which will scan the package",
help="Technical field set when an operator uses the device "
"to scan this package",
)
def _measuring_device_assign(self, device):
"""Assign the measuring device to the current product packaging"""
# self can be an empty recordset, for the unit line which updates the
# product info
if self:
self.measuring_device_id = device
def _measuring_device_release(self):
"""Free the measuring device from the current product packaging"""
# self can be an empty recordset, for the unit line which updates the
# product info
if self:
self.measuring_device_id = False
def _measuring_device_find_assigned(self, device):
"""search packaging being assigned to the specified device"""
packaging = self.search(
[("measuring_device_id", "=", device.id)], order="write_date DESC", limit=2
)
if len(packaging) > 1:
warning_msg = _(
"Several packagings ({}) found to update by "
"device {}. Will update the first: {}".format(
packaging, self.name, packaging[0]
)
)
_logger.warning(warning_msg)
packaging = packaging[0]
return packaging

View File

@@ -0,0 +1,12 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
measuring_device_ids = fields.One2many(
"measuring.device", "warehouse_id", string="Measuring Devices"
)

View File

@@ -0,0 +1,5 @@
The first step is to configure the Packaging Types (Pallet, Box, ...) in Inventory > Configuration > Product Packaging Types.
Configure the measuring device in Inventory > Configuration > Measuring
Devices, don't forget to set the device type, and any other additional
parameters.

View File

@@ -0,0 +1,3 @@
* Patrick Tombez <patrick.tombez@camptocamp.com>
* Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
* Carlos Serra Toro <carlos.serra@camptocamp.com>

View File

@@ -0,0 +1,3 @@
Different manufacturers produce devices which are able to measure and weigh
packages and parcels. Each brand has a different communication protocol. This
module provides an framework to interface such devices with Odoo.

View File

@@ -0,0 +1,4 @@
This module by itself does not do anything.
You will need to install a module implementing the communication with your
device. Look for modules with a name starting with stock_measuring_device.

View File

@@ -0,0 +1,2 @@
* The UI could get some improvements
* Being able to open the measurement screen from a product would be nice

View File

@@ -0,0 +1,2 @@
Use the "Wizard" button on a Measuring Device to open the screen and take
measurements.

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_measuring_device_inventory_manager,measuring.device.inventory.manager,stock_measuring_device.model_measuring_device,stock.group_stock_manager,1,1,1,1
access_measuring_device_inventory_user,measuring.device.inventory.user,stock_measuring_device.model_measuring_device,stock.group_stock_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_measuring_device_inventory_manager measuring.device.inventory.manager stock_measuring_device.model_measuring_device stock.group_stock_manager 1 1 1 1
3 access_measuring_device_inventory_user measuring.device.inventory.user stock_measuring_device.model_measuring_device stock.group_stock_user 1 0 0 0

View File

@@ -0,0 +1,458 @@
<?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: http://docutils.sourceforge.net/" />
<title>Stock Measuring Device</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="stock-measuring-device">
<h1 class="title">Stock Measuring Device</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_measuring_device"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_measuring_device"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/153/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>Different manufacturers produce devices which are able to measure and weigh
packages and parcels. Each brand has a different communication protocol. This
module provides an framework to interface such devices with Odoo.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id4">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
<p>This module by itself does not do anything.</p>
<p>You will need to install a module implementing the communication with your
device. Look for modules with a name starting with stock_measuring_device.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>The first step is to configure the Packaging Types (Pallet, Box, …) in Inventory &gt; Configuration &gt; Product Packaging Types.</p>
<p>Configure the measuring device in Inventory &gt; Configuration &gt; Measuring
Devices, dont forget to set the device type, and any other additional
parameters.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
<p>Use the “Wizard” button on a Measuring Device to open the screen and take
measurements.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id4">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>The UI could get some improvements</li>
<li>Being able to open the measurement screen from a product would be nice</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id5">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_measuring_device%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id8">Contributors</a></h2>
<ul class="simple">
<li>Patrick Tombez &lt;<a class="reference external" href="mailto:patrick.tombez&#64;camptocamp.com">patrick.tombez&#64;camptocamp.com</a>&gt;</li>
<li>Alexandre Fayolle &lt;<a class="reference external" href="mailto:alexandre.fayolle&#64;camptocamp.com">alexandre.fayolle&#64;camptocamp.com</a>&gt;</li>
<li>Carlos Serra Toro &lt;<a class="reference external" href="mailto:carlos.serra&#64;camptocamp.com">carlos.serra&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_measuring_device">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,33 @@
.o_web_client.o_fullscreen {
.o_form_view.measuring_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;
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<odoo>
<template
id="stock_measuring_device_assets"
name="stock_measuring_device assets"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<link
rel="stylesheet"
type="text/scss"
href="/stock_measuring_device/static/src/scss/measuring_wizard.scss"
/>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" ?>
<odoo>
<record model="ir.ui.view" id="view_measuring_device_form">
<field name="name">measuring.device.form</field>
<field name="model">measuring.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>
<group>
<group name="device">
<field name="name" />
<field name="warehouse_id" />
<field name="device_type" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_measuring_device_tree">
<field name="name">measuring.device.tree</field>
<field name="model">measuring.device</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="warehouse_id" />
<field name="device_type" />
</tree>
</field>
</record>
<record id="action_measuring_device_form" model="ir.actions.act_window">
<field name="name">Measuring Devices</field>
<field name="res_model">measuring.device</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<odoo>
<menuitem
id="menu_measuring_device"
action="action_measuring_device_form"
parent="stock.menu_warehouse_config"
sequence="100"
/>
</odoo>

View File

@@ -0,0 +1,4 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import measuring_wizard
from . import measuring_wizard_line

View File

@@ -0,0 +1,178 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import api, fields, models
class MeasuringWizard(models.TransientModel):
_name = "measuring.wizard"
_inherit = "barcodes.barcode_events_mixin"
_description = "measuring Wizard"
_rec_name = "device_id"
product_id = fields.Many2one("product.product", domain=[("type", "=", "product")])
line_ids = fields.One2many("measuring.wizard.line", "wizard_id")
device_id = fields.Many2one("measuring.device", readonly=True)
@api.onchange("product_id")
def onchange_product_id(self):
if self.product_id:
to_create = []
to_create += self._prepare_unit_line()
to_create += self._prepare_packaging_lines()
recs = self.env["measuring.wizard.line"].create(to_create)
self.line_ids = recs
else:
self.line_ids = [(5, 0, 0)]
def _prepare_unit_line(self):
vals = {
"wizard_id": self.id,
"sequence": 0,
"name": "Unit",
"qty": 1,
"max_weight": self.product_id.weight,
"lngth": self.product_id.product_length,
"width": self.product_id.product_width,
"height": self.product_id.product_height,
"is_unit_line": True,
}
product_dimension_uom = self.product_id.dimensional_uom_id
mm_uom = self.env.ref("stock_measuring_device.product_uom_mm")
if mm_uom != product_dimension_uom:
vals.update(
{
"lngth": product_dimension_uom._compute_quantity(
self.product_id.product_length, mm_uom
),
"width": product_dimension_uom._compute_quantity(
self.product_id.product_width, mm_uom
),
"height": product_dimension_uom._compute_quantity(
self.product_id.product_height, mm_uom
),
}
)
return [vals]
def _prepare_packaging_lines(self):
vals_list = []
product_packaging = self.env["product.packaging"]
packaging_types = self.env["product.packaging.type"].search([])
for seq, pack_type in enumerate(packaging_types):
pack = 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,
}
)
vals_list.append(vals)
return vals_list
def action_reopen_fullscreen(self):
self.ensure_one()
res = self.device_id.open_wizard()
res["res_id"] = self.id
return res
def on_barcode_scanned(self, barcode):
self.ensure_one()
prod = self.env["product.product"].search([("barcode", "=", barcode)], limit=1)
self.product_id = prod
def action_save(self):
self.ensure_one()
product_vals = {}
packaging_ids_list = []
for line in self.line_ids:
packaging_type = line.packaging_type_id
if packaging_type:
# Handle lines with packaging
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:
packaging_ids_list.append((1, pack.id, vals))
else:
packaging_ids_list.append((0, 0, vals))
else:
# Handle unit line
mm_uom = self.env.ref("stock_measuring_device.product_uom_mm")
product_vals.update(
{
"product_length": line.lngth,
"product_width": line.width,
"product_height": line.height,
"dimensional_uom_id": mm_uom.id,
"weight": line.max_weight,
}
)
product_vals.update({"packaging_ids": packaging_ids_list})
self.product_id.write(product_vals)
# Call onchange to update volume on product.product
self.product_id.onchange_calculate_volume()
# reload lines
self.onchange_product_id()
def action_close(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": self.device_id._name,
"res_id": self.device_id.id,
"view_mode": "form",
"target": "main",
"flags": {"headless": False, "clear_breadcrumbs": True},
}
def reload(self):
return {
"type": "ir.actions.act_view_reload",
}
def _notify(self, message):
"""Show a gentle notification on the wizard
We can't use the user set in the current environment because the user
that attends the screen (that opened the wizard, thus created it) may
be not the same than the one (artificial user) that scans and submits
the data, e.g. by using an api call via a controller. We have to send
this original user in the environment because notify_warning checks
that you only notify a user which is the same than the one set in
the environment.
"""
self.ensure_one()
self.create_uid.with_user(self.create_uid.id).notify_warning(message=message)

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_measuring_wizard" model="ir.ui.view">
<field name="name">measuring.wizard.form</field>
<field name="model">measuring.wizard</field>
<field name="arch" type="xml">
<form class="measuring_wizard">
<header>
<button
name="action_reopen_fullscreen"
string="Fullscreen"
type="object"
/>
</header>
<group>
<group name="col1">
<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"
/>
<button
name="reload"
type="object"
string="Refresh"
icon="fa-refresh"
/>
</group>
<group name="col2" />
</group>
<separator />
<field name="line_ids">
<tree
editable="bottom"
create="0"
delete="0"
decoration-warning="scan_requested is True"
>
<field name="scan_requested" invisible="1" />
<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="measuring_select_for_measure"
type="object"
string="Scan"
class="btn btn-warning"
/>
<button
name="measuring_select_for_measure_cancel"
type="object"
string="Cancel Scan"
class="btn btn-danger"
attrs="{'invisible': [('scan_requested', '!=', True)]}"
/>
<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>

View File

@@ -0,0 +1,74 @@
# Copyright 2021 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import _, api, fields, models
class MeasuringWizardLine(models.TransientModel):
_name = "measuring.wizard.line"
_description = "measuring Wizard Line"
_order = "sequence"
scan_requested = fields.Boolean()
wizard_id = fields.Many2one("measuring.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)
is_unit_line = fields.Boolean(readonly=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 measuring_select_for_measure(self):
"""Current line has been selected for measurement
This implies that the device is acquired and locked,
and the packaging is assigned the device."""
self.ensure_one()
success = True
if not self.packaging_id and not self.is_unit_line:
pack_vals = {
"name": self.name,
"packaging_type_id": self.packaging_type_id.id,
"product_id": self.wizard_id.product_id.id,
}
pack = self.env["product.packaging"].create(pack_vals)
self.packaging_id = pack.id
if self.wizard_id.device_id._is_being_used():
self.wizard_id._notify(_("Measurement machine already in use."))
success = False
if success:
self.scan_requested = True
device = self.wizard_id.device_id
self.packaging_id._measuring_device_assign(device)
return success
def measuring_select_for_measure_cancel(self):
"""Current line has been de-selected for measurement
This implies that the packaging clears is assigned device."""
self.ensure_one()
self.scan_requested = False
self.packaging_id._measuring_device_release()
return True