[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:
@@ -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
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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."),
|
||||
|
||||
Reference in New Issue
Block a user