diff --git a/spp_drims/README.rst b/spp_drims/README.rst
index bcf7e3a2..70f8ed5d 100644
--- a/spp_drims/README.rst
+++ b/spp_drims/README.rst
@@ -179,6 +179,15 @@ Dependencies
Changelog
=========
+19.0.4.0.3
+~~~~~~~~~~
+
+- fix(drims): show only the states a dispatch can reach on its status
+ bar. A request dispatch is confirmed the moment it is created, so
+ Draft never applies to it; the shared status bar keeps Draft for every
+ other outgoing transfer. Waiting is hidden as a future step but still
+ shows when a dispatch is actually in it (#1086)
+
19.0.4.0.2
~~~~~~~~~~
diff --git a/spp_drims/__manifest__.py b/spp_drims/__manifest__.py
index 51d332c6..9650778f 100644
--- a/spp_drims/__manifest__.py
+++ b/spp_drims/__manifest__.py
@@ -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.2",
+ "version": "19.0.4.0.3",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
diff --git a/spp_drims/readme/HISTORY.md b/spp_drims/readme/HISTORY.md
index 4b776a4b..bd72b17c 100644
--- a/spp_drims/readme/HISTORY.md
+++ b/spp_drims/readme/HISTORY.md
@@ -1,3 +1,7 @@
+### 19.0.4.0.3
+
+- fix(drims): show only the states a dispatch can reach on its status bar. A request dispatch is confirmed the moment it is created, so Draft never applies to it; the shared status bar keeps Draft for every other outgoing transfer. Waiting is hidden as a future step but still shows when a dispatch is actually in it (#1086)
+
### 19.0.4.0.2
- feat(drims): confirm a delivery through a popup rather than the dispatch form. Confirm Delivery now collects the receiver, signature, photos and GPS together with what actually arrived per line, and records the delivered quantities against the request so fulfilment reflects what was received rather than what was sent (#1088)
diff --git a/spp_drims/static/description/index.html b/spp_drims/static/description/index.html
index c811220b..666c2787 100644
--- a/spp_drims/static/description/index.html
+++ b/spp_drims/static/description/index.html
@@ -565,6 +565,16 @@
+
19.0.4.0.3
+
+- fix(drims): show only the states a dispatch can reach on its status
+bar. A request dispatch is confirmed the moment it is created, so
+Draft never applies to it; the shared status bar keeps Draft for every
+other outgoing transfer. Waiting is hidden as a future step but still
+shows when a dispatch is actually in it (#1086)
+
+
+
19.0.4.0.2
- feat(drims): confirm a delivery through a popup rather than the
@@ -574,7 +584,7 @@
19.0.4.0.2
reflects what was received rather than what was sent (#1088)
-
+
19.0.4.0.1
- fix(drims): only let a dispatch ship what its request approved.
@@ -584,7 +594,7 @@
19.0.4.0.1
and delete affordances hidden to match (#1057)
-
+
19.0.4.0.0
- feat(drims): Donations review — creation, receipt, inspection and
@@ -604,7 +614,7 @@
19.0.4.0.0
but no longer readable through the ORM or shown in any view (#1076)
-
+
19.0.3.1.0
- feat(drims): Incident Management review — incidents are entered as a
@@ -621,7 +631,7 @@
19.0.3.1.0
refresh cron skips (#1100)
-
+
19.0.3.0.4
- feat(drims): rework the dispatch page and correct the waybill.
@@ -641,7 +651,7 @@
19.0.3.0.4
barcode (#1151)
-
+
19.0.3.0.1
- fix(drims): a dispatch validated short no longer leaves the request
@@ -653,7 +663,7 @@
19.0.3.0.1
API (#1087)
-
+
19.0.3.0.0
- feat(drims): allocate stock per source warehouse. The Allocate Stock
@@ -673,7 +683,7 @@
19.0.3.0.0
destination-type selector (#1075)
-
+
19.0.2.0.0
- Initial migration to OpenSPP2
diff --git a/spp_drims/tests/__init__.py b/spp_drims/tests/__init__.py
index 52428d64..ca274d4f 100644
--- a/spp_drims/tests/__init__.py
+++ b/spp_drims/tests/__init__.py
@@ -5,6 +5,7 @@
from . import test_allocation_preview_wizard
from . import test_approval
from . import test_coordination
+from . import test_dispatch_statusbar
from . import test_delivery_confirmation_wizard
from . import test_dispatch_line_lock
from . import test_dispatch_page
diff --git a/spp_drims/tests/test_dispatch_statusbar.py b/spp_drims/tests/test_dispatch_statusbar.py
new file mode 100644
index 00000000..d225f9bb
--- /dev/null
+++ b/spp_drims/tests/test_dispatch_statusbar.py
@@ -0,0 +1,197 @@
+# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
+from datetime import date, timedelta
+
+from lxml import etree
+
+from odoo.tests import tagged
+from odoo.tools.safe_eval import safe_eval
+
+from .common import DrimsTestCommon
+
+
+@tagged("post_install", "-at_install")
+class TestDrimsDispatchStatusbar(DrimsTestCommon):
+ """OP#1086: a request dispatch should not advertise states it never sits in.
+
+ Core renders two statusbars for stock.picking, split on picking_type_code. A
+ dispatch is outgoing, so it picks up the non-incoming one
+ (draft,confirmed,assigned,done). spp_drims narrows that one to exclude
+ dispatches and adds a dispatch-only bar, rather than editing the shared
+ statusbar_visible — which would drop Draft from every non-incoming transfer.
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.future_date = date.today() + timedelta(days=30)
+
+ # ------------------------------------------------------------------
+ # helpers
+ # ------------------------------------------------------------------
+
+ def _statusbars(self):
+ """The state statusbar fields in the combined stock.picking form arch."""
+ view = self.env["stock.picking"].get_view(self.env.ref("stock.view_picking_form").id, "form")
+ tree = etree.fromstring(view["arch"])
+ return tree.xpath("//header/field[@name='state'][@widget='statusbar']")
+
+ def _stock_up(self, quantity):
+ self.env["stock.quant"].create(
+ {
+ "product_id": self.product.id,
+ "location_id": self.warehouse.lot_stock_id.id,
+ "quantity": quantity,
+ }
+ )
+
+ def _allocated_request(self, requested=100):
+ request = self.env["spp.drims.request"].create(
+ {
+ "incident_id": self.incident.id,
+ "destination_area_id": self.area.id,
+ "date_needed": self.future_date,
+ "line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": self.product.id,
+ "quantity_requested": requested,
+ "uom_id": self.product.uom_id.id,
+ },
+ )
+ ],
+ }
+ )
+ request.action_submit()
+ request.action_approve()
+ return request
+
+ # ------------------------------------------------------------------
+ # the statusbar arch
+ # ------------------------------------------------------------------
+
+ def test_dispatch_statusbar_shows_only_ready_and_done(self):
+ """A dispatch-only statusbar exists, listing just Ready and Done."""
+ bars = [b for b in self._statusbars() if b.get("statusbar_visible") == "assigned,done"]
+ self.assertEqual(len(bars), 1, "expected exactly one dispatch statusbar")
+ condition = bars[0].get("invisible")
+ self.assertIn("drims_type != 'request_dispatch'", condition)
+ self.assertIn("picking_type_code == 'incoming'", condition)
+
+ def test_other_picking_types_keep_the_full_statusbar(self):
+ """AC: no regression for receipts, returns or internal transfers.
+
+ Core's two bars must survive untouched apart from the added exclusion, so
+ anything that is not a request dispatch still shows Draft and Waiting.
+ """
+ by_visible = {b.get("statusbar_visible"): b for b in self._statusbars()}
+
+ # Incoming bar: completely untouched.
+ self.assertIn("draft,assigned,done", by_visible)
+ self.assertEqual(by_visible["draft,assigned,done"].get("invisible"), "picking_type_code != 'incoming'")
+
+ # Non-incoming bar: still lists Draft and Waiting, now skipped only for dispatches.
+ self.assertIn("draft,confirmed,assigned,done", by_visible)
+ non_incoming = by_visible["draft,confirmed,assigned,done"].get("invisible")
+ self.assertIn("drims_type == 'request_dispatch'", non_incoming)
+
+ def _visible_statusbars(self, **record):
+ """statusbar_visible of every bar whose ``invisible`` is falsy for ``record``.
+
+ The attributes are ordinary Python expressions over field values, so
+ evaluate them rather than pattern-matching the strings.
+ """
+ visible = []
+ for bar in self._statusbars():
+ if not safe_eval(bar.get("invisible", "False"), dict(record)):
+ visible.append(bar.get("statusbar_visible"))
+ return visible
+
+ def test_exactly_one_statusbar_applies_per_picking(self):
+ """The three bars must be mutually exclusive, or the form renders two.
+
+ Also pins the actual outcome per picking kind, which is the AC.
+ """
+ self.assertEqual(
+ self._visible_statusbars(picking_type_code="outgoing", drims_type="request_dispatch"),
+ ["assigned,done"],
+ "a request dispatch should show only Ready and Done",
+ )
+ self.assertEqual(
+ self._visible_statusbars(picking_type_code="outgoing", drims_type=False),
+ ["draft,confirmed,assigned,done"],
+ "a plain delivery keeps the full bar",
+ )
+ self.assertEqual(
+ self._visible_statusbars(picking_type_code="incoming", drims_type="donation_receipt"),
+ ["draft,assigned,done"],
+ "a DRIMS donation receipt keeps core's incoming bar",
+ )
+ self.assertEqual(
+ self._visible_statusbars(picking_type_code="internal", drims_type="internal_transfer"),
+ ["draft,confirmed,assigned,done"],
+ "a DRIMS internal transfer keeps the full bar",
+ )
+
+ # ------------------------------------------------------------------
+ # the premise behind hiding those states
+ # ------------------------------------------------------------------
+
+ def test_dispatch_is_confirmed_on_creation_never_draft(self):
+ """Why Draft is safe to hide: action_create_dispatch confirms immediately."""
+ self._stock_up(100)
+ request = self._allocated_request()
+ request.action_allocate()
+ request.state_id = self.env["spp.vocabulary.code"].search(
+ [
+ ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:drims:request-states"),
+ ("code", "=", "allocated"),
+ ],
+ limit=1,
+ )
+ request.action_create_dispatch()
+
+ self.assertNotEqual(request.picking_ids.state, "draft")
+ self.assertIn(request.picking_ids.state, ("confirmed", "assigned"))
+
+ def test_waiting_state_is_reachable_for_a_dispatch(self):
+ """Waiting is NOT unreachable, which is why it is hidden only as a
+ *future* step rather than removed.
+
+ DRIMS allocation records per-warehouse rows against the request line; it
+ creates no Odoo reservation. Since OP#1079 it will not promise the same
+ stock to two requests, but it still cannot guarantee the stock is there
+ when the dispatch is finally created — an inventory adjustment, a
+ transfer, or any other movement can empty the shelf in between. A
+ dispatch with nothing to reserve lands in ``confirmed``, which the UI
+ labels Waiting.
+
+ The statusbar widget always renders the current value even when it is
+ excluded from ``statusbar_visible`` (``getAllItems`` keeps any value
+ equal to the current one), so such a dispatch still shows Waiting to
+ warehouse staff. Hiding it as a future step costs them nothing.
+ """
+ self._stock_up(100)
+ allocated_state = self.env["spp.vocabulary.code"].search(
+ [
+ ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:drims:request-states"),
+ ("code", "=", "allocated"),
+ ],
+ limit=1,
+ )
+ request = self._allocated_request()
+ request.action_allocate()
+ self.assertEqual(request.line_ids[0].quantity_allocated, 100)
+
+ # The shelf empties between allocating and dispatching. Allocation
+ # reserved nothing, so nothing was holding it.
+ self.env["stock.quant"]._update_available_quantity(self.product, self.warehouse.lot_stock_id, -100)
+
+ request.state_id = allocated_state
+ request.action_create_dispatch()
+
+ self.assertEqual(
+ request.picking_ids.state,
+ "confirmed",
+ "a dispatch with nothing left to reserve should sit in Waiting",
+ )
diff --git a/spp_drims/views/stock_picking_views.xml b/spp_drims/views/stock_picking_views.xml
index 8675ea30..5afc00b4 100644
--- a/spp_drims/views/stock_picking_views.xml
+++ b/spp_drims/views/stock_picking_views.xml
@@ -117,6 +117,46 @@
state != 'done' or drims_type_id
+
+
+ picking_type_code == 'incoming' or drims_type == 'request_dispatch'
+
+
+
+
+