-
-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add shopfloor automatic creation of picking batch
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
Showing
13 changed files
with
476 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from . import actions | ||
from . import models | ||
from . import services |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import picking_batch_auto_create |
171 changes: 171 additions & 0 deletions
171
shopfloor_batch_automatic_creation/actions/picking_batch_auto_create.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import shopfloor_menu |
39 changes: 39 additions & 0 deletions
39
shopfloor_batch_automatic_creation/models/shopfloor_menu.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Guewen Baconnier <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import cluster_picking |
25 changes: 25 additions & 0 deletions
25
shopfloor_batch_automatic_creation/services/cluster_picking.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_batch_create |
Oops, something went wrong.