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
3 changes: 2 additions & 1 deletion shopfloor_reception/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Shopfloor Reception
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
.. |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%2Fwms-lightgray.png?logo=github
Expand Down Expand Up @@ -71,6 +71,7 @@ Contributors
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
* Michael Tietz (MT Software) <mtietz@mt-software.de>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Nicolas Delbovier (Acsone) <nicolas.delbovier@acsone.eu>

Maintainers
~~~~~~~~~~~
Expand Down
3 changes: 2 additions & 1 deletion shopfloor_reception/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* Juan Miguel Sánchez Arce <juan.sanchez@camptocamp.com>
* Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
* Michael Tietz (MT Software) <mtietz@mt-software.de>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Nicolas Delbovier (Acsone) <nicolas.delbovier@acsone.eu>
189 changes: 164 additions & 25 deletions shopfloor_reception/services/reception.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,10 +598,43 @@ def _check_move_available(self, move, message_code="product") -> bool:
return self.msg_store.move_already_done()
return False

def _set_quantity__check_quantity_done(self, selected_line):
def _set_quantity__check_quantity_done(self, selected_line, new_qty_done=None):
"""
Compare the total quantity done of a stock move with its expected quantity.

This function calculates the total quantity done for a stock move, including a new
quantity for a specific move line, and compares it with the move's
`product_uom_qty`.

Input:
selected_line: The `stock.move.line` record being updated.
new_qty_done: The new quantity to set on `selected_line`. If None,
use the `qty_done` of the selected line.

Output:
An integer representing the comparison result:
- 1: The total quantity done exceeds the expected quantity.
- 0: The total quantity done equals the expected quantity.
- -1: The total quantity done is less than the expected quantity.
"""
move = selected_line.move_id
max_qty_done = move.product_uom_qty
qty_done = sum(move.move_line_ids.mapped("qty_done"))

# In case `new_qty_done` is set, use this instead of `selected_line.qty_done`
#
# This enables to compute the expected total qty_done on the move before
# apply the new qty done to the selected line in order to avoid having to
# potentially rollback this value afterwards
if new_qty_done:
qty_done = (
sum(
[m.qty_done for m in move.move_line_ids if m.id != selected_line.id]
)
+ new_qty_done
)
else:
qty_done = sum(move.move_line_ids.mapped("qty_done"))

rounding = selected_line.product_uom_id.rounding
return float_compare(qty_done, max_qty_done, precision_rounding=rounding)

Expand Down Expand Up @@ -701,7 +734,7 @@ def _set_package_on_move_line(self, picking, line, package):
return self._response_for_set_quantity(picking, line, message=message)
quantity = line.qty_done
response = self._set_quantity__process__set_qty_and_split(
picking, line, quantity
picking, line, quantity, "_set_package_on_move_line"
)
if response:
return response
Expand Down Expand Up @@ -917,6 +950,42 @@ def _response_for_set_quantity(
)
return self._align_display_product_uom_qty(line, response)

def _response_for_confirm_over_reception(
self,
picking,
line,
quantity,
action,
message,
):
"""
Create a response message to send user to 'confirm_over_reception' UI state.

Input:
picking: the picking being processed
line: the line being processed
quantity: the quantity entered by the worker
action: the action function the user is coming from
(e.g. "process_without_pack", "process_with_new_pack",
"process_with_existing_pack")
message: the warning message to show in the UI

Transitions:
- set_quantity: a bigger qty than expected has been entered and
user still pressed on an action button.
"""
response = self._response(
next_state="confirm_over_reception",
data={
"selected_move_line": self._data_for_move_lines(line),
"picking": self._data_for_stock_picking(picking, with_lines=True),
"quantity": quantity,
"action": action,
},
message=message,
)
return response

