Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP][14.0] shopfloor delivery: Better messages #972

Draft
wants to merge 4 commits into
base: 14.0
Choose a base branch
from
Draft
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
18 changes: 18 additions & 0 deletions shopfloor/actions/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def confirm_pack_moved(self):
def already_done(self):
return {"message_type": "info", "body": _("Operation already processed.")}

def transfer_cancelled(self):
return {
"message_type": "info",
"body": _(
"Transfer has been cancelled. "
"This cannot be processed using this scenario"
),
}

def move_already_done(self):
return {"message_type": "warning", "body": _("Move already processed.")}

Expand Down Expand Up @@ -952,3 +961,12 @@ def lot_change_no_line_found(self):
"message_type": "error",
"body": _("Unable to find a line with the same product but different lot."),
}

def picking_type_is_return(self):
return {
"message_type": "error",
"body": _(
"The scanned barcode is related to a return transfer."
"This product is meant to be transfered by another scenario."
),
}
96 changes: 56 additions & 40 deletions shopfloor/services/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,47 +147,54 @@ def scan_deliver(self, barcode, picking_id=None, location_id=None):
if picking_id:
picking = self.env["stock.picking"].browse(picking_id)

# Validate picking anyway
if not barcode_valid:
package = search.package_from_scan(barcode)
if package:
return self._deliver_package(picking, package, location)

if not barcode_valid:
product = search.product_from_scan(barcode)
if product:
return self._deliver_product(
picking, product, product_qty=1, location=location
)

if not barcode_valid:
packaging = search.packaging_from_scan(barcode)
if packaging:
# By scanning a packaging, we want to process
# the full quantity of the packaging
packaging_qty = packaging.product_uom_id._compute_quantity(
packaging.qty, packaging.product_id.uom_id
)
return self._deliver_product(
picking,
packaging.product_id,
product_qty=packaging_qty,
location=location,
)
handlers_by_type = {
"package": self._scan_deliver__by_package,
"product": self._scan_deliver__by_product,
"packaging": self._scan_deliver__by_packaging,
"lot": self._scan_deliver__by_lot,
"location": self._scan_deliver__by_location,
}
search_result = search.find(barcode, handlers_by_type.keys())
handler = handlers_by_type.get(search_result.type)
if handler:
result = handler(search_result.record, picking, location)
if result:
return result
return self._scan_deliver__fallback(picking, location, barcode_valid)

def _scan_deliver__by_package(self, package, picking, location):
return self._deliver_package(picking, package, location)

def _scan_deliver__by_product(self, product, picking, location):
return self._deliver_product(picking, product, product_qty=1, location=location)

def _scan_deliver__by_packaging(self, packaging, picking, location):
# By scanning a packaging, we want to process
# the full quantity of the packaging
packaging_qty = packaging.product_uom_id._compute_quantity(
packaging.qty, packaging.product_id.uom_id
)
return self._deliver_product(
picking,
packaging.product_id,
product_qty=packaging_qty,
location=location,
)

if not barcode_valid:
lot = search.lot_from_scan(barcode)
if lot:
return self._deliver_lot(picking, lot, product_qty=1, location=location)
def _scan_deliver__by_lot(self, lot, picking, location):
return self._deliver_lot(picking, lot, product_qty=1, location=location)

if not barcode_valid:
sublocation = search.location_from_scan(barcode)
if sublocation and sublocation.is_sublocation_of(
self.picking_types.mapped("default_location_src_id")
):
message = self.msg_store.location_src_set_to_sublocation(sublocation)
return self._response_for_deliver(location=sublocation, message=message)
def _scan_deliver__by_location(self, scanned_location, picking, location):
if scanned_location.is_sublocation_of(
self.picking_types.mapped("default_location_src_id")
):
message = self.msg_store.location_src_set_to_sublocation(scanned_location)
return self._response_for_deliver(
location=scanned_location, message=message
)

