diff --git a/addons/account_analytic_analysis/account_analytic_analysis_view.xml b/addons/account_analytic_analysis/account_analytic_analysis_view.xml
index 1f1117aec1b..eaadbe0fa83 100644
--- a/addons/account_analytic_analysis/account_analytic_analysis_view.xml
+++ b/addons/account_analytic_analysis/account_analytic_analysis_view.xml
@@ -68,11 +68,12 @@
No order to invoice, create
+
+ />
|
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 4d510853660..de9cebdd09b 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -26,6 +26,7 @@ from openerp import netsvc
from openerp.osv import fields, osv
import openerp.addons.decimal_precision as dp
from openerp.tools.translate import _
+from openerp.tools import float_compare
class res_company(osv.osv):
_inherit = "res.company"
@@ -599,12 +600,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
@@ -1083,17 +1083,19 @@ class account_voucher(osv.osv):
voucher_brw = self.pool.get('account.voucher').browse(cr, uid, voucher_id, context)
ctx = context.copy()
ctx.update({'date': voucher_brw.date})
+ prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
for line in voucher_brw.line_ids:
#create one move line per voucher line where amount is not 0.0
- if not line.amount:
+ # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value)
+ if not line.amount and not (line.move_line_id and not float_compare(line.move_line_id.debit, line.move_line_id.credit, precision_rounding=prec) and not float_compare(line.move_line_id.debit, 0.0, precision_rounding=prec)):
continue
# convert the amount set on the voucher line into the currency of the voucher's company
amount = self._convert_amount(cr, uid, line.untax_amount or line.amount, voucher_brw.id, context=ctx)
# if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
# currency rate difference
if line.amount == line.amount_unreconciled:
- if not line.move_line_id.amount_residual:
- raise osv.except_osv(_('Wrong bank statement line'),_("You have to delete the bank statement line which the payment was reconciled to manually. Please check the payment of the partner %s by the amount of %s.")%(line.voucher_id.partner_id.name, line.voucher_id.amount))
+ if not line.move_line_id:
+ raise osv.except_osv(_('Wrong voucher line'),_("The invoice you are willing to pay is not valid anymore."))
sign = voucher_brw.type in ('payment', 'purchase') and -1 or 1
currency_rate_difference = sign * (line.move_line_id.amount_residual - amount)
else:
diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py
index c9454c340f0..07552172d9b 100644
--- a/addons/analytic/analytic.py
+++ b/addons/analytic/analytic.py
@@ -299,18 +299,12 @@ class account_analytic_account(osv.osv):
if name:
account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
if not account_ids:
- names=map(lambda i : i.strip(),name.split('/'))
- for i in range(len(names)):
- dom=[('name', operator, names[i])]
- if i>0:
- dom+=[('id','child_of',account_ids)]
- account_ids = self.search(cr, uid, dom, limit=limit, context=context)
- newacc = account_ids
- while newacc:
- newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
- account_ids += newacc
- if args:
- account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
+ dom = []
+ for name2 in name.split('/'):
+ name = name2.strip()
+ account_ids = self.search(cr, uid, dom + [('name', 'ilike', name)] + args, limit=limit, context=context)
+ if not account_ids: break
+ dom = [('parent_id','in',account_ids)]
else:
account_ids = self.search(cr, uid, args, limit=limit, context=context)
return self.name_get(cr, uid, account_ids, context=context)
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/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py
index 6381c507f97..7557bb1abfe 100644
--- a/addons/hr_expense/hr_expense.py
+++ b/addons/hr_expense/hr_expense.py
@@ -168,6 +168,8 @@ class hr_expense_expense(osv.osv):
journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id})
if journal_id:
journal = account_journal.browse(cr, uid, journal_id, context=context)
+ if not journal:
+ raise osv.except_osv(_('Error!'), _("No expense journal found. Please make sure you have a journal with type 'purchase' configured."))
for line in exp.line_ids:
if line.product_id:
acc = line.product_id.property_account_expense
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 296c212e0fb..f9406699190 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -886,9 +886,9 @@ class mail_thread(osv.AbstractModel):
return mail_message.create(cr, uid, values, context=context)
- def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False,
- attachment_ids=None, context=None, content_subtype='plaintext',
- extra_email=[], **kwargs):
+ def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
+ attachment_ids=None, extra_emails=None, content_subtype='plaintext',
+ context=None, **kwargs):
""" Wrapper on message_post, used for user input :
- mail gateway
- quick reply in Chatter (refer to mail.js), not
@@ -902,30 +902,41 @@ class mail_thread(osv.AbstractModel):
to the related document. Should only be set by Chatter.
- extra_email: [ 'Fabien ', 'al@openerp.com' ]
"""
+ partner_obj = self.pool.get('res.partner')
+ mail_message_obj = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
+ extra_emails = extra_emails or []
- # 1. Pre-processing: body, partner_ids, type and subtype
- if content_subtype == 'plaintext':
- body = tools.plaintext2html(body)
+ # 1.A.1: pre-process partners and incoming extra_emails
+ partner_ids = set([])
+ for email in extra_emails:
+ partner_id = partner_obj.find_or_create(cr, uid, email, context=context)
+ # link mail with this from mail to the new partner id
+ partner_msg_ids = mail_message_obj.search(cr, SUPERUSER_ID, [('email_from', '=', email), ('author_id', '=', False)], context=context)
+ if partner_id and partner_msg_ids:
+ mail_message_obj.write(cr, SUPERUSER_ID, partner_msg_ids, {'email_from': None, 'author_id': partner_id}, context=context)
+ partner_ids.add((4, partner_id))
+ if partner_ids:
+ self.message_subscribe(cr, uid, [thread_id], [item[1] for item in partner_ids], context=context)
- for partner in extra_email:
- part_ids = self.pool.get('res.partner').search(cr, uid, [('email', '=', partner)], context=context)
- if not part_ids:
- part_ids = [self.pool.get('res.partner').name_create(cr, uid, partner, context=context)[0]]
- self.message_subscribe(cr, uid, [thread_id], part_ids, context=context)
-
- partner_ids = kwargs.pop('partner_ids', [])
+ # 1.A.2: add recipients of parent message
if parent_id:
- parent_message = self.pool.get('mail.message').browse(cr, uid, parent_id, context=context)
- partner_ids += [(4, partner.id) for partner in parent_message.partner_ids]
+ parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
+ partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
# TDE FIXME HACK: mail.thread -> private message
if self._name == 'mail.thread' and parent_message.author_id.id:
- partner_ids.append((4, parent_message.author_id.id))
+ partner_ids.add((4, parent_message.author_id.id))
- message_type = kwargs.pop('type', 'comment')
- message_subtype = kwargs.pop('subtype', 'mail.mt_comment')
+ # 1.A.3: add specified recipients
+ partner_ids |= set(kwargs.pop('partner_ids', []))
- # 2. Pre-processing: free attachments linked to the model
+ # 1.B: handle body, message_type and message_subtype
+ if content_subtype == 'plaintext':
+ body = tools.plaintext2html(body)
+ msg_type = kwargs.pop('type', 'comment')
+ msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
+
+ # 2. Pre-processing: attachments
# HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
if attachment_ids:
# TDE FIXME (?): when posting a private message, we use mail.thread as a model
@@ -943,13 +954,12 @@ class mail_thread(osv.AbstractModel):
ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
else:
attachment_ids = []
+ attachment_ids = [(4, id) for id in attachment_ids]
# 3. Post message
- new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type,
- subtype=message_subtype, parent_id=parent_id, attachment_ids=[(4, id) for id in attachment_ids],
- context=context, partner_ids=partner_ids, **kwargs)
-
- return new_message_id
+ return self.message_post(cr, uid, thread_id=thread_id, body=body,
+ type=msg_type, subtype=msg_subtype, parent_id=parent_id,
+ attachment_ids=attachment_ids, partner_ids=partner_ids, context=context, **kwargs)
#------------------------------------------------------
# Followers API
@@ -969,7 +979,12 @@ class mail_thread(osv.AbstractModel):
def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
""" Add partners to the records followers. """
- self.check_access_rights(cr, uid, 'read')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ if set(partner_ids) == set([user_pid]):
+ self.check_access_rights(cr, uid, 'read')
+ else:
+ self.check_access_rights(cr, uid, 'write')
+
self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
# if subtypes are not specified (and not set to a void list), fetch default ones
if subtype_ids is None:
@@ -991,7 +1006,11 @@ class mail_thread(osv.AbstractModel):
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
""" Remove partners from the records followers. """
- self.check_access_rights(cr, uid, 'read')
+ user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+ if set(partner_ids) == set([user_pid]):
+ self.check_access_rights(cr, uid, 'read')
+ else:
+ self.check_access_rights(cr, uid, 'write')
return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
def message_subscribe_from_parent(self, cr, uid, ids, updated_fields, context=None):
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index cf9a2272a98..cfcb1106afc 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -42,8 +42,7 @@ class res_partner_mail(osv.Model):
'notification_email_send': lambda *args: 'comment'
}
- def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
- subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
+ def message_post(self, cr, uid, thread_id, **kwargs):
""" Override related to res.partner. In case of email message, set it as
private:
- add the target partner in the message partner_ids
@@ -58,7 +57,6 @@ class res_partner_mail(osv.Model):
partner_ids.append((4, thread_id))
kwargs['partner_ids'] = partner_ids
thread_id = False
- return super(res_partner_mail, self).message_post(cr, uid, thread_id, body=body, subject=subject,
- type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
+ return super(res_partner_mail, self).message_post(cr, uid, thread_id, **kwargs)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 4c6bf6e150d..160335fba2a 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -116,32 +116,26 @@ class res_users(osv.Model):
alias_pool.unlink(cr, uid, alias_ids, context=context)
return res
- def message_post_user_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False,
- attachment_ids=None, context=None, content_subtype='plaintext', **kwargs):
- """ Redirect the posting of message on res.users to the related partner.
- This is done because when giving the context of Chatter on the
- various mailboxes, we do not have access to the current partner_id.
- We therefore post on the user and redirect on its partner. """
+ def _message_post_get_pid(self, cr, uid, thread_id, context=None):
assert thread_id, "res.users does not support posting global messages"
if context and 'thread_model' in context:
context['thread_model'] = 'res.partner'
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id[0]
- partner_id = self.browse(cr, uid, thread_id).partner_id.id
- return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, body=body, subject=subject,
- parent_id=parent_id, attachment_ids=attachment_ids, context=context, content_subtype=content_subtype, **kwargs)
+ return self.browse(cr, uid, thread_id).partner_id.id
+
+ def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
+ """ Redirect the posting of message on res.users to the related partner.
+ This is done because when giving the context of Chatter on the
+ various mailboxes, we do not have access to the current partner_id. """
+ partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
+ return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
- various mailboxes, we do not have access to the current partner_id.
- We therefore post on the user and redirect on its partner. """
- assert thread_id, "res.users does not support posting global messages"
- if context and 'thread_model' in context:
- context['thread_model'] = 'res.partner'
- if isinstance(thread_id, (list, tuple)):
- thread_id = thread_id[0]
- partner_id = self.browse(cr, uid, thread_id).partner_id.id
+ various mailboxes, we do not have access to the current partner_id. """
+ partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
return self.pool.get('res.partner').message_post(cr, uid, partner_id, context=context, **kwargs)
def message_update(self, cr, uid, ids, msg_dict, update_vals=None, context=None):
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index ea03e43edcc..6a75b513bc4 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -271,6 +271,19 @@
.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{
display: block;
}
+.openerp .oe_mail .oe_msg_composer .oe_emails_from{
+ font-size: 12px;
+ margin-left: 20px;
+ margin-bottom: 2px;
+}
+.openerp .oe_mail .oe_msg_composer .oe_emails_from label{
+ vertical-align: middle;
+ display: block;
+ line-height: 14px;
+}
+.openerp .oe_mail .oe_msg_composer .oe_emails_from input{
+ vertical-align: middle;
+}
.openerp .oe_mail .oe_attachment{
display: inline-block;
width: 100px;
@@ -483,8 +496,8 @@
}
.openerp .oe_mail .oe_msg_content .oe_mail_list_recipients{
font-size: 12px;
- margin-top: 4px;
- margin-bottom: 4px;
+ margin: 4px 0 4px 20px;
+ text-indent: -20px;
}
/* ---------------- HIDDEN MESSAGES ------------------ */
@@ -596,6 +609,14 @@
margin-right:4px;
border-radius: 2px;
}
+.openerp .oe_followers .oe_remove_follower{
+ cursor: pointer;
+ float: right;
+}
+
+.openerp .oe_followers .oe_show_more{
+ cursor: pointer;
+}
/* ---------------- MESSAGES BODY ------------------ */
.openerp .oe_mail .oe_msg_content .oe_blockquote,
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index 37fafe4ad01..09142368c99 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -369,6 +369,7 @@ openerp.mail = function (session) {
this._super(parent, datasets, options);
this.show_compact_message = false;
this.show_delete_attachment = true;
+ this.emails_from = [];
},
start: function () {
@@ -478,9 +479,9 @@ openerp.mail = function (session) {
this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) );
this.$('.oe_post').on('click', _.bind( this.on_message_post, this) );
this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') );
-
/* stack for don't close the compose form if the user click on a button */
- this.$('.oe_msg_footer, .oe_mail_list_recipients').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
+ this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
+ this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
var ev_stay = {};
ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; };
this.$('textarea').on(ev_stay);
@@ -491,6 +492,8 @@ openerp.mail = function (session) {
// event: delete child attachments off the oe_msg_attachment_list box
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
+
+ this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from);
},
on_compose_fullmail: function (default_composition_mode) {
@@ -559,46 +562,79 @@ openerp.mail = function (session) {
}
},
- /*post a message and fetch the message*/
+ check_recipient_partners: function (emails) {
+ var self = this;
+ var deferreds = [];
+ var ds_partner = new session.web.DataSetSearch(this, 'res.partner');
+ _.each(emails, function (email) {
+ ds_partner.call('search', [[['email', '=', email]]]).then(function (partner_ids) {
+ if (!partner_ids.length) {
+ var deferred = $.Deferred();
+ var pop = new session.web.form.FormOpenPopup(this);
+ pop.show_element(
+ 'res.partner',
+ 0,
+ {
+ 'default_email': email,
+ 'force_email': true,
+ 'ref': "compound_context",
+ },
+ {
+ title: _t("Please complete partner's informations"),
+ }
+ );
+ pop.on('write_completed, closed', self, function () {
+ deferred.resolve();
+ });
+ deferreds.push(deferred);
+ }
+ });
+ });
+ return $.when.apply( $, deferreds );
+ },
+
on_message_post: function (event) {
var self = this;
-
- var comment_node = this.$('textarea');
- var body = comment_node.val();
-
- if (this.do_check_attachment_upload() && (this.attachment_ids.length || body.match(/\S+/))) {
- //session.web.blockUI();
- this.parent_thread.ds_thread.call('message_post_user_api', [
- this.context.default_res_id,
- body,
- false,
- this.context.default_parent_id,
- _.map(this.attachment_ids, function (file) {return file.id;}),
- this.parent_thread.context
- ]).done(function (record) {
- var thread = self.parent_thread;
- var root = thread == self.options.root_thread;
- if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
- var thread = thread.parent_message.parent_thread;
- }
- // create object and attach to the thread object
- thread.message_fetch([['id', 'child_of', [self.id]]], false, [record], function (arg, data) {
- var message = thread.create_message_object( data[0] );
- // insert the message on dom
- thread.insert_message( message, root ? undefined : self.$el, root );
- });
- self.on_cancel();
- //session.web.unblockUI();
- });
- return true;
+ if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
+ // create list of new partners
+ var extra_email = _.map(_.filter(this.emails_from, function (f) {return f[1]}), function (f) {return f[0]});
+ this.check_recipient_partners(extra_email).done(function () {
+ self.do_send_message_post();
+ });
}
},
+ /*do post a message and fetch the message*/
+ do_send_message_post: function () {
+ var self = this;
+ this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
+ 'body': this.$('textarea').val(),
+ 'subject': false,
+ 'parent_id': this.context.default_parent_id,
+ 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
+ 'extra_emails': _.map(_.filter(this.emails_from, function (f) {return f[1]}), function (f) {return f[0]}),
+ 'context': this.parent_thread.context,
+ }).done(function (message_id) {
+ var thread = self.parent_thread;
+ var root = thread == self.options.root_thread;
+ if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
+ var thread = thread.parent_message.parent_thread;
+ }
+ // create object and attach to the thread object
+ thread.message_fetch([["id", "=", message_id]], false, [message_id], function (arg, data) {
+ var message = thread.create_message_object( data[0] );
+ // insert the message on dom
+ thread.insert_message( message, root ? undefined : self.$el, root );
+ });
+ self.on_cancel();
+ });
+ },
+
/* convert the compact mode into the compose message
*/
on_compose_expandable: function (event) {
-
- if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/))) {
+ this.get_emails_from();
+ if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/) && !this.attachment_ids.length)) {
this.show_composer = !this.show_composer || this.stay_open;
this.reinit();
}
@@ -620,6 +656,41 @@ openerp.mail = function (session) {
if (!this.show_composer) {
this.reinit();
}
+ },
+
+ get_emails_from: function () {
+ var self = this;
+ var messages = [];
+
+ if (this.parent_thread.parent_message) {
+ // go to the parented message
+ var message = this.parent_thread.parent_message;
+ var parent_message = message.parent_id ? message.parent_thread.parent_message : message;
+ var messages = [parent_message].concat(parent_message.get_childs());
+ } else if (this.options.emails_from_on_composer) {
+ // get all wall messages if is not a mail.Wall
+ _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
+ }
+
+ var emails_from = _.map(_.filter(messages,
+ function (thread) {return thread.author_id && !thread.author_id[0];}),
+ function (thread) {return thread.author_id[1];});
+
+ return _.each(emails_from, function (email_from) {
+ if (!_.find(self.emails_from, function (from) {return from[0] == email_from;})) {
+ self.emails_from.push([email_from, true]);
+ }
+ });
+ },
+
+ on_checked_email_from: function (event) {
+ var $input = $(event.target);
+ var email = $input.attr("data");
+ _.each(this.emails_from, function (email_from) {
+ if (email_from[0] == email) {
+ email_from[1] = $input.is(":checked");
+ }
+ });
}
});
@@ -982,9 +1053,11 @@ openerp.mail = function (session) {
this.is_private = datasets.is_private || false;
this.author_id = datasets.author_id || false;
this.thread_level = (datasets.thread_level+1) || 0;
- this.partner_ids = datasets.partner_ids || [];
- if (datasets.author_id)
- this.partner_ids.push(datasets.author_id);
+ datasets.partner_ids = datasets.partner_ids || [];
+ if (datasets.author_id && ! _.contains(datasets.partner_ids, datasets.author_id) && datasets.author_id[0]) {
+ datasets.partner_ids.push(datasets.author_id);
+ }
+ this.partner_ids = datasets.partner_ids;
this.messages = [];
this.options.flat_mode = !!(this.options.display_indented_thread > this.thread_level ? this.options.display_indented_thread - this.thread_level : 0);
@@ -1450,6 +1523,7 @@ openerp.mail = function (session) {
'message_ids': undefined,
'compose_as_todo' : false,
'readonly' : false,
+ 'emails_from_on_composer': true,
}, this.action.params);
this.action.params.help = this.action.help || false;
@@ -1490,7 +1564,7 @@ openerp.mail = function (session) {
$(window).resize( _.bind(this.thread.on_scroll, this.thread) );
this.$el.resize( _.bind(this.thread.on_scroll, this.thread) );
window.setTimeout( _.bind(this.thread.on_scroll, this.thread), 500 );
- }
+ },
});
@@ -1527,6 +1601,10 @@ openerp.mail = function (session) {
}
this.domain = this.node.params && this.node.params.domain || [];
+
+ if (!this.__parentedParent.is_action_enabled('edit')) {
+ this.node.params.show_link = false;
+ }
},
start: function () {
@@ -1615,6 +1693,7 @@ openerp.mail = function (session) {
'show_record_name': true,
'show_compact_message': this.action.params.view_mailbox ? false : 1,
'view_inbox': false,
+ 'emails_from_on_composer': false,
}, this.action.params);
},
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 0645c2b9225..1e481271eda 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -26,12 +26,16 @@ openerp_mail_followers = function(session, mail) {
this._super.apply(this, arguments);
this.image = this.node.attrs.image || 'image_small';
this.comment = this.node.attrs.help || false;
- this.displayed_nb = this.node.attrs.displayed_nb || 10;
+ this.displayed_limit = this.node.attrs.displayed_nb || 10;
+ this.displayed_nb = this.displayed_limit;
this.ds_model = new session.web.DataSetSearch(this, this.view.model);
this.ds_follow = new session.web.DataSetSearch(this, this.field.relation);
this.ds_users = new session.web.DataSetSearch(this, 'res.users');
this.value = [];
+ this.followers = [];
+
+ this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
},
start: function() {
@@ -69,29 +73,47 @@ openerp_mail_followers = function(session, mail) {
// event: click on a subtype, that (un)subscribe for this subtype
this.$el.on('click', '.oe_subtype_list input', self.do_update_subscription);
// event: click on 'invite' button, that opens the invite wizard
- this.$('.oe_invite').on('click', function (event) {
- action = {
- type: 'ir.actions.act_window',
- res_model: 'mail.wizard.invite',
- view_mode: 'form',
- view_type: 'form',
- views: [[false, 'form']],
- target: 'new',
- context: {
- 'default_res_model': self.view.dataset.model,
- 'default_res_id': self.view.datarecord.id,
- },
- }
- self.do_action(action, {
- on_close: function() {
- self.read_value();
- },
- });
+ this.$('.oe_invite').on('click', self.on_invite_follower);
+ this.$el.on('click', '.oe_remove_follower', self.on_remove_follower);
+ this.$el.on('click', '.oe_show_more', self.on_show_more_followers)
+ },
+
+ on_invite_follower: function (event) {
+ var self = this;
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.wizard.invite',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ 'default_res_model': this.view.dataset.model,
+ 'default_res_id': this.view.datarecord.id,
+ },
+ }
+ this.do_action(action, {
+ on_close: function() {
+ self.read_value();
+ },
});
},
+ on_show_more_followers: function (event) {
+ this.displayed_nb += this.displayed_limit;
+ this.display_followers(false);
+ },
+
+ on_remove_follower: function (event) {
+ var partner_id = $(event.target).data('id');
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
+ .then(this.proxy('read_value'));
+ },
+
read_value: function () {
var self = this;
+ this.displayed_nb = this.displayed_limit;
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).then(function (results) {
self.value = results[0].message_follower_ids;
self.render_value();
@@ -145,20 +167,20 @@ openerp_mail_followers = function(session, mail) {
/** Display the followers */
display_followers: function (records) {
var self = this;
- records = records || [];
- this.message_is_follower = this.set_is_follower(records);
+ this.followers = records || this.followers;
+ this.message_is_follower = this.set_is_follower(this.followers);
// clean and display title
var node_user_list = this.$('.oe_follower_list').empty();
- this.$('.oe_follower_title').html(this._format_followers(records.length));
+ this.$('.oe_follower_title').html(this._format_followers(this.followers.length));
// truncate number of displayed followers
- truncated = records.splice(0, this.displayed_nb);
+ var truncated = this.followers.slice(0, this.displayed_nb);
_(truncated).each(function (record) {
record.avatar_url = mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record.id);
- $(session.web.qweb.render('mail.followers.partner', {'record': record})).appendTo(node_user_list);
+ $(session.web.qweb.render('mail.followers.partner', {'record': record, 'widget': self})).appendTo(node_user_list);
});
// FVA note: be sure it is correctly translated
- if (truncated.length < records.length) {
- $(' And ' + (records.length - truncated.length) + ' more. ').appendTo(node_user_list);
+ if (truncated.length < this.followers.length) {
+ $(session.web.qweb.render('mail.followers.show_more', {'number': (this.followers.length - truncated.length)} )).appendTo(node_user_list);
}
},
@@ -196,18 +218,23 @@ openerp_mail_followers = function(session, mail) {
var subtype_list_ul = this.$('.oe_subtype_list');
subtype_list_ul.empty();
var records = data[this.view.datarecord.id || this.view.dataset.ids[0]].message_subtype_data;
- _(records).each(function (record, record_name) {
- record.name = record_name;
- record.followed = record.followed || undefined;
- $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
- });
+ if (records.length > 1) {
+ _(records).each(function (record, record_name) {
+ record.name = record_name;
+ record.followed = record.followed || undefined;
+ $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
+ });
+ }
},
do_follow: function () {
- _(this.$('.oe_msg_subtype_check')).each(function (record) {
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], undefined, context])
+ .then(this.proxy('read_value'));
+
+ _.each(this.$('.oe_subtype_list input'), function (record) {
$(record).attr('checked', 'checked');
});
- this.do_update_subscription();
},
do_unfollow: function () {
@@ -229,9 +256,13 @@ openerp_mail_followers = function(session, mail) {
}
});
- var context = new session.web.CompoundContext(this.build_context(), {});
- return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], this.message_is_follower ? checklist : undefined, context])
- .then(this.proxy('read_value'));
+ if (!checklist.length) {
+ this.do_unfollow();
+ } else {
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context])
+ .then(this.proxy('read_value'));
+ }
},
});
};
diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml
index 9396385d98e..c0510b1c00a 100644
--- a/addons/mail/static/src/xml/mail.xml
+++ b/addons/mail/static/src/xml/mail.xml
@@ -81,7 +81,7 @@
- [
+ [
uploading
@@ -93,7 +93,7 @@
- [
+ [
uploading
@@ -115,19 +115,28 @@
this document
- and
+ and
-
+
-
+
+
-
- , others...
+
+ , others...
<<<
+
+
+
+
+
|