def _response_for_set_destination(self, picking, line, message=None):
return self._response(
next_state="set_destination",
Expand Down Expand Up @@ -1311,19 +1380,44 @@ def set_quantity__cancel_action(self, picking_id, selected_line_id):
selected_line.unlink()
return self._response_for_select_move(picking)

def _set_quantity__process__set_qty_and_split(self, picking, line, quantity):
move = line.move_id
sum(move.move_line_ids.mapped("qty_done"))
savepoint = self._actions_for("savepoint").new()
line.qty_done = quantity
compare = self._set_quantity__check_quantity_done(line)
if compare == 1:
# If move's qty_done > to move's qty_todo, rollback and return an error
savepoint.rollback()
return self._response_for_set_quantity(
picking, line, message=self.msg_store.unable_to_pick_qty()
def _after_over_reception_confirmed_hook(self, picking, line):
"""
Post-processing hook for handling over-reception.

This hook function is called when a user confirms an over-reception on a picking.
It can be extended to implement custom business logic, such as:
- Creating a new helpdesk ticket for the supplier.
- Sending a notification to a specific team.
- Automatically adjusting the purchase order quantity.
"""

def _set_quantity__process__set_qty_and_split(
self, picking, line, quantity, action=None, is_over_reception_confirmed=False
):
"""
Input:
picking: the current picking being processed
line: the current move line being processed
quantity: the quantity entered by the user
action: the action function the user is coming from
(e.g. "process_without_pack", "process_with_new_pack",
"process_with_existing_pack")
is_over_reception_confirmed: True if user already confirmed he wants
to receive more goods than expected.
"""
compare = self._set_quantity__check_quantity_done(line, quantity)
if compare == 1 and not is_over_reception_confirmed:
message = self._response_for_confirm_over_reception(
picking,
line,
quantity,
action,
message=self.msg_store.line_scanned_qty_done_higher_than_allowed(),
)
savepoint.release()
return message
else:
line.qty_done = quantity

# Only if total_qty_done < qty_todo, we split the move line
if compare == -1:
default_values = {
Expand All @@ -1333,7 +1427,12 @@ def _set_quantity__process__set_qty_and_split(self, picking, line, quantity):
}
line._split_qty_to_be_done(quantity, **default_values)

def process_with_existing_pack(self, picking_id, selected_line_id, quantity):
if is_over_reception_confirmed:
self._after_over_reception_confirmed_hook(picking, line)

def process_with_existing_pack(
self, picking_id, selected_line_id, quantity, is_over_reception_confirmed=False
):
picking = self.env["stock.picking"].browse(picking_id)
selected_line = self.env["stock.move.line"].browse(selected_line_id)
message = self._check_picking_processible(picking)
Expand All @@ -1342,13 +1441,19 @@ def process_with_existing_pack(self, picking_id, selected_line_id, quantity):
picking, selected_line, message=message
)
response = self._set_quantity__process__set_qty_and_split(
picking, selected_line, quantity
picking,
selected_line,
quantity,
action="process_with_existing_pack",
is_over_reception_confirmed=is_over_reception_confirmed,
)
if response:
return response
return self._response_for_select_dest_package(picking, selected_line)

def process_with_new_pack(self, picking_id, selected_line_id, quantity):
def process_with_new_pack(
self, picking_id, selected_line_id, quantity, is_over_reception_confirmed=False
):
picking = self.env["stock.picking"].browse(picking_id)
selected_line = self.env["stock.move.line"].browse(selected_line_id)
message = self._check_picking_processible(picking)
Expand All @@ -1357,14 +1462,20 @@ def process_with_new_pack(self, picking_id, selected_line_id, quantity):
picking, selected_line, message=message
)
response = self._set_quantity__process__set_qty_and_split(
picking, selected_line, quantity
picking,
selected_line,
quantity,
action="process_with_new_pack",
is_over_reception_confirmed=is_over_reception_confirmed,
)
if response:
return response
picking._put_in_pack(selected_line)
return self._response_for_set_destination(picking, selected_line)

