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
5 changes: 5 additions & 0 deletions spp_change_request_v2/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@
"assets": {
"web.assets_backend": [
"spp_change_request_v2/static/src/components/**/*",
"spp_change_request_v2/static/src/css/cr_search_results.css",
"spp_change_request_v2/static/src/js/create_change_request.js",
"spp_change_request_v2/static/src/js/search_delay_field.js",
"spp_change_request_v2/static/src/js/cr_search_results_field.js",
"spp_change_request_v2/static/src/xml/create_change_request_template.xml",
"spp_change_request_v2/static/src/xml/search_delay_field.xml",
"spp_change_request_v2/static/src/xml/cr_search_results_field.xml",
],
},
"installable": True,
Expand Down
4 changes: 4 additions & 0 deletions spp_change_request_v2/models/change_request_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class SPPChangeRequestConflict(models.Model):
_name = "spp.change.request"
_inherit = ["spp.change.request", "spp.cr.conflict.mixin"]

is_conflict_detection_enabled = fields.Boolean(
related="request_type_id.enable_conflict_detection",
)

# ══════════════════════════════════════════════════════════════════════════
# OVERRIDE CRUD TO INTEGRATE CONFLICT DETECTION
# ══════════════════════════════════════════════════════════════════════════
Expand Down
28 changes: 27 additions & 1 deletion spp_change_request_v2/models/change_request_detail_base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from odoo import fields, models
from odoo import _, api, fields, models
from odoo.exceptions import UserError


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

@api.depends("change_request_id.name")
def _compute_display_name(self):
for rec in self:
if rec.change_request_id and rec.change_request_id.name:
rec.display_name = rec.change_request_id.name
else:
super(SPPCRDetailBase, rec)._compute_display_name()

change_request_id = fields.Many2one(
"spp.change.request",
string="Change Request",
Expand All @@ -33,6 +42,23 @@ class SPPCRDetailBase(models.AbstractModel):
related="change_request_id.is_applied",
)

def action_proceed_to_cr(self):
"""Navigate to the parent Change Request form if there are proposed changes."""
self.ensure_one()
cr = self.change_request_id
if not cr.has_proposed_changes:
raise UserError(
_("No proposed changes detected. Please make changes before proceeding.")
)
return {
"type": "ir.actions.act_window",
"name": cr.name,
"res_model": "spp.change.request",
"res_id": cr.id,
"view_mode": "form",
"target": "current",
}

def action_submit_for_approval(self):
"""Submit the parent CR for approval."""
self.ensure_one()
Expand Down
20 changes: 19 additions & 1 deletion spp_change_request_v2/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Extend res.partner for better registrant display in CR wizard."""

from odoo import api, models
from odoo import api, fields, models


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

_inherit = "res.partner"

reg_id_display = fields.Char(
string="Registrant ID",
compute="_compute_reg_id_display",
)

@api.depends("reg_ids.value", "reg_ids.id_type_id")
def _compute_reg_id_display(self):
for rec in self:
if rec.reg_ids:
parts = []
for rid in rec.reg_ids:
if rid.value:
label = rid.id_type_as_str or "ID"
parts.append(f"{label} ({rid.value})")
rec.reg_id_display = ", ".join(parts) if parts else ""
else:
rec.reg_id_display = ""
Comment on lines +19 to +28

Choose a reason for hiding this comment

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

medium

The logic in this method can be made more concise and Pythonic by using a list comprehension. This would eliminate the need for the outer if/else block and the conditional expression within the join call, making the code easier to read and maintain.

        parts = [
            f"{(rid.id_type_as_str or 'ID')} ({rid.value})"
            for rid in rec.reg_ids
            if rid.value
        ]
        rec.reg_id_display = ", ".join(parts)


def _compute_display_name(self):
"""Add registrant ID to display name when in CR wizard context."""
super()._compute_display_name()
Expand Down
7 changes: 7 additions & 0 deletions spp_change_request_v2/static/src/css/cr_search_results.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* Force the search results widget to take full width in the form */
.o_field_cr_search_results,
.o_field_widget:has(.o_field_cr_search_results) {
width: 100% !important;
max-width: 100% !important;
display: block !important;
}
62 changes: 62 additions & 0 deletions spp_change_request_v2/static/src/js/cr_search_results_field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/** @odoo-module **/

import {Component, onMounted, onPatched, useRef} from "@odoo/owl";
import {_t} from "@web/core/l10n/translation";
import {registry} from "@web/core/registry";
import {standardFieldProps} from "@web/views/fields/standard_field_props";

/**
* Custom widget that renders HTML search results and handles row clicks.
* When a row with class "o_cr_search_result" is clicked, it writes the
* partner ID to the _selected_partner_id bridge field, which triggers
* a server-side onchange to set registrant_id.
*/
export class CrSearchResultsField extends Component {
static template = "spp_change_request_v2.CrSearchResultsField";
static props = {...standardFieldProps};

setup() {
this.containerRef = useRef("container");
onMounted(() => this._attachClickHandler());
onPatched(() => this._attachClickHandler());
}

get htmlContent() {
return this.props.record.data[this.props.name] || "";
}

_attachClickHandler() {
const el = this.containerRef.el;
if (!el) return;
// Row selection
el.querySelectorAll(".o_cr_search_result").forEach((row) => {
row.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const partnerId = parseInt(row.dataset.partnerId);
if (partnerId) {
this.props.record.update({_selected_partner_id: partnerId});
}
};
});
// Pagination
el.querySelectorAll(".o_cr_page_prev, .o_cr_page_next").forEach((link) => {
link.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const page = parseInt(link.dataset.page);
if (!isNaN(page) && page >= 0) {
this.props.record.update({_search_page: page});
}
};
});
}
Comment on lines +18 to +53

Choose a reason for hiding this comment

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

medium

For better performance and maintainability, consider using event delegation for handling clicks instead of attaching individual onclick handlers to each result row and pagination link. By adding a single event listener to the container, you can avoid re-attaching handlers on every render (onPatched) and reduce the total number of event listeners, which is more efficient, especially with many search results.

Also, it's a good practice to always provide the radix parameter to parseInt to avoid unexpected behavior.

    setup() {
        this.containerRef = useRef("container");
        onMounted(() => {
            this.containerRef.el.addEventListener("click", this._onClick.bind(this));
        });
    }

    get htmlContent() {
        return this.props.record.data[this.props.name] || "";
    }

    _onClick(ev) {
        // Row selection
        const row = ev.target.closest(".o_cr_search_result");
        if (row) {
            ev.preventDefault();
            ev.stopPropagation();
            const partnerId = parseInt(row.dataset.partnerId, 10);
            if (partnerId) {
                this.props.record.update({_selected_partner_id: partnerId});
            }
            return;
        }

        // Pagination
        const pageLink = ev.target.closest(".o_cr_page_prev, .o_cr_page_next");
        if (pageLink) {
            ev.preventDefault();
            ev.stopPropagation();
            const page = parseInt(pageLink.dataset.page, 10);
            if (!isNaN(page) && page >= 0) {
                this.props.record.update({_search_page: page});
            }
        }
    }

}

export const crSearchResultsField = {
component: CrSearchResultsField,
displayName: _t("CR Search Results"),
supportedTypes: ["html"],
};

registry.category("fields").add("cr_search_results", crSearchResultsField);
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,6 @@ patch(FormController.prototype, {
if (this.props.resModel === "spp.change.request") {
this.hideFormCreateButton = true;
}
// Row click handling for CR create wizard is now in cr_search_results_field.js
},
});
58 changes: 58 additions & 0 deletions spp_change_request_v2/static/src/js/search_delay_field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @odoo-module **/

import {Component, useEffect, useRef, onWillUnmount} from "@odoo/owl";
import {_t} from "@web/core/l10n/translation";
import {useDebounced} from "@web/core/utils/timing";
import {registry} from "@web/core/registry";
import {standardFieldProps} from "@web/views/fields/standard_field_props";

/**
* Char field that triggers onchange after a typing delay (500ms).
* Used for search fields where we want live results without waiting for blur.
*/
export class SearchDelayField extends Component {
static template = "spp_change_request_v2.SearchDelayField";
static props = {
...standardFieldProps,
placeholder: {type: String, optional: true},
};

setup() {
this.inputRef = useRef("input");

this.debouncedCommit = useDebounced((value) => {
this.props.record.update({[this.props.name]: value});
}, 500);

// Keep input in sync when record updates externally (e.g. onchange clears it)
useEffect(
() => {
const el = this.inputRef.el;
if (el) {
const recordValue = this.props.record.data[this.props.name] || "";
if (el !== document.activeElement || !recordValue) {
el.value = recordValue;
}
}
},
() => [this.props.record.data[this.props.name]]
);
}

get value() {
return this.props.record.data[this.props.name] || "";
}

onInput(ev) {
this.debouncedCommit(ev.target.value);
}
}

export const searchDelayField = {
component: SearchDelayField,
displayName: _t("Search with Delay"),
supportedTypes: ["char"],
extractProps: ({placeholder}) => ({placeholder}),
Copy link

Choose a reason for hiding this comment

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

Placeholder prop never extracted from view attrs

Low Severity

extractProps: ({placeholder}) => ({placeholder}) destructures placeholder directly from the top-level fieldInfo argument, which has the shape {attrs, field, ...}. In Odoo 16/17, XML view attributes like placeholder="Enter name or ID number..." are stored at attrs.placeholder, not at the top level. The other widget in this same codebase (cel_editor_field.js) correctly uses ({attrs, options}) => ({placeholder: attrs.placeholder}). As written, placeholder is always undefined, so the search input shows no hint text.

Fix in Cursor Fix in Web

};

registry.category("fields").add("search_delay", searchDelayField);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="spp_change_request_v2.CrSearchResultsField">
<div t-ref="container" class="o_field_cr_search_results" style="width:100%" t-out="htmlContent"/>

Choose a reason for hiding this comment

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

medium

The inline style style="width:100%" is redundant, as the o_field_cr_search_results class already has its width set to 100% !important in the associated CSS file (cr_search_results.css). It's best practice to rely on the stylesheet for styling to improve maintainability and separation of concerns. Please remove the inline style.

Suggested change
<div t-ref="container" class="o_field_cr_search_results" style="width:100%" t-out="htmlContent"/>
<div t-ref="container" class="o_field_cr_search_results" t-out="htmlContent"/>

</t>

</templates>
14 changes: 14 additions & 0 deletions spp_change_request_v2/static/src/xml/search_delay_field.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="spp_change_request_v2.SearchDelayField">
<input t-ref="input"
type="text"
class="o_input"
t-att-value="value"
t-att-placeholder="props.placeholder || ''"
t-att-readonly="props.readonly ? 'readonly' : undefined"
t-on-input="onInput"/>
</t>

</templates>
29 changes: 3 additions & 26 deletions spp_change_request_v2/views/change_request_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -232,36 +232,13 @@
/>
</header>
<sheet>
<!-- Button Box with Smart Buttons -->
<div class="oe_button_box" name="button_box">
<button
name="action_view_registrant"
type="object"
class="oe_stat_button"
icon="fa-user"
>
<div class="o_stat_info">
<span class="o_stat_text">View</span>
<span class="o_stat_value">Registrant</span>
</div>
</button>
<button
name="action_open_detail"
type="object"
class="oe_stat_button"
icon="fa-edit"
invisible="approval_state not in ('draft', 'revision')"
>
<div class="o_stat_info">
<span class="o_stat_text">Edit</span>
<span class="o_stat_value">Details</span>
</div>
</button>
<button
name="action_open_preview_wizard"
type="object"
class="oe_stat_button"
icon="fa-eye"
groups="base.group_no_one"
invisible="approval_state in ('applied', 'rejected')"
>
<div class="o_stat_info">
Expand Down Expand Up @@ -452,14 +429,14 @@
/>Current Data
</h5>
<button
name="action_open_registrant"
name="action_view_registrant"
type="object"
class="btn btn-sm btn-link text-primary"
invisible="not registrant_id"
>
<i
class="fa fa-external-link me-1"
/>Open Form
/>View Registrant
</button>
</div>
<div class="card-body">
Expand Down
2 changes: 1 addition & 1 deletion spp_change_request_v2/views/conflict_extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
string="Check for Updates"
type="object"
class="btn-link"
invisible="approval_state != 'draft'"
invisible="approval_state != 'draft' or not is_conflict_detection_enabled"
help="Check if other requests have been completed"
/>
</xpath>
Expand Down
44 changes: 32 additions & 12 deletions spp_change_request_v2/views/create_wizard_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,51 @@
</group>
</group>

<!-- Registrant Selection (shown after type selected) -->
<!-- Hidden fields -->
<field name="show_registrant" invisible="1" />
<field name="registrant_domain" invisible="1" />
<field name="registrant_id" invisible="1" />
<field name="_selected_partner_id" invisible="1" />
<field name="_search_page" invisible="1" />

<group invisible="not show_registrant">
<!-- Registrant Search (shown after type selected, hidden after selection) -->
<group invisible="not show_registrant or registrant_id">
<group>
<field
name="registrant_id"
string="Registrant"
domain="registrant_domain"
context="{'show_registrant_id': True}"
options="{'no_create': True}"
placeholder="Search by name or ID..."
required="show_registrant"
name="search_text"
string="Search Registrant"
placeholder="Enter name or ID number..."
widget="search_delay"
/>
</group>
</group>

<!-- Selected registrant info (compact) -->
<!-- Search Results (clickable rows, handled by widget) -->
<div
class="alert alert-light border py-2 mb-3"
class="w-100"
invisible="not search_results_html or registrant_id"
>
<field
name="search_results_html"
nolabel="1"
readonly="1"
widget="cr_search_results"
/>
</div>

<!-- Selected registrant info -->
<div
class="alert alert-success py-2 mb-3"
role="status"
invisible="not registrant_id"
>
<field name="registrant_info_html" nolabel="1" readonly="1" />
<button
name="action_clear_registrant"
type="object"
string="Change Registrant"
class="btn btn-sm btn-outline-secondary mt-2"
icon="fa-pencil"
/>
</div>
</sheet>
<footer>
Expand Down
Loading
Loading