├── .gitignore ├── README.md ├── __init__.py ├── __openerp__.py ├── api.py ├── backends.py ├── data └── ir_cron_datas.xml ├── dbq.py ├── models ├── __init__.py ├── ir_cron.py ├── oe_event.py ├── outer_result.py ├── task_result.py └── task_task.py ├── od13.py ├── requirements.txt ├── security ├── ir.model.access.csv └── ir_rule.xml ├── tasks.py └── views ├── oe_event_log_views.xml ├── oe_event_subscribe_views.xml ├── oe_event_views.xml ├── oe_task_result_views.xml ├── oe_task_views.xml └── task_result_views.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # C extensions 4 | *.so 5 | 6 | 7 | # Mr Developer 8 | .mr.developer.cfg 9 | .project 10 | .pydevproject 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task_queue 2 | Odoo async task with db store or celery 3 | 4 | # 基于Odoo db的使用 5 | 1. 安装此模块 task_queue 6 | 2. 将需要异步执行的方法加上装饰器 7 | ```python 8 | from odoo.addons.task_queue.api import AysncDB 9 | 10 | @AysncDB() 11 | @api.model 12 | def func(self, a, b): 13 | pass 14 | 15 | @AysncDB() 16 | @api.multi 17 | def func1(self, a, b): 18 | pass 19 | 20 | @AysncDB() 21 | @api.one 22 | def func2(self, a, b): 23 | pass 24 | 25 | @AsyncDB(countdown=10*60) 26 | @api.model 27 | def test_countdown(self): 28 | # 将延迟10分钟执行 29 | _logger.info('>>> It is time to do') 30 | ``` 31 | 任务的执行默认在odoo的cron中,不需要额外的运行 32 | 33 | # 基于celery的使用 34 | 详见 http://oejia.net/blog/2022/02/02/task_queue_use.html 35 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import od13 2 | from . import models 3 | -------------------------------------------------------------------------------- /__openerp__.py: -------------------------------------------------------------------------------- 1 | { 2 | 'name': 'Tase Queue', 3 | 'depends': [ 4 | 'base', 'base_setup' 5 | ], 6 | 'author': '', 7 | 'data': [ 8 | 'data/ir_cron_datas.xml', 9 | 'security/ir.model.access.csv', 10 | 'security/ir_rule.xml', 11 | 12 | 'views/oe_task_views.xml', 13 | 'views/oe_task_result_views.xml', 14 | #'views/task_result_views.xml', 15 | 'views/oe_event_views.xml', 16 | 'views/oe_event_subscribe_views.xml', 17 | ], 18 | 'installable': True, 19 | 'application': False, 20 | 'external_dependencies': { 21 | 'python': [], 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import datetime 5 | from hashlib import sha1 6 | import inspect 7 | from odoo.tools import config 8 | import logging 9 | 10 | _logger = logging.getLogger(__name__) 11 | celery_default_queue = config.get('celery_default_queue', 'odoo') 12 | 13 | enqueue_fail_then_exec = False 14 | 15 | class DateEncoder(json.JSONEncoder): 16 | 17 | def default(self, obj): 18 | if isinstance(obj,datetime.datetime): 19 | return obj.strftime("%Y-%m-%d %H:%M:%S") 20 | elif isinstance(obj, datetime.date): 21 | return obj.strftime("%Y-%m-%d") 22 | else: 23 | try: 24 | return json.JSONEncoder.default(self,obj) 25 | except: 26 | import traceback;traceback.print_exc() 27 | return str(obj) 28 | 29 | class _CeleryTask(object): 30 | 31 | def __init__(self, *args, **kwargs): 32 | self.countdown = 0 33 | self.eta = None 34 | self.expires = None 35 | self.priority = 5 36 | self.queue = celery_default_queue 37 | for arg, value in kwargs.items(): 38 | setattr(self, arg, value) 39 | 40 | def __call__(self, f, *args, **kwargs): 41 | token = sha1(f.__name__).hexdigest() 42 | 43 | def f_job(*args, **kwargs): 44 | _logger.info(str(args)) 45 | if len(args) == 1 or args[-1] != token: 46 | args += (token,) 47 | osv_object = args[0]._name 48 | arglist = list(args) 49 | arglist.pop(0) # Remove self 50 | cr = arglist.pop(0) 51 | uid = arglist.pop(0) 52 | dbname = cr.dbname 53 | fname = f.__name__ 54 | # Pass OpenERP server config to the worker 55 | conf_attrs = dict( 56 | [(attr, value) for attr, value in config.options.items()] 57 | ) 58 | task_args = (conf_attrs, dbname, uid, osv_object, fname) 59 | if arglist: 60 | task_args += tuple(arglist) 61 | pprint(task_args) 62 | try: 63 | celery_task = execute.apply_async( 64 | args=task_args, kwargs=kwargs, 65 | countdown=self.countdown, eta=self.eta, 66 | expires=self.expires, priority=self.priority, 67 | queue=getattr(self, "queue", celery_default_queue)) 68 | 69 | _logger.info('Enqueued task %s.%s(%s) on celery with id %s' 70 | % (osv_object, fname, str(args[3:]), 71 | celery_task and celery_task.id)) 72 | return celery_task.id 73 | except Exception as exc: 74 | if args[-1] == token: 75 | args = args[:-1] 76 | _logger.error( 77 | 'Celery enqueue task failed %s.%s ' 78 | 'executing task now ' 79 | 'Exception: %s' % (osv_object, fname, exc)) 80 | return f(*args, **kwargs) 81 | else: 82 | args = args[:-1] 83 | return f(*args, **kwargs) 84 | return f_job 85 | 86 | 87 | class Base(object): 88 | 89 | def __init__(self, *args, **kwargs): 90 | self.countdown = 0 91 | self.eta = None 92 | self.expires = None 93 | self.priority = 5 94 | self.queue = celery_default_queue 95 | for arg, value in kwargs.items(): 96 | setattr(self, arg, value) 97 | self.env = None 98 | 99 | def __call__(self, f, *args, **kwargs): 100 | token = sha1(f.__name__.encode('utf-8')).hexdigest() 101 | 102 | def f_job(*args, **kwargs): 103 | _logger.info('>>>> user %s call %s.%s %s %s'%(args[0].env.uid, args[0], f.__name__, str(args[1:]), str(kwargs))) 104 | if len(args) == 1 or args[-1] != token: 105 | # 加入任务队列 106 | args += (token,) # 普通参数尾部增加一个标志参数 107 | _self = args[0] 108 | self.env = _self.env 109 | model_name = _self._name # 第一个参数为 self 110 | argspec = inspect.getfullargspec(f) 111 | argspec_args = argspec.args if argspec.args else [] 112 | argspec_args += [None] * 4 113 | argspecargs = tuple(argspec_args) 114 | arglist = list(args) 115 | 116 | obj_ids = None 117 | _logger.info('>>> argspecargs: %s hasattr api: %s', argspecargs, hasattr(f, '_api')) 118 | if argspecargs[1] not in ('cr', 'cursor'): 119 | # 当为新API时 120 | cr, uid, context = _self.env.cr, _self.env.uid, dict(_self.env.context) 121 | obj = arglist.pop(0) 122 | obj_ids = obj.ids 123 | else: 124 | # 当为老API时 125 | cr, uid, context = args[0].env.cr, args[0].env.uid, \ 126 | dict(args[0].env.context) 127 | #arglist.pop(0) # Remove self 128 | #cr = arglist.pop(0) 129 | #uid = arglist.pop(0) 130 | kwargs['context'] = { k: v for k,v in context.items() if not hasattr(v, '_name') } 131 | 132 | dbname = cr.dbname 133 | fname = f.__name__ 134 | task_doc = f.__doc__ or f.__name__ 135 | # Pass OpenERP server config to the worker 136 | odoo_conf_attrs = dict( 137 | [(attr, value) for attr, value in config.options.items()] 138 | ) 139 | # 拼接任务参数 140 | task_args = (odoo_conf_attrs, dbname, uid, model_name, fname) 141 | #if obj_ids: 142 | task_args += (obj_ids,) 143 | if arglist: 144 | task_args += tuple(arglist) 145 | 146 | try: 147 | #cr.commit() 148 | task = self.gen_task(task_args, kwargs, task_doc) 149 | 150 | _logger.info('Enqueued task %s.%s%s on celery with id %s' % (model_name, fname, str(args[1:-1]), task and task.id)) 151 | return task and task.id or None 152 | except Exception as exc: 153 | # 入队失败时 154 | import traceback;traceback.print_exc() 155 | if enqueue_fail_then_exec: 156 | kwargs.pop('context') 157 | if args[-1] == token: 158 | args = args[:-1] 159 | _logger.error('Enqueue task failed %s.%s executing task now Exception: %s' % (model_name, fname, exc)) 160 | return f(*args, **kwargs) 161 | else: 162 | raise exc 163 | else: 164 | # 工作进程时直接执行 165 | args = args[:-1] 166 | return f(*args, **kwargs) 167 | return f_job 168 | 169 | class Async(Base): 170 | 171 | def gen_task(self, task_args, kwargs, task_doc): 172 | from .tasks import execute 173 | task = execute.apply_async( 174 | args=task_args, kwargs=kwargs, 175 | countdown=self.countdown, eta=self.eta, 176 | expires=self.expires, priority=self.priority, 177 | queue=getattr(self, "queue", celery_default_queue)) 178 | return task 179 | 180 | class AsyncDB(Base): 181 | 182 | def gen_task(self, task_args, kwargs, task_doc): 183 | #_logger.info('>>> gen task %s %s', task_args, kwargs) 184 | odoo_conf_attrs = task_args[0] 185 | dbname = task_args[1] 186 | uid = task_args[2] 187 | model_name = task_args[3] 188 | method = task_args[4] 189 | ids = task_args[5] 190 | task = self.env['oe.task'].sudo().create({ 191 | 'task_id': '', 192 | 'task_name': '%s.%s()'%(model_name, method), 193 | 'task_doc': task_doc, 194 | 'task_args': json.dumps(task_args[1:], cls=DateEncoder), 195 | 'task_kwargs': json.dumps(kwargs, cls=DateEncoder), 196 | 'countdown': self.countdown, 197 | }) 198 | self.env['oe.task.result'].sudo().create({ 199 | 'task_id': task.id, 200 | 'task_name': task.task_name, 201 | 'task_doc': task.task_doc, 202 | 'task_args': task.task_args, 203 | 'task_kwargs': task.task_kwargs, 204 | 'countdown': task.countdown, 205 | }) 206 | return task 207 | -------------------------------------------------------------------------------- /backends.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from celery.backends import database 4 | from celery.backends.database import DatabaseBackend, retry, session_cleanup 5 | from celery.backends.database.models import Task, ResultModelBase 6 | 7 | import sqlalchemy as sa 8 | from datetime import datetime 9 | from sqlalchemy.types import PickleType 10 | from celery import states 11 | from celery.five import python_2_unicode_compatible 12 | 13 | 14 | @python_2_unicode_compatible 15 | class Task(ResultModelBase): 16 | """Task result/status.""" 17 | 18 | __tablename__ = 'celery_taskmeta_ext' 19 | __table_args__ = {'sqlite_autoincrement': True} 20 | 21 | id = sa.Column(sa.Integer, sa.Sequence('task_id_sequence'), 22 | primary_key=True, autoincrement=True) 23 | task_id = sa.Column(sa.String(155), unique=True) 24 | status = sa.Column(sa.String(50), default=states.PENDING) 25 | result = sa.Column(PickleType, nullable=True) 26 | date_done = sa.Column(sa.DateTime, default=datetime.utcnow, 27 | onupdate=datetime.utcnow, nullable=True) 28 | traceback = sa.Column(sa.Text, nullable=True) 29 | 30 | task_name = sa.Column(sa.String(255), nullable=True) 31 | task_args = sa.Column(sa.Text, nullable=True) 32 | task_kwargs = sa.Column(sa.Text, nullable=True) 33 | 34 | def __init__(self, task_id): 35 | self.task_id = task_id 36 | 37 | def to_dict(self): 38 | return { 39 | 'task_id': self.task_id, 40 | 'status': self.status, 41 | 'result': self.result, 42 | 'traceback': self.traceback, 43 | 'date_done': self.date_done, 44 | } 45 | 46 | def __repr__(self): 47 | return ''.format(self) 48 | 49 | database.Task = Task 50 | 51 | class ExtDatabaseBackend(DatabaseBackend): 52 | 53 | def __init__(self, dburi=None, engine_options=None, url=None, **kwargs): 54 | conf = kwargs['app'].conf 55 | url = getattr(conf,'result_backend_db', None) 56 | super(ExtDatabaseBackend, self).__init__(dburi=dburi, engine_options=engine_options, url=url, **kwargs) 57 | 58 | @retry 59 | def _store_result(self, task_id, result, state, 60 | traceback=None, max_retries=3, **kwargs): 61 | request = kwargs.get('request',{}) 62 | session = self.ResultSession() 63 | with session_cleanup(session): 64 | task = list(session.query(Task).filter(Task.task_id == task_id)) 65 | task = task and task[0] 66 | if not task: 67 | task = Task(task_id) 68 | session.add(task) 69 | session.flush() 70 | task.result = result 71 | task.status = state 72 | task.traceback = traceback 73 | task.task_name = repr(getattr(request, 'task', None)) 74 | _args = self.get_args(getattr(request, 'args', [])) 75 | task.task_args = repr(_args) 76 | task.task_kwargs = repr(getattr(request, 'kwargs', None)) 77 | session.commit() 78 | return result 79 | 80 | def get_args(self,data): 81 | if len(data)>=5: 82 | if type(data[0])==dict: 83 | if 'xmlrpc_port' in data[0]: 84 | return data[1:] 85 | return data 86 | 87 | -------------------------------------------------------------------------------- /data/ir_cron_datas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Task Queue Worker 6 | 1 7 | minutes 8 | -1 9 | 10 | 11 | model._process() 12 | code 13 | 14 | specific 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dbq.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from celery import Celery 5 | from kombu import Exchange, Queue 6 | from odoo.tools import config 7 | 8 | 9 | _logger = logging.getLogger('Celery Worker') 10 | 11 | class Task(object): 12 | 13 | def __init__(self): 14 | pass 15 | 16 | class DBQ(object): 17 | 18 | def __init__(self, name='dbq'): 19 | pass 20 | 21 | 22 | def task(self, *opt, **kopt): 23 | def wrapper(f): 24 | def _wrapper(*args, **kwargs): 25 | return f(*args, **kargs) 26 | return _wrapper 27 | return wrapper 28 | 29 | 30 | 31 | app = Celery('celery_queue') 32 | 33 | 34 | class CeleryConfig(): 35 | # 默认的队列 36 | celery_default_queue = config.get('celery_default_queue', 'odoo10') 37 | # 定义的所有队列 38 | celery_queues = config.get('celery_queues', "") 39 | 40 | BROKER_URL = config.get('celery_broker_url') 41 | CELERY_DEFAULT_QUEUE = celery_default_queue 42 | CELERY_QUEUES = ( 43 | Queue(celery_default_queue, Exchange(celery_default_queue), 44 | routing_key=celery_default_queue, durable=True), 45 | ) 46 | for queue in filter(lambda q: q.strip(), celery_queues.split(",")): 47 | CELERY_QUEUES = CELERY_QUEUES + \ 48 | (Queue(queue, Exchange(queue), routing_key=queue, durable=True),) 49 | 50 | app.config_from_object(CeleryConfig) 51 | 52 | if config.get('celery_result_backend_db'): 53 | app.conf.result_backend = 'odoo.addons.task_queue.backends.ExtDatabaseBackend'#'db+postgresql://user:passwd@localhost/celery_result' 54 | app.conf.result_backend_db = config.get('celery_result_backend_db') 55 | app.conf.task_ignore_result = True 56 | app.conf.task_store_errors_even_if_ignored = True 57 | 58 | 59 | @app.task 60 | def add(x,y): 61 | return x+y 62 | 63 | 64 | import odoo 65 | from odoo.api import Environment 66 | from odoo.modules.registry import Registry 67 | 68 | @app.task(name='odoo.addons.celery_queue.tasks.execute') 69 | def execute(conf_attrs, dbname, uid, obj, method, *args, **kwargs): 70 | _logger.info(str([dbname, uid, obj, method, args, kwargs])) 71 | 72 | if conf_attrs and len(conf_attrs.keys())>1: 73 | for attr, value in conf_attrs.items(): 74 | odoo.tools.config[attr] = value 75 | with Environment.manage(): 76 | registry = Registry(dbname) 77 | cr = registry.cursor() 78 | context = 'context' in kwargs and kwargs.pop('context') or {} 79 | env = Environment(cr, uid, context) 80 | cr.autocommit(True) 81 | # odoo.api.Environment._local.environments = env 82 | try: 83 | Model = env[obj] 84 | args = list(args) 85 | _logger.info('>>> %s'%str(args)) 86 | ids = args.pop(0) 87 | if ids: 88 | target = Model.search([('id', 'in', ids)]) 89 | else: 90 | target = Model 91 | getattr(env.registry[obj], method)(target, *args, **kwargs) 92 | # Commit only when function finish 93 | # env.cr.commit() 94 | except Exception as exc: 95 | env.cr.rollback() 96 | import traceback;traceback.print_exc() 97 | raise exc 98 | #try: 99 | # raise execute.retry( 100 | # queue=execute.request.delivery_info['routing_key'], 101 | # exc=exc, countdown=(execute.request.retries + 1) * 60, 102 | # max_retries=5) 103 | #except Exception as retry_exc: 104 | # raise retry_exc 105 | finally: 106 | env.cr.close() 107 | return True 108 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import task_task 2 | from . import task_result 3 | #from . import ir_cron 4 | from . import oe_event 5 | -------------------------------------------------------------------------------- /models/ir_cron.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | from odoo import _, models, fields, api 6 | 7 | 8 | _logger = logging.getLogger(__name__) 9 | 10 | class IrCron(models.Model): 11 | 12 | _inherit = 'ir.cron' 13 | 14 | @classmethod 15 | def _process_job(cls, job_cr, job, cron_cr): 16 | if job['cron_name']=='task_queue_worker': 17 | job['interval_number'] = 0.1 18 | return super(IrCron, cls)._process_job(job_cr, job, cron_cr) 19 | -------------------------------------------------------------------------------- /models/oe_event.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import logging 4 | import json 5 | import datetime 6 | 7 | from odoo import _, models, fields, api 8 | from odoo import modules 9 | 10 | from ..api import AsyncDB 11 | 12 | _logger = logging.getLogger(__name__) 13 | 14 | 15 | class DateEncoder(json.JSONEncoder): 16 | 17 | def default(self, obj): 18 | if isinstance(obj,datetime.datetime): 19 | return obj.strftime("%Y-%m-%d %H:%M:%S") 20 | else: 21 | return json.JSONEncoder.default(self,obj) 22 | 23 | class OeEvent(models.Model): 24 | 25 | _name = 'oe.event' 26 | _description = u'事件' 27 | 28 | model_id = fields.Many2one('ir.model', string='模型') 29 | field_ids = fields.Many2many('ir.model.fields', string='监听字段') 30 | etype = fields.Selection([('create', '创建'), ('write',u'更新'),('unlink',u'删除')], string=u'事件类型', required=True) 31 | enable = fields.Boolean('启用', default=False) 32 | subscribe_ids = fields.One2many('oe.event.subscribe', 'event_id', string='事件订阅') 33 | 34 | def _register_hook(self): 35 | super(OeEvent, self)._register_hook() 36 | _logger.info('>>> _register_hook') 37 | if not self: 38 | self = self.search([('enable', '=', True)]) 39 | return self._patch_methods() 40 | 41 | def _make_create(self): 42 | obj = self 43 | event_obj_id = obj.id 44 | 45 | @api.model_create_multi 46 | @api.returns('self', lambda value: value.id) 47 | def event_create(self, vals_list, **kwargs): 48 | new_records = event_create.origin(self, vals_list, **kwargs) 49 | for i in range(len(vals_list)): 50 | self.env(user=1)['oe.event'].browse(event_obj_id).execute_create(new_records[i].id, json.dumps(vals_list[i], cls=DateEncoder)) 51 | return new_records 52 | return event_create 53 | 54 | def _make_write(self): 55 | obj = self 56 | event_obj_id = obj.id 57 | 58 | @api.multi 59 | def event_write(self, vals, **kwargs): 60 | event_obj = self.env(user=1)['oe.event'].browse(event_obj_id) 61 | field_list = [e.name for e in event_obj.field_ids] 62 | flag = False 63 | if field_list: 64 | for k in vals.keys(): 65 | if k in field_list: 66 | flag = True 67 | break 68 | else: 69 | flag = True 70 | 71 | if flag: 72 | old_vals_map = {e.id : e.read(vals.keys())[0] for e in self.sudo()} 73 | res = event_write.origin(self, vals, **kwargs) 74 | for res in self.sudo(): 75 | old_vals = old_vals_map.get(res.id) 76 | record_flag = False 77 | if field_list: 78 | for _field in field_list: 79 | old_val = old_vals[_field] 80 | if type(old_val) in [list, tuple]: 81 | old_val = old_val[0] 82 | if vals[_field]!=old_val: 83 | record_flag = True 84 | break 85 | else: 86 | record_flag = True 87 | if record_flag: 88 | event_obj.execute_write(res.id, json.dumps(old_vals, cls=DateEncoder), json.dumps(vals, cls=DateEncoder)) 89 | return res 90 | else: 91 | return event_write.origin(self, vals, **kwargs) 92 | 93 | return event_write 94 | 95 | def _make_unlink(self): 96 | obj = self 97 | event_obj_id = obj.id 98 | 99 | @api.multi 100 | def event_unlink(self, **kwargs): 101 | res = event_unlink.origin(self, **kwargs) 102 | for res in self.sudo(): 103 | self.env(user=1)['oe.event'].browse(event_obj_id).execute_unlink(res.id) 104 | return res 105 | return event_unlink 106 | 107 | @api.multi 108 | def _patch_methods(self): 109 | updated = False 110 | for obj in self: 111 | model_model = self.env[obj.model_id.model] 112 | if obj.etype=='create' and not hasattr(model_model, 'oe_event_create'): 113 | model_model._patch_method('create', obj._make_create()) 114 | setattr(type(model_model), 'oe_event_create', True) 115 | updated = True 116 | if obj.etype=='write' and not hasattr(model_model, 'oe_event_write'): 117 | model_model._patch_method('write', obj._make_write()) 118 | setattr(type(model_model), 'oe_event_write', True) 119 | updated = True 120 | if obj.etype=='unlink' and not hasattr(model_model, 'oe_event_unlink'): 121 | model_model._patch_method('unlink', obj._make_unlink()) 122 | setattr(type(model_model), 'oe_event_unlink', True) 123 | updated = True 124 | 125 | @AsyncDB() 126 | @api.multi 127 | def execute_create(self, res_id, vals): 128 | for obj in self: 129 | for subscribe in obj.subscribe_ids: 130 | log = self.env['oe.event.log'].create({ 131 | 'subscribe_id': subscribe.id, 132 | 'res_model': subscribe.event_id.model_id.model, 133 | 'res_id': res_id, 134 | 'etype': subscribe.event_id.etype, 135 | 'new_vals': vals, 136 | }) 137 | self.exec_server_action(log, subscribe.action_server_id) 138 | 139 | @AsyncDB() 140 | @api.multi 141 | def execute_write(self, res_id, old_vals, new_vals): 142 | self.ensure_one() 143 | if not self.env.user.has_group('base.group_system'): 144 | raise AccessError(_("Only system users can execute events")) 145 | # 验证输入 146 | if not self._validate_vals(old_vals, new_vals): 147 | raise ValidationError(_("Invalid values")) 148 | for obj in self: 149 | for subscribe in obj.subscribe_ids: 150 | log = self.env['oe.event.log'].create({ 151 | 'subscribe_id': subscribe.id, 152 | 'res_model': subscribe.event_id.model_id.model, 153 | 'res_id': res_id, 154 | 'etype': subscribe.event_id.etype, 155 | 'old_vals': old_vals, 156 | 'new_vals': new_vals, 157 | }) 158 | self.exec_server_action(log, subscribe.action_server_id) 159 | 160 | 161 | @AsyncDB() 162 | @api.multi 163 | def execute_unlink(self, res_id): 164 | for obj in self: 165 | for subscribe in obj.subscribe_ids: 166 | log = self.env['oe.event.log'].create({ 167 | 'subscribe_id': subscribe.id, 168 | 'res_model': subscribe.event_id.model_id.model, 169 | 'res_id': res_id, 170 | 'etype': subscribe.event_id.etype, 171 | }) 172 | self.exec_server_action(log, subscribe.action_server_id) 173 | 174 | def exec_server_action(self, log, action): 175 | _logger.info('>>> exec_server_action %s %s', log, action) 176 | new_context = dict(self._context) or {} 177 | new_context.update({ 178 | 'active_id': log.id, 179 | 'active_ids': [log.id], 180 | 'active_model': 'oe.event.log', 181 | }) 182 | #self.env.cr.commit() 183 | action.sudo().with_context(new_context).run() 184 | #self.env.cr.commit() 185 | 186 | @api.multi 187 | def _revert_methods(self): 188 | updated = False 189 | for obj in self: 190 | model_model = self.env[obj.model_id.model] 191 | method = obj.etype 192 | if getattr(model_model, 'oe_event_%s' % method) and hasattr(getattr(model_model, method), 'origin'): 193 | model_model._revert_method(method) 194 | delattr(type(model_model), 'oe_event_%s' % method) 195 | updated = True 196 | if updated: 197 | modules.registry.Registry(self.env.cr.dbname).signal_changes() 198 | 199 | @api.multi 200 | def subscribe(self): 201 | self.write({'enable': True}) 202 | return True 203 | 204 | @api.multi 205 | def unsubscribe(self): 206 | self._revert_methods() 207 | for obj in self: 208 | pass 209 | self.write({'enable': False}) 210 | return True 211 | 212 | class EventSubscribe(models.Model): 213 | 214 | _name = 'oe.event.subscribe' 215 | _description = u'事件订阅' 216 | 217 | event_id = fields.Many2one('oe.event', string='事件', required=True) 218 | domain = fields.Char(string='执行条件') 219 | action_server_id = fields.Many2one('ir.actions.server', '执行动作') 220 | enable = fields.Boolean('启用', default=False) 221 | 222 | class EventLog(models.Model): 223 | 224 | _name = 'oe.event.log' 225 | _description = u'事件记录' 226 | 227 | subscribe_id = fields.Many2one('oe.event.subscribe', string='所属订阅', required=True) 228 | res_model = fields.Char(u'记录模型') 229 | res_id = fields.Integer(u'记录ID') 230 | old_vals = fields.Text(u'旧值') 231 | new_vals = fields.Text(u'新值') 232 | etype = fields.Selection([('create', '创建'), ('write',u'更新'),('unlink',u'删除')], string=u'事件类型', required=True) 233 | 234 | def get_res(self): 235 | obj = self.env[self.res_model].browse(self.res_id) 236 | return obj 237 | 238 | def get_old(self): 239 | return json.loads(self.old_vals) 240 | 241 | def get_new(self): 242 | return json.loads(self.new_vals) 243 | -------------------------------------------------------------------------------- /models/outer_result.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | 5 | from odoo import _, models, fields, api 6 | from odoo import tools 7 | from odoo.tools import config 8 | from celery import states 9 | 10 | 11 | ALL_STATES = sorted(states.ALL_STATES) 12 | TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) 13 | TASK_STATE_CHOICES.append(('re_executed', u'已处理')) 14 | 15 | 16 | class TaskResult(models.Model): 17 | 18 | _name = 'task.result' 19 | _description = u'Task' 20 | _auto = False 21 | 22 | task_id = fields.Char(_('task id')) 23 | 24 | task_name = fields.Char(_('task name')) 25 | task_args = fields.Char(_('task arguments')) 26 | task_kwargs = fields.Char(_('task kwargs')) 27 | 28 | status = fields.Selection(TASK_STATE_CHOICES, string=_('state'), default=states.PENDING) 29 | # content_type = fields.Char(_('content type')) 30 | # content_encoding = fields.Char(_('content encoding')) 31 | result = fields.Html(_('result'), default=None) 32 | date_done = fields.Datetime('done at', default=fields.Datetime.now) 33 | traceback = fields.Html(_('traceback')) 34 | # hidden = fields.Boolean(_('hidden'), default=False, index=True) 35 | # meta = fields.Html(_('meta'), default=None) 36 | 37 | @api.model_cr 38 | def init(self): 39 | tools.drop_view_if_exists(self.env.cr, self._table) 40 | self.env.cr.execute("""CREATE or REPLACE VIEW %s as ( 41 | select * from celery_taskmeta order by id desc 42 | )""" % self._table) 43 | 44 | @api.multi 45 | def name_get(self): 46 | return [(e.id, ''.format(e)) for e in self] 47 | 48 | @api.multi 49 | def re_execute(self): 50 | from .tasks import execute 51 | countdown = 0 52 | eta = None 53 | expires = None 54 | priority = 5 55 | queue = config.get('celery_default_queue', 'odoo10') 56 | 57 | for obj in self: 58 | celery_task = execute.apply_async( 59 | args=[{'xmlrpc_port':''}] + eval(obj.task_args), kwargs=eval(obj.task_kwargs), 60 | countdown=countdown, eta=eta, 61 | expires=expires, priority=priority, 62 | queue=queue) 63 | obj.write({'status': 're_executed'}) 64 | -------------------------------------------------------------------------------- /models/task_result.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import json 5 | 6 | from odoo import _, models, fields, api 7 | 8 | 9 | class TaskResult(models.Model): 10 | 11 | _name = 'oe.task.result' 12 | _description = u'Task Result' 13 | _inherit = ['oe.task.abstract'] 14 | _rec_name = 'task_name' 15 | 16 | result = fields.Text(_('result'), default=None) 17 | date_done = fields.Datetime('done at') 18 | traceback = fields.Text(_('traceback')) 19 | execution_time = fields.Float('执行时长(秒)', digits=(16,3), readonly=True, help="任务执行耗时(秒)") 20 | display_args = fields.Char('参数信息', compute='_compute_display_args', store=False) 21 | 22 | @api.depends('task_args') 23 | def _compute_display_args(self): 24 | for record in self: 25 | if not record.task_args: 26 | record.display_args = '' 27 | continue 28 | 29 | try: 30 | args = json.loads(record.task_args) 31 | if len(args) < 4: # 参数不完整 32 | record.display_args = '' 33 | continue 34 | 35 | # 解析参数 36 | dbname = args[0] # 数据库名 37 | uid = args[1] # 用户ID 38 | model_name = args[2] # 模型名 39 | method = args[3] # 方法名 40 | ids = args[4] # 记录IDs 41 | other_args = args[5:-1] # 其他参数(排除token) 42 | 43 | # 获取记录名称 44 | records_name = '' 45 | if ids and isinstance(ids, list): 46 | try: 47 | records = self.env[model_name].sudo().browse(ids).exists() 48 | if records: 49 | records_name = ','.join(records.mapped('display_name')) 50 | except Exception as e: 51 | _logger.warning('Failed to get record names: %s', e) 52 | 53 | # 组装显示内容 54 | parts = [] 55 | if records_name: 56 | parts.append(records_name) 57 | elif ids: 58 | parts.append(f"IDs: {ids}") 59 | 60 | # 添加其他参数 61 | if other_args: 62 | args_str = ', '.join(str(arg) for arg in other_args) 63 | if args_str: 64 | parts.append(f"参数: {args_str}") 65 | 66 | record.display_args = ' | '.join(parts) or '无参数' 67 | 68 | except Exception as e: 69 | record.display_args = '参数解析失败' 70 | _logger.error('Failed to compute display_args: %s', e) 71 | 72 | @api.multi 73 | def re_execute(self): 74 | for obj in self: 75 | task = self.env['oe.task'].sudo().create({ 76 | 'task_id': '', 77 | 'task_name': obj.task_name, 78 | 'task_doc': obj.task_doc, 79 | 'task_args': obj.task_args, 80 | 'task_kwargs': obj.task_kwargs, 81 | 'countdown': 0, 82 | }) 83 | obj.write({'status': 'RETRY'}) 84 | 85 | @api.multi 86 | def view_result(self): 87 | self.ensure_one() 88 | if not self.result: 89 | return { 90 | 'type': 'ir.actions.client', 91 | 'tag': 'display_notification', 92 | 'params': { 93 | 'title': _('提示'), 94 | 'message': _('没有可查看的执行结果'), 95 | 'type': 'warning', 96 | } 97 | } 98 | 99 | try: 100 | result = json.loads(self.result) 101 | # 判断是否是Odoo action 102 | if isinstance(result, dict) and result.get('type') in ['ir.actions.act_window', 'ir.actions.client']: 103 | return result 104 | else: 105 | return { 106 | 'type': 'ir.actions.client', 107 | 'tag': 'display_notification', 108 | 'params': { 109 | 'title': _('提示'), 110 | 'message': _('执行结果不是可打开的操作'), 111 | 'type': 'warning', 112 | } 113 | } 114 | except Exception as e: 115 | return { 116 | 'type': 'ir.actions.client', 117 | 'tag': 'display_notification', 118 | 'params': { 119 | 'title': _('错误'), 120 | 'message': _('解析执行结果失败: %s') % str(e), 121 | 'type': 'danger', 122 | } 123 | } 124 | 125 | @api.multi 126 | def name_get(self): 127 | result = [] 128 | for record in self: 129 | # 组合显示名称: 任务说明 - 任务名称 [状态] 130 | name = "%s - %s [%s]" % ( 131 | record.task_doc or '', 132 | record.task_name or '', 133 | dict(self._fields['status'].selection).get(record.status, '') 134 | ) 135 | result.append((record.id, name)) 136 | return result 137 | -------------------------------------------------------------------------------- /models/task_task.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import logging 5 | import traceback 6 | from datetime import datetime, timedelta 7 | import time 8 | 9 | import odoo 10 | from odoo import _, models, fields, api 11 | from odoo.api import Environment 12 | from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT 13 | 14 | from ..api import AsyncDB 15 | 16 | _logger = logging.getLogger(__name__) 17 | 18 | class DateEncoder(json.JSONEncoder): 19 | 20 | def default(self, obj): 21 | if isinstance(obj,datetime.datetime): 22 | return obj.strftime("%Y-%m-%d %H:%M:%S") 23 | elif isinstance(obj, datetime.date): 24 | return obj.strftime("%Y-%m-%d") 25 | else: 26 | try: 27 | return json.JSONEncoder.default(self,obj) 28 | except: 29 | import traceback;traceback.print_exc() 30 | return str(obj) 31 | 32 | class TaskAbstract(models.AbstractModel): 33 | 34 | _name = "oe.task.abstract" 35 | 36 | task_id = fields.Char(_('task id')) 37 | task_name = fields.Char(_('task name')) 38 | task_doc = fields.Char(_('task doc')) 39 | task_args = fields.Char(_('task arguments')) 40 | task_kwargs = fields.Char(_('task kwargs')) 41 | 42 | status = fields.Selection([ 43 | ('PENDING', '待执行'), 44 | ('STARTED', '执行中'), 45 | ('SUCCESS', '完成'), 46 | ('FAILURE','执行失败'), 47 | ('REVOKED','已撤销'), 48 | ('RETRY','已重执行'), # RECEIVED 49 | ], string=_('state'), default='PENDING') 50 | 51 | priority = fields.Integer('priority') 52 | queue = fields.Char('queue') 53 | countdown = fields.Integer('countdown', default=0) 54 | 55 | 56 | class TaskTask(models.Model): 57 | 58 | _name = 'oe.task' 59 | _description = u'Task' 60 | _inherit = ['oe.task.abstract'] 61 | 62 | 63 | def run(self, tasks): 64 | for task in tasks: 65 | task_args = json.loads(task['task_args']) 66 | task_kwargs = json.loads(task['task_kwargs']) 67 | dbname = task_args.pop(0) 68 | uid = task_args.pop(0) 69 | model_name = task_args.pop(0) 70 | method = task_args.pop(0) 71 | ids = task_args.pop(0) 72 | 73 | if task['countdown']>0: 74 | _now = datetime.now() 75 | if _now < task['create_date'] + timedelta(seconds=task['countdown']): 76 | continue 77 | 78 | _context = 'context' in task_kwargs and task_kwargs.pop('context') or {} 79 | env = Environment(self.env.cr, uid, _context) 80 | Model = env[model_name] 81 | 82 | start_time = time.time() # 记录开始时间 83 | try: 84 | objs = Model.sudo().search([('id', 'in', ids)]) 85 | result = getattr(env.registry[model_name], method)(objs, *task_args, **task_kwargs) 86 | env.cr.commit() 87 | execution_time = time.time() - start_time # 计算执行时长 88 | 89 | # 序列化结果 90 | try: 91 | result_json = json.dumps(result, cls=DateEncoder) 92 | except Exception as e: 93 | result_json = json.dumps({ 94 | 'error': '执行结果无法序列化', 95 | 'detail': str(e) 96 | }) 97 | 98 | self.env['oe.task.result'].with_user(uid).sudo().search([('task_id', '=', task['id'])], limit=1).write({ 99 | 'status': 'SUCCESS', 100 | 'result': result_json, 101 | 'date_done': fields.Datetime.now(), 102 | 'execution_time': execution_time, 103 | }) 104 | except Exception as exc: 105 | execution_time = time.time() - start_time # 失败时也记录执行时长 106 | env.cr.rollback() 107 | self.env['oe.task.result'].with_user(uid).sudo().search([('task_id', '=', task['id'])], limit=1).write({ 108 | 'status': 'FAILURE', 109 | 'traceback': '{}'.format(traceback.format_exc()), 110 | 'date_done': fields.Datetime.now(), 111 | 'execution_time': execution_time, 112 | }) 113 | finally: 114 | self.delete(task) 115 | self.env.cr.commit() 116 | 117 | def delete(self,task): 118 | self.env.cr.execute('delete from oe_task where id=%s'%task['id']) 119 | 120 | def _process(self): 121 | while True: 122 | import time;time.sleep(2) 123 | db = odoo.sql_db.db_connect(self.env.cr.dbname) 124 | tasks = [] 125 | with db.cursor() as cr: 126 | cr.execute("select * from oe_task where status='PENDING'") 127 | tasks = cr.dictfetchall() 128 | self.run(tasks) 129 | break 130 | 131 | @AsyncDB() 132 | @api.model 133 | def get_count(self, a, b='second'): 134 | ''' 135 | 获取任务数量 136 | ''' 137 | _logger.info('>>> get_task_count result %s', self.search_count([])) 138 | domain = [('id', '<', 100)] 139 | return { 140 | 'name': u'产品', 141 | 'type': 'ir.actions.act_window', 142 | 'view_mode': 'tree', 143 | 'res_model': 'product.template', 144 | 'domain': domain, 145 | } 146 | 147 | @AsyncDB(countdown=10) 148 | @api.model 149 | def test_countdown(self): 150 | _logger.info('>>> It is time to do') 151 | 152 | @AsyncDB() 153 | @api.model 154 | def test_trace_log(self): 155 | d = {} 156 | v = d['k'] 157 | -------------------------------------------------------------------------------- /od13.py: -------------------------------------------------------------------------------- 1 | import logging 2 | _logger = logging.getLogger(__name__) 3 | 4 | def multi(method): 5 | method._api = 'multi' 6 | return method 7 | 8 | def model_cr(method): 9 | method._api = 'model_cr' 10 | return method 11 | 12 | from odoo import api 13 | api.multi = multi 14 | api.model_cr = model_cr 15 | try: 16 | from odoo import api 17 | api.multi = multi 18 | api.model_cr = model_cr 19 | except: 20 | import traceback;traceback.print_exc() 21 | 22 | 23 | from odoo import models 24 | origin_write = models.BaseModel.write 25 | def write(self, vals): 26 | _vals = {} 27 | for k,v in vals.items(): 28 | if k in self._fields: 29 | _vals[k] = v 30 | else: 31 | _logger.warning('>>> odoo 13 hook: model %s has no field %s', self._name, k) 32 | #vals = { k:v for k,v in vals.items() if k in self._fields} 33 | return origin_write(self, _vals) 34 | models.BaseModel.write = write 35 | 36 | origin_create = models.BaseModel.create 37 | @api.model_create_multi 38 | def create(self, vals_list): 39 | _vals_list = [] 40 | for vals in vals_list: 41 | _vals = {} 42 | for k,v in vals.items(): 43 | if k in self._fields: 44 | _vals[k] = v 45 | else: 46 | _logger.warning('>>> odoo 13 hook: model %s has no field %s', self._name, k) 47 | #vals = { k:v for k,v in vals.items() if k in self._fields} 48 | _vals_list.append(_vals) 49 | return origin_create(self, _vals_list) 50 | models.BaseModel.create = create 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sqlalchemy==1.2.5 2 | kombu==4.1.0 3 | celery==4.1.0 4 | -------------------------------------------------------------------------------- /security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | 3 | access_oe_task_system,oe_task.group_system,model_oe_task,base.group_system,1,1,1,1 4 | access_oe_task_user,oe_task.group_user,model_oe_task,base.group_user,1,0,0,0 5 | access_oe_task_result_system,oe_task_result.group_system,model_oe_task_result,base.group_system,1,1,1,1 6 | access_oe_task_result_user,oe_task_result.group_user,model_oe_task_result,base.group_user,1,0,0,0 7 | access_oe_event_group_system,oe_event.group_system,model_oe_event,base.group_system,1,1,1,1 8 | access_oe_event_subscribe_group_system,oe_event_subscribe.group_system,model_oe_event_subscribe,base.group_system,1,1,1,1 9 | access_oe_event_log_group_system,oe_event_log.group_system,model_oe_event_log,base.group_system,1,1,1,1 -------------------------------------------------------------------------------- /security/ir_rule.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Task: User can only see their own records 7 | 8 | [('create_uid', '=', user.id)] 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Task: Admin can see all records 19 | 20 | [(1, '=', 1)] 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Task Result: User can only see their own records 31 | 32 | [('create_uid', '=', user.id)] 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Task Result: Admin can see all records 43 | 44 | [(1, '=', 1)] 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from celery import Celery 5 | from kombu import Exchange, Queue 6 | from odoo.tools import config 7 | 8 | 9 | _logger = logging.getLogger('Celery Worker') 10 | 11 | 12 | 13 | app = Celery('celery_queue') 14 | 15 | 16 | class CeleryConfig(): 17 | # 默认的队列 18 | celery_default_queue = config.get('celery_default_queue', 'odoo10') 19 | # 定义的所有队列 20 | celery_queues = config.get('celery_queues', "") 21 | 22 | BROKER_URL = config.get('celery_broker_url') 23 | CELERY_DEFAULT_QUEUE = celery_default_queue 24 | CELERY_QUEUES = ( 25 | Queue(celery_default_queue, Exchange(celery_default_queue), 26 | routing_key=celery_default_queue, durable=True), 27 | ) 28 | for queue in filter(lambda q: q.strip(), celery_queues.split(",")): 29 | CELERY_QUEUES = CELERY_QUEUES + \ 30 | (Queue(queue, Exchange(queue), routing_key=queue, durable=True),) 31 | 32 | app.config_from_object(CeleryConfig) 33 | 34 | if config.get('celery_result_backend_db'): 35 | app.conf.result_backend = 'odoo.addons.task_queue.backends.ExtDatabaseBackend'#'db+postgresql://user:passwd@localhost/celery_result' 36 | app.conf.result_backend_db = config.get('celery_result_backend_db') 37 | app.conf.task_ignore_result = True 38 | app.conf.task_store_errors_even_if_ignored = True 39 | 40 | 41 | @app.task 42 | def add(x,y): 43 | return x+y 44 | 45 | 46 | import odoo 47 | from odoo.api import Environment 48 | from odoo.modules.registry import Registry 49 | 50 | @app.task(name='odoo.addons.celery_queue.tasks.execute') 51 | def execute(conf_attrs, dbname, uid, obj, method, *args, **kwargs): 52 | _logger.info(str([dbname, uid, obj, method, args, kwargs])) 53 | 54 | if conf_attrs and len(conf_attrs.keys())>1: 55 | for attr, value in conf_attrs.items(): 56 | odoo.tools.config[attr] = value 57 | with Environment.manage(): 58 | registry = Registry(dbname) 59 | cr = registry.cursor() 60 | context = 'context' in kwargs and kwargs.pop('context') or {} 61 | env = Environment(cr, uid, context) 62 | cr.autocommit(True) 63 | # odoo.api.Environment._local.environments = env 64 | try: 65 | Model = env[obj] 66 | args = list(args) 67 | _logger.info('>>> %s'%str(args)) 68 | ids = args.pop(0) 69 | if ids: 70 | target = Model.search([('id', 'in', ids)]) 71 | else: 72 | target = Model 73 | getattr(env.registry[obj], method)(target, *args, **kwargs) 74 | # Commit only when function finish 75 | # env.cr.commit() 76 | except Exception as exc: 77 | env.cr.rollback() 78 | import traceback;traceback.print_exc() 79 | raise exc 80 | #try: 81 | # raise execute.retry( 82 | # queue=execute.request.delivery_info['routing_key'], 83 | # exc=exc, countdown=(execute.request.retries + 1) * 60, 84 | # max_retries=5) 85 | #except Exception as retry_exc: 86 | # raise retry_exc 87 | finally: 88 | env.cr.close() 89 | return True 90 | -------------------------------------------------------------------------------- /views/oe_event_log_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | ir.actions.server 8 | 9 | 10 | code 11 | # Available variables: 12 | # - env: Odoo Environment on which the action is triggered 13 | # - model: Odoo Model of the record on which the action is triggered; is a void recordset 14 | # - record: record on which the action is triggered; may be void 15 | # - records: recordset of all records on which the action is triggered in multi-mode; may be void 16 | # - time, datetime, dateutil, timezone: useful Python libraries 17 | # - log: log(message, level='info'): logging function to record debug information in ir.logging table 18 | # - Warning: Warning Exception to use with raise 19 | # To return an action, assign: action = {...} 20 | log('>>>>---------------go', level='info') 21 | 22 | 23 | 24 | ir_actions_server 25 | 26 | 27 | test unlink 28 | ir.actions.server 29 | 30 | 31 | code 32 | # Available variables: 33 | # - env: Odoo Environment on which the action is triggered 34 | # - model: Odoo Model of the record on which the action is triggered; is a void recordset 35 | # - record: record on which the action is triggered; may be void 36 | # - records: recordset of all records on which the action is triggered in multi-mode; may be void 37 | # - time, datetime, dateutil, timezone: useful Python libraries 38 | # - log: log(message, level='info'): logging function to record debug information in ir.logging table 39 | # - Warning: Warning Exception to use with raise 40 | # To return an action, assign: action = {...} 41 | log('>>>>---------------go unlink', level='info') 42 | 43 | 44 | 45 | ir_actions_server 46 | 47 | 48 | test write 49 | ir.actions.server 50 | 51 | 52 | code 53 | # Available variables: 54 | # - env: Odoo Environment on which the action is triggered 55 | # - model: Odoo Model of the record on which the action is triggered; is a void recordset 56 | # - record: record on which the action is triggered; may be void 57 | # - records: recordset of all records on which the action is triggered in multi-mode; may be void 58 | # - time, datetime, dateutil, timezone: useful Python libraries 59 | # - log: log(message, level='info'): logging function to record debug information in ir.logging table 60 | # - Warning: Warning Exception to use with raise 61 | # To return an action, assign: action = {...} 62 | log('>>>>---------------%s %s %s '%(record.get_res(), record.get_old(), record.get_new()), level='info') 63 | 64 | 65 | 66 | 67 | ir_actions_server 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /views/oe_event_subscribe_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.event.subscribe.view_tree 7 | oe.event.subscribe 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | oe.event.subscribe.view_form 20 | oe.event.subscribe 21 | form 22 | 999 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | 36 |
37 |
-------------------------------------------------------------------------------- /views/oe_event_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oe.event.view_tree 7 | oe.event 8 | tree 9 | 999 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | oe.event.view_form 21 | oe.event 22 | form 23 | 999 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 | 事件 40 | oe.event 41 | form 42 | tree,form 43 | current 44 | 49 | 50 | 51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /views/oe_task_result_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | oe.task.result.search 6 | oe.task.result 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | oe.task.result.tree 36 | oe.task.result 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | oe.task.result.form 54 | oe.task.result 55 | 56 |
57 |
58 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 |
93 |
94 | 95 | 96 | 任务执行记录 97 | oe.task.result 98 | form 99 | tree,form 100 | 101 | {'search_default_today':1} 102 | 103 | 104 | 105 | 106 | 107 | oe.task.result.user.search 108 | oe.task.result 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 118 | 120 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | oe.task.result.user.tree 132 | oe.task.result 133 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 |