def process_without_pack(self, picking_id, selected_line_id, quantity):
def process_without_pack(
self, picking_id, selected_line_id, quantity, is_over_reception_confirmed=False
):
picking = self.env["stock.picking"].browse(picking_id)
selected_line = self.env["stock.move.line"].browse(selected_line_id)
message = self._check_picking_processible(picking)
Expand All @@ -1373,7 +1484,11 @@ def process_without_pack(self, picking_id, selected_line_id, quantity):
picking, selected_line, message=message
)
response = self._set_quantity__process__set_qty_and_split(
picking, selected_line, quantity
picking,
selected_line,
quantity,
"process_without_pack",
is_over_reception_confirmed,
)
if response:
return response
Expand Down Expand Up @@ -1614,6 +1729,7 @@ def set_quantity(self):
"quantity": {"type": "float"},
"barcode": {"type": "string"},
"confirmation": {"type": "string", "nullable": True},
"is_over_reception_confirmed": {"type": "boolean"},
}

def set_quantity__cancel_action(self):
Expand All @@ -1635,6 +1751,7 @@ def process_with_existing_pack(self):
"required": True,
},
"quantity": {"coerce": to_float, "type": "float"},
"is_over_reception_confirmed": {"type": "boolean"},
}

def process_with_new_pack(self):
Expand All @@ -1646,6 +1763,7 @@ def process_with_new_pack(self):
"required": True,
},
"quantity": {"coerce": to_float, "type": "float"},
"is_over_reception_confirmed": {"type": "boolean"},
}

def process_without_pack(self):
Expand All @@ -1657,6 +1775,7 @@ def process_without_pack(self):
"required": True,
},
"quantity": {"coerce": to_float, "type": "float"},
"is_over_reception_confirmed": {"type": "boolean"},
}

def set_destination(self):
Expand Down Expand Up @@ -1720,6 +1839,7 @@ def _states(self):
"manual_selection": self._schema_manual_selection,
"select_move": self._schema_select_move,
"confirm_done": self._schema_confirm_done,
"confirm_over_reception": self._schema_confirm_over_reception,
"set_lot": self._schema_set_lot,
"set_quantity": self._schema_set_quantity,
"set_destination": self._schema_set_destination,
Expand Down Expand Up @@ -1755,7 +1875,12 @@ def _set_lot_next_states(self):
return {"select_move", "set_lot", "set_quantity"}

def _set_quantity_next_states(self):
return {"set_quantity", "select_move", "set_destination"}
return {
"set_quantity",
"select_move",
"set_destination",
"confirm_over_reception",
}

def _set_quantity__cancel_action_next_states(self):
return {"set_quantity", "select_move"}
Expand All @@ -1773,13 +1898,13 @@ def _set_lot_confirm_action_next_states(self):
return {"set_lot", "set_quantity"}

def _process_with_existing_pack_next_states(self):
return {"set_quantity", "select_dest_package"}
return {"set_quantity", "select_dest_package", "confirm_over_reception"}

def _process_with_new_pack_next_states(self):
return {"set_quantity", "set_destination"}
return {"set_quantity", "set_destination", "confirm_over_reception"}

def _process_without_pack_next_states(self):
return {"set_quantity", "set_destination"}
return {"set_quantity", "set_destination", "confirm_over_reception"}

# SCHEMAS

Expand Down Expand Up @@ -1840,6 +1965,20 @@ def _schema_set_quantity(self):
},
}

@property
def _schema_confirm_over_reception(self):
return {
"selected_move_line": {
"type": "list",
"schema": {"type": "dict", "schema": self.schemas.move_line()},
},
"picking": self.schemas._schema_dict_of(
self._schema_stock_picking_with_lines(), required=True
),
"quantity": {"type": "float", "required": True},
"action": {"type": "string", "required": True},
}

@property
def _schema_set_quantity__cancel_action(self):
return {
Expand Down
Loading