def _scan_deliver__fallback(self, picking, location, barcode_valid):
message = self.msg_store.barcode_not_found() if not barcode_valid else None
return self._response_for_deliver(
picking=picking, location=location, message=message
Expand Down Expand Up @@ -255,11 +262,20 @@ def _deliver_package(self, picking, package, location):
return self._response_for_deliver(picking=new_picking, location=location)

def _lines_base_domain(self, no_qty_done=True):
# we added auto_join for this, otherwise, the ORM would search all pickings
# in the picking type, and then use IN (ids)
domain = [
# we added auto_join for this, otherwise, the ORM would search all pickings
# in the picking type, and then use IN (ids)
# Accepting return_picking_types in order to display meaningful
# messages when trying to process a return move.
# Those returns are blocked later in `_check_picking_status`
"|",
("picking_id.picking_type_id", "in", self.picking_types.ids),
("picking_id.state", "not in", ("done", "cancel")),
(
"picking_id.picking_type_id",
"in",
self.picking_types.return_picking_type_id.ids,
),
("picking_id.state", "not in", ("done",)),
]
if no_qty_done:
domain.append(("qty_done", "=", 0))
Expand Down
4 changes: 4 additions & 0 deletions shopfloor/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ def _check_picking_status(self, pickings, states=("assigned",)):
return self.msg_store.stock_picking_not_found()
if picking.state == "done":
return self.msg_store.already_done()
if picking.state == "cancel":
return self.msg_store.transfer_cancelled()
if picking.state not in states: # the picking must be ready
return self.msg_store.stock_picking_not_available(picking)
if picking.picking_type_id in self.picking_types.return_picking_type_id:
return self.msg_store.picking_type_is_return()
if picking.picking_type_id not in self.picking_types:
return self.msg_store.cannot_move_something_in_picking_type()

Expand Down
133 changes: 133 additions & 0 deletions shopfloor/tests/test_delivery_scan_deliver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ class DeliveryScanDeliverCase(DeliveryCommonCase):
@classmethod
def setUpClassBaseData(cls):
super().setUpClassBaseData()
cls.cleanup_type = (
cls.env["stock.picking.type"]
.sudo()
.create(
{
"name": "Cancel Cleanup",
"default_location_dest_id": cls.stock_location.id,
"sequence_code": "CCP",
"code": "internal",
}
)
)
cls.picking_type.sudo().return_picking_type_id = cls.cleanup_type
cls.product_e.tracking = "lot"
cls.picking = picking = cls._create_picking(
lines=[
Expand Down Expand Up @@ -445,6 +458,126 @@ def test_scan_deliver_scan_product_alone_in_package_qty_one(self):
move_lines = pick.move_lines.mapped("move_line_ids")
self._test_scan_set_done_ok(move_lines, self.product_c.barcode, [1])

def test_scan_deliver_picking_cancelled(self):
self.picking.action_cancel()
params = {"barcode": self.picking.name}
response = self.service.dispatch("scan_deliver", params=params)
self.assert_response_deliver(
response,
message=self.service.msg_store.transfer_cancelled(),
)

def test_scan_deliver_return_package(self):
self.picking.action_cancel()
cleanup_picking = self._create_picking(
picking_type=self.cleanup_type, lines=[(self.product_a, 1)]
)
package_vals = [(self.product_a, 1, None)]
cleanup_package = self._create_package_in_location(
cleanup_picking.location_id, package_vals
)
cleanup_package.name = "CLEANUP_PACKAGE"
cleanup_picking.move_line_ids.package_id = cleanup_package
params = {"barcode": "CLEANUP_PACKAGE"}
response = self.service.dispatch("scan_deliver", params=params)
expected_body = "\n".join(
[
"Package CLEANUP_PACKAGE belongs to a picking without a valid state.",
(
"The scanned barcode is related to a return transfer."
"This product is meant to be transfered by another scenario."
),
]
)
self.assertEqual(response.get("message").get("body"), expected_body)

def test_scan_deliver_return_product(self):
self.picking.action_cancel()
cleanup_picking = self._create_picking(
picking_type=self.cleanup_type, lines=[(self.product_a, 1)]
)
params = {"barcode": self.product_a.barcode}
response = self.service.dispatch("scan_deliver", params=params)
expected_body = "\n".join(
[
"Product Product A belongs to a picking without a valid state.",
(
"The scanned barcode is related to a return transfer."
"This product is meant to be transfered by another scenario."
),
]
)
self.assertEqual(response.get("message").get("body"), expected_body)

def test_scan_deliver_return_packaging(self):
self.picking.action_cancel()
cleanup_picking = self._create_picking(
picking_type=self.cleanup_type, lines=[(self.product_a, 1)]
)
packaging = (
self.env["product.packaging"]
.sudo()
.create(
{
"name": "CLEANUP PACKAGING",
"product_id": self.product_a.id,
"qty": 1,
"product_uom_id": self.product_a.id,
"barcode": "CLEANUP_PACKAGING",
}
)
)
params = {"barcode": "CLEANUP_PACKAGING"}
response = self.service.dispatch("scan_deliver", params=params)
expected_body = "\n".join(
[
"Product Product A belongs to a picking without a valid state.",
(
"The scanned barcode is related to a return transfer."
"This product is meant to be transfered by another scenario."
),
]
)
self.assertEqual(response.get("message").get("body"), expected_body)

def test_scan_deliver_return_lot(self):
self.picking.action_cancel()
cleanup_picking = self._create_picking(
picking_type=self.cleanup_type, lines=[(self.product_a, 1)]
)
cleanup_lot = self.env["stock.production.lot"].create(
{
"product_id": self.product_a.id,
"company_id": self.env.company.id,
"name": "CLEANUP_LOT",
"ref": "CLEANUP_LOT",
}
)
cleanup_picking.move_line_ids.lot_id = cleanup_lot
params = {"barcode": "CLEANUP_LOT"}
response = self.service.dispatch("scan_deliver", params=params)
expected_body = "\n".join(
[
"Lot CLEANUP_LOT belongs to a picking without a valid state.",
(
"The scanned barcode is related to a return transfer."
"This product is meant to be transfered by another scenario."
),
]
)
self.assertEqual(response.get("message").get("body"), expected_body)

def test_scan_delivery_return_picking(self):
cleanup_picking = self._create_picking(
picking_type=self.cleanup_type, lines=[(self.product_c, 1)]
)
params = {"barcode": cleanup_picking.name}
response = self.service.dispatch("scan_deliver", params=params)
self.assert_response_deliver(
response,
message=self.service.msg_store.picking_type_is_return(),
)

def test_scan_deliver_picking_done(self):
# Set qty done for all lines (packages/raw product/lot...), picking is
# automatically set to done when the last line is completed
Expand Down
2 changes: 1 addition & 1 deletion shopfloor/tests/test_delivery_set_qty_done_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_set_qty_done_line_picking_canceled(self):
)
self.assert_response_deliver(
response,
message=self.service.msg_store.stock_picking_not_available(self.picking),
message=self.service.msg_store.transfer_cancelled(),
)

def test_set_qty_done_line_line_not_found(self):
Expand Down
2 changes: 1 addition & 1 deletion shopfloor/tests/test_delivery_set_qty_done_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def test_set_qty_done_pack_picking_canceled(self):
)
self.assert_response_deliver(
response,
message=self.service.msg_store.stock_picking_not_available(self.picking),
message=self.service.msg_store.transfer_cancelled(),
)

def test_set_qty_done_pack_package_not_found(self):
Expand Down
10 changes: 10 additions & 0 deletions stock_available_to_promise_release/models/stock_location_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
class Route(models.Model):
_inherit = "stock.location.route"

allow_unrelease_return_done_move = fields.Boolean(
string="Reverse done transfer on cancellation",
default=False,
help=(
"If checked, unreleasing the delivery may create a new inverse "
"internal operation on the last done pulled transfer. "
"Otherwise, you won't be able to unrelease as soon as one of "
"the pulled transfer is done"
),
)
available_to_promise_defer_pull = fields.Boolean(
string="Release based on Available to Promise",
default=False,
Expand Down
Loading
Loading