Skip to content
Open
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
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}),
};

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>
14 changes: 3 additions & 11 deletions spp_change_request_v2/views/change_request_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,6 @@
<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"
Expand All @@ -207,6 +198,7 @@
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">
<span class="o_stat_text">Preview</span>
Expand Down Expand Up @@ -344,11 +336,11 @@
<h5 class="mb-0">
<i class="fa fa-user me-2"/>Current Data
</h5>
<button name="action_open_registrant"
<button 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
<i class="fa fa-external-link me-1"/>View Registrant
</button>
</div>
<div class="card-body">
Expand Down
33 changes: 21 additions & 12 deletions spp_change_request_v2/views/create_wizard_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,34 @@
</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"/>
<field name="search_text"
string="Search Registrant"
placeholder="Enter name or ID number..."
widget="search_delay"/>
</group>
</group>

<!-- Selected registrant info (compact) -->
<div class="alert alert-light border py-2 mb-3" role="status" invisible="not registrant_id">
<!-- Search Results (clickable rows, handled by widget) -->
<div 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
8 changes: 4 additions & 4 deletions spp_change_request_v2/views/detail_add_member_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
<form>
<header>
<button
name="action_submit_for_approval"
string="Submit for Approval"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'draft'"
/>
<button
name="action_submit_for_approval"
string="Resubmit for Review"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'revision'"
Expand Down
8 changes: 4 additions & 4 deletions spp_change_request_v2/views/detail_change_hoh_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
<form string="Change Head of Household Details">
<header>
<button
name="action_submit_for_approval"
string="Submit for Approval"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'draft'"
/>
<button
name="action_submit_for_approval"
string="Resubmit for Review"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'revision'"
Expand Down
8 changes: 4 additions & 4 deletions spp_change_request_v2/views/detail_create_group_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
<form string="Create New Group Details">
<header>
<button
name="action_submit_for_approval"
string="Submit for Approval"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'draft'"
/>
<button
name="action_submit_for_approval"
string="Resubmit for Review"
name="action_proceed_to_cr"
string="Proceed"
type="object"
class="btn-primary"
invisible="approval_state != 'revision'"
Expand Down
Loading
Loading