From a4eb1112c21d500889e272a9a0ab999980bdaf69 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 21 Nov 2012 10:58:31 +0100 Subject: [PATCH 01/15] [IMP] mail: models inherting from mail.thread can now flag their fields as 'tracked'. Changed tracked fields are logged automatically in the thread. (work in progess) bzr revid: chs@openerp.com-20121121095831-uto0g3e6f2713828 --- addons/mail/mail_thread.py | 92 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 15bc53223ba..a90dc9b3a2b 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} + + """ def _get_message_data(self, cr, uid, ids, name, args, context=None): """ Computes: @@ -249,6 +263,84 @@ 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): + # XXX is translation a good idea? + # TODO get user lang if not in context + # XXX idea: store a datastructure and render it on demand in the correct language + # TODO? use link to record for m2o + # TODO handle x2m fields. how? + + 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): + if not v: + return false_value(f) + if isinstance(v, browse_record): + return v.id + return v + + def convert_for_display(v, f): + if not v: + return false_value(f) + if f._type == 'many2one': + if not isinstance(v, browse_record): + v = self.pool[f.relation].browse(cr, SUPERUSER_ID, v) + return v.name_get()[0][1] + if f._type == 'selection': + # TODO get translated value + pass + 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] + + 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) + + result = super(mail_thread, self).write(cr, uid, ids, values, context=context) + + updated_fields = _('Updated Fields:') + + Trans = self.pool['ir.translation'] + def _t(c): + 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) + + for record, changed_fields in changes.items(): + # TODO tpl changed_fields + chg = [] + for f in changed_fields: + ci = tracked[f] + from_ = convert_for_display(record[f], ci.column) + to = convert_for_display(values[f], ci.column) + chg.append((_t(ci), from_, to)) + + message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(updated_fields=updated_fields, + changes=chg) + + record.message_post(message) + + return result + #------------------------------------------------------ # mail.message wrappers and tools #------------------------------------------------------ From 6e1e4f084bc4ee04bba1cd03ea805eda6e00c42a Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Thu, 22 Nov 2012 10:47:59 +0530 Subject: [PATCH 02/15] [IMP]improve code for tracking field and add code for tracked it bzr revid: sgo@tinyerp.com-20121122051759-lks3ykule5w5om6x --- addons/crm/crm_lead.py | 8 ++++---- addons/mail/mail_thread.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 0a824aef83e..d3795c39d34 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -197,7 +197,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'), @@ -213,9 +213,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', \ @@ -228,7 +228,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), diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index a90dc9b3a2b..4d98ecbdc97 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -295,7 +295,7 @@ class mail_thread(osv.AbstractModel): return false_value(f) if f._type == 'many2one': if not isinstance(v, browse_record): - v = self.pool[f.relation].browse(cr, SUPERUSER_ID, v) + v = self.pool[f._obj].browse(cr, SUPERUSER_ID, v) return v.name_get()[0][1] if f._type == 'selection': # TODO get translated value From b2be129ead052721c56f22d28b2edced404060af Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Thu, 22 Nov 2012 10:56:35 +0530 Subject: [PATCH 03/15] [IMP]tracked project task fields bzr revid: sgo@tinyerp.com-20121122052635-vutkm8h23gc6t171 --- addons/project/project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index 68122d1a614..7beef9e1050 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -749,7 +749,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), ('case_default', '=', True)]"), 'state': fields.related('stage_id', 'state', type="selection", store=True, selection=_TASK_STATE, string="Status", readonly=True, @@ -769,7 +769,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'), @@ -795,7 +795,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'), From 104bf4c64b4430de0ceab7f57413602313314a55 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Thu, 22 Nov 2012 11:00:37 +0530 Subject: [PATCH 04/15] [IMP]tracked hr recruitment fields bzr revid: sgo@tinyerp.com-20121122053037-vt1pkzajh8yqmzlu --- addons/hr_recruitment/hr_recruitment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index de1539da55a..a6c62265206 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), From 0afceb9dbd1dd38965b252fa3a2a2a47d807fc15 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Thu, 22 Nov 2012 11:50:23 +0530 Subject: [PATCH 05/15] [IMP]tracked sale project_issue and invoice fields bzr revid: sgo@tinyerp.com-20121122062023-wzj8p4s0yj9gntep --- addons/account/account_invoice.py | 4 ++-- addons/project_issue/project_issue.py | 4 ++-- addons/sale/sale.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 35afac5a508..0d558afbbbc 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -207,7 +207,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. \ @@ -275,7 +275,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/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 90640363906..7c6b6efec2c 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), ('case_default', '=', True)]"), - '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 3c568cc8227..4ee0c05b510 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."), From 36710694fe28260d814cc651e60fba9acfca79ea Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Thu, 22 Nov 2012 12:17:20 +0530 Subject: [PATCH 06/15] [IMP]tracked res partner fields bzr revid: sgo@tinyerp.com-20121122064720-e8ohgrvas0iazvdi --- addons/crm/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm/res_partner.py b/addons/crm/res_partner.py index e43534476fd..5304569302c 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', From ccd78ad74520f706bbfda7d8889158172a2959a9 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Mon, 26 Nov 2012 11:25:41 +0530 Subject: [PATCH 07/15] [IMP] remove message post for stage change and implement subtype on changed stage bzr revid: sgo@tinyerp.com-20121126055541-nh8wjs35wviiybks --- addons/crm/crm.py | 1 + addons/crm/crm_lead.py | 15 --------------- addons/crm/crm_lead_data.xml | 8 ++++++++ addons/crm/crm_view.xml | 1 + addons/mail/mail_thread.py | 9 ++++++--- addons/project/project.py | 26 ++------------------------ addons/project/project_data.xml | 9 +++++++++ addons/project/project_view.xml | 1 + 8 files changed, 28 insertions(+), 42 deletions(-) diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 368656224ac..d53f416a8eb 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -78,6 +78,7 @@ class crm_case_stage(osv.osv): 'state': fields.selection(AVAILABLE_STATES, 'Related Status', required=True, help="The status of your document will automatically change regarding the selected stage. " \ "For example, if a stage is related to the status 'Close', when your document reaches this stage, it is automatically closed."), + 'subtype': fields.char('Related Subtype', size=64), 'case_default': fields.boolean('Common to All Teams', help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."), 'fold': fields.boolean('Hide in Views when Empty', diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index d3795c39d34..4ee26fde712 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -382,7 +382,6 @@ class crm_lead(base_stage, format_address, osv.osv): stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context) if stage_id: self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context) - self.case_mark_lost_send_note(cr, uid, ids, context=context) return True def case_mark_won(self, cr, uid, ids, context=None): @@ -391,7 +390,6 @@ class crm_lead(base_stage, format_address, osv.osv): stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context) if stage_id: self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context) - self.case_mark_won_send_note(cr, uid, ids, context=context) return True def set_priority(self, cr, uid, ids, priority): @@ -843,11 +841,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] @@ -859,14 +852,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..5490f590855 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -7,6 +7,7 @@ New draft + mail.mt_crm_stage both @@ -15,6 +16,7 @@ Opportunity open + mail.mt_crm_stage lead @@ -23,6 +25,7 @@ Qualification open + mail.mt_crm_stage opportunity @@ -31,6 +34,7 @@ Proposition open + mail.mt_crm_stage opportunity @@ -39,6 +43,7 @@ Negotiation open + mail.mt_crm_stage opportunity @@ -47,6 +52,7 @@ Won done + mail.mt_crm_won @@ -57,6 +63,7 @@ cancel + mail.mt_crm_stage lead @@ -66,6 +73,7 @@ cancel + mail.mt_crm_lost opportunity diff --git a/addons/crm/crm_view.xml b/addons/crm/crm_view.xml index 99d3752529e..d6731f8f0bd 100644 --- a/addons/crm/crm_view.xml +++ b/addons/crm/crm_view.xml @@ -203,6 +203,7 @@ + diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 4d98ecbdc97..cc19e9d6007 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -296,7 +296,7 @@ class mail_thread(osv.AbstractModel): if f._type == 'many2one': if not isinstance(v, browse_record): v = self.pool[f._obj].browse(cr, SUPERUSER_ID, v) - return v.name_get()[0][1] + return v if f._type == 'selection': # TODO get translated value pass @@ -327,17 +327,20 @@ class mail_thread(osv.AbstractModel): for record, changed_fields in changes.items(): # TODO tpl changed_fields + subtype = False chg = [] for f in changed_fields: ci = tracked[f] from_ = convert_for_display(record[f], ci.column) to = convert_for_display(values[f], ci.column) - chg.append((_t(ci), from_, to)) + if to.__hasattr__('subtype'): + subtype = to.subtype + chg.append((_t(ci), from_.name_get()[0][1], to.name_get()[0][1])) message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(updated_fields=updated_fields, changes=chg) - record.message_post(message) + record.message_post(message,subtype=subtype) return result diff --git a/addons/project/project.py b/addons/project/project.py index 7beef9e1050..e8e22e5fb09 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -46,6 +46,7 @@ class project_task_type(osv.osv): 'state': fields.selection(_TASK_STATE, 'Related Status', required=True, help="The status of your document is automatically changed regarding the selected stage. " \ "For example, if a stage is related to the status 'Close', when your document reaches this stage, it is automatically closed."), + 'subtype': fields.char('Related Subtype', size=64), 'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."), } @@ -993,14 +994,12 @@ class task(base_stage, osv.osv): if not task.date_end: vals['date_end'] = fields.datetime.now() self.case_set(cr, uid, [task.id], 'done', vals, context=context) - self.case_close_send_note(cr, uid, [task.id], context=context) return True def do_reopen(self, cr, uid, ids, context=None): for task in self.browse(cr, uid, ids, context=context): project = task.project_id self.case_set(cr, uid, [task.id], 'open', {}, context=context) - self.case_open_send_note(cr, uid, [task.id], context) return True def do_cancel(self, cr, uid, ids, context=None): @@ -1012,7 +1011,6 @@ class task(base_stage, osv.osv): self._check_child_task(cr, uid, ids, context=context) for task in tasks: self.case_set(cr, uid, [task.id], 'cancelled', {'remaining_hours': 0.0}, context=context) - self.case_cancel_send_note(cr, uid, [task.id], context=context) return True def do_open(self, cr, uid, ids, context=None): @@ -1022,7 +1020,6 @@ class task(base_stage, osv.osv): def case_open(self, cr, uid, ids, context=None): if not isinstance(ids,list): ids = [ids] self.case_set(cr, uid, ids, 'open', {'date_start': fields.datetime.now()}, context=context) - self.case_open_send_note(cr, uid, ids, context) return True def do_draft(self, cr, uid, ids, context=None): @@ -1031,7 +1028,6 @@ class task(base_stage, osv.osv): def case_draft(self, cr, uid, ids, context=None): self.case_set(cr, uid, ids, 'draft', {}, context=context) - self.case_draft_send_note(cr, uid, ids, context=context) return True def do_pending(self, cr, uid, ids, context=None): @@ -1039,8 +1035,7 @@ class task(base_stage, osv.osv): return self.case_pending(cr, uid, ids, context=context) def case_pending(self, cr, uid, ids, context=None): - self.case_set(cr, uid, ids, 'pending', {}, context=context) - return self.case_pending_send_note(cr, uid, ids, context=context) + return self.case_set(cr, uid, ids, 'pending', {}, context=context) def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None): attachment = self.pool.get('ir.attachment') @@ -1160,7 +1155,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 @@ -1288,24 +1282,8 @@ class task(base_stage, osv.osv): res = super(task, self).message_get_monitored_follower_fields(cr, uid, ids, context=context) return res + ['user_id', 'manager_id'] - 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('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1] - return self.message_post(cr, uid, ids, body=_("Stage changed to %s.") % (stage_name), - context=context) - - def create_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body=_("Task has been created."), context=context) - - def case_draft_send_note(self, cr, uid, ids, context=None): return self.message_post(cr, uid, ids, body=_('Task has been set as draft.'), context=context) - def do_delegation_send_note(self, cr, uid, ids, context=None): - for task in self.browse(cr, uid, ids, context=context): - msg = _('Task has been delegated to %s.') % (task.user_id.name) - self.message_post(cr, uid, [task.id], body=msg, context=context) - return True - def project_task_reevaluate(self, cr, uid, ids, context=None): if self.pool.get('res.users').has_group(cr, uid, 'project.group_time_work_estimation_tasks'): return { diff --git a/addons/project/project_data.xml b/addons/project/project_data.xml index 08981bd126a..ee88595956a 100644 --- a/addons/project/project_data.xml +++ b/addons/project/project_data.xml @@ -30,30 +30,35 @@ 1 Analysis draft + project.mt_task_new 2 Specification draft + project.mt_task_change 2 Design open + project.mt_task_change 3 Negotiation pending + project.mt_task_change 4 Development + project.mt_task_change open @@ -61,12 +66,14 @@ 5 Testing open + project.mt_task_change 6 Merge open + project.mt_task_change @@ -74,12 +81,14 @@ 7 Done done + project.mt_task_closed 7 Cancelled cancelled + project.mt_task_canceled diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 8bb45ca6738..df2188be6b6 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -750,6 +750,7 @@ + From 9adb2e1eb142ff40ad3763836446c2165ae72001 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Mon, 26 Nov 2012 18:27:33 +0530 Subject: [PATCH 08/15] [IMP]improve code bzr revid: sgo@tinyerp.com-20121126125733-av712p9bixk20ew9 --- addons/mail/mail_thread.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index cc19e9d6007..e78e2574e49 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -291,16 +291,19 @@ class mail_thread(osv.AbstractModel): return v def convert_for_display(v, f): + subtype = False if not v: return false_value(f) if f._type == 'many2one': if not isinstance(v, browse_record): v = self.pool[f._obj].browse(cr, SUPERUSER_ID, v) - return v + if v.__hasattr__('subtype'): + subtype = v.subtype + return (v.name_get()[0][1],subtype) if f._type == 'selection': # TODO get translated value pass - return v + return (v,subtype) 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] @@ -327,20 +330,17 @@ class mail_thread(osv.AbstractModel): for record, changed_fields in changes.items(): # TODO tpl changed_fields - subtype = False chg = [] for f in changed_fields: ci = tracked[f] - from_ = convert_for_display(record[f], ci.column) + from_ = convert_for_display(record[f], ci.column)[0] to = convert_for_display(values[f], ci.column) - if to.__hasattr__('subtype'): - subtype = to.subtype - chg.append((_t(ci), from_.name_get()[0][1], to.name_get()[0][1])) + chg.append((_t(ci), from_, to[0])) message = MakoTemplate(self._TRACK_TEMPLATE).render_unicode(updated_fields=updated_fields, changes=chg) - record.message_post(message,subtype=subtype) + record.message_post(message,subtype=to[1]) return result From 766cad2f029f0c0524ea1d6764020de6e2b6afab Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 28 Nov 2012 17:18:23 +0530 Subject: [PATCH 09/15] [IMP]improve code for subtype implementation and if remove record then string shows as removed bzr revid: sgo@tinyerp.com-20121128114823-whnokm2a1j9m3ddk --- addons/mail/mail_thread.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index e78e2574e49..cf8dc453867 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -291,19 +291,16 @@ class mail_thread(osv.AbstractModel): return v def convert_for_display(v, f): - subtype = False if not v: return false_value(f) if f._type == 'many2one': if not isinstance(v, browse_record): v = self.pool[f._obj].browse(cr, SUPERUSER_ID, v) - if v.__hasattr__('subtype'): - subtype = v.subtype - return (v.name_get()[0][1],subtype) + return v.name_get()[0][1] if f._type == 'selection': # TODO get translated value pass - return (v,subtype) + 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] @@ -327,20 +324,32 @@ class mail_thread(osv.AbstractModel): 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): + 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: ci = tracked[f] - from_ = convert_for_display(record[f], ci.column)[0] + from_ = convert_for_display(record[f], ci.column) to = convert_for_display(values[f], ci.column) - chg.append((_t(ci), from_, to[0])) + if to is None: + to = "Removed" + if ci.column._type == "many2one": + 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=to[1]) + record.message_post(message,subtype=subtype) return result From 4080fc712cca0a750f4379c4adba96e22a70d55f Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 28 Nov 2012 17:41:26 +0530 Subject: [PATCH 10/15] [IMP]remove unused messge post bzr revid: sgo@tinyerp.com-20121128121126-s9n0910qjcgc66pt --- addons/project/project.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index e8e22e5fb09..7dff3fa28f5 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -1075,7 +1075,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 @@ -1172,7 +1171,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) From c850d5eac4e63a0b61639c1e1926147d98cbe613 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Tue, 4 Dec 2012 17:59:30 +0530 Subject: [PATCH 11/15] [IMP]improve code remove convert for display method bzr revid: sgo@tinyerp.com-20121204122930-oebqygeibqjw9rt6 --- addons/mail/mail_thread.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 01f4f8c0628..00d36b830fe 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -268,12 +268,7 @@ class mail_thread(osv.AbstractModel): #------------------------------------------------------ def write(self, cr, uid, ids, values, context=None): - # XXX is translation a good idea? - # TODO get user lang if not in context - # XXX idea: store a datastructure and render it on demand in the correct language - # TODO? use link to record for m2o - # TODO handle x2m fields. how? - + if context is None: context = {} #import pudb;pudb.set_trace() @@ -284,27 +279,17 @@ class mail_thread(osv.AbstractModel): 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 - def convert_for_display(v, f): - if not v: - return false_value(f) - if f._type == 'many2one': - if not isinstance(v, browse_record): - v = self.pool[f._obj].browse(cr, SUPERUSER_ID, v) - return v.name_get()[0][1] - if f._type == 'selection': - # TODO get translated value - pass - 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): @@ -314,6 +299,7 @@ class mail_thread(osv.AbstractModel): 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) @@ -321,11 +307,13 @@ class mail_thread(osv.AbstractModel): 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 @@ -336,14 +324,14 @@ class mail_thread(osv.AbstractModel): chg = [] subtype = False for f in changed_fields: + to = self.browse(cr, uid, ids[0], context)[f] ci = tracked[f] - from_ = convert_for_display(record[f], ci.column) - to = convert_for_display(values[f], ci.column) if to is None: to = "Removed" if ci.column._type == "many2one": + to = to.name + from_ = from_.name 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, From 73343c5656b36c712c2e4ea973872525f05379e9 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 5 Dec 2012 10:35:13 +0530 Subject: [PATCH 12/15] [IMP]improve code bzr revid: sgo@tinyerp.com-20121205050513-c0y5rla3aqb320iv --- addons/mail/mail_thread.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 00d36b830fe..3f002c4c2ac 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -326,11 +326,14 @@ class mail_thread(osv.AbstractModel): for f in changed_fields: to = self.browse(cr, uid, ids[0], context)[f] ci = tracked[f] - if to is None: - to = "Removed" if ci.column._type == "many2one": - to = to.name - from_ = from_.name + if to: + to = to.name_get()[0][1] + else: + to = "Removed" + if from_: + from_ = from_.name_get()[0][1] + subtype = get_subtype(ci.column._obj,values[f]) chg.append((_t(ci), from_, to)) From 0b6059cdb29de7461f29338665705ef9bd833d51 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 5 Dec 2012 11:32:54 +0530 Subject: [PATCH 13/15] [IMP]minor changes bzr revid: sgo@tinyerp.com-20121205060254-82gnc2y2w37d0pi5 --- addons/mail/mail_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 3f002c4c2ac..f558d93ed1d 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -331,7 +331,7 @@ class mail_thread(osv.AbstractModel): to = to.name_get()[0][1] else: to = "Removed" - if from_: + if isinstance(from_, browse_record): from_ = from_.name_get()[0][1] subtype = get_subtype(ci.column._obj,values[f]) From aa594bd81935d46840f929957d53ea0d776757b1 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Fri, 7 Dec 2012 12:46:33 +0530 Subject: [PATCH 14/15] [IMP]added rst file for tracked field bzr revid: sgo@tinyerp.com-20121207071633-g9isaastt1yha9ia --- addons/mail/doc/tracked_field.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 addons/mail/doc/tracked_field.rst 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 From 2386d334a368974351b4225f205dbbfca99a5390 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Mon, 10 Dec 2012 18:23:48 +0530 Subject: [PATCH 15/15] [IMP]add test cases for tracking field bzr revid: sgo@tinyerp.com-20121210125348-edca0z6jiudfgigc --- addons/mail/tests/test_mail.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index bc6df813ef7..6d5e7d048e0 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -817,3 +817,31 @@ class test_mail(test_mail_mockup.TestMailMockups): msg.refresh() # Test: msg starred by Raoul self.assertEqual(set(msg.favorite_user_ids), set([user_raoul]), 'mail_message favorite: after unstarring, Raoul should be in favorite_user_ids') + + def test_80_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.')