Skip to content

Commit c2d2683

Browse files
Merge pull request #29 from OpenSPP/feat/cr-wizard-ux-improvements
feat(spp_change_request_v2): improve CR creation wizard UX
2 parents 0cbb46f + c350022 commit c2d2683

26 files changed

+424
-134
lines changed

spp_change_request_v2/__manifest__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@
6060
"assets": {
6161
"web.assets_backend": [
6262
"spp_change_request_v2/static/src/components/**/*",
63+
"spp_change_request_v2/static/src/css/cr_search_results.css",
6364
"spp_change_request_v2/static/src/js/create_change_request.js",
65+
"spp_change_request_v2/static/src/js/search_delay_field.js",
66+
"spp_change_request_v2/static/src/js/cr_search_results_field.js",
6467
"spp_change_request_v2/static/src/xml/create_change_request_template.xml",
68+
"spp_change_request_v2/static/src/xml/search_delay_field.xml",
69+
"spp_change_request_v2/static/src/xml/cr_search_results_field.xml",
6570
],
6671
},
6772
"installable": True,

spp_change_request_v2/models/change_request_conflict.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ class SPPChangeRequestConflict(models.Model):
1212
_name = "spp.change.request"
1313
_inherit = ["spp.change.request", "spp.cr.conflict.mixin"]
1414

15+
is_conflict_detection_enabled = fields.Boolean(
16+
related="request_type_id.enable_conflict_detection",
17+
)
18+
1519
# ══════════════════════════════════════════════════════════════════════════
1620
# OVERRIDE CRUD TO INTEGRATE CONFLICT DETECTION
1721
# ══════════════════════════════════════════════════════════════════════════

spp_change_request_v2/models/change_request_detail_base.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from odoo import fields, models
1+
from odoo import _, api, fields, models
2+
from odoo.exceptions import UserError
23

34

45
class SPPCRDetailBase(models.AbstractModel):
@@ -11,6 +12,14 @@ class SPPCRDetailBase(models.AbstractModel):
1112
_name = "spp.cr.detail.base"
1213
_description = "Change Request Detail Base"
1314

15+
@api.depends("change_request_id.name")
16+
def _compute_display_name(self):
17+
for rec in self:
18+
if rec.change_request_id and rec.change_request_id.name:
19+
rec.display_name = rec.change_request_id.name
20+
else:
21+
super(SPPCRDetailBase, rec)._compute_display_name()
22+
1423
change_request_id = fields.Many2one(
1524
"spp.change.request",
1625
string="Change Request",
@@ -33,6 +42,23 @@ class SPPCRDetailBase(models.AbstractModel):
3342
related="change_request_id.is_applied",
3443
)
3544

45+
def action_proceed_to_cr(self):
46+
"""Navigate to the parent Change Request form if there are proposed changes."""
47+
self.ensure_one()
48+
cr = self.change_request_id
49+
if not cr.has_proposed_changes:
50+
raise UserError(
51+
_("No proposed changes detected. Please make changes before proceeding.")
52+
)
53+
return {
54+
"type": "ir.actions.act_window",
55+
"name": cr.name,
56+
"res_model": "spp.change.request",
57+
"res_id": cr.id,
58+
"view_mode": "form",
59+
"target": "current",
60+
}
61+
3662
def action_submit_for_approval(self):
3763
"""Submit the parent CR for approval."""
3864
self.ensure_one()

spp_change_request_v2/models/res_partner.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
22
"""Extend res.partner for better registrant display in CR wizard."""
33

4-
from odoo import api, models
4+
from odoo import api, fields, models
55

66

77
class ResPartner(models.Model):
88
"""Extend res.partner to show more info when searching for registrants."""
99

1010
_inherit = "res.partner"
1111

12+
reg_id_display = fields.Char(
13+
string="Registrant ID",
14+
compute="_compute_reg_id_display",
15+
)
16+
17+
@api.depends("reg_ids.value", "reg_ids.id_type_id")
18+
def _compute_reg_id_display(self):
19+
for rec in self:
20+
if rec.reg_ids:
21+
parts = []
22+
for rid in rec.reg_ids:
23+
if rid.value:
24+
label = rid.id_type_as_str or "ID"
25+
parts.append(f"{label} ({rid.value})")
26+
rec.reg_id_display = ", ".join(parts) if parts else ""
27+
else:
28+
rec.reg_id_display = ""
29+
1230
def _compute_display_name(self):
1331
"""Add registrant ID to display name when in CR wizard context."""
1432
super()._compute_display_name()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* Force the search results widget to take full width in the form */
2+
.o_field_cr_search_results,
3+
.o_field_widget:has(.o_field_cr_search_results) {
4+
width: 100% !important;
5+
max-width: 100% !important;
6+
display: block !important;
7+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/** @odoo-module **/
2+
3+
import {Component, onMounted, onPatched, useRef} from "@odoo/owl";
4+
import {_t} from "@web/core/l10n/translation";
5+
import {registry} from "@web/core/registry";
6+
import {standardFieldProps} from "@web/views/fields/standard_field_props";
7+
8+
/**
9+
* Custom widget that renders HTML search results and handles row clicks.
10+
* When a row with class "o_cr_search_result" is clicked, it writes the
11+
* partner ID to the _selected_partner_id bridge field, which triggers
12+
* a server-side onchange to set registrant_id.
13+
*/
14+
export class CrSearchResultsField extends Component {
15+
static template = "spp_change_request_v2.CrSearchResultsField";
16+
static props = {...standardFieldProps};
17+
18+
setup() {
19+
this.containerRef = useRef("container");
20+
onMounted(() => this._attachClickHandler());
21+
onPatched(() => this._attachClickHandler());
22+
}
23+
24+
get htmlContent() {
25+
return this.props.record.data[this.props.name] || "";
26+
}
27+
28+
_attachClickHandler() {
29+
const el = this.containerRef.el;
30+
if (!el) return;
31+
// Row selection
32+
el.querySelectorAll(".o_cr_search_result").forEach((row) => {
33+
row.onclick = (ev) => {
34+
ev.preventDefault();
35+
ev.stopPropagation();
36+
const partnerId = parseInt(row.dataset.partnerId);
37+
if (partnerId) {
38+
this.props.record.update({_selected_partner_id: partnerId});
39+
}
40+
};
41+
});
42+
// Pagination
43+
el.querySelectorAll(".o_cr_page_prev, .o_cr_page_next").forEach((link) => {
44+
link.onclick = (ev) => {
45+
ev.preventDefault();
46+
ev.stopPropagation();
47+
const page = parseInt(link.dataset.page);
48+
if (!isNaN(page) && page >= 0) {
49+
this.props.record.update({_search_page: page});
50+
}
51+
};
52+
});
53+
}
54+
}
55+
56+
export const crSearchResultsField = {
57+
component: CrSearchResultsField,
58+
displayName: _t("CR Search Results"),
59+
supportedTypes: ["html"],
60+
};
61+
62+
registry.category("fields").add("cr_search_results", crSearchResultsField);

spp_change_request_v2/static/src/js/create_change_request.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,6 @@ patch(FormController.prototype, {
8484
if (this.props.resModel === "spp.change.request") {
8585
this.hideFormCreateButton = true;
8686
}
87+
// Row click handling for CR create wizard is now in cr_search_results_field.js
8788
},
8889
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/** @odoo-module **/
2+
3+
import {Component, useEffect, useRef, onWillUnmount} from "@odoo/owl";
4+
import {_t} from "@web/core/l10n/translation";
5+
import {useDebounced} from "@web/core/utils/timing";
6+
import {registry} from "@web/core/registry";
7+
import {standardFieldProps} from "@web/views/fields/standard_field_props";
8+
9+
/**
10+
* Char field that triggers onchange after a typing delay (500ms).
11+
* Used for search fields where we want live results without waiting for blur.
12+
*/
13+
export class SearchDelayField extends Component {
14+
static template = "spp_change_request_v2.SearchDelayField";
15+
static props = {
16+
...standardFieldProps,
17+
placeholder: {type: String, optional: true},
18+
};
19+
20+
setup() {
21+
this.inputRef = useRef("input");
22+
23+
this.debouncedCommit = useDebounced((value) => {
24+
this.props.record.update({[this.props.name]: value});
25+
}, 500);
26+
27+
// Keep input in sync when record updates externally (e.g. onchange clears it)
28+
useEffect(
29+
() => {
30+
const el = this.inputRef.el;
31+
if (el) {
32+
const recordValue = this.props.record.data[this.props.name] || "";
33+
if (el !== document.activeElement || !recordValue) {
34+
el.value = recordValue;
35+
}
36+
}
37+
},
38+
() => [this.props.record.data[this.props.name]]
39+
);
40+
}
41+
42+
get value() {
43+
return this.props.record.data[this.props.name] || "";
44+
}
45+
46+
onInput(ev) {
47+
this.debouncedCommit(ev.target.value);
48+
}
49+
}
50+
51+
export const searchDelayField = {
52+
component: SearchDelayField,
53+
displayName: _t("Search with Delay"),
54+
supportedTypes: ["char"],
55+
extractProps: ({placeholder}) => ({placeholder}),
56+
};
57+
58+
registry.category("fields").add("search_delay", searchDelayField);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="spp_change_request_v2.CrSearchResultsField">
5+
<div t-ref="container" class="o_field_cr_search_results" style="width:100%" t-out="htmlContent"/>
6+
</t>
7+
8+
</templates>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="spp_change_request_v2.SearchDelayField">
5+
<input t-ref="input"
6+
type="text"
7+
class="o_input"
8+
t-att-value="value"
9+
t-att-placeholder="props.placeholder || ''"
10+
t-att-readonly="props.readonly ? 'readonly' : undefined"
11+
t-on-input="onInput"/>
12+
</t>
13+
14+
</templates>

0 commit comments

Comments
 (0)