mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 16:52:02 +02:00
315 lines
14 KiB
Python
315 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class MailTrackingValue(models.Model):
|
|
_name = 'mail.tracking.value'
|
|
_description = 'Mail Tracking Value'
|
|
_rec_name = 'field_id'
|
|
_order = 'id DESC'
|
|
|
|
field_id = fields.Many2one(
|
|
'ir.model.fields', required=False, readonly=True,
|
|
index=True, ondelete='set null')
|
|
field_info = fields.Json('Removed field information')
|
|
|
|
old_value_integer = fields.Integer('Old Value Integer', readonly=True)
|
|
old_value_float = fields.Float('Old Value Float', readonly=True)
|
|
old_value_char = fields.Char('Old Value Char', readonly=True)
|
|
old_value_text = fields.Text('Old Value Text', readonly=True)
|
|
old_value_datetime = fields.Datetime('Old Value DateTime', readonly=True)
|
|
|
|
new_value_integer = fields.Integer('New Value Integer', readonly=True)
|
|
new_value_float = fields.Float('New Value Float', readonly=True)
|
|
new_value_char = fields.Char('New Value Char', readonly=True)
|
|
new_value_text = fields.Text('New Value Text', readonly=True)
|
|
new_value_datetime = fields.Datetime('New Value Datetime', readonly=True)
|
|
|
|
currency_id = fields.Many2one('res.currency', 'Currency', readonly=True, ondelete='set null',
|
|
help="Used to display the currency when tracking monetary values")
|
|
|
|
mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, index=True, ondelete='cascade')
|
|
|
|
def _filter_has_field_access(self, env):
|
|
""" Return the subset of self for which the user in env has access. As
|
|
this model is admin-only, it is generally accessed as sudo and we need
|
|
to distinguish context environment from tracking values environment.
|
|
|
|
If tracking is linked to a field, user should have access to the field.
|
|
Otherwise only members of "base.group_system" can access it. """
|
|
|
|
def has_field_access(tracking):
|
|
if not tracking.field_id:
|
|
return env.is_system()
|
|
model = env[tracking.field_id.model]
|
|
model_field = model._fields.get(tracking.field_id.name)
|
|
return model._has_field_access(model_field, 'read') if model_field else False
|
|
|
|
return self.filtered(has_field_access)
|
|
|
|
def _filter_free_field_access(self):
|
|
""" Return the subset of self which is available for all users: trackings
|
|
linked to an existing field without access group. It is used notably
|
|
when sending tracking summary through notifications. """
|
|
|
|
def has_free_access(tracking):
|
|
if not tracking.field_id:
|
|
return False
|
|
model_field = self.env[tracking.field_id.model]._fields.get(tracking.field_id.name)
|
|
return model_field and not model_field.groups
|
|
|
|
return self.filtered(has_free_access)
|
|
|
|
@api.model
|
|
def _create_tracking_values(self, initial_value, new_value, col_name, col_info, record):
|
|
""" Prepare values to create a mail.tracking.value. It prepares old and
|
|
new value according to the field type.
|
|
|
|
:param initial_value: field value before the change, could be text, int,
|
|
date, datetime, ...;
|
|
:param new_value: field value after the change, could be text, int,
|
|
date, datetime, ...;
|
|
:param str col_name: technical field name, column name (e.g. 'user_id);
|
|
:param dict col_info: result of fields_get(col_name);
|
|
:param <record> record: record on which tracking is performed, used for
|
|
related computation e.g. finding currency of monetary fields;
|
|
|
|
:return: a dict values valid for 'mail.tracking.value' creation;
|
|
"""
|
|
field = self.env['ir.model.fields']._get(record._name, col_name)
|
|
if not field:
|
|
raise ValueError(f'Unknown field {col_name} on model {record._name}')
|
|
|
|
values = {'field_id': field.id}
|
|
|
|
if col_info['type'] in {'integer', 'float', 'char', 'text', 'datetime'}:
|
|
values.update({
|
|
f'old_value_{col_info["type"]}': initial_value,
|
|
f'new_value_{col_info["type"]}': new_value
|
|
})
|
|
elif col_info['type'] == 'monetary':
|
|
values.update({
|
|
'currency_id': record[col_info['currency_field']].id,
|
|
'old_value_float': initial_value,
|
|
'new_value_float': new_value
|
|
})
|
|
elif col_info['type'] == 'date':
|
|
values.update({
|
|
'old_value_datetime': initial_value and fields.Datetime.to_string(datetime.combine(fields.Date.from_string(initial_value), datetime.min.time())) or False,
|
|
'new_value_datetime': new_value and fields.Datetime.to_string(datetime.combine(fields.Date.from_string(new_value), datetime.min.time())) or False,
|
|
})
|
|
elif col_info['type'] == 'boolean':
|
|
values.update({
|
|
'old_value_integer': initial_value,
|
|
'new_value_integer': new_value
|
|
})
|
|
elif col_info['type'] == 'selection':
|
|
values.update({
|
|
'old_value_char': initial_value and dict(col_info['selection']).get(initial_value, initial_value) or '',
|
|
'new_value_char': new_value and dict(col_info['selection'])[new_value] or ''
|
|
})
|
|
elif col_info['type'] == 'many2one':
|
|
# Can be:
|
|
# - False value
|
|
# - recordset, in case of standard field
|
|
# - (id, display name), in case of properties (read format)
|
|
if not initial_value:
|
|
initial_value = (0, '')
|
|
elif isinstance(initial_value, models.BaseModel):
|
|
initial_value = (initial_value.id, initial_value.display_name)
|
|
|
|
if not new_value:
|
|
new_value = (0, '')
|
|
elif isinstance(new_value, models.BaseModel):
|
|
new_value = (new_value.id, new_value.display_name)
|
|
|
|
values.update({
|
|
'old_value_integer': initial_value[0],
|
|
'new_value_integer': new_value[0],
|
|
'old_value_char': initial_value[1],
|
|
'new_value_char': new_value[1]
|
|
})
|
|
elif col_info['type'] in {'one2many', 'many2many', 'tags'}:
|
|
# Can be:
|
|
# - False value
|
|
# - recordset, in case of standard field
|
|
# - [(id, display name), ...], in case of properties (read format)
|
|
model_name = self.env['ir.model']._get(field.relation).display_name
|
|
if not initial_value:
|
|
old_value_char = ''
|
|
elif isinstance(initial_value, models.BaseModel):
|
|
old_value_char = ', '.join(
|
|
value.display_name or self.env._(
|
|
'Unnamed %(record_model_name)s (%(record_id)s)',
|
|
record_model_name=model_name, record_id=value.id
|
|
)
|
|
for value in initial_value
|
|
)
|
|
else:
|
|
old_value_char = ', '.join(value[1] for value in initial_value)
|
|
if not new_value:
|
|
new_value_char = ''
|
|
elif isinstance(new_value, models.BaseModel):
|
|
new_value_char = ', '.join(
|
|
value.display_name or self.env._(
|
|
'Unnamed %(record_model_name)s (%(record_id)s)',
|
|
record_model_name=model_name, record_id=value.id
|
|
)
|
|
for value in new_value
|
|
)
|
|
else:
|
|
new_value_char = ', '.join(value[1] for value in new_value)
|
|
|
|
values.update({
|
|
'old_value_char': old_value_char,
|
|
'new_value_char': new_value_char,
|
|
})
|
|
else:
|
|
raise NotImplementedError(f'Unsupported tracking on field {field.name} (type {col_info["type"]}')
|
|
|
|
return values
|
|
|
|
@api.model
|
|
def _create_tracking_values_property(self, initial_value, col_name, col_info, record):
|
|
"""Generate the values for the <mail.tracking.values> corresponding to a property."""
|
|
col_info = col_info | {'type': initial_value['type'], 'selection': initial_value.get('selection')}
|
|
|
|
field_info = {
|
|
'desc': f"{col_info['string']}: {initial_value['string']}",
|
|
'name': col_name,
|
|
'type': initial_value['type'],
|
|
}
|
|
value = initial_value.get('value', False)
|
|
if value and initial_value['type'] == 'tags':
|
|
value = [t for t in initial_value.get('tags', []) if t[0] in value]
|
|
|
|
tracking_values = self.env['mail.tracking.value']._create_tracking_values(
|
|
value, False, col_name, col_info, record)
|
|
return {**tracking_values, 'field_info': field_info}
|
|
|
|
def _tracking_value_format(self):
|
|
""" Return structure and formatted data structure to be used by chatter
|
|
to display tracking values. Order it according to asked display, aka
|
|
ascending sequence (and field name).
|
|
|
|
:return: for each tracking value in self, their formatted display
|
|
values given as a dict;
|
|
:rtype: list[dict]
|
|
"""
|
|
model_map = {}
|
|
for tracking in self:
|
|
model = tracking.field_id.model or tracking.mail_message_id.model
|
|
model_map.setdefault(model, self.browse())
|
|
model_map[model] += tracking
|
|
formatted = []
|
|
for model, trackings in model_map.items():
|
|
formatted += trackings._tracking_value_format_model(model)
|
|
return formatted
|
|
|
|
def _tracking_value_format_model(self, model):
|
|
""" Return structure and formatted data structure to be used by chatter
|
|
to display tracking values. Order it according to asked display, aka
|
|
ascending sequence (and field name).
|
|
|
|
:returns: for each tracking value in self, their formatted display
|
|
values given as a dict;
|
|
:rtype: list[dict]
|
|
"""
|
|
if not self:
|
|
return []
|
|
|
|
# fetch model-based information
|
|
if model:
|
|
TrackedModel = self.env[model]
|
|
tracked_fields = TrackedModel.fields_get(self.field_id.mapped('name'), attributes={'digits', 'string', 'type'})
|
|
model_sequence_info = dict(TrackedModel._mail_track_order_fields(tracked_fields)) if model else {}
|
|
else:
|
|
tracked_fields, model_sequence_info = {}, {}
|
|
|
|
# generate sequence of trackings
|
|
fields_sequence_map = dict(
|
|
{
|
|
tracking.field_info['name']: tracking.field_info.get('sequence', 100)
|
|
for tracking in self.filtered('field_info')
|
|
},
|
|
**model_sequence_info,
|
|
)
|
|
# generate dict of field information, if available
|
|
fields_col_info = (
|
|
tracking.field_id.ttype != 'properties'
|
|
and tracked_fields.get(tracking.field_id.name)
|
|
or {
|
|
'string': tracking.field_info['desc'] if tracking.field_info else self.env._('Unknown'),
|
|
'type': tracking.field_info['type'] if tracking.field_info else 'char',
|
|
} for tracking in self
|
|
)
|
|
|
|
def sort_tracking_info(tracking_info_tuple):
|
|
tracking = tracking_info_tuple[0]
|
|
field_name = tracking.field_id.name or (tracking.field_info['name'] if tracking.field_info else 'unknown')
|
|
return (
|
|
fields_sequence_map.get(field_name, 100),
|
|
tracking.field_id.ttype == 'properties',
|
|
field_name,
|
|
)
|
|
|
|
formatted = [
|
|
{
|
|
'id': tracking.id,
|
|
'fieldInfo': {
|
|
'changedField': col_info['string'],
|
|
'currencyId': tracking.currency_id.id,
|
|
'floatPrecision': col_info.get('digits'),
|
|
'fieldType': col_info['type'],
|
|
'isPropertyField': tracking.field_id.ttype == 'properties',
|
|
},
|
|
'newValue': tracking._format_display_value(col_info['type'], new=True)[0],
|
|
'oldValue': tracking._format_display_value(col_info['type'], new=False)[0],
|
|
}
|
|
for tracking, col_info in sorted(zip(self, fields_col_info), key=sort_tracking_info)
|
|
]
|
|
return formatted
|
|
|
|
def _format_display_value(self, field_type, new=True):
|
|
""" Format value of 'mail.tracking.value', according to the field type.
|
|
|
|
:param str field_type: Odoo field type;
|
|
:param bool new: if True, display the 'new' value. Otherwise display
|
|
the 'old' one.
|
|
"""
|
|
field_mapping = {
|
|
'boolean': ('old_value_integer', 'new_value_integer'),
|
|
'date': ('old_value_datetime', 'new_value_datetime'),
|
|
'datetime': ('old_value_datetime', 'new_value_datetime'),
|
|
'char': ('old_value_char', 'new_value_char'),
|
|
'float': ('old_value_float', 'new_value_float'),
|
|
'integer': ('old_value_integer', 'new_value_integer'),
|
|
'monetary': ('old_value_float', 'new_value_float'),
|
|
'text': ('old_value_text', 'new_value_text'),
|
|
}
|
|
|
|
result = []
|
|
for record in self:
|
|
value_fname = field_mapping.get(
|
|
field_type, ('old_value_char', 'new_value_char')
|
|
)[bool(new)]
|
|
value = record[value_fname]
|
|
|
|
if field_type in {'integer', 'float', 'char', 'text', 'monetary'}:
|
|
result.append(value)
|
|
elif field_type in {'date', 'datetime'}:
|
|
if not record[value_fname]:
|
|
result.append(value)
|
|
elif field_type == 'date':
|
|
result.append(fields.Date.to_string(value))
|
|
else:
|
|
result.append(f'{value}Z')
|
|
elif field_type == 'boolean':
|
|
result.append(bool(value))
|
|
else:
|
|
result.append(value)
|
|
return result
|