Skip to content
Merged
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
9 changes: 9 additions & 0 deletions spp_drims/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ Dependencies
Changelog
=========

19.0.4.0.1
~~~~~~~~~~

- fix(drims): only let a dispatch ship what its request approved.
Products cannot be added to a request dispatch and quantities cannot
be raised past what was allocated — enforced on the model, so imports
and API callers are covered too, with the Operations tab's Add a line
and delete affordances hidden to match (#1057)

19.0.4.0.0
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_drims/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"and distribution tracking. Links to hazard incidents with multi-tier "
"approval workflows and warehouse operations.",
"category": "OpenSPP/Inventory",
"version": "19.0.4.0.0",
"version": "19.0.4.0.1",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down
89 changes: 87 additions & 2 deletions spp_drims/models/stock_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,13 +321,98 @@ def action_view_drims_return(self):
"res_id": self.drims_return_id.id,
}

def _check_drims_dispatch_matches_request(self):
"""Refuse to dispatch anything the request did not approve (OP#1057).

A dispatch is generated from an approved request, but the Operations tab
stays editable in Ready state, so two things could still be smuggled past
the approval workflow:

1. **Extra products.** "Add a Product" attaches a move that no request
line asked for. Note this is keyed on ``drims_request_line_id`` rather
than Odoo's ``additional`` flag: ``additional`` is only set when a line
is added through the form, so a move created over RPC or by an import
has ``additional = False`` and would slip past a check based on it.

2. **Inflated quantities.** Unlocking the picking makes Demand editable
again, so an approved line can be raised above what was allocated.
Allocation is itself capped at the requested quantity by
``_allocate_stock_fifo``, so comparing against ``quantity_allocated``
transitively enforces the approved amount.

Raises:
UserError: naming the offending products, if either check fails.
"""
for picking in self:
if picking.drims_type != "request_dispatch" or not picking.drims_request_id:
continue

live_moves = picking.move_ids.filtered(lambda m: m.state != "cancel")
live_move_ids = set(live_moves.ids)
approved_line_ids = set(picking.drims_request_id.line_ids.ids)

# 1. Every move has to trace back to a line of *this* request.
unapproved_products = sorted(
{m.product_id.display_name for m in live_moves if m.drims_request_line_id.id not in approved_line_ids}
)
if unapproved_products:
raise UserError(
_(
"Dispatch %(picking)s contains items that are not part of "
"request %(request)s: %(products)s.\n\n"
"A dispatch may only ship what the request had approved and "
"allocated. Remove these lines, or raise a new request for "
"them and have it approved.",
picking=picking.name,
request=picking.drims_request_id.reference,
products=", ".join(unapproved_products),
)
)

# 2. Nothing may ship beyond what the request line had allocated,
# counting what earlier dispatches already shipped for that line.
over_dispatched = []
for line in live_moves.drims_request_line_id:
line_moves = self.env["stock.move"].search(
[
("drims_request_line_id", "=", line.id),
("state", "!=", "cancel"),
]
)
already_shipped = sum(m.quantity for m in line_moves if m.state == "done")
about_to_ship = sum(m.quantity for m in line_moves if m.id in live_move_ids)
if line.uom_id.compare(already_shipped + about_to_ship, line.quantity_allocated) > 0:
over_dispatched.append(
_(
"%(product)s: dispatching %(total)s but only %(allocated)s is allocated",
product=line.product_id.display_name,
total=already_shipped + about_to_ship,
allocated=line.quantity_allocated,
)
)
if over_dispatched:
raise UserError(
_(
"Dispatch %(picking)s would ship more than request "
"%(request)s allocated:\n\n%(details)s\n\n"
"Reduce the quantities, or allocate more stock to the "
"request first.",
picking=picking.name,
request=picking.drims_request_id.reference,
details="\n".join(over_dispatched),
)
)

def button_validate(self):
"""Override button_validate to enforce beneficiary validation and invalidate cache.

When a request_dispatch picking is validated, this:
1. Validates that beneficiary tracking fields are filled (beneficiary_count, beneficiary_area_id)
2. Invalidates the cached KPI values to ensure dashboard shows current data
1. Refuses items or quantities the request never approved (OP#1057)
2. Validates that beneficiary tracking fields are filled (beneficiary_count, beneficiary_area_id)
3. Invalidates the cached KPI values to ensure dashboard shows current data
"""
self._check_drims_dispatch_matches_request()

# Validate beneficiary tracking for DRIMS dispatches
for picking in self:
if picking.drims_type == "request_dispatch":
Expand Down
4 changes: 4 additions & 0 deletions spp_drims/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 19.0.4.0.1

- fix(drims): only let a dispatch ship what its request approved. Products cannot be added to a request dispatch and quantities cannot be raised past what was allocated — enforced on the model, so imports and API callers are covered too, with the Operations tab's Add a line and delete affordances hidden to match (#1057)

### 19.0.4.0.0

- feat(drims): Donations review — creation, receipt, inspection and follow-up. Donations start in a new **Draft** state; the donor list is limited to organisations whose role is Donor and a donation cannot be recorded against a closed incident; at least one item is required to save, Pledged must be entered and be greater than zero, and Received is entered manually rather than copied from Pledged. Line columns appear progressively through the lifecycle (Received and Variance from Announced; Condition and Action from Inspected), non-accepted items gain a follow-up/disposal trail, and adding an item is blocked once the donation has moved past its editable states (#1055, #1058, #1108, #1163)
Expand Down
20 changes: 15 additions & 5 deletions spp_drims/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,16 @@ <h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
</div>
</div>
<div class="section" id="section-1">
<h1>19.0.4.0.1</h1>
<ul class="simple">
<li>fix(drims): only let a dispatch ship what its request approved.
Products cannot be added to a request dispatch and quantities cannot
be raised past what was allocated — enforced on the model, so imports
and API callers are covered too, with the Operations tab’s Add a line
and delete affordances hidden to match (#1057)</li>
</ul>
</div>
<div class="section" id="section-2">
<h1>19.0.4.0.0</h1>
<ul class="simple">
<li>feat(drims): Donations review — creation, receipt, inspection and
Expand All @@ -584,7 +594,7 @@ <h1>19.0.4.0.0</h1>
but no longer readable through the ORM or shown in any view (#1076)</li>
</ul>
</div>
<div class="section" id="section-2">
<div class="section" id="section-3">
<h1>19.0.3.1.0</h1>
<ul class="simple">
<li>feat(drims): Incident Management review — incidents are entered as a
Expand All @@ -601,7 +611,7 @@ <h1>19.0.3.1.0</h1>
refresh cron skips (#1100)</li>
</ul>
</div>
<div class="section" id="section-3">
<div class="section" id="section-4">
<h1>19.0.3.0.4</h1>
<ul class="simple">
<li>feat(drims): rework the dispatch page and correct the waybill.
Expand All @@ -621,7 +631,7 @@ <h1>19.0.3.0.4</h1>
barcode (#1151)</li>
</ul>
</div>
<div class="section" id="section-4">
<div class="section" id="section-5">
<h1>19.0.3.0.1</h1>
<ul class="simple">
<li>fix(drims): a dispatch validated short no longer leaves the request
Expand All @@ -633,7 +643,7 @@ <h1>19.0.3.0.1</h1>
API (#1087)</li>
</ul>
</div>
<div class="section" id="section-5">
<div class="section" id="section-6">
<h1>19.0.3.0.0</h1>
<ul class="simple">
<li>feat(drims): allocate stock per source warehouse. The Allocate Stock
Expand All @@ -653,7 +663,7 @@ <h1>19.0.3.0.0</h1>
destination-type selector (#1075)</li>
</ul>
</div>
<div class="section" id="section-6">
<div class="section" id="section-7">
<h1>19.0.2.0.0</h1>
<ul class="simple">
<li>Initial migration to OpenSPP2</li>
Expand Down
1 change: 1 addition & 0 deletions spp_drims/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from . import test_allocation_preview_wizard
from . import test_approval
from . import test_coordination
from . import test_dispatch_line_lock
from . import test_dispatch_page
from . import test_dispatch_backorder
from . import test_donation
Expand Down
Loading
Loading