[MERGE] Merged lp:~openerp-dev/openobject-addons/trunk-tracked-fields-chs, with some selective choices.

bzr revid: tde@openerp.com-20121217095510-8upbg071esk5hlk1
This commit is contained in:
Thibault Delavallée
2012-12-17 10:55:10 +01:00
11 changed files with 160 additions and 31 deletions
+2 -2
View File
@@ -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 = {
+4 -17
View File
@@ -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 <b>%s</b>.") % (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 <b>lost</b>.")
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 <b>won</b>.")
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'
+4
View File
@@ -39,6 +39,7 @@
<field name="name">Negotiation</field>
<field eval="1" name="case_default"/>
<field name="state">open</field>
<field name="subtype">mail.mt_crm_stage</field>
<field eval="'60'" name="probability"/>
<field eval="'14'" name="sequence"/>
<field name="type">opportunity</field>
@@ -47,6 +48,7 @@
<field name="name">Won</field>
<field eval="1" name="case_default"/>
<field name="state">done</field>
<field name="subtype">mail.mt_crm_won</field>
<field eval="'100'" name="probability"/>
<field eval="'15'" name="sequence"/>
<field eval="1" name="on_change"/>
@@ -57,6 +59,7 @@
<field eval="1" name="case_default"/>
<field eval="False" name="fold"/>
<field name="state">cancel</field>
<field name="subtype">mail.mt_crm_stage</field>
<field eval="'0'" name="probability"/>
<field eval="'16'" name="sequence"/>
<field name="type">lead</field>
@@ -66,6 +69,7 @@
<field eval="1" name="case_default"/>
<field eval="True" name="fold"/>
<field name="state">cancel</field>
<field name="subtype">mail.mt_crm_lost</field>
<field eval="'0'" name="probability"/>
<field eval="'17'" name="sequence"/>
<field name="type">opportunity</field>
+1 -1
View File
@@ -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',
+2 -2
View File
@@ -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),
+18
View File
@@ -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
+95
View File
@@ -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 = """
<span>${updated_fields}</span>
<ul>
%for chg in changes:
<li><span>${chg[0]}</span>: ${chg[1]} -> ${chg[2]}</li>
%endfor
</ul>
"""
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
#------------------------------------------------------
+28
View File
@@ -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.')
+3 -6
View File
@@ -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)
+2 -2
View File
@@ -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', \
+1 -1
View File
@@ -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."),