Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shopfloor_reception/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from . import services
from . import services, models
from .hooks import post_init_hook, uninstall_hook
1 change: 1 addition & 0 deletions shopfloor_reception/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import stock_move_line
19 changes: 19 additions & 0 deletions shopfloor_reception/models/stock_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

from odoo import models


class StockMoveLine(models.Model):

_inherit = "stock.move.line"

@property
def shopfloor_should_create_lot(self) -> bool:
"""
This will return True if the line should be used to create lots
"""
return bool(
(not self.lot_id and not self.lot_name)
and self.picking_type_use_create_lots
)
44 changes: 39 additions & 5 deletions shopfloor_reception/services/reception.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@


import pytz
from decorator import contextmanager

from odoo import fields
from odoo.tools import float_compare

from odoo.addons.base_rest.components.service import to_int
from odoo.addons.component.core import Component
from odoo.addons.shopfloor.actions.search import SearchResult
from odoo.addons.shopfloor.utils import to_float


Expand Down Expand Up @@ -48,6 +50,19 @@ class Reception(Component):
_usage = "reception"
_description = __doc__

search_result = SearchResult()

@contextmanager
def with_search_result(self, search_result: SearchResult):
"""
Use this context manager if you want to include search result in
component behavior.

"""
self.search_result = search_result
yield
self.search_result = SearchResult()

def _check_picking_processible(self, pickings):
# When returns are allowed,
# the created picking might be empty and cannot be assigned.
Expand Down Expand Up @@ -428,7 +443,13 @@ def _scan_line__by_product__return(self, picking, product):
picking.action_assign()
return self._scan_line__find_or_create_line(picking, return_move)

def _scan_line__dummy(self):
return

def _scan_line__by_product(self, picking, product):
"""
Try to find a move by product
"""
moves = picking.move_ids.filtered(lambda m: m.product_id == product)
# Only create a return if don't already have a maching reception move
if not moves and self.work.menu.allow_return:
Expand Down Expand Up @@ -488,6 +509,9 @@ def _scan_line__by_packaging(self, picking, packaging):
return self._scan_line__find_or_create_line(picking, move)

def _scan_line__by_lot(self, picking, lot):
"""
Try to find a move line by its lot (it should already be assigned)
"""
lines = picking.move_line_ids.filtered(
lambda l: (
lot == l.lot_id
Expand Down Expand Up @@ -529,7 +553,11 @@ def _scan_line__fallback(self, picking, barcode):
message=message,
)

def _check_move_available(self, move, message_code="product"):
def _check_move_available(self, move, message_code="product") -> bool:
"""
This will check if move is available to be selected by user
scan
"""
if not move:
message_code = message_code.capitalize()
return self.msg_store.x_not_found_or_already_in_dest_package(message_code)
Expand All @@ -538,6 +566,7 @@ def _check_move_available(self, move, message_code="product"):
)
if move.product_uom_qty - move.quantity_done < 1 and not line_without_package:
return self.msg_store.move_already_done()
return False

def _set_quantity__check_quantity_done(self, selected_line):
move = selected_line.move_id
Expand Down Expand Up @@ -946,14 +975,19 @@ def scan_line(self, picking_id, barcode):
"product": self._scan_line__by_product,
"packaging": self._scan_line__by_packaging,
"lot": self._scan_line__by_lot,
"expiration_date": self._scan_line__dummy,
}
search = self._actions_for("search")
search_result = search.find(barcode, handlers_by_type.keys())
# Fallback handler, returns a barcode not found error
handler = handlers_by_type.get(search_result.type)
if handler:
return handler(picking, search_result.record)
return self._scan_line__fallback(picking, barcode)

# This could maybe be removed if we pass instead
# the search result through all calls
with self.with_search_result(search_result):
if handler:
return handler(picking, search_result.record)
return self._scan_line__fallback(picking, barcode)

def manual_select_move(self, move_id):
move = self.env["stock.move"].browse(move_id)
Expand Down Expand Up @@ -1045,7 +1079,7 @@ def set_lot(
)
selected_line.lot_id = lot.id
selected_line._onchange_lot_id()
elif expiration_date:
if expiration_date:
selected_line.write({"expiration_date": expiration_date})
selected_line.lot_id.write({"expiration_date": expiration_date})
return self._response_for_set_lot(picking, selected_line)
Expand Down
1 change: 0 additions & 1 deletion shopfloor_reception/tests/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# pylint: disable=missing-return

from odoo import fields

from odoo.addons.shopfloor.tests.common import CommonCase as BaseCommonCase
Expand Down
59 changes: 59 additions & 0 deletions shopfloor_reception/tests/test_multi_barcode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import mock

from odoo import fields

from odoo.addons.shopfloor.actions.barcode_parser import BarcodeParser, BarcodeResult

from .common import CommonCase


class TestStructuredBarcode(CommonCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product_a.tracking = "lot"
cls.product_a.use_expiration_date = True
cls.picking_type.sudo().use_create_lots = True

def test_scan_multiple_attribute_barcode(self):
"""
Check that scanning a product with multi attribute barcode
will fill in the lot
"""
picking = self._create_picking()
lot = self._create_lot()
selected_move_line = picking.move_line_ids.filtered(
lambda l: l.product_id == self.product_a
)
# selected_move_line.lot_id = lot
with mock.patch.object(BarcodeParser, "parse") as mock_parse:
mock_parse.return_value = [
BarcodeResult(type="lot", value=lot.name, raw=lot.name),
BarcodeResult(
type="expiration_date",
value=fields.Date.to_date("2025-04-15"),
raw="250415",
),
]
response = self.service.dispatch(
"scan_line",
params={
"picking_id": picking.id,
"barcode": lot.name,
},
)
data = self.data.picking(picking)
self.assert_response(
response,
next_state="set_lot",
data={
"picking": data,
"selected_move_line": self.data.move_lines(selected_move_line),
},
)
self.assertEqual(
selected_move_line.expiration_date,
fields.Datetime.to_datetime("2025-04-15"),
)
2 changes: 2 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
vcrpy-unittest
odoo_test_helper

odoo-addon-shopfloor @ git+https://github.com/OCA/wms@refs/pull/1001/head#subdirectory=setup/shopfloor
Loading