diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 4d510853660..842e50ebff1 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -599,12 +599,11 @@ class account_voucher(osv.osv): This function returns True if the line is considered as noise and should not be displayed """ if line.reconcile_partial_id: - sign = 1 if ttype == 'receipt' else -1 if currency_id == line.currency_id.id: - if line.amount_residual_currency * sign <= 0: + if line.amount_residual_currency <= 0: return True else: - if line.amount_residual * sign <= 0: + if line.amount_residual <= 0: return True return False diff --git a/addons/base_action_rule/__init__.py b/addons/base_action_rule/__init__.py index 207ed3cb30f..c8c827aaba2 100644 --- a/addons/base_action_rule/__init__.py +++ b/addons/base_action_rule/__init__.py @@ -20,5 +20,6 @@ ############################################################################## import base_action_rule +import test_models # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index a7798f40bb1..a3237c1620b 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -19,27 +19,30 @@ # ############################################################################## -from datetime import datetime -from datetime import timedelta -import re +from datetime import datetime, timedelta import time +import logging -from openerp.osv import fields, osv, orm -from openerp.tools.translate import _ -from openerp.tools.safe_eval import safe_eval -from openerp.tools import ustr -from openerp import pooler -from openerp import tools +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +_logger = logging.getLogger(__name__) -def get_datetime(date_field): +DATE_RANGE_FUNCTION = { + 'minutes': lambda interval: timedelta(minutes=interval), + 'hour': lambda interval: timedelta(hours=interval), + 'day': lambda interval: timedelta(days=interval), + 'month': lambda interval: timedelta(months=interval), + False: lambda interval: timedelta(0), +} + +def get_datetime(date_str): '''Return a datetime from a date string or a datetime string''' - #complete date time if date_field contains only a date - date_split = date_field.split(' ') - if len(date_split) == 1: - date_field = date_split[0] + " 00:00:00" - - return datetime.strptime(date_field[:19], '%Y-%m-%d %H:%M:%S') + # complete date time if date_str contains only a date + if ' ' not in date_str: + date_str = date_str + " 00:00:00" + return datetime.strptime(date_str, DEFAULT_SERVER_DATETIME_FORMAT) class base_action_rule(osv.osv): @@ -48,320 +51,208 @@ class base_action_rule(osv.osv): _name = 'base.action.rule' _description = 'Action Rules' - def _state_get(self, cr, uid, context=None): - """ Get State """ - return self.state_get(cr, uid, context=context) - - def state_get(self, cr, uid, context=None): - """ Get State """ - return [('', '')] - - def priority_get(self, cr, uid, context=None): - """ Get Priority """ - return [('', '')] - _columns = { 'name': fields.char('Rule Name', size=64, required=True), - 'model_id': fields.many2one('ir.model', 'Related Document Model', required=True, domain=[('osv_memory','=', False)]), + 'model_id': fields.many2one('ir.model', 'Related Document Model', + required=True, domain=[('osv_memory', '=', False)]), 'model': fields.related('model_id', 'model', type="char", size=256, string='Model'), 'create_date': fields.datetime('Create Date', readonly=1), - 'active': fields.boolean('Active', help="If the active field is set to False,\ - it will allow you to hide the rule without removing it."), - 'sequence': fields.integer('Sequence', help="Gives the sequence order \ -when displaying a list of rules."), - 'trg_date_type': fields.selection([ - ('none', 'None'), - ('create', 'Creation Date'), - ('write', 'Last Modified Date'), - ('action_last', 'Last Action Date'), - ('date', 'Date'), - ('deadline', 'Deadline'), - ], 'Trigger Date', size=16), - 'trg_date_range': fields.integer('Delay after trigger date', \ - help="Delay After Trigger Date,\ -specifies you can put a negative number. If you need a delay before the \ -trigger date, like sending a reminder 15 minutes before a meeting."), - 'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'), \ + 'active': fields.boolean('Active', + help="When unchecked, the rule is hidden and will not be executed."), + 'sequence': fields.integer('Sequence', + help="Gives the sequence order when displaying a list of rules."), + 'trg_date_id': fields.many2one('ir.model.fields', string='Trigger Date', + domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"), + 'trg_date_range': fields.integer('Delay after trigger date', + help="Delay after the trigger date." \ + "You can put a negative number if you need a delay before the" \ + "trigger date, like sending a reminder 15 minutes before a meeting."), + 'trg_date_range_type': fields.selection([('minutes', 'Minutes'), ('hour', 'Hours'), ('day', 'Days'), ('month', 'Months')], 'Delay type'), - 'trg_user_id': fields.many2one('res.users', 'Responsible'), - 'trg_partner_id': fields.many2one('res.partner', 'Partner'), - 'trg_partner_categ_id': fields.many2one('res.partner.category', 'Partner Category'), - 'trg_state_from': fields.selection(_state_get, 'Status', size=16), - 'trg_state_to': fields.selection(_state_get, 'Button Pressed', size=16), - - 'act_user_id': fields.many2one('res.users', 'Set Responsible to'), - 'act_state': fields.selection(_state_get, 'Set State to', size=16), - 'act_followers': fields.many2many("res.partner", string="Set Followers"), - 'regex_name': fields.char('Regex on Resource Name', size=128, help="Regular expression for matching name of the resource\ -\ne.g.: 'urgent.*' will search for records having name starting with the string 'urgent'\ -\nNote: This is case sensitive search."), - 'server_action_ids': fields.one2many('ir.actions.server', 'action_rule_id', 'Server Action', help="Define Server actions.\neg:Email Reminders, Call Object Service, etc.."), #TODO: set domain [('model_id','=',model_id)] - 'filter_id':fields.many2one('ir.filters', 'Filter', required=False), #TODO: set domain [('model_id','=',model_id.model)] + 'act_user_id': fields.many2one('res.users', 'Set Responsible'), + 'act_followers': fields.many2many("res.partner", string="Add Followers"), + 'server_action_ids': fields.many2many('ir.actions.server', string='Server Actions', + domain="[('model_id', '=', model_id)]", + help="Examples: email reminders, call object service, etc."), + 'filter_pre_id': fields.many2one('ir.filters', string='Before Update Filter', + ondelete='restrict', + domain="[('model_id', '=', model_id.model)]", + help="If present, this condition must be satisfied before the update of the record."), + 'filter_id': fields.many2one('ir.filters', string='After Update Filter', + ondelete='restrict', + domain="[('model_id', '=', model_id.model)]", + help="If present, this condition must be satisfied after the update of the record."), 'last_run': fields.datetime('Last Run', readonly=1), } _defaults = { 'active': True, - 'trg_date_type': 'none', 'trg_date_range_type': 'day', } _order = 'sequence' + def _filter(self, cr, uid, action, action_filter, record_ids, context=None): + """ filter the list record_ids that satisfy the action filter """ + if record_ids and action_filter: + assert action.model == action_filter.model_id, "Filter model different from action rule model" + model = self.pool.get(action_filter.model_id) + domain = [('id', 'in', record_ids)] + eval(action_filter.domain) + ctx = dict(context or {}) + ctx.update(eval(action_filter.context)) + record_ids = model.search(cr, uid, domain, context=ctx) + return record_ids + + def _process(self, cr, uid, action, record_ids, context=None): + """ process the given action on the records """ + # execute server actions + model = self.pool.get(action.model_id.model) + if action.server_action_ids: + server_action_ids = map(int, action.server_action_ids) + for record in model.browse(cr, uid, record_ids, context): + action_server_obj = self.pool.get('ir.actions.server') + ctx = dict(context, active_model=model._name, active_ids=[record.id], active_id=record.id) + action_server_obj.run(cr, uid, server_action_ids, context=ctx) + + # modify records + values = {} + if 'date_action_last' in model._all_columns: + values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + if action.act_user_id and 'user_id' in model._all_columns: + values['user_id'] = action.act_user_id.id + if values: + model.write(cr, uid, record_ids, values, context=context) + + if action.act_followers and hasattr(model, 'message_subscribe'): + follower_ids = map(int, action.act_followers) + model.message_subscribe(cr, uid, record_ids, follower_ids, context=context) - def post_action(self, cr, uid, ids, model, context=None): - # Searching for action rules - cr.execute("SELECT model.model, rule.id FROM base_action_rule rule \ - LEFT JOIN ir_model model on (model.id = rule.model_id) \ - WHERE active and model = %s", (model,)) - res = cr.fetchall() - # Check if any rule matching with current object - for obj_name, rule_id in res: - obj = self.pool.get(obj_name) - # If the rule doesn't involve a time condition, run it immediately - # Otherwise we let the scheduler run the action - if self.browse(cr, uid, rule_id, context=context).trg_date_type == 'none': - self._action(cr, uid, [rule_id], obj.browse(cr, uid, ids, context=context), context=context) return True - def _create(self, old_create, model, context=None): + def _wrap_create(self, old_create, model): + """ Return a wrapper around `old_create` calling both `old_create` and + `_process`, in that order. """ - Return a wrapper around `old_create` calling both `old_create` and - `post_action`, in that order. - """ - def wrapper(cr, uid, vals, context=context): - if context is None: - context = {} + def wrapper(cr, uid, vals, context=None): + # avoid loops or cascading actions + if context and context.get('action'): + return old_create(cr, uid, vals, context=context) + + context = dict(context or {}, action=True) new_id = old_create(cr, uid, vals, context=context) - if not context.get('action'): - self.post_action(cr, uid, [new_id], model, context=context) + + # as it is a new record, we do not consider the actions that have a prefilter + action_dom = [('model', '=', model), ('trg_date_id', '=', False), ('filter_pre_id', '=', False)] + action_ids = self.search(cr, uid, action_dom, context=context) + + # check postconditions, and execute actions on the records that satisfy them + for action in self.browse(cr, uid, action_ids, context=context): + if self._filter(cr, uid, action, action.filter_id, [new_id], context=context): + self._process(cr, uid, action, [new_id], context=context) return new_id + return wrapper - def _write(self, old_write, model, context=None): + def _wrap_write(self, old_write, model): + """ Return a wrapper around `old_write` calling both `old_write` and + `_process`, in that order. """ - Return a wrapper around `old_write` calling both `old_write` and - `post_action`, in that order. - """ - def wrapper(cr, uid, ids, vals, context=context): - if context is None: - context = {} - if isinstance(ids, (str, int, long)): - ids = [ids] + def wrapper(cr, uid, ids, vals, context=None): + # avoid loops or cascading actions + if context and context.get('action'): + return old_write(cr, uid, ids, vals, context=context) + + context = dict(context or {}, action=True) + ids = [ids] if isinstance(ids, (int, long, str)) else ids + + # retrieve the action rules to possibly execute + action_dom = [('model', '=', model), ('trg_date_id', '=', False)] + action_ids = self.search(cr, uid, action_dom, context=context) + actions = self.browse(cr, uid, action_ids, context=context) + + # check preconditions + pre_ids = {} + for action in actions: + pre_ids[action] = self._filter(cr, uid, action, action.filter_pre_id, ids, context=context) + + # execute write old_write(cr, uid, ids, vals, context=context) - if not context.get('action'): - self.post_action(cr, uid, ids, model, context=context) + + # check postconditions, and execute actions on the records that satisfy them + for action in actions: + post_ids = self._filter(cr, uid, action, action.filter_id, pre_ids[action], context=context) + if post_ids: + self._process(cr, uid, action, post_ids, context=context) return True + return wrapper - def _register_hook(self, cr, uid, ids, context=None): + def _register_hook(self, cr, ids=None): + """ Wrap the methods `create` and `write` of the models specified by + the rules given by `ids` (or all existing rules if `ids` is `None`.) """ - Wrap every `create` and `write` methods of the models specified by - the rules (given by `ids`). - """ - for action_rule in self.browse(cr, uid, ids, context=context): + if ids is None: + ids = self.search(cr, SUPERUSER_ID, []) + for action_rule in self.browse(cr, SUPERUSER_ID, ids): model = action_rule.model_id.model - obj_pool = self.pool.get(model) - if not hasattr(obj_pool, 'base_action_ruled'): - obj_pool.create = self._create(obj_pool.create, model, context=context) - obj_pool.write = self._write(obj_pool.write, model, context=context) - obj_pool.base_action_ruled = True - + model_obj = self.pool.get(model) + if not hasattr(model_obj, 'base_action_ruled'): + model_obj.create = self._wrap_create(model_obj.create, model) + model_obj.write = self._wrap_write(model_obj.write, model) + model_obj.base_action_ruled = True return True def create(self, cr, uid, vals, context=None): res_id = super(base_action_rule, self).create(cr, uid, vals, context=context) - self._register_hook(cr, uid, [res_id], context=context) + self._register_hook(cr, [res_id]) return res_id def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, (int, long)): + ids = [ids] super(base_action_rule, self).write(cr, uid, ids, vals, context=context) - self._register_hook(cr, uid, ids, context=context) + self._register_hook(cr, ids) return True - def _check(self, cr, uid, automatic=False, use_new_cursor=False, \ - context=None): - """ - This Function is call by scheduler. - """ - rule_ids = self.search(cr, uid, [], context=context) - self._register_hook(cr, uid, rule_ids, context=context) - if context is None: - context = {} - for rule in self.browse(cr, uid, rule_ids, context=context): - model = rule.model_id.model - model_pool = self.pool.get(model) - last_run = False - if rule.last_run: - last_run = get_datetime(rule.last_run) + def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None): + """ This Function is called by scheduler. """ + context = context or {} + # retrieve all the action rules that have a trg_date_id and no precondition + action_dom = [('trg_date_id', '!=', False), ('filter_pre_id', '=', False)] + action_ids = self.search(cr, uid, action_dom, context=context) + for action in self.browse(cr, uid, action_ids, context=context): now = datetime.now() - ctx = dict(context) - if rule.filter_id and rule.model_id.model == rule.filter_id.model_id: - ctx.update(eval(rule.filter_id.context)) - obj_ids = model_pool.search(cr, uid, eval(rule.filter_id.domain), context=ctx) - else: - obj_ids = model_pool.search(cr, uid, [], context=ctx) - for obj in model_pool.browse(cr, uid, obj_ids, context=ctx): - # Calculate when this action should next occur for this object - base = False - if rule.trg_date_type=='create' and hasattr(obj, 'create_date'): - base = obj.create_date - elif rule.trg_date_type=='write' and hasattr(obj, 'write_date'): - base = obj.write_date - elif (rule.trg_date_type=='action_last' - and hasattr(obj, 'create_date')): - if hasattr(obj, 'date_action_last') and obj.date_action_last: - base = obj.date_action_last - else: - base = obj.create_date - elif (rule.trg_date_type=='deadline' - and hasattr(obj, 'date_deadline') - and obj.date_deadline): - base = obj.date_deadline - elif (rule.trg_date_type=='date' - and hasattr(obj, 'date') - and obj.date): - base = obj.date - if base: - fnct = { - 'minutes': lambda interval: timedelta(minutes=interval), - 'day': lambda interval: timedelta(days=interval), - 'hour': lambda interval: timedelta(hours=interval), - 'month': lambda interval: timedelta(months=interval), - } - base = get_datetime(base) - delay = fnct[rule.trg_date_range_type](rule.trg_date_range) - action_date = base + delay - if (not last_run or (last_run <= action_date < now)): - try: - self._action(cr, uid, [rule.id], obj, context=ctx) - self.write(cr, uid, [rule.id], {'last_run': now}, context=context) - except Exception, e: - import traceback - print traceback.format_exc() - - + last_run = get_datetime(action.last_run) if action.last_run else False - def do_check(self, cr, uid, action, obj, context=None): - """ check Action """ - if context is None: - context = {} - ok = True - if action.filter_id and action.model_id.model == action.filter_id.model_id: + # retrieve all the records that satisfy the action's condition + model = self.pool.get(action.model_id.model) + domain = [] ctx = dict(context) - ctx.update(eval(action.filter_id.context)) - obj_ids = self.pool.get(action.model_id.model).search(cr, uid, eval(action.filter_id.domain), context=ctx) - ok = ok and obj.id in obj_ids - if getattr(obj, 'user_id', False): - ok = ok and (not action.trg_user_id.id or action.trg_user_id.id==obj.user_id.id) - if getattr(obj, 'partner_id', False): - ok = ok and (not action.trg_partner_id.id or action.trg_partner_id.id==obj.partner_id.id) - ok = ok and ( - not action.trg_partner_categ_id.id or - ( - obj.partner_id.id and - (action.trg_partner_categ_id.id in map(lambda x: x.id, obj.partner_id.category_id or [])) - ) - ) - state_to = context.get('state_to', False) - state = getattr(obj, 'state', False) - if state: - ok = ok and (not action.trg_state_from or action.trg_state_from==state) - if state_to: - ok = ok and (not action.trg_state_to or action.trg_state_to==state_to) - elif action.trg_state_to: - ok = False - reg_name = action.regex_name - result_name = True - if reg_name: - ptrn = re.compile(ustr(reg_name)) - _result = ptrn.search(ustr(obj.name)) - if not _result: - result_name = False - regex_n = not reg_name or result_name - ok = ok and regex_n - return ok + if action.filter_id: + domain = eval(action.filter_id.domain) + ctx.update(eval(action.filter_id.context)) + record_ids = model.search(cr, uid, domain, context=ctx) - def do_action(self, cr, uid, action, obj, context=None): - """ Do Action """ - if context is None: - context = {} - ctx = dict(context) - model_obj = self.pool.get(action.model_id.model) - action_server_obj = self.pool.get('ir.actions.server') - if action.server_action_ids: - ctx.update({'active_model': action.model_id.model, 'active_id':obj.id, 'active_ids':[obj.id]}) - action_server_obj.run(cr, uid, [x.id for x in action.server_action_ids], context=ctx) + # determine when action should occur for the records + date_field = action.trg_date_id.name + if date_field == 'date_action_last' and 'create_date' in model._all_columns: + get_record_dt = lambda record: record[date_field] or record.create_date + else: + get_record_dt = lambda record: record[date_field] - write = {} - if hasattr(obj, 'user_id') and action.act_user_id: - write['user_id'] = action.act_user_id.id - if hasattr(obj, 'date_action_last'): - write['date_action_last'] = time.strftime('%Y-%m-%d %H:%M:%S') - if hasattr(obj, 'state') and action.act_state: - write['state'] = action.act_state + delay = DATE_RANGE_FUNCTION[action.trg_date_range_type](action.trg_date_range) - model_obj.write(cr, uid, [obj.id], write, context) - if hasattr(obj, 'state') and hasattr(obj, 'message_post') and action.act_state: - model_obj.message_post(cr, uid, [obj], _(action.act_state), context=context) - - if hasattr(obj, 'message_subscribe') and action.act_followers: - exits_followers = [x.id for x in obj.message_follower_ids] - new_followers = [x.id for x in action.act_followers if x.id not in exits_followers] - if new_followers: - model_obj.message_subscribe(cr, uid, [obj.id], new_followers, context=context) - return True + # process action on the records that should be executed + for record in model.browse(cr, uid, record_ids, context=context): + record_dt = get_record_dt(record) + if not record_dt: + continue + action_dt = get_datetime(record_dt) + delay + if last_run and (last_run <= action_dt < now) or (action_dt < now): + try: + self._process(cr, uid, action, [record.id], context=context) + except Exception: + import traceback + _logger.error(traceback.format_exc()) - def _action(self, cr, uid, ids, objects, scrit=None, context=None): - """ Do Action """ - if context is None: - context = {} - - context.update({'action': True}) - if not scrit: - scrit = [] - if not isinstance(objects, list): - objects = [objects] - for action in self.browse(cr, uid, ids, context=context): - for obj in objects: - if self.do_check(cr, uid, action, obj, context=context): - self.do_action(cr, uid, action, obj, context=context) - - context.update({'action': False}) - return True - -base_action_rule() - -class actions_server(osv.osv): - _inherit = 'ir.actions.server' - _columns = { - 'action_rule_id': fields.many2one("base.action.rule", string="Action Rule") - } -actions_server() - -class ir_cron(osv.osv): - _inherit = 'ir.cron' - _init_done = False - - def _poolJobs(self, db_name, check=False): - if not self._init_done: - self._init_done = True - try: - db = pooler.get_db(db_name) - except: - return False - cr = db.cursor() - try: - next = datetime.now().strftime('%Y-%m-%d %H:00:00') - # Putting nextcall always less than current time in order to call it every time - cr.execute('UPDATE ir_cron set nextcall = \'%s\' where numbercall<>0 and active and model=\'base.action.rule\' ' % (next)) - finally: - cr.commit() - cr.close() - - super(ir_cron, self)._poolJobs(db_name, check=check) - -ir_cron() - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + action.write({'last_run': now.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) diff --git a/addons/base_action_rule/base_action_rule_view.xml b/addons/base_action_rule/base_action_rule_view.xml index bb711be9928..25ea4048bad 100644 --- a/addons/base_action_rule/base_action_rule_view.xml +++ b/addons/base_action_rule/base_action_rule_view.xml @@ -4,57 +4,55 @@ - - - base.action.rule.form + + + base.action.rule.form base.action.rule
- - - - - - - +
-
+
- - - - base.action.rule.tree + + + base.action.rule.tree base.action.rule - + - - - + + Automated Actions base.action.rule @@ -109,6 +104,5 @@ - diff --git a/addons/base_action_rule/test_models.py b/addons/base_action_rule/test_models.py new file mode 100644 index 00000000000..9469b42c705 --- /dev/null +++ b/addons/base_action_rule/test_models.py @@ -0,0 +1,32 @@ +from osv import osv, fields + +AVAILABLE_STATES = [ + ('draft', 'New'), + ('cancel', 'Cancelled'), + ('open', 'In Progress'), + ('pending', 'Pending'), + ('done', 'Closed') +] + +class lead_test(osv.Model): + _name = "base.action.rule.lead.test" + + _columns = { + 'name': fields.char('Subject', size=64, required=True, select=1), + 'user_id': fields.many2one('res.users', 'Responsible'), + 'state': fields.selection(AVAILABLE_STATES, string="Status", readonly=True), + 'active': fields.boolean('Active', required=False), + 'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null'), + 'date_action_last': fields.datetime('Last Action', readonly=1), + } + + _defaults = { + 'state' : 'draft', + 'active' : True, + } + + def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs): + pass + + def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None): + pass diff --git a/addons/base_action_rule/tests/__init__.py b/addons/base_action_rule/tests/__init__.py new file mode 100644 index 00000000000..a2de50226da --- /dev/null +++ b/addons/base_action_rule/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import base_action_rule_test + +checks = [ + base_action_rule_test, +] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/base_action_rule/tests/base_action_rule_test.py b/addons/base_action_rule/tests/base_action_rule_test.py new file mode 100644 index 00000000000..36c928a84e3 --- /dev/null +++ b/addons/base_action_rule/tests/base_action_rule_test.py @@ -0,0 +1,146 @@ +from openerp import SUPERUSER_ID +from openerp.tests import common +from .. import test_models + +class base_action_rule_test(common.TransactionCase): + + def setUp(self): + """*****setUp*****""" + super(base_action_rule_test, self).setUp() + cr, uid = self.cr, self.uid + self.demo = self.registry('ir.model.data').get_object(cr, uid, 'base', 'user_demo').id + self.admin = SUPERUSER_ID + self.model = self.registry('base.action.rule.lead.test') + self.base_action_rule = self.registry('base.action.rule') + + def create_filter_done(self, cr, uid, context=None): + filter_pool = self.registry('ir.filters') + return filter_pool.create(cr, uid, { + 'name': "Lead is in done state", + 'is_default': False, + 'model_id': 'base.action.rule.lead.test', + 'domain' : "[('state','=','done')]", + }, context=context) + + def create_filter_draft(self, cr, uid, context=None): + filter_pool = self.registry('ir.filters') + return filter_pool.create(cr, uid, { + 'name': "Lead is in draft state", + 'is_default': False, + 'model_id': "base.action.rule.lead.test", + 'domain' : "[('state','=','draft')]", + }, context=context) + + def create_lead_test_1(self, cr, uid, context=None): + """ + Create a new lead_test + """ + return self.model.create(cr, uid, { + 'name': "Lead Test 1", + 'user_id': self.admin, + }, context=context) + + def create_rule(self, cr, uid, filter_id=False, filter_pre_id=False, context=None): + """ + The "Rule 1" says that when a lead goes to the 'draft' state, the responsible for that lead changes to user "demo" + """ + return self.base_action_rule.create(cr,uid,{ + 'name' : "Rule 1", + 'model_id': self.registry('ir.model').search(cr, uid, [('model','=','base.action.rule.lead.test')], context=context)[0], + 'active' : 1, + 'filter_pre_id' : filter_pre_id, + 'filter_id' : filter_id, + 'act_user_id': self.demo, + }, context=context) + + def delete_rules(self, cr, uid, context=None): + """ delete all the rules on model 'base.action.rule.lead.test' """ + action_ids = self.base_action_rule.search(cr, uid, [('model', '=', self.model._name)], context=context) + return self.base_action_rule.unlink(cr, uid, action_ids, context=context) + + def test_00_check_to_state_draft_pre(self): + """ + Check that a new record (with state = draft) doesn't change its responsible when there is a precondition filter which check that the state is draft. + """ + cr, uid = self.cr, self.uid + filter_draft = self.create_filter_draft(cr, uid) + self.create_rule(cr, uid, filter_pre_id=filter_draft) + new_lead_id = self.create_lead_test_1(cr, uid) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'draft') + self.assertEquals(new_lead.user_id.id, self.admin) + self.delete_rules(cr, uid) + + def test_01_check_to_state_draft_post(self): + """ + Check that a new record (with state = draft) changes its responsible when there is a postcondition filter which check that the state is draft. + """ + cr, uid = self.cr, self.uid + filter_draft = self.create_filter_draft(cr, uid) + self.create_rule(cr, uid, filter_id=filter_draft) + new_lead_id = self.create_lead_test_1(cr, uid) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'draft') + self.assertEquals(new_lead.user_id.id, self.demo) + self.delete_rules(cr, uid) + + def test_02_check_from_draft_to_done_with_steps(self): + """ + A new record will be created and will goes from draft to done state via the other states (open, pending and cancel) + We will create a rule that says in precondition that the record must be in the "draft" state while a postcondition filter says + that the record will be done. If the state goes from 'draft' to 'done' the responsible will change. If those two conditions aren't + verified, the responsible will stay the same + The responsible in that test will never change + """ + cr, uid = self.cr, self.uid + filter_draft = self.create_filter_draft(cr, uid) + filter_done = self.create_filter_done(cr, uid) + self.create_rule(cr, uid, filter_pre_id=filter_draft, filter_id=filter_done) + new_lead_id = self.create_lead_test_1(cr, uid) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'draft') + self.assertEquals(new_lead.user_id.id, self.admin) + """ change the state of new_lead to open and check that responsible doen't change""" + new_lead.write({'state': 'open'}) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'open') + self.assertEquals(new_lead.user_id.id, self.admin) + """ change the state of new_lead to pending and check that responsible doen't change""" + new_lead.write({'state': 'pending'}) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'pending') + self.assertEquals(new_lead.user_id.id, self.admin) + """ change the state of new_lead to cancel and check that responsible doen't change""" + new_lead.write({'state': 'cancel'}) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'cancel') + self.assertEquals(new_lead.user_id.id, self.admin) + """ change the state of new_lead to done and check that responsible doen't change """ + new_lead.write({'state': 'done'}) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'done') + self.assertEquals(new_lead.user_id.id, self.admin) + self.delete_rules(cr, uid) + + def test_02_check_from_draft_to_done_without_steps(self): + """ + A new record will be created and will goes from draft to done in one operation + We will create a rule that says in precondition that the record must be in the "draft" state while a postcondition filter says + that the record will be done. If the state goes from 'draft' to 'done' the responsible will change. If those two conditions aren't + verified, the responsible will stay the same + The responsible in that test will change to user "demo" + """ + cr, uid = self.cr, self.uid + filter_draft = self.create_filter_draft(cr, uid) + filter_done = self.create_filter_done(cr, uid) + self.create_rule(cr, uid, filter_pre_id=filter_draft, filter_id=filter_done) + new_lead_id = self.create_lead_test_1(cr, uid) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'draft') + self.assertEquals(new_lead.user_id.id, self.admin) + """ change the state of new_lead to done and check that responsible change to Demo_user""" + new_lead.write({'state': 'done'}) + new_lead = self.model.browse(cr, uid, new_lead_id) + self.assertEquals(new_lead.state, 'done') + self.assertEquals(new_lead.user_id.id, self.demo) + self.delete_rules(cr, uid) diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py index 2430ed4837b..edf9db166af 100644 --- a/addons/base_status/base_stage.py +++ b/addons/base_status/base_stage.py @@ -262,28 +262,3 @@ class base_stage(object): if values_to_update: self.write(cr, uid, ids, values_to_update, context=context) return True - - def write(self, cr, uid, ids, vals, context=None): - res = super(base_stage,self).write(cr, uid, ids, vals, context) - if vals.get('stage_id'): - for case in self.browse(cr, uid, ids, context=context): - self._action(cr, uid, case, case.stage_id.state, context=context) - return res - - def _action(self, cr, uid, cases, state_to, scrit=None, context=None): - if context is None: - context = {} - context['state_to'] = state_to - rule_obj = self.pool.get('base.action.rule') - if not rule_obj: - return True - model_obj = self.pool.get('ir.model') - model_ids = model_obj.search(cr, uid, [('model','=',self._name)], context=context) - rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])], context=context) - return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context) - - def _check(self, cr, uid, ids=False, context=None): - """ Function called by the scheduler to process cases for date actions. - Must be overriden by inheriting classes. - """ - return True diff --git a/addons/base_status/base_state.py b/addons/base_status/base_state.py index aed78a005e7..b856bbd74ad 100644 --- a/addons/base_status/base_state.py +++ b/addons/base_status/base_state.py @@ -106,7 +106,7 @@ class base_state(object): else: raise osv.except_osv(_('Error !'), _('You can not escalate, you are already at the top level regarding your sales-team category.')) self.write(cr, uid, [case.id], data, context=context) - self._action(cr, uid, cases, 'escalate', context=context) + case.case_escalate_send_note(parent_id, context=context) return True def case_open(self, cr, uid, ids, context=None): @@ -152,16 +152,50 @@ class base_state(object): if update_values is None: update_values = {} update_values['state'] = state_name - res = self.write(cr, uid, ids, update_values, context=context) - self._action(cr, uid, cases, state_name, context=context) - return res + return self.write(cr, uid, ids, update_values, context=context) + + # ****************************** + # Notifications + # ****************************** + + def case_get_note_msg_prefix(self, cr, uid, id, context=None): + return '' - def _action(self, cr, uid, cases, state_to, scrit=None, context=None): - if context is None: - context = {} - context['state_to'] = state_to - rule_obj = self.pool.get('base.action.rule') - model_obj = self.pool.get('ir.model') - model_ids = model_obj.search(cr, uid, [('model','=',self._name)]) - rule_ids = rule_obj.search(cr, uid, [('model_id','=',model_ids[0])]) - return rule_obj._action(cr, uid, rule_ids, cases, scrit=scrit, context=context) + def case_open_send_note(self, cr, uid, ids, context=None): + for id in ids: + msg = _('%s has been opened.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True + + def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None): + for id in ids: + if new_section: + msg = '%s has been escalated to %s.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name) + else: + msg = '%s has been escalated.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True + + def case_close_send_note(self, cr, uid, ids, context=None): + for id in ids: + msg = _('%s has been closed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True + + def case_cancel_send_note(self, cr, uid, ids, context=None): + for id in ids: + msg = _('%s has been canceled.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True + + def case_pending_send_note(self, cr, uid, ids, context=None): + for id in ids: + msg = _('%s is now pending.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True + + def case_reset_send_note(self, cr, uid, ids, context=None): + for id in ids: + msg = _('%s has been renewed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context)) + self.message_post(cr, uid, [id], body=msg, context=context) + return True diff --git a/addons/crm/__init__.py b/addons/crm/__init__.py index 729d54e0fc6..d78fb09eae7 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -20,7 +20,6 @@ ############################################################################## import crm -import crm_action_rule import crm_segmentation import crm_lead import crm_meeting diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index b4ec404470c..4a792abc9ee 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -77,7 +77,6 @@ Dashboard for CRM will include: 'crm_view.xml', - 'crm_action_rule_view.xml', 'crm_lead_view.xml', 'crm_lead_menu.xml', @@ -108,7 +107,6 @@ Dashboard for CRM will include: 'test/process/lead2opportunity_assign_salesmen.yml', 'test/process/merge_opportunity.yml', 'test/process/cancel_lead.yml', - 'test/process/action_rule.yml', 'test/process/segmentation.yml', 'test/process/phonecalls.yml', 'test/ui/crm_demo.yml', diff --git a/addons/crm/crm_action_rule.py b/addons/crm/crm_action_rule.py deleted file mode 100644 index b17ba5665d7..00000000000 --- a/addons/crm/crm_action_rule.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import re -from openerp import tools - -from openerp.tools.translate import _ -from openerp.tools import ustr -from openerp.osv import fields -from openerp.osv import osv - -import crm - -class base_action_rule(osv.osv): - """ Base Action Rule """ - _inherit = 'base.action.rule' - _description = 'Action Rules' - - _columns = { - 'trg_section_id': fields.many2one('crm.case.section', 'Sales Team'), - 'trg_max_history': fields.integer('Maximum Communication History'), - 'trg_categ_id': fields.many2one('crm.case.categ', 'Category'), - 'regex_history' : fields.char('Regular Expression on Case History', size=128), - 'act_section_id': fields.many2one('crm.case.section', 'Set Team to'), - 'act_categ_id': fields.many2one('crm.case.categ', 'Set Category to'), - } - - def do_check(self, cr, uid, action, obj, context=None): - ok = super(base_action_rule, self).do_check(cr, uid, action, obj, context=context) - - if hasattr(obj, 'section_id'): - ok = ok and (not action.trg_section_id or action.trg_section_id.id == obj.section_id.id) - if hasattr(obj, 'categ_ids'): - ok = ok and (not action.trg_categ_id or action.trg_categ_id.id in [x.id for x in obj.categ_ids]) - - #Cheking for history - regex = action.regex_history - if regex: - res = False - ptrn = re.compile(ustr(regex)) - for history in obj.message_ids: - _result = ptrn.search(ustr(history.subject)) - if _result: - res = True - break - ok = ok and res - - if action.trg_max_history: - res_count = False - history_ids = filter(lambda x: x.email_from, obj.message_ids) - if len(history_ids) <= action.trg_max_history: - res_count = True - ok = ok and res_count - return ok - - def do_action(self, cr, uid, action, obj, context=None): - res = super(base_action_rule, self).do_action(cr, uid, action, obj, context=context) - model_obj = self.pool.get(action.model_id.model) - write = {} - if hasattr(action, 'act_section_id') and action.act_section_id: - write['section_id'] = action.act_section_id.id - - if hasattr(action, 'act_categ_id') and action.act_categ_id: - write['categ_ids'] = [(4, action.act_categ_id.id)] - - model_obj.write(cr, uid, [obj.id], write, context) - return res - - def state_get(self, cr, uid, context=None): - """Gets available states for crm""" - res = super(base_action_rule, self).state_get(cr, uid, context=context) - return res + crm.AVAILABLE_STATES - - def priority_get(self, cr, uid, context=None): - res = super(base_action_rule, self).priority_get(cr, uid, context=context) - return res + crm.AVAILABLE_PRIORITIES - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/crm_action_rule_demo.xml b/addons/crm/crm_action_rule_demo.xml index 6885aba9bff..bf1f7d4082f 100644 --- a/addons/crm/crm_action_rule_demo.xml +++ b/addons/crm/crm_action_rule_demo.xml @@ -27,7 +27,7 @@ Description: [[object.description]] 1 - create + 5 day @@ -40,18 +40,27 @@ Description: [[object.description]] [('country_id','=','United States')] + + Set team to Sales Department + + True + ir.actions.server + code + sales_team = self.pool.get('ir.model.data').get_object(cr, uid, 'crm', 'section_sales_department') +object.write({'section_id': sales_team.id}) + + Set Auto Followers on leads which are urgent and come from USA. 2 - urgent.* - - create + 0 minutes + diff --git a/addons/crm/crm_action_rule_view.xml b/addons/crm/crm_action_rule_view.xml deleted file mode 100644 index e21b74de382..00000000000 --- a/addons/crm/crm_action_rule_view.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - base.action.rule.form.inherit - base.action.rule - - - - - - - - - - - - - - - - - - - - diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index 17bef812e4b..74394d3c5ce 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -30,7 +30,7 @@ access_res_partner,res.partner.crm.user,base.model_res_partner,base.group_sale_s access_res_partner_category,res.partner.category.crm.user,base.model_res_partner_category,base.group_sale_salesman,1,1,1,0 mail_mailgate_thread,mail.thread,mail.model_mail_thread,base.group_sale_salesman,1,1,1,1 access_crm_case_categ_manager,crm.case.categ manager,model_crm_case_categ,base.group_sale_manager,1,1,1,1 -access_base_action_rule_manager,base.action.rule manager,model_base_action_rule,base.group_sale_manager,1,1,1,1 +access_base_action_rule_manager,base.action.rule manager,base_action_rule.model_base_action_rule,base.group_sale_manager,1,1,1,1 access_crm_lead_report_user,crm.lead.report user,model_crm_lead_report,base.group_sale_salesman,1,1,1,1 access_res_partner_bank_type_crm_user,res.partner.bank.type.crm.user,base.model_res_partner_bank_type,base.group_sale_salesman,1,0,0,0 access_crm_lead_partner_manager,crm.lead.partner.manager,model_crm_lead,base.group_partner_manager,1,0,0,0 diff --git a/addons/crm/test/process/action_rule.yml b/addons/crm/test/process/action_rule.yml deleted file mode 100644 index f76e6c250bc..00000000000 --- a/addons/crm/test/process/action_rule.yml +++ /dev/null @@ -1,20 +0,0 @@ -- - I create a record rule. -- - !python {model: base.action.rule}: | - model_ids = self.pool.get("ir.model").search(cr, uid, [('model', '=', 'crm.lead')]) - from datetime import datetime - new_id = self.create(cr, uid, {'name': 'New Rule', 'model_id': model_ids[0], 'trg_user_id': ref('base.user_root'), 'trg_partner_id': ref('base.res_partner_1'), 'act_user_id': ref('base.user_demo') }) - self._check(cr, uid) -- - I create a new lead to check the record rule. -- - !record {model: crm.lead, id: crm_lead_test_rules_id}: - name: 'Test lead rules' - partner_id: base.res_partner_1 -- - I check if the record rule is applied and the responsible is changed. -- - !python {model: crm.lead}: | - lead_user = self.browse(cr, uid, ref('crm_lead_test_rules_id')) - assert lead_user.user_id.id == ref('base.user_demo'), "Responsible of lead is not changed." diff --git a/addons/note/static/src/css/note.css b/addons/note/static/src/css/note.css index 303e35e9146..fe9c8510e7d 100644 --- a/addons/note/static/src/css/note.css +++ b/addons/note/static/src/css/note.css @@ -2,13 +2,15 @@ .oe_kanban_column .note_text_line_through { text-decoration: line-through; } - .openerp .oe_form .oe_form_field.oe_memo { margin: 0; padding: 0px; width: 100%; min-height: 200px; } +.openerp .oe_form .oe_pad.oe_memo { + width: auto; +} .openerp .oe_form .oe_form_field.oe_memo .cleditorMain { border: none; padding: 0px; diff --git a/addons/note/static/src/css/note.sass b/addons/note/static/src/css/note.sass index 3fafb7b7c21..6fa76b32d43 100644 --- a/addons/note/static/src/css/note.sass +++ b/addons/note/static/src/css/note.sass @@ -17,6 +17,8 @@ .openerp .oe_form + .oe_pad.oe_memo + width: auto .oe_form_field.oe_memo margin: 0 padding: 0px diff --git a/addons/note_pad/note_pad_view.xml b/addons/note_pad/note_pad_view.xml index 9e45a3952f1..0032905b6b5 100644 --- a/addons/note_pad/note_pad_view.xml +++ b/addons/note_pad/note_pad_view.xml @@ -7,7 +7,7 @@ - + diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 9e1b73e1f3b..b96047340ad 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -4,6 +4,13 @@ openerp.pad = function(instance) { template: 'FieldPad', configured: false, content: "", + start: function() { + this._super(); + var self = this; + this.on('change:effective_readonly',this,function(){ + self.renderElement(); + }); + }, render_value: function() { var self = this; var _super = _.bind(this._super, this); @@ -27,6 +34,9 @@ openerp.pad = function(instance) { renderElement: function(){ var self = this; var value = this.get('value'); + if (this.pad_loading_request) { + this.pad_loading_request.abort(); + } if(!_.str.startsWith(value,'http')){ this.configured = false; this.content = ""; @@ -36,7 +46,8 @@ openerp.pad = function(instance) { this.content = ''; }else{ this.content = '
... Loading pad ...
'; - $.get(value+'/export/html').success(function(data){ + this.pad_loading_request = $.get(value+'/export/html') + .done(function(data){ groups = /\<\s*body\s*\>(.*?)\<\s*\/body\s*\>/.exec(data); data = (groups || []).length >= 2 ? groups[1] : ''; self.$('.oe_pad_content').html('
'); @@ -51,9 +62,6 @@ openerp.pad = function(instance) { this.$('.oe_pad_switch').click(function(){ self.$el.toggleClass('oe_pad_fullscreen'); }); - this.on('change:effective_readonly',this,function(){ - self.renderElement(); - }); }, });