Skip to content

Commit

Permalink
Add shopfloor automatic creation of picking batch
Browse files Browse the repository at this point in the history
When a user uses the "Get Work" button on the barcode device, if no transfer
batch is available, it automatically creates a new batch for the user.
  • Loading branch information
guewen authored and lmignon committed May 10, 2024
1 parent dfece68 commit a305256
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 0 deletions.
3 changes: 3 additions & 0 deletions shopfloor_batch_automatic_creation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import actions
from . import models
from . import services
21 changes: 21 additions & 0 deletions shopfloor_batch_automatic_creation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2020 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
"name": "Shopfloor - Batch Transfer Automatic Creation",
"summary": "Create batch transfers for Cluster Picking",
"version": "13.0.1.0.0",
"development_status": "Alpha",
"category": "Inventory",
"website": "https://github.com/OCA/wms",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": True,
"depends": [
"shopfloor",
"stock_packaging_calculator", # OCA/stock-logistics-warehouse
"product_packaging_dimension", # OCA/stock-logistics-warehouse
"product_total_weight_from_packaging", # OCA/product-attribute
],
"data": ["views/shopfloor_menu_views.xml"],
}
1 change: 1 addition & 0 deletions shopfloor_batch_automatic_creation/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import picking_batch_auto_create
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2020 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import hashlib
import logging
import struct

from odoo import fields, tools

from odoo.addons.component.core import Component

_logger = logging.getLogger(__name__)


class PickingBatchAutoCreateAction(Component):
"""Automatic creation of picking batches"""

_name = "shopfloor.picking.batch.auto.create"
_inherit = "shopfloor.process.action"
_usage = "picking.batch.auto.create"

_advisory_lock_name = "shopfloor_batch_picking_create"

def create_batch(self, picking_types, max_pickings=0, max_weight=0, max_volume=0):
self._lock()
pickings = self._search_pickings(picking_types, user=self.env.user)
if not pickings:
pickings = self._search_pickings(picking_types)

pickings = self._sort(pickings)
pickings = self._apply_limits(pickings, max_pickings, max_weight, max_volume)
if not pickings:
return self.env["stock.picking.batch"].browse()
return self._create_batch(pickings)

def _lock(self):
"""Lock to prevent concurrent creation of batch
Use a blocking advisory lock to prevent 2 transactions to create
a batch at the same time. The lock is released at the commit or
rollback of the transaction.
The creation of a new batch should be short enough not to block
the users for too long.
"""
_logger.info(
"trying to acquire lock to create a picking batch (%s)", self.env.user.login
)
hasher = hashlib.sha1(str(self._advisory_lock_name).encode())
# pg_lock accepts an int8 so we build an hash composed with
# contextual information and we throw away some bits
int_lock = struct.unpack("q", hasher.digest()[:8])

self.env.cr.execute("SELECT pg_advisory_xact_lock(%s);", (int_lock,))
self.env.cr.fetchone()[0]
# Note: if the lock had to wait, the snapshot of the transaction is
# very much probably outdated already (i.e. if the transaction which
# had the lock before this one set a 'batch_id' on stock.picking this
# transaction will not be aware of), we'll probably have a retry. But
# the lock can help limit the number of retries.
_logger.info(
"lock acquired to create a picking batch (%s)", self.env.user.login
)

def _search_pickings_domain(self, picking_types, user=None):
domain = [
("picking_type_id", "in", picking_types.ids),
("state", "in", ("assigned", "partially_available")),
("batch_id", "=", False),
("user_id", "=", user.id if user else False),
]
return domain

def _search_pickings(self, picking_types, user=None):
# We can't use a limit in the SQL search because the 'priority' fields
# is sometimes empty (it seems the inverse StockPicking.priority field
# mess up with default on stock.move), we have to sort in Python.
return self.env["stock.picking"].search(
self._search_pickings_domain(picking_types, user=user)
)

def _sort(self, pickings):
return pickings.sorted(
lambda picking: (
-(int(picking.priority) if picking.priority else 1),
picking.scheduled_date,
picking.id,
)
)

def _precision_weight(self):
return self.env["decimal.precision"].precision_get("Product Unit of Measure")

def _precision_volume(self):
return max(
6,
self.env["decimal.precision"].precision_get("Product Unit of Measure") * 2,
)

def _apply_limits(self, pickings, max_pickings, max_weight, max_volume):
current_priority = fields.first(pickings).priority or "1"
selected_pickings = self.env["stock.picking"].browse()

precision_weight = self._precision_weight()
precision_volume = self._precision_volume()

def gt(value1, value2, digits):
"""Return True if value1 is greater than value2"""
return tools.float_compare(value1, value2, precision_digits=digits) == 1

total_weight = 0.0
total_volume = 0.0
for picking in pickings:
if (picking.priority or "1") != current_priority:
# as we sort by priority, exit as soon as the priority changes,
# we do not mix priorities to make delivery of high priority
# transfers faster
break

weight = self._picking_weight(picking)
volume = self._picking_volume(picking)
if max_weight and gt(total_weight + weight, max_weight, precision_weight):
continue

if max_volume and gt(total_volume + volume, max_volume, precision_volume):
continue

selected_pickings |= picking
total_weight += weight
total_volume += volume

if max_pickings and len(selected_pickings) == max_pickings:
# selected enough!
break

return selected_pickings

def _picking_weight(self, picking):
weight = 0.0
for line in picking.move_lines:
weight += line.product_id.get_total_weight_from_packaging(
line.product_uom_qty
)
return weight

def _picking_volume(self, picking):
volume = 0.0
for line in picking.move_lines:
product = line.product_id
packagings_with_volume = product.with_context(
_packaging_filter=lambda p: p.volume
).product_qty_by_packaging(line.product_uom_qty)
for packaging_info in packagings_with_volume:
if packaging_info.get("is_unit"):
pack_volume = product.volume
else:
packaging = self.env["product.packaging"].browse(
packaging_info["id"]
)
pack_volume = packaging.volume

volume += pack_volume * packaging_info["qty"]
return volume

def _create_batch(self, pickings):
return self.env["stock.picking.batch"].create(
self._create_batch_values(pickings)
)

def _create_batch_values(self, pickings):
return {"picking_ids": [(6, 0, pickings.ids)]}
1 change: 1 addition & 0 deletions shopfloor_batch_automatic_creation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import shopfloor_menu
39 changes: 39 additions & 0 deletions shopfloor_batch_automatic_creation/models/shopfloor_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2020 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import fields, models


class ShopfloorMenu(models.Model):
_inherit = "shopfloor.menu"

batch_create = fields.Boolean(
string="Automatic Batch Creation",
default=False,
help='Automatically create a batch when an operator uses the "Get Work"'
" button and no existing batch has been found. The system will first look"
" for priority transfers and fill up the batch till the defined"
" constraints (max of transfers, volume, weight, ...)."
" It never mixes priorities, so if you get 2 available priority transfers"
" and a max quantity of 3, the batch will only contain the 2"
" priority transfers.",
)
batch_create_max_picking = fields.Integer(
string="Max transfers",
default=0,
help="Maximum number of transfers to add in an automatic batch."
" 0 means no limit.",
)
batch_create_max_volume = fields.Float(
string="Max volume (m³)",
default=0,
digits=(8, 4),
help="Maximum volume in cubic meters of goods in transfers to"
" add in an automatic batch. 0 means no limit.",
)
batch_create_max_weight = fields.Float(
string="Max Weight (kg)",
default=0,
help="Maximum weight in kg of goods in transfers to add"
" in an automatic batch. 0 means no limit.",
)
1 change: 1 addition & 0 deletions shopfloor_batch_automatic_creation/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <[email protected]>
22 changes: 22 additions & 0 deletions shopfloor_batch_automatic_creation/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Extension for Shopfloor's cluster picking.

When a user uses the "Get Work" button on the barcode device, if no transfer
batch is available, it automatically creates a new batch for the user.

Some options can be configured on the Shopfloor menu:

* Activate or not the batch creation
* Max number of transfer per batch
* Max weight per batch
* Max volume per batch

The rules are:

* Transfers of higher priority are put first in the batch
* If some transfers are assigned to the user, the batch will only contain
those, otherwise, it looks for unassigned transfers
* Priorities are not mixed to make transfers with higher priority faster
e.g. if the limit is 5, with 3 Very Urgent transfer and 10 Normal transfer,
the batch will contain only the 3 Very Urgent despite the higher limit
* The weight and volume are based on the Product Packaging when their weight and
volume are defined
1 change: 1 addition & 0 deletions shopfloor_batch_automatic_creation/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import cluster_picking
25 changes: 25 additions & 0 deletions shopfloor_batch_automatic_creation/services/cluster_picking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2020 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.addons.component.core import Component


class ClusterPicking(Component):
_inherit = "shopfloor.cluster.picking"

def _select_a_picking_batch(self, batches):
batch = super()._select_a_picking_batch(batches)
if not batch and self.work.menu.batch_create:
batch = self._batch_auto_create()
batch.write({"user_id": self.env.uid, "state": "in_progress"})
return batch

def _batch_auto_create(self):
auto_batch = self.actions_for("picking.batch.auto.create")
menu = self.work.menu
return auto_batch.create_batch(
self.picking_types,
max_pickings=menu.batch_create_max_picking,
max_volume=menu.batch_create_max_volume,
max_weight=menu.batch_create_max_weight,
)
1 change: 1 addition & 0 deletions shopfloor_batch_automatic_creation/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_batch_create
Loading

0 comments on commit a305256

Please sign in to comment.