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 -
  • +
    + + + + +
    - -
    \ 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, {})