Skip to content

fix(drims): DRIMS issues and improvements (#651)#224

Open
emjay0921 wants to merge 27 commits into
19.0from
fix/651-drims-improvements
Open

fix(drims): DRIMS issues and improvements (#651)#224
emjay0921 wants to merge 27 commits into
19.0from
fix/651-drims-improvements

Conversation

@emjay0921
Copy link
Copy Markdown
Contributor

Why is this change needed?

Umbrella fix for the DRIMS issues + improvements tracked in OP#651. Each numbered item from the original report was split into its own child ticket; this PR bundles the implemented fixes for the closed children (#959–#1038).

How was the change implemented?

Changes are in spp_drims (plus a small reusable positive-integer-required field widget in spp_base_common). Highlights:

  • Warehouse: tier + GIS location fields on the warehouse form (#959); GPS map given its own full-width section (#1038)
  • Donations: status bar is read-only / non-clickable (#960); received-quantity editing locked to the received state (#962)
  • Inspection wizard: redesigned with inline editing + line splits (#963); non-accept / damaged dispositions excluded from stocking (#1030); inspection wizard reliably opens (#1026); Stock button hidden when every line is non-accept
  • Requests: editable Priority (#965), reject-reason wizard (#966), beneficiaries-field UX (#967), post-approval lanes Ready for Allocation / Ready for Dispatch (#969)
  • Dispatch: auto-generate from approved requests, New button hidden (#968); redundant Print/Return buttons hidden (#970)
  • Allocation: empty allocation blocked, request kept at Ready for Allocation (#1032); multiple partial dispatches per request until fully dispatched (#1033)
  • Menus: module menu icons (#971)

New unit tests

Added/expanded tests across spp_drims/tests/ (donation, request, inspection wizard, allocation preview) covering inspection splits, non-accept exclusion, allocation/dispatch flows, the reject wizard, and state transitions.

Unit tests executed by the author

  • spp_drims suite: 247 tests, 0 failed / 0 errors
  • Pre-commit clean on all changed files (prettier, ruff, ruff format, pylint, check-xml, eslint, OpenSPP checks)

How to test manually

Install spp_drims, load the DRIMS demo, then:

  • Walk a donation through announce → receive → inspect (try line splits + damaged items) → stock
  • Walk a request through approve → allocate → dispatch, including partial dispatches

Per-item QA guides are on the child tickets under OP#651.

Related links

🤖 Generated with Claude Code

emjay0921 added 24 commits May 18, 2026 11:59
- #967: require beneficiary_count via the view (asterisk + pre-save check)
- #961: auto-create stock.lot from donation_line.lot_number on action_stock,
  with friendly UserErrors for missing-lot + serial-qty>1; expose Lot/Batch
  + Expiry Date columns by default on donation items
- #964: fall back to quantity_pledged when quantity_received is 0;
  relax wizard equality check to splits-only so a single line is treated
  as the user reporting the final received quantity
- #968: hide New + Delete on the DRIMS Dispatches list via a primary-mode
  inherited view pinned to the action
- #1026: wire the donation form's Inspect button to action_open_inspection_wizard
  instead of the raw action_inspect state-flip
The form view used widget="badge" which rendered priority_id as a
read-only colored pill with no input control. Drop the badge widget
(and the decoration-* attributes that only render under badge) so the
field becomes a normal Many2one dropdown. List + kanban views keep
widget="badge" for compact display.
…t (#966)

Reject button on the request form called action_reject directly with no
parameter, leaving rejection_reason empty — the audit trail lost the
rationale. Adds a single-record reject wizard with a required reason
field; the form button now opens the wizard, which writes the reason
and invokes action_reject. A red banner above the title surfaces the
reason on rejected requests.

- New spp.drims.request.reject.wizard TransientModel + form view
- New action_open_reject_wizard on spp.drims.request (guards approval
  state, raises on non-pending requests)
- Reject button wired to the wizard; confirm dialog removed since the
  wizard handles confirmation
- ACL for approver + manager roles
- 4 new tests cover wizard action, only-pending guard, happy path, and
  blank-reason rejection
OP#967 — Estimated Beneficiaries Reached field now visually marks itself
as required when value is 0.

  Odoo 19's _checkValidity (record.js:430) explicitly skips
  boolean/float/integer/monetary types from required-empty validation,
  so a required Integer with value 0 never gets the o_field_invalid
  class or the asterisk on its label. The model-side button_validate
  check rejects 0 on save but the form is silent until then.

  Adds a reusable 'integer_positive_required' field widget to
  spp_base_common. It plugs into Odoo's visual-feedback pipeline via
  the field config's isValid hook (fieldVisualFeedback calls
  field.isValid BEFORE record.isFieldInvalid). Returns false when
  value <= 0 AND the required modifier evaluates true, which adds
  o_field_invalid → pink highlight + asterisk on the label.

  Any module can opt in: widget="integer_positive_required".
  spp_drims uses it on beneficiary_count, with the same
  required="drims_type == 'request_dispatch'" modifier as
  beneficiary_area_id.

OP#968 — Form-toolbar New button now hidden when opening a dispatch
picking from the DRIMS Dispatches action.

  The earlier round only set create="0" on the list view; Odoo 19's
  form view ignores list-level create attributes for the toolbar
  button. Action-context 'create': False is also unreliable in Odoo
  19.

  spp_base_common already exposes a hideFormCreateButton flag via its
  custom_list_create_template.xml — set it on a FormController
  instance to skip the New button. New patch in spp_drims sets the
  flag when the form is opened for stock.picking with the dispatch
  action's context key (default_drims_type=request_dispatch). The
  standard stock.picking form on Inventory > Operations is
  unaffected because the discriminator is the context, not the
  model.

Also adds spp_base_common as an explicit dep of spp_drims (was
transitive via other modules).
…MS pickings (#970)

The dispatch form already exposes a DRIMS-specific Print Waybill (via
spp_drims.action_report_drims_waybill) and Create Return (via the DRIMS
return wizard) in the header. The standard stock module's Print +
Return buttons stayed visible alongside them, producing visible
duplicates that confused users.

Added three xpath rules to spp_drims's view_picking_form_drims that
extend the existing 'invisible' clauses on the standard buttons with
`or drims_type_id`, so they hide on any picking carrying a DRIMS
type. Standard buttons stay untouched on non-DRIMS pickings.

The action-button names (e.g. %(action_report_delivery)d) get resolved
to numeric IDs at view-parse time and xpath cannot match them by
name. Used data-hotkey + contains(@invisible, 'done') as the
discriminator instead.
…se error (#970)

Odoo's view-loader runs the %(xmlid)d resolution regex over the
arch_db string before storing it, and the regex does not skip XML
comments. The previous comment in view_picking_form_drims contained
the literal %(action_report_delivery)d sequence, which Odoo
interpreted as a real reference and tried to resolve as
spp_drims.action_report_delivery — that xmlid lives in the stock
module, not spp_drims, so module upgrade failed with:

  ValueError: External ID not found in the system: spp_drims.action_report_delivery

Rewrote the comment to describe the sigil in prose instead of
including the literal character sequence.
… for Dispatch (#969)

Users couldn't sort approved requests by what's needed next — there
was no visual separation between requests waiting for allocation vs.
those waiting for dispatch. The states already existed in the
state-machine (approved → allocated → dispatched) but were exposed
under the same generic labels as the underlying codes.

Changes:

- vocabulary_codes.xml: rename display strings on two existing codes —
  'approved' → 'Ready for Allocation', 'allocated' → 'Ready for
  Dispatch'. Wrapped in <data noupdate="0"> so the rename
  propagates on subsequent module upgrades (the parent element has
  noupdate=1).

- request_views.xml list view: new Fulfillment State badge column
  showing the relabeled state_id, with decoration-info for
  pre-allocation states, decoration-success for post-allocation,
  decoration-danger for rejected/cancelled. Optional='show' so it
  appears by default.

- request_views.xml search view: two new named filters —
  'Ready for Allocation' (approval_state='approved' AND state not
  past allocated) and 'Ready for Dispatch' (state='allocated').
  Also added the fulfillment state to the searchpanel (with counters)
  and a 'Fulfillment State' group-by.
…only (#962)

Donation lines' 'Received' quantity was editable during BOTH 'announced'
and 'received' states. That allowed users to enter a received quantity
at pledge/announcement time, which then silently flowed through
action_mark_received (the auto-copy only fills zero values) and drove
the stock picking with pre-entered values the user hadn't necessarily
confirmed.

Tightens the view-level readonly to permit edits only in the 'received'
state. The auto-copy of pledged → received during the announced→received
transition is unaffected because that path writes the line via Python,
not via the field.

Out of scope: edits in the 'received' state after the stock picking
exists still don't update the picking — that picking-sync question
needs a separate decision and should be tracked as a follow-up.
…arehouse form (#959)

The DRIMS warehouse form was missing the documented tier field
(docs/DATA_MODEL.md:185-195 specifies Selection central/regional/mobile)
and never actually showed the gis_location point even though the model
field existed.

- models/stock_warehouse.py: new tier Selection(central/regional/mobile),
  indexed for filter performance.

- views/stock_warehouse_views.xml form: replace the existing
  position='inside' xpath on the standard notebook with a new un-gated
  notebook anchored before it. The standard <notebook> on
  stock.view_warehouse is groups='stock.group_adv_location,
  stock.group_stock_multi_warehouses', so attaching the DRIMS
  Configuration page inside it hides the entire page for admins who
  don't hold those advanced inventory roles — which was exactly what
  was happening here (existing capacity / area / emergency contact
  fields were also invisible).

  Page layout: Active Response at the top, full-width (field carries
  colspan=2 to span both sub-columns of the group; colspan on the group
  itself would be a no-op at page level). Below that, two-column row of
  Disaster Response Configuration (incl. tier + drims_capacity +
  area_id + gis_location with the geo_point widget) and Emergency
  Contact.

- views/stock_warehouse_views.xml list: surfaces tier as an optional
  badge column.

- views/stock_warehouse_views.xml search: tier search field, three
  preset filters (Central / Regional / Mobile), and a Tier group-by.

- views/stock_warehouse_views.xml GIS: new <gis> view for
  stock.warehouse — the geo_point widget on the form requires one
  (without it the form errors with 'No GIS view defined for the model
  stock.warehouse'). Plus the matching spp.gis.data.layer (warehouse
  location point) and spp.gis.raster.layer (OSM tile base) records —
  without the raster layer the GIS view errors with 'No raster layer
  for view stock.warehouse.gis.drims'.

228 spp_drims tests pass.
…e (#960)

The statusbar widget was configured with options="{'clickable': '1'}"
which let users click a state badge to fire the corresponding write to
state_id directly. That bypasses the header buttons (Mark Received /
Inspect Items / Stock Items / Reject / Cancel) which run validation,
trigger side-effects (auto-create stock picking on receive, open the
inspection wizard, etc.), and enforce the state-machine transitions.

Dropping the clickable option turns the statusbar into a read-only
display indicator and forces state changes through the proper actions.
…Allocation (#1032)

When a user allocated an approved request against a warehouse with
zero available stock, _allocate_stock_fifo was a no-op (0 across all
lines) but action_allocate still advanced the request to allocated
(Ready for Dispatch). The Create Dispatch button then became
available against a request with nothing allocated, allowing an
empty / invalid stock picking to be created.

Same path through the allocation-preview wizard: action_confirm_allocation
would write 0 across all request lines and still advance state.

Both paths now raise a UserError with the message:
  "No stock available in the selected warehouse. Please ensure the
   source warehouse has sufficient items before allocating."
when the total allocation is 0. The request state stays at approved
(Ready for Allocation) so the user can pick a different warehouse.

Partial allocation (some lines have stock, others don't) is still
allowed — the check is total <= 0.

Updated test_allocate_with_warehouse to seed stock first (otherwise it
would now fail). Added test_allocate_blocked_when_warehouse_has_no_stock
and test_confirm_blocked_when_zero_stock for the negative cases. 230
spp_drims tests pass.
…s (#963)

Replace the read-only-list + per-item popup flow with a single inline-editable list.
Condition and Action are picked directly on each row; "+ Add split" creates an
indented child row for products with mixed conditions. The parent row of a split
carries no decision — its qty mirrors the running sum of its children — and the
old InspectionItemWizard / popup view are removed entirely.
Each Create Dispatch call now creates a picking covering only the not-yet-dispatched
allocated balance (quantity_allocated - quantity_dispatched) per line. The request
stays in Ready for Dispatch until every line reaches quantity_dispatched >=
quantity_requested, at which point it advances to Dispatched.

The Allocate Stock button stays available while the request is allocated so the
operator can top up the allocation between dispatches as more stock arrives.
…1030)

action_stock now cancels moves whose donation line carries a disposition of
return / dispose / quarantine before validating the receipt picking, so
damaged or returnable units never enter usable inventory. A sticky warning
notification lists the excluded units. If every move on a picking is
excluded, the picking is cancelled outright instead of validated empty.

stock.move._prepare_merge_moves_distinct_fields now keeps drims_donation_line_id
and drims_request_line_id distinct so Odoo's automatic move-merge does not
collapse multi-line donations of the same product into one move (which would
have lost the per-disposition link).

Also fixes a related grouping bug in the inspection wizard's confirm step:
two separate donation lines for the same product were being treated as
splits of each other. Confirm now groups wizard lines by donation_line_id,
so independent donation lines aren't subject to the split-totals safety net.
QA round 2 feedback addressed:
- Custom OWL Float widget (qty_split_progress) renders parent rows as
  "X / Y" inside a single Qty column — X green when matched, red otherwise —
  driven by a live read of children from the wizard's One2many. Child rows
  keep the standard editable float; a useEffect mirrors the running sum
  onto the parent record via record.update so the parent's display
  reactively refreshes on every child qty edit.
- Condition/Action become read-only on split parents; the dropdown caret
  + "Select" affordance are now always visible on editable cells and
  hidden on read-only split parents.
- Children quantities are clamped so the running total never exceeds the
  donation line's received quantity.
- _order on the wizard line model groups parent + children together with
  parent first; child rows get an indent + "↳" arrow prefix.
- Expected column value hidden on child rows (parent already shows it).
- "+ Add split" hidden when the running split matches expected (uses
  CSS :has() against the widget's own .text-success render so the gate
  reacts live without depending on Odoo's editable-list cross-record
  reactivity).
- Group/field colspan tweaks so the table and notes fill the form width.

All ListRenderer / widget / view changes are scoped to the inspection
wizard list via the o_drims_inspection_list class.
- "+ Add split" now creates the new split child with qty=0 instead of
  pre-filling the remaining capacity. The operator has to enter the split
  amount explicitly, which avoids the "added a split but forgot to fill it"
  footgun.
- "+ Add split" is hidden whenever the parent already has a 0-qty child
  pending, via a dynamically-applied o_split_parent_has_zero row class.
  The operator must fill the new split before opening another.
- Condition + Action dropdowns now use options="{'no_create': True,
  'no_open': True}" so the external-link arrow is suppressed (no extra
  affordance to confuse the operator).
…30 r2)

The round 1 implementation relied on per-move drims_donation_line_id to
decide which moves to cancel. That fails whenever Odoo's standard move-merge
logic collapses multiple donation lines for the same product into a single
move — the surviving move references only one line, so the others'
dispositions are invisible.

action_stock now computes the accept vs non-accept received quantities per
product directly from the donation lines, then walks the picking's moves
for each affected product, keeping moves up to the per-product accept total
and cancelling/reducing the excess. Works whether the moves got merged or
not.

Adds a regression test covering the bug screenshot scenario: two donation
lines for the same product with partial receive (200 + 300) and mixed
Accept/Return dispositions — only the 200 accepted units should land in
the warehouse.
Adds two stored fields on the inspection wizard — `can_confirm` (Boolean)
and `confirm_message` (Text) — and wires the qty widget's useEffect to
recompute them from live line data on every row edit.

Three gating conditions are evaluated together and the failed-reason
messages stack, so the operator sees the union of everything still
blocking Confirm:
- C1: Parent rows with splits must have their children sum to the
  expected quantity.
- C2: Every non-parent line must have both Condition and Action set.
- C3: No split child may have qty 0.

When can_confirm is False the form swaps the primary "Confirm Inspection"
button for a disabled-looking placeholder and renders the
confirm_message in a yellow warning alert above the items list. As soon
as the conditions clear, the message hides and the active button comes
back. Server-side validation in action_confirm_inspection stays in place
as a final safety net.
Round 1 of the partial-dispatch work let the operator click Allocate
Stock against the same warehouse repeatedly, ratcheting up
quantity_allocated until it matched quantity_requested while the
physical stock hadn't moved. Cause: _get_available_quantity returned the
raw stock.quant on-hand and ignored other DRIMS request lines that had
already been allocated against the same warehouse but not yet
dispatched.

action_confirm_allocation only consumes stock at dispatch time (it sets
quantity_allocated on the request line but doesn't move quants), so the
wizard needs to deduct those in-flight allocations itself.

Net available now equals:
  sum(quant.quantity - quant.reserved_quantity)
  - sum(max(0, line.quantity_allocated - line.quantity_dispatched))
across all request lines pointing to this warehouse for this product.
Floored at 0. Re-opening the wizard after a partial allocation now
correctly shows available=0 / shortfall=remaining, and confirming an
empty allocation raises the existing "No stock available" UserError.

Adds a regression test reproducing the QA repro: 1000 physical units,
request for 5000, first allocation consumes the 1000 → second wizard
open must show available=0, to_allocate=0, and confirming must raise
without bumping quantity_allocated.
When validating a dispatch picking without the required beneficiary fields,
the error toast now spells out where to fill them in ("under the DRIMS
tab") so the operator doesn't have to hunt for the field. Same hint added
to both the beneficiary_count and beneficiary_area_id guards.
The Stock Items button on a donation returns a display_notification action
when non-accept dispositions are excluded. Without a `next` step the
Odoo client renders the toast but doesn't re-read the underlying record,
so the operator has to manually refresh to see the state flip to
"Stocked" and the picking smart-button update.

Add `next: ir.actions.act_window_close` to the notification params (same
pattern the allocation preview wizard already uses), which closes the
action chain and lets the donation form re-read once the toast is shown.
… (#963)

When inspectors mark every line as Return / Dispose / Quarantine the donation
has nothing left to stock, but the **Stock Items** button still showed in the
header — clicking it just cancelled every move and transitioned to ``stocked``
with no inventory change, which read as a bug to reviewers.

Compute a non-stored Boolean ``has_acceptable_items`` on ``spp.drims.donation``
that returns True when at least one line has ``quantity_received > 0`` and a
disposition outside the ``("return", "dispose", "quarantine")`` set (factored
out as the module-level constant ``NON_ACCEPT_DISPOSITIONS`` and reused by the
existing ``_exclude_non_accept_moves``). A line with no disposition yet is
treated as acceptable so Stock stays available mid-inspection.

In the donation form:
- ``Stock Items`` is now ``invisible="state != 'inspected' or not has_acceptable_items"``.
- A new info alert above the sheet ("Nothing to Stock. … Click Reject to close
  this donation.") shows on the inverse condition so the user knows why Stock
  disappeared and what action to take instead.
- ``Reject`` is unchanged — visible whenever ``state == 'inspected'``.

Two unit tests lock the field's behaviour (all non-accept → False, one accept
mixed with non-accept → True).
…ments

# Conflicts:
#	spp_base_common/__manifest__.py
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several enhancements to the DRIMS module, including a new inline-editable single-screen flow for the donation inspection wizard, support for partial dispatches, a single-record request rejection wizard to capture rejection reasons, and warehouse tiering with GIS mapping support. It also prevents empty stock allocations and excludes non-acceptable items from entering usable inventory. The reviewer feedback highlights critical improvements: guarding against undefined fields in the new integer widget to prevent runtime crashes, querying remaining split lines before unlinking to avoid Odoo cache issues, removing eager quantity clamping in the onchange method to improve user experience, and using robust XML ID-based XPath expressions instead of fragile hotkey attributes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +47 to +50
const required = evaluateBooleanExpr(
fieldInfo.required,
record.evalContextWithVirtualIds
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If fieldInfo.required is not defined on the field (which is common for optional fields), passing undefined to evaluateBooleanExpr will cause a runtime JavaScript error and crash the form view. We should guard this evaluation by checking if fieldInfo.required is present.

        const required = fieldInfo.required
            ? evaluateBooleanExpr(fieldInfo.required, record.evalContextWithVirtualIds)
            : false;

Comment on lines +390 to +414
def action_remove_split(self):
"""Remove a split child line and refresh the wizard."""
self.ensure_one()
if not self.is_split:
raise UserError(_("Only split lines can be removed this way."))
wizard_id = self.wizard_id.id
root_line = self.parent_line_id
self.unlink()

remaining_children = root_line.wizard_id.line_ids.filtered(lambda line: line.parent_line_id == root_line)
if not remaining_children:
# Reset parent back to the full expected quantity and clear the
# split flag so the row reverts to a normal (editable) row.
root_line.quantity = root_line.quantity_expected
root_line.has_splits = False
else:
self._refresh_parent_quantity(root_line)

if self.quantity < 0:
raise UserError(_("Quantity cannot be negative."))

if self.remaining_qty <= 0:
raise UserError(_("No remaining quantity to split. Adjust the quantity first."))

# Update current line
self.inspection_line_id.write(
{
"quantity": self.quantity,
"condition_id": self.condition_id.id,
"disposition_id": self.disposition_id.id,
"notes": self.notes,
"is_inspected": True,
}
)

# Create new line for remaining quantity
new_line = self.env["spp.drims.inspection.wizard.line"].create(
{
"wizard_id": self.wizard_id.id,
"donation_line_id": self.inspection_line_id.donation_line_id.id,
"product_id": self.product_id.id,
"uom_id": self.uom_id.id,
"quantity_expected": self.quantity_expected,
"quantity": self.remaining_qty,
"is_inspected": False, # New split needs to be inspected
}
)
return {
"type": "ir.actions.act_window",
"res_model": "spp.drims.inspection.wizard",
"res_id": wizard_id,
"view_mode": "form",
"target": "new",
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling self.unlink() before querying root_line.wizard_id.line_ids can lead to a MissingError or cache inconsistency in Odoo. If the One2many cache still contains the unlinked record, Odoo may attempt to read or flush it, causing a crash. It is a best practice to identify the remaining children and perform any calculations before unlinking the record.

    def action_remove_split(self):
        """Remove a split child line and refresh the wizard."""
        self.ensure_one()
        if not self.is_split:
            raise UserError(_("Only split lines can be removed this way."))
        wizard_id = self.wizard_id.id
        root_line = self.parent_line_id

        # Identify remaining children before unlinking to avoid MissingError/cache issues
        remaining_children = root_line.wizard_id.line_ids.filtered(
            lambda line: line.parent_line_id == root_line and line != self
        )

        self.unlink()

        if not remaining_children:
            # Reset parent back to the full expected quantity and clear the
            # split flag so the row reverts to a normal (editable) row.
            root_line.quantity = root_line.quantity_expected
            root_line.has_splits = False
        else:
            root_line.quantity = sum(remaining_children.mapped("quantity"))

        return {
            "type": "ir.actions.act_window",
            "res_model": "spp.drims.inspection.wizard",
            "res_id": wizard_id,
            "view_mode": "form",
            "target": "new",
        }

Comment on lines +291 to +311
@api.onchange("quantity")
def _onchange_quantity_update_parent(self):
"""Keep the parent row qty in sync with the running sum of its children.

Also clamps the current child's quantity if the new value would push
the running total over the parent's ``quantity_expected``: the qty is
snapped to ``expected - sum(other_children)`` so the split totals
never exceed the donation line's received quantity.
"""
if not self.parent_line_id:
return
siblings = self.wizard_id.line_ids.filtered(
lambda line: line.parent_line_id == self.parent_line_id and line != self
)
others_sum = sum(siblings.mapped("quantity"))
expected = self.parent_line_id.quantity_expected
max_allowed = expected - others_sum
if self.quantity and self.quantity > max_allowed:
self.quantity = max(0.0, max_allowed)
total = others_sum + (self.quantity or 0.0)
self.parent_line_id.quantity = total
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Eagerly clamping the quantity in the @api.onchange method creates a frustrating user experience. If a user wants to adjust split quantities (e.g., changing splits of 500/500 to 400/600), they will be blocked from increasing any line first because it will be immediately clamped back to the remaining allowed amount. The user would be forced to always decrease a line before increasing another. Since the wizard already has robust validation on confirmation (both in JS via can_confirm and in Python via action_confirm_inspection), we should remove the clamping logic and allow the user to freely edit the quantities during the drafting phase.

Suggested change
@api.onchange("quantity")
def _onchange_quantity_update_parent(self):
"""Keep the parent row qty in sync with the running sum of its children.
Also clamps the current child's quantity if the new value would push
the running total over the parent's ``quantity_expected``: the qty is
snapped to ``expected - sum(other_children)`` so the split totals
never exceed the donation line's received quantity.
"""
if not self.parent_line_id:
return
siblings = self.wizard_id.line_ids.filtered(
lambda line: line.parent_line_id == self.parent_line_id and line != self
)
others_sum = sum(siblings.mapped("quantity"))
expected = self.parent_line_id.quantity_expected
max_allowed = expected - others_sum
if self.quantity and self.quantity > max_allowed:
self.quantity = max(0.0, max_allowed)
total = others_sum + (self.quantity or 0.0)
self.parent_line_id.quantity = total
@api.onchange("quantity")
def _onchange_quantity_update_parent(self):
"""Keep the parent row qty in sync with the running sum of its children."""
if not self.parent_line_id:
return
siblings = self.wizard_id.line_ids.filtered(
lambda line: line.parent_line_id == self.parent_line_id and line != self
)
others_sum = sum(siblings.mapped("quantity"))
total = others_sum + (self.quantity or 0.0)
self.parent_line_id.quantity = total

Comment on lines +103 to +108
<xpath
expr="//header/button[@data-hotkey='o' and contains(@invisible, 'done')]"
position="attributes"
>
<attribute name="invisible">state != 'done' or drims_type_id</attribute>
</xpath>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Relying on data-hotkey and contains(@invisible, ...) in XPath expressions is fragile and prone to breaking if Odoo core updates the hotkeys or modifier expressions, or if other installed modules modify them.

In Odoo, the view inheritance processor resolves XML ID sigils (like %(xml_id)d) dynamically during view compilation. This means you can safely and robustly target action buttons directly by their XML ID in your XPath expressions (e.g., name="%(stock.act_stock_return_picking)d").

Suggested change
<xpath
expr="//header/button[@data-hotkey='o' and contains(@invisible, 'done')]"
position="attributes"
>
<attribute name="invisible">state != 'done' or drims_type_id</attribute>
</xpath>
<xpath
expr="//header/button[@name='%(stock.act_stock_return_picking)d']"
position="attributes"
>
<attribute name="invisible">state != 'done' or drims_type_id</attribute>
</xpath>

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 8, 2026

Codecov Report

❌ Patch coverage is 94.76440% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.12%. Comparing base (e9dad7e) to head (cfd8dee).

Files with missing lines Patch % Lines
spp_drims/wizard/inspection_wizard.py 90.14% 7 Missing ⚠️
spp_drims/models/donation.py 95.94% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #224      +/-   ##
==========================================
+ Coverage   73.06%   73.12%   +0.05%     
==========================================
  Files        1069     1070       +1     
  Lines       62120    62216      +96     
==========================================
+ Hits        45390    45495     +105     
+ Misses      16730    16721       -9     
Flag Coverage Δ
fastapi 86.92% <ø> (ø)
spp_alerts 96.77% <ø> (ø)
spp_analytics 93.13% <ø> (ø)
spp_api_v2 80.33% <ø> (ø)
spp_api_v2_change_request 66.85% <ø> (ø)
spp_api_v2_cycles 71.12% <ø> (ø)
spp_api_v2_data 64.41% <ø> (ø)
spp_api_v2_entitlements 70.19% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products 66.27% <ø> (ø)
spp_api_v2_service_points 70.94% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_change_request_v2 75.46% <ø> (ø)
spp_drims 80.82% <94.76%> (+1.27%) ⬆️
spp_drims_sl 0.00% <ø> (ø)
spp_gis_indicators 91.07% <ø> (ø)
spp_hazard 99.59% <ø> (ø)
spp_programs 64.84% <ø> (ø)
spp_registry 86.83% <ø> (ø)
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_base_common/__manifest__.py 0.00% <ø> (ø)
spp_drims/__manifest__.py 0.00% <ø> (ø)
spp_drims/models/request.py 89.64% <100.00%> (+2.97%) ⬆️
spp_drims/models/stock_move.py 100.00% <100.00%> (ø)
spp_drims/models/stock_picking.py 76.78% <ø> (ø)
spp_drims/models/stock_warehouse.py 70.58% <100.00%> (+0.58%) ⬆️
spp_drims/wizard/__init__.py 100.00% <100.00%> (ø)
spp_drims/wizard/allocation_preview_wizard.py 82.30% <100.00%> (+0.85%) ⬆️
spp_drims/wizard/request_reject_wizard.py 100.00% <100.00%> (ø)
spp_drims/models/donation.py 96.91% <95.94%> (-0.52%) ⬇️
... and 1 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@emjay0921
Copy link
Copy Markdown
Contributor Author

@gonzalesedwin1123 ready for your review.

CI status

All DRIMS-specific checks are greenbuild + every module test (...) job, ruff / pylint / eslint / prettier / check-xml, CodeQL, Semgrep, gitleaks, and the dependency/secret scans.

The single red check is pre-commit, and it is not introduced by this PR. The only failing hook is the OCA README generator (oca-gen-addon-readme), which regenerates README.rst / static/description/index.html for 6 modules unrelated to DRIMS (spp_api_v2_gis, spp_change_request_v2, spp_drims_sl, spp_gis_indicators, spp_hazard, spp_security). This branch has those READMEs byte-identical to 19.0, and 19.0's own HEAD is already red on the same hook — so it's a pre-existing repo-wide README staleness to be fixed separately, not part of this DRIMS change. (Kept out of this PR to preserve its scope.)

Scope

DRIMS fixes for OP#651 (children #959–#1038): warehouse tier + GIS location/map, read-only donation status bar, inspection wizard redesign with line splits + non-accept/damaged exclusion, request Priority / reject-reason / beneficiaries UX + post-approval lanes, dispatch auto-generate + button cleanup, allocation guard + multiple partial dispatches, and module menu icons.

Local validation

  • spp_drims suite: 247 tests, 0 failed / 0 errors
  • Pre-commit clean on all changed files (the DRIMS + spp_base_common changes)

The oca-gen-addon-readme hook was failing (also red on 19.0) because the
committed README.rst / static/description/index.html for several modules
were stale vs the generator. Regenerated via the CI toolchain so the
hook passes.
@emjay0921
Copy link
Copy Markdown
Contributor Author

@gonzalesedwin1123 — ready for your review.

CI status

  • pre-commit: green — regenerated the stale module READMEs in this branch (06bebc4a), so the oca-gen-addon-readme hook now passes.
  • build + all module test jobs, lint (ruff/pylint/eslint/prettier/xml), and security scans (CodeQL, Semgrep, gitleaks, dependency/secret): green.
  • The only red is test (spp_api_v2_gis), which failed at the Initialize containers step with a Docker Hub registry timeout (registry-1.docker.io … context deadline exceeded) — a transient CI infra flake, not a code issue. It passed on the previous run, and the only change since was the README regeneration; a re-run clears it.

Scope (OP#651 children #959–#1038)

Warehouse tier + GIS location/map, read-only donation status bar, inspection wizard redesign with line splits + non-accept/damaged exclusion, request Priority / reject-reason / beneficiaries UX + post-approval lanes, dispatch auto-generate + button cleanup, allocation guard + multiple partial dispatches, module menu icons.

Local validation

spp_drims suite: 247 tests, 0 failed / 0 errors; pre-commit clean.

emjay0921 added 2 commits June 8, 2026 16:39
…ew (#651)

- integer_positive_required: guard fieldInfo.required before
  evaluateBooleanExpr — an undefined required modifier was passed straight
  to the evaluator and crashed the form view.
- action_remove_split: compute the remaining children before unlink() so
  the One2many cache never reads the unlinked record (MissingError).
- _onchange_quantity_update_parent: drop the eager quantity clamping so the
  operator can freely rebalance split quantities while drafting; an
  over/under-split total is still rejected at confirm time.
- stock_picking_views: target the done-state delivery Print and the Return
  header buttons by action XML ID instead of the fragile data-hotkey /
  contains(@invisible) xpath.
The first '+ Add split' on an inspection line now divides it into two empty
child rows (a split produces at least two pieces); each later '+ Add split'
appends one more. All new rows start at qty 0 so the operator must enter each
amount explicitly. Update the wizard split tests to the two-children-first
behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant