I have an overlay widget with a toggle of pass fail, the toggle works in such that it toggles as expected but each time in generated the following error and nothing seems to fix it. The error is as follows and I'm using
Odoo Client Error
UncaughtPromiseError > TypeError
Uncaught Promise > can't convert undefined to object
Occured on [redacted] on 2025-07-02 11:46:49 GMT
TypeError: can't convert undefined to object
_preprocessPropertiesChanges@[redacted]/web/assets/debug/web.assets_web.js:57849:49
_update@[redacted]/web/assets/debug/web.assets_web.js:58170:18
update/<@[redacted]/web/assets/debug/web.assets_web.js:57185:24
always@[redacted]/web/assets/debug/web.assets_web.js:41466:36
promise callback*exec@[redacted]/web/assets/debug/web.assets_web.js:41472:33
update@[redacted]/web/assets/debug/web.assets_web.js:57184:33
togglePassFail@[redacted]/web/assets/debug/web.assets_web.js:248270:31
template/hdlr2<@[redacted]/web/assets/debug/web.assets_web.js line 13745 > Function:27:27
mainEventHandler@[redacted]/web/assets/debug/web.assets_web.js:14072:25
listener@[redacted]/web/assets/debug/web.assets_web.js:8404:20
EventListener.handleEvent*setup@[redacted]/web/assets/debug/web.assets_web.js:8408:18
mount@[redacted]/web/assets/debug/web.assets_web.js:9118:37
mount@[redacted]/web/assets/debug/web.assets_web.js:9219:27
mount@[redacted]/web/assets/debug/web.assets_web.js:9131:35
mount@[redacted]/web/assets/debug/web.assets_web.js:9027:27
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:9131:35
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:9131:35
mount@[redacted]/web/assets/debug/web.assets_web.js:9027:27
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:9131:35
mount@[redacted]/web/assets/debug/web.assets_web.js:9027:27
mount@[redacted]/web/assets/debug/web.assets_web.js:8483:27
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:9131:35
mount@[redacted]/web/assets/debug/web.assets_web.js:9027:27
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:9501:28
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:10620:18
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
mount@[redacted]/web/assets/debug/web.assets_web.js:8035:24
patch@[redacted]/web/assets/debug/web.assets_web.js:9179:36
This is the code im using:
/** @odoo-module **/
import { Component, xml, useRef, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
console.log("van_check_overlay.js loaded!", new Date().toISOString());
function extractM2O(val, fallbackName) {
if (val && typeof val === "object" && val.target && Array.isArray(val.target)) {
val = val.target;
}
if (Array.isArray(val)) return Array.from(val);
let v = val, safety = 0;
while (v && typeof v === "object" && typeof v.id !== "undefined" && v.id !== null && safety < 5) {
if (typeof v.id === "object" && v.id !== null) {
v = v.id;
safety++;
} else {
return [v.id, v.display_name || v.name || fallbackName || ""];
}
}
if (v && v.data && typeof v.data.id !== "undefined" && v.data.id !== null) {
return [v.data.id, v.data.display_name || v.data.name || fallbackName || ""];
}
if (v && v.target && typeof v.target.id !== "undefined" && v.target.id !== null) {
return [v.target.id, v.target.display_name || v.target.name || fallbackName || ""];
}
if (typeof v === "number" || typeof v === "string") {
return [v, fallbackName || ""];
}
return false;
}
function safe(obj, key, fallback) {
if (typeof obj[key] !== "undefined" && obj[key] !== null) return obj[key];
return fallback;
}
class VanCheckOverlay extends Component {
static props = { ...standardFieldProps };
static supportedTypes = ["one2many"];
setup() {
this.orm = useService("orm");
this.state = useState({ template: null, trigger: 0 });
this.imgRef = useRef("imgRef");
onWillStart(async () => {
const raw = this.props.record.data.template_id;
const tplId = Array.isArray(raw) ? raw[0] : (raw?.id || raw);
if (!tplId) return;
const [tpl] = await this.orm.read(
"van.weekly.checklist.template",
[tplId],
["image", "line_template_ids"]
);
if (!tpl) return;
tpl.line_template_ids = tpl.line_template_ids.length
? await this.orm.read(
"van.weekly.checklist.line.template",
tpl.line_template_ids,
["id", "code", "name", "position_x", "position_y", "sequence"]
)
: [];
this.state.template = tpl;
});
}
get imgSrc() {
return this.state.template?.image
? `data:image/png;base64,${this.state.template.image}`
: "/van_weekly_checklist/static/src/img/van.png";
}
get instanceLineMap() {
const map = {};
const lines = this.props.record.data.line_ids?.records || this.props.record.data.line_ids || [];
for (const rec of lines) {
let data = rec.data || rec;
let tli = data.template_line_id;
let tid;
if (tli && typeof tli === "object" && typeof tli.id !== "undefined" && tli.id !== null) {
tid = tli.id;
} else if (tli && tli.target && typeof tli.target.id !== "undefined" && tli.target.id !== null) {
tid = tli.target.id;
} else if (tli && tli.data && typeof tli.data.id !== "undefined" && tli.data.id !== null) {
tid = tli.data.id;
} else if (typeof tli === "object" && Array.isArray(tli.target)) {
tid = tli.target[0];
} else if (Array.isArray(tli)) {
tid = tli[0];
} else {
tid = tli;
}
if (tid) map[tid] = rec;
}
return map;
}
get templateLines() {
return this.state.template?.line_template_ids || [];
}
getInstanceLine(tline) {
return this.instanceLineMap[tline.id];
}
spotClass(rec) {
const data = rec?.data || rec;
if (data?.pass_fail === "pass") return "van-check-spot pass";
if (data?.pass_fail === "fail") return "van-check-spot fail";
return "van-check-spot";
}
spotIcon(rec) {
const data = rec?.data || rec;
if (data?.pass_fail === "pass") return "✔️";
if (data?.pass_fail === "fail") return "❌";
return "";
}
async togglePassFail(tline) {
const rec = this.getInstanceLine(tline);
if (!rec) return;
const data = rec.data || rec;
const current = data.pass_fail || false;
const next = current === "pass" ? "fail" : current === "fail" ? false : "pass";
let rawM2O = data.template_line_id;
let resolvedM2O = extractM2O(rawM2O, tline.name);
if (!resolvedM2O || resolvedM2O[0] === undefined || resolvedM2O[0] === null) {
resolvedM2O = [tline.id, tline.name || ""];
}
// Defensive: try id, but fallback to [id, name] for new records
let isNew = !rec.res_id;
let updateObj = {
pass_fail: next,
template_line_id: isNew ? resolvedM2O : resolvedM2O[0],
code: safe(data, "code", safe(tline, "code", "")),
name: safe(data, "name", safe(tline, "name", "")),
position_x: typeof safe(data, "position_x", safe(tline, "position_x", 0)) === "number" ? safe(data, "position_x", safe(tline, "position_x", 0)) : 0,
position_y: typeof safe(data, "position_y", safe(tline, "position_y", 0)) === "number" ? safe(data, "position_y", safe(tline, "position_y", 0)) : 0,
sequence: typeof safe(data, "sequence", safe(tline, "sequence", 0)) === "number" ? safe(data, "sequence", safe(tline, "sequence", 0)) : 0,
};
// Final double-check: no undefined/null at all
Object.keys(updateObj).forEach(field => {
if (typeof updateObj[field] === "undefined" || updateObj[field] === null) {
if (["position_x", "position_y", "sequence"].includes(field)) updateObj[field] = 0;
else updateObj[field] = "";
}
});
// Log every field & type for debug
console.log("togglePassFail updateObj:");
Object.entries(updateObj).forEach(([k, v]) => console.log(` ${k}:`, v, typeof v));
try {
if (rec.update) {
rec.update(updateObj);
} else {
throw new Error("rec.update missing, will patch manually");
}
} catch (e) {
// Fallback: patch data directly
data.pass_fail = next;
data.template_line_id = isNew ? resolvedM2O : resolvedM2O[0];
data.code = updateObj.code;
data.name = updateObj.name;
data.position_x = updateObj.position_x;
data.position_y = updateObj.position_y;
data.sequence = updateObj.sequence;
console.warn("rec.update failed, patched data manually", e);
}
if (rec.res_id) {
try {
await this.orm.write("van.weekly.checklist.line", [rec.res_id], { pass_fail: next });
} catch (err) {
console.warn("ORM write failed:", err);
}
}
if (this.props.record.update) {
this.props.record.update();
}
this.state.trigger = Math.random();
const lines = this.props.record.data.line_ids?.records || this.props.record.data.line_ids || [];
console.log("After update:", lines.map(l => l.data || l));
}
debug() {
console.log("INSTANCE MAP", this.instanceLineMap);
const lines = this.props.record.data.line_ids?.records || this.props.record.data.line_ids || [];
lines.forEach((line, i) => {
let data = line.data || line;
console.log(`Line ${i}:`, line, data);
});
}
}
VanCheckOverlay.template = xml`
<div class="van-image-container" style="position:relative;">
<button type="button" t-on-click="debug" style="margin-bottom:12px;">Debug</button>
<img t-ref="imgRef" t-att-src="imgSrc" class="van-full-image"/>
<t t-foreach="templateLines" t-as="tline" t-key="tline.id">
<div
t-att-class="spotClass(getInstanceLine(tline))"
t-att-style="'left:' + (tline.position_x||0) + 'px; top:' + (tline.position_y||0) + 'px;'"
style="cursor:pointer;"
t-on-click="() => this.togglePassFail(tline)"
>
<span class="label" t-esc="tline.code"/>
<span t-esc="tline.name"/>
<t t-set="rec" t-value="getInstanceLine(tline)"/>
<t t-if="rec">
<span class="toggle-value" style="margin-left:10px;font-weight:bold;font-size:18px;">
<t t-esc="spotIcon(rec)"/>
</span>
</t>
</div>
</t>
</div>
`;
registry.category("fields").add("check_overlay", {
component: VanCheckOverlay,
fields: [
"line_ids",
"line_ids.template_line_id",
"line_ids.code",
"line_ids.name",
"line_ids.position_x",
"line_ids.position_y",
"line_ids.pass_fail",
"line_ids.sequence"
]
});
export default VanCheckOverlay;