diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index cdd340ef551..1404c2f0633 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -210,7 +210,7 @@ class account_invoice(osv.osv):
('open','Open'),
('paid','Paid'),
('cancel','Cancelled'),
- ],'Status', select=True, readonly=True,
+ ],'Status', select=True, readonly=True, tracked=True,
help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Invoice. \
\n* The \'Pro-forma\' when invoice is in Pro-forma status,invoice does not have an invoice number. \
\n* The \'Open\' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice. \
@@ -278,7 +278,7 @@ class account_invoice(osv.osv):
help="Remaining amount due."),
'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
- 'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, states={'draft':[('readonly',False)]}),
+ 'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, tracked=True, states={'draft':[('readonly',False)]}),
'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
}
_defaults = {
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 8303f858b7b..ea7cc2ccc8a 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -224,7 +224,7 @@ class crm_lead(base_stage, format_address, osv.osv):
'date_action_next': fields.datetime('Next Action', readonly=1),
'email_from': fields.char('Email', size=128, help="Email address of the contact", select=1),
'section_id': fields.many2one('crm.case.section', 'Sales Team', \
- select=True, help='When sending mails, the default email address is taken from the sales team.'),
+ select=True, tracked=True, help='When sending mails, the default email address is taken from the sales team.'),
'create_date': fields.datetime('Creation Date' , readonly=True),
'email_cc': fields.text('Global CC', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'description': fields.text('Notes'),
@@ -240,9 +240,9 @@ class crm_lead(base_stage, format_address, osv.osv):
'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
'date_closed': fields.datetime('Closed', readonly=True),
- 'stage_id': fields.many2one('crm.case.stage', 'Stage',
+ 'stage_id': fields.many2one('crm.case.stage', 'Stage',tracked=True,
domain="['&', ('fold', '=', False), '&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
- 'user_id': fields.many2one('res.users', 'Salesperson'),
+ 'user_id': fields.many2one('res.users', 'Salesperson', tracked=True),
'referred': fields.char('Referred By', size=64),
'date_open': fields.datetime('Opened', readonly=True),
'day_open': fields.function(_compute_day, string='Days to Open', \
@@ -255,7 +255,7 @@ class crm_lead(base_stage, format_address, osv.osv):
# Only used for type opportunity
'probability': fields.float('Success Rate (%)',group_operator="avg"),
- 'planned_revenue': fields.float('Expected Revenue'),
+ 'planned_revenue': fields.float('Expected Revenue', tracked=True),
'ref': fields.reference('Reference', selection=crm._links_get, size=128),
'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
'phone': fields.char("Phone", size=64),
@@ -972,11 +972,6 @@ class crm_lead(base_stage, format_address, osv.osv):
# OpenChatter methods and notifications
# ----------------------------------------
- def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
- """ Override of the (void) default notification method. """
- stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
- return self.message_post(cr, uid, ids, body=_("Stage changed to %s.") % (stage_name), subtype="mt_crm_stage", context=context)
-
def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
if isinstance(lead, (int, long)):
lead = self.browse(cr, uid, [lead], context=context)[0]
@@ -988,14 +983,6 @@ class crm_lead(base_stage, format_address, osv.osv):
self.message_post(cr, uid, [id], body=message, context=context)
return True
- def case_mark_lost_send_note(self, cr, uid, ids, context=None):
- message = _("Opportunity has been lost.")
- return self.message_post(cr, uid, ids, body=message, subtype="mt_crm_lost", context=context)
-
- def case_mark_won_send_note(self, cr, uid, ids, context=None):
- message = _("Opportunity has been won.")
- return self.message_post(cr, uid, ids, body=message, subtype="mt_crm_won", context=context)
-
def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
if action == 'log': prefix = 'Logged'
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index b7541b6daf0..0f4c1e383b4 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -39,6 +39,7 @@
Negotiation
open
+ mail.mt_crm_stage
opportunity
@@ -47,6 +48,7 @@
Won
done
+ mail.mt_crm_won
@@ -57,6 +59,7 @@
cancel
+ mail.mt_crm_stage
lead
@@ -66,6 +69,7 @@
cancel
+ mail.mt_crm_lost
opportunity
diff --git a/addons/crm/res_partner.py b/addons/crm/res_partner.py
index bb0ad606a25..a1f84759493 100644
--- a/addons/crm/res_partner.py
+++ b/addons/crm/res_partner.py
@@ -39,7 +39,7 @@ class res_partner(osv.osv):
return res
_columns = {
- 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
+ 'section_id': fields.many2one('crm.case.section', 'Sales Team', tracked=True),
'opportunity_ids': fields.one2many('crm.lead', 'partner_id',\
'Leads and Opportunities', domain=[('state','in', ('draft','open','pending'))]),
'meeting_ids': fields.many2many('crm.meeting', 'crm_meeting_partner_rel','partner_id', 'meeting_id',
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index de1539da55a..489f218532e 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -186,7 +186,7 @@ class hr_applicant(base_stage, osv.Model):
'partner_id': fields.many2one('res.partner', 'Contact'),
'create_date': fields.datetime('Creation Date', readonly=True, select=True),
'write_date': fields.datetime('Update Date', readonly=True),
- 'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage',
+ 'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage', tracked=True,
domain="['&', ('fold', '=', False), '|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
'state': fields.related('stage_id', 'state', type="selection", store=True,
selection=AVAILABLE_STATES, string="Status", readonly=True,
@@ -197,7 +197,7 @@ class hr_applicant(base_stage, osv.Model):
set to \'Pending\'.'),
'categ_ids': fields.many2many('hr.applicant_category', string='Tags'),
'company_id': fields.many2one('res.company', 'Company'),
- 'user_id': fields.many2one('res.users', 'Responsible'),
+ 'user_id': fields.many2one('res.users', 'Responsible',tracked=True),
# Applicant Columns
'date_closed': fields.datetime('Closed', readonly=True, select=True),
'date_open': fields.datetime('Opened', readonly=True, select=True),
diff --git a/addons/mail/doc/tracked_field.rst b/addons/mail/doc/tracked_field.rst
new file mode 100644
index 00000000000..8d06ae1fa41
--- /dev/null
+++ b/addons/mail/doc/tracked_field.rst
@@ -0,0 +1,18 @@
+Tracked fields
+---------------
+Tracked fields is generic logging system for the changes applied to some fields in open chatter.
+ - In the definition of a fields. you have to simply add tracked=True.
+ - When changing one of the fields having tracked=True on saved automatically write a log in openchatter for the changes in fields.
+
+How it works:
++++++++++++++
+ - You have to add tracked=True in field defination as following.
+ - ``'stage_id': fields.many2one('project.task.type', 'Stage',tracked=True),``
+ - For developed this feature we override write method of mail_thread.
+ - And make one mako template which shows old field and updated field.
+
+Open chatter log:
++++++++++++++++++
+ - After changing field follower can show log of tracked field in open chatter as followed.
+ - Updated Fields:
+ - Stage: Analysis -> Specification
\ No newline at end of file
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 63043f992f3..0db7fbfc2db 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -20,19 +20,25 @@
##############################################################################
import base64
+from collections import defaultdict
import dateutil
import email
+from functools import partial
import logging
import pytz
import time
import tools
import xmlrpclib
+from mako.template import Template as MakoTemplate
+
from email.message import Message
from mail_message import decode
from openerp import SUPERUSER_ID
from osv import osv, fields
+from openerp.osv.orm import browse_record
from tools.safe_eval import safe_eval as eval
+from tools.translate import _
_logger = logging.getLogger(__name__)
@@ -68,6 +74,14 @@ class mail_thread(osv.AbstractModel):
_name = 'mail.thread'
_description = 'Email Thread'
_mail_flat_thread = True
+ _TRACK_TEMPLATE = """
+ ${updated_fields}
+
+ %for chg in changes:
+ - ${chg[0]}: ${chg[1]} -> ${chg[2]}
+ %endfor
+
+ """
def _get_message_data(self, cr, uid, ids, name, args, context=None):
""" Computes:
@@ -241,6 +255,87 @@ class mail_thread(osv.AbstractModel):
default['message_follower_ids'] = []
return super(mail_thread, self).copy(cr, uid, id, default=default, context=context)
+ #------------------------------------------------------
+ # Automatically log tracked fields
+ #------------------------------------------------------
+
+ def write(self, cr, uid, ids, values, context=None):
+
+ if context is None:
+ context = {}
+ #import pudb;pudb.set_trace()
+
+ def false_value(f):
+ if f._type == 'boolean':
+ return False
+ return f._symbol_set[1](False)
+
+ def convert_for_comparison(v, f):
+ # It will convert value for comparison between current and new.
+ if not v:
+ return false_value(f)
+ if isinstance(v, browse_record):
+ return v.id
+ return v
+
+ tracked = dict((n, f) for n, f in self._all_columns.items() if getattr(f.column, 'tracked', False))
+ to_log = [k for k in values if k in tracked]
+
+ from_ = None
+ changes = defaultdict(list)
+ if to_log:
+ for record in self.browse(cr, uid, ids, context):
+ for tl in to_log:
+ column = tracked[tl].column
+ current = convert_for_comparison(record[tl], column)
+ new = convert_for_comparison(values[tl], column)
+ if new != current:
+ changes[record].append(tl)
+ from_ = record[tl]
+
+ result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
+
+ updated_fields = _('Updated Fields:')
+
+ Trans = self.pool['ir.translation']
+ def _t(c):
+ # translate field
+ model = c.parent_model or self._name
+ lang = context.get('lang')
+ return Trans._get_source(cr, uid, '{0},{1}'.format(model, c.name), 'field', lang, ci.column.string)
+
+ def get_subtype(model, record):
+ # it will return subtype name(xml_id) for stage.
+ record_model = self.pool[model].browse(cr, SUPERUSER_ID, record)
+ if record_model.__hasattr__('subtype'):
+ return record_model.subtype
+ return False
+
+ for record, changed_fields in changes.items():
+ # TODO tpl changed_fields
+ chg = []
+ subtype = False
+ for f in changed_fields:
+ to = self.browse(cr, uid, ids[0], context)[f]
+ ci = tracked[f]
+ if ci.column._type == "many2one":
+ if to:
+ to = to.name_get()[0][1]
+ else:
+ to = "Removed"
+ if isinstance(from_, browse_record):
+ from_ = from_.name_get()[0][1]
+
+ subtype = get_subtype(ci.column._obj,values[f])
+ chg.append((_t(ci), from_, to))
+
+ message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(updated_fields=updated_fields,
+ changes=chg)
+
+ record.message_post(message,subtype=subtype)
+
+ return result
+
#------------------------------------------------------
# mail.message wrappers and tools
#------------------------------------------------------
diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py
index b97c9a4af94..3a8a5ff767c 100644
--- a/addons/mail/tests/test_mail_features.py
+++ b/addons/mail/tests/test_mail_features.py
@@ -586,3 +586,31 @@ class test_mail(TestMailBase):
na_demo_group = self.mail_message._needaction_count(cr, user_raoul.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction')
self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs')
+
+ def test_40_track_field(self):
+ """ Test designed for tracking field. """
+
+ cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
+ partner_user_id = self.res_partner._columns.get('user_id')
+
+ # Set tracked attribute as True for tracked field.
+ partner_user_id.tracked = True
+ p_d_id = self.res_partner.create(cr, uid, {'name': 'Osbert Armour', 'notification_email_send': 'all'})
+
+ #Set Raoul as sales person.
+ self.res_partner.write(cr, uid, [p_d_id], {'user_id' : user_raoul.id})
+ mail_ids = self.mail_message.search(cr, uid, [('model', '=', 'res.partner'),('res_id','=',p_d_id)])
+
+ #Test: tracked record logged in openchatter
+ self.assertEqual(1,len(mail_ids), ' After change in field logged in openchatter.')
+
+ # Set tracked attribute as False for tracked field.
+ partner_user_id.tracked = False
+ p_a_id = self.res_partner.create(cr, uid, {'name': 'Timmy Simons', 'notification_email_send': 'all'})
+
+ #Set Admin as sales person.
+ self.res_partner.write(cr, uid, [p_a_id], {'user_id' : user_admin.id})
+ mail_a_ids = self.mail_message.search(cr, uid, [('model', '=', 'res.partner'),('res_id','=',p_a_id)])
+
+ #Test: tracked record logged in openchatter.
+ self.assertEqual(len(mail_a_ids),0,' No more logged openchatter.')
diff --git a/addons/project/project.py b/addons/project/project.py
index 2e5a990bc5c..a3a4567800f 100644
--- a/addons/project/project.py
+++ b/addons/project/project.py
@@ -742,7 +742,7 @@ class task(base_stage, osv.osv):
'description': fields.text('Description'),
'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
- 'stage_id': fields.many2one('project.task.type', 'Stage',
+ 'stage_id': fields.many2one('project.task.type', 'Stage', tracked=True,
domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
'state': fields.related('stage_id', 'state', type="selection", store=True,
selection=_TASK_STATE, string="Status", readonly=True,
@@ -762,7 +762,7 @@ class task(base_stage, osv.osv):
'date_start': fields.datetime('Starting Date',select=True),
'date_end': fields.datetime('Ending Date',select=True),
'date_deadline': fields.date('Deadline',select=True),
- 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
+ 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1", tracked=True),
'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
'notes': fields.text('Notes'),
@@ -788,7 +788,7 @@ class task(base_stage, osv.osv):
'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
'project.task.work': (_get_task, ['hours'], 10),
}),
- 'user_id': fields.many2one('res.users', 'Assigned to'),
+ 'user_id': fields.many2one('res.users', 'Assigned to', tracked=True),
'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
'partner_id': fields.many2one('res.partner', 'Customer'),
'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
@@ -1060,7 +1060,6 @@ class task(base_stage, osv.osv):
self.do_pending(cr, uid, [task.id], context=context)
elif delegate_data['state'] == 'done':
self.do_close(cr, uid, [task.id], context=context)
- self.do_delegation_send_note(cr, uid, [task.id], context)
delegated_tasks[task.id] = delegated_task_id
return delegated_tasks
@@ -1140,7 +1139,6 @@ class task(base_stage, osv.osv):
self._subscribe_project_followers_to_task(cr, uid, task_id, context=context)
self._store_history(cr, uid, [task_id], context=context)
- self.create_send_note(cr, uid, [task_id], context=context)
return task_id
# Overridden to reset the kanban_state to normal whenever
@@ -1158,7 +1156,6 @@ class task(base_stage, osv.osv):
#raise osv.except_osv(_('Warning!'), _('Stage is not defined in the project.'))
write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
super(task, self).write(cr, uid, [t.id], write_vals, context=context)
- self.stage_set_send_note(cr, uid, [t.id], new_stage, context=context)
result = True
else:
result = super(task, self).write(cr, uid, ids, vals, context=context)
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py
index 3168e253fe8..ced3ac748b6 100644
--- a/addons/project_issue/project_issue.py
+++ b/addons/project_issue/project_issue.py
@@ -245,14 +245,14 @@ class project_issue(base_stage, osv.osv):
'version_id': fields.many2one('project.issue.version', 'Version'),
'stage_id': fields.many2one ('project.task.type', 'Stage',
domain="['&', ('fold', '=', False), ('project_ids', '=', project_id)]"),
- 'project_id':fields.many2one('project.project', 'Project'),
+ 'project_id':fields.many2one('project.project', 'Project', tracked=True),
'duration': fields.float('Duration'),
'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"),
'day_open': fields.function(_compute_day, string='Days to Open', \
multi='compute_day', type="float", store=True),
'day_close': fields.function(_compute_day, string='Days to Close', \
multi='compute_day', type="float", store=True),
- 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
+ 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1, tracked=True),
'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
multi='compute_day', type="float", store=True),
'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 6f4d38685a6..de11a3acce9 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -187,7 +187,7 @@ class sale_order(osv.osv):
('manual', 'Sale to Invoice'),
('invoice_except', 'Invoice Exception'),
('done', 'Done'),
- ], 'Status', readonly=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
+ ], 'Status', readonly=True, tracked=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),