Hi,
I'm working on Odoo 18.
Here's my issue: I need to display the activities from the child_ids of a contact in the Chatter component on the contact's view (res.partner).
To do this, I created a computed field all_activity_ids One2Many on res.partner, wich collects all the activities i want to display in the Chatter (including those from child_ids).
In short, i want to replace the default activity_ids field used by Chatter by my custom all_activity_ids, but only on the contact form view.
I thought about overriding activity_ids with the contents of my all_activity_ids, but that doesn't because activity filtrers should not include activities from the child contact.
I've tried different approaches using both JS and XML, but i haven't been able to achieve the expected result.
As a fallback, I also considered adding a second actvity list in the Chatter and disabling the default one for res.partner, but that didn't work either.
I don't know if I was cleared in the explication of my problem.
Does anyone have an idea of how I could achieve this properly ?
Thanks in advance !
Hi everyone,
I made some progress on my previous issue, but I’m stuck on the last part: realtime modification.
Here’s the problem:
When I mark an activity as done (only if it belongs to a child_ids), the confirmation popup stays open, and I can mark the activity as done infinitely without refreshing the page.
Here’s the solution I have so far:
/** @odoo-module **/
import { onWillStart } from "@odoo/owl";
import { patch } from "@web/core/utils/patch";
import { Chatter } from "@mail/chatter/web_portal/chatter";
patch(Chatter.prototype, {
setup() {
super.setup();
onWillStart(async () => {
if (this.props?.threadModel !== "res.partner") return;
const ids = this._getAllActivityIds();
if (!ids.length) return;
await this._primeActivities(ids);
});
},
get activities() {
console.log('[ChatterPatch] get activities');
if (this.props.threadModel !== "res.partner") return this.state?.thread?.activities ?? [];
const baseActivities = this.state?.thread?.activities ?? [];
const ids = [...new Set(this._getAllActivityIds())];
const baseMap = new Map(baseActivities.map(a => [a.id, a]));
const resolved = this._resolveFromStore(ids);
const extras = resolved.filter(a => a && !baseMap.has(a.id));
return [...baseActivities, ...extras];
},
// private methods
_getMailStore() {
const s = this?.env?.services;
if (s && (s["mail.store"] || s.mailStore)) return s["mail.store"] || s.mailStore;
const ts = this?.state?.thread?.store;
if (ts) return ts;
try {
const dbg = window.odoo?.__DEBUG__?.services;
if (dbg && (dbg["mail.store"] || dbg.mailStore)) return dbg["mail.store"] || dbg.mailStore;
} catch {}
return null;
},
async _primeActivities(ids) {
const store = this._getMailStore();
const orm = this?.env?.services?.orm;
if (!store || !orm || !ids?.length) return;
const have = (id) =>
(store.Activity?.get?.(id)) ||
(store.activityById?.[id]) ||
(store.activities instanceof Map ? store.activities.get(id) : null);
const missing = ids.filter((id) => !have(id));
if (!missing.length) return;
const fieldsInfo = await orm.call("mail.activity", "fields_get", [], {});
const fieldNames = Object.keys(fieldsInfo);
const activities = await orm.read("mail.activity", missing, fieldNames);
if (!activities?.length) return;
if (store.Activity?.insertFromServer) store.Activity.insertFromServer(activities);
else if (store.Activity?.insert) store.Activity.insert(activities);
this._rowsByActId = new Map(activities.map(a => [a.id, a]));
const userIds = [...new Set(activities
.map(a => Array.isArray(a.user_id) ? a.user_id[0] : a.user_id)
.filter(Boolean))];
if (!userIds.length) return;
const userRows = await orm.read("res.users", userIds, ["partner_id", "name"]);
const partnerIds = [...new Set(userRows
.map(u => Array.isArray(u.partner_id) ? u.partner_id[0] : u.partner_id)
.filter(Boolean))];
if (!partnerIds.length) return;
const partners = await orm.read("res.partner", partnerIds, ["id", "name", "display_name", "email"]);
this._personaByPartnerId = new Map(partners.map(p => {
const avatarUrl = `/web/image/res.partner/${p.id}/avatar_128`;
return [p.id, {
id: p.id,
name: p.display_name || p.name,
display_name: p.display_name || p.name,
email: p.email,
avatarUrl,
}];
}));
},
_resolveFromStore(ids) {
const store = this._getMailStore();
let arr = [];
if (store?.Activity?.get) arr = ids.map(id => store.Activity.get(id)).filter(Boolean);
if (!arr.length && store?.Activity?.findFromIdentifyingData)
arr = ids.map(id => store.Activity.findFromIdentifyingData({ id })).filter(Boolean);
if (!arr.length && store?.activityById)
arr = ids.map(id => store.activityById[id]).filter(Boolean);
if (!arr.length && store?.activities instanceof Map)
arr = ids.map(id => store.activities.get(id)).filter(Boolean);
const rowsById = this._rowsByActId || new Map();
const personaByPartnerId = this._personaByPartnerId || new Map();
const defaultPersona = this._defaultPersona || {
id: 0, name: "User", display_name: "User",
avatarUrl: (store?.DEFAULT_AVATAR) || "/mail/static/src/img/smiley/avatar.jpg",
};
for (const a of arr) {
if (a && !a.persona) {
const row = rowsById.get(a.id);
let partnerId = null;
let userName = null;
if (row?.user_id) {
partnerId = Array.isArray(row.user_id) ? row.user_id[0] : null;
userName = Array.isArray(row.user_id) ? row.user_id[1] : null;
}
const persona = partnerId && personaByPartnerId.get(partnerId)
? personaByPartnerId.get(partnerId)
: defaultPersona;
if (userName && persona === defaultPersona) {
a.persona = { ...defaultPersona, name: userName, display_name: userName };
} else {
a.persona = persona;
}
}
}
return arr;
},
_getAllActivityIds() {
const rel = this.props?.webRecord?.data?.all_activity_ids;
if (!rel) return [];
if (Array.isArray(rel._currentIds)) return rel._currentIds.slice();
if (Array.isArray(rel.records)) return rel.records.map(r => r.id).filter(Boolean);
if (Array.isArray(rel.res_ids)) return rel.res_ids;
if (Array.isArray(rel)) return rel;
return [];
}
});