From 62bcae6fccf34fe8512a306550e05466ca426c99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 15 Jul 2013 17:24:24 +0200
Subject: [PATCH 01/79] [REF] ir.actions.server: cleaning and refactoring. Main
modifications: - removed dummy, email (now coming with email_template), loop,
sms - cleaned code, made it easy to override - improved view to ease the
definition of new server actions - changed/updated fields - added tests -
added changelog
bzr revid: tde@openerp.com-20130715152424-deucc2rlg2ax3tyc
---
doc/changelog.rst | 1 +
openerp/addons/base/ir/ir_actions.py | 737 +++++++++++--------
openerp/addons/base/ir/ir_actions.xml | 249 +++++--
openerp/addons/base/tests/__init__.py | 2 +
openerp/addons/base/tests/test_ir_actions.py | 390 ++++++++++
5 files changed, 1000 insertions(+), 379 deletions(-)
create mode 100644 openerp/addons/base/tests/test_ir_actions.py
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 70d959941ad..57a54eba506 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -6,6 +6,7 @@ Changelog
`trunk`
-------
+- Cleaned and slightly refactored ``ir.actions.server``
- Almost removed ``LocalService()``. For reports,
``openerp.osv.orm.Model.print_report()`` can be used. For workflows, see
:ref:`orm-workflows`.
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index aaaa8513925..412350ef1b3 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -22,17 +22,16 @@
import logging
import operator
import os
-import re
-from socket import gethostname
import time
import openerp
from openerp import SUPERUSER_ID
from openerp import tools
+from openerp import workflow
from openerp.osv import fields, osv
+from openerp.osv.orm import browse_record
import openerp.report.interface
from openerp.report.report_sxw import report_sxw, report_rml
-from openerp.tools.config import config
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
import openerp.workflow
@@ -429,211 +428,473 @@ class server_object_lines(osv.osv):
_name = 'ir.server.object.lines'
_sequence = 'ir_actions_id_seq'
_columns = {
- 'server_id': fields.many2one('ir.actions.server', 'Object Mapping'),
- 'col1': fields.many2one('ir.model.fields', 'Destination', required=True),
+ 'server_id': fields.many2one('ir.actions.server', 'Related Server Action'),
+ 'col1': fields.many2one('ir.model.fields', 'Field', required=True),
'value': fields.text('Value', required=True, help="Expression containing a value specification. \n"
"When Formula type is selected, this field may be a Python expression "
" that can use the same values as for the condition field on the server action.\n"
"If Value type is selected, the value will be used directly without evaluation."),
'type': fields.selection([
- ('value','Value'),
- ('equation','Formula')
- ], 'Type', required=True, size=32, change_default=True),
+ ('value', 'Value'),
+ ('equation', 'Python expression')
+ ], 'Evaluation Type', required=True, change_default=True),
}
_defaults = {
- 'type': 'equation',
+ 'type': 'value',
}
-server_object_lines()
+
##
# Actions that are run on the server side
#
class actions_server(osv.osv):
-
- def _select_signals(self, cr, uid, context=None):
- cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
- WHERE w.id = a.wkf_id AND
- (t.act_from = a.id OR t.act_to = a.id) AND
- t.signal IS NOT NULL""")
- result = cr.fetchall() or []
- res = []
- for rs in result:
- if rs[0] is not None and rs[1] is not None:
- line = rs[1], "%s - (%s)" % (rs[1], rs[0])
- res.append(line)
- return res
-
- def _select_objects(self, cr, uid, context=None):
- model_pool = self.pool.get('ir.model')
- ids = model_pool.search(cr, uid, [('name','not ilike','.')])
- res = model_pool.read(cr, uid, ids, ['model', 'name'])
- return [(r['model'], r['name']) for r in res] + [('','')]
-
- def change_object(self, cr, uid, ids, copy_object, state, context=None):
- if state == 'object_copy' and copy_object:
- if context is None:
- context = {}
- model_pool = self.pool.get('ir.model')
- model = copy_object.split(',')[0]
- mid = model_pool.search(cr, uid, [('model','=',model)])
- return {
- 'value': {'srcmodel_id': mid[0]},
- 'context': context
- }
- else:
- return {}
-
_name = 'ir.actions.server'
_table = 'ir_act_server'
_inherit = 'ir.actions.actions'
_sequence = 'ir_actions_id_seq'
_order = 'sequence,name'
+
+ def _select_objects(self, cr, uid, context=None):
+ model_pool = self.pool.get('ir.model')
+ ids = model_pool.search(cr, uid, [('name', 'not ilike', '.')])
+ res = model_pool.read(cr, uid, ids, ['model', 'name'])
+ return [(r['model'], r['name']) for r in res] + [('', '')]
+
+ def _get_states(self, cr, uid, context=None):
+ """ Override me in order to add new states in the server action. Please
+ note that the added key length should not be higher than already-existing
+ ones. """
+ return [('code', 'Execute Python Code'),
+ ('trigger', 'Trigger a Workflow Signal'),
+ ('client_action', 'Run a Client Action'),
+ ('object_create', 'Create or Copy a new Record'),
+ ('object_write', 'Write on a Record'),
+ ('multi', 'Execute several actions')]
+
+ def _get_states_wrapper(self, cr, uid, context=None):
+ return self._get_states(cr, uid, context)
+
_columns = {
'name': fields.char('Action Name', required=True, size=64, translate=True),
- 'condition' : fields.char('Condition', size=256, required=True,
- help="Condition that is tested before the action is executed, "
- "and prevent execution if it is not verified.\n"
- "Example: object.list_price > 5000\n"
- "It is a Python expression that can use the following values:\n"
- " - self: ORM model of the record on which the action is triggered\n"
- " - object or obj: browse_record of the record on which the action is triggered\n"
- " - pool: ORM model pool (i.e. self.pool)\n"
- " - time: Python time module\n"
- " - cr: database cursor\n"
- " - uid: current user id\n"
- " - context: current context"),
- 'state': fields.selection([
- ('client_action','Client Action'),
- ('dummy','Dummy'),
- ('loop','Iteration'),
- ('code','Python Code'),
- ('trigger','Trigger'),
- ('email','Email'),
- ('sms','SMS'),
- ('object_create','Create Object'),
- ('object_copy','Copy Object'),
- ('object_write','Write Object'),
- ('other','Multi Actions'),
- ], 'Action Type', required=True, size=32, help="Type of the Action that is to be executed"),
- 'code':fields.text('Python Code', help="Python code to be executed if condition is met.\n"
- "It is a Python block that can use the same values as for the condition field"),
- 'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
- 'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create).", ondelete='cascade'),
- 'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
- 'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
- 'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
- 'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
- 'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
- "Example: object.invoice_address_id.email, or 'me@example.com'"),
- 'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
- "available in the condition field, e.g. `Hello [[ object.partner_id.name ]]`"),
- 'message': fields.text('Message', translate=True, help="Email contents, may contain expressions enclosed in double brackets based on the same values as those "
- "available in the condition field, e.g. `Dear [[ object.partner_id.name ]]`"),
- 'mobile': fields.char('Mobile No', size=512, help="Provides fields that be used to fetch the mobile number, e.g. you select the invoice, then `object.invoice_address_id.mobile` is the field which gives the correct mobile number"),
- 'sms': fields.char('SMS', size=160, translate=True),
- 'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', 'Other Actions'),
+ 'condition': fields.char('Condition',
+ help="Condition verified before executing the server action. If it "
+ "is not verified, the action will not be executed. The condition is "
+ "a Python expression, like 'object.list_price > 5000'. A void "
+ "condition is considered as always True. Help about pyhon expression "
+ "is given in the help tab."),
+ 'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
+ help="Type of server action. The following values are available:\n"
+ "- 'Execute Python Code': a block of python code that will be executed\n"
+ "- 'Trigger a Workflow Signal': send a signal to a workflow\n"
+ "- 'Run a Client Action': choose a client action to launch\n"
+ "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
+ "- 'Write on a Record': update the values of a record\n"
+ "- 'Execute several actions': define an action that triggers several other sever actions\n"
+ "- 'Send Email': automatically send an email (available in email_template)"),
'usage': fields.char('Action Usage', size=32),
'type': fields.char('Action Type', size=32, required=True),
- 'srcmodel_id': fields.many2one('ir.model', 'Model', help="Object in which you want to create / write the object. If it is empty then refer to the Object field."),
- 'fields_lines': fields.one2many('ir.server.object.lines', 'server_id', 'Field Mappings.'),
- 'record_id':fields.many2one('ir.model.fields', 'Create Id', help="Provide the field name where the record id is stored after the create operations. If it is empty, you can not track the new record."),
- 'write_id':fields.char('Write Id', size=256, help="Provide the field name that the record id refers to for the write operation. If it is empty it will refer to the active id of the object."),
- 'loop_action':fields.many2one('ir.actions.server', 'Loop Action', help="Select the action that will be executed. Loop action will not be avaliable inside loop."),
- 'expression':fields.char('Loop Expression', size=512, help="Enter the field/expression that will return the list. E.g. select the sale order in Object, and you can have loop on the sales order line. Expression = `object.order_line`."),
- 'copy_object': fields.reference('Copy Of', selection=_select_objects, size=256),
+ # Generic
+ 'sequence': fields.integer('Sequence',
+ help="When dealing with multiple actions, the execution order is "
+ "based on the sequence. Low number means high priority."),
+ 'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
+ help="Base model on which the server action runs."),
+ 'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
+ help='More menu entry.'),
+ # Client Action
+ 'action_id': fields.many2one('ir.actions.actions', 'Client Action',
+ help="Select the client action that has to be executed."),
+ # Python code
+ 'code': fields.text('Python Code',
+ help="Write Python code that the action will execute. Some variables are "
+ "available for use; help about pyhon expression is given in the help tab."),
+ # Workflow signal
+ 'use_relational_model': fields.selection([('base', 'Use the base model of the action'),
+ ('relational', 'Use a relation field on the base model')],
+ string='Target Model', required=True),
+ 'wkf_transition_id': fields.many2one('workflow.transition', string='Signal to Trigger',
+ help="Select the workflow signal to trigger."),
+ 'wkf_model_id': fields.many2one('ir.model', 'Target Model',
+ help="The model that will receive the workflow signal. Note that it should have a workflow associated with it."),
+ 'wkf_model_name': fields.related('wkf_model_id', 'model', type='char', string='Target Model Name', store=True, readonly=True),
+ 'wkf_field_id': fields.many2one('ir.model.fields', string='Relation Field',
+ oldname='trigger_obj_id',
+ help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
+ # Multi
+ 'child_ids': fields.many2many('ir.actions.server', 'rel_server_actions',
+ 'server_id', 'action_id',
+ string='Child Actions',
+ help='Child server actions that will be executed. Note that the last return returned action value will be used as global return value.'),
+ # Create/Copy/Write
+ 'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
+ ('new_other', 'Create a new record in another model'),
+ ('copy_current', 'Copy the current record'),
+ ('copy_other', 'Copy another record')],
+ string="Creation Policy", required=True,
+ help=""),
+ 'crud_model_id': fields.many2one('ir.model', 'Target Model',
+ oldname='srcmodel_id',
+ help="Model for record creation / update. Set this field only to specify a different model than the base model."),
+ 'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
+ oldname='copy_object'),
+ 'link_new_record': fields.boolean('Link to current record',
+ help="Check this if you want to link the newly-created record "
+ "to the current record on which the server action runs."),
+ 'link_field_id': fields.many2one('ir.model.fields', 'Link Field',
+ oldname='record_id',
+ help="Provide the field where the record id is stored after the operations."),
+ 'use_write': fields.selection([('current', 'Update the current record'),
+ ('other', 'Update another record'),
+ ('expression', 'Update according a Python expression')],
+ string='Update Policy', required=True,
+ help=""),
+ 'write_expression': fields.char('Write Record Expression',
+ oldname='write_id',
+ help="Provide the field name that the record id refers to for the write operation. If it is empty it will refer to the active id of the object."),
+ 'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
+ string='Value Mapping',
+ help=""),
+
+ # Fake fields used to implement the placeholder assistant
+ 'model_object_field': fields.many2one('ir.model.fields', string="Field",
+ help="Select target field from the related document model.\n"
+ "If it is a relationship field you will be able to select "
+ "a target field at the destination of the relationship."),
+ 'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
+ help="When a relationship field is selected as first field, "
+ "this field shows the document model the relationship goes to."),
+ 'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
+ help="When a relationship field is selected as first field, "
+ "this field lets you select the target field within the "
+ "destination document model (sub-model)."),
+ 'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
}
+
_defaults = {
- 'state': 'dummy',
+ 'state': 'code',
'condition': 'True',
'type': 'ir.actions.server',
'sequence': 5,
- 'code': """# You can use the following variables:
-# - self: ORM model of the record on which the action is triggered
-# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
-# - pool: ORM model pool (i.e. self.pool)
-# - time: Python time module
-# - cr: database cursor
-# - uid: current user id
-# - context: current context
-# If you plan to return an action, assign: action = {...}
-""",
+ 'use_relational_model': 'base',
+ 'use_create': 'new',
+ 'use_write': 'current',
}
- def get_email(self, cr, uid, action, context):
+ def _check_expression(self, cr, uid, expression, model_id, context):
+ """ Check python expression (condition, write_expression) """
+ if not model_id:
+ return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
+ # fetch current model
+ current_model_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
+ # transform expression into a path that should look like 'object.many2onefield.many2onefield'
+ path = expression.split('.')
+ initial = path.pop(0)
+ if initial not in ['obj', 'object']:
+ return (False, None, 'Your expression should begin with obj or object.\nAn expression builder is available in the help tab.')
+ # analyze path
+ while path:
+ step = path.pop(0)
+ column_info = self.pool[current_model_name]._all_columns.get(step)
+ if not column_info:
+ return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
+ column_type = column_info.column._type
+ if column_type not in ['many2one']:
+ return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one)' % (step, column_type))
+ current_model_name = column_info.column._obj
+ return (True, current_model_name, None)
+
+ def _check_write_expression(self, cr, uid, ids, context=None):
+ for record in self.browse(cr, uid, ids, context=context):
+ if record.write_expression and record.model_id:
+ correct, model_name, message = self._check_expression(cr, uid, record.write_expression, record.model_id.id, context=context)
+ if not correct:
+ _logger.warning('Invalid expression: %s' % message)
+ return False
+ return True
+
+ _constraints = [
+ (_check_write_expression,
+ 'Incorrect Write Record Expression',
+ ['write_expression']),
+ ]
+
+ def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
+ """ When changing the action base model, reset workflow and crud config
+ to ease value coherence """
+ values = {
+ 'use_create': 'new',
+ 'use_write': 'current',
+ 'use_relational_model': 'base',
+ 'wkf_model_id': model_id,
+ 'crud_model_id': model_id,
+ }
+ return {'value': values}
+
+ def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
+ """ Update workflow configuration
+ - update the workflow model (for base (model_id) /relational (field.relation))
+ - update wkf_transition_id to False if workflow model changes, to force the user
+ to choose a new one
+ """
+ values = {}
+ if use_relational_model == 'relational' and wkf_field_id:
+ field = self.pool['ir.model.fields'].browse(cr, uid, wkf_field_id, context=context)
+ new_wkf_model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', field.relation)], context=context)[0]
+ values['wkf_model_id'] = new_wkf_model_id
+ else:
+ values['wkf_model_id'] = model_id
+ if values.get('wkf_model_id') != wkf_model_id:
+ values['wkf_transition_id'] = False
+ return {'value': values}
+
+ def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
+ """ When changing the workflow model, update its stored name also """
+ wkf_model_name = False
+ if wkf_model_id:
+ wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
+ return {'value': {'wkf_model_name': wkf_model_name}}
+
+ def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
+ """ TODO """
+ if state == 'object_create':
+ return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
+ elif state == 'object_write':
+ return self.on_change_write_config(cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=context)
+ else:
+ return {}
+
+ def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
+ """ TODO """
+ values = {}
+ if use_create == 'new':
+ values['crud_model_id'] = model_id
+ elif use_create == 'new_other':
+ pass
+ elif use_create == 'copy_current':
+ values['crud_model_id'] = model_id
+ elif use_create == 'copy_other' and ref_object:
+ ref_model, ref_id = ref_object.split(',')
+ ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
+ values['crud_model_id'] = ref_model_id
+
+ if values.get('crud_model_id') != crud_model_id:
+ values['link_field_id'] = False
+ return {'value': values}
+
+ def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
+ """ TODO """
+ values = {}
+ if use_write == 'current':
+ values['crud_model_id'] = model_id
+ elif use_write == 'other' and ref_object:
+ ref_model, ref_id = ref_object.split(',')
+ ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', ref_model)], context=context)[0]
+ values['crud_model_id'] = ref_model_id
+ elif use_write == 'expression':
+ pass
+
+ if values.get('crud_model_id') != crud_model_id:
+ values['link_field_id'] = False
+ return {'value': values}
+
+ def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
+ """ Check the write_expression, about fields and models. """
+ values = {}
+ valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
+ if valid:
+ ref_model_id = self.pool['ir.model'].search(cr, uid, [('model', '=', model_name)], context=context)[0]
+ values['crud_model_id'] = ref_model_id
+ return {'value': values}
+ if not message:
+ message = 'Invalid expression'
+ return {
+ 'warning': {
+ 'title': 'Incorrect expression',
+ 'message': message,
+ }
+ }
+
+ def build_expression(self, field_name, sub_field_name):
+ """Returns a placeholder expression for use in a template field,
+ based on the values provided in the placeholder assistant.
+
+ :param field_name: main field name
+ :param sub_field_name: sub field name (M2O)
+ :return: final placeholder expression
+ """
+ expression = ''
+ if field_name:
+ expression = "object." + field_name
+ if sub_field_name:
+ expression += "." + sub_field_name
+ return expression
+
+ def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, context=None):
+ result = {
+ 'sub_object': False,
+ 'copyvalue': False,
+ 'sub_model_object_field': False,
+ }
+ if model_object_field:
+ fields_obj = self.pool.get('ir.model.fields')
+ field_value = fields_obj.browse(cr, uid, model_object_field, context)
+ if field_value.ttype in ['many2one', 'one2many', 'many2many']:
+ res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
+ sub_field_value = False
+ if sub_model_object_field:
+ sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
+ if res_ids:
+ result.update({
+ 'sub_object': res_ids[0],
+ 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
+ 'sub_model_object_field': sub_model_object_field or False,
+ })
+ else:
+ result.update({
+ 'copyvalue': self.build_expression(field_value.name, False),
+ })
+ return {'value': result}
+
+ def create_action(self, cr, uid, ids, context=None):
+ """ Create a contextual action for each of the server actions. """
+ for action in self.browse(cr, uid, ids, context=context):
+ ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, {
+ 'name': _('Run %s') % action.name,
+ 'model': action.model_id.model,
+ 'key2': 'client_action_multi',
+ 'value': "ir.actions.server,%s" % action.id,
+ }, context)
+ action.write({
+ 'menu_ir_values_id': ir_values_id,
+ })
+
+ return True
+
+ def unlink_action(self, cr, uid, ids, context=None):
+ """ Remove the contextual actions created for the server actions. """
+ for action in self.browse(cr, uid, ids, context=context):
+ if action.menu_ir_values_id:
+ try:
+ self.pool.get('ir.values').unlink(cr, SUPERUSER_ID, action.menu_ir_values_id.id, context)
+ except Exception:
+ raise osv.except_osv(_('Warning'), _('Deletion of the action record failed.'))
+ return True
+
+ def run_action_client_action(self, cr, uid, action, eval_context=None, context=None):
+ if not action.action_id:
+ raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
+ return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
+
+ def run_action_code(self, cr, uid, action, eval_context=None, context=None):
+ eval(action.code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action'
+ if 'action' in eval_context:
+ return eval_context['action']
+
+ def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
+ """ Trigger a workflow signal, depending on the use_relational_model:
+ - `base`: base_model_pool.signal_(cr, uid, context.get('active_id'))
+ - `relational`: find the related model and object, using the relational
+ field, then target_model_pool.signal_(cr, uid, target_id)
+ """
obj_pool = self.pool[action.model_id.model]
- id = context.get('active_id')
- obj = obj_pool.browse(cr, uid, id)
+ if action.use_relational_model == 'base':
+ target_id = context.get('active_id')
+ target_pool = obj_pool
+ else:
+ value = getattr(obj_pool.browse(cr, uid, context.get('active_id'), context=context), action.wkf_field_id.name)
+ if action.wkf_field_id.ttype == 'many2one':
+ target_id = value.id
+ else:
+ target_id = value
+ target_pool = self.pool[action.wkf_model_id.model]
- fields = None
+ trigger_name = action.wkf_transition_id.signal
- if '/' in action.email.complete_name:
- fields = action.email.complete_name.split('/')
- elif '.' in action.email.complete_name:
- fields = action.email.complete_name.split('.')
+ workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
- for field in fields:
- try:
- obj = getattr(obj, field)
- except Exception:
- _logger.exception('Failed to parse: %s', field)
+ def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
+ # TDE FIXME: loops are not considered here ^^
+ res = []
+ for act in action.child_ids:
+ # context['active_id'] = context['active_ids'][0]
+ result = self.run(cr, uid, [act.id], context)
+ if result:
+ res.append(result)
+ return res and res[0] or False
- return obj
+ def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
+ res = {}
+ for exp in action.fields_lines:
+ if exp.type == 'equation':
+ expr = eval(exp.value, eval_context)
+ else:
+ expr = exp.value
+ res[exp.col1.name] = expr
- def get_mobile(self, cr, uid, action, context):
- obj_pool = self.pool[action.model_id.model]
- id = context.get('active_id')
- obj = obj_pool.browse(cr, uid, id)
+ if action.use_write == 'current':
+ model = action.model_id.model
+ ref_id = context.get('active_id')
+ elif action.use_write == 'other':
+ model = action.crud_model_id.model
+ ref_id = action.ref_object.id
+ elif action.use_write == 'expression':
+ model = action.crud_model_id.model
+ ref = eval(action.write_expression, eval_context)
+ if isinstance(ref, browse_record):
+ ref_id = getattr(ref, 'id')
+ else:
+ ref_id = int(ref)
- fields = None
+ obj_pool = self.pool[model]
+ obj_pool.write(cr, uid, [ref_id], res, context=context)
- if '/' in action.mobile.complete_name:
- fields = action.mobile.complete_name.split('/')
- elif '.' in action.mobile.complete_name:
- fields = action.mobile.complete_name.split('.')
+ def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
+ res = {}
+ for exp in action.fields_lines:
+ if exp.type == 'equation':
+ expr = eval(exp.value, eval_context)
+ else:
+ expr = exp.value
+ res[exp.col1.name] = expr
- for field in fields:
- try:
- obj = getattr(obj, field)
- except Exception:
- _logger.exception('Failed to parse: %s', field)
+ if action.use_create in ['new', 'copy_current']:
+ model = action.model_id.model
+ elif action.use_create in ['new_other', 'copy_other']:
+ model = action.crud_model_id.model
- return obj
+ obj_pool = self.pool[model]
+ if action.use_create == 'copy_current':
+ ref_id = context.get('active_id')
+ res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
+ elif action.use_create == 'copy_other':
+ ref_id = action.ref_object.id
+ res_id = obj_pool.copy(cr, uid, ref_id, res, context=context)
+ else:
+ res_id = obj_pool.create(cr, uid, res, context=context)
- def merge_message(self, cr, uid, keystr, action, context=None):
- if context is None:
- context = {}
+ if action.link_new_record and action.link_field_id:
+ self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
- def merge(match):
- obj_pool = self.pool[action.model_id.model]
- id = context.get('active_id')
- obj = obj_pool.browse(cr, uid, id)
- exp = str(match.group()[2:-2]).strip()
- result = eval(exp,
- {
- 'object': obj,
- 'context': dict(context), # copy context to prevent side-effects of eval
- 'time': time,
- })
- if result in (None, False):
- return str("--------")
- return tools.ustr(result)
-
- com = re.compile('(\[\[.+?\]\])')
- message = com.sub(merge, keystr)
-
- return message
-
- # Context should contains:
- # ids : original ids
- # id : current id of the object
- # OUT:
- # False : Finished correctly
- # ACTION_ID : Action to launch
-
- # FIXME: refactor all the eval() calls in run()!
def run(self, cr, uid, ids, context=None):
+ """ Run the server action, by check the condition and then calling
+ run_action_, i.e. run_action_code, allowing easy inheritance
+ of the server actions.
+
+ A void (aka False) condition is considered as always valid.
+
+ Note coming from previous implementation: FIXME: refactor all the eval()
+ calls in run()!
+
+ :param dict context: context should contain following keys:
+ - active_id: current id of the object
+ - active_model: current model that should equal the action's model
+ - TDE: ?? ids: original ids
+
+ :return: False: finished correctly or action_id: action to lanch
+ """
if context is None:
context = {}
+ res = False
user = self.pool.get('res.users').browse(cr, uid, uid)
for action in self.browse(cr, uid, ids, context):
obj = None
@@ -647,150 +908,22 @@ class actions_server(osv.osv):
'pool': self.pool,
'time': time,
'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
+ 'context': dict(context), # copy context to prevent side-effects of eval
'uid': uid,
'user': user
}
- expr = eval(str(action.condition), cxt)
+ # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
+ condition = action.condition
+ if action.condition is False:
+ condition = True
+ expr = eval(str(condition), cxt)
if not expr:
continue
+ # call the method related to the action: run_action_
+ if hasattr(self, 'run_action_%s' % action.state):
+ res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=cxt, context=context)
+ return res
- if action.state=='client_action':
- if not action.action_id:
- raise osv.except_osv(_('Error'), _("Please specify an action to launch!"))
- return self.pool[action.action_id.type].read(cr, uid, action.action_id.id, context=context)
-
- if action.state=='code':
- eval(action.code.strip(), cxt, mode="exec", nocopy=True) # nocopy allows to return 'action'
- if 'action' in cxt:
- return cxt['action']
-
- if action.state == 'email':
- email_from = config['email_from']
- if not email_from:
- _logger.debug('--email-from command line option is not specified, using a fallback value instead.')
- if user.email:
- email_from = user.email
- else:
- email_from = "%s@%s" % (user.login, gethostname())
-
- try:
- address = eval(str(action.email), cxt)
- except Exception:
- address = str(action.email)
-
- if not address:
- _logger.info('No partner email address specified, not sending any email.')
- continue
-
- # handle single and multiple recipient addresses
- addresses = address if isinstance(address, (tuple, list)) else [address]
- subject = self.merge_message(cr, uid, action.subject, action, context)
- body = self.merge_message(cr, uid, action.message, action, context)
-
- ir_mail_server = self.pool.get('ir.mail_server')
- msg = ir_mail_server.build_email(email_from, addresses, subject, body)
- res_email = ir_mail_server.send_email(cr, uid, msg)
- if res_email:
- _logger.info('Email successfully sent to: %s', addresses)
- else:
- _logger.warning('Failed to send email to: %s', addresses)
-
- if action.state == 'trigger':
- model = action.wkf_model_id.model
- m2o_field_name = action.trigger_obj_id.name
- target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
- target_id = target_id[0] if isinstance(target_id,tuple) else target_id
- openerp.workflow.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
-
- if action.state == 'sms':
- #TODO: set the user and password from the system
- # for the sms gateway user / password
- # USE smsclient module from extra-addons
- _logger.warning('SMS Facility has not been implemented yet. Use smsclient module!')
-
- if action.state == 'other':
- res = []
- for act in action.child_ids:
- context['active_id'] = context['active_ids'][0]
- result = self.run(cr, uid, [act.id], context)
- if result:
- res.append(result)
- return res
-
- if action.state == 'loop':
- expr = eval(str(action.expression), cxt)
- context['object'] = obj
- for i in expr:
- context['active_id'] = i.id
- self.run(cr, uid, [action.loop_action.id], context)
-
- if action.state == 'object_write':
- res = {}
- for exp in action.fields_lines:
- euq = exp.value
- if exp.type == 'equation':
- expr = eval(euq, cxt)
- else:
- expr = exp.value
- res[exp.col1.name] = expr
-
- if not action.write_id:
- if not action.srcmodel_id:
- obj_pool = self.pool[action.model_id.model]
- obj_pool.write(cr, uid, [context.get('active_id')], res)
- else:
- write_id = context.get('active_id')
- obj_pool = self.pool[action.srcmodel_id.model]
- obj_pool.write(cr, uid, [write_id], res)
-
- elif action.write_id:
- obj_pool = self.pool[action.srcmodel_id.model]
- rec = self.pool[action.model_id.model].browse(cr, uid, context.get('active_id'))
- id = eval(action.write_id, {'object': rec})
- try:
- id = int(id)
- except:
- raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
-
- if type(id) != type(1):
- raise osv.except_osv(_('Error'), _("Problem in configuration `Record Id` in Server Action!"))
- write_id = id
- obj_pool.write(cr, uid, [write_id], res)
-
- if action.state == 'object_create':
- res = {}
- for exp in action.fields_lines:
- euq = exp.value
- if exp.type == 'equation':
- expr = eval(euq, cxt)
- else:
- expr = exp.value
- res[exp.col1.name] = expr
-
- obj_pool = self.pool[action.srcmodel_id.model]
- res_id = obj_pool.create(cr, uid, res)
- if action.record_id:
- self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.record_id.name:res_id})
-
- if action.state == 'object_copy':
- res = {}
- for exp in action.fields_lines:
- euq = exp.value
- if exp.type == 'equation':
- expr = eval(euq, cxt)
- else:
- expr = exp.value
- res[exp.col1.name] = expr
-
- model = action.copy_object.split(',')[0]
- cid = action.copy_object.split(',')[1]
- obj_pool = self.pool[model]
- obj_pool.copy(cr, uid, int(cid), res)
-
- return False
-
-actions_server()
class act_window_close(osv.osv):
_name = 'ir.actions.act_window_close'
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 145627058ef..0713c333922 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -309,86 +309,181 @@
ir.actions.server
+
+
+
+
+
+
+
+
+
+
+
+
If you use client actions in your multiple actions, only the last client action will be executed. Other client actions will be discarded.
+
+
+
+
+
+
+
Help with Python expressions.
+
Various fields may use Python code or Python expressions. The following variables can be used:
+
+
self: ORM model of the record on which the action is triggered
+
object or obj: browse_record of the record on which the action is triggered
+
pool: ORM model pool (i.e. self.pool)
+
time: Python time module
+
cr: database cursor
+
uid: current user id
+
context: current context
+
+
+
Hints for using python in the condition
+
+
condition: True
+
condition: object.list_price > 5000
+
+
+
+
Hints for using python for a code action
+
+
if you plan to return an action, assign action = {...}
+ Please set the Base Model of the action to enable the dynamic expression buidler.
+
+
+
+
+
+
+
+
+
+
diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py
index e54fc892282..4c6a1e444f5 100644
--- a/openerp/addons/base/tests/__init__.py
+++ b/openerp/addons/base/tests/__init__.py
@@ -1,5 +1,6 @@
import test_base
import test_expression
+import test_ir_actions
import test_ir_attachment
import test_ir_values
import test_menu
@@ -10,6 +11,7 @@ import test_search
checks = [
test_base,
test_expression,
+ test_ir_actions,
test_ir_attachment,
test_ir_values,
test_menu,
diff --git a/openerp/addons/base/tests/test_ir_actions.py b/openerp/addons/base/tests/test_ir_actions.py
new file mode 100644
index 00000000000..63ed58670b9
--- /dev/null
+++ b/openerp/addons/base/tests/test_ir_actions.py
@@ -0,0 +1,390 @@
+import unittest2
+
+import openerp.tests.common as common
+
+
+class TestServerActionsBase(common.TransactionCase):
+
+ def setUp(self):
+ super(TestServerActionsBase, self).setUp()
+ cr, uid = self.cr, self.uid
+
+ # Models
+ self.ir_actions_server = self.registry('ir.actions.server')
+ self.ir_actions_client = self.registry('ir.actions.client')
+ self.ir_values = self.registry('ir.values')
+ self.ir_model = self.registry('ir.model')
+ self.ir_model_fields = self.registry('ir.model.fields')
+ self.res_partner = self.registry('res.partner')
+ self.res_country = self.registry('res.country')
+
+ # Data on which we will run the server action
+ self.test_country_id = self.res_country.create(cr, uid, {
+ 'name': 'TestingCountry',
+ 'code': 'TY',
+ 'address_format': 'SuperFormat',
+ })
+ self.test_country = self.res_country.browse(cr, uid, self.test_country_id)
+ self.test_partner_id = self.res_partner.create(cr, uid, {
+ 'name': 'TestingPartner',
+ 'city': 'OrigCity',
+ 'country_id': self.test_country_id,
+ })
+ self.test_partner = self.res_partner.browse(cr, uid, self.test_partner_id)
+ self.context = {
+ 'active_id': self.test_partner_id,
+ 'active_model': 'res.partner',
+ }
+
+ # Model data
+ self.res_partner_model_id = self.ir_model.search(cr, uid, [('model', '=', 'res.partner')])[0]
+ self.res_partner_name_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.partner'), ('name', '=', 'name')])[0]
+ self.res_partner_city_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.partner'), ('name', '=', 'city')])[0]
+ self.res_partner_country_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.partner'), ('name', '=', 'country_id')])[0]
+ self.res_partner_parent_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.partner'), ('name', '=', 'parent_id')])[0]
+ self.res_country_model_id = self.ir_model.search(cr, uid, [('model', '=', 'res.country')])[0]
+ self.res_country_name_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.country'), ('name', '=', 'name')])[0]
+ self.res_country_code_field_id = self.ir_model_fields.search(cr, uid, [('model', '=', 'res.country'), ('name', '=', 'code')])[0]
+
+ # create server action to
+ self.act_id = self.ir_actions_server.create(cr, uid, {
+ 'name': 'TestAction',
+ 'condition': 'True',
+ 'model_id': self.res_partner_model_id,
+ 'state': 'code',
+ 'code': 'obj.write({"comment": "MyComment"})',
+ })
+
+
+class TestServerActions(TestServerActionsBase):
+
+ def test_00_action(self):
+ cr, uid = self.cr, self.uid
+
+ # Do: eval 'True' condition
+ self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ self.test_partner.refresh()
+ self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check')
+ self.test_partner.write({'comment': False})
+
+ # Do: eval False condition, that should be considered as True (void = True)
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'condition': False})
+ self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ self.test_partner.refresh()
+ self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: invalid condition check')
+
+ # Do: create contextual action
+ self.ir_actions_server.create_action(cr, uid, [self.act_id])
+
+ # Test: ir_values created
+ ir_values_ids = self.ir_values.search(cr, uid, [('name', '=', 'Run TestAction')])
+ self.assertEqual(len(ir_values_ids), 1, 'ir_actions_server: create_action should have created an entry in ir_values')
+ ir_value = self.ir_values.browse(cr, uid, ir_values_ids[0])
+ self.assertEqual(ir_value.value, 'ir.actions.server,%s' % self.act_id, 'ir_actions_server: created ir_values should reference the server action')
+ self.assertEqual(ir_value.model, 'res.partner', 'ir_actions_server: created ir_values should be linked to the action base model')
+
+ # Do: remove contextual action
+ self.ir_actions_server.unlink_action(cr, uid, [self.act_id])
+
+ # Test: ir_values removed
+ ir_values_ids = self.ir_values.search(cr, uid, [('name', '=', 'Run TestAction')])
+ self.assertEqual(len(ir_values_ids), 0, 'ir_actions_server: unlink_action should remove the ir_values record')
+
+ def test_10_code(self):
+ cr, uid = self.cr, self.uid
+ self.ir_actions_server.write(cr, uid, self.act_id, {
+ 'state': 'code',
+ 'code': """partner_name = obj.name + '_code'
+self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context)"""
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ self.assertFalse(run_res, 'ir_actions_server: code server action correctly finished should return False')
+
+ pids = self.res_partner.search(cr, uid, [('name', 'ilike', 'TestingPartner_code')])
+ self.assertEqual(len(pids), 1, 'ir_actions_server: 1 new partner should have been created')
+
+ def test_20_trigger(self):
+ cr, uid = self.cr, self.uid
+
+ # Data: code server action (at this point code-based actions should work)
+ act_id2 = self.ir_actions_server.create(cr, uid, {
+ 'name': 'TestAction2',
+ 'type': 'ir.actions.server',
+ 'condition': 'True',
+ 'model_id': self.res_partner_model_id,
+ 'state': 'code',
+ 'code': 'obj.write({"comment": "MyComment"})',
+ })
+ act_id3 = self.ir_actions_server.create(cr, uid, {
+ 'name': 'TestAction3',
+ 'type': 'ir.actions.server',
+ 'condition': 'True',
+ 'model_id': self.res_country_model_id,
+ 'state': 'code',
+ 'code': 'obj.write({"code": "ZZ"})',
+ })
+
+ # Data: create workflows
+ partner_wf_id = self.registry('workflow').create(cr, uid, {
+ 'name': 'TestWorkflow',
+ 'osv': 'res.partner',
+ 'on_create': True,
+ })
+ partner_act1_id = self.registry('workflow.activity').create(cr, uid, {
+ 'name': 'PartnerStart',
+ 'wkf_id': partner_wf_id,
+ 'flow_start': True
+ })
+ partner_act2_id = self.registry('workflow.activity').create(cr, uid, {
+ 'name': 'PartnerTwo',
+ 'wkf_id': partner_wf_id,
+ 'kind': 'function',
+ 'action': 'True',
+ 'action_id': act_id2,
+ })
+ partner_trs1_id = self.registry('workflow.transition').create(cr, uid, {
+ 'signal': 'partner_trans',
+ 'act_from': partner_act1_id,
+ 'act_to': partner_act2_id
+ })
+ country_wf_id = self.registry('workflow').create(cr, uid, {
+ 'name': 'TestWorkflow',
+ 'osv': 'res.country',
+ 'on_create': True,
+ })
+ country_act1_id = self.registry('workflow.activity').create(cr, uid, {
+ 'name': 'CountryStart',
+ 'wkf_id': country_wf_id,
+ 'flow_start': True
+ })
+ country_act2_id = self.registry('workflow.activity').create(cr, uid, {
+ 'name': 'CountryTwo',
+ 'wkf_id': country_wf_id,
+ 'kind': 'function',
+ 'action': 'True',
+ 'action_id': act_id3,
+ })
+ country_trs1_id = self.registry('workflow.transition').create(cr, uid, {
+ 'signal': 'country_trans',
+ 'act_from': country_act1_id,
+ 'act_to': country_act2_id
+ })
+
+ # Data: re-create country and partner to benefit from the workflows
+ self.test_country_id = self.res_country.create(cr, uid, {
+ 'name': 'TestingCountry2',
+ 'code': 'T2',
+ })
+ self.test_country = self.res_country.browse(cr, uid, self.test_country_id)
+ self.test_partner_id = self.res_partner.create(cr, uid, {
+ 'name': 'TestingPartner2',
+ 'country_id': self.test_country_id,
+ })
+ self.test_partner = self.res_partner.browse(cr, uid, self.test_partner_id)
+ self.context = {
+ 'active_id': self.test_partner_id,
+ 'active_model': 'res.partner',
+ }
+
+ # Run the action on partner object itself ('base')
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'trigger',
+ 'use_relational_model': 'base',
+ 'wkf_model_id': self.res_partner_model_id,
+ 'wkf_model_name': 'res.partner',
+ 'wkf_transition_id': partner_trs1_id,
+ })
+ self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ self.test_partner.refresh()
+ self.assertEqual(self.test_partner.comment, 'MyComment', 'ir_actions_server: incorrect signal trigger')
+
+ # Run the action on related country object ('relational')
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'use_relational_model': 'relational',
+ 'wkf_model_id': self.res_country_model_id,
+ 'wkf_model_name': 'res.country',
+ 'wkf_field_id': self.res_partner_country_field_id,
+ 'wkf_transition_id': country_trs1_id,
+ })
+ self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ self.test_country.refresh()
+ self.assertEqual(self.test_country.code, 'ZZ', 'ir_actions_server: incorrect signal trigger')
+
+ # Clear workflow cache, otherwise openerp will try to create workflows even if it has been deleted
+ from openerp.workflow import clear_cache
+ clear_cache(cr, uid)
+
+ def test_30_client(self):
+ cr, uid = self.cr, self.uid
+ client_action_id = self.registry('ir.actions.client').create(cr, uid, {
+ 'name': 'TestAction2',
+ 'tag': 'Test',
+ })
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'client_action',
+ 'action_id': client_action_id,
+ })
+ res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertEqual(res['name'], 'TestAction2', 'ir_actions_server: incorrect return result for a client action')
+
+ def test_40_crud_create(self):
+ cr, uid = self.cr, self.uid
+ _city = 'TestCity'
+ _name = 'TestNew'
+
+ # Do: create a new record in the same model and link it
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'object_create',
+ 'use_create': 'new',
+ 'link_new_record': True,
+ 'link_field_id': self.res_partner_parent_field_id,
+ 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field_id, 'value': _name}),
+ (0, 0, {'col1': self.res_partner_city_field_id, 'value': _city})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new partner created
+ pids = self.res_partner.search(cr, uid, [('name', 'ilike', _name)])
+ self.assertEqual(len(pids), 1, 'ir_actions_server: TODO')
+ partner = self.res_partner.browse(cr, uid, pids[0])
+ self.assertEqual(partner.city, _city, 'ir_actions_server: TODO')
+ # Test: new partner linked
+ self.test_partner.refresh()
+ self.assertEqual(self.test_partner.parent_id.id, pids[0], 'ir_actions_server: TODO')
+
+ # Do: copy current record
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'fields_lines': [[5]]})
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'object_create',
+ 'use_create': 'copy_current',
+ 'link_new_record': False,
+ 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field_id, 'value': 'TestCopyCurrent'}),
+ (0, 0, {'col1': self.res_partner_city_field_id, 'value': 'TestCity'})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new partner created
+ pids = self.res_partner.search(cr, uid, [('name', 'ilike', 'TestingPartner (copy)')]) # currently res_partner overrides default['name'] whatever its value
+ self.assertEqual(len(pids), 1, 'ir_actions_server: TODO')
+ partner = self.res_partner.browse(cr, uid, pids[0])
+ self.assertEqual(partner.city, 'TestCity', 'ir_actions_server: TODO')
+ self.assertEqual(partner.country_id.id, self.test_partner.country_id.id, 'ir_actions_server: TODO')
+
+ # Do: create a new record in another model
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'fields_lines': [[5]]})
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'object_create',
+ 'use_create': 'new_other',
+ 'crud_model_id': self.res_country_model_id,
+ 'link_new_record': False,
+ 'fields_lines': [(0, 0, {'col1': self.res_country_name_field_id, 'value': 'obj.name', 'type': 'equation'}),
+ (0, 0, {'col1': self.res_country_code_field_id, 'value': 'obj.name[0:2]', 'type': 'equation'})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new country created
+ cids = self.res_country.search(cr, uid, [('name', 'ilike', 'TestingPartner')])
+ self.assertEqual(len(cids), 1, 'ir_actions_server: TODO')
+ country = self.res_country.browse(cr, uid, cids[0])
+ self.assertEqual(country.code, 'TE', 'ir_actions_server: TODO')
+
+ # Do: copy a record in another model
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'fields_lines': [[5]]})
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'object_create',
+ 'use_create': 'copy_other',
+ 'crud_model_id': self.res_country_model_id,
+ 'link_new_record': False,
+ 'ref_object': 'res.country,%s' % self.test_country_id,
+ 'fields_lines': [(0, 0, {'col1': self.res_country_name_field_id, 'value': 'NewCountry', 'type': 'value'}),
+ (0, 0, {'col1': self.res_country_code_field_id, 'value': 'NY', 'type': 'value'})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new country created
+ cids = self.res_country.search(cr, uid, [('name', 'ilike', 'NewCountry')])
+ self.assertEqual(len(cids), 1, 'ir_actions_server: TODO')
+ country = self.res_country.browse(cr, uid, cids[0])
+ self.assertEqual(country.code, 'NY', 'ir_actions_server: TODO')
+ self.assertEqual(country.address_format, 'SuperFormat', 'ir_actions_server: TODO')
+
+ def test_50_crud_write(self):
+ cr, uid = self.cr, self.uid
+ _name = 'TestNew'
+
+ # Do: create a new record in the same model and link it
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'object_write',
+ 'use_write': 'current',
+ 'fields_lines': [(0, 0, {'col1': self.res_partner_name_field_id, 'value': _name})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new partner created
+ pids = self.res_partner.search(cr, uid, [('name', 'ilike', _name)])
+ self.assertEqual(len(pids), 1, 'ir_actions_server: TODO')
+ partner = self.res_partner.browse(cr, uid, pids[0])
+ self.assertEqual(partner.city, 'OrigCity', 'ir_actions_server: TODO')
+
+ # Do: copy current record
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'fields_lines': [[5]]})
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'use_write': 'other',
+ 'crud_model_id': self.res_country_model_id,
+ 'ref_object': 'res.country,%s' % self.test_country_id,
+ 'fields_lines': [(0, 0, {'col1': self.res_country_name_field_id, 'value': 'obj.name', 'type': 'equation'})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new country created
+ cids = self.res_country.search(cr, uid, [('name', 'ilike', 'TestNew')])
+ self.assertEqual(len(cids), 1, 'ir_actions_server: TODO')
+
+ # Do: copy a record in another model
+ self.ir_actions_server.write(cr, uid, [self.act_id], {'fields_lines': [[5]]})
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'use_write': 'expression',
+ 'crud_model_id': self.res_country_model_id,
+ 'write_expression': 'object.country_id',
+ 'fields_lines': [(0, 0, {'col1': self.res_country_name_field_id, 'value': 'NewCountry', 'type': 'value'})],
+ })
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+ self.assertFalse(run_res, 'ir_actions_server: create record action correctly finished should return False')
+ # Test: new country created
+ cids = self.res_country.search(cr, uid, [('name', 'ilike', 'NewCountry')])
+ self.assertEqual(len(cids), 1, 'ir_actions_server: TODO')
+
+ def test_60_multi(self):
+ cr, uid = self.cr, self.uid
+
+ # Data: 2 server actions that will be nested
+ act1_id = self.ir_actions_server.create(cr, uid, {
+ 'name': 'Subaction1',
+ 'model_id': self.res_partner_model_id,
+ 'state': 'code',
+ 'code': 'action = {"type": "ir.actions.act_window"}',
+ })
+ # Do: create a new record in the same model and link it
+ act2_id = self.ir_actions_server.create(cr, uid, {
+ 'name': 'Subaction2',
+ 'model_id': self.res_partner_model_id,
+ 'state': 'object_create',
+ 'use_create': 'copy_current',
+ })
+ self.ir_actions_server.write(cr, uid, [self.act_id], {
+ 'state': 'multi',
+ 'child_ids': [(6, 0, [act1_id, act2_id])],
+ })
+
+ # Do: run the action
+ res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
+
+ # Test: new partner created
+ pids = self.res_partner.search(cr, uid, [('name', 'ilike', 'TestingPartner (copy)')]) # currently res_partner overrides default['name'] whatever its value
+ self.assertEqual(len(pids), 1, 'ir_actions_server: TODO')
+ # Test: action returned
+ self.assertEqual(res.get('type'), 'ir.actions.act_window', '')
+
+
+if __name__ == '__main__':
+ unittest2.main()
From 8aeba8a3a34b60f15cb9fe19ad67a49e7882f8ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 15 Jul 2013 17:25:16 +0200
Subject: [PATCH 02/79] [ADD] email_template: added support for 'email' server
action. Email server action are now entirely based on templates. Added tests.
bzr revid: tde@openerp.com-20130715152516-69gokja8u0afgfh8
---
addons/email_template/__init__.py | 1 +
addons/email_template/__openerp__.py | 1 +
addons/email_template/ir_actions.py | 73 +++++++++++++++++++
addons/email_template/ir_actions_view.xml | 49 +++++++++++++
addons/email_template/tests/__init__.py | 3 +-
.../email_template/tests/test_ir_actions.py | 55 ++++++++++++++
6 files changed, 181 insertions(+), 1 deletion(-)
create mode 100644 addons/email_template/ir_actions.py
create mode 100644 addons/email_template/ir_actions_view.xml
create mode 100644 addons/email_template/tests/test_ir_actions.py
diff --git a/addons/email_template/__init__.py b/addons/email_template/__init__.py
index d1cf5b28431..3f091fd8645 100644
--- a/addons/email_template/__init__.py
+++ b/addons/email_template/__init__.py
@@ -22,5 +22,6 @@
import email_template
import wizard
import res_partner
+import ir_actions
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py
index 1dc3baaf63c..b7977660134 100644
--- a/addons/email_template/__openerp__.py
+++ b/addons/email_template/__openerp__.py
@@ -59,6 +59,7 @@ campaigns on any OpenERP document.
'wizard/email_template_preview_view.xml',
'email_template_view.xml',
'res_partner_view.xml',
+ 'ir_actions_view.xml',
'wizard/mail_compose_message_view.xml',
'security/ir.model.access.csv'
],
diff --git a/addons/email_template/ir_actions.py b/addons/email_template/ir_actions.py
new file mode 100644
index 00000000000..ae2ce4f2f82
--- /dev/null
+++ b/addons/email_template/ir_actions.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2014 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 openerp.osv import fields, osv
+
+
+class actions_server(osv.Model):
+ """ Add email option in server actions. """
+ _name = 'ir.actions.server'
+ _inherit = ['ir.actions.server']
+
+ def _get_states(self, cr, uid, context=None):
+ res = super(actions_server, self)._get_states(cr, uid, context=context)
+ res.insert(0, ('email', 'Send Email'))
+ return res
+
+ _columns = {
+ 'email_from': fields.char('From', readonly=True,
+ help="Sender address; define the template to see its value. If not set, the default "
+ "value will be the author's email alias if configured, or email address."),
+ 'email_to': fields.char('To (Emails)', readonly=True,
+ help="Comma-separated recipient addresses; define the template to see its value"),
+ 'partner_to': fields.char('To (Partners)', readonly=True,
+ help="Comma-separated ids of recipient partners; define the template to see its value"),
+ 'subject': fields.char('Subject', readonly=True,
+ help="Email subject; define the template to see its value"),
+ 'body_html': fields.text('Body', readonly=True,
+ help="Rich-text/HTML version of the message; define the template to see its value"),
+ 'template_id': fields.many2one('email.template', 'Email Template', ondelete='set null',
+ help="Define the email template to use for the email to send.")
+ }
+
+ def on_change_template_id(self, cr, uid, ids, template_id, context=None):
+ """ - mass_mailing: we cannot render, so return the template values
+ - normal mode: return rendered values """
+ if template_id:
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
+ template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
+ values = dict((field, template_values[field]) for field in fields if template_values.get(field))
+ if not values.get('email_from'):
+ return {'warning': {'title': 'Incomplete template', 'message': 'Your template should define email_from'}, 'value': values}
+ else:
+ values = self.default_get(cr, uid, ['subject', 'body_html', 'email_from', 'email_to', 'partner_to'], context=context)
+
+ return {'value': values}
+
+ def run_action_email(self, cr, uid, action, eval_context=None, context=None):
+ if not action.template_id or not context.get('active_id'):
+ return False
+ self.pool['email.template'].send_mail(cr, uid, action.template_id.id, context.get('active_id'),
+ force_send=False, raise_exception=False, context=context)
+ return False
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/ir_actions_view.xml b/addons/email_template/ir_actions_view.xml
new file mode 100644
index 00000000000..f673fda687d
--- /dev/null
+++ b/addons/email_template/ir_actions_view.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ ir.actions.server.form
+ ir.actions.server
+
+
+
+
+
+ Please set the Base Model before setting the action details.
+
+
+
+
+ Choose a template to display its values.
+
+
+ The values displayed hereunder are informative. When sending the email, the values
+ will be taken from the email template.
+
+
+
+
+
+
+
+ Your template does not defined any email_from. Please update your template.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/email_template/tests/__init__.py b/addons/email_template/tests/__init__.py
index d63c5634cc3..9418985b06c 100644
--- a/addons/email_template/tests/__init__.py
+++ b/addons/email_template/tests/__init__.py
@@ -18,10 +18,11 @@
# along with this program. If not, see .
#
##############################################################################
-from . import test_mail
+from . import test_mail, test_ir_actions
checks = [
test_mail,
+ test_ir_actions,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/email_template/tests/test_ir_actions.py b/addons/email_template/tests/test_ir_actions.py
new file mode 100644
index 00000000000..d323ee36c1f
--- /dev/null
+++ b/addons/email_template/tests/test_ir_actions.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2013-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 openerp.addons.base.tests.test_ir_actions import TestServerActionsBase
+
+
+class TestServerActionsEmail(TestServerActionsBase):
+
+ def test_00_state_email(self):
+ """ Test ir.actions.server email type """
+ cr, uid = self.cr, self.uid
+
+ # create email_template
+ template_id = self.registry('email.template').create(cr, uid, {
+ 'name': 'TestTemplate',
+ 'email_from': 'myself@example.com',
+ 'email_to': 'brigitte@example.com',
+ 'partner_to': '[%s]' % self.test_partner_id,
+ 'model_id': self.res_partner_model_id,
+ 'subject': 'About ${object.name}',
+ 'body_html': '
Dear ${object.name}, your parent is ${object.parent_id and object.parent_id.name or "False"}
',
+ 'ir_actions_server: TODO')
From ce19e05c9991f08da002a6d5adf090e83f804869 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 15 Jul 2013 17:25:33 +0200
Subject: [PATCH 03/79] [IMP] Addons: cleaned server action definition
according to refactoring.
bzr revid: tde@openerp.com-20130715152533-pok1xd9kde2mu6b3
---
addons/crm/crm_action_rule_demo.xml | 8 +-------
.../marketing_campaign_demo.xml | 6 +++---
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/addons/crm/crm_action_rule_demo.xml b/addons/crm/crm_action_rule_demo.xml
index 7204f666db5..5dca69a40d4 100644
--- a/addons/crm/crm_action_rule_demo.xml
+++ b/addons/crm/crm_action_rule_demo.xml
@@ -14,13 +14,7 @@
Trueir.actions.serveremail
- object.user_id.email
- Reminder on Lead: [[object.id ]] [[object.partner_id and 'of ' +object.partner_id.name or '']]
- Warning unprocessed incoming lead is more than 5 day old.
-Name: [[object.name ]]
-ID: [[object.id ]]
-Description: [[object.description]]
-
+ Set Auto Reminder on leads which are not open since 5 days.
diff --git a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
index f6eb8a1d905..bdcbb984193 100644
--- a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
@@ -1,10 +1,10 @@
- Dummy Action
+ Dummy Python Code
- dummy
-
+ code
+ True
From 8397f3de476f48635877e1fedc9574e3527bd10f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 15 Jul 2013 18:03:25 +0200
Subject: [PATCH 04/79] [FIX] [IMP] ir.actions.server: better management of
active_id / active_ids / no id
bzr revid: tde@openerp.com-20130715160325-95h13stsv6516nwr
---
openerp/addons/base/ir/ir_actions.py | 53 +++++++++++++++-------------
1 file changed, 29 insertions(+), 24 deletions(-)
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 412350ef1b3..54bfe0e1110 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -816,7 +816,6 @@ class actions_server(osv.osv):
# TDE FIXME: loops are not considered here ^^
res = []
for act in action.child_ids:
- # context['active_id'] = context['active_ids'][0]
result = self.run(cr, uid, [act.id], context)
if result:
res.append(result)
@@ -896,32 +895,38 @@ class actions_server(osv.osv):
context = {}
res = False
user = self.pool.get('res.users').browse(cr, uid, uid)
+ active_ids = context.get('active_ids', [context.get('active_id'), None])
for action in self.browse(cr, uid, ids, context):
obj = None
obj_pool = self.pool[action.model_id.model]
- if context.get('active_model') == action.model_id.model and context.get('active_id'):
- obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- cxt = {
- 'self': obj_pool,
- 'object': obj,
- 'obj': obj,
- 'pool': self.pool,
- 'time': time,
- 'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
- 'uid': uid,
- 'user': user
- }
- # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
- condition = action.condition
- if action.condition is False:
- condition = True
- expr = eval(str(condition), cxt)
- if not expr:
- continue
- # call the method related to the action: run_action_
- if hasattr(self, 'run_action_%s' % action.state):
- res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=cxt, context=context)
+ for active_id in active_ids:
+ if context.get('active_model') == action.model_id.model and active_id:
+ obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
+ # evaluation context for python strings to evaluate
+ eval_context = {
+ 'self': obj_pool,
+ 'object': obj,
+ 'obj': obj,
+ 'pool': self.pool,
+ 'time': time,
+ 'cr': cr,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'uid': uid,
+ 'user': user
+ }
+ # run context dedicated to a particular active_id
+ run_context = dict(context, active_id=active_id)
+
+ # evaluate the condition, with the specific case that a void (aka False) condition is considered as True
+ condition = action.condition
+ if action.condition is False:
+ condition = True
+ expr = eval(str(condition), eval_context)
+ if not expr:
+ continue
+ # call the method related to the action: run_action_
+ if hasattr(self, 'run_action_%s' % action.state):
+ res = getattr(self, 'run_action_%s' % action.state)(cr, uid, action, eval_context=eval_context, context=run_context)
return res
From a95b32188394a5387f25f1a02265e254190ebc95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Tue, 16 Jul 2013 10:23:44 +0200
Subject: [PATCH 05/79] [FIX] Fixed active_ids / active_id management, wrong
getter on active_id making server action run twice.
bzr revid: tde@openerp.com-20130716082344-cvw0djli0n8pq3bt
---
openerp/addons/base/ir/ir_actions.py | 8 ++++----
openerp/addons/base/tests/test_ir_actions.py | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 54bfe0e1110..3e8b4a76761 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -895,13 +895,15 @@ class actions_server(osv.osv):
context = {}
res = False
user = self.pool.get('res.users').browse(cr, uid, uid)
- active_ids = context.get('active_ids', [context.get('active_id'), None])
+ active_ids = context.get('active_ids', [context.get('active_id', None)])
for action in self.browse(cr, uid, ids, context):
obj = None
obj_pool = self.pool[action.model_id.model]
for active_id in active_ids:
if context.get('active_model') == action.model_id.model and active_id:
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
+ # run context dedicated to a particular active_id
+ run_context = dict(context, active_ids=[active_id], active_id=active_id)
# evaluation context for python strings to evaluate
eval_context = {
'self': obj_pool,
@@ -910,12 +912,10 @@ class actions_server(osv.osv):
'pool': self.pool,
'time': time,
'cr': cr,
- 'context': dict(context), # copy context to prevent side-effects of eval
+ 'context': dict(run_context), # copy context to prevent side-effects of eval
'uid': uid,
'user': user
}
- # run context dedicated to a particular active_id
- run_context = dict(context, active_id=active_id)
# evaluate the condition, with the specific case that a void (aka False) condition is considered as True
condition = action.condition
diff --git a/openerp/addons/base/tests/test_ir_actions.py b/openerp/addons/base/tests/test_ir_actions.py
index 63ed58670b9..c0e1de13845 100644
--- a/openerp/addons/base/tests/test_ir_actions.py
+++ b/openerp/addons/base/tests/test_ir_actions.py
@@ -97,7 +97,7 @@ class TestServerActions(TestServerActionsBase):
'code': """partner_name = obj.name + '_code'
self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context)"""
})
- run_res = self.ir_actions_server.run(cr, uid, [self.act_id], self.context)
+ run_res = self.ir_actions_server.run(cr, uid, [self.act_id], context=self.context)
self.assertFalse(run_res, 'ir_actions_server: code server action correctly finished should return False')
pids = self.res_partner.search(cr, uid, [('name', 'ilike', 'TestingPartner_code')])
From b802e66079816acfaca49796581bb8aea780d111 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Wed, 17 Jul 2013 14:09:36 +0200
Subject: [PATCH 06/79] [IMP] [FIX] ir_actions_server: form view fixes and
improvements + some model update to ease the process of creating new server
actions.
bzr revid: tde@openerp.com-20130717120936-52f0emxeufinzqg4
---
openerp/addons/base/ir/ir_actions.py | 36 ++++++++++++++++++-----
openerp/addons/base/ir/ir_actions.xml | 42 ++++++++++++++++++---------
2 files changed, 56 insertions(+), 22 deletions(-)
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 3e8b4a76761..7bad1e7f0dd 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -529,12 +529,15 @@ class actions_server(osv.osv):
'use_create': fields.selection([('new', 'Create a new record in the Base Model'),
('new_other', 'Create a new record in another model'),
('copy_current', 'Copy the current record'),
- ('copy_other', 'Copy another record')],
+ ('copy_other', 'Choose and copy a record in the database')],
string="Creation Policy", required=True,
help=""),
'crud_model_id': fields.many2one('ir.model', 'Target Model',
oldname='srcmodel_id',
help="Model for record creation / update. Set this field only to specify a different model than the base model."),
+ 'crud_model_name': fields.related('crud_model_id', 'model', type='char',
+ string='Create/Write Target Model Name',
+ store=True, readonly=True),
'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
oldname='copy_object'),
'link_new_record': fields.boolean('Link to current record',
@@ -544,13 +547,13 @@ class actions_server(osv.osv):
oldname='record_id',
help="Provide the field where the record id is stored after the operations."),
'use_write': fields.selection([('current', 'Update the current record'),
- ('other', 'Update another record'),
- ('expression', 'Update according a Python expression')],
+ ('expression', 'Update a record linked to the current record using python'),
+ ('other', 'Choose and Update a record in the database')],
string='Update Policy', required=True,
help=""),
- 'write_expression': fields.char('Write Record Expression',
+ 'write_expression': fields.char('Expression',
oldname='write_id',
- help="Provide the field name that the record id refers to for the write operation. If it is empty it will refer to the active id of the object."),
+ help="Provide an expression that, applied on the current record, gives the field to update."),
'fields_lines': fields.one2many('ir.server.object.lines', 'server_id',
string='Value Mapping',
help=""),
@@ -575,6 +578,15 @@ class actions_server(osv.osv):
'condition': 'True',
'type': 'ir.actions.server',
'sequence': 5,
+ 'code': """# You can use the following variables:
+# - self: ORM model of the record on which the action is triggered
+# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
+# - pool: ORM model pool (i.e. self.pool)
+# - cr: database cursor
+# - uid: current user id
+# - context: current context
+# - time: Python time module
+# If you plan to return an action, assign: action = {...}""",
'use_relational_model': 'base',
'use_create': 'new',
'use_write': 'current',
@@ -626,6 +638,7 @@ class actions_server(osv.osv):
'use_write': 'current',
'use_relational_model': 'base',
'wkf_model_id': model_id,
+ 'wkf_field_id': False,
'crud_model_id': model_id,
}
return {'value': values}
@@ -643,8 +656,6 @@ class actions_server(osv.osv):
values['wkf_model_id'] = new_wkf_model_id
else:
values['wkf_model_id'] = model_id
- if values.get('wkf_model_id') != wkf_model_id:
- values['wkf_transition_id'] = False
return {'value': values}
def on_change_wkf_model_id(self, cr, uid, ids, wkf_model_id, context=None):
@@ -652,7 +663,8 @@ class actions_server(osv.osv):
wkf_model_name = False
if wkf_model_id:
wkf_model_name = self.pool.get('ir.model').browse(cr, uid, wkf_model_id, context).model
- return {'value': {'wkf_model_name': wkf_model_name}}
+ values = {'wkf_transition_id': False, 'wkf_model_name': wkf_model_name}
+ return {'value': values}
def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
""" TODO """
@@ -714,6 +726,14 @@ class actions_server(osv.osv):
}
}
+ def on_change_crud_model_id(self, cr, uid, ids, crud_model_id, context=None):
+ """ When changing the CRUD model, update its stored name also """
+ crud_model_name = False
+ if crud_model_id:
+ crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
+ values = {'link_field_id': False, 'crud_model_name': crud_model_name}
+ return {'value': values}
+
def build_expression(self, field_name, sub_field_name):
"""Returns a placeholder expression for use in a template field,
based on the values provided in the placeholder assistant.
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 0713c333922..15edd14523c 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -355,9 +355,9 @@
attrs="{'required': [('state', '=', 'trigger'), ('use_relational_model', '=', 'relational')],
'invisible': [('use_relational_model', '=', 'base')]}"
domain="[('model_id', '=', model_id), ('ttype', 'in', ['many2one'])]"/>
-
-
+
+
@@ -385,17 +385,27 @@
on_change="on_change_crud_config(state, use_create, use_write, ref_object, crud_model_id, model_id)"
attrs="{'invisible': [('state', '!=', 'object_write')]}"/>
-
+
+
+
+
-
+
+
-
If you use client actions in your multiple actions, only the last client action will be executed. Other client actions will be discarded.
+ If several child actions return an action, only the last one will be executed.
+ This may happen when having server actions executing code that returns an action, or server actions returning a client action.
+
+
-
+
Help with Python expressions.
From db0cd38559bc704527af856d516c5f61be391f6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 22 Jul 2013 13:23:06 +0200
Subject: [PATCH 07/79] [IMP] mail.compose.message: now proposes to work on
active_domain
bzr revid: tde@openerp.com-20130722112306-r1u77tyqzk4bknrb
---
addons/mail/wizard/mail_compose_message.py | 15 +++++++++++----
addons/mail/wizard/mail_compose_message_view.xml | 3 +++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 7685a27f1ed..02fac7f010f 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -73,6 +73,7 @@ class mail_compose_message(osv.TransientModel):
res_id = context.get('default_res_id', context.get('active_id'))
message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
active_ids = context.get('active_ids')
+ result['active_domain'] = '%s' % context.get('active_domain', [])
# get default values according to the composition mode
if composition_mode == 'reply':
@@ -112,6 +113,8 @@ class mail_compose_message(osv.TransientModel):
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional contacts'),
+ 'use_active_domain': fields.boolean('Use active domain'),
+ 'active_domain': fields.char('Active domain', readonly=True),
'post': fields.boolean('Post a copy in the document',
help='Post a copy of the message on the document communication history.'),
'notify': fields.boolean('Notify followers',
@@ -127,9 +130,7 @@ class mail_compose_message(osv.TransientModel):
_defaults = {
'composition_mode': 'comment',
'body': lambda self, cr, uid, ctx={}: '',
- 'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
- 'notify': lambda self, cr, uid, ctx={}: False,
'post': lambda self, cr, uid, ctx={}: True,
'same_thread': lambda self, cr, uid, ctx={}: True,
}
@@ -238,8 +239,14 @@ class mail_compose_message(osv.TransientModel):
context['thread_model'] = wizard.model
active_model_pool = self.pool['mail.thread']
- # wizard works in batch mode: [res_id] or active_ids
- res_ids = active_ids if mass_mail_mode and wizard.model and active_ids else [wizard.res_id]
+ # wizard works in batch mode: [res_id] or active_ids or active_domain
+ if mass_mail_mode and wizard.use_active_domain and wizard.model:
+ res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context)
+ elif mass_mail_mode and wizard.model and active_ids:
+ res_ids = active_ids
+ else:
+ res_ids = [wizard.res_id]
+
for res_id in res_ids:
# mail.message values, according to the wizard options
post_values = {
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 1ca07b6a94c..c8c877e37fa 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -25,6 +25,9 @@
+
+
Date: Mon, 22 Jul 2013 13:23:38 +0200
Subject: [PATCH 08/79] [IMP] sidebar: when cliking on an item, add
active_domain along with active_id and active_ids. This domain is taken from
the search view.
bzr revid: tde@openerp.com-20130722112338-iws5ffxi77ykdvlu
---
addons/web/static/src/js/views.js | 46 ++++++++++++++++++++-----------
1 file changed, 30 insertions(+), 16 deletions(-)
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index 48a108b2f01..67490a4652d 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1183,25 +1183,39 @@ instance.web.Sidebar = instance.web.Widget.extend({
var active_ids_context = {
active_id: ids[0],
active_ids: ids,
- active_model: self.getParent().dataset.model
+ active_model: self.getParent().dataset.model,
};
- var c = instance.web.pyeval.eval('context',
+
+ var search_view = self.getParent().ViewManager.searchview;
+ var search_data = search_view.build_search_data();
+ var active_domain_done = instance.web.pyeval.eval_domains_and_contexts({
+ domains: search_data.domains,
+ contexts: search_data.contexts,
+ group_by_seq: search_data.groupbys || []
+ }).done(function (results) {
+ active_ids_context['active_domain'] = results.domain;
+ });
+
+ $.when(active_domain_done).done(function () {
+ var c = instance.web.pyeval.eval('context',
new instance.web.CompoundContext(
sidebar_eval_context, active_ids_context));
- self.rpc("/web/action/load", {
- action_id: item.action.id,
- context: c
- }).done(function(result) {
- result.context = new instance.web.CompoundContext(
- result.context || {}, active_ids_context)
- .set_eval_context(c);
- result.flags = result.flags || {};
- result.flags.new_window = true;
- self.do_action(result, {
- on_close: function() {
- // reload view
- self.getParent().reload();
- },
+
+ self.rpc("/web/action/load", {
+ action_id: item.action.id,
+ context: c
+ }).done(function(result) {
+ result.context = new instance.web.CompoundContext(
+ result.context || {}, active_ids_context)
+ .set_eval_context(c);
+ result.flags = result.flags || {};
+ result.flags.new_window = true;
+ self.do_action(result, {
+ on_close: function() {
+ // reload view
+ self.getParent().reload();
+ },
+ });
});
});
});
From f1f2b2ba06c6b62c886b7dea13118687e9b93b21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Mon, 22 Jul 2013 15:17:25 +0200
Subject: [PATCH 09/79] [IMP] mail: mail.message now has mail_server_id from
template_id.
bzr revid: tde@openerp.com-20130722131725-i30xd4b03nui3hyw
---
addons/email_template/wizard/mail_compose_message.py | 12 +++++++-----
addons/mail/mail_followers.py | 2 +-
addons/mail/mail_mail.py | 1 -
addons/mail/mail_message.py | 5 +++--
addons/mail/wizard/mail_compose_message.py | 1 +
addons/mail/wizard/mail_compose_message_view.xml | 1 +
6 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py
index 746c2cc55f6..92bf7c3aedf 100644
--- a/addons/email_template/wizard/mail_compose_message.py
+++ b/addons/email_template/wizard/mail_compose_message.py
@@ -22,6 +22,7 @@
from openerp import tools
from openerp.osv import osv, fields
+
def _reopen(self, res_id, model):
return {'type': 'ir.actions.act_window',
'view_mode': 'form',
@@ -34,7 +35,8 @@ def _reopen(self, res_id, model):
'context': {
'default_model': model,
},
- }
+ }
+
class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
@@ -58,7 +60,7 @@ class mail_compose_message(osv.TransientModel):
context = {}
wizard_context = dict(context)
for wizard in self.browse(cr, uid, ids, context=context):
- if wizard.template_id and not wizard.template_id.user_signature:
+ if wizard.template_id:
wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html
if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
continue
@@ -75,7 +77,7 @@ class mail_compose_message(osv.TransientModel):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
- fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:
@@ -95,7 +97,7 @@ class mail_compose_message(osv.TransientModel):
}
values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else:
- values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids'], context=context)
+ values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context)
if values.get('body_html'):
values['body'] = values.pop('body_html')
@@ -150,7 +152,7 @@ class mail_compose_message(osv.TransientModel):
mail.compose.message, transform email_cc and email_to into partner_ids """
template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
# filter template values
- fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments']
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
values['body'] = values.pop('body_html', '')
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index 9d96a6ff59c..ac350810aa6 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -151,7 +151,7 @@ class mail_notification(osv.Model):
return footer
def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None,
- force_send=False, user_signature=True):
+ force_send=False, user_signature=True):
""" Send by email the notification depending on the user preferences
:param list partners_to_notify: optional list of partner ids restricting
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index d45ae04ddd1..bf76216e7e4 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -44,7 +44,6 @@ class mail_mail(osv.Model):
_columns = {
'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
- 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
'state': fields.selection([
('outgoing', 'Outgoing'),
('sent', 'Sent'),
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 9d6829f3f6e..a5d8f348a2b 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -191,6 +191,7 @@ class mail_message(osv.Model):
'vote_user_ids': fields.many2many('res.users', 'mail_vote',
'message_id', 'user_id', string='Votes',
help='Users that voted for this message'),
+ 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
}
def _needaction_domain_get(self, cr, uid, context=None):
@@ -780,8 +781,8 @@ class mail_message(osv.Model):
values['message_id'] = tools.generate_tracking_message_id('private')
newid = super(mail_message, self).create(cr, uid, values, context)
self._notify(cr, uid, newid, context=context,
- force_send=context.get('mail_notify_force_send', True),
- user_signature=context.get('mail_notify_user_signature', True))
+ force_send=context.get('mail_notify_force_send', True),
+ user_signature=context.get('mail_notify_user_signature', True))
# TDE FIXME: handle default_starred. Why not setting an inv on starred ?
# Because starred will call set_message_starred, that looks for notifications.
# When creating a new mail_message, it will create a notification to a message
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 02fac7f010f..95647e95d8e 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -130,6 +130,7 @@ class mail_compose_message(osv.TransientModel):
_defaults = {
'composition_mode': 'comment',
'body': lambda self, cr, uid, ctx={}: '',
+ 'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
'post': lambda self, cr, uid, ctx={}: True,
'same_thread': lambda self, cr, uid, ctx={}: True,
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index c8c877e37fa..20b529c0faa 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -12,6 +12,7 @@
+
From 0568f5c65384cdc70bae5d03f8932a5db6b97cbb Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Mon, 22 Jul 2013 17:17:40 +0200
Subject: [PATCH 10/79] [ADD] test_workflow: added some tests for the
workflows.
bzr revid: vmt@openerp.com-20130722151740-mucw6i5vn38blh66
---
.../tests/addons/test_workflow/__init__.py | 3 +
.../tests/addons/test_workflow/__openerp__.py | 15 ++++
openerp/tests/addons/test_workflow/data.xml | 86 +++++++++++++++++++
openerp/tests/addons/test_workflow/models.py | 37 ++++++++
openerp/tests/addons/test_workflow/run.py | 44 ++++++++++
.../addons/test_workflow/tests/__init__.py | 12 +++
.../test_workflow/tests/test_workflow.py | 62 +++++++++++++
7 files changed, 259 insertions(+)
create mode 100644 openerp/tests/addons/test_workflow/__init__.py
create mode 100644 openerp/tests/addons/test_workflow/__openerp__.py
create mode 100644 openerp/tests/addons/test_workflow/data.xml
create mode 100644 openerp/tests/addons/test_workflow/models.py
create mode 100755 openerp/tests/addons/test_workflow/run.py
create mode 100644 openerp/tests/addons/test_workflow/tests/__init__.py
create mode 100644 openerp/tests/addons/test_workflow/tests/test_workflow.py
diff --git a/openerp/tests/addons/test_workflow/__init__.py b/openerp/tests/addons/test_workflow/__init__.py
new file mode 100644
index 00000000000..fe4487156b1
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import models
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/__openerp__.py b/openerp/tests/addons/test_workflow/__openerp__.py
new file mode 100644
index 00000000000..1b846f9f227
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/__openerp__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'test-workflow',
+ 'version': '0.1',
+ 'category': 'Tests',
+ 'description': """A module to play with workflows.""",
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base'],
+ 'data': ['data.xml'],
+ 'installable': True,
+ 'auto_install': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/data.xml b/openerp/tests/addons/test_workflow/data.xml
new file mode 100644
index 00000000000..c50dfc03bf6
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/data.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+ Test workflow
+ test.workflow.model
+
+
+
+
+
+
+
+
+
+
+
+ Test workflow
+ ir.actions.act_window
+ test.workflow.model
+ form
+ tree,form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test.workflow
+ test.workflow.model
+ True
+
+
+
+
+ True
+ a
+ function
+ print_a()
+
+
+
+ b
+ function
+ print_b()
+
+
+
+ True
+ c
+ function
+ print_c()
+
+
+
+
+
+ a-b
+
+
+
+
+ condition()
+ test.workflow.trigger
+ [1]
+
+
+
diff --git a/openerp/tests/addons/test_workflow/models.py b/openerp/tests/addons/test_workflow/models.py
new file mode 100644
index 00000000000..7f79aeb1484
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/models.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+import openerp
+
+class m(openerp.osv.orm.Model):
+ """ A model for which we will define a worflow (see data.xml). """
+ _name = 'test.workflow.model'
+
+ def print_(self, cr, uid, ids, s, context=None):
+ print ' Running activity `%s` for record %s' % (s, ids)
+ return True
+
+ def print_a(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'a', context)
+
+ def print_b(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'b', context)
+
+ def print_c(self, cr, uid, ids, context=None):
+ return self.print_(cr, uid, ids, 'c', context)
+
+ def condition(self, cr, uid, ids, context=None):
+ m = self.pool['test.workflow.trigger']
+ for r in m.browse(cr, uid, [1], context=context):
+ if not r.value:
+ return False
+ return True
+
+ def trigger(self, cr, uid, context=None):
+ return openerp.workflow.trg_trigger(uid, 'test.workflow.trigger', 1, cr)
+
+class n(openerp.osv.orm.Model):
+ """ A model used for the trigger feature. """
+ _name = 'test.workflow.trigger'
+ _columns = { 'value': openerp.osv.fields.boolean('Value') }
+ _defaults = { 'value': False }
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/run.py b/openerp/tests/addons/test_workflow/run.py
new file mode 100755
index 00000000000..dda955f9ce9
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/run.py
@@ -0,0 +1,44 @@
+#! /usr/bin/env python2
+import os
+
+import openerp
+from openerp import SUPERUSER_ID
+
+DATABASE='t'
+
+def configure():
+ conf = openerp.tools.config
+ conf['addons_path'] = os.path.join(os.path.dirname(os.path.dirname(__file__))) + ",/home/thu/repos/web/trunk/addons"
+ conf['log_handler'] = [':CRITICAL']
+ openerp.modules.module.initialize_sys_path()
+ openerp.netsvc.init_logger()
+
+if __name__ == '__main__':
+ configure()
+
+ print '> Loading registry `%s`...' % DATABASE
+ registry = registry = openerp.registry(DATABASE)
+ cr = registry.db.cursor()
+ print '< Registry `%s` loaded.' % DATABASE
+
+ model = registry['test.workflow.model']
+ trigger = registry['test.workflow.trigger']
+
+ print '> Creating new record...'
+ i = model.create(cr, SUPERUSER_ID, {})
+ print '< Record created with ID %s' % i
+
+ print '> Calling signal `a-b`...'
+ model.signal_workflow(cr, SUPERUSER_ID, [i], 'a-b')
+ print '< Signal `a-b` called.'
+
+ print '> Triggering associated record...'
+ model.trigger(cr, SUPERUSER_ID, [i])
+ print '< Associated record triggered.'
+
+ print '> Triggering associated record (this time with the condition evaluating to True)...'
+ trigger.write(cr, SUPERUSER_ID, [1], {'value': True})
+ model.trigger(cr, SUPERUSER_ID)
+ print '< Associated record triggered.'
+
+ cr.close()
diff --git a/openerp/tests/addons/test_workflow/tests/__init__.py b/openerp/tests/addons/test_workflow/tests/__init__.py
new file mode 100644
index 00000000000..a09d09fe74a
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/tests/__init__.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+
+from . import test_workflow
+
+fast_suite = [
+]
+
+checks = [
+ test_workflow,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/tests/test_workflow.py b/openerp/tests/addons/test_workflow/tests/test_workflow.py
new file mode 100644
index 00000000000..60ace7ee269
--- /dev/null
+++ b/openerp/tests/addons/test_workflow/tests/test_workflow.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+import openerp
+from openerp import SUPERUSER_ID
+from openerp.tests import common
+
+
+class test_workflows(common.TransactionCase):
+
+ def check_activities(self, model_name, i, names):
+ """ Check that the record i has workitems in the given activity names.
+ """
+ instance = self.registry('workflow.instance')
+ workitem = self.registry('workflow.workitem')
+
+ # Given the workflow instance associated to the record ...
+ instance_id = instance.search(
+ self.cr, SUPERUSER_ID,
+ [('res_type', '=', model_name), ('res_id', '=', i)])
+ self.assertTrue( instance_id, 'A workflow instance is expected.')
+
+ # ... get all its workitems ...
+ workitem_ids = workitem.search(
+ self.cr, SUPERUSER_ID,
+ [('inst_id', '=', instance_id[0])])
+ self.assertTrue(
+ workitem_ids,
+ 'The workflow instance should have workitems.')
+
+ # ... and check the activity the are in against the provided names.
+ workitem_records = workitem.browse(
+ self.cr, SUPERUSER_ID, workitem_ids)
+ self.assertEqual(
+ sorted([item.act_id.name for item in workitem_records]),
+ sorted(names))
+
+ def test_workflow(self):
+ model = self.registry('test.workflow.model')
+ trigger = self.registry('test.workflow.trigger')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+
+ # a -> b is just a signal.
+ model.signal_workflow(self.cr, SUPERUSER_ID, [i], 'a-b')
+ self.check_activities(model._name, i, ['b'])
+
+ # b -> c is a trigger (which is False),
+ # so we remain in the b activity.
+ model.trigger(self.cr, SUPERUSER_ID, [i])
+ self.check_activities(model._name, i, ['b'])
+
+ # b -> c is a trigger (which is set to True).
+ # so we go in c when the trigger is called.
+ trigger.write(self.cr, SUPERUSER_ID, [1], {'value': True})
+ model.trigger(self.cr, SUPERUSER_ID)
+ self.check_activities(model._name, i, ['c'])
+
+ self.assertEqual(
+ True,
+ True)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
From 9357ed959a8b9228264f60377a877ae5a70629d6 Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Tue, 23 Jul 2013 13:21:47 +0200
Subject: [PATCH 11/79] [DOC] workflows: started to write some workflow
documentation.
bzr revid: vmt@openerp.com-20130723112147-wje0mt5e1a59zj68
---
doc/index.rst | 1 +
doc/workflows.rst | 104 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 105 insertions(+)
create mode 100644 doc/workflows.rst
diff --git a/doc/index.rst b/doc/index.rst
index e641de1da67..b5ac49f6a8f 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -14,6 +14,7 @@ OpenERP Server
02_architecture
03_module_dev
04_security
+ workflows
05_test_framework
06_misc
deployment-gunicorn
diff --git a/doc/workflows.rst b/doc/workflows.rst
new file mode 100644
index 00000000000..6e7308d485d
--- /dev/null
+++ b/doc/workflows.rst
@@ -0,0 +1,104 @@
+.. _workflows:
+
+Workflows
+=========
+
+A workflow is a directed graph where the nodes are called "activities" and the
+arcs are called "transitions".
+
+- Activities define work that should be done within the OpenERP server, such as
+ changing the state of some records, or sending mails.
+
+- Transitions control how the workflow will go from activities to activities.
+
+When defining a workflow, one can attach conditions, signals, and triggers to
+transitions, so that the behavior of the workflow can depend on user actions
+(such as clicking on a button), changes to records, or arbitrary Python code.
+
+XML definition
+--------------
+
+Defining a workflow with data files is straightforward: a record "workflow" is
+needed together with records for the activities and the transitions. For
+instance here is a simple sequence of two activities defined in XML::
+
+
+ test.workflow
+ test.workflow.model
+ True
+
+
+
+
+ True
+ a
+ function
+ print_a()
+
+
+
+ True
+ b
+ function
+ print_b()
+
+
+
+
+
+
+
+A worfklow is always defined with respect to a particular model (the model is
+given through the ``osv`` attribute on the ``workflow`` model). Methods
+specified in the activities or transitions will be called on that model.
+
+In the example code above, a workflow called "test_workflow" is created. It is
+made up of two activies, named "a" and "b", and one transition, going from "a"
+to "b".
+
+The first activity has its ``flow_start`` attribute set to True so that OpenERP
+knows where to start the workflow when it is instanciated. Because
+``on_create`` is set to True on the workflow record, the workflow is
+instanciated for each newly created record. (Otherwise, the workflow should be
+created by other means, such as from some module Python code.)
+
+When the workflow is instanciated, it will start by the "a" activity. That
+activity is of kind ``function`` which means the action ``print_a()`` is a
+method to be called on the ``test.workflow`` model (the usual ``cr, uid, ids,
+context`` arguments are passed for you).
+
+The transition between "a" and "b" does not specify any conditions. This means
+the workflow instance will immediately progress from "a" to "b" (after "a" has
+been processed), and thus elso process the "b" activity.
+
+Conditions
+----------
+
+Triggers
+--------
+
+When an activity is completed, the workflow engine will try to get across
+transitions departing from the completed activity, towards the next activities.
+To get across a transition, its associated condition should evaluate to True.
+If the condition evaluates to False, the transition is not taken (and thus the
+activity it leads to will not be processed). Still, the workflow instance can
+get new chances to progress across that transition by providing so-called
+triggers. The idea is that when the condition fails, triggers (actually just
+model name/record IDs pairs) are recorded in database. Later, it is possible to
+wake-up specifically the workflow instances that installed those triggers,
+offering them a new chance to evaluation their transition conditions. This
+mechnism makes it cheaper to wake-up workflow instances by targetting just a
+few of them (those that have installed the triggers) instead of all of them.
+
+On each transition, in addition to a condition, records can be defined as a
+trigger. The records will be defined as triggers as the transition is tried
+withing a workflow, after the condition has failed. The actual records are
+stored as model name and record ids. The model name is defined by the
+trigger_model attribute of the transition while the record IDs are retrived by
+evaluating the trigger_expression (also defined on the transition).
+
+- I think the triggers are never deleted from the database. They are: they are
+ 'on delete cascade' on both the workflow instance and the workitem.
+
+- Are those triggers re-installed whenever the transition is tried ? Nope.
+
From b5068764adb294f3d914d46751a3cbe0bfc55dd2 Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Tue, 23 Jul 2013 14:51:43 +0200
Subject: [PATCH 12/79] [DOC] workflows: added Conditions section.
bzr revid: vmt@openerp.com-20130723125143-20n8ukewixxbkbt4
---
doc/workflows.rst | 49 ++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 44 insertions(+), 5 deletions(-)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index 6e7308d485d..939433440a2 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -15,8 +15,8 @@ When defining a workflow, one can attach conditions, signals, and triggers to
transitions, so that the behavior of the workflow can depend on user actions
(such as clicking on a button), changes to records, or arbitrary Python code.
-XML definition
---------------
+Basics
+------
Defining a workflow with data files is straightforward: a record "workflow" is
needed together with records for the activities and the transitions. For
@@ -69,13 +69,52 @@ context`` arguments are passed for you).
The transition between "a" and "b" does not specify any conditions. This means
the workflow instance will immediately progress from "a" to "b" (after "a" has
-been processed), and thus elso process the "b" activity.
+been processed), and thus also process the "b" activity.
+
+Transitions
+-----------
+
+Transitions provide the control structures to orchestrate a workflow. In their
+simplest form they just link activities from one to the others (as in the
+example above), and activities are processed as soon as the activities
+preceding them are completed.
+
+But instead of running all activities in one fell swoop, it is also possible to
+block on transitions, going through them only when some criteria are met. Such
+criteria are the conditions, the signals, and the triggers. They are detailed
+in the next sections.
Conditions
-----------
+''''''''''
+
+When an activity has been completed, its outgoing transitions will be inspected
+to see if it is possible for the workflow instance to proceed through them and
+reach the next activities. When only a condition is defined (i.e. no signal or
+trigger is defined), the condition is evaluated by OpenERP, and if it evaluates
+to ``True``, the worklfow instance will go through.
+
+By default, the ``condition`` attribute (i.e. the expression to be evaluated)
+is just "True", which will trivially evaluate to ``True``.
+
+Actually, the condition can be several lines long, and the value of the last
+one will be used to test if the transition can be taken.
+
+In the condition evaluation environment, several symbols are conveniently
+defined:
+
+- The database cursor (``cr``),
+- the user ID (``uid``), the record ID tied to the workflow instance (``id``),
+- the user ID wrapped in a list (``ids``),
+- the model name (``model``),
+- the model instance (``obj``),
+- all the model column names,
+- and all the record (the one obtained by browsing the provided ID) attributes.
+
+Signals
+'''''''
Triggers
---------
+''''''''
When an activity is completed, the workflow engine will try to get across
transitions departing from the completed activity, towards the next activities.
From 2397c1a98ba6b93dc013d6d338a50ec49bacd47f Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Tue, 23 Jul 2013 14:52:42 +0200
Subject: [PATCH 13/79] [IMP] workflows: added docstring, some variable
renaming (to wkf_expr).
bzr revid: vmt@openerp.com-20130723125242-mpzpjfbnyrmx3bzt
---
openerp/workflow/wkf_expr.py | 74 +++++++++++++++++++++++++-----------
1 file changed, 52 insertions(+), 22 deletions(-)
diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py
index b43fa83e3de..0b83195aec2 100644
--- a/openerp/workflow/wkf_expr.py
+++ b/openerp/workflow/wkf_expr.py
@@ -19,62 +19,92 @@
#
##############################################################################
+"""
+Evaluate workflow code found in activity actions and transition conditions.
+"""
+
import openerp
from openerp.tools.safe_eval import safe_eval as eval
class Env(dict):
- def __init__(self, cr, uid, model, ids):
+ """
+ Dictionary class used as an environment to evaluate workflow code (such as
+ the condition on transitions).
+
+ This environment provides sybmols for cr, uid, id, model name, model
+ instance, column names, and all the record (the one obtained by browsing
+ the provided ID) attributes.
+ """
+ def __init__(self, cr, uid, model, id):
self.cr = cr
self.uid = uid
self.model = model
- self.ids = ids
+ self.id = id
+ self.ids = [id]
self.obj = openerp.registry(cr.dbname)[model]
self.columns = self.obj._columns.keys() + self.obj._inherit_fields.keys()
def __getitem__(self, key):
if (key in self.columns) or (key in dir(self.obj)):
- res = self.obj.browse(self.cr, self.uid, self.ids[0])
+ res = self.obj.browse(self.cr, self.uid, self.id)
return res[key]
else:
return super(Env, self).__getitem__(key)
-def _eval_expr(cr, ident, workitem, action):
- ret=False
- assert action, 'You used a NULL action in a workflow, use dummy node instead.'
- for line in action.split('\n'):
+def _eval_expr(cr, ident, workitem, lines):
+ """
+ Evaluate each line of ``lines`` with the ``Env`` environment, returning
+ the value of the last line.
+ """
+ assert lines, 'You used a NULL action in a workflow, use dummy node instead.'
+ uid, model, id = ident
+ result = False
+ for line in lines.split('\n'):
line = line.strip()
if not line:
continue
- uid=ident[0]
- model=ident[1]
- ids=[ident[2]]
- if line =='True':
- ret=True
- elif line =='False':
- ret=False
+ if line == 'True':
+ result = True
+ elif line == 'False':
+ result = False
else:
- env = Env(cr, uid, model, ids)
- ret = eval(line, env, nocopy=True)
- return ret
+ env = Env(cr, uid, model, id)
+ result = eval(line, env, nocopy=True)
+ return result
def execute_action(cr, ident, workitem, activity):
- obj = openerp.registry(cr.dbname)['ir.actions.server']
- ctx = {'active_model':ident[1], 'active_id':ident[2], 'active_ids':[ident[2]]}
- result = obj.run(cr, ident[0], [activity['action_id']], ctx)
+ """
+ Evaluate the ir.actions.server action specified in the activity.
+ """
+ uid, model, id = ident
+ ir_actions_server = openerp.registry(cr.dbname)['ir.actions.server']
+ context = { 'active_model': model, 'active_id': id, 'active_ids': [id] }
+ result = ir_actions_server.run(cr, uid, [activity['action_id']], context)
return result
def execute(cr, ident, workitem, activity):
+ """
+ Evaluate the action specified in the activity.
+ """
return _eval_expr(cr, ident, workitem, activity['action'])
def check(cr, workitem, ident, transition, signal):
+ """
+ Test if a transition can be taken. The transition can be taken if:
+
+ - the signal name matches,
+ - the uid is SUPERUSER_ID or the user groups contains the transition's
+ group,
+ - the condition evaluates to a truish value.
+ """
if transition['signal'] and signal != transition['signal']:
return False
uid = ident[0]
- if transition['group_id'] and uid != 1:
+ if uid != openerp.SUPERUSER_ID and transition['groups_id']:
registry = openerp.registry(cr.dbname)
user_groups = registry['res.users'].read(cr, uid, [uid], ['groups_id'])[0]['groups_id']
- if not transition['group_id'] in user_groups:
+ if transition['group_id'] not in user_groups:
return False
return _eval_expr(cr, ident, workitem, transition['condition'])
From 3f9fb6eacc5468c62b9017fd2af6e259e16b7c8f Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Tue, 23 Jul 2013 15:27:51 +0200
Subject: [PATCH 14/79] [DOC] workflows: added Signals section.
bzr revid: vmt@openerp.com-20130723132751-lwn5r6y1m51jhj9o
---
doc/workflows.rst | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index 939433440a2..19b845f7f98 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -113,6 +113,19 @@ defined:
Signals
'''''''
+In addition of a condition, a transition can specify a signal name. When such
+signal name is present, the transition will not be taken directly (even if the
+condition evaluates to true). Instead the transition will block, waiting to be
+woken up.
+
+To wake up a transition with a defined signal name, the signal must be sent to
+the workflow. A common way to send a signal is to use a button in the web
+interface, using the ```` element with the signal name as the ``name``
+attribute of the button.
+
+.. note:: The condition is still evaluated when the signal is sent to the
+ workflow instance.
+
Triggers
''''''''
From 8501e89735f826332d3cd4fbeb543af637405e1b Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Tue, 23 Jul 2013 17:12:49 +0200
Subject: [PATCH 15/79] [DOC] workflows: improved Triggers section.
bzr revid: vmt@openerp.com-20130723151249-oa18owwzg82ea79t
---
doc/workflows.rst | 39 ++++++++++++++++++---------------------
1 file changed, 18 insertions(+), 21 deletions(-)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index 19b845f7f98..928cd49fbcc 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -74,7 +74,9 @@ been processed), and thus also process the "b" activity.
Transitions
-----------
-Transitions provide the control structures to orchestrate a workflow. In their
+Transitions provide the control structures to orchestrate a workflow. When an
+activity is completed, the workflow engine will try to get across transitions
+departing from the completed activity, towards the next activities. In their
simplest form they just link activities from one to the others (as in the
example above), and activities are processed as soon as the activities
preceding them are completed.
@@ -129,28 +131,23 @@ attribute of the button.
Triggers
''''''''
-When an activity is completed, the workflow engine will try to get across
-transitions departing from the completed activity, towards the next activities.
-To get across a transition, its associated condition should evaluate to True.
-If the condition evaluates to False, the transition is not taken (and thus the
+With conditions that evaluate to false, transitions are not taken (and thus the
activity it leads to will not be processed). Still, the workflow instance can
get new chances to progress across that transition by providing so-called
-triggers. The idea is that when the condition fails, triggers (actually just
-model name/record IDs pairs) are recorded in database. Later, it is possible to
-wake-up specifically the workflow instances that installed those triggers,
-offering them a new chance to evaluation their transition conditions. This
-mechnism makes it cheaper to wake-up workflow instances by targetting just a
-few of them (those that have installed the triggers) instead of all of them.
+triggers. The idea is that when the condition fails, triggers are recorded in
+database. Later, it is possible to wake-up specifically the workflow instances
+that installed those triggers, offering them a new chance to evaluation their
+transition conditions. This mechnism makes it cheaper to wake-up workflow
+instances by targetting just a few of them (those that have installed the
+triggers) instead of all of them.
-On each transition, in addition to a condition, records can be defined as a
-trigger. The records will be defined as triggers as the transition is tried
-withing a workflow, after the condition has failed. The actual records are
-stored as model name and record ids. The model name is defined by the
-trigger_model attribute of the transition while the record IDs are retrived by
-evaluating the trigger_expression (also defined on the transition).
+Triggers are recorded in database as record IDs (together with the model name)
+and refer to the workflow instance waiting for them. The transition definition
+can thus provide a Python expression (using the ``trigger_model`` attribute)
+that when evaluated will return the record IDs. Unlike the other expressions
+defined on the workflow, this one is evaluated with respect to a model that can
+be chosen on a per-transition basis with the ``trigger_expression`` attribute.
-- I think the triggers are never deleted from the database. They are: they are
- 'on delete cascade' on both the workflow instance and the workitem.
-
-- Are those triggers re-installed whenever the transition is tried ? Nope.
+.. note:: Note that triggers are not re-installed whenever the transition is
+ re-tried.
From 86c544d2024495d6c533a7263245fda7446bdcaf Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Wed, 24 Jul 2013 14:37:41 +0200
Subject: [PATCH 16/79] [DOC] workflows: added Flow start and flow stop
section.
bzr revid: vmt@openerp.com-20130724123741-jf3l9w3flxf7ty4k
---
doc/workflows.rst | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index 928cd49fbcc..d631fdfb265 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -151,3 +151,33 @@ be chosen on a per-transition basis with the ``trigger_expression`` attribute.
.. note:: Note that triggers are not re-installed whenever the transition is
re-tried.
+Activities
+----------
+
+While the transitions can be seen as the control structure of the workflows,
+activities are the place where everything happen, from changing record states
+to sending email.
+
+Different kind of activities exist: ``Dummy``, ``Function``, ``Subflow``, and
+``Stop all``; different kind of activities can do different and they are
+detailed below.
+
+In addition to the activity kind, activies have some properties, detailed in
+the next sections.
+
+Flow start and flow stop
+''''''''''''''''''''''''
+
+The ``flow_start`` attribute is a boolean value specifying if the activity
+starts the workflow. Multiple activities can have the ``flow_start`` attribute
+set to ``True`` and when instanciating a workflow for a record, OpenERP will
+simply process all of them, and try all their outgoing transitions afterwards.
+
+The ``flow_stop`` attribute is also a boolean value, specifying if the activity
+ends the workflow. A workflow is considered to be completed when all its
+activities with the ``flow_stop`` attribute set to ``True`` are completed.
+
+It is important for OpenERP to know when a workflow instance is completed: a
+workflow can have an activity that is actually another workflow (called a
+subflow) and that activity will be completed only when the subflow is
+completed.
From eff32c650c5c69d64ad21bc4a7a003fce9f8e1ac Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Wed, 24 Jul 2013 15:51:02 +0200
Subject: [PATCH 17/79] [DOC] workflows: added Subflow section.
bzr revid: vmt@openerp.com-20130724135102-2zzaoqpjpebim59k
---
doc/workflows.rst | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index d631fdfb265..521ca0bb6d3 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -181,3 +181,29 @@ It is important for OpenERP to know when a workflow instance is completed: a
workflow can have an activity that is actually another workflow (called a
subflow) and that activity will be completed only when the subflow is
completed.
+
+Subflow
+'''''''
+
+An activity can embed a complete workflow, called a subflow (the embedding
+workflow is called the parent workflow). The workflow to instanciate is
+specified by the ``subflow_id`` attribute.
+
+.. note:: In the GUI, that attribute can not be set unless the kind of the
+ activity is ``Subflow``.
+
+The activity will be completed (and its outgoing transitions will be tried)
+when the subflow is completed (see the ``flow_stop`` attribute above to read
+about when a workflow is considered completed by OpenERP).
+
+Sending a signal from a subflow
+'''''''''''''''''''''''''''''''
+
+When a workflow is used (as a sublfow) in the activity of a (parent) workflow,
+the sublow can send a signal from its own activities to the parent by specifying a
+signal name in the ``signal_send`` attribute. OpenERP will process those
+activities normally and send to the parent workflow instance a signal with
+``signal_send`` value prefixed with ``subflow.``.
+
+In other words, it is possible to react and take transitions in the parent
+workflow as activities are executed in the sublow.
From 33ab08b8b37aa859eae55b334e2d959c2457d3c4 Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Wed, 24 Jul 2013 17:25:18 +0200
Subject: [PATCH 18/79] [DOC] workflows: added remaining sections for the
Activities.
bzr revid: vmt@openerp.com-20130724152518-gab0fa44t8a81u30
---
doc/workflows.rst | 69 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 69 insertions(+)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index 521ca0bb6d3..a1165b6f5d2 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -151,6 +151,14 @@ be chosen on a per-transition basis with the ``trigger_expression`` attribute.
.. note:: Note that triggers are not re-installed whenever the transition is
re-tried.
+Splitting and joining transitions
+'''''''''''''''''''''''''''''''''
+
+When multiple transitions leave the same activity, or lead to the same
+activity, OpenERP provides some control about which transitions will be
+crossed, or how the reached activity will be processed. The ``split_mode`` and
+``join_mode`` attributes on the activity are used for such control.
+
Activities
----------
@@ -207,3 +215,64 @@ activities normally and send to the parent workflow instance a signal with
In other words, it is possible to react and take transitions in the parent
workflow as activities are executed in the sublow.
+
+Server actions
+''''''''''''''
+
+An activity can run a "Server Action" by specifying its ID in the ``action_id``
+attribute.
+
+Python action
+'''''''''''''
+
+An activity can run some Python code, provided through the ``action``
+attribute. See the section about transition conditions to read about the
+evaluation environment.
+
+Split mode
+''''''''''
+
+After an activity has been processed, its outgoing transitions will be tried.
+Normally, if a transition can be taken, OpenERP will do it and proceed to the
+activity the transition leads to.
+
+Actually, when more than a single transition is leaving an activity, OpenERP
+can proceed, or not, depending on the other transitions. That is, the condition
+on the transitions can be combined together, and the combined result will
+instruct OpenERP to cross zero, one, or all the transitions. The way they are
+combined is controlled by the ``split_mode`` attribute.
+
+There are indeed three modes to decide how to combine the transition
+conditions, ``XOR``, ``OR``, and ``AND``.
+
+``XOR``
+ When the transitions are combined with a ``XOR`` split mode, as soon as a
+ transition with a condition that evaluates to true is found, the
+ transition is taken and the other will not be tried.
+
+``OR``
+ With an ``OR`` mode, all the transitions with a condition that evaluates to
+ true are taken. The remaining transitions will not be tried later.
+
+``AND``
+ With an ``AND`` mode, OpenERP will wait for all transition conditions to
+ evaluate to true, then cross all the transitions at the same time.
+
+Join mode
+'''''''''
+
+Just as departing transition conditions can be combined together to decide
+whether they can be taken or not, arriving transitions can be combined together
+to decide if an activity must be run. The attribute to control that behavior is
+called ``join_mode``.
+
+The join mode is a bit simpler than the split mode: only two modes are
+provided, ``XOR`` and ``AND``.
+
+``XOR``
+ An activity with a ``XOR`` join mode will be run as soon as a transition is
+ crossed to arrive at the activity.
+
+``AND``
+ With an ``AND`` mode, the activity will wait for all its incoming
+ transitions to be crossed before being run.
From b7f02c329e05343c5e4bfa3a35d9bb81ffcff8cc Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Thu, 25 Jul 2013 11:42:19 +0200
Subject: [PATCH 19/79] [DOC] workflows: added Kinds section.
bzr revid: vmt@openerp.com-20130725094219-tintybvpdl5izdca
---
doc/workflows.rst | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/doc/workflows.rst b/doc/workflows.rst
index a1165b6f5d2..293b2c347c0 100644
--- a/doc/workflows.rst
+++ b/doc/workflows.rst
@@ -276,3 +276,32 @@ provided, ``XOR`` and ``AND``.
``AND``
With an ``AND`` mode, the activity will wait for all its incoming
transitions to be crossed before being run.
+
+Kinds
+'''''
+
+Activities can be of different kinds: ``dummy``, ``function``, ``subflow``, or
+``stopall``. The kind defines what type of work an activity can do.
+
+Dummy
+ The ``dummy`` kind is for activities that do nothing (i.e. they act as hubs
+ to gather/dispatch transitions), or for activities that only call a Server
+ Action.
+
+Function
+ The ``function`` kind is for activities that only need to run some Python
+ code, and possibly a Server Action.
+
+Stop all
+ The ``stopall`` kind is for activities that will completely halt the
+ workflow instance. In addition they can also run some Python code.
+
+Subflow
+ When the kind of the activity is ``subflow``, the activity will run
+ another workflow. When the sub-workflow is completed, the activity will also be
+ considered completed.
+
+ Normally the sub-workflow is instanciated for the same record as the parent
+ workflow. It is possible to change that default behavior by providing
+ Python code that has to return a record ID for which a workflow has been
+ instanciated.
From ae01b6ab21c3ab26aab135087cf94d80a660c6c2 Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Thu, 25 Jul 2013 11:42:46 +0200
Subject: [PATCH 20/79] [ADD] workflows: added some tests.
bzr revid: vmt@openerp.com-20130725094246-4v2p0sgzocnt5vwf
---
openerp/tests/addons/test_workflow/data.xml | 84 ++++++++++++++++++-
openerp/tests/addons/test_workflow/models.py | 19 ++++-
.../test_workflow/tests/test_workflow.py | 43 ++++++++++
3 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/openerp/tests/addons/test_workflow/data.xml b/openerp/tests/addons/test_workflow/data.xml
index c50dfc03bf6..a07451e2f42 100644
--- a/openerp/tests/addons/test_workflow/data.xml
+++ b/openerp/tests/addons/test_workflow/data.xml
@@ -37,7 +37,6 @@
-
+
+ test.workflow.a
+ test.workflow.model.a
+ True
+
+
+
+
+ True
+ True
+ a
+ dummy
+
+
+
+
+ test.workflow.b
+ test.workflow.model.b
+ True
+
+
+
+
+ True
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+
+ test.workflow.c
+ test.workflow.model.c
+ True
+
+
+
+
+ True
+ True
+ a
+ dummy
+ write({'value': 1})
+
+
+
+
+ test.workflow.d
+ test.workflow.model.d
+ True
+
+
+
+
+ True
+ True
+ a
+ stopall
+ write({'value': 1})
+
diff --git a/openerp/tests/addons/test_workflow/models.py b/openerp/tests/addons/test_workflow/models.py
index 7f79aeb1484..b1df8e662d3 100644
--- a/openerp/tests/addons/test_workflow/models.py
+++ b/openerp/tests/addons/test_workflow/models.py
@@ -2,7 +2,7 @@
import openerp
class m(openerp.osv.orm.Model):
- """ A model for which we will define a worflow (see data.xml). """
+ """ A model for which we will define a workflow (see data.xml). """
_name = 'test.workflow.model'
def print_(self, cr, uid, ids, s, context=None):
@@ -34,4 +34,21 @@ class n(openerp.osv.orm.Model):
_columns = { 'value': openerp.osv.fields.boolean('Value') }
_defaults = { 'value': False }
+class a(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.a'
+ _columns = { 'value': openerp.osv.fields.integer('Value') }
+ _defaults = { 'value': 0 }
+
+class b(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.b'
+ _inherit = 'test.workflow.model.a'
+
+class c(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.c'
+ _inherit = 'test.workflow.model.a'
+
+class d(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.d'
+ _inherit = 'test.workflow.model.a'
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/tests/test_workflow.py b/openerp/tests/addons/test_workflow/tests/test_workflow.py
index 60ace7ee269..c09511dc168 100644
--- a/openerp/tests/addons/test_workflow/tests/test_workflow.py
+++ b/openerp/tests/addons/test_workflow/tests/test_workflow.py
@@ -33,6 +33,13 @@ class test_workflows(common.TransactionCase):
sorted([item.act_id.name for item in workitem_records]),
sorted(names))
+ def check_value(self, model_name, i, value):
+ """ Check that the record i has the given value.
+ """
+ model = self.registry(model_name)
+ record = model.read(self.cr, SUPERUSER_ID, [i], ['value'])[0]
+ self.assertEqual(record['value'], value)
+
def test_workflow(self):
model = self.registry('test.workflow.model')
trigger = self.registry('test.workflow.trigger')
@@ -60,3 +67,39 @@ class test_workflows(common.TransactionCase):
True)
model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_a(self):
+ model = self.registry('test.workflow.model.a')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 0)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_b(self):
+ model = self.registry('test.workflow.model.b')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_c(self):
+ model = self.registry('test.workflow.model.c')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 0)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_d(self):
+ model = self.registry('test.workflow.model.d')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
From 64ddf5f26422dfef9c545cc74e6db975d4a2eb50 Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Thu, 25 Jul 2013 12:43:27 +0200
Subject: [PATCH 21/79] [ADD] workflows: more tests.
bzr revid: vmt@openerp.com-20130725104327-l7onjhipi6fdnrj6
---
openerp/tests/addons/test_workflow/data.xml | 65 +++++++++++++++++++
openerp/tests/addons/test_workflow/models.py | 13 ++++
.../test_workflow/tests/test_workflow.py | 22 +++++++
3 files changed, 100 insertions(+)
diff --git a/openerp/tests/addons/test_workflow/data.xml b/openerp/tests/addons/test_workflow/data.xml
index a07451e2f42..c6f542879a0 100644
--- a/openerp/tests/addons/test_workflow/data.xml
+++ b/openerp/tests/addons/test_workflow/data.xml
@@ -164,5 +164,70 @@
stopallwrite({'value': 1})
+
+
+
+ test.workflow.e
+ test.workflow.model.e
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+
+
+
+
+ test.workflow.f
+ test.workflow.model.f
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+ a-b
+
diff --git a/openerp/tests/addons/test_workflow/models.py b/openerp/tests/addons/test_workflow/models.py
index b1df8e662d3..84789cc9b69 100644
--- a/openerp/tests/addons/test_workflow/models.py
+++ b/openerp/tests/addons/test_workflow/models.py
@@ -51,4 +51,17 @@ class d(openerp.osv.orm.Model):
_name = 'test.workflow.model.d'
_inherit = 'test.workflow.model.a'
+class e(openerp.osv.orm.Model):
+ _name = 'test.workflow.model.e'
+ _inherit = 'test.workflow.model.a'
+
+for name in 'bcdef':
+ type(
+ name,
+ (openerp.osv.orm.Model,),
+ {
+ '_name': 'test.workflow.model.%s' % name,
+ '_inherit': 'test.workflow.model.a',
+ })
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tests/addons/test_workflow/tests/test_workflow.py b/openerp/tests/addons/test_workflow/tests/test_workflow.py
index c09511dc168..d55657f6a6a 100644
--- a/openerp/tests/addons/test_workflow/tests/test_workflow.py
+++ b/openerp/tests/addons/test_workflow/tests/test_workflow.py
@@ -103,3 +103,25 @@ class test_workflows(common.TransactionCase):
self.check_value(model._name, i, 1)
model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_e(self):
+ model = self.registry('test.workflow.model.e')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_f(self):
+ model = self.registry('test.workflow.model.f')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.signal_workflow(self.cr, SUPERUSER_ID, [i], 'a-b')
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
From f737eb63d06e728ae754bcbed8efe0e41994bd0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 12:49:14 +0200
Subject: [PATCH 22/79] [IMP] ir_actions_server: added doc + comments +
recursion check orm: added _check_m2my_recursion, to check loops in many2many
recursive fields tools: removed sms_send
bzr revid: tde@openerp.com-20130725104914-dutxfon3odp8z167
---
doc/changelog.rst | 7 ++-
doc/index.rst | 1 +
openerp/addons/base/ir/ir_actions.py | 53 +++++++++++++++-----
openerp/addons/base/tests/test_ir_actions.py | 8 +++
openerp/osv/orm.py | 34 +++++++++++++
openerp/tools/misc.py | 12 -----
6 files changed, 89 insertions(+), 26 deletions(-)
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 9a84e5dfd00..02196aa5e28 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -6,7 +6,12 @@ Changelog
`trunk`
-------
-- Cleaned and slightly refactored ``ir.actions.server``
+- Cleaned and slightly refactored ``ir.actions.server``. The ``loop``, ``sms``
+ and ``dummy`` server actions have been removed; ``object_create`` and
+ ``object_copy`` have been merged into ``object_create``; ``other`` is now ``multi``
+ and raises in case of loops. See :ref:`ir-actions-server` for more details.
+- Removed ``sms_send`` method.
+- Added checking of recursions in many2many loops using ``_check_m2m_recursion``.
- Added MONTHS attribute on fields.date and fields.datetime, holding the list
(month_number, month_name)
- Almost removed ``LocalService()``. For reports,
diff --git a/doc/index.rst b/doc/index.rst
index e641de1da67..a5af574d09e 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -19,6 +19,7 @@ OpenERP Server
deployment-gunicorn
deployment-mod-wsgi
form-view-guidelines
+ ir_actions
OpenERP Command
'''''''''''''''
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index 7bad1e7f0dd..e05ba533fa3 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -19,6 +19,7 @@
#
##############################################################################
+from functools import partial
import logging
import operator
import os
@@ -448,6 +449,27 @@ class server_object_lines(osv.osv):
# Actions that are run on the server side
#
class actions_server(osv.osv):
+ """ Server actions model. Server action work on a base model and offer various
+ type of actions that can be executed automatically, for example using base
+ action rules, of manually, by adding the action in the 'More' contextual
+ menu.
+
+ Since OpenERP 8.0 a button 'Create Menu Action' button is available on the
+ action form view. It creates an entry in the More menu of the base model.
+ This allows to create server actions and run them in mass mode easily through
+ the interface.
+
+ The available actions are :
+
+ - 'Execute Python Code': a block of python code that will be executed
+ - 'Trigger a Workflow Signal': send a signal to a workflow
+ - 'Run a Client Action': choose a client action to launch
+ - 'Create or Copy a new Record': create a new record with new values, or
+ copy an existing record in your database
+ - 'Write on a Record': update the values of a record
+ - 'Execute several actions': define an action that triggers several other
+ server actions
+ """
_name = 'ir.actions.server'
_table = 'ir_act_server'
_inherit = 'ir.actions.actions'
@@ -487,9 +509,9 @@ class actions_server(osv.osv):
"- 'Execute Python Code': a block of python code that will be executed\n"
"- 'Trigger a Workflow Signal': send a signal to a workflow\n"
"- 'Run a Client Action': choose a client action to launch\n"
- "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
+ "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n"
"- 'Write on a Record': update the values of a record\n"
- "- 'Execute several actions': define an action that triggers several other sever actions\n"
+ "- 'Execute several actions': define an action that triggers several other server actions\n"
"- 'Send Email': automatically send an email (available in email_template)"),
'usage': fields.char('Action Usage', size=32),
'type': fields.char('Action Type', size=32, required=True),
@@ -628,6 +650,9 @@ class actions_server(osv.osv):
(_check_write_expression,
'Incorrect Write Record Expression',
['write_expression']),
+ (partial(osv.Model._check_m2m_recursion, field_name='child_ids'),
+ 'Recursion found in child server actions',
+ ['child_ids']),
]
def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
@@ -895,21 +920,23 @@ class actions_server(osv.osv):
self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
def run(self, cr, uid, ids, context=None):
- """ Run the server action, by check the condition and then calling
- run_action_, i.e. run_action_code, allowing easy inheritance
- of the server actions.
+ """ Run the server action. For each server action, the condition is
+ checked. Note that A void (aka False) condition is considered as always
+ valid. If it is verified, the run_action_ method is called. This
+ allows easy inheritance of the server actions.
- A void (aka False) condition is considered as always valid.
+ :param dict context: context should contain following keys
- Note coming from previous implementation: FIXME: refactor all the eval()
- calls in run()!
+ - active_id: id of the current object (single mode)
+ - active_model: current model that should equal the action's model
- :param dict context: context should contain following keys:
- - active_id: current id of the object
- - active_model: current model that should equal the action's model
- - TDE: ?? ids: original ids
+ The following keys are optional:
- :return: False: finished correctly or action_id: action to lanch
+ - active_ids: ids of the current records (mass mode). If active_ids
+ and active_id are present, active_ids is given precedence.
+
+ :return: an action_id to be executed, or False is finished correctly without
+ return action
"""
if context is None:
context = {}
diff --git a/openerp/addons/base/tests/test_ir_actions.py b/openerp/addons/base/tests/test_ir_actions.py
index c0e1de13845..04f67617ad6 100644
--- a/openerp/addons/base/tests/test_ir_actions.py
+++ b/openerp/addons/base/tests/test_ir_actions.py
@@ -1,6 +1,8 @@
import unittest2
+from openerp.osv.orm import except_orm
import openerp.tests.common as common
+from openerp.tools import mute_logger
class TestServerActionsBase(common.TransactionCase):
@@ -354,6 +356,7 @@ self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context
cids = self.res_country.search(cr, uid, [('name', 'ilike', 'NewCountry')])
self.assertEqual(len(cids), 1, 'ir_actions_server: TODO')
+ @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm')
def test_60_multi(self):
cr, uid = self.cr, self.uid
@@ -385,6 +388,11 @@ self.pool["res.partner"].create(cr, uid, {"name": partner_name}, context=context
# Test: action returned
self.assertEqual(res.get('type'), 'ir.actions.act_window', '')
+ # Test loops
+ self.assertRaises(except_orm, self.ir_actions_server.write, cr, uid, [self.act_id], {
+ 'child_ids': [(6, 0, [self.act_id])]
+ })
+
if __name__ == '__main__':
unittest2.main()
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index eefe013cc42..163abcb006d 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -5114,6 +5114,40 @@ class BaseModel(object):
return False
return True
+ def _check_m2m_recursion(self, cr, uid, ids, field_name):
+ """
+ Verifies that there is no loop in a hierarchical structure of records,
+ by following the parent relationship using the **parent** field until a loop
+ is detected or until a top-level record is found.
+
+ :param cr: database cursor
+ :param uid: current user id
+ :param ids: list of ids of records to check
+ :param field_name: field to check
+ :return: **True** if the operation can proceed safely, or **False** if an infinite loop is detected.
+ """
+
+ field = self._all_columns.get(field_name)
+ field = field.column if field else None
+ if not field or field._type != 'many2many' or field._obj != self._name:
+ # field must be a many2many on itself
+ raise ValueError('invalid field_name: %r' % (field_name,))
+
+ query = 'SELECT distinct "%s" FROM "%s" WHERE "%s" IN %%s' % (field._id2, field._rel, field._id1)
+ ids_parent = ids[:]
+ while ids_parent:
+ ids_parent2 = []
+ for i in range(0, len(ids_parent), cr.IN_MAX):
+ j = i + cr.IN_MAX
+ sub_ids_parent = ids_parent[i:j]
+ cr.execute(query, (tuple(sub_ids_parent),))
+ ids_parent2.extend(filter(None, map(lambda x: x[0], cr.fetchall())))
+ ids_parent = ids_parent2
+ for i in ids_parent:
+ if i in ids:
+ return False
+ return True
+
def _get_external_ids(self, cr, uid, ids, *args, **kwargs):
"""Retrieve the External ID(s) of any database record.
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 7a54d6b9b6e..4f2d8177c0f 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -272,18 +272,6 @@ def reverse_enumerate(l):
"""
return izip(xrange(len(l)-1, -1, -1), reversed(l))
-#----------------------------------------------------------
-# SMS
-#----------------------------------------------------------
-# text must be latin-1 encoded
-def sms_send(user, password, api_id, text, to):
- import urllib
- url = "http://api.urlsms.com/SendSMS.aspx"
- #url = "http://196.7.150.220/http/sendmsg"
- params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
- urllib.urlopen(url+"?"+params)
- # FIXME: Use the logger if there is an error
- return True
class UpdateableStr(local):
""" Class that stores an updateable string (used in wizards)
From 8c7e11aca371ef99fd9e5cad59d0fb119d160997 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 14:22:57 +0200
Subject: [PATCH 23/79] [IMP] server action: improved form view to be more
clear
bzr revid: tde@openerp.com-20130725122257-im3172kw7k1ksgu7
---
openerp/addons/base/ir/ir_actions.py | 30 +++++++++----
openerp/addons/base/ir/ir_actions.xml | 61 ++++++++++++++++-----------
2 files changed, 58 insertions(+), 33 deletions(-)
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index e05ba533fa3..e561a2c9b50 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -502,7 +502,7 @@ class actions_server(osv.osv):
help="Condition verified before executing the server action. If it "
"is not verified, the action will not be executed. The condition is "
"a Python expression, like 'object.list_price > 5000'. A void "
- "condition is considered as always True. Help about pyhon expression "
+ "condition is considered as always True. Help about python expression "
"is given in the help tab."),
'state': fields.selection(_get_states_wrapper, 'Action To Do', required=True,
help="Type of server action. The following values are available:\n"
@@ -562,10 +562,10 @@ class actions_server(osv.osv):
store=True, readonly=True),
'ref_object': fields.reference('Reference record', selection=_select_objects, size=128,
oldname='copy_object'),
- 'link_new_record': fields.boolean('Link to current record',
+ 'link_new_record': fields.boolean('Attach the new record',
help="Check this if you want to link the newly-created record "
"to the current record on which the server action runs."),
- 'link_field_id': fields.many2one('ir.model.fields', 'Link Field',
+ 'link_field_id': fields.many2one('ir.model.fields', 'Link using field',
oldname='record_id',
help="Provide the field where the record id is stored after the operations."),
'use_write': fields.selection([('current', 'Update the current record'),
@@ -593,6 +593,9 @@ class actions_server(osv.osv):
"this field lets you select the target field within the "
"destination document model (sub-model)."),
'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
+ # Fake fields used to implement the ID finding assistant
+ 'id_object': fields.reference('Record', selection=_select_objects, size=128),
+ 'id_value': fields.char('Record ID'),
}
_defaults = {
@@ -632,9 +635,12 @@ class actions_server(osv.osv):
if not column_info:
return (False, None, 'Part of the expression (%s) is not recognized as a column in the model %s.' % (step, current_model_name))
column_type = column_info.column._type
- if column_type not in ['many2one']:
- return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one)' % (step, column_type))
- current_model_name = column_info.column._obj
+ if column_type not in ['many2one', 'int']:
+ return (False, None, 'Part of the expression (%s) is not a valid column type (is %s, should be a many2one or an int)' % (step, column_type))
+ if column_type == 'int' and path:
+ return (False, None, 'Part of the expression (%s) is an integer field that is only allowed at the end of an expression' % (step))
+ if column_type == 'many2one':
+ current_model_name = column_info.column._obj
return (True, current_model_name, None)
def _check_write_expression(self, cr, uid, ids, context=None):
@@ -759,7 +765,7 @@ class actions_server(osv.osv):
values = {'link_field_id': False, 'crud_model_name': crud_model_name}
return {'value': values}
- def build_expression(self, field_name, sub_field_name):
+ def _build_expression(self, field_name, sub_field_name):
"""Returns a placeholder expression for use in a template field,
based on the values provided in the placeholder assistant.
@@ -791,15 +797,21 @@ class actions_server(osv.osv):
if res_ids:
result.update({
'sub_object': res_ids[0],
- 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
+ 'copyvalue': self._build_expression(field_value.name, sub_field_value and sub_field_value.name or False),
'sub_model_object_field': sub_model_object_field or False,
})
else:
result.update({
- 'copyvalue': self.build_expression(field_value.name, False),
+ 'copyvalue': self._build_expression(field_value.name, False),
})
return {'value': result}
+ def onchange_id_object(self, cr, uid, ids, id_object, context=None):
+ if id_object:
+ ref_model, ref_id = id_object.split(',')
+ return {'value': {'id_value': ref_id}}
+ return {'value': {'id_value': False}}
+
def create_action(self, cr, uid, ids, context=None):
""" Create a contextual action for each of the server actions. """
for action in self.browse(cr, uid, ids, context=context):
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 15edd14523c..db5beff5e0f 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -316,10 +316,10 @@
+
+
From 0810ff005cd4937f33d50d8feb4b5a90547fa5ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 15:50:01 +0200
Subject: [PATCH 24/79] [IMP] Better implementation of header hook check -
list_view: propagate a deselected flag when (un)checking a row - list_view:
define a get_active_domain method that returns an active domain when having
header hook selected
bzr revid: tde@openerp.com-20130725135001-zt8x1leexf7ion4z
---
addons/web/static/src/js/view_list.js | 43 +++++++++++++++++++++++----
addons/web/static/src/js/views.js | 19 +++++-------
2 files changed, 44 insertions(+), 18 deletions(-)
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index 4f58b002328..bc2d0c0b0bf 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -132,8 +132,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.groups = groups;
$(this.groups).bind({
- 'selected': function (e, ids, records) {
- self.do_select(ids, records);
+ 'selected': function (e, ids, records, deselected) {
+ self.do_select(ids, records, deselected);
},
'deleted': function (e, ids) {
self.do_delete(ids);
@@ -603,7 +603,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
* @param {Array} ids selected record ids
* @param {Array} records selected record values
*/
- do_select: function (ids, records) {
+ do_select: function (ids, records, deselected) {
+ // uncheck header hook if at least one row has been deselected
+ if (deselected) {
+ this.$('.oe_list_record_selector').prop('checked', false);
+ }
+
if (!ids.length) {
this.dataset.index = 0;
if (this.sidebar) {
@@ -790,6 +795,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
var ids = this.groups.get_selection().ids;
return ids;
},
+ /**
+ * Calculate the active domain of the list view. This should be done only
+ * if the header checkbox has been checked.
+ */
+ get_active_domain: function () {
+ if (this.$('.oe_list_record_selector').prop('checked')) {
+ var search_view = this.getParent().searchview;
+ var search_data = search_view.build_search_data();
+ var active_domain_done = instance.web.pyeval.eval_domains_and_contexts({
+ domains: search_data.domains,
+ contexts: search_data.contexts,
+ group_by_seq: search_data.groupbys || []
+ }).done(function (results) {
+ return results.domain;
+ });
+ return active_domain_done;
+ }
+ else {
+ return $.Deferred().resolve();
+ }
+ },
/**
* Adds padding columns at the start or end of all table rows (including
* field names row)
@@ -951,8 +977,13 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
.delegate('th.oe_list_record_selector', 'click', function (e) {
e.stopPropagation();
var selection = self.get_selection();
+ var checked = $(e.currentTarget).find('input').prop('checked');
+ // console.log(checked);
+ // if (! checked) {
+ // $(self).find('.oe_list_record_selector').prop('checked', false);
+ // }
$(self).trigger(
- 'selected', [selection.ids, selection.records]);
+ 'selected', [selection.ids, selection.records, ! checked]);
})
.delegate('td.oe_list_record_delete button', 'click', function (e) {
e.stopPropagation();
@@ -1398,10 +1429,10 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
bind_child_events: function (child) {
var $this = $(this),
self = this;
- $(child).bind('selected', function (e) {
+ $(child).bind('selected', function (e, _0, _1, deselected) {
// can have selections spanning multiple links
var selection = self.get_selection();
- $this.trigger(e, [selection.ids, selection.records]);
+ $this.trigger(e, [selection.ids, selection.records, deselected]);
}).bind(this.passthrough_events, function (e) {
// additional positional parameters are provided to trigger as an
// Array, following the event type or event object, but are
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index 67490a4652d..e043715f67b 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1176,6 +1176,7 @@ instance.web.Sidebar = instance.web.Widget.extend({
var self = this;
self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
var ids = self.getParent().get_selected_ids();
+ var domain = self.getParent().get_active_domain();
if (ids.length == 0) {
instance.web.dialog($("").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true });
return false;
@@ -1184,19 +1185,13 @@ instance.web.Sidebar = instance.web.Widget.extend({
active_id: ids[0],
active_ids: ids,
active_model: self.getParent().dataset.model,
- };
+ };
- var search_view = self.getParent().ViewManager.searchview;
- var search_data = search_view.build_search_data();
- var active_domain_done = instance.web.pyeval.eval_domains_and_contexts({
- domains: search_data.domains,
- contexts: search_data.contexts,
- group_by_seq: search_data.groupbys || []
- }).done(function (results) {
- active_ids_context['active_domain'] = results.domain;
- });
-
- $.when(active_domain_done).done(function () {
+ $.when(domain).done(function (results) {
+ if (results !== undefined) {
+ console.log(results);
+ active_ids_context.active_domain = results.domain;
+ }
var c = instance.web.pyeval.eval('context',
new instance.web.CompoundContext(
sidebar_eval_context, active_ids_context));
From 91ef7de7090e92314abebe377f7eacd01b483000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 15:50:29 +0200
Subject: [PATCH 25/79] [IMP] mail: better implementation of active domain /
use active domain
bzr revid: tde@openerp.com-20130725135029-mx946vaibvz1rzii
---
addons/mail/wizard/mail_compose_message.py | 6 +++++-
addons/mail/wizard/mail_compose_message_view.xml | 5 ++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 95647e95d8e..432840d50ef 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -73,7 +73,11 @@ class mail_compose_message(osv.TransientModel):
res_id = context.get('default_res_id', context.get('active_id'))
message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
active_ids = context.get('active_ids')
- result['active_domain'] = '%s' % context.get('active_domain', [])
+ if context.get('active_domain'):
+ result['use_active_domain'] = True
+ result['active_domain'] = '%s' % context.get('active_domain')
+ else:
+ result['active_domain'] = ''
# get default values according to the composition mode
if composition_mode == 'reply':
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 20b529c0faa..c4236adb9cf 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -13,7 +13,9 @@
+
+
@@ -26,9 +28,6 @@
-
-
Date: Thu, 25 Jul 2013 15:55:35 +0200
Subject: [PATCH 26/79] [REM] Removed console.log statemetn
bzr revid: tde@openerp.com-20130725135535-qjucav1ss11a2sgd
---
addons/web/static/src/js/views.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index 19f75a80d1d..0b4022bf91a 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1188,7 +1188,6 @@ instance.web.Sidebar = instance.web.Widget.extend({
$.when(domain).done(function (results) {
if (results !== undefined) {
- console.log(results);
active_ids_context.active_domain = results.domain;
}
var c = instance.web.pyeval.eval('context',
From 68ff38763ab40845e15b4a1305d8dc592b7b9b8b Mon Sep 17 00:00:00 2001
From: Vo Minh Thu
Date: Thu, 25 Jul 2013 16:30:56 +0200
Subject: [PATCH 27/79] [ADD] workflows: some split_mode tests.
I am not sure if the Workflow K is determinist though
so it could maybe end up in activity c instead of b
(and thus fail the tests).
bzr revid: vmt@openerp.com-20130725143056-p6oic1m9jjke9j8r
---
openerp/tests/addons/test_workflow/data.xml | 216 ++++++++++++++++++
openerp/tests/addons/test_workflow/models.py | 2 +-
.../test_workflow/tests/test_workflow.py | 45 ++++
3 files changed, 262 insertions(+), 1 deletion(-)
diff --git a/openerp/tests/addons/test_workflow/data.xml b/openerp/tests/addons/test_workflow/data.xml
index c6f542879a0..b9f1e367000 100644
--- a/openerp/tests/addons/test_workflow/data.xml
+++ b/openerp/tests/addons/test_workflow/data.xml
@@ -229,5 +229,221 @@
a-b
+
+
+
+ test.workflow.g
+ test.workflow.model.g
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.h
+ test.workflow.model.h
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ OR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 2})
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test.workflow.i
+ test.workflow.model.i
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ OR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.j
+ test.workflow.model.j
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ AND
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+
+
+
+
+
+
+
+
+
+ False
+
+
+
+
+ test.workflow.k
+ test.workflow.model.k
+ True
+
+
+
+
+ True
+ a
+ function
+ write({'value': 1})
+ XOR
+
+
+
+ True
+ b
+ function
+ write({'value': 2})
+
+
+
+ True
+ c
+ function
+ write({'value': 3})
+
+
+
+
+
+
+
+
+
+
diff --git a/openerp/tests/addons/test_workflow/models.py b/openerp/tests/addons/test_workflow/models.py
index 84789cc9b69..8214d6313ff 100644
--- a/openerp/tests/addons/test_workflow/models.py
+++ b/openerp/tests/addons/test_workflow/models.py
@@ -55,7 +55,7 @@ class e(openerp.osv.orm.Model):
_name = 'test.workflow.model.e'
_inherit = 'test.workflow.model.a'
-for name in 'bcdef':
+for name in 'bcdefghijk':
type(
name,
(openerp.osv.orm.Model,),
diff --git a/openerp/tests/addons/test_workflow/tests/test_workflow.py b/openerp/tests/addons/test_workflow/tests/test_workflow.py
index d55657f6a6a..4d9cee98b35 100644
--- a/openerp/tests/addons/test_workflow/tests/test_workflow.py
+++ b/openerp/tests/addons/test_workflow/tests/test_workflow.py
@@ -125,3 +125,48 @@ class test_workflows(common.TransactionCase):
self.check_value(model._name, i, 2)
model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_g(self):
+ model = self.registry('test.workflow.model.g')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_h(self):
+ model = self.registry('test.workflow.model.h')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b', 'c'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_i(self):
+ model = self.registry('test.workflow.model.i')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_j(self):
+ model = self.registry('test.workflow.model.j')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['a'])
+ self.check_value(model._name, i, 1)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
+
+ def test_workflow_k(self):
+ model = self.registry('test.workflow.model.k')
+
+ i = model.create(self.cr, SUPERUSER_ID, {})
+ self.check_activities(model._name, i, ['b'])
+ self.check_value(model._name, i, 2)
+
+ model.unlink(self.cr, SUPERUSER_ID, [i])
From 0979b3b6fc24f9cbc6a88aff58f93137fb5c1807 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 17:02:43 +0200
Subject: [PATCH 28/79] [IMP] view_list: better active_domain computation
bzr revid: tde@openerp.com-20130725150243-or624rj2qw6dgxiz
---
addons/web/static/src/js/view_list.js | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index e4059ce9026..975e2a2d248 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -805,14 +805,13 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (this.$('.oe_list_record_selector').prop('checked')) {
var search_view = this.getParent().searchview;
var search_data = search_view.build_search_data();
- var active_domain_done = instance.web.pyeval.eval_domains_and_contexts({
+ return instance.web.pyeval.eval_domains_and_contexts({
domains: search_data.domains,
contexts: search_data.contexts,
group_by_seq: search_data.groupbys || []
- }).done(function (results) {
+ }).then(function (results) {
return results.domain;
});
- return active_domain_done;
}
else {
return $.Deferred().resolve();
@@ -980,10 +979,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
e.stopPropagation();
var selection = self.get_selection();
var checked = $(e.currentTarget).find('input').prop('checked');
- // console.log(checked);
- // if (! checked) {
- // $(self).find('.oe_list_record_selector').prop('checked', false);
- // }
$(self).trigger(
'selected', [selection.ids, selection.records, ! checked]);
})
From 20c11d67031d5d69a4ca3575f26dce41064c12ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 17:28:46 +0200
Subject: [PATCH 29/79] [FIX] Fixed last commits
bzr revid: tde@openerp.com-20130725152846-zlfd5gluz15tjg2t
---
addons/web/static/src/js/views.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index 0b4022bf91a..5cb8ee44e53 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1186,9 +1186,9 @@ instance.web.Sidebar = instance.web.Widget.extend({
active_model: self.getParent().dataset.model,
};
- $.when(domain).done(function (results) {
- if (results !== undefined) {
- active_ids_context.active_domain = results.domain;
+ $.when(domain).done(function (domain) {
+ if (domain !== undefined) {
+ active_ids_context.active_domain = domain;
}
var c = instance.web.pyeval.eval('context',
new instance.web.CompoundContext(
From 27735a2b850571798585975610ef03b9c72d6353 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 17:31:14 +0200
Subject: [PATCH 30/79] [IMP] export: work on selected_ids or all_ids if header
is selected; ugly hack to solve issue when exporting
bzr revid: tde@openerp.com-20130725153114-6rjhktvuz1k1qrnb
---
addons/web/controllers/main.py | 3 ++-
addons/web/static/src/js/data_export.js | 35 +++++++++++++++----------
addons/web/static/src/xml/base.xml | 3 ---
3 files changed, 23 insertions(+), 18 deletions(-)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index da3cc8b8d2a..33c3f337ecb 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1407,7 +1407,8 @@ class Export(http.Controller):
:returns: for each export format, a pair of identifier and printable name
:rtype: [(str, str)]
"""
- return ["CSV", "Excel"]
+ # TDE regression spotted since frameworek update: was impossible to export, hack to make it work temporarily
+ return [{'label': 'csv', 'tag': 'csv'}]
def fields_get(self, model):
Model = request.session.model(model)
diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js
index 0540a0acbe8..d0ac4718c17 100644
--- a/addons/web/static/src/js/data_export.js
+++ b/addons/web/static/src/js/data_export.js
@@ -378,21 +378,28 @@ instance.web.DataExport = instance.web.Dialog.extend({
exported_fields.unshift({name: 'id', label: 'External ID'});
var export_format = this.$el.find("#export_format").val();
- var ids_to_export = this.$('#export_selection_only').prop('checked')
- ? this.getParent().get_selected_ids()
- : this.dataset.ids;
- instance.web.blockUI();
- this.session.get_file({
- url: '/web/export/' + export_format,
- data: {data: JSON.stringify({
- model: this.dataset.model,
- fields: exported_fields,
- ids: ids_to_export,
- domain: this.dataset.domain,
- import_compat: !!this.$el.find("#import_compat").val(),
- })},
- complete: instance.web.unblockUI,
+ var ids_computation = this.getParent().get_active_domain().then(function (results) {
+ if (results === undefined) {
+ return self.getParent().get_selected_ids();
+ }
+ else {
+ return self.dataset.call('search', [results]);
+ }
+ });
+ $.when(ids_computation).done(function (ids_to_export) {
+ instance.web.blockUI();
+ self.session.get_file({
+ url: '/web/export/' + export_format,
+ data: {data: JSON.stringify({
+ model: self.dataset.model,
+ fields: exported_fields,
+ ids: ids_to_export,
+ domain: self.dataset.domain,
+ import_compat: !!self.$el.find("#import_compat").val(),
+ })},
+ complete: instance.web.unblockUI,
+ });
});
},
close: function() {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index fb622b056a1..27b85d49e5a 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1744,9 +1744,6 @@
-
-
-
From 47d72f7a0fe80d2ba070c12ff074e9e959524929 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Thu, 25 Jul 2013 17:57:40 +0200
Subject: [PATCH 31/79] [IMP] export: delegated ids computation to the python
side, the js gives ids or the domain to use; + fixed statically export
formats
bzr revid: tde@openerp.com-20130725155740-g9a76z08ogjejp86
---
addons/web/controllers/main.py | 6 ++++--
addons/web/static/src/js/data_export.js | 15 ++++++++-------
2 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 33c3f337ecb..966c9e7641a 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1407,8 +1407,10 @@ class Export(http.Controller):
:returns: for each export format, a pair of identifier and printable name
:rtype: [(str, str)]
"""
- # TDE regression spotted since frameworek update: was impossible to export, hack to make it work temporarily
- return [{'label': 'csv', 'tag': 'csv'}]
+ # TDE note: was dynamic, broken when improving the framework, temporarily static to make export working
+ return [{'tag': 'csv', 'label': 'CSV'},
+ {'tag': 'xls', 'label': 'Excel', 'error': None if xlwt else "XLWT required"}]
+
def fields_get(self, model):
Model = request.session.model(model)
diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js
index d0ac4718c17..e58cc551ed1 100644
--- a/addons/web/static/src/js/data_export.js
+++ b/addons/web/static/src/js/data_export.js
@@ -379,15 +379,16 @@ instance.web.DataExport = instance.web.Dialog.extend({
var export_format = this.$el.find("#export_format").val();
- var ids_computation = this.getParent().get_active_domain().then(function (results) {
- if (results === undefined) {
- return self.getParent().get_selected_ids();
+ this.getParent().get_active_domain().then(function (domain) {
+ if (domain === undefined) {
+ var ids_to_export = self.getParent().get_selected_ids();
+ var domain = self.dataset.domain;
}
else {
- return self.dataset.call('search', [results]);
+ var ids_to_export = false;
+ var domain = domain;
}
- });
- $.when(ids_computation).done(function (ids_to_export) {
+ var ids_t_
instance.web.blockUI();
self.session.get_file({
url: '/web/export/' + export_format,
@@ -395,7 +396,7 @@ instance.web.DataExport = instance.web.Dialog.extend({
model: self.dataset.model,
fields: exported_fields,
ids: ids_to_export,
- domain: self.dataset.domain,
+ domain: domain,
import_compat: !!self.$el.find("#import_compat").val(),
})},
complete: instance.web.unblockUI,
From 1dff0b7c4fe5b60a15012c6b69fe022e7f79432b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 10:18:25 +0200
Subject: [PATCH 32/79] [DOC] email_template: added changelod
bzr revid: tde@openerp.com-20130726081825-xmq8agjvph3trb7t
---
addons/email_template/doc/changelog.rst | 11 +++++++++++
addons/email_template/doc/index.rst | 13 +++++++++++++
addons/email_template/ir_actions.py | 5 ++---
3 files changed, 26 insertions(+), 3 deletions(-)
create mode 100644 addons/email_template/doc/changelog.rst
create mode 100644 addons/email_template/doc/index.rst
diff --git a/addons/email_template/doc/changelog.rst b/addons/email_template/doc/changelog.rst
new file mode 100644
index 00000000000..2581175fe0b
--- /dev/null
+++ b/addons/email_template/doc/changelog.rst
@@ -0,0 +1,11 @@
+.. _changelog:
+
+Changelog
+=========
+
+`trunk (saas-2)`
+----------------
+
+ - Server action update
+
+ - added `email` server action type, now entirely based on email templates.
\ No newline at end of file
diff --git a/addons/email_template/doc/index.rst b/addons/email_template/doc/index.rst
new file mode 100644
index 00000000000..58cf9413601
--- /dev/null
+++ b/addons/email_template/doc/index.rst
@@ -0,0 +1,13 @@
+Email Template module documentation
+===================================
+
+Email Template documentation topics
+'''''''''''''''''''''''''''''''''''
+
+Changelog
+'''''''''
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog.rst
diff --git a/addons/email_template/ir_actions.py b/addons/email_template/ir_actions.py
index ae2ce4f2f82..181cc9a1c5b 100644
--- a/addons/email_template/ir_actions.py
+++ b/addons/email_template/ir_actions.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
-# Copyright (c) 2014 OpenERP S.A.
+# Copyright (c) 2013 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
@@ -49,8 +49,7 @@ class actions_server(osv.Model):
}
def on_change_template_id(self, cr, uid, ids, template_id, context=None):
- """ - mass_mailing: we cannot render, so return the template values
- - normal mode: return rendered values """
+ """ Render the raw template in the server action fields. """
if template_id:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
From d3f0d70efeac1940a44d8902fe8b6fa0302cf230 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 10:41:38 +0200
Subject: [PATCH 33/79] [DOC] server actions: added comments in various methods
bzr revid: tde@openerp.com-20130726084138-vca3lamr0ye3bwuv
---
openerp/addons/base/ir/ir_actions.py | 78 +++++++++++++++++++++-------
1 file changed, 59 insertions(+), 19 deletions(-)
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index e561a2c9b50..999431788f8 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -618,7 +618,14 @@ class actions_server(osv.osv):
}
def _check_expression(self, cr, uid, expression, model_id, context):
- """ Check python expression (condition, write_expression) """
+ """ Check python expression (condition, write_expression). Each step of
+ the path must be a valid many2one field, or an integer field for the last
+ step.
+
+ :param str expression: a python expression, beginning by 'obj' or 'object'
+ :param int model_id: the base model of the server action
+ :returns tuple: (is_valid, target_model_name, error_msg)
+ """
if not model_id:
return (False, None, 'Your expression cannot be validated because the Base Model is not set.')
# fetch current model
@@ -663,7 +670,7 @@ class actions_server(osv.osv):
def on_change_model_id(self, cr, uid, ids, model_id, wkf_model_id, crud_model_id, context=None):
""" When changing the action base model, reset workflow and crud config
- to ease value coherence """
+ to ease value coherence. """
values = {
'use_create': 'new',
'use_write': 'current',
@@ -675,10 +682,11 @@ class actions_server(osv.osv):
return {'value': values}
def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
- """ Update workflow configuration
- - update the workflow model (for base (model_id) /relational (field.relation))
- - update wkf_transition_id to False if workflow model changes, to force the user
- to choose a new one
+ """ Update workflow type configuration
+
+ - update the workflow model (for base (model_id) /relational (field.relation))
+ - update wkf_transition_id to False if workflow model changes, to force
+ the user to choose a new one
"""
values = {}
if use_relational_model == 'relational' and wkf_field_id:
@@ -698,7 +706,7 @@ class actions_server(osv.osv):
return {'value': values}
def on_change_crud_config(self, cr, uid, ids, state, use_create, use_write, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ Wrapper on CRUD-type (create or write) on_change """
if state == 'object_create':
return self.on_change_create_config(cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=context)
elif state == 'object_write':
@@ -707,7 +715,14 @@ class actions_server(osv.osv):
return {}
def on_change_create_config(self, cr, uid, ids, use_create, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ When changing the object_create type configuration:
+
+ - `new` and `copy_current`: crud_model_id is the same as base model
+ - `new_other`: user choose crud_model_id
+ - `copy_other`: disassemble the reference object to have its model
+ - if the target model has changed, then reset the link field that is
+ probably not correct anymore
+ """
values = {}
if use_create == 'new':
values['crud_model_id'] = model_id
@@ -725,7 +740,12 @@ class actions_server(osv.osv):
return {'value': values}
def on_change_write_config(self, cr, uid, ids, use_write, ref_object, crud_model_id, model_id, context=None):
- """ TODO """
+ """ When changing the object_write type configuration:
+
+ - `current`: crud_model_id is the same as base model
+ - `other`: disassemble the reference object to have its model
+ - `expression`: has its own on_change, nothing special here
+ """
values = {}
if use_write == 'current':
values['crud_model_id'] = model_id
@@ -741,7 +761,7 @@ class actions_server(osv.osv):
return {'value': values}
def on_change_write_expression(self, cr, uid, ids, write_expression, model_id, context=None):
- """ Check the write_expression, about fields and models. """
+ """ Check the write_expression and update crud_model_id accordingly """
values = {}
valid, model_name, message = self._check_expression(cr, uid, write_expression, model_id, context=context)
if valid:
@@ -766,12 +786,12 @@ class actions_server(osv.osv):
return {'value': values}
def _build_expression(self, field_name, sub_field_name):
- """Returns a placeholder expression for use in a template field,
- based on the values provided in the placeholder assistant.
+ """ Returns a placeholder expression for use in a template field,
+ based on the values provided in the placeholder assistant.
- :param field_name: main field name
- :param sub_field_name: sub field name (M2O)
- :return: final placeholder expression
+ :param field_name: main field name
+ :param sub_field_name: sub field name (M2O)
+ :return: final placeholder expression
"""
expression = ''
if field_name:
@@ -849,9 +869,10 @@ class actions_server(osv.osv):
def run_action_trigger(self, cr, uid, action, eval_context=None, context=None):
""" Trigger a workflow signal, depending on the use_relational_model:
- - `base`: base_model_pool.signal_(cr, uid, context.get('active_id'))
- - `relational`: find the related model and object, using the relational
- field, then target_model_pool.signal_(cr, uid, target_id)
+
+ - `base`: base_model_pool.signal_(cr, uid, context.get('active_id'))
+ - `relational`: find the related model and object, using the relational
+ field, then target_model_pool.signal_(cr, uid, target_id)
"""
obj_pool = self.pool[action.model_id.model]
if action.use_relational_model == 'base':
@@ -870,7 +891,6 @@ class actions_server(osv.osv):
workflow.trg_validate(uid, target_pool._name, target_id, trigger_name, cr)
def run_action_multi(self, cr, uid, action, eval_context=None, context=None):
- # TDE FIXME: loops are not considered here ^^
res = []
for act in action.child_ids:
result = self.run(cr, uid, [act.id], context)
@@ -879,6 +899,15 @@ class actions_server(osv.osv):
return res and res[0] or False
def run_action_object_write(self, cr, uid, action, eval_context=None, context=None):
+ """ Write server action.
+
+ - 1. evaluate the value mapping
+ - 2. depending on the write configuration:
+
+ - `current`: id = active_id
+ - `other`: id = from reference object
+ - `expression`: id = from expression evaluation
+ """
res = {}
for exp in action.fields_lines:
if exp.type == 'equation':
@@ -905,6 +934,17 @@ class actions_server(osv.osv):
obj_pool.write(cr, uid, [ref_id], res, context=context)
def run_action_object_create(self, cr, uid, action, eval_context=None, context=None):
+ """ Create and Copy server action.
+
+ - 1. evaluate the value mapping
+ - 2. depending on the write configuration:
+
+ - `new`: new record in the base model
+ - `copy_current`: copy the current record (id = active_id) + gives custom values
+ - `new_other`: new record in target model
+ - `copy_other`: copy the current record (id from reference object)
+ + gives custom values
+ """
res = {}
for exp in action.fields_lines:
if exp.type == 'equation':
From 6e6108e9b69b296bb30d8d36e258027ad7860cbd Mon Sep 17 00:00:00 2001
From: niv-openerp
Date: Fri, 26 Jul 2013 10:46:40 +0200
Subject: [PATCH 34/79] [FIX] Problem with exports that make them crash
lp bug: https://launchpad.net/bugs/1205238 fixed
bzr revid: nicolas.vanhoren@openerp.com-20130726084640-lz11kdlsk27mrjec
---
addons/web/controllers/main.py | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index da3cc8b8d2a..5d176122fcf 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1407,7 +1407,10 @@ class Export(http.Controller):
:returns: for each export format, a pair of identifier and printable name
:rtype: [(str, str)]
"""
- return ["CSV", "Excel"]
+ return [
+ {'tag': 'csv', 'label': 'CSV'},
+ {'tag': 'xls', 'label': 'Excel', 'error': None if xlwt else "XLWT required"},
+ ]
def fields_get(self, model):
Model = request.session.model(model)
@@ -1583,7 +1586,6 @@ class ExportFormat(object):
cookies={'fileToken': token})
class CSVExport(ExportFormat, http.Controller):
- fmt = {'tag': 'csv', 'label': 'CSV'}
@http.route('/web/export/csv', type='http', auth="user")
def index(self, data, token):
@@ -1621,11 +1623,6 @@ class CSVExport(ExportFormat, http.Controller):
return data
class ExcelExport(ExportFormat, http.Controller):
- fmt = {
- 'tag': 'xls',
- 'label': 'Excel',
- 'error': None if xlwt else "XLWT required"
- }
@http.route('/web/export/xls', type='http', auth="user")
def index(self, data, token):
From 26ff871ab8f35b39129747a7ff69917efdcdf615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 10:54:26 +0200
Subject: [PATCH 35/79] [FIX] crm: fixed a typo in onchange method name
bzr revid: tde@openerp.com-20130726085426-gpw86mn4ugno43o1
---
addons/crm/crm_lead_view.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index cb4302166a1..aa5aac76b9d 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -408,7 +408,7 @@
From c4210aeeb0579850cd02d99bebf121112b10d4b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 11:12:12 +0200
Subject: [PATCH 36/79] [FIX] email_template: ir_actions: fixed email server
action form view, a copy and paste introduced an error
bzr revid: tde@openerp.com-20130726091212-kzt029fechedq0yp
---
addons/email_template/ir_actions_view.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/email_template/ir_actions_view.xml b/addons/email_template/ir_actions_view.xml
index f673fda687d..c8a54997089 100644
--- a/addons/email_template/ir_actions_view.xml
+++ b/addons/email_template/ir_actions_view.xml
@@ -35,7 +35,7 @@
Your template does not defined any email_from. Please update your template.
-
+
From a3c546a6008c41a8557e73d1886974bbe1b2add3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 11:13:05 +0200
Subject: [PATCH 37/79] [REM] crm: removed new_email_send, now unused and
deprecated with chatter / templates.
bzr revid: tde@openerp.com-20130726091305-8lsmidcxyau3shw0
---
addons/crm/crm_lead.py | 36 ------------------------------------
1 file changed, 36 deletions(-)
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 46a3f6e199e..c0f003f9792 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -945,42 +945,6 @@ class crm_lead(format_address, osv.osv):
vals.update(onchange_stage_values)
return super(crm_lead, self).write(cr, uid, ids, vals, context=context)
- def new_mail_send(self, cr, uid, ids, context=None):
- '''
- This function opens a window to compose an email, with the edi sale template message loaded by default
- '''
- assert len(ids) == 1, 'This option should only be used for a single id at a time.'
- ir_model_data = self.pool.get('ir.model.data')
- try:
- template_id = ir_model_data.get_object_reference(cr, uid, 'crm', 'email_template_opportunity_mail')[1]
- except ValueError:
- template_id = False
- try:
- compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
- except ValueError:
- compose_form_id = False
- if context is None:
- context = {}
- ctx = context.copy()
- ctx.update({
- 'default_model': 'crm.lead',
- 'default_res_id': ids[0],
- 'default_use_template': bool(template_id),
- 'default_template_id': template_id,
- 'default_composition_mode': 'comment',
- })
- return {
- 'name': _('Compose Email'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'mail.compose.message',
- 'views': [(compose_form_id, 'form')],
- 'view_id': compose_form_id,
- 'target': 'new',
- 'context': ctx,
- }
-
# ----------------------------------------
# Mail Gateway
# ----------------------------------------
From d758de88f06fe58b64bcf262757d32e7c4b70fcc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 11:33:35 +0200
Subject: [PATCH 38/79] [IMP] email_template: server action: better management
of template fields, saved but not editable
bzr revid: tde@openerp.com-20130726093335-vxniwn121djpaw32
---
addons/email_template/ir_actions.py | 24 ++++++++++++++++++-----
addons/email_template/ir_actions_view.xml | 10 +++++-----
2 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/addons/email_template/ir_actions.py b/addons/email_template/ir_actions.py
index 181cc9a1c5b..bd860785da4 100644
--- a/addons/email_template/ir_actions.py
+++ b/addons/email_template/ir_actions.py
@@ -33,16 +33,16 @@ class actions_server(osv.Model):
return res
_columns = {
- 'email_from': fields.char('From', readonly=True,
+ 'email_from': fields.char('From',
help="Sender address; define the template to see its value. If not set, the default "
"value will be the author's email alias if configured, or email address."),
- 'email_to': fields.char('To (Emails)', readonly=True,
+ 'email_to': fields.char('To (Emails)',
help="Comma-separated recipient addresses; define the template to see its value"),
- 'partner_to': fields.char('To (Partners)', readonly=True,
+ 'partner_to': fields.char('To (Partners)',
help="Comma-separated ids of recipient partners; define the template to see its value"),
- 'subject': fields.char('Subject', readonly=True,
+ 'subject': fields.char('Subject',
help="Email subject; define the template to see its value"),
- 'body_html': fields.text('Body', readonly=True,
+ 'body_html': fields.text('Body',
help="Rich-text/HTML version of the message; define the template to see its value"),
'template_id': fields.many2one('email.template', 'Email Template', ondelete='set null',
help="Define the email template to use for the email to send.")
@@ -61,6 +61,20 @@ class actions_server(osv.Model):
return {'value': values}
+ def create(self, cr, uid, values, context=None):
+ if values.get('template_id'):
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
+ template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
+ values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
+ return super(actions_server, self).create(cr, uid, values, context=context)
+
+ def write(self, cr, uid, ids, values, context=None):
+ if values.get('template_id'):
+ fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids']
+ template_values = self.pool.get('email.template').read(cr, uid, values.get('template_id'), fields, context)
+ values.update(dict((field, template_values[field]) for field in fields if template_values.get(field)))
+ return super(actions_server, self).write(cr, uid, ids, values, context=context)
+
def run_action_email(self, cr, uid, action, eval_context=None, context=None):
if not action.template_id or not context.get('active_id'):
return False
diff --git a/addons/email_template/ir_actions_view.xml b/addons/email_template/ir_actions_view.xml
index c8a54997089..fae9c364312 100644
--- a/addons/email_template/ir_actions_view.xml
+++ b/addons/email_template/ir_actions_view.xml
@@ -29,16 +29,16 @@
-
Your template does not defined any email_from. Please update your template.
-
-
-
-
+
+
+
+
From ac26676968d006723ddf1190c77df4ed5268bc8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 11:33:47 +0200
Subject: [PATCH 39/79] [IMP] crm: reminder to user: uses a different and more
adequate template
bzr revid: tde@openerp.com-20130726093347-jhu39cmvg2b947gj
---
addons/crm/crm_action_rule_demo.xml | 2 +-
addons/crm/crm_lead_data.xml | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/addons/crm/crm_action_rule_demo.xml b/addons/crm/crm_action_rule_demo.xml
index 5dca69a40d4..569b2d676e0 100644
--- a/addons/crm/crm_action_rule_demo.xml
+++ b/addons/crm/crm_action_rule_demo.xml
@@ -14,7 +14,7 @@
Trueir.actions.serveremail
-
+ Set Auto Reminder on leads which are not open since 5 days.
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index 92914542d9d..091a4d217de 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -220,6 +220,15 @@
${not object.partner_id and object.email_from}
+
+ Reminder to User
+
+
+ admin@example.com
+ [${object.user_id and object.user_id.email}]
+ Reminder
+ This opportunity did not have any activity since at least 5 days.
+
From b4d2fc5512352f57f9d684caa7f068994b3c6877 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?=
Date: Fri, 26 Jul 2013 11:56:38 +0200
Subject: [PATCH 40/79] [IMP] crm: improved reminder ot user template
bzr revid: tde@openerp.com-20130726095638-0pdkph3xl9cl8v3j
---
addons/crm/crm_lead_data.xml | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index 091a4d217de..219ff2cdeee 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -225,9 +225,14 @@
admin@example.com
- [${object.user_id and object.user_id.email}]
- Reminder
- This opportunity did not have any activity since at least 5 days.
+ ${object.user_id != False and object.user_id.email}
+ Reminder on Lead: ${object.id} from ${object.partner_id != False and object.partner_id.name or object.contact_name}
+ This opportunity did not have any activity since at least 5 days. Here are some details: