diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index cd75fedbe76..06622858f26 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -844,20 +844,6 @@ class Message(models.Model):
else:
author = (0, message_sudo.email_from)
- # Tracking values
- tracking_value_ids = []
- for tracking in message_sudo.tracking_value_ids:
- groups = tracking.field_groups
- if not groups or self.env.is_superuser() or self.user_has_groups(groups):
- tracking_value_ids.append({
- 'id': tracking.id,
- 'changed_field': tracking.field_desc,
- 'old_value': tracking.get_old_display_value()[0],
- 'new_value': tracking.get_new_display_value()[0],
- 'field_type': tracking.field_type,
- 'currency_id': tracking.currency_id.id,
- })
-
if message_sudo.model and message_sudo.res_id:
record_name = self.env[message_sudo.model] \
.browse(message_sudo.res_id) \
@@ -886,10 +872,11 @@ class Message(models.Model):
} for content, reactions in reactions_per_content.items()])]
if format_reply and message_sudo.model == 'mail.channel' and message_sudo.parent_id:
vals['parentMessage'] = message_sudo.parent_id.message_format(format_reply=False)[0]
+ allowed_tracking_ids = message_sudo.tracking_value_ids.filtered(lambda tracking: not tracking.field_groups or self.env.is_superuser() or self.user_has_groups(tracking.field_groups))
vals.update({
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
'attachment_ids': message_sudo.attachment_ids._attachment_format(),
- 'tracking_value_ids': tracking_value_ids,
+ 'trackingValues': [('insert-and-replace', allowed_tracking_ids._tracking_value_format())],
'messageReactionGroups': reaction_groups,
'record_name': record_name,
})
@@ -930,12 +917,20 @@ class Message(models.Model):
],
'needaction_partner_ids': [], # list of partner ids
'res_id': 7,
- 'tracking_value_ids': [
+ 'trackingValues': [('insert-and-replace',
{
- 'old_value': "",
- 'changed_field': "Customer",
+ 'changedField': "Customer",
'id': 2965,
- 'new_value': "Axelor"
+ 'newValue': [('insert-and-replace', {
+ 'currencyId': "",
+ 'fieldType': 'char',
+ 'value': "Axelor",
+ })],
+ 'oldValue': [('insert-and-replace', {
+ 'currencyId': "",
+ 'fieldType': 'char',
+ 'value': "",
+ })],
}
],
'author_id': (3, u'Administrator'),
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index cd8a21fdd06..0f7b87eb3af 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -2580,8 +2580,8 @@ class MailThread(models.AbstractModel):
groups = tracking_value.field_groups
if not groups or self.env.is_superuser() or self.user_has_groups(groups):
tracking.append((tracking_value.field_desc,
- tracking_value.get_old_display_value()[0],
- tracking_value.get_new_display_value()[0]))
+ tracking_value._get_old_display_value()[0],
+ tracking_value._get_new_display_value()[0]))
subtype_id = msg_vals.get('subtype_id') if msg_vals and 'subtype_id' in msg_vals else message.subtype_id.id
is_discussion = subtype_id == self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py
index 2edfab5abfc..92ae0601028 100644
--- a/addons/mail/models/mail_tracking_value.py
+++ b/addons/mail/models/mail_tracking_value.py
@@ -88,34 +88,49 @@ class MailTracking(models.Model):
return values
return {}
- def get_display_value(self, type):
- assert type in ('new', 'old')
+ def _tracking_value_format(self):
+ tracking_values = [{
+ 'changedField': tracking.field_desc,
+ 'id': tracking.id,
+ 'newValue': [('insert-and-replace', {
+ 'currencyId': tracking.currency_id.id,
+ 'fieldType': tracking.field_type,
+ 'value': tracking._get_new_display_value()[0]})],
+ 'oldValue': [('insert-and-replace', {
+ 'currencyId': tracking.currency_id.id,
+ 'fieldType': tracking.field_type,
+ 'value': tracking._get_old_display_value()[0]})],
+ } for tracking in self]
+ return tracking_values
+
+ def _get_display_value(self, prefix):
+ assert prefix in ('new', 'old')
result = []
for record in self:
if record.field_type in ['integer', 'float', 'char', 'text', 'monetary']:
- result.append(getattr(record, '%s_value_%s' % (type, record.field_type)))
+ result.append(record[f'{prefix}_value_{record.field_type}'])
elif record.field_type == 'datetime':
- if record['%s_value_datetime' % type]:
- new_datetime = getattr(record, '%s_value_datetime' % type)
- result.append('%sZ' % new_datetime)
+ if record[f'{prefix}_value_datetime']:
+ new_datetime = record[f'{prefix}_value_datetime']
+ result.append(f'{new_datetime}Z')
else:
- result.append(record['%s_value_datetime' % type])
+ result.append(record[f'{prefix}_value_datetime'])
elif record.field_type == 'date':
- if record['%s_value_datetime' % type]:
- new_date = record['%s_value_datetime' % type]
+ if record[f'{prefix}_value_datetime']:
+ new_date = record[f'{prefix}_value_datetime']
result.append(fields.Date.to_string(new_date))
else:
- result.append(record['%s_value_datetime' % type])
+ result.append(record[f'{prefix}_value_datetime'])
elif record.field_type == 'boolean':
- result.append(bool(record['%s_value_integer' % type]))
+ result.append(bool(record[f'{prefix}_value_integer']))
else:
- result.append(record['%s_value_char' % type])
+ result.append(record[f'{prefix}_value_char'])
return result
- def get_old_display_value(self):
+ def _get_old_display_value(self):
# grep : # old_value_integer | old_value_datetime | old_value_char
- return self.get_display_value('old')
+ return self._get_display_value('old')
- def get_new_display_value(self):
+ def _get_new_display_value(self):
# grep : # new_value_integer | new_value_datetime | new_value_char
- return self.get_display_value('new')
+ return self._get_display_value('new')
diff --git a/addons/mail/static/src/components/message/message.xml b/addons/mail/static/src/components/message/message.xml
index 53c0b347659..18132b175c9 100644
--- a/addons/mail/static/src/components/message/message.xml
+++ b/addons/mail/static/src/components/message/message.xml
@@ -147,8 +147,10 @@
diff --git a/addons/mail/static/src/components/tracking_value/tracking_value.js b/addons/mail/static/src/components/tracking_value/tracking_value.js
index 17856fb3e33..f86660432ed 100644
--- a/addons/mail/static/src/components/tracking_value/tracking_value.js
+++ b/addons/mail/static/src/components/tracking_value/tracking_value.js
@@ -4,7 +4,16 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
-export class TrackingValue extends Component {}
+export class TrackingValue extends Component {
+
+ /**
+ * @returns {TrackingValue}
+ */
+ get trackingValue() {
+ return this.props.value;
+ }
+
+}
Object.assign(TrackingValue, {
props: { value: Object },
diff --git a/addons/mail/static/src/components/tracking_value/tracking_value.scss b/addons/mail/static/src/components/tracking_value/tracking_value.scss
deleted file mode 100644
index 3c0215bc542..00000000000
--- a/addons/mail/static/src/components/tracking_value/tracking_value.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-
-.o_TrackingValue_item {
- // Could be replaced by "me-1" after the migration to BS5
- // for the backend AND the frontend
- margin-inline-end: map-get($spacers, 1);
-}
\ No newline at end of file
diff --git a/addons/mail/static/src/components/tracking_value/tracking_value.xml b/addons/mail/static/src/components/tracking_value/tracking_value.xml
index 7990674b0ae..7249ddf841d 100644
--- a/addons/mail/static/src/components/tracking_value/tracking_value.xml
+++ b/addons/mail/static/src/components/tracking_value/tracking_value.xml
@@ -1,21 +1,11 @@
-
-
-
+
+
-
-
-
-
-
-
-
None
-
-
-
-
-
None
-
-
+
+
+
+
+
+
-
-
\ No newline at end of file
+
diff --git a/addons/mail/static/src/models/message.js b/addons/mail/static/src/models/message.js
index 9e00f5e0ca2..9fc77200fdb 100644
--- a/addons/mail/static/src/models/message.js
+++ b/addons/mail/static/src/models/message.js
@@ -7,9 +7,7 @@ import emojis from '@mail/js/emojis';
import { addLink, htmlToTextContentInline, parseAndTransform } from '@mail/js/utils';
import { session } from '@web/session';
-import { sprintf } from '@web/core/utils/strings';
-import { format } from 'web.field_utils';
import { getLangDatetimeFormat, str_to_datetime } from 'web.time';
const { markup } = owl;
@@ -125,8 +123,8 @@ registerModel({
if ('subtype_id' in data) {
data2.subtype_id = data.subtype_id;
}
- if ('tracking_value_ids' in data) {
- data2.tracking_value_ids = data.tracking_value_ids;
+ if ('trackingValues' in data) {
+ data2.trackingValues = data.trackingValues;
}
return data2;
@@ -333,7 +331,7 @@ registerModel({
if (!this.originThread) {
return false;
}
- if (this.tracking_value_ids.length > 0) {
+ if (this.trackingValues.length > 0) {
return false;
}
if (this.message_type !== 'comment') {
@@ -479,7 +477,7 @@ registerModel({
return (
this.isBodyEmpty &&
!this.hasAttachments &&
- this.tracking_value_ids.length === 0 &&
+ this.trackingValues.length === 0 &&
!this.subtype_description
);
},
@@ -599,88 +597,6 @@ registerModel({
}
return replace(threads);
},
- /**
- * @private
- * @returns {Object[]}
- */
- _computeTrackingValues() {
- return this.tracking_value_ids.map(trackingValue => {
- const value = Object.assign({}, trackingValue);
- value.changed_field = sprintf(this.env._t("%s:"), value.changed_field);
- /**
- * Maps tracked field type to a JS formatter. Tracking values are
- * not always stored in the same field type as their origin type.
- * Field types that are not listed here are not supported by
- * tracking in Python. Also see `create_tracking_values` in Python.
- */
- switch (value.field_type) {
- case 'boolean':
- value.old_value = value.old_value ? this.env._t('Yes') : this.env._t('No');
- value.new_value = value.new_value ? this.env._t('Yes') : this.env._t('No');
- break;
- /**
- * many2one formatter exists but is expecting id/name_get or data
- * object but only the target record name is known in this context.
- *
- * Selection formatter exists but requires knowing all
- * possibilities and they are not given in this context.
- */
- case 'char':
- case 'many2one':
- case 'selection':
- value.old_value = format.char(value.old_value);
- value.new_value = format.char(value.new_value);
- break;
- case 'date':
- if (value.old_value) {
- value.old_value = moment.utc(value.old_value);
- }
- if (value.new_value) {
- value.new_value = moment.utc(value.new_value);
- }
- value.old_value = format.date(value.old_value);
- value.new_value = format.date(value.new_value);
- break;
- case 'datetime':
- if (value.old_value) {
- value.old_value = moment.utc(value.old_value);
- }
- if (value.new_value) {
- value.new_value = moment.utc(value.new_value);
- }
- value.old_value = format.datetime(value.old_value);
- value.new_value = format.datetime(value.new_value);
- break;
- case 'float':
- value.old_value = format.float(value.old_value);
- value.new_value = format.float(value.new_value);
- break;
- case 'integer':
- value.old_value = format.integer(value.old_value);
- value.new_value = format.integer(value.new_value);
- break;
- case 'monetary':
- value.old_value = format.monetary(value.old_value, undefined, {
- currency: value.currency_id
- ? this.env.session.currencies[value.currency_id]
- : undefined,
- forceString: true,
- });
- value.new_value = format.monetary(value.new_value, undefined, {
- currency: value.currency_id
- ? this.env.session.currencies[value.currency_id]
- : undefined,
- forceString: true,
- });
- break;
- case 'text':
- value.old_value = format.text(value.old_value);
- value.new_value = format.text(value.new_value);
- break;
- }
- return value;
- });
- },
},
fields: {
authorName: attr({
@@ -922,11 +838,9 @@ registerModel({
compute: '_computeThreads',
inverse: 'messages',
}),
- trackingValues: attr({
- compute: '_computeTrackingValues',
- }),
- tracking_value_ids: attr({
- default: [],
+ trackingValues: many('TrackingValue', {
+ inverse: 'messageOwner',
+ isCausal: true,
}),
},
});
diff --git a/addons/mail/static/src/models/tracking_value.js b/addons/mail/static/src/models/tracking_value.js
new file mode 100644
index 00000000000..7bc63e8755b
--- /dev/null
+++ b/addons/mail/static/src/models/tracking_value.js
@@ -0,0 +1,49 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+import { sprintf } from '@web/core/utils/strings';
+
+registerModel({
+ name: 'TrackingValue',
+ identifyingFields: ['id'],
+ recordMethods: {
+ /**
+ * @private
+ * @returns {string}
+ */
+ _computeFormattedChangedField() {
+ return sprintf(this.env._t("%s:"), this.changedField);
+ },
+ },
+ fields: {
+ /**
+ * States the original field of changed tracking value, such as "Status", "Date".
+ */
+ changedField: attr({
+ required: true,
+ }),
+ /**
+ * The translated `changedFiled` according to the language setting.
+ */
+ formattedChangedField: attr({
+ compute: '_computeFormattedChangedField',
+ }),
+ id: attr({
+ readonly: true,
+ required: true,
+ }),
+ messageOwner: one('Message', {
+ inverse: 'trackingValues',
+ readonly: true,
+ required: true,
+ }),
+ newValue: one('TrackingValueItem', {
+ inverse: 'trackingValueAsNewValue',
+ isCausal: true,
+ }),
+ oldValue: one('TrackingValueItem', {
+ inverse: 'trackingValueAsOldValue',
+ isCausal: true,
+ }),
+}});
diff --git a/addons/mail/static/src/models/tracking_value_item.js b/addons/mail/static/src/models/tracking_value_item.js
new file mode 100644
index 00000000000..a8e164d5173
--- /dev/null
+++ b/addons/mail/static/src/models/tracking_value_item.js
@@ -0,0 +1,100 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+
+import { format } from 'web.field_utils';
+
+registerModel({
+ name: 'TrackingValueItem',
+ identifyingFields: [['trackingValueAsNewValue','trackingValueAsOldValue']],
+ recordMethods: {
+ /**
+ * @private
+ * @returns {string}
+ */
+ _computeFormattedValue() {
+ /**
+ * Maps tracked field type to a JS formatter. Tracking values are
+ * not always stored in the same field type as their origin type.
+ * Field types that are not listed here are not supported by
+ * tracking in Python. Also see `create_tracking_values` in Python.
+ */
+ switch (this.fieldType) {
+ case 'boolean':
+ return this.value ? this.env._t("Yes") : this.env._t("No");
+ /**
+ * many2one formatter exists but is expecting id/name_get or data
+ * object but only the target record name is known in this context.
+ *
+ * Selection formatter exists but requires knowing all
+ * possibilities and they are not given in this context.
+ */
+ case 'char':
+ case 'many2one':
+ case 'selection':
+ return format.char(this.value);
+ case 'date':
+ if (this.value) {
+ return format.date(moment.utc(this.value));
+ }
+ return format.date(this.value);
+ case 'datetime':
+ if (this.value) {
+ return format.datetime(moment.utc(this.value));
+ }
+ return format.datetime(this.value);
+ case 'float':
+ return format.float(this.value);
+ case 'integer':
+ return format.integer(this.value);
+ case 'text':
+ return format.text(this.value);
+ case 'monetary':
+ return format.monetary(this.value, undefined, {
+ currency: this.currencyId
+ ? this.env.session.currencies[this.currencyId]
+ : undefined,
+ forceString: true,
+ });
+ default :
+ return this.value;
+ }
+ },
+ /**
+ * @private
+ * @returns {string}
+ */
+ _computeFormattedValueOrNone() {
+ return this.formattedValue || this.env._t("None");
+ },
+ },
+ fields: {
+ /**
+ * Used when the currency changes as the tracking value. This only makes sense for field of type monetary.
+ */
+ currencyId: attr(),
+ /**
+ * Indicates the type of the tracking value item.
+ * The supported types are: boolean, char, many2one, selection, date, datetime, float, integer, text, monetary.
+ */
+ fieldType: attr(),
+ formattedValue: attr({
+ compute: '_computeFormattedValue',
+ }),
+ formattedValueOrNone: attr({
+ compute: '_computeFormattedValueOrNone',
+ }),
+ trackingValueAsNewValue: one('TrackingValue', {
+ inverse: 'newValue',
+ readonly: true,
+ }),
+ trackingValueAsOldValue: one('TrackingValue', {
+ inverse: 'oldValue',
+ readonly: true,
+ }),
+ /**
+ * The original value of the tracking value item.
+ */
+ value: attr(),
+}});
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index 711c1f6cb5c..2ee7f80e99d 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -1515,13 +1515,7 @@ MockServer.include({
const trackingValueIds = this.getRecords('mail.tracking.value', [
['id', 'in', message.tracking_value_ids],
]);
- const formattedTrackingValues = trackingValueIds.map(tracking => ({
- changed_field: tracking.field_desc,
- field_type: tracking.field_type,
- id: tracking.id,
- new_value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new'),
- old_value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old'),
- }));
+ const formattedTrackingValues = [['insert-and-replace', this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds)]];
const partners = this.getRecords(
'res.partner',
[['id', 'in', message.partner_ids]],
@@ -1535,7 +1529,7 @@ MockServer.include({
parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
recipients: partners.map(p => ({ id: p.id, name: p.name })),
record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
- tracking_value_ids: formattedTrackingValues,
+ trackingValues: formattedTrackingValues,
});
if (message.subtype_id) {
const subtype = this.getRecords('mail.message.subtype', [
@@ -2087,7 +2081,25 @@ MockServer.include({
return false;
},
/**
- * Simulates `get_display_value` on `mail.tracking.value`
+ * Simulates `_tracking_value_format` on `mail.tracking.value`
+ */
+ _mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) {
+ const trackingValues = tracking_value_ids.map(tracking => ({
+ changedField: tracking.field_desc,
+ id: tracking.id,
+ newValue: [['insert-and-replace', {
+ fieldType: tracking.field_type,
+ value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new')
+ }]],
+ oldValue: [['insert-and-replace', {
+ fieldType: tracking.field_type,
+ value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old')
+ }]],
+ }));
+ return trackingValues;
+ },
+ /**
+ * Simulates `_get_display_value` on `mail.tracking.value`
*/
_mockMailTrackingValue_GetDisplayValue(record, type) {
switch (record.field_type) {
diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py
index c0c90e81ef1..30f54dd4b7b 100644
--- a/addons/test_mail/tests/test_message_track.py
+++ b/addons/test_mail/tests/test_message_track.py
@@ -346,8 +346,23 @@ class TestTrackingInternals(TestMailCommon):
msg_emp = self.record.message_ids.message_format()
msg_sudo = self.record.sudo().message_ids.message_format()
- self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
- self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values")
+ tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
+ formattedTrackingValues = [('insert-and-replace', [{
+ 'changedField': 'Email From',
+ 'id': tracking_values[0]['id'],
+ 'newValue': [('insert-and-replace', {
+ 'currencyId': False,
+ 'fieldType': 'char',
+ 'value': 'X',
+ })],
+ 'oldValue': [('insert-and-replace', {
+ 'currencyId': False,
+ 'fieldType': 'char',
+ 'value': False,
+ })],
+ }])]
+ self.assertEqual(msg_emp[0].get('trackingValues'), [('insert-and-replace', [])], "should not have protected tracking values")
+ self.assertEqual(msg_sudo[0].get('trackingValues'), formattedTrackingValues, "should have protected tracking values")
msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_context(self.record.message_ids, {})