Add ir.trigger to trigger ModelStorage change
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add ir.trigger to trigger ModelStorage change
|
||||
* Add default value to Boolean fields at database level
|
||||
|
||||
Version 1.6.0 - 2010-05-08
|
||||
|
||||
@@ -203,6 +203,11 @@ Instance methods:
|
||||
Create a record. ``values`` is a dictionary with fields names as key and
|
||||
created values as value.
|
||||
|
||||
.. method:: ModelStorage.trigger_create(cursor, user, id[, context])
|
||||
|
||||
Trigger create actions. It will call actions defined in ``ir.trigger`` if
|
||||
``on_create`` is set and ``condition`` is true.
|
||||
|
||||
.. method:: ModelStorage.read(cursor, user, ids[, fields_names[, context]])
|
||||
|
||||
Return values for the ids. If ``fields_names`` is set, there will be only
|
||||
@@ -217,10 +222,26 @@ Instance methods:
|
||||
``values`` is a dictionary with fields names as key and writen values as
|
||||
value.
|
||||
|
||||
.. method:: ModelStorage.trigger_write_get_eligibles(cursor, user, ids[, context])
|
||||
|
||||
Return eligible record ids for write actions by triggers. This dictionary
|
||||
is to pass to :method:`~ModelStorage.trigger_write`.
|
||||
|
||||
.. method:: ModelStorage.trigger_write(cursor, user, eligibles[, context])
|
||||
|
||||
Trigger write actions. It will call actions defined in ``ir.trigger`` if
|
||||
``on_write`` is set and ``condition`` was false before
|
||||
:method:`~ModelStorage.write` and true after.
|
||||
|
||||
.. method:: ModelStorage.delete(cursor, user, ids[, context])
|
||||
|
||||
Delete records. ``ids`` can be a list of ids or an id.
|
||||
|
||||
.. method:: ModelStorage.trigger_delete(cursor, user, ids[, context])
|
||||
|
||||
Trigger delete actions. It will call actions defined in ``ir.trigger`` if
|
||||
``on_delete`` is set and ``condition`` is true.
|
||||
|
||||
.. method:: ModelStorage.copy(cursor, user, ids[, default[, context]])
|
||||
|
||||
Duplicate the records. ``ids`` can be a list of ids or an id. ``default``
|
||||
|
||||
@@ -15,3 +15,4 @@ import module
|
||||
from default import *
|
||||
from cache import *
|
||||
from date import *
|
||||
from trigger import *
|
||||
|
||||
@@ -40,6 +40,7 @@ Fournit les concepts et l'administration des modèles, actions, séquences, loca
|
||||
'property.xml',
|
||||
'module/module.xml',
|
||||
'default.xml',
|
||||
'trigger.xml',
|
||||
],
|
||||
'translation': [
|
||||
'fr_FR.csv',
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
import datetime
|
||||
import time
|
||||
from trytond.model import ModelView, ModelSQL, fields
|
||||
from trytond.pyson import Eval, Or
|
||||
from trytond.tools import Cache, safe_eval
|
||||
from trytond.backend import TableHandler
|
||||
from trytond.tools import reduce_ids
|
||||
|
||||
|
||||
class Trigger(ModelSQL, ModelView):
|
||||
"Trigger"
|
||||
_name = 'ir.trigger'
|
||||
_description = __doc__
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
active = fields.Boolean('Active', select=2)
|
||||
model = fields.Many2One('ir.model', 'Model', required=True, select=1)
|
||||
on_time = fields.Boolean('On Time', select=1, states={
|
||||
'invisible': Or(Eval('on_create', False), Eval('on_write', False),
|
||||
Eval('on_delete', False)),
|
||||
}, depends=['on_create', 'on_write', 'on_delete'],
|
||||
on_change=['on_time'])
|
||||
on_create = fields.Boolean('On Create', select=1, states={
|
||||
'invisible': Eval('on_time', False),
|
||||
}, depends=['on_time'],
|
||||
on_change=['on_create'])
|
||||
on_write = fields.Boolean('On Write', select=1, states={
|
||||
'invisible': Eval('on_time', False),
|
||||
}, depends=['on_time'],
|
||||
on_change=['on_write'])
|
||||
on_delete = fields.Boolean('On Delete', select=1, states={
|
||||
'invisible': Eval('on_time', False),
|
||||
}, depends=['on_time'],
|
||||
on_change=['on_delete'])
|
||||
condition = fields.Char('Condition', required=True,
|
||||
help='A Python statement evaluated with record represented by '
|
||||
'"self"\nIt triggers the action if true.')
|
||||
limit_number = fields.Integer('Limit Number', help='Limit the number of '
|
||||
'call to "Action Function" by records.\n'
|
||||
'0 for no limit.')
|
||||
minimum_delay = fields.Float('Minimum Delay', help='Set a minimu delay '
|
||||
'in minutes between call to "Action Function" for the same record.\n'
|
||||
'0 for no delay.')
|
||||
action_model = fields.Many2One('ir.model', 'Action Model', required=True)
|
||||
action_function = fields.Char('Action Function', required=True)
|
||||
|
||||
def __init__(self):
|
||||
super(Trigger, self).__init__()
|
||||
self._sql_constraints += [
|
||||
('on_exclusive',
|
||||
'CHECK(NOT(on_time AND (on_create OR on_write OR on_delete)))',
|
||||
'"On Time" and others are mutually exclusive!'),
|
||||
]
|
||||
self._constraints += [
|
||||
('check_condition', 'invalid_condition'),
|
||||
]
|
||||
self._error_messages.update({
|
||||
'invalid_condition': 'Condition must be a python expression!',
|
||||
})
|
||||
self._order.insert(0, ('name', 'ASC'))
|
||||
|
||||
def check_condition(self, cursor, user, ids):
|
||||
'''
|
||||
Check condition
|
||||
'''
|
||||
for trigger in self.browse(cursor, user, ids):
|
||||
try:
|
||||
compile(trigger.condition, '', 'eval')
|
||||
except (SyntaxError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
def default_active(self, cursor, user, context=None):
|
||||
return True
|
||||
|
||||
def on_change_on_time(self, cursor, user, values, context=None):
|
||||
if values.get('on_time'):
|
||||
return {
|
||||
'on_create': False,
|
||||
'on_write': False,
|
||||
'on_delete': False,
|
||||
}
|
||||
return {}
|
||||
|
||||
def on_change_on_create(self, cursor, user, values, context=None):
|
||||
if values.get('on_create'):
|
||||
return {
|
||||
'on_time': False,
|
||||
}
|
||||
return {}
|
||||
|
||||
def on_change_on_write(self, cursor, user, values, context=None):
|
||||
if values.get('on_write'):
|
||||
return {
|
||||
'on_time': False,
|
||||
}
|
||||
return {}
|
||||
|
||||
def on_change_on_delete(self, cursor, user, values, context=None):
|
||||
if values.get('on_delete'):
|
||||
return {
|
||||
'on_time': False,
|
||||
}
|
||||
return {}
|
||||
|
||||
@Cache('ir_trigger.get_triggers')
|
||||
def get_triggers(self, cursor, user, model_name, mode):
|
||||
"""
|
||||
Return trigger ids for a model and a mode
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param model_name: the name of the model
|
||||
:param mode: the mode that can be 'create', 'write', 'delete' or 'time'
|
||||
:return: a list of ir.trigger ids
|
||||
"""
|
||||
assert mode in ['create', 'write', 'delete', 'time'], \
|
||||
'Invalid trigger mode'
|
||||
|
||||
if user == 0:
|
||||
return [] # XXX is it want we want?
|
||||
|
||||
trigger_ids = self.search(cursor, user, [
|
||||
('model.model', '=', model_name),
|
||||
('on_%s' % mode, '=', True),
|
||||
])
|
||||
return trigger_ids
|
||||
|
||||
def eval(self, cursor, user, trigger, record, context=None):
|
||||
"""
|
||||
Evaluate the condition of trigger
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param trigger: a BrowseRecord of ir.trigger
|
||||
:param record: a BrowseRecord of the tested model
|
||||
:param context: the context
|
||||
:return: a boolean
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
model_obj = self.pool.get(trigger.model.model)
|
||||
env = {}
|
||||
env.update(context)
|
||||
env['current_date'] = datetime.datetime.today()
|
||||
env['time'] = time
|
||||
env['context'] = context
|
||||
env['self'] = record
|
||||
return bool(safe_eval(trigger.condition, env))
|
||||
|
||||
def trigger_action(self, cursor, user, ids, trigger_id, context=None):
|
||||
"""
|
||||
Trigger the action define on trigger_id for the ids
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param ids: the list of record ids triggered
|
||||
:param trigger_id: the trigger id
|
||||
:param context: the context
|
||||
"""
|
||||
trigger_log_obj = self.pool.get('ir.trigger.log')
|
||||
trigger = self.browse(cursor, user, trigger_id, context=context)
|
||||
model_obj = self.pool.get(trigger.action_model.model)
|
||||
|
||||
# Filter on limit_number
|
||||
if trigger.limit_number:
|
||||
new_ids = []
|
||||
for i in range(0, len(ids), cursor.IN_MAX):
|
||||
sub_ids = ids[i:i + cursor.IN_MAX]
|
||||
red_sql, red_ids = reduce_ids('"record_id"', sub_ids)
|
||||
cursor.execute('SELECT "record_id", COUNT(1) FROM "%s" '
|
||||
'WHERE %s AND "trigger" = %%s '
|
||||
'GROUP BY "record_id"'
|
||||
% (trigger_log_obj._table, red_sql),
|
||||
red_ids + [trigger.id])
|
||||
number = dict(cursor.fetchall())
|
||||
for record_id in sub_ids:
|
||||
if record_id not in number:
|
||||
new_ids.append(record_id)
|
||||
continue
|
||||
if number[record_id] < trigger.limit_number:
|
||||
new_ids.append(record_id)
|
||||
ids = new_ids
|
||||
|
||||
# Filter on minimum_delay
|
||||
if trigger.minimum_delay:
|
||||
new_ids = []
|
||||
for i in range(0, len(ids), cursor.IN_MAX):
|
||||
sub_ids = ids[i:i + cursor.IN_MAX]
|
||||
red_sql, red_ids = reduce_ids('"record_id"', sub_ids)
|
||||
cursor.execute('SELECT "record_id", MAX("create_date") '
|
||||
'FROM "%s" '
|
||||
'WHERE %s AND "trigger" = %%s '
|
||||
'GROUP BY "record_id"'
|
||||
% (trigger_log_obj._table, red_sql),
|
||||
red_ids + [trigger.id])
|
||||
delay = dict(cursor.fetchall())
|
||||
for record_id in sub_ids:
|
||||
if record_id not in delay:
|
||||
new_ids.append(record_id)
|
||||
continue
|
||||
# SQLite return string for MAX
|
||||
if isinstance(delay[record_id], basestring):
|
||||
datepart, timepart = delay[record_id].split(" ")
|
||||
year, month, day = map(int, datepart.split("-"))
|
||||
timepart_full = timepart.split(".")
|
||||
hours, minutes, seconds = map(int, timepart_full[0].split(":"))
|
||||
if len(timepart_full) == 2:
|
||||
microseconds = int(timepart_full[1])
|
||||
delay[record_id] = datetime.datetime(year, month, day,
|
||||
hours, minutes, seconds, microseconds)
|
||||
if datetime.datetime.now() - delay[record_id] \
|
||||
>= datetime.timedelta(minutes=trigger.minimum_delay):
|
||||
new_ids.append(record_id)
|
||||
ids = new_ids
|
||||
|
||||
if ids:
|
||||
getattr(model_obj, trigger.action_function)(cursor, user, ids,
|
||||
trigger_id, context=context)
|
||||
if trigger.limit_number or trigger.minimum_delay:
|
||||
for record_id in ids:
|
||||
trigger_log_obj.create(cursor, user, {
|
||||
'trigger': trigger.id,
|
||||
'record_id': record_id,
|
||||
}, context=context)
|
||||
|
||||
def trigger_time(self, cursor, user, context=None):
|
||||
'''
|
||||
Trigger time actions
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param context: the context
|
||||
'''
|
||||
trigger_ids = self.search(cursor, user, [
|
||||
('on_time', '=', True),
|
||||
], context=context)
|
||||
for trigger in self.browse(cursor, user, trigger_ids, context=context):
|
||||
model_obj = self.pool.get(trigger.model.model)
|
||||
triggered_ids = []
|
||||
# TODO add a domain
|
||||
record_ids = model_obj.search(cursor, user, [], context=context)
|
||||
for record in model_obj.browse(cursor, user, record_ids,
|
||||
context=context):
|
||||
if self.eval(cursor, user, trigger, record, context=context):
|
||||
triggered_ids.append(record.id)
|
||||
if triggered_ids:
|
||||
self.trigger_action(cursor, user, triggered_ids, trigger.id,
|
||||
context=context)
|
||||
|
||||
def create(self, cursor, user, values, context=None):
|
||||
res = super(Trigger, self).create(cursor, user, values, context=context)
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.get_triggers(cursor.dbname)
|
||||
return res
|
||||
|
||||
def write(self, cursor, user, ids, values, context=None):
|
||||
res = super(Trigger, self).write(cursor, user, ids, values,
|
||||
context=context)
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.get_triggers(cursor.dbname)
|
||||
return res
|
||||
|
||||
def delete(self, cursor, user, ids, context=None):
|
||||
res = super(Trigger, self).delete(cursor, user, ids, context=context)
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.get_triggers(cursor.dbname)
|
||||
return res
|
||||
|
||||
Trigger()
|
||||
|
||||
|
||||
class TriggerLog(ModelSQL):
|
||||
'Trigger Log'
|
||||
_name = 'ir.trigger.log'
|
||||
_description = __doc__
|
||||
trigger = fields.Many2One('ir.trigger', 'Trigger', required=True)
|
||||
record_id = fields.Integer('Record ID', required=True)
|
||||
|
||||
def init(self, cursor, module_name):
|
||||
super(TriggerLog, self).init(cursor, module_name)
|
||||
|
||||
table = TableHandler(cursor, self, module_name)
|
||||
table.index_action(['trigger', 'record_id'], 'add')
|
||||
|
||||
TriggerLog()
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="trigger_view_form">
|
||||
<field name="model">ir.trigger</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<form string="Trigger">
|
||||
<label name="name"/>
|
||||
<field name="name" colspan="3"/>
|
||||
<label name="model"/>
|
||||
<field name="model"/>
|
||||
<group colspan="2" col="10" id="checkboxes">
|
||||
<label name="active"/>
|
||||
<field name="active" xexpand="0"/>
|
||||
<label name="on_time"/>
|
||||
<field name="on_time" xexpand="0"/>
|
||||
<label name="on_create"/>
|
||||
<field name="on_create" xexpand="0"/>
|
||||
<label name="on_write"/>
|
||||
<field name="on_write" xexpand="0"/>
|
||||
<label name="on_delete"/>
|
||||
<field name="on_delete" xexpand="0"/>
|
||||
</group>
|
||||
<label name="condition"/>
|
||||
<field name="condition" colspan="3"/>
|
||||
<label name="limit_number"/>
|
||||
<field name="limit_number"/>
|
||||
<label name="minimum_delay"/>
|
||||
<field name="minimum_delay" widget="float_time"/>
|
||||
<label name="action_model"/>
|
||||
<field name="action_model"/>
|
||||
<label name="action_function"/>
|
||||
<field name="action_function"/>
|
||||
</form>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="trigger_view_tree">
|
||||
<field name="model">ir.trigger</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<![CDATA[
|
||||
<tree string="Triggers">
|
||||
<field name="name" select="1"/>
|
||||
<field name="model" select="1"/>
|
||||
<field name="on_time" select="2"/>
|
||||
<field name="on_create" select="2"/>
|
||||
<field name="on_write" select="2"/>
|
||||
<field name="on_delete" select="2"/>
|
||||
<field name="active" select="2"/>
|
||||
</tree>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_trigger_form">
|
||||
<field name="name">Triggers</field>
|
||||
<field name="res_model">ir.trigger</field>
|
||||
<field name="view_type">form</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_trigger_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="trigger_view_tree"/>
|
||||
<field name="act_window" ref="act_trigger_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_trigger_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="trigger_view_form"/>
|
||||
<field name="act_window" ref="act_trigger_form"/>
|
||||
</record>
|
||||
<menuitem parent="menu_models"
|
||||
action="act_trigger_form" id="menu_trigger_form"/>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -417,6 +417,8 @@ class ModelSQL(ModelStorage):
|
||||
self._update_tree(cursor, user, id_new, k, field.left,
|
||||
field.right)
|
||||
|
||||
self.trigger_create(cursor, user, id_new, context=context)
|
||||
|
||||
return id_new
|
||||
|
||||
def read(self, cursor, user, ids, fields_names=None, context=None):
|
||||
@@ -716,6 +718,11 @@ class ModelSQL(ModelStorage):
|
||||
return res
|
||||
|
||||
def write(self, cursor, user, ids, values, context=None):
|
||||
|
||||
# Call before cursor cache cleaning
|
||||
trigger_eligibles = self.trigger_write_get_eligibles(cursor, user,
|
||||
isinstance(ids, (int, long)) and [ids] or ids, context=context)
|
||||
|
||||
super(ModelSQL, self).write(cursor, user, ids, values, context=context)
|
||||
|
||||
if context is None:
|
||||
@@ -966,6 +973,9 @@ class ModelSQL(ModelStorage):
|
||||
field.left, field.right)
|
||||
else:
|
||||
self._rebuild_tree(cursor, 0, k, False, 0)
|
||||
|
||||
self.trigger_write(cursor, user, trigger_eligibles, context=context)
|
||||
|
||||
return True
|
||||
|
||||
def delete(self, cursor, user, ids, context=None):
|
||||
@@ -1067,6 +1077,8 @@ class ModelSQL(ModelStorage):
|
||||
self.raise_user_error(cursor, 'access_error',
|
||||
self._description, context=context)
|
||||
|
||||
self.trigger_delete(cursor, user, ids, context=context)
|
||||
|
||||
for i in range(0, len(ids), cursor.IN_MAX):
|
||||
sub_ids = ids[i:i + cursor.IN_MAX]
|
||||
red_sql, red_ids = reduce_ids('id', sub_ids)
|
||||
|
||||
@@ -88,6 +88,26 @@ class ModelStorage(Model):
|
||||
self.__clean_xxx2many_cache(cursor, user, context=context)
|
||||
return False
|
||||
|
||||
def trigger_create(self, cursor, user, id, context=None):
|
||||
'''
|
||||
Trigger create actions
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param id: the created id
|
||||
:param context: the context
|
||||
'''
|
||||
trigger_obj = self.pool.get('ir.trigger')
|
||||
trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'create')
|
||||
if not trigger_ids:
|
||||
return
|
||||
record = self.browse(cursor, user, id, context=context)
|
||||
triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
|
||||
for trigger in triggers:
|
||||
if trigger_obj.eval(cursor, user, trigger, record, context=context):
|
||||
trigger_obj.trigger_action(cursor, user, [id], trigger.id,
|
||||
context=context)
|
||||
|
||||
def read(self, cursor, user, ids, fields_names=None, context=None):
|
||||
'''
|
||||
Read records.
|
||||
@@ -144,6 +164,58 @@ class ModelStorage(Model):
|
||||
self.__clean_xxx2many_cache(cursor, user, context=context)
|
||||
return False
|
||||
|
||||
def trigger_write_get_eligibles(self, cursor, user, ids, context=None):
|
||||
'''
|
||||
Return eligible ids for write actions by triggers
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param ids: a list of ids
|
||||
:param context: the context
|
||||
:return: a dictionary of the lists of eligible ids by triggers
|
||||
'''
|
||||
trigger_obj = self.pool.get('ir.trigger')
|
||||
trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'write')
|
||||
if not trigger_ids:
|
||||
return {}
|
||||
records = self.browse(cursor, user, ids, context=context)
|
||||
triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
|
||||
eligibles = {}
|
||||
for trigger in triggers:
|
||||
eligibles[trigger.id] = []
|
||||
for record in records:
|
||||
if not trigger_obj.eval(cursor, user, trigger, record,
|
||||
context=context):
|
||||
eligibles[trigger.id].append(record.id)
|
||||
return eligibles
|
||||
|
||||
def trigger_write(self, cursor, user, eligibles, context=None):
|
||||
'''
|
||||
Trigger write actions
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param eligibles: a dictionary of the lists of eligible ids by triggers
|
||||
:param context: the context
|
||||
'''
|
||||
trigger_obj = self.pool.get('ir.trigger')
|
||||
trigger_ids = eligibles.keys()
|
||||
if not trigger_ids:
|
||||
return
|
||||
records = self.browse(cursor, user, chain(*eligibles.values()),
|
||||
context=context)
|
||||
id2record = dict((x.id, x) for x in records)
|
||||
triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
|
||||
for trigger in triggers:
|
||||
triggered_ids = []
|
||||
for record_id in eligibles[trigger.id]:
|
||||
record = id2record[record_id]
|
||||
if trigger_obj.eval(cursor, user, trigger, record, context=context):
|
||||
triggered_ids.append(record.id)
|
||||
if triggered_ids:
|
||||
trigger_obj.trigger_action(cursor, user, triggered_ids,
|
||||
trigger.id, context=context)
|
||||
|
||||
def delete(self, cursor, user, ids, context=None):
|
||||
'''
|
||||
Delete records.
|
||||
@@ -176,6 +248,30 @@ class ModelStorage(Model):
|
||||
self.__clean_xxx2many_cache(cursor, user, context=context)
|
||||
return False
|
||||
|
||||
def trigger_delete(self, cursor, user, ids, context=None):
|
||||
'''
|
||||
Trigger delete actions
|
||||
|
||||
:param cursor: the database cursor
|
||||
:param user: the user id
|
||||
:param ids: the deleted ids
|
||||
:param context: the context
|
||||
'''
|
||||
trigger_obj = self.pool.get('ir.trigger')
|
||||
trigger_ids = trigger_obj.get_triggers(cursor, user, self._name, 'delete')
|
||||
if not trigger_ids:
|
||||
return
|
||||
records = self.browse(cursor, user, ids, context=context)
|
||||
triggers = trigger_obj.browse(cursor, user, trigger_ids, context=context)
|
||||
for trigger in triggers:
|
||||
triggered_ids = []
|
||||
for record in records:
|
||||
if trigger_obj.eval(cursor, user, trigger, record, context=context):
|
||||
triggered_ids.append(record.id)
|
||||
if triggered_ids:
|
||||
trigger_obj.trigger_action(cursor, user, triggered_ids,
|
||||
trigger.id, context=context)
|
||||
|
||||
def copy(self, cursor, user, ids, default=None, context=None):
|
||||
'''
|
||||
Duplicate the record(s) in ids.
|
||||
|
||||
@@ -348,6 +348,36 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_trigger">
|
||||
<field name="model" search="[('model', '=', 'ir.trigger')]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_trigger_admin">
|
||||
<field name="model" search="[('model', '=', 'ir.trigger')]"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_trigger_log">
|
||||
<field name="model" search="[('model', '=', 'ir.trigger.log')]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_trigger_log_admin">
|
||||
<field name="model" search="[('model', '=', 'ir.trigger.log')]"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.rule.group" id="rule_group_menu">
|
||||
<field name="model" search="[('model', '=', 'ir.ui.menu')]"/>
|
||||
<field name="global_p" eval="True"/>
|
||||
@@ -494,5 +524,24 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="rule_group" ref="rule_group_sequence_strict"/>
|
||||
</record>
|
||||
|
||||
<record model="res.user" id="user_trigger">
|
||||
<field name="login">user_cron_trigger</field>
|
||||
<field name="name">Cron Trigger</field>
|
||||
<field name="groups" eval="[('add', ref('group_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="cron_trigger_time">
|
||||
<field name="name">Run On Time Triggers</field>
|
||||
<field name="request_user" ref="user_admin"/>
|
||||
<field name="user" ref="user_trigger"/>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model">ir.trigger</field>
|
||||
<field name="function">trigger_time</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
||||
@@ -6,3 +6,4 @@ from model import *
|
||||
from mptt import *
|
||||
from import_data import *
|
||||
from export_data import *
|
||||
from trigger import *
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
from trytond.model import ModelSQL, fields
|
||||
|
||||
TRIGGER_LOGS = []
|
||||
|
||||
class Triggered(ModelSQL):
|
||||
'Triggered'
|
||||
_name = 'test.triggered'
|
||||
_description = __doc__
|
||||
|
||||
name = fields.Char('Name')
|
||||
|
||||
def trigger(self, cursor, user, ids, trigger_id, context=None):
|
||||
'''
|
||||
Trigger function for test
|
||||
'''
|
||||
TRIGGER_LOGS.append((ids, trigger_id))
|
||||
|
||||
Triggered()
|
||||
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
#this repository contains the full copyright notices and license terms.
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import time
|
||||
try:
|
||||
from itertools import combinations
|
||||
except ImportError:
|
||||
def combinations(iterable, r):
|
||||
pool = tuple(iterable)
|
||||
n = len(pool)
|
||||
if r > n:
|
||||
return
|
||||
indices = range(r)
|
||||
yield tuple(pool[i] for i in indices)
|
||||
while True:
|
||||
for i in reversed(range(r)):
|
||||
if indices[i] != i + n - r:
|
||||
break
|
||||
else:
|
||||
return
|
||||
indices[i] += 1
|
||||
for j in range(i+1, r):
|
||||
indices[j] = indices[j-1] + 1
|
||||
yield tuple(pool[i] for i in indices)
|
||||
from trytond.tests.test_tryton import POOL, DB, USER, CONTEXT, install_module
|
||||
from trytond.test.trigger import TRIGGER_LOGS
|
||||
|
||||
|
||||
class TriggerTestCase(unittest.TestCase):
|
||||
'''
|
||||
Test Trigger
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
install_module('test')
|
||||
self.triggered = POOL.get('test.triggered')
|
||||
self.trigger = POOL.get('ir.trigger')
|
||||
self.trigger_log = POOL.get('ir.trigger.log')
|
||||
self.model = POOL.get('ir.model')
|
||||
|
||||
def test0010constraints(self):
|
||||
'''
|
||||
Test constraints
|
||||
'''
|
||||
cursor = DB.cursor()
|
||||
|
||||
model_id = self.model.search(cursor, USER, [
|
||||
('model', '=', 'test.triggered'),
|
||||
], context=CONTEXT)[0]
|
||||
|
||||
values = {
|
||||
'name': 'Test',
|
||||
'model': model_id,
|
||||
'on_time': True,
|
||||
'condition': 'True',
|
||||
'action_model': model_id,
|
||||
'action_function': 'test',
|
||||
}
|
||||
self.assert_(self.trigger.create(cursor, USER, values, context=CONTEXT))
|
||||
|
||||
# on_exclusive
|
||||
for i in range(1, 4):
|
||||
for combination in combinations(['create', 'write', 'delete'], i):
|
||||
combination_values = values.copy()
|
||||
for mode in combination:
|
||||
combination_values['on_%s' % mode] = True
|
||||
self.failUnlessRaises(Exception, self.trigger.create, cursor, USER,
|
||||
combination_values, context=CONTEXT)
|
||||
|
||||
# check_condition
|
||||
condition_values = values.copy()
|
||||
condition_values['condition'] = '='
|
||||
self.failUnlessRaises(Exception, self.trigger.create, cursor, USER,
|
||||
condition_values, context=CONTEXT)
|
||||
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.trigger.get_triggers(cursor.dbname)
|
||||
cursor.rollback()
|
||||
cursor.close()
|
||||
|
||||
def test0020on_create(self):
|
||||
'''
|
||||
Test on_create
|
||||
'''
|
||||
cursor = DB.cursor()
|
||||
|
||||
model_id = self.model.search(cursor, USER, [
|
||||
('model', '=', 'test.triggered'),
|
||||
], context=CONTEXT)[0]
|
||||
|
||||
trigger_id = self.trigger.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
'model': model_id,
|
||||
'on_create': True,
|
||||
'condition': 'True',
|
||||
'action_model': model_id,
|
||||
'action_function': 'trigger',
|
||||
}, context=CONTEXT)
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Trigger with condition
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'self.name == "Bar"',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# Matching condition
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Non matching condition
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
# With limit number
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'True',
|
||||
'limit_number': 1,
|
||||
}, context=CONTEXT)
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# With minimum delay
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'limit_number': 0,
|
||||
'minimum_delay': 1,
|
||||
}, context=CONTEXT)
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.trigger.get_triggers(cursor.dbname)
|
||||
cursor.rollback()
|
||||
cursor.close()
|
||||
|
||||
def test0030on_write(self):
|
||||
'''
|
||||
Test on_write
|
||||
'''
|
||||
cursor = DB.cursor()
|
||||
|
||||
model_id = self.model.search(cursor, USER, [
|
||||
('model', '=', 'test.triggered'),
|
||||
], context=CONTEXT)[0]
|
||||
|
||||
trigger_id = self.trigger.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
'model': model_id,
|
||||
'on_write': True,
|
||||
'condition': 'True',
|
||||
'action_model': model_id,
|
||||
'action_function': 'trigger',
|
||||
}, context=CONTEXT)
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
# Trigger with condition
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'self.name == "Bar"',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# Matching condition
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# No change in condition
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
# Different change in condition
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
# With limit number
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'self.name == "Bar"',
|
||||
'limit_number': 1,
|
||||
}, context=CONTEXT)
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# With minimum delay
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'limit_number': 0,
|
||||
'minimum_delay': sys.maxint,
|
||||
}, context=CONTEXT)
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
for name in ('Bar', 'Foo', 'Bar'):
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': name,
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'minimum_delay': 0.02,
|
||||
}, context=CONTEXT)
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
for name in ('Bar', 'Foo'):
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': name,
|
||||
}, context=CONTEXT)
|
||||
time.sleep(1.2)
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id),
|
||||
([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.trigger.get_triggers(cursor.dbname)
|
||||
cursor.rollback()
|
||||
cursor.close()
|
||||
|
||||
def test0040on_delete(self):
|
||||
'''
|
||||
Test on_delete
|
||||
'''
|
||||
cursor = DB.cursor()
|
||||
|
||||
model_id = self.model.search(cursor, USER, [
|
||||
('model', '=', 'test.triggered'),
|
||||
], context=CONTEXT)[0]
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
|
||||
trigger_id = self.trigger.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
'model': model_id,
|
||||
'on_delete': True,
|
||||
'condition': 'True',
|
||||
'action_model': model_id,
|
||||
'action_function': 'trigger',
|
||||
}, context=CONTEXT)
|
||||
|
||||
self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Trigger with condition
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'self.name == "Bar"',
|
||||
}, context=CONTEXT)
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# Matching condition
|
||||
self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# Non matching condition
|
||||
self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# With limit number
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'True',
|
||||
'limit_number': 1,
|
||||
}, context=CONTEXT)
|
||||
self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
# Delete trigger logs because SQLite reuse the same triggered_id
|
||||
self.trigger_log.delete(cursor, USER,
|
||||
self.trigger_log.search(cursor, USER, [
|
||||
('trigger', '=', trigger_id),
|
||||
], context=CONTEXT))
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# With minimum delay
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'limit_number': 0,
|
||||
'minimum_delay': 1,
|
||||
}, context=CONTEXT)
|
||||
self.triggered.delete(cursor, USER, triggered_id, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], triggered_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.trigger.get_triggers(cursor.dbname)
|
||||
cursor.rollback()
|
||||
cursor.close()
|
||||
|
||||
def test0050on_time(self):
|
||||
'''
|
||||
Test on_time
|
||||
'''
|
||||
cursor = DB.cursor()
|
||||
|
||||
model_id = self.model.search(cursor, USER, [
|
||||
('model', '=', 'test.triggered'),
|
||||
], context=CONTEXT)[0]
|
||||
|
||||
trigger_id = self.trigger.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
'model': model_id,
|
||||
'on_time': True,
|
||||
'condition': 'True',
|
||||
'action_model': model_id,
|
||||
'action_function': 'trigger',
|
||||
}, context=CONTEXT)
|
||||
|
||||
triggered_id = self.triggered.create(cursor, USER, {
|
||||
'name': 'Test',
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Trigger with condition
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'self.name == "Bar"',
|
||||
}, context=CONTEXT)
|
||||
|
||||
# Matching condition
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Bar',
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Non matching condition
|
||||
self.triggered.write(cursor, USER, triggered_id, {
|
||||
'name': 'Foo',
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [])
|
||||
|
||||
# With limit number
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'condition': 'True',
|
||||
'limit_number': 1,
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Delete trigger logs of limit number test
|
||||
self.trigger_log.delete(cursor, USER,
|
||||
self.trigger_log.search(cursor, USER, [
|
||||
('trigger', '=', trigger_id),
|
||||
], context=CONTEXT))
|
||||
|
||||
# With minimum delay
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'limit_number': 0,
|
||||
'minimum_delay': sys.maxint,
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Delete trigger logs of previous minimum delay test
|
||||
self.trigger_log.delete(cursor, USER,
|
||||
self.trigger_log.search(cursor, USER, [
|
||||
('trigger', '=', trigger_id),
|
||||
], context=CONTEXT))
|
||||
|
||||
self.trigger.write(cursor, USER, trigger_id, {
|
||||
'minimum_delay': 0.02,
|
||||
}, context=CONTEXT)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
time.sleep(1.2)
|
||||
self.trigger.trigger_time(cursor, USER, context=CONTEXT)
|
||||
self.assert_(TRIGGER_LOGS == [([triggered_id], trigger_id),
|
||||
([triggered_id], trigger_id)])
|
||||
TRIGGER_LOGS.pop()
|
||||
TRIGGER_LOGS.pop()
|
||||
|
||||
# Restart the cache on the get_triggers method of ir.trigger
|
||||
self.trigger.get_triggers(cursor.dbname)
|
||||
cursor.rollback()
|
||||
cursor.close()
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(TriggerTestCase)
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = suite()
|
||||
unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
@@ -172,6 +172,8 @@ def all_suite():
|
||||
suite_.addTests(test_importdata.suite())
|
||||
import trytond.tests.test_exportdata as test_exportdata
|
||||
suite_.addTests(test_exportdata.suite())
|
||||
import trytond.tests.test_trigger as test_trigger
|
||||
suite_.addTests(test_trigger.suite())
|
||||
return suite_
|
||||
|
||||
def modules_suite():
|
||||
|
||||
Reference in New Issue
Block a user