diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index e56ed80af30..6e57fa5e8c3 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -24,17 +24,11 @@ import crm_action_rule import crm_segmentation import crm_meeting import crm_lead -import crm_opportunity - import crm_phonecall - import crm_installer import report import wizard - import res_partner - - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index d1751618506..5e812207921 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -69,7 +69,6 @@ Creates a dashboard for CRM that includes: 'crm_meeting_data.xml', 'crm_lead_data.xml', 'crm_meeting_data.xml', - 'crm_opportunity_data.xml', 'crm_phonecall_data.xml', ], 'update_xml': [ @@ -103,9 +102,6 @@ Creates a dashboard for CRM that includes: 'crm_phonecall_view.xml', 'crm_phonecall_menu.xml', - 'crm_opportunity_view.xml', - 'crm_opportunity_menu.xml', - 'report/crm_lead_report_view.xml', 'report/crm_phonecall_report_view.xml', @@ -121,7 +117,6 @@ Creates a dashboard for CRM that includes: 'crm_demo.xml', 'crm_lead_demo.xml', 'crm_meeting_demo.xml', - 'crm_opportunity_demo.xml', 'crm_phonecall_demo.xml', ], 'test': [ diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 9575aa9e102..fd8d2942337 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -44,17 +44,157 @@ AVAILABLE_PRIORITIES = [ ('5', 'Lowest'), ] +class crm_case_stage(osv.osv): + """ Stage of case """ + + _name = "crm.case.stage" + _description = "Stage of case" + _rec_name = 'name' + _order = "sequence" + + _columns = { + 'name': fields.char('Stage Name', size=64, required=True, translate=True), + 'sequence': fields.integer('Sequence', help="Used to order stages."), + 'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"), + 'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."), + 'requirements': fields.text('Requirements'), + 'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', 'Sections'), + } + + _defaults = { + 'sequence': lambda *args: 1, + 'probability': lambda *args: 0.0, + } + +class crm_case_section(osv.osv): + """Sales Team""" + + _name = "crm.case.section" + _description = "Sales Teams" + _order = "complete_name" + + def get_full_name(self, cr, uid, ids, field_name, arg, context=None): + return dict(self.name_get(cr, uid, ids, context=context)) + + _columns = { + 'name': fields.char('Sales Team', size=64, required=True, translate=True), + 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True), + 'code': fields.char('Code', size=8), + 'active': fields.boolean('Active', help="If the active field is set to "\ + "true, it will allow you to hide the sales team without removing it."), + 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"), + 'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the saleman with the team leader."), + 'user_id': fields.many2one('res.users', 'Team Leader'), + 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'), + 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"), + 'parent_id': fields.many2one('crm.case.section', 'Parent Team'), + 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'), + 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"), + 'note': fields.text('Description'), + 'working_hours': fields.float('Working Hours', digits=(16,2 )), + 'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'), + } + + _defaults = { + 'active': lambda *a: 1, + 'allow_unlink': lambda *a: 1, + } + + _sql_constraints = [ + ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !') + ] + + def _check_recursion(self, cr, uid, ids, context=None): + + """ + Checks for recursion level for sales team + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of Sales team ids + """ + level = 100 + + while len(ids): + cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),)) + ids = filter(None, map(lambda x: x[0], cr.fetchall())) + if not level: + return False + level -= 1 + + return True + + _constraints = [ + (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id']) + ] + + def name_get(self, cr, uid, ids, context=None): + """Overrides orm name_get method + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param ids: List of sales team ids + """ + if context is None: + context = {} + if not isinstance(ids, list) : + ids = [ids] + res = [] + if not ids: + return res + reads = self.read(cr, uid, ids, ['name', 'parent_id'], context) + + for record in reads: + name = record['name'] + if record['parent_id']: + name = record['parent_id'][1] + ' / ' + name + res.append((record['id'], name)) + return res + +class crm_case_categ(osv.osv): + """ Category of Case """ + _name = "crm.case.categ" + _description = "Category of Case" + _columns = { + 'name': fields.char('Name', size=64, required=True, translate=True), + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), + 'object_id': fields.many2one('ir.model', 'Object Name'), + } + + def _find_object_id(self, cr, uid, context=None): + """Finds id for case object + @param self: The object pointer + @param cr: the current row, from the database cursor, + @param uid: the current user’s ID for security checks, + @param context: A standard dictionary for contextual values + """ + + object_id = context and context.get('object_id', False) or False + ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)]) + return ids and ids[0] + + _defaults = { + 'object_id' : _find_object_id + } + +class crm_case_resource_type(osv.osv): + """ Resource Type of case """ + _name = "crm.case.resource.type" + _description = "Campaign" + _rec_name = "name" + _columns = { + 'name': fields.char('Campaign Name', size=64, required=True, translate=True), + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), + } + class crm_base(object): - """ - Base classe for crm object, - Object that inherit from this class should have - date_open - date_closed - user_id - partner_id - partner_address_id - as field to be compatible with this class - + """ Base utility mixin class for crm objects, + Object subclassing this should define colums: + date_open + date_closed + user_id + partner_id + partner_address_id """ def _get_default_partner_address(self, cr, uid, context=None): """Gives id of default address for current user @@ -77,7 +217,7 @@ class crm_base(object): return False user = self.pool.get('res.users').browse(cr, uid, uid, context=context) return user.company_id.partner_id.id - + def _get_default_email(self, cr, uid, context=None): """Gives default email address for current user :param context: if portal in context is false return false anyway @@ -86,7 +226,7 @@ class crm_base(object): return False user = self.pool.get('res.users').browse(cr, uid, uid, context=context) return user.user_email - + def _get_default_user(self, cr, uid, context=None): """Gives current user id :param context: if portal in context is false return false anyway @@ -100,7 +240,7 @@ class crm_base(object): """ user = self.pool.get('res.users').browse(cr, uid, uid, context=context) return user.context_section_id.id or False - + def onchange_partner_address_id(self, cr, uid, ids, add, email=False): """This function returns value of partner email based on Partner Address @param ids: List of case IDs @@ -114,7 +254,7 @@ class crm_base(object): return {'value': {'email_from': address.email, 'phone': address.phone}} else: return {'value': {'phone': address.phone}} - + def onchange_partner_id(self, cr, uid, ids, part, email=False): """This function returns value of partner address based on partner @param ids: List of case IDs @@ -127,11 +267,9 @@ class crm_base(object): data = {'partner_address_id': addr['contact']} data.update(self.onchange_partner_address_id(cr, uid, ids, addr['contact'])['value']) return {'value': data} - - + def case_open(self, cr, uid, ids, *args): """Opens Case - @param ids: List of case Ids """ cases = self.browse(cr, uid, ids) for case in cases: @@ -140,7 +278,6 @@ class crm_base(object): data['user_id'] = uid self.write(cr, uid, case.id, data) - self._action(cr, uid, cases, 'open') return True @@ -150,12 +287,8 @@ class crm_base(object): """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache - self.write(cr, uid, ids, {'state': 'done', - 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'), - }) - # + self.write(cr, uid, ids, {'state': 'done', 'date_closed': time.strftime('%Y-%m-%d %H:%M:%S'), }) # We use the cache of cases to keep the old case state - # self._action(cr, uid, cases, 'done') return True @@ -165,17 +298,13 @@ class crm_base(object): """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache - self.write(cr, uid, ids, {'state': 'cancel', - 'active': True}) + self.write(cr, uid, ids, {'state': 'cancel', 'active': True}) + # We use the cache of cases to keep the old case state self._action(cr, uid, cases, 'cancel') - for case in cases: - message = _("The case '%s' has been cancelled.") % (case.name,) - self.log(cr, uid, case.id, message) return True def case_pending(self, cr, uid, ids, *args): """Marks case as pending - @param ids: List of case Ids """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache @@ -185,7 +314,6 @@ class crm_base(object): def case_reset(self, cr, uid, ids, *args): """Resets case as draft - @param ids: List of case Ids """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache @@ -204,187 +332,69 @@ class crm_base(object): return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context) class crm_case(crm_base): - """ - A simple python class to be used for common functions - Object that inherit from this class should inherit from mailgate.thread - And need a stage_id field - - And object that inherit (orm inheritance) from a class the overwrite copy + """ A simple python class to be used for common functions + Object that inherit from this class should inherit from mailgate.thread + And need a stage_id field + And object that inherit (orm inheritance) from a class the overwrite copy """ - def _find_lost_stage(self, cr, uid, type, section_id): - return self._find_percent_stage(cr, uid, 0.0, type, section_id) + def stage_find(self, cr, uid, section_id, domain=[], order='sequence'): + domain = list(domain) + if section_id: + domain.append(('section_ids', '=', section_id)) + stage_ids = self.pool.get('crm.case.stage').search(cr, uid, domain, order=order) + if stage_ids: + return stage_ids[0] - def _find_won_stage(self, cr, uid, type, section_id): - return self._find_percent_stage(cr, uid, 100.0, type, section_id) + def stage_set(self, cr, uid, ids, stage_id, context=None): + value = {} + if hasattr(self,'onchange_stage_id'): + value = self.onchange_stage_id(cr, uid, ids, stage_id)['value'] + value['stage_id'] = stage_id + self.write(cr, uid, ids, value, context=context) - def _find_percent_stage(self, cr, uid, percent, type, section_id): + def stage_change(self, cr, uid, ids, op, order, context=None): + if context is None: + context = {} + for case in self.browse(cr, uid, ids, context=context): + seq = 0 + if case.stage_id: + seq = case.stage_id.sequence + section_id = None + if case.section_id: + section_id = case.section_id.id + next_stage_id = self.stage_find(cr, uid, section_id, [('sequence',op,seq)],order) + if next_stage_id: + self.stage_set(cr, uid, [case.id], next_stage_id, context=context) + + def stage_next(self, cr, uid, ids, context=None): + """This function computes next stage for case from its current stage + using available stage for that case type """ - Return the first stage with a probability == percent + self.stage_change(cr, uid, ids, '>','sequence', context) + + def stage_previous(self, cr, uid, ids, context=None): + """This function computes previous stage for case from its current + stage using available stage for that case type """ - stage_pool = self.pool.get('crm.case.stage') - if section_id : - ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("type", 'like', type), ("section_ids", 'in', [section_id])]) - else : - ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("type", 'like', type)]) - - if ids: - return ids[0] - return False - - - def _find_first_stage(self, cr, uid, type, section_id): - """ - return the first stage that has a sequence number equal or higher than sequence - """ - stage_pool = self.pool.get('crm.case.stage') - if section_id : - ids = stage_pool.search(cr, uid, [("sequence", '>', 0), ("type", 'like', type), ("section_ids", 'in', [section_id])]) - else : - ids = stage_pool.search(cr, uid, [("sequence", '>', 0), ("type", 'like', type)]) - - if ids: - stages = stage_pool.browse(cr, uid, ids) - stage_min = stages[0] - for stage in stages: - if stage_min.sequence > stage.sequence: - stage_min = stage - return stage_min.id - else : - return False - - def onchange_stage_id(self, cr, uid, ids, stage_id, context={}): - - """ @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of stage’s IDs - @stage_id: change state id on run time """ - - if not stage_id: - return {'value':{}} - - stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context) - - if not stage.on_change: - return {'value':{}} - return {'value':{'probability': stage.probability}} - - + self.stage_change(cr, uid, ids, '<', 'sequence desc', context) def copy(self, cr, uid, id, default=None, context=None): + """ Overrides orm copy method. """ - Overrides orm copy method. - @param self: the object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param id: Id of mailgate thread - @param default: Dictionary of default values for copy. - @param context: A standard dictionary for contextual values - """ - if context is None: context = {} if default is None: default = {} - default.update({ - 'message_ids': [], - }) + default.update({ 'message_ids': [], }) if hasattr(self, '_columns'): if self._columns.get('date_closed'): - default.update({ - 'date_closed': False, - }) + default.update({ 'date_closed': False, }) if self._columns.get('date_open'): - default.update({ - 'date_open': False - }) + default.update({ 'date_open': False }) return super(osv.osv, self).copy(cr, uid, id, default, context=context) - - - - - def _find_next_stage(self, cr, uid, stage_list, index, current_seq, stage_pool, context=None): - if index + 1 == len(stage_list): - return False - next_stage_id = stage_list[index + 1] - next_stage = stage_pool.browse(cr, uid, next_stage_id, context=context) - if not next_stage: - return False - next_seq = next_stage.sequence - if not current_seq : - current_seq = 0 - - if (abs(next_seq - current_seq)) >= 1: - return next_stage - else : - return self._find_next_stage(cr, uid, stage_list, index + 1, current_seq, stage_pool) - - def stage_change(self, cr, uid, ids, context=None, order='sequence'): - if context is None: - context = {} - - stage_pool = self.pool.get('crm.case.stage') - stage_type = context and context.get('stage_type','') - current_seq = False - next_stage_id = False - - for case in self.browse(cr, uid, ids, context=context): - - next_stage = False - value = {} - if case.section_id.id : - domain = [('type', '=', stage_type),('section_ids', '=', case.section_id.id)] - else : - domain = [('type', '=', stage_type)] - - - stages = stage_pool.search(cr, uid, domain, order=order) - current_seq = case.stage_id.sequence - index = -1 - if case.stage_id and case.stage_id.id in stages: - index = stages.index(case.stage_id.id) - - next_stage = self._find_next_stage(cr, uid, stages, index, current_seq, stage_pool, context=context) - - if next_stage: - next_stage_id = next_stage.id - value.update({'stage_id': next_stage.id}) - if next_stage.on_change: - value.update({'probability': next_stage.probability}) - self.write(cr, uid, [case.id], value, context=context) - - - return next_stage_id #FIXME should return a list of all id - - - def stage_next(self, cr, uid, ids, context=None): - """This function computes next stage for case from its current stage - using available stage for that case type - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values""" - - return self.stage_change(cr, uid, ids, context=context, order='sequence') - - def stage_previous(self, cr, uid, ids, context=None): - """This function computes previous stage for case from its current stage - using available stage for that case type - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param context: A standard dictionary for contextual values""" - return self.stage_change(cr, uid, ids, context=context, order='sequence desc') - - - - - def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=[], context=None): mailgate_pool = self.pool.get('mailgate.thread') return mailgate_pool.history(cr, uid, cases, keyword, history=history,\ @@ -395,17 +405,11 @@ class crm_case(crm_base): def case_open(self, cr, uid, ids, *args): """Opens Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ - cases = self.browse(cr, uid, ids) self._history(cr, uid, cases, _('Open')) for case in cases: - data = {'state': 'open', 'active': True} + data = {'state': 'open', 'active': True } if not case.user_id: data['user_id'] = uid self.write(cr, uid, case.id, data) @@ -414,11 +418,6 @@ class crm_case(crm_base): def case_close(self, cr, uid, ids, *args): """Closes Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache @@ -434,11 +433,6 @@ class crm_case(crm_base): def case_escalate(self, cr, uid, ids, *args): """Escalates case to top level - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ cases = self.browse(cr, uid, ids) for case in cases: @@ -459,11 +453,6 @@ class crm_case(crm_base): def case_cancel(self, cr, uid, ids, *args): """Cancels Case - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache @@ -478,11 +467,6 @@ class crm_case(crm_base): def case_pending(self, cr, uid, ids, *args): """Marks case as pending - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ cases = self.browse(cr, uid, ids) cases[0].state # to fill the browse record cache @@ -493,11 +477,6 @@ class crm_case(crm_base): def case_reset(self, cr, uid, ids, *args): """Resets case as draft - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params """ state = 'draft' if 'crm.phonecall' in args: @@ -600,8 +579,6 @@ class crm_case(crm_base): cases = self.browse(cr, uid, ids2, context=context) return self._action(cr, uid, cases, False, context=context) - - def format_body(self, body): return self.pool.get('base.action.rule').format_body(body) @@ -621,189 +598,6 @@ class crm_case(crm_base): res[case.id] = l return res - -class crm_case_stage(osv.osv): - """ Stage of case """ - - _name = "crm.case.stage" - _description = "Stage of case" - _rec_name = 'name' - _order = "sequence" - - - - def _get_type_value(self, cr, user, context): - return [('lead','Lead'),('opportunity','Opportunity')] - - - _columns = { - 'name': fields.char('Stage Name', size=64, required=True, translate=True), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of case stages."), - 'probability': fields.float('Probability (%)', required=True, help="This percentage depicts the default/average probability of the Case for this stage to be a success"), - 'on_change': fields.boolean('Change Probability Automatically', \ - help="Change Probability on next and previous stages."), - 'requirements': fields.text('Requirements'), - 'type': fields.selection(_get_type_value, 'Type', required=True), - } - - - def _find_stage_type(self, cr, uid, context=None): - """Finds type of stage according to object. - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - type = context and context.get('type', '') or '' - return type - - _defaults = { - 'sequence': lambda *args: 1, - 'probability': lambda *args: 0.0, - 'type': _find_stage_type, - } - -crm_case_stage() - - -class crm_case_section(osv.osv): - """Sales Team""" - - _name = "crm.case.section" - _description = "Sales Teams" - _order = "complete_name" - - def get_full_name(self, cr, uid, ids, field_name, arg, context=None): - return dict(self.name_get(cr, uid, ids, context=context)) - - _columns = { - 'name': fields.char('Sales Team', size=64, required=True, translate=True), - 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True), - 'code': fields.char('Code', size=8), - 'active': fields.boolean('Active', help="If the active field is set to "\ - "true, it will allow you to hide the sales team without removing it."), - 'allow_unlink': fields.boolean('Allow Delete', help="Allows to delete non draft cases"), - 'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the saleman with the team leader."), - 'user_id': fields.many2one('res.users', 'Team Leader'), - 'member_ids':fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'), - 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"), - 'parent_id': fields.many2one('crm.case.section', 'Parent Team'), - 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'), - 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"), - 'note': fields.text('Description'), - 'working_hours': fields.float('Working Hours', digits=(16,2 )), - 'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'), - } - - _defaults = { - 'active': lambda *a: 1, - 'allow_unlink': lambda *a: 1, - } - - _sql_constraints = [ - ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !') - ] - - def _check_recursion(self, cr, uid, ids, context=None): - - """ - Checks for recursion level for sales team - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of Sales team ids - """ - level = 100 - - while len(ids): - cr.execute('select distinct parent_id from crm_case_section where id IN %s', (tuple(ids),)) - ids = filter(None, map(lambda x: x[0], cr.fetchall())) - if not level: - return False - level -= 1 - - return True - - _constraints = [ - (_check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - """Overrides orm name_get method - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of sales team ids - """ - if context is None: - context = {} - if not isinstance(ids, list) : - ids = [ids] - res = [] - if not ids: - return res - reads = self.read(cr, uid, ids, ['name', 'parent_id'], context) - - for record in reads: - name = record['name'] - if record['parent_id']: - name = record['parent_id'][1] + ' / ' + name - res.append((record['id'], name)) - return res - -crm_case_section() - - -class crm_case_categ(osv.osv): - """ Category of Case """ - _name = "crm.case.categ" - _description = "Category of Case" - _columns = { - 'name': fields.char('Name', size=64, required=True, translate=True), - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), - 'object_id': fields.many2one('ir.model', 'Object Name'), - } - - def _find_object_id(self, cr, uid, context=None): - """Finds id for case object - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param context: A standard dictionary for contextual values - """ - - object_id = context and context.get('object_id', False) or False - ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', object_id)]) - return ids and ids[0] - - _defaults = { - 'object_id' : _find_object_id - - } -crm_case_categ() - - -class crm_case_stage(osv.osv): - _inherit = "crm.case.stage" - _columns = { - 'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', 'Sections'), - } - -crm_case_stage() - - -class crm_case_resource_type(osv.osv): - """ Resource Type of case """ - _name = "crm.case.resource.type" - _description = "Campaign" - _rec_name = "name" - _columns = { - 'name': fields.char('Campaign Name', size=64, required=True, translate=True), - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), - } -crm_case_resource_type() - - def _links_get(self, cr, uid, context=None): """Gets links value for reference field @param self: The object pointer @@ -830,13 +624,6 @@ class users(osv.osv): if vals.get('context_section_id', False): section_obj.write(cr, uid, [vals['context_section_id']], {'member_ids':[(4, res)]}, context) return res + users() - -class res_partner(osv.osv): - _inherit = 'res.partner' - _columns = { - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), - } -res_partner() - diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index afd98a94806..f573acb6b00 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -39,8 +39,9 @@ class crm_lead(crm_case, osv.osv): """ CRM Lead Case """ _name = "crm.lead" _description = "Lead/Opportunity" - _order = "date_action, priority, id desc" + _order = "priority,date_action,id desc" _inherit = ['mailgate.thread','res.partner.address'] + def _compute_day(self, cr, uid, ids, fields, args, context=None): """ @param cr: the current row, from the database cursor, @@ -124,7 +125,6 @@ class crm_lead(crm_case, osv.osv): 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null', select=True, help="Optional linked partner, usually after conversion of the lead"), - # From crm.case 'id': fields.integer('ID'), 'name': fields.char('Name', size=64, select=1), 'active': fields.boolean('Active', required=False), @@ -138,7 +138,6 @@ class crm_lead(crm_case, osv.osv): 'description': fields.text('Notes'), 'write_date': fields.datetime('Update Date' , readonly=True), - # Lead fields 'categ_id': fields.many2one('crm.case.categ', 'Category', \ domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]"), 'type_id': fields.many2one('crm.case.resource.type', 'Campaign', \ @@ -148,15 +147,11 @@ class crm_lead(crm_case, osv.osv): 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner that will be created while converting the into opportunity', select=1), 'optin': fields.boolean('Opt-In', help="If opt-in is checked, this contact has accepted to receive emails."), 'optout': fields.boolean('Opt-Out', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."), - 'type':fields.selection([ - ('lead','Lead'), - ('opportunity','Opportunity'), - - ],'Type', help="Type is used to separate Leads and Opportunities"), + 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"), 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'), 'date_closed': fields.datetime('Closed', readonly=True), - 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[('type','=','lead')]"), - 'user_id': fields.many2one('res.users', 'Salesman', select=1), + 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"), + 'user_id': fields.many2one('res.users', 'Salesman'), 'referred': fields.char('Referred By', size=64), 'date_open': fields.datetime('Opened', readonly=True), 'day_open': fields.function(_compute_day, string='Days to Open', \ @@ -170,9 +165,21 @@ class crm_lead(crm_case, osv.osv): \nIf the case needs to be reviewed then the state is set to \'Pending\'.'), 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]), 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64), - } + # Only used for type opportunity + 'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', domain="[('partner_id','=',partner_id)]"), + 'probability': fields.float('Probability (%)',group_operator="avg"), + 'planned_revenue': fields.float('Expected Revenue'), + '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), + 'date_deadline': fields.date('Expected Closing'), + 'date_action': fields.date('Next Action Date'), + 'title_action': fields.char('Next Action', size=64), + 'stage_id': fields.many2one('crm.case.stage', 'Stage', domain="[(section_ids', '=', section_id)]"), + } + _defaults = { 'active': lambda *a: 1, 'user_id': crm_case._get_default_user, @@ -185,82 +192,120 @@ class crm_lead(crm_case, osv.osv): #'stage_id': _get_stage_id, } - - def onchange_partner_address_id(self, cr, uid, ids, add, email=False): """This function returns value of partner email based on Partner Address - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case IDs - @param add: Id of Partner's address - @email: Partner's email ID """ if not add: return {'value': {'email_from': False, 'country_id': False}} address = self.pool.get('res.partner.address').browse(cr, uid, add) return {'value': {'email_from': address.email, 'phone': address.phone, 'country_id': address.country_id.id}} - def case_open(self, cr, uid, ids, *args): - """Overrides cancel for crm_case for setting Open Date - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case's Ids - @param *args: Give Tuple Value + def on_change_optin(self, cr, uid, ids, optin): + return {'value':{'optin':optin,'optout':False}} + + def on_change_optout(self, cr, uid, ids, optout): + return {'value':{'optout':optout,'optin':False}} + + def onchange_stage_id(self, cr, uid, ids, stage_id, context={}): + if not stage_id: + return {'value':{}} + stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context) + if not stage.on_change: + return {'value':{}} + return {'value':{'probability': stage.probability}} + + def stage_find_percent(self, cr, uid, percent, section_id): + """ Return the first stage with a probability == percent """ - leads = self.browse(cr, uid, ids) + stage_pool = self.pool.get('crm.case.stage') + if section_id : + ids = stage_pool.search(cr, uid, [("probability", '=', percent), ("section_ids", 'in', [section_id])]) + else : + ids = stage_pool.search(cr, uid, [("probability", '=', percent)]) + if ids: + return ids[0] + return False + def stage_find_lost(self, cr, uid, section_id): + return self.stage_find_percent(cr, uid, 0.0, section_id) - for i in xrange(0, len(ids)): - if leads[i].state == 'draft': - value = {} - if not leads[i].stage_id : - stage_id = self._find_first_stage(cr, uid, leads[i].type, leads[i].section_id.id or False) - value.update({'stage_id' : stage_id}) - value.update({'date_open': time.strftime('%Y-%m-%d %H:%M:%S')}) - self.write(cr, uid, [ids[i]], value) - self.log_open( cr, uid, leads[i]) + def stage_find_won(self, cr, uid, section_id): + return self.stage_find_percent(cr, uid, 100.0, section_id) + + def case_open(self, cr, uid, ids, *args): + for l in self.browse(cr, uid, ids): + # When coming from draft override date and stage otherwise just set state + if l.state == 'draft': + if l.type == 'lead': + message = _("The lead '%s' has been opened.") % l.name + elif l.type == 'opportunity': + message = _("The opportunity '%s' has been opened.") % l.name + else: + message = _("The case '%s' has been opened.") % l.name + self.log(cr, uid, l.id, message) + value = {'date_open': time.strftime('%Y-%m-%d %H:%M:%S')} + self.write(cr, uid, [l.id], value) + if l.type == 'opportunity' and not l.stage_id: + stage_id = self.stage_find(cr, uid, l.section_id.id or False, [('sequence','>',0)]) + if stage_id: + self.stage_set(cr, uid, [l.id], stage_id) res = super(crm_lead, self).case_open(cr, uid, ids, *args) return res - def log_open(self, cr, uid, case): - if case.type == 'lead': - message = _("The lead '%s' has been opened.") % case.name - elif case.type == 'opportunity': - message = _("The opportunity '%s' has been opened.") % case.name - else: - message = _("The case '%s' has been opened.") % case.name - self.log(cr, uid, case.id, message) - def case_close(self, cr, uid, ids, *args): - """Overrides close for crm_case for setting close date - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of case Ids - @param *args: Tuple Value for additional Params - """ res = super(crm_lead, self).case_close(cr, uid, ids, *args) self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')}) for case in self.browse(cr, uid, ids): if case.type == 'lead': message = _("The lead '%s' has been closed.") % case.name - elif case.type == 'opportunity': - message = _("The opportunity '%s' has been closed.") % case.name else: message = _("The case '%s' has been closed.") % case.name self.log(cr, uid, case.id, message) return res + def case_cancel(self, cr, uid, ids, *args): + """Overrides cancel for crm_case for setting probability + """ + res = super(crm_lead, self).case_cancel(cr, uid, ids, args) + self.write(cr, uid, ids, {'probability' : 0.0}) + return res + + def case_reset(self, cr, uid, ids, *args): + """Overrides reset as draft in order to set the stage field as empty + """ + res = super(crm_lead, self).case_reset(cr, uid, ids, *args) + self.write(cr, uid, ids, {'stage_id': False, 'probability': 0.0}) + return res + + def case_mark_lost(self, cr, uid, ids, *args): + """Mark the case as lost: state = done and probability = 0% + """ + res = super(crm_lead, self).case_close(cr, uid, ids, *args) + self.write(cr, uid, ids, {'probability' : 0.0}) + for l in self.browse(cr, uid, ids): + stage_id = self.stage_find_lost(cr, uid, l.section_id.id or False) + if stage_id: + self.stage_set(cr, uid, [l.id], stage_id) + message = _("The opportunity '%s' has been marked as lost.") % l.name + self.log(cr, uid, l.id, message) + return res + + def case_mark_won(self, cr, uid, ids, *args): + """Mark the case as lost: state = done and probability = 0% + """ + res = super(crm_lead, self).case_close(cr, uid, ids, *args) + self.write(cr, uid, ids, {'probability' : 100.0}) + for l in self.browse(cr, uid, ids): + stage_id = self.stage_find_won(cr, uid, l.section_id.id or False) + if stage_id: + self.stage_set(cr, uid, [l.id], stage_id) + message = _("The opportunity '%s' has been been won.") % l.name + self.log(cr, uid, l.id, message) + return res + def convert_opportunity(self, cr, uid, ids, context=None): """ Precomputation for converting lead to opportunity - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of closeday’s IDs - @param context: A standard dictionary for contextual values - @return: Value of action in dict """ if context is None: context = {} @@ -290,57 +335,8 @@ class crm_lead(crm_case, osv.osv): } return value - def write(self, cr, uid, ids, vals, context=None): - if not context: - context = {} - - if 'date_closed' in vals: - return super(crm_lead,self).write(cr, uid, ids, vals, context=context) - - if 'stage_id' in vals and vals['stage_id']: - stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context) - self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name) - message='' - for case in self.browse(cr, uid, ids, context=context): - if case.type == 'lead' or context.get('stage_type',False)=='lead': - message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name) - elif case.type == 'opportunity': - message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name) - self.log(cr, uid, case.id, message) - return super(crm_lead,self).write(cr, uid, ids, vals, context) - - def stage_next(self, cr, uid, ids, context=None): - stage = super(crm_lead, self).stage_next(cr, uid, ids, context=context) - if stage: - stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context) - if stage_obj.on_change: - data = {'probability': stage_obj.probability} - self.write(cr, uid, ids, data) - return stage - - def stage_previous(self, cr, uid, ids, context=None): - stage = super(crm_lead, self).stage_previous(cr, uid, ids, context=context) - if stage: - stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, stage, context=context) - if stage_obj.on_change: - data = {'probability': stage_obj.probability} - self.write(cr, uid, ids, data) - return stage - - def unlink(self, cr, uid, ids, context=None): - for lead in self.browse(cr, uid, ids, context): - if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'): - raise osv.except_osv(_('Warning !'), - _('You can not delete this lead. You should better cancel it.')) - return super(crm_lead, self).unlink(cr, uid, ids, context) - def message_new(self, cr, uid, msg, context=None): - """ - Automatically calls when new email message arrives - - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks + """ Automatically calls when new email message arrives """ mailgate_pool = self.pool.get('email.server.tools') @@ -380,9 +376,6 @@ class crm_lead(crm_case, osv.osv): def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None): """ - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, @param ids: List of update mail’s IDs """ if isinstance(ids, (str, int, long)): @@ -416,22 +409,82 @@ class crm_lead(crm_case, osv.osv): return res def msg_send(self, cr, uid, id, *args, **argv): - """ Send The Message - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of email’s IDs - @param *args: Return Tuple Value - @param **args: Return Dictionary of Keyword Value + @param ids: List of email’s IDs """ return True - def on_change_optin(self, cr, uid, ids, optin): - return {'value':{'optin':optin,'optout':False}} + def action_makeMeeting(self, cr, uid, ids, context=None): + """ + This opens Meeting's calendar view to schedule meeting on current Opportunity + @return : Dictionary value for created Meeting view + """ + value = {} + for opp in self.browse(cr, uid, ids, context=context): + data_obj = self.pool.get('ir.model.data') + + # Get meeting views + result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter') + res = data_obj.read(cr, uid, result, ['res_id']) + id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet') + id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet') + id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet') + if id1: + id1 = data_obj.browse(cr, uid, id1, context=context).res_id + if id2: + id2 = data_obj.browse(cr, uid, id2, context=context).res_id + if id3: + id3 = data_obj.browse(cr, uid, id3, context=context).res_id + + context = { + 'default_opportunity_id': opp.id, + 'default_partner_id': opp.partner_id and opp.partner_id.id or False, + 'default_user_id': uid, + 'default_section_id': opp.section_id and opp.section_id.id or False, + 'default_email_from': opp.email_from, + 'default_state': 'open', + 'default_name': opp.name + } + value = { + 'name': _('Meetings'), + 'context': context, + 'view_type': 'form', + 'view_mode': 'calendar,form,tree', + 'res_model': 'crm.meeting', + 'view_id': False, + 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')], + 'type': 'ir.actions.act_window', + 'search_view_id': res['res_id'], + 'nodestroy': True + } + return value + + def write(self, cr, uid, ids, vals, context=None): + if not context: + context = {} + + if 'date_closed' in vals: + return super(crm_lead,self).write(cr, uid, ids, vals, context=context) + + if 'stage_id' in vals and vals['stage_id']: + stage_obj = self.pool.get('crm.case.stage').browse(cr, uid, vals['stage_id'], context=context) + self.history(cr, uid, ids, _("Changed Stage to: %s") % stage_obj.name, details=_("Changed Stage to: %s") % stage_obj.name) + message='' + for case in self.browse(cr, uid, ids, context=context): + if case.type == 'lead' or context.get('stage_type',False)=='lead': + message = _("The stage of lead '%s' has been changed to '%s'.") % (case.name, stage_obj.name) + elif case.type == 'opportunity': + message = _("The stage of opportunity '%s' has been changed to '%s'.") % (case.name, stage_obj.name) + self.log(cr, uid, case.id, message) + return super(crm_lead,self).write(cr, uid, ids, vals, context) + + def unlink(self, cr, uid, ids, context=None): + for lead in self.browse(cr, uid, ids, context): + if (not lead.section_id.allow_unlink) and (lead.state <> 'draft'): + raise osv.except_osv(_('Warning !'), + _('You can not delete this lead. You should better cancel it.')) + return super(crm_lead, self).unlink(cr, uid, ids, context) - def on_change_optout(self, cr, uid, ids, optout): - return {'value':{'optout':optout,'optin':False}} crm_lead() diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml index 3cb34c49299..0c7215c09eb 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -2,97 +2,119 @@ - + + Lost - lead - New - - lead + - Qualification - - lead + - Proposition - - lead + - Negotiation - - lead + - Won - + - lead - - - Sales Department Sales - + Telesales - Mail Campaign 1 - Mail Campaign 2 - Twitter Ads - Google Adwords - Google Adwords 2 - Television - Newsletter + + + Interest in Computer + + + + + Interest in Accessories + + + + + Need Services + + + + + Need Information + + + + + Need a Website Design + + + + + Potential Reseller + + + + + Need Consulting + + + + + Other + + + + diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index 0051ac7b44a..4a4dbf9d28f 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -1,11 +1,7 @@ - - - + @@ -37,7 +33,7 @@ - + @@ -186,26 +182,105 @@ - - + - - - + + + + + + opportunity + + + + + + + + + + + + + + + opportunity + + + + + + + + + + + + + + + + + opportunity + + + + + + + + + + + + + + + + + + opportunity + + + + + + + + + + + + + + + + + opportunity + + + + + + + + + + diff --git a/addons/crm/crm_lead_menu.xml b/addons/crm/crm_lead_menu.xml index 2bd20f1c7ca..5d5d0ef5824 100644 --- a/addons/crm/crm_lead_menu.xml +++ b/addons/crm/crm_lead_menu.xml @@ -35,5 +35,68 @@ id="menu_crm_case_categ0_act_leads" action="crm_case_category_act_leads_all" sequence="1" /> + + + + + + + Opportunities + crm.lead + tree,form,graph,calendar + [('type','=','opportunity')] + {'search_default_user_id':uid,'search_default_current':1, 'search_default_section_id':section_id, 'stage_type': 'opportunity'} + + + With opportunities you can manage and keep track of your sales pipeline by creating specific customer- or prospect-related sales documents to follow up potential sales. Information such as expected revenue, opportunity stage, expected closing date, communication history and much more can be stored. Opportunities can be connected to the email gateway: new emails may create opportunities, each of them automatically gets the history of the conversation with the customer. + +You and your team(s) will be able to plan meetings and phone calls from opportunities, convert them into quotations, manage related documents, track all customer related activities, and much more. + + + + + tree + + + + + + + form + + + + + + + graph + + + + + + + + diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 3ee6b1bba00..dcc73141f22 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -3,31 +3,23 @@ - - + Stage - Search crm.case.stage search - - - Stages crm.case.stage form - {'search_default_opportunity':1} - Add specific stages to leads and opportunities allowing your sales to better organise their sales pipeline. Stages will allow them to easily track how a specific lead or opportunity is positioned in the sales cycle. @@ -74,7 +66,7 @@ - +