diff --git a/openerp-long-polling b/openerp-gevent similarity index 69% rename from openerp-long-polling rename to openerp-gevent index 304cd01c1ee..55044976492 100755 --- a/openerp-long-polling +++ b/openerp-gevent @@ -2,8 +2,8 @@ import gevent.monkey gevent.monkey.patch_all() -import gevent_psycopg2 -gevent_psycopg2.monkey_patch() +import psycogreen.gevent +psycogreen.gevent.patch_psycopg() import openerp diff --git a/openerp/__init__.py b/openerp/__init__.py index 22c8c98256a..a06c85d0c21 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -30,7 +30,6 @@ evented = False if sys.modules.get("gevent") is not None: evented = True - # Make sure the OpenERP server runs in UTC. This is especially necessary # under Windows as under Linux it seems the real import of time is # sufficiently deferred so that setting the TZ environment variable diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index d666a6abbf7..a5bc4c59484 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -20,6 +20,7 @@ ############################################################################## import ir +import workflow import module import res import report diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index f2df88506f5..808dd18d3e5 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -35,21 +35,20 @@ The kernel of OpenERP, needed for all installation. 'depends': [], 'data': [ 'base_data.xml', - 'currency_data.xml', + 'res/res_currency_data.xml', 'res/res_country_data.xml', 'security/base_security.xml', 'base_menu.xml', 'res/res_security.xml', 'res/res_config.xml', - 'data/res.country.state.csv', - 'ir/wizard/wizard_menu_view.xml', + 'res/res.country.state.csv', 'ir/ir_actions.xml', - 'ir/ir_attachment_view.xml', 'ir/ir_config_parameter_view.xml', 'ir/ir_cron_view.xml', 'ir/ir_filters.xml', 'ir/ir_mail_server_view.xml', 'ir/ir_model_view.xml', + 'ir/ir_attachment_view.xml', 'ir/ir_rule_view.xml', 'ir/ir_sequence_view.xml', 'ir/ir_translation_view.xml', @@ -57,8 +56,8 @@ The kernel of OpenERP, needed for all installation. 'ir/ir_ui_view_view.xml', 'ir/ir_values_view.xml', 'ir/osv_memory_autovacuum.xml', - 'ir/report/ir_report.xml', - 'ir/workflow/workflow_view.xml', + 'ir/ir_model_report.xml', + 'workflow/workflow_view.xml', 'module/module_view.xml', 'module/module_data.xml', 'module/module_report.xml', @@ -76,11 +75,9 @@ The kernel of OpenERP, needed for all installation. 'res/res_lang_view.xml', 'res/res_partner_report.xml', 'res/res_partner_view.xml', - 'res/res_partner_shortcut_data.xml', 'res/res_bank_view.xml', 'res/res_country_view.xml', 'res/res_currency_view.xml', - 'res/wizard/change_password_wizard_view.xml', 'res/res_users_view.xml', 'res/res_partner_data.xml', 'res/ir_property_view.xml', diff --git a/openerp/addons/base/base_menu.xml b/openerp/addons/base/base_menu.xml index 2e6a6d3a27f..19f1e8465e2 100644 --- a/openerp/addons/base/base_menu.xml +++ b/openerp/addons/base/base_menu.xml @@ -27,7 +27,6 @@ - Open Settings Menu diff --git a/openerp/addons/base/data/administration-hover.png b/openerp/addons/base/data/administration-hover.png deleted file mode 100644 index 5d10833d2e4..00000000000 Binary files a/openerp/addons/base/data/administration-hover.png and /dev/null differ diff --git a/openerp/addons/base/data/administration.png b/openerp/addons/base/data/administration.png deleted file mode 100644 index ba4e403ff33..00000000000 Binary files a/openerp/addons/base/data/administration.png and /dev/null differ diff --git a/openerp/addons/base/data/sales-hover.png b/openerp/addons/base/data/sales-hover.png deleted file mode 100644 index 01249ee3dfd..00000000000 Binary files a/openerp/addons/base/data/sales-hover.png and /dev/null differ diff --git a/openerp/addons/base/data/sales.png b/openerp/addons/base/data/sales.png deleted file mode 100644 index 0025e04b932..00000000000 Binary files a/openerp/addons/base/data/sales.png and /dev/null differ diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index c6034e0f7ee..35184f7a565 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -32,9 +32,7 @@ import ir_filters import ir_values import ir_translation import ir_exports -import workflow import ir_rule -import wizard import ir_config_parameter import osv_memory_autovacuum import ir_mail_server diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index e13602d6a77..d8198f7270a 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -54,10 +54,9 @@ class actions(osv.osv): _defaults = { 'usage': lambda *a: False, } -actions() -class report_xml(osv.osv): +class ir_actions_report_xml(osv.osv): def _report_content(self, cursor, user, ids, name, arg, context=None): res = {} @@ -174,9 +173,8 @@ class report_xml(osv.osv): 'attachment': False, } -report_xml() -class act_window(osv.osv): +class ir_actions_act_window(osv.osv): _name = 'ir.actions.act_window' _table = 'ir_act_window' _inherit = 'ir.actions.actions' @@ -291,7 +289,7 @@ class act_window(osv.osv): ids_int = isinstance(ids, (int, long)) if ids_int: ids = [ids] - results = super(act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load) + results = super(ir_actions_act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load) if not fields or 'help' in fields: context = dict(context or {}) @@ -328,8 +326,6 @@ class act_window(osv.osv): res_id = dataobj.browse(cr, uid, data_id, context).res_id return self.read(cr, uid, res_id, [], context) -act_window() - VIEW_TYPES = [ ('tree', 'Tree'), ('form', 'Form'), @@ -337,7 +333,7 @@ VIEW_TYPES = [ ('calendar', 'Calendar'), ('gantt', 'Gantt'), ('kanban', 'Kanban')] -class act_window_view(osv.osv): +class ir_actions_act_window_view(osv.osv): _name = 'ir.actions.act_window.view' _table = 'ir_act_window_view' _rec_name = 'view_id' @@ -354,33 +350,22 @@ class act_window_view(osv.osv): 'multi': False, } def _auto_init(self, cr, context=None): - super(act_window_view, self)._auto_init(cr, context) + super(ir_actions_act_window_view, self)._auto_init(cr, context) cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'') if not cr.fetchone(): cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)') -act_window_view() -class act_wizard(osv.osv): - _name = 'ir.actions.wizard' + +class ir_actions_act_window_close(osv.osv): + _name = 'ir.actions.act_window_close' _inherit = 'ir.actions.actions' - _table = 'ir_act_wizard' - _sequence = 'ir_actions_id_seq' - _order = 'name' - _columns = { - 'name': fields.char('Wizard Info', size=64, required=True, translate=True), - 'type': fields.char('Action Type', size=32, required=True), - 'wiz_name': fields.char('Wizard Name', size=64, required=True), - 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the wizard will not be displayed on the right toolbar of a form view."), - 'groups_id': fields.many2many('res.groups', 'res_groups_wizard_rel', 'uid', 'gid', 'Groups'), - 'model': fields.char('Object', size=64), - } + _table = 'ir_actions' _defaults = { - 'type': 'ir.actions.wizard', - 'multi': False, + 'type': 'ir.actions.act_window_close', } -act_wizard() -class act_url(osv.osv): + +class ir_actions_act_url(osv.osv): _name = 'ir.actions.act_url' _table = 'ir_act_url' _inherit = 'ir.actions.actions' @@ -400,55 +385,9 @@ class act_url(osv.osv): 'type': 'ir.actions.act_url', 'target': 'new' } -act_url() - -def model_get(self, cr, uid, context=None): - wkf_pool = self.pool.get('workflow') - ids = wkf_pool.search(cr, uid, []) - osvs = wkf_pool.read(cr, uid, ids, ['osv']) - - res = [] - mpool = self.pool.get('ir.model') - for osv in osvs: - model = osv.get('osv') - id = mpool.search(cr, uid, [('model','=',model)]) - name = mpool.read(cr, uid, id)[0]['name'] - res.append((model, name)) - - return res - -class ir_model_fields(osv.osv): - _inherit = 'ir.model.fields' - _rec_name = 'field_description' - _columns = { - 'complete_name': fields.char('Complete Name', size=64, select=1), - } -ir_model_fields() - -class server_object_lines(osv.osv): - _name = 'ir.server.object.lines' - _sequence = 'ir_actions_id_seq' - _columns = { - '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', 'Python expression') - ], 'Evaluation Type', required=True, change_default=True), - } - _defaults = { - 'type': 'value', - } -## -# Actions that are run on the server side -# -class actions_server(osv.osv): +class ir_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 @@ -1029,16 +968,26 @@ class actions_server(osv.osv): return res -class act_window_close(osv.osv): - _name = 'ir.actions.act_window_close' - _inherit = 'ir.actions.actions' - _table = 'ir_actions' - _defaults = { - 'type': 'ir.actions.act_window_close', +class ir_server_object_lines(osv.osv): + _name = 'ir.server.object.lines' + _sequence = 'ir_actions_id_seq' + _columns = { + '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', 'Python expression') + ], 'Evaluation Type', required=True, change_default=True), + } + _defaults = { + 'type': 'value', } -act_window_close() -# This model use to register action services. + TODO_STATES = [('open', 'To Do'), ('done', 'Done')] TODO_TYPES = [('manual', 'Launch Manually'),('once', 'Launch Manually Once'), @@ -1139,9 +1088,8 @@ Launch Manually Once: after having been launched manually, it sets automatically 'todo': len(total) - len(done) } -ir_actions_todo() -class act_client(osv.osv): +class ir_actions_act_client(osv.osv): _name = 'ir.actions.client' _inherit = 'ir.actions.actions' _table = 'ir_act_client' @@ -1182,6 +1130,8 @@ class act_client(osv.osv): 'context': '{}', } -act_client() + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml index db5beff5e0f..843dc5df35b 100644 --- a/openerp/addons/base/ir/ir_actions.xml +++ b/openerp/addons/base/ir/ir_actions.xml @@ -1,7 +1,9 @@ - + + + ir.actions.actions ir.actions.actions @@ -34,7 +36,6 @@ - Actions ir.actions.act_window @@ -46,7 +47,7 @@ - + ir.actions.report.xml @@ -121,7 +122,6 @@ - Reports ir.actions.act_window @@ -132,6 +132,8 @@ + + ir.actions.windows.tree ir.actions.act_window @@ -145,7 +147,6 @@ - ir.actions.windows.form ir.actions.act_window @@ -221,7 +222,6 @@ - Window Actions ir.actions.act_window @@ -243,66 +243,7 @@ - - ir.actions.wizard.tree - ir.actions.wizard - - - - - - - - - - - ir.actions.wizard - ir.actions.wizard - -
- - - - - - -
- - - ir.actions.wizard.search - ir.actions.wizard - - - - - - - - - Wizards - ir.actions.act_window - ir.actions.wizard - form - - - - - - - - Property multi-company - - - ['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)] - - - + Server Action @@ -514,7 +455,6 @@ - Server Actions ir.actions.server @@ -527,7 +467,6 @@ - ir.actions.server.search ir.actions.server @@ -542,7 +481,6 @@ - Server Actions ir.actions.act_window @@ -573,7 +511,6 @@ - ir.actions.todo Config Wizard Steps @@ -602,7 +539,6 @@ - ir.actions.todo ir.actions.todo.select @@ -614,7 +550,6 @@ - Configuration Wizards ir.actions.todo @@ -622,9 +557,7 @@ form The configuration wizards are used to help you configure a new instance of OpenERP. They are launched during the installation of new modules, but you can choose to restart some wizards manually from this menu. - - - + Run Remaining Action Todo True @@ -638,6 +571,5 @@ if config.get('type') not in ('ir.actions.act_window_close',): -
diff --git a/openerp/addons/base/ir/ir_attachment_view.xml b/openerp/addons/base/ir/ir_attachment_view.xml index 7b224f71b61..02ccfe73a25 100644 --- a/openerp/addons/base/ir/ir_attachment_view.xml +++ b/openerp/addons/base/ir/ir_attachment_view.xml @@ -89,7 +89,7 @@
- + diff --git a/openerp/addons/base/ir/ir_cron_view.xml b/openerp/addons/base/ir/ir_cron_view.xml index edbc2d74b86..63c8994b48b 100644 --- a/openerp/addons/base/ir/ir_cron_view.xml +++ b/openerp/addons/base/ir/ir_cron_view.xml @@ -86,8 +86,7 @@ {'active_test': False} - - + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 3b709934a27..396de27903e 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -219,9 +219,11 @@ class ir_model(osv.osv): class ir_model_fields(osv.osv): _name = 'ir.model.fields' _description = "Fields" + _rec_name = 'field_description' _columns = { 'name': fields.char('Name', required=True, size=64, select=1), + 'complete_name': fields.char('Complete Name', size=64, select=1), 'model': fields.char('Object Name', size=64, required=True, select=1, help="The technical name of the model this field belongs to"), 'relation': fields.char('Object Relation', size=64, @@ -1134,4 +1136,32 @@ class ir_model_data(osv.osv): _logger.info('Deleting %s@%s', res_id, model) self.pool[model].unlink(cr, uid, [res_id]) +class wizard_model_menu(osv.osv_memory): + _name = 'wizard.ir.model.menu.create' + _columns = { + 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True), + 'name': fields.char('Menu Name', size=64, required=True), + } + + def menu_create(self, cr, uid, ids, context=None): + if not context: + context = {} + model_pool = self.pool.get('ir.model') + for menu in self.browse(cr, uid, ids, context): + model = model_pool.browse(cr, uid, context.get('model_id'), context=context) + val = { + 'name': menu.name, + 'res_model': model.model, + 'view_type': 'form', + 'view_mode': 'tree,form' + } + action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val) + self.pool.get('ir.ui.menu').create(cr, uid, { + 'name': menu.name, + 'parent_id': menu.menu_id.id, + 'action': 'ir.actions.act_window,%d' % (action_id,), + 'icon': 'STOCK_INDENT' + }, context) + return {'type':'ir.actions.act_window_close'} + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/report/modeloverview.sxw b/openerp/addons/base/ir/ir_model_report.sxw similarity index 100% rename from openerp/addons/base/ir/report/modeloverview.sxw rename to openerp/addons/base/ir/ir_model_report.sxw diff --git a/openerp/addons/base/ir/report/ir_report.xml b/openerp/addons/base/ir/ir_model_report.xml similarity index 84% rename from openerp/addons/base/ir/report/ir_report.xml rename to openerp/addons/base/ir/ir_model_report.xml index 3d077f453c8..9fcf97b6c49 100644 --- a/openerp/addons/base/ir/report/ir_report.xml +++ b/openerp/addons/base/ir/ir_model_report.xml @@ -7,7 +7,7 @@ model="ir.model" name="ir.model.overview" report_type="sxw" - file="base/ir/report/modeloverview.sxw" + file="base/ir/ir_model_report.sxw" header="False" /> diff --git a/openerp/addons/base/ir/ir_model_view.xml b/openerp/addons/base/ir/ir_model_view.xml index 537c046f95d..fb68df9b5ff 100644 --- a/openerp/addons/base/ir/ir_model_view.xml +++ b/openerp/addons/base/ir/ir_model_view.xml @@ -1,6 +1,27 @@ + + + + Create Menu + wizard.ir.model.menu.create + +
+ + + + +
+
+
+
+
+ + ir.model diff --git a/openerp/addons/base/ir/ir_rule_view.xml b/openerp/addons/base/ir/ir_rule_view.xml index 9448af058ea..6a1e3aead43 100644 --- a/openerp/addons/base/ir/ir_rule_view.xml +++ b/openerp/addons/base/ir/ir_rule_view.xml @@ -85,5 +85,15 @@ + + + + Property multi-company + + + ['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)] + + +
diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index c23d0e08be8..f01ce54bfc2 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -32,14 +32,6 @@ from openerp import SUPERUSER_ID MENU_ITEM_SEPARATOR = "/" -def one_in(setA, setB): - """Check the presence of an element of setA in setB - """ - for x in setA: - if x in setB: - return True - return False - class ir_ui_menu(osv.osv): _name = 'ir.ui.menu' diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 353ed4037a4..528865c9d42 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -868,45 +868,3 @@ class view(osv.osv): ids = map(itemgetter(0), cr.fetchall()) return self._check_xml(cr, uid, ids) - - -MOVABLE_BRANDING = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath'] - -class view_sc(osv.osv): - _name = 'ir.ui.view_sc' - _columns = { - 'name': fields.char('Shortcut Name', size=64), # Kept for backwards compatibility only - resource name used instead (translatable) - 'res_id': fields.integer('Resource Ref.', help="Reference of the target resource, whose model/table depends on the 'Resource Name' field."), - 'sequence': fields.integer('Sequence'), - 'user_id': fields.many2one('res.users', 'User Ref.', required=True, ondelete='cascade', select=True), - 'resource': fields.char('Resource Name', size=64, required=True, select=True) - } - - def _auto_init(self, cr, context=None): - super(view_sc, self)._auto_init(cr, context) - cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'') - if not cr.fetchone(): - cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)') - - def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None): - ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context) - results = self.read(cr, uid, ids, ['res_id'], context=context) - name_map = dict(self.pool[model].name_get(cr, uid, [x['res_id'] for x in results], context=context)) - # Make sure to return only shortcuts pointing to exisintg menu items. - filtered_results = filter(lambda result: result['res_id'] in name_map, results) - for result in filtered_results: - result.update(name=name_map[result['res_id']]) - return filtered_results - - _order = 'sequence,name' - _defaults = { - 'resource': 'ir.ui.menu', - 'user_id': lambda obj, cr, uid, context: uid, - } - _sql_constraints = [ - ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'), - ] - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/ir/ir_ui_view_view.xml b/openerp/addons/base/ir/ir_ui_view_view.xml index 1c59aae0170..0a59bd36df7 100644 --- a/openerp/addons/base/ir/ir_ui_view_view.xml +++ b/openerp/addons/base/ir/ir_ui_view_view.xml @@ -115,27 +115,5 @@ - - - ir.ui.view_sc - -
- - - - -
-
-
- - ir.ui.view_sc - - - - - - - - diff --git a/openerp/addons/base/ir/wizard/__init__.py b/openerp/addons/base/ir/wizard/__init__.py deleted file mode 100644 index 23cc77e8c15..00000000000 --- a/openerp/addons/base/ir/wizard/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import wizard_menu -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/ir/wizard/wizard_menu.py b/openerp/addons/base/ir/wizard/wizard_menu.py deleted file mode 100644 index a374b3dad2d..00000000000 --- a/openerp/addons/base/ir/wizard/wizard_menu.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp.osv import fields, osv - -class wizard_model_menu(osv.osv_memory): - _name = 'wizard.ir.model.menu.create' - _columns = { - 'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True), - 'name': fields.char('Menu Name', size=64, required=True), - } - - def menu_create(self, cr, uid, ids, context=None): - if not context: - context = {} - model_pool = self.pool.get('ir.model') - for menu in self.browse(cr, uid, ids, context): - model = model_pool.browse(cr, uid, context.get('model_id'), context=context) - val = { - 'name': menu.name, - 'res_model': model.model, - 'view_type': 'form', - 'view_mode': 'tree,form' - } - action_id = self.pool.get('ir.actions.act_window').create(cr, uid, val) - self.pool.get('ir.ui.menu').create(cr, uid, { - 'name': menu.name, - 'parent_id': menu.menu_id.id, - 'action': 'ir.actions.act_window,%d' % (action_id,), - 'icon': 'STOCK_INDENT' - }, context) - return {'type':'ir.actions.act_window_close'} - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/ir/wizard/wizard_menu_view.xml b/openerp/addons/base/ir/wizard/wizard_menu_view.xml deleted file mode 100644 index 7fd32b256e2..00000000000 --- a/openerp/addons/base/ir/wizard/wizard_menu_view.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Create Menu - wizard.ir.model.menu.create - -
- - - - -
-
-
-
-
- -
-
diff --git a/openerp/addons/base/ir/workflow/__init__.py b/openerp/addons/base/ir/workflow/__init__.py deleted file mode 100644 index daa5bb1d29a..00000000000 --- a/openerp/addons/base/ir/workflow/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import workflow -import print_instance - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index e160c49481e..6279a5fe6ae 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -716,7 +716,7 @@ class module(osv.osv): if already_installed: # in this case, force server restart to reload python code... cr.commit() - openerp.service.restart_server() + openerp.service.server.restart() return { 'type': 'ir.actions.client', 'tag': 'home', diff --git a/openerp/addons/base/res/__init__.py b/openerp/addons/base/res/__init__.py index 59403545112..50807e20116 100644 --- a/openerp/addons/base/res/__init__.py +++ b/openerp/addons/base/res/__init__.py @@ -31,8 +31,5 @@ import res_request import res_lang import ir_property -import report -import wizard - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/res/config_pixmaps/1.png b/openerp/addons/base/res/config_pixmaps/1.png deleted file mode 100755 index 63bd4337369..00000000000 Binary files a/openerp/addons/base/res/config_pixmaps/1.png and /dev/null differ diff --git a/openerp/addons/base/res/config_pixmaps/2.png b/openerp/addons/base/res/config_pixmaps/2.png deleted file mode 100755 index 80d4e3602bb..00000000000 Binary files a/openerp/addons/base/res/config_pixmaps/2.png and /dev/null differ diff --git a/openerp/addons/base/res/config_pixmaps/3.png b/openerp/addons/base/res/config_pixmaps/3.png deleted file mode 100755 index 63acab34263..00000000000 Binary files a/openerp/addons/base/res/config_pixmaps/3.png and /dev/null differ diff --git a/openerp/addons/base/res/report/__init__.py b/openerp/addons/base/res/report/__init__.py deleted file mode 100644 index 0a7dcc8620f..00000000000 --- a/openerp/addons/base/res/report/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -#from report import report_sxw -#report_sxw.report_sxw('report.partner.list', 'res.partner', 'addons/base/res/partner/report/partner_list.rml') - - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/res/report/business_card.xml b/openerp/addons/base/res/report/business_card.xml deleted file mode 100644 index c4695ffa020..00000000000 --- a/openerp/addons/base/res/report/business_card.xml +++ /dev/null @@ -1,15 +0,0 @@ - - -
- - - - - - Tiny sprl - Rue du Vieux Chateau, 21 - BE-1457 - Walhain - http://tiny.be -
-
diff --git a/openerp/addons/base/res/report/business_card.xsl b/openerp/addons/base/res/report/business_card.xsl deleted file mode 100644 index f274d9437f6..00000000000 --- a/openerp/addons/base/res/report/business_card.xsl +++ /dev/null @@ -1,80 +0,0 @@ - - - -20.5 -0.5 -5.5 -8.5 -5.5cm -8.5cm -3 -8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - cm - - - - cm - - - - - - - - PACKINGLIST - - - - Logo - - - - - - - - - - - - - - - - - - diff --git a/openerp/addons/base/data/res.country.state.csv b/openerp/addons/base/res/res.country.state.csv similarity index 100% rename from openerp/addons/base/data/res.country.state.csv rename to openerp/addons/base/res/res.country.state.csv diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index abfe8df7268..c59f56d9fa6 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -131,6 +131,7 @@ class res_config_configurable(osv.osv_memory): """ raise NotImplementedError( 'Configuration items need to implement execute') + def cancel(self, cr, uid, ids, context=None): """ Method called when the user click on the ``Skip`` button. @@ -184,8 +185,6 @@ class res_config_configurable(osv.osv_memory): if next: return next return self.next(cr, uid, ids, context=context) -res_config_configurable() - class res_config_installer(osv.osv_memory, res_config_module_installation_mixin): """ New-style configuration base specialized for addons selection and installation. @@ -314,7 +313,6 @@ class res_config_installer(osv.osv_memory, res_config_module_installation_mixin) context=context), context=context) - def modules_to_install(self, cr, uid, ids, context=None): """ selects all modules to install: @@ -397,45 +395,6 @@ class res_config_installer(osv.osv_memory, res_config_module_installation_mixin) return self._install_modules(cr, uid, modules, context=context) -res_config_installer() - -DEPRECATION_MESSAGE = 'You are using an addon using old-style configuration '\ - 'wizards (ir.actions.configuration.wizard). Old-style configuration '\ - 'wizards have been deprecated.\n'\ - 'The addon should be migrated to res.config objects.' -class ir_actions_configuration_wizard(osv.osv_memory): - ''' Compatibility configuration wizard - - The old configuration wizard has been replaced by res.config, but in order - not to break existing but not-yet-migrated addons, the old wizard was - reintegrated and gutted. - ''' - _name='ir.actions.configuration.wizard' - _inherit = 'res.config' - - def _next_action_note(self, cr, uid, ids, context=None): - next = self._next_action(cr, uid) - if next: - # if the next one is also an old-style extension, you never know... - if next.note: - return next.note - return _("Click 'Continue' to configure the next addon...") - return _("Your database is now fully configured.\n\n"\ - "Click 'Continue' and enjoy your OpenERP experience...") - - _columns = { - 'note': fields.text('Next Wizard', readonly=True), - } - _defaults = { - 'note': _next_action_note, - } - - def execute(self, cr, uid, ids, context=None): - _logger.warning(DEPRECATION_MESSAGE) - -ir_actions_configuration_wizard() - - class res_config_settings(osv.osv_memory, res_config_module_installation_mixin): """ Base configuration wizard for application settings. It provides support for setting default values, assigning groups to employee users, and installing modules. @@ -694,4 +653,5 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin): if (action_id): return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel')) return exceptions.Warning(msg % values) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/res/res_config.xml b/openerp/addons/base/res/res_config.xml index 342aeaa6e0f..066f9435434 100644 --- a/openerp/addons/base/res/res_config.xml +++ b/openerp/addons/base/res/res_config.xml @@ -17,23 +17,6 @@
- - Compabitiliby configuration wizard - ir.actions.configuration.wizard - -
- - - -
-
-
-
-
- Inheritable view for installer objects res.config.installer diff --git a/openerp/addons/base/currency_data.xml b/openerp/addons/base/res/res_currency_data.xml similarity index 100% rename from openerp/addons/base/currency_data.xml rename to openerp/addons/base/res/res_currency_data.xml diff --git a/openerp/addons/base/res/res_partner_demo.xml b/openerp/addons/base/res/res_partner_demo.xml index a79623c5917..723d2c0972d 100644 --- a/openerp/addons/base/res/res_partner_demo.xml +++ b/openerp/addons/base/res/res_partner_demo.xml @@ -410,15 +410,7 @@ vauxoo.com /9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCABUAN0DASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBQgBAwQCCf/EAEIQAAEDAwMDAQUGAgYJBQAAAAECAwQABREGBxITITFBCCIyUWEUFUJxgZEjsTM4UmKCoRYXQ3J1tMHR4WODhJKj/8QAGwEBAAIDAQEAAAAAAAAAAAAAAAEDAgQGBQf/xAAsEQACAgEDAgQGAgMAAAAAAAAAAQIDEQQhMQUSE0FRgQYiMmGRoXHRFLHB/9oADAMBAAIRAxEAPwDculKUApSlAKUrF6tvsHTOmrhfrivjGhMqdWMgFZ/CgZ/EokJH1IoZQi5yUYrdnru0+JarXKuc94MxYjKnnnD+FCQST+wrR3V26mtb3qWfdIupb3bY0h4qZixp7jbbLfhKQEkDIAGTjucn1ru1TvFr3UkCVbbpdWXLfJXyXFENoIwFBSU548iAQPJ9O+aiCbvIT4jW79YLR/mmtSy3u4PoPRuhvRKUrkpSf6XujN2nU+u71do1ub1dqFbkhwJyq4PqCR6qICs4AyT9Aa3B2hvBlWU2h9911+CAG1vOFa1tehKicqIPYn6itdtlbc9dIsy8TY0FCQehEU3DZaUF4ypXJKQoDulPyIKvlVmaImybfqmCuOhSlLdDK2x+JKjgj/r+Yq6iPy5Zz/xJqoz1CpgklHnHq/6/svWlKVac4KUpQClKUApSlAKUpQClKUApSlAKUpQClKUApXDi0NoU44pKEJBKlKOAAPU1VupL/JuN0W9GfeZYR7rSUrKe3zOPU0BaZIAJJAA7kmvltaHEBba0rSfBScitX979c3K1ac+4WblMMq6NkLSXl4RHyQo+ce8QUeowF+Dg1r6kKKgE5yfGKpnd2vB0vTPhyeto8aU+3PG2dvXlH6H6v1BbtLabnX+7LUiHDb5r4DKlHOEpSDgFSiQBkgZI7itcd3d3NHbi2SLYkT9RWOGl/rSCbY291iB7gPGQn3QSTjByeJ7ce9e7jLOmdPW/b5pzMtkpn3xQUDmWtPuM5GezSDjsSCpSj6VX9VWWt7Ht9J6BTWlc5NvPyvjbyeHnnlexPHdLberjhyHuk0HT/spdikt4/VHMV12bb+JeZzcO27gaRcfecDbSHXJTKlqJwAObA7k1B6tz2btNmXe5OpZDZ6MEFmMSDhTyh7xB8Hig9x/6iT6VhFdzxg9TXWT0Wmnc7W8eqjz5LZItG0WVOnLZHsSU8fsSOkr6qB95X6qJP61Ldt7QiRqR67LwUx28hOe/UVkZ/YE/mfpXTqBn7TDbuKRlxrDUj6j8C/290/kPnUi2zgFqC/cFggvq4I/3U+T+/b9K31sj5bOcrJOct29yX0qKbvX++aW21vupNOW6NcbhbIplpjSFFKFtoIU73BByGwtQA8kAetYX2ddy2919so2qFRmYk5L7sWdGaUVIadQcgAnvgoUhX05Y9KgxLFpVZ+0lukjaXbhWomorM24vym4sGM6ohDi1ZUoqwcgBCVnI9eI9alG193vmoNvbJfdRwI9vudwiJkvRmOXFoL95Cfe754FOQfBzQEkpVJwt9LfI9qOTtUlxkwEQeg3IBBzcU5cW3yB8dMlGD3C0EY71dlAKVTOzG8F211u/r3RU60wYsXTMt5iO+ypRW8ESFtAqBOM4SD29anm7up5Oi9s7/qqHGZlSLZDVIbZdJCFkY7HHfFASqlVftjr3VOvvZ9ja7tNptqdRS48pyNAUpXQccafcbS3yKgRzDYGScAqz4GKx3sw70I3csNyFxgsWrUFrkcJcJtSsdNWeDieXfyFJI9Cn0yKAuGlUTbN7r1qr2jZG2eirLAmWa1E/e91fUvLfTOHuABH4yloefeyrumrC3V3HsO3tqRIuZXImvg/ZYTRHUdx6kn4Uj1Uf0BPaobSWWW002XzVday2TSlUC1uZvhcmDc7TtpGRbiOaEvsul1ST3BGVpKu3qE96le1e8cLVV4Xpq/Wt7T+omyU/ZXyeLqgO4TyAKVDueJHjwT3xirEzet6Tqa4OezxzhptfykWnSuqbKjQob0yY+3HjMNqcddcUEpQkDJUSfAA9aoy474aj1Fdn7ftdox68tR1cVzZLayg5zg8UlPAHBIKlAn5DFTKSjya+l0N2qy61suW9kvdl8Uqg0b0660tOZTuVoJUKC6sI+1wkLSEnz25KUlZ+nIHzV32K626+WiNd7TLblwZSOoy834UP17gg5BB7ggg4IpGalwTqtBdpUpTWz4aeV+URncS9dNH3RGWQtQCpCgfA9E/r5P0x86ru6T4lqtkq5z1lEWK2XHSnHIjwEpz25EkJH1IqxteWZ4pVdYeV8Rl9ojkAP7YB8fX9/nUCm22z6hifdWoITUmItxLiBzUyEuAEAkoKe2FEd/GfzrJ5xsa9Ph+IvFz2+eOcGrOpbxKv99l3eZwD0lfLigYShIACUj6BIA79+3fJqX7Tw49pYn7i3VpK4diKRAaX4lXBXdlA7jIR/SKwcgJHY5q2J2zOiXnFJEa4w8HBSxKOQf8A3AqshqPaKBfNG2ax2m+ybXbLVzK2jHD6nH3DkvOEKRyJGAO3YZA81q+FJPJ3U/iHQ2VqmLcY8PbiPosZ54/Zq9PlyZ85+dMeW/JkOKdedWcqWtRypR+pJJroq6Z3s93tLpTA1DbXkeipDbjX+SQusFcdj9eRXunHjwJ4/tsS0pT/APpxNVuuXoexX1nQT2jal+v94K2ZbcedQ00hS3FqCUpSMlRPgCtutBafa0xpSDZ0BHVab5SFpwebqu6zkAchk4BPfiEj0qq9p9rtQ2nWzVy1PazEYt6esz/FQ4lx78GChRHu91efKR86vGrqIY3ZyvxR1KF7jRVLMVu2t1ny/H/TtjLbStSHklTDqS26keSk/L6jyPqBU+trDMW3x48chTTbaUpUMe8Mee3z81AIzK35DbDY99agkfrVhxmksR22UZ4tpCRn5Ctg5JH04hDjam3EJWhQKVJUMgg+QRWoXs3le0PtS6t2ilKcRabyTItJWc5KQXWe/Yd2VLSogd1tgVt/WrHt5aduFpVpTePTyQi6acmtMvucOQCOp1GFK/upcykj16woSYfelP8Arq9r7T22zWH9P6VR17pjipBPuuPgkHODhhjB7pVy7ea2P3n1vF262yverpAQtcGOfsrSwSHZCjxaQQCDgrKc48JyfSqV9gnSsxWl79upflfaLxquc6UPqCMqaQ4rqLGBlJW8XMjx/DQcVFvbDnXndbeHTmxekXo/VjEzJy3l8WkyC0pSeZCSQG2eR7Zz1sYyBQFWyNrtT2jYW2b/ACZcw6qF8F2dddJK/sy1jpvKB+JReAcz6pdyfFb5bZatg670BZdXW4BLFzipeLYUT0nPhcbyQMlCwpOcd+Na0S9kPablaZc0y/ujpxVmcifYlQgtxLXQ4cOmAI3Ycewx49K9HsRXq66K1nq3YvVSkon26QuZA7nivGEuhBUQSlSek6gBIyC4o+aA59kz+tLvV/xOX/zztXR7UP8AV81t/wAKc/6VQG0t9gbXe2VuLbNazWLNH1A9Ikw5UpYbZV1H+u1lZ90AoUoZJxyTx89qs/2yNxdK2bZS+2L77gSLxeI6I0SEy+lbqkrUMuFIJKUBIUeR7E4HkigMn7Ev9WLSP/zf+dfqhfaxiXfZHehO4ug7izAXq2FLalMBXvIeUkB1zjkEgqU26k9wHEkntgHYT2X4TuifZj03/pKRbkxIEi4SVPHAZZcddkBSvlhtYJHkdwe9UDofST/tU7p6r13qQyoek4LDlssyBlKkLKD0uwOOSAvrLGcFa0jukkUBdvsabbwtBbPwLiVNP3bUbLVymPtqChwWnky0kgD3UoVk+feWvBIIrA7exEa59prU95vaRJasDjjcNpZylCm3Ok2eJ9AAtf0WQawvsWawumm7xe9htaH7PeLFIdXbOfuh1rPJxtGQCpPfqoPcqQ4T2SkVk7zJf2c9oGZqKfHcc03qQuFb6ElRQVqC1nH9pLgzjv7iu3fxXZ5N8HtdHTkrq6/rcdvzul92jZKqJ9ryxMN6dtetIIMa8W6a20JLRCV9M5Ke4GSUrSkp79sq+dW5bdW6XuNqN1hahtb0JKeS3hKQEtjGfeyfdP0OCKojfLVSN0r7att9CqbuQ+19eXNRktJKQU9lDsUJClKUrBB93jk9isa7R0Wm6GsjPDSj9TfCXnk9XtKaumz9mdKBs9A6jQzJlJT4KQ0lwo/LmtB/w1dmhtMWzR+mIdhtTSUMx0DmvjhTzmPecV/eUe/07AdgBVc+0Ft4/ddpbfb9PMOPyNOpbMdlI5OOsob4KSn5qwEqx3J44AJIrJbO7u6f1hYYrFzuUWBf20Jbkx5DiW+uvHxtZwFBWCeI7p8HtgmFtPcsvi7unx/x18sZS7kvv9LfttksK+2m3X2zyrRdojcuDKbLbzSx2UP5gg4II7ggEYIrXz2e9Qv6Lnav0fPcXNjWu5dNgp7AKCnELIz4B6aTj8/nVrbl7paW0TaJDz1xizbmlOGLcw8lTq1ke7yAzwT81H0HbJwDCPZm0ZLXYbpq7VLCnZeoZCZLaXgQooBWeqfHxqWo/kEkeaS3msDSRdWgtlevkk44Xq88r25ZeNQXUmjpC5q5FpQ2Wl+8WirjxP09MVOqVaeCV8zars1DP2yC4hbI+MKCgpP6E9x/L8u/FvlKiSOpx5oIKXEE/Ek+RVhV1PRo7/8ATMNOf76AaEYItIbSgpW2rmy4OTavmP8AuPBrqqTO2yKqKWGkBocuQI/CfyrGSbO60kr67XAeqspqMEmMISpJQsZSoYNYp5tTTim1eR/n9azphSePJLfUT4y2Qr+VeG4RllP9GoOo/CU9yP8Ax/3oiGeQOmDa5VwyQ4R0GD3HvqHcgj5Jz+4qTaFuX3hY0IWrL0b+Ev5kD4T+3b8wahOrHgmSzbUEFMROFkeriu6v27D9Kyu2PFNwkcnwkuNYQ1n48EZP6ZH/ANj8jWRJP6i27dr07fNtr9Z9V3CHbbPNhqYfmS1oQ3GKuyHcrISClfFQyfiAqU1DdyrXdH5+mr7bbSb4LJcFyXbYl1tDjoWy40HGi4Ut9VBXkBakjiV4PLiDAMrt8xp6JoizQdKTY06xxIbcaFIjvpeQ422OAPNPZR93uR65rAaJ2n0hpHXd61vbGpzt+vQcEuTJlKcyFuBxQSnwAVJT6dgkAYFRXUidTWPR2qdTRrfN09Mv+o7UWLdFejqloSuRDiuKUrKmA+6OQ+JSAOBJzyw1LC3fe21tTFofu0W5/fEgyBmG/cEQCH+gFkvNMrWkljlxd8AZLmFBYFzVW+r9ttvWtw2N37xI+5btbEI6lwMxMZgpSCjLxV7pyhXTJJ+HA9BXxfbZuSvVFtlxLrJVFiMWhL6GFMtMSnDKWLgpTasqADBSQAfUceShUi3Tt8i56PdixrLLu6xKjPBiHMRGkoLbyHEvMrcIQXG1ISsJWUpVxwTg4IGH3L2y273isMR++RGLigs84F1gvgOoQoZCm3U5C0nOQDySfOKhm3vsqbT6PvSLuYtzv8llQWwLu+h1tpQ9QhCEJV/jCh2BGDWWvMPdFOidPNtM3VUxESWJjFofgsSkySU/Y1ulzDBSlHPqhrILhTxSpGRWYvCdyhrS2MR2lO2x9i2GZKjOMpjR3WnX1TQULV1SHUKZSnilXw9ynBJAku4uj7VrzSUrS98clptsso+0IjO9NTgSoKCSrBIHIJPbGcY8ZB+9v9I2LQuk4WmNNxDFtsMK6aVLKlKKlFSlKUe5JJJ/8YFROda9wGtJ6jksXG7P3eVeeUaM2/GCmrcmbkojlQCEuLilWC4rIUU5Kcdsdq2Bum/e9Gq05Mu0G0oZbFxQ+3EkPtudVsqMo9dsKT0+SSWer354GeCgBlNb7XaBm67ibp3hx20XezoQ4q4tTPszYQ1k8nj8KhxJSSryj3T2GKl0hjTWttMcFm3X6yzkZQttxLzLo7jkhaSRkHOFJOQR2INeXcy2ru2iZ8Buzu3hSy0oRGpgjOr4uoVybcOAlxPHmjkQCpKQSkEkVtfJOurZpbRtsmJ1D94XG/TWno9udgtXF6N0ZjzIcXyTG6oCG1uFKgCQrHInBExk4vKeGd8/2btvpM5Uhl++Q2lHIjsykFtP0BWhSv3UasDSOkNJaCtcgWW3xraxxLkqQ4slSgkZJW4sk8QMnGcDv2GTUDvdu3oXddCqau5DTUKIm+qhR47japYcR9pW6FutHpKRyCemlw/HhKTwVWbmRdcO3/WqFNXZ1p6C8mwuJfifYADHaCGygnq9frdU8lpKOJ+LwkYqEVukbd3UNVfDsssbX8liMutvMoeZcQ404kKQtCspUD3BBHkVVettstrNYXea7JUi23VE9MSU9CfDC1yVtpeDZSoFClqQ4leQkqOfPmudUxNyhqCy/dabuuGmNbEKEKRDbYZcTJJnGSHCFrCmOAR0gruF/DkE5BNi1PH1XdpkSKExZ2sY85Sits84SbVHYWvBOR/GaKcdldsgYOTLSfJTRqLdPLuqk0/seTSGxG3unZaZhgybvIQrk2q5OhxKOxHwJSlCvP4knB7irQqnrxp/cC/WrU9muAuZYXqm3TLVKcejNupit3Fp1zpJQtYCWmmwpKl8Vr8FsKBK7hoopcE6jVXaiXdbJyf3FKUqSgUpSgFcLSlaChQylQwR865pQEWuMRUOSUjPA90K+Yr5bmy2yCmQ529Ccj/OpS4hDieK0JUPkRmofdzJt81bTrTTqFd2llPHI/w4GajAIlfYq489alFSkukrSo+uT3H55rFN3R+BqWNJiZWqIsNpSCDz7++MjyCSoA/IipbdHkyYRDUdSZKAVNLDg7Lx28jxnHk1H9s7UZ+qmluIPSh/xl5H4h8I/Plg/oayRBc48eMUpSoJBAPkZpSlAKUpQClKUApSlAKEA47eKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBXVIjsSAA+y26B45pBxSlAY9/T9sdzhpTRPqhZ/kcivjTloh2xya5GSrm+6C4pWO+B28D6k/mTSlCDMUpShIpSlAKUpQClKUApSlAKUpQClKUApSlAKUpQH/9k= - - - ASUSTEK-Subject - - - diff --git a/openerp/addons/base/res/res_partner_report.xml b/openerp/addons/base/res/res_partner_report.xml index 80ab161e62c..00b8a84608e 100644 --- a/openerp/addons/base/res/res_partner_report.xml +++ b/openerp/addons/base/res/res_partner_report.xml @@ -1,9 +1,13 @@ - - + diff --git a/openerp/addons/base/res/report/partner_address.xml b/openerp/addons/base/res/res_partner_report_address.xml similarity index 100% rename from openerp/addons/base/res/report/partner_address.xml rename to openerp/addons/base/res/res_partner_report_address.xml diff --git a/openerp/addons/base/res/report/partner_address.xsl b/openerp/addons/base/res/res_partner_report_address.xsl similarity index 100% rename from openerp/addons/base/res/report/partner_address.xsl rename to openerp/addons/base/res/res_partner_report_address.xsl diff --git a/openerp/addons/base/res/res_partner_shortcut_data.xml b/openerp/addons/base/res/res_partner_shortcut_data.xml deleted file mode 100644 index ee534a2ff6b..00000000000 --- a/openerp/addons/base/res/res_partner_shortcut_data.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Customers - ir.ui.menu - - - - - - diff --git a/openerp/addons/base/res/res_request.py b/openerp/addons/base/res/res_request.py index e91ebacbe2a..a565f3c7844 100644 --- a/openerp/addons/base/res/res_request.py +++ b/openerp/addons/base/res/res_request.py @@ -19,74 +19,14 @@ # ############################################################################## -import time - from openerp.osv import osv, fields -def _links_get(self, cr, uid, context=None): +def referencable_models(self, cr, uid, context=None): obj = self.pool.get('res.request.link') ids = obj.search(cr, uid, [], context=context) res = obj.read(cr, uid, ids, ['object', 'name'], context) return [(r['object'], r['name']) for r in res] -class res_request(osv.osv): - _name = 'res.request' - - def request_send(self, cr, uid, ids, *args): - for id in ids: - cr.execute('update res_request set state=%s,date_sent=%s where id=%s', ('waiting', time.strftime('%Y-%m-%d %H:%M:%S'), id)) - cr.execute('select act_from,act_to,body,date_sent from res_request where id=%s', (id,)) - values = cr.dictfetchone() - if values['body'] and (len(values['body']) > 128): - values['name'] = values['body'][:125] + '...' - else: - values['name'] = values['body'] or '/' - values['req_id'] = id - self.pool.get('res.request.history').create(cr, uid, values) - return True - - def request_reply(self, cr, uid, ids, *args): - for id in ids: - cr.execute("update res_request set state='active', act_from=%s, act_to=act_from, trigger_date=NULL, body='' where id=%s", (uid,id)) - return True - - def request_close(self, cr, uid, ids, *args): - self.write(cr, uid, ids, {'state':'closed'}) - return True - - def request_get(self, cr, uid): - cr.execute('select id from res_request where act_to=%s and (trigger_date<=%s or trigger_date is null) and active=True and state != %s', (uid,time.strftime('%Y-%m-%d'), 'closed')) - ids = map(lambda x:x[0], cr.fetchall()) - cr.execute('select id from res_request where act_from=%s and (act_to<>%s) and (trigger_date<=%s or trigger_date is null) and active=True and state != %s', (uid,uid,time.strftime('%Y-%m-%d'), 'closed')) - ids2 = map(lambda x:x[0], cr.fetchall()) - return ids, ids2 - - _columns = { - 'create_date': fields.datetime('Created Date', readonly=True), - 'name': fields.char('Subject', states={'waiting':[('readonly',True)],'active':[('readonly',True)],'closed':[('readonly',True)]}, required=True, size=128), - 'active': fields.boolean('Active'), - 'priority': fields.selection([('0','Low'),('1','Normal'),('2','High')], 'Priority', states={'waiting':[('readonly',True)],'closed':[('readonly',True)]}, required=True), - 'act_from': fields.many2one('res.users', 'From', required=True, readonly=True, states={'closed':[('readonly',True)]}, select=1), - 'act_to': fields.many2one('res.users', 'To', required=True, states={'waiting':[('readonly',True)],'closed':[('readonly',True)]}, select=1), - 'body': fields.text('Request', states={'waiting':[('readonly',True)],'closed':[('readonly',True)]}), - 'date_sent': fields.datetime('Date', readonly=True), - 'trigger_date': fields.datetime('Trigger Date', states={'waiting':[('readonly',True)],'closed':[('readonly',True)]}, select=1), - 'ref_partner_id':fields.many2one('res.partner', 'Partner Ref.', states={'closed':[('readonly',True)]}), - 'ref_doc1':fields.reference('Document Ref 1', selection=_links_get, size=128, states={'closed':[('readonly',True)]}), - 'ref_doc2':fields.reference('Document Ref 2', selection=_links_get, size=128, states={'closed':[('readonly',True)]}), - 'state': fields.selection([('draft','draft'),('waiting','waiting'),('active','active'),('closed','closed')], 'Status', required=True, readonly=True), - 'history': fields.one2many('res.request.history','req_id', 'History') - } - _defaults = { - 'act_from': lambda obj,cr,uid,context=None: uid, - 'state': 'draft', - 'active': True, - 'priority': '1', - } - _order = 'priority desc, trigger_date, create_date desc' - _table = 'res_request' -res_request() - class res_request_link(osv.osv): _name = 'res.request.link' _columns = { @@ -98,26 +38,6 @@ class res_request_link(osv.osv): 'priority': 5, } _order = 'priority' -res_request_link() - -class res_request_history(osv.osv): - _name = 'res.request.history' - _columns = { - 'name': fields.char('Summary', size=128, states={'active':[('readonly',True)],'waiting':[('readonly',True)]}, required=True), - 'req_id': fields.many2one('res.request', 'Request', required=True, ondelete='cascade', select=True), - 'act_from': fields.many2one('res.users', 'From', required=True, readonly=True), - 'act_to': fields.many2one('res.users', 'To', required=True, states={'waiting':[('readonly',True)]}), - 'body': fields.text('Body', states={'waiting':[('readonly',True)]}), - 'date_sent': fields.datetime('Date sent', states={'waiting':[('readonly',True)]}, required=True) - } - _defaults = { - 'name': 'NoName', - 'act_from': lambda obj,cr,uid,context=None: uid, - 'act_to': lambda obj,cr,uid,context=None: uid, - 'date_sent': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), - } -res_request_history() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/res/res_request_view.xml b/openerp/addons/base/res/res_request_view.xml index 7ad47a53c2e..03713f79926 100644 --- a/openerp/addons/base/res/res_request_view.xml +++ b/openerp/addons/base/res/res_request_view.xml @@ -35,16 +35,15 @@ - - Request Reference Types + Referencable Models ir.actions.act_window res.request.link form - + diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index ba709a48175..beb0018dc8d 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -34,7 +34,11 @@ from openerp.tools.translate import _ _logger = logging.getLogger(__name__) -class groups(osv.osv): +#---------------------------------------------------------- +# Basic res.groups and res.users +#---------------------------------------------------------- + +class res_groups(osv.osv): _name = "res.groups" _description = "Access Groups" _rec_name = 'full_name' @@ -81,29 +85,27 @@ class groups(osv.osv): def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): # add explicit ordering if search is sorted on full_name if order and order.startswith('full_name'): - ids = super(groups, self).search(cr, uid, args, context=context) + ids = super(res_groups, self).search(cr, uid, args, context=context) gs = self.browse(cr, uid, ids, context) gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC')) gs = gs[offset:offset+limit] if limit else gs[offset:] return map(int, gs) - return super(groups, self).search(cr, uid, args, offset, limit, order, context, count) + return super(res_groups, self).search(cr, uid, args, offset, limit, order, context, count) def copy(self, cr, uid, id, default=None, context=None): group_name = self.read(cr, uid, [id], ['name'])[0]['name'] default.update({'name': _('%s (copy)')%group_name}) - return super(groups, self).copy(cr, uid, id, default, context) + return super(res_groups, self).copy(cr, uid, id, default, context) def write(self, cr, uid, ids, vals, context=None): if 'name' in vals: if vals['name'].startswith('-'): raise osv.except_osv(_('Error'), _('The name of the group can not start with "-"')) - res = super(groups, self).write(cr, uid, ids, vals, context=context) + res = super(res_groups, self).write(cr, uid, ids, vals, context=context) self.pool['ir.model.access'].call_cache_clearing_methods(cr) return res -groups() - class res_users(osv.osv): """ User class. A res.users record models an OpenERP user and is different from an employee. @@ -516,12 +518,13 @@ class res_users(osv.osv): (uid, module, ext_id)) return bool(cr.fetchone()) - -# -# Extension of res.groups and res.users with a relation for "implied" or -# "inherited" groups. Once a user belongs to a group, it automatically belongs -# to the implied groups (transitively). +#---------------------------------------------------------- +# Implied groups # +# Extension of res.groups and res.users with a relation for "implied" +# or "inherited" groups. Once a user belongs to a group, it +# automatically belongs to the implied groups (transitively). +#---------------------------------------------------------- class cset(object): """ A cset (constrained set) is a set of elements that may be constrained to @@ -551,7 +554,6 @@ def concat(ls): return res - class groups_implied(osv.osv): _inherit = 'res.groups' @@ -618,6 +620,8 @@ class users_implied(osv.osv): super(users_implied, self).write(cr, uid, [user.id], vals, context) return res +#---------------------------------------------------------- +# Vitrual checkbox and selection for res.user form view # # Extension of res.groups and res.users for the special groups view in the users # form. This extension presents groups with selection and boolean widgets: @@ -638,6 +642,7 @@ class users_implied(osv.osv): # any of ID1, ..., IDk is in 'groups_id' # - selection field 'sel_groups_ID1_..._IDk' is ID iff # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk} +#---------------------------------------------------------- def name_boolean_group(id): return 'in_group_' + str(id) def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids)) @@ -661,7 +666,6 @@ def partition(f, xs): return yes, nos - class groups_view(osv.osv): _inherit = 'res.groups' @@ -869,4 +873,67 @@ class users_view(osv.osv): } return res +#---------------------------------------------------------- +# change password wizard +#---------------------------------------------------------- + +class change_password_wizard(osv.TransientModel): + """ + A wizard to manage the change of users' passwords + """ + + _name = "change.password.wizard" + _description = "Change Password Wizard" + _columns = { + 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'), + } + + def default_get(self, cr, uid, fields, context=None): + if context == None: + context = {} + user_ids = context.get('active_ids', []) + wiz_id = context.get('active_id', None) + res = [] + users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context) + for user in users: + res.append((0, 0, { + 'wizard_id': wiz_id, + 'user_id': user.id, + 'user_login': user.login, + })) + return {'user_ids': res} + + + def change_password_button(self, cr, uid, id, context=None): + wizard = self.browse(cr, uid, id, context=context)[0] + user_ids = [] + for user in wizard.user_ids: + user_ids.append(user.id) + self.pool.get('change.password.user').change_password_button(cr, uid, user_ids, context=context) + return { + 'type': 'ir.actions.act_window_close', + } + +class change_password_user(osv.TransientModel): + """ + A model to configure users in the change password wizard + """ + + _name = 'change.password.user' + _description = 'Change Password Wizard User' + _columns = { + 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True), + 'user_id': fields.many2one('res.users', string='User', required=True), + 'user_login': fields.char('User Login', readonly=True), + 'new_passwd': fields.char('New Password'), + } + _defaults = { + 'new_passwd': '', + } + + def change_password_button(self, cr, uid, ids, context=None): + for user in self.browse(cr, uid, ids, context=context): + self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd}) + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml index 770cb45149b..628cfc4f858 100644 --- a/openerp/addons/base/res/res_users_view.xml +++ b/openerp/addons/base/res/res_users_view.xml @@ -1,6 +1,41 @@ + + + + Change Password + change.password.wizard + +
+ +
+
+ +
+
+ + Change Password Users + change.password.user + + + + + + + + + + res.groups.search @@ -277,5 +312,6 @@ +
diff --git a/openerp/addons/base/res/wizard/__init__.py b/openerp/addons/base/res/wizard/__init__.py deleted file mode 100644 index f572814a2cf..00000000000 --- a/openerp/addons/base/res/wizard/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2011 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 . -# -############################################################################## - -import change_password_wizard \ No newline at end of file diff --git a/openerp/addons/base/res/wizard/change_password_wizard.py b/openerp/addons/base/res/wizard/change_password_wizard.py deleted file mode 100644 index 4d1bfe67fce..00000000000 --- a/openerp/addons/base/res/wizard/change_password_wizard.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2011 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 change_password_wizard(osv.TransientModel): - """ - A wizard to manage the change of users' passwords - """ - - _name = "change.password.wizard" - _description = "Change Password Wizard" - _columns = { - 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'), - } - - def default_get(self, cr, uid, fields, context=None): - if context == None: - context = {} - user_ids = context.get('active_ids', []) - wiz_id = context.get('active_id', None) - res = [] - users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context) - for user in users: - res.append((0, 0, { - 'wizard_id': wiz_id, - 'user_id': user.id, - 'user_login': user.login, - })) - return {'user_ids': res} - - - def change_password_button(self, cr, uid, id, context=None): - wizard = self.browse(cr, uid, id, context=context)[0] - user_ids = [] - for user in wizard.user_ids: - user_ids.append(user.id) - self.pool.get('change.password.user').change_password_button(cr, uid, user_ids, context=context) - return { - 'type': 'ir.actions.act_window_close', - } - -class change_password_user(osv.TransientModel): - """ - A model to configure users in the change password wizard - """ - - _name = 'change.password.user' - _description = 'Change Password Wizard User' - _columns = { - 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True), - 'user_id': fields.many2one('res.users', string='User', required=True), - 'user_login': fields.char('User Login', readonly=True), - 'new_passwd': fields.char('New Password'), - } - _defaults = { - 'new_passwd': '', - } - - def change_password_button(self, cr, uid, ids, context=None): - for user in self.browse(cr, uid, ids, context=context): - self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd}) - diff --git a/openerp/addons/base/res/wizard/change_password_wizard_view.xml b/openerp/addons/base/res/wizard/change_password_wizard_view.xml deleted file mode 100644 index cc190c1525a..00000000000 --- a/openerp/addons/base/res/wizard/change_password_wizard_view.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - Change Password - change.password.wizard - -
- -
-
- -
-
- - - - Change Password Users - change.password.user - - - - - - - - - -
-
diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index e4a0891db69..62b68c67aab 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -40,7 +40,6 @@ "access_ir_ui_view_group_user","ir_ui_view group_user","model_ir_ui_view",,1,0,0,0 "access_ir_ui_view_group_system","ir_ui_view group_system","model_ir_ui_view","group_system",1,1,1,1 "access_ir_ui_view_custom_group_user","ir_ui_view_custom_group_user","model_ir_ui_view_custom",,1,1,1,1 -"access_ir_ui_view_sc_group_user","ir_ui_view_sc group_user","model_ir_ui_view_sc",,1,1,1,1 "access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1 "access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1 "access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0 @@ -69,10 +68,6 @@ "access_res_partner_category_group_partner_manager","res_partner_category group_partner_manager","model_res_partner_category","group_partner_manager",1,1,1,1 "access_res_partner_title_group_user","res_partner_title group_user","model_res_partner_title","group_partner_manager",1,1,1,1 "access_res_partner_title_group_partner_manager","res_partner_title group_partner_manager","model_res_partner_title",,1,0,0,0 -"access_res_request_group_user","res_request group_user","model_res_request","base.group_user",1,1,1,1 -"access_res_request_history_group_user","res_request_history group_user","model_res_request_history","base.group_user",1,1,1,1 -"access_res_request_group_user_all","res_request group_user all","model_res_request",,1,0,0,0 -"access_res_request_history_group_user_all","res_request_history group_user all","model_res_request_history",,1,0,0,0 "access_res_request_link_group_system","res_request_link group_system","model_res_request_link","group_system",1,1,1,1 "access_res_request_link_group_user","res_request_link group_user","model_res_request_link",,1,0,0,0 "access_res_users_all","res_users all","model_res_users",,1,0,0,0 @@ -85,8 +80,6 @@ "access_ir_actions_act_window_close_group_system","ir_actions_act_window_close_group_system","model_ir_actions_act_window_close","group_system",1,1,1,1 "access_ir_actions_report_xml_all","ir_actions_report_xml","model_ir_actions_report_xml",,1,0,0,0 "access_ir_actions_report_xml_group_system","ir_actions_report_xml_group_system","model_ir_actions_report_xml","group_system",1,1,1,1 -"access_ir_actions_wizard_all","ir_actions_wizard","model_ir_actions_wizard",,1,0,0,0 -"access_ir_actions_wizard_group_system","ir_actions_wizard_group_system","model_ir_actions_wizard","group_system",1,1,1,1 "access_ir_actions_todo_group_system","ir_actions_todo group system","model_ir_actions_todo","group_system",1,1,1,1 "access_workflow_all","workflow_all","model_workflow",,1,0,0,0 "access_workflow_group_system","workflow_group_system","model_workflow","group_system",1,1,1,1 diff --git a/openerp/addons/base/workflow/__init__.py b/openerp/addons/base/workflow/__init__.py new file mode 100644 index 00000000000..45a9f36fa36 --- /dev/null +++ b/openerp/addons/base/workflow/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +import workflow +import workflow_report + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/openerp/addons/base/ir/workflow/workflow.py b/openerp/addons/base/workflow/workflow.py similarity index 100% rename from openerp/addons/base/ir/workflow/workflow.py rename to openerp/addons/base/workflow/workflow.py diff --git a/openerp/addons/base/ir/workflow/print_instance.py b/openerp/addons/base/workflow/workflow_report.py similarity index 100% rename from openerp/addons/base/ir/workflow/print_instance.py rename to openerp/addons/base/workflow/workflow_report.py diff --git a/openerp/addons/base/ir/workflow/workflow_view.xml b/openerp/addons/base/workflow/workflow_view.xml similarity index 100% rename from openerp/addons/base/ir/workflow/workflow_view.xml rename to openerp/addons/base/workflow/workflow_view.xml diff --git a/openerp/cli/server.py b/openerp/cli/server.py index fc1bf61b617..24987c098e4 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -147,112 +147,15 @@ def import_translation(): cr.commit() cr.close() -# Variable keeping track of the number of calls to the signal handler defined -# below. This variable is monitored by ``quit_on_signals()``. -quit_signals_received = 0 - -def signal_handler(sig, frame): - """ Signal handler: exit ungracefully on the second handled signal. - - :param sig: the signal number - :param frame: the interrupted stack frame or None - """ - global quit_signals_received - quit_signals_received += 1 - if quit_signals_received > 1: - # logging.shutdown was already called at this point. - sys.stderr.write("Forced shutdown.\n") - os._exit(0) - -def dumpstacks(sig, frame): - """ Signal handler: dump a stack trace for each existing thread.""" - # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696 - # modified for python 2.5 compatibility - threads_info = dict([(th.ident, {'name': th.name, - 'uid': getattr(th,'uid','n/a')}) - for th in threading.enumerate()]) - code = [] - for threadId, stack in sys._current_frames().items(): - thread_info = threads_info.get(threadId) - code.append("\n# Thread: %s (id:%s) (uid:%s)" % \ - (thread_info and thread_info['name'] or 'n/a', - threadId, - thread_info and thread_info['uid'] or 'n/a')) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) - if line: - code.append(" %s" % (line.strip())) - _logger.info("\n".join(code)) - -def setup_signal_handlers(signal_handler): - """ Register the given signal handler. """ - SIGNALS = (signal.SIGINT, signal.SIGTERM) - if os.name == 'posix': - map(lambda sig: signal.signal(sig, signal_handler), SIGNALS) - signal.signal(signal.SIGQUIT, dumpstacks) - elif os.name == 'nt': - import win32api - win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1) - -def quit_on_signals(): - """ Wait for one or two signals then shutdown the server. - - The first SIGINT or SIGTERM signal will initiate a graceful shutdown while - a second one if any will force an immediate exit. - - """ - # Wait for a first signal to be handled. (time.sleep will be interrupted - # by the signal handler.) The try/except is for the win32 case. - try: - while quit_signals_received == 0: - time.sleep(60) - except KeyboardInterrupt: - pass - - config = openerp.tools.config - openerp.service.stop_services() - - if getattr(openerp, 'phoenix', False): - # like the phoenix, reborn from ashes... - openerp.service._reexec() - return - - if config['pidfile']: - os.unlink(config['pidfile']) - sys.exit(0) - -def watch_parent(beat=4): - import gevent - ppid = os.getppid() - while True: - if ppid != os.getppid(): - pid = os.getpid() - _logger.info("LongPolling (%s) Parent changed", pid) - # suicide !! - os.kill(pid, signal.SIGTERM) - return - gevent.sleep(beat) - def main(args): check_root_user() openerp.tools.config.parse_config(args) - - if openerp.tools.config.options["gevent"]: - openerp.evented = True - _logger.info('Using gevent mode') - import gevent.monkey - gevent.monkey.patch_all() - import gevent_psycopg2 - gevent_psycopg2.monkey_patch() - check_postgres_user() openerp.netsvc.init_logger() report_configuration() config = openerp.tools.config - setup_signal_handlers(signal_handler) - if config["test_file"]: run_test_file(config['db_name'], config['test_file']) sys.exit(0) @@ -265,28 +168,19 @@ def main(args): import_translation() sys.exit(0) - if not config["stop_after_init"]: - setup_pid_file() - # Some module register themselves when they are loaded so we need the - # services to be running before loading any registry. - if not openerp.evented: - if config['workers']: - openerp.service.start_services_workers() - else: - openerp.service.start_services() - else: - config['xmlrpc_port'] = config['longpolling_port'] - import gevent - gevent.spawn(watch_parent) - openerp.service.start_services() - + # preload registryies, needed for -u --stop_after_init rc = 0 if config['db_name']: for dbname in config['db_name'].split(','): if not preload_registry(dbname): rc += 1 - if config["stop_after_init"]: + if not config["stop_after_init"]: + setup_pid_file() + openerp.service.server.start() + if config['pidfile']: + os.unlink(config['pidfile']) + else: sys.exit(rc) _logger.info('OpenERP server is running, waiting for connections...') diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 2aeea751f89..52426af751f 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -183,7 +183,7 @@ class reference(_column): _type = 'reference' _classic_read = False # post-process to handle missing target - def __init__(self, string, selection, size, **args): + def __init__(self, string, selection, size=None, **args): _column.__init__(self, string=string, size=size, selection=selection, **args) def get(self, cr, obj, ids, name, uid=None, context=None, values=None): diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 2c6c80eb80c..6e3ae985fab 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -20,29 +20,12 @@ # ############################################################################## -import logging -import os -import signal -import subprocess -import sys -import threading -import time - -import cron -import wsgi_server - -import openerp -import openerp.modules -import openerp.netsvc -import openerp.osv -from openerp.release import nt_service_name -import openerp.tools -from openerp.tools.misc import stripped_sys_argv - import common import db import model import report +import wsgi_server +import server #.apidoc title: RPC Services @@ -55,100 +38,5 @@ import report low-level behavior of the wire. """ -_logger = logging.getLogger(__name__) - -def load_server_wide_modules(): - for m in openerp.conf.server_wide_modules: - try: - openerp.modules.module.load_openerp_module(m) - except Exception: - msg = '' - if m == 'web': - msg = """ -The `web` module is provided by the addons found in the `openerp-web` project. -Maybe you forgot to add those addons in your addons_path configuration.""" - _logger.exception('Failed to load server-wide module `%s`.%s', m, msg) - -start_internal_done = False -main_thread_id = threading.currentThread().ident - -def start_internal(): - global start_internal_done - if start_internal_done: - return - openerp.netsvc.init_logger() - - load_server_wide_modules() - start_internal_done = True - -def start_services(): - """ Start all services including http, and cron """ - start_internal() - # Start the WSGI server. - wsgi_server.start_service() - # Start the main cron thread. - if not openerp.evented: - cron.start_service() - -def stop_services(): - """ Stop all services. """ - # stop services - if not openerp.evented: - cron.stop_service() - wsgi_server.stop_service() - - _logger.info("Initiating shutdown") - _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") - - # Manually join() all threads before calling sys.exit() to allow a second signal - # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. - # threading.Thread.join() should not mask signals (at least in python 2.5). - me = threading.currentThread() - _logger.debug('current thread: %r', me) - for thread in threading.enumerate(): - _logger.debug('process %r (%r)', thread, thread.isDaemon()) - if thread != me and not thread.isDaemon() and thread.ident != main_thread_id: - while thread.isAlive(): - _logger.debug('join and sleep') - # Need a busyloop here as thread.join() masks signals - # and would prevent the forced shutdown. - thread.join(0.05) - time.sleep(0.05) - - _logger.debug('--') - openerp.modules.registry.RegistryManager.delete_all() - logging.shutdown() - -def start_services_workers(): - import openerp.service.workers - openerp.multi_process = True - openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run() - -def _reexec(): - """reexecute openerp-server process with (nearly) the same arguments""" - if openerp.tools.osutil.is_running_as_nt_service(): - subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True) - exe = os.path.basename(sys.executable) - args = stripped_sys_argv() - if not args or args[0] != exe: - args.insert(0, exe) - os.execv(sys.executable, args) - -def restart_server(): - if openerp.multi_process: - raise NotImplementedError("Multicorn is not supported (but gunicorn was)") - pid = openerp.wsgi.core.arbiter_pid - os.kill(pid, signal.SIGHUP) - else: - if os.name == 'nt': - def reborn(): - stop_services() - _reexec() - - # run in a thread to let the current thread return response to the caller. - threading.Thread(target=reborn).start() - else: - openerp.phoenix = True - os.kill(os.getpid(), signal.SIGINT) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/cron.py b/openerp/service/cron.py deleted file mode 100644 index f21b27f7fea..00000000000 --- a/openerp/service/cron.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2011 OpenERP SA () -# -# 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 . -# -############################################################################## - -""" Cron jobs scheduling - -Cron jobs are defined in the ir_cron table/model. This module deals with all -cron jobs, for all databases of a single OpenERP server instance. - -""" - -import logging -import threading -import time -from datetime import datetime - -import openerp - -_logger = logging.getLogger(__name__) - -SLEEP_INTERVAL = 60 # 1 min - -def cron_runner(number): - while True: - time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style - registries = openerp.modules.registry.RegistryManager.registries - _logger.debug('cron%d polling for jobs', number) - for db_name, registry in registries.items(): - while True and registry.ready: - acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) - if not acquired: - break - -def start_service(): - """ Start the above runner function in a daemon thread. - - The thread is a typical daemon thread: it will never quit and must be - terminated when the main process exits - with no consequence (the processing - threads it spawns are not marked daemon). - - """ - - # Force call to strptime just before starting the cron thread - # to prevent time.strptime AttributeError within the thread. - # See: http://bugs.python.org/issue7980 - datetime.strptime('2012-01-01', '%Y-%m-%d') - - for i in range(openerp.tools.config['max_cron_threads']): - def target(): - cron_runner(i) - t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i) - t.setDaemon(True) - t.start() - _logger.debug("cron%d started!" % i) - -def stop_service(): - pass - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/server.py b/openerp/service/server.py new file mode 100644 index 00000000000..cddb0bee444 --- /dev/null +++ b/openerp/service/server.py @@ -0,0 +1,866 @@ +#----------------------------------------------------------- +# Threaded, Gevent and Prefork Servers +#----------------------------------------------------------- +import datetime +import errno +import fcntl +import logging +import os +import os.path +import platform +import psutil +import random +import resource +import select +import signal +import socket +import subprocess +import sys +import threading +import time +import traceback + +import werkzeug.serving +try: + from setproctitle import setproctitle +except ImportError: + setproctitle = lambda x: None + +import openerp +import openerp.tools.config as config +from openerp.release import nt_service_name +from openerp.tools.misc import stripped_sys_argv + +import wsgi_server + +_logger = logging.getLogger(__name__) + +SLEEP_INTERVAL = 60 # 1 min + +#---------------------------------------------------------- +# Werkzeug WSGI servers patched +#---------------------------------------------------------- + +class BaseWSGIServerNoBind(werkzeug.serving.BaseWSGIServer): + """ werkzeug Base WSGI Server patched to skip socket binding. PreforkServer + use this class, sets the socket and calls the process_request() manually + """ + def __init__(self, app): + werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app) + def server_bind(self): + # we dont bind beause we use the listen socket of PreforkServer#socket + # instead we close the socket + if self.socket: + self.socket.close() + def server_activate(self): + # dont listen as we use PreforkServer#socket + pass + +# _reexec() should set LISTEN_* to avoid connection refused during reload time. It +# should also work with systemd socket activation. This is currently untested +# and not yet used. + +class ThreadedWSGIServerReloadable(werkzeug.serving.ThreadedWSGIServer): + """ werkzeug Threaded WSGI Server patched to allow reusing a listen socket + given by the environement, this is used by autoreload to keep the listen + socket open when a reload happens. + """ + def server_bind(self): + envfd = os.environ.get('LISTEN_FDS') + if envfd and os.environ.get('LISTEN_PID') == str(os.getpid()): + self.reload_socket = True + self.socket = socket.fromfd(int(envfd), socket.AF_INET, socket.SOCK_STREAM) + # should we os.close(int(envfd)) ? it seem python duplicate the fd. + else: + self.reload_socket = False + super(ThreadedWSGIServerReloadable, self).server_bind() + + def server_activate(self): + if not self.reload_socket: + super(ThreadedWSGIServerReloadable, self).server_activate() + +#---------------------------------------------------------- +# AutoReload watcher +#---------------------------------------------------------- + +class AutoReload(object): + def __init__(self, server): + self.server = server + self.files = {} + self.modules = {} + import pyinotify + class EventHandler(pyinotify.ProcessEvent): + def __init__(self, autoreload): + self.autoreload = autoreload + + def process_IN_CREATE(self, event): + _logger.debug('File created: %s', event.pathname) + self.autoreload.files[event.pathname] = 1 + + def process_IN_MODIFY(self, event): + _logger.debug('File modified: %s', event.pathname) + self.autoreload.files[event.pathname] = 1 + + self.wm = pyinotify.WatchManager() + self.handler = EventHandler(self) + self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0) + mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ? + for path in openerp.tools.config.options["addons_path"].split(','): + _logger.info('Watching addons folder %s', path) + self.wm.add_watch(path, mask, rec=True) + + def process_data(self, files): + xml_files = [i for i in files if i.endswith('.xml')] + addons_path = openerp.tools.config.options["addons_path"].split(',') + for i in xml_files: + for path in addons_path: + if i.startswith(path): + # find out wich addons path the file belongs to + # and extract it's module name + right = i[len(path) + 1:].split('/') + if len(right) < 2: + continue + module = right[0] + self.modules[module]=1 + if self.modules: + _logger.info('autoreload: xml change detected, autoreload activated') + restart() + + def process_python(self, files): + # process python changes + py_files = [i for i in files if i.endswith('.py')] + py_errors = [] + # TODO keep python errors until they are ok + if py_files: + for i in py_files: + try: + source = open(i, 'rb').read() + '\n' + compile(source, i, 'exec') + except SyntaxError: + py_errors.append(i) + if py_errors: + _logger.info('autoreload: python code change detected, errors found') + for i in py_errors: + _logger.info('autoreload: SyntaxError %s',i) + else: + _logger.info('autoreload: python code updated, autoreload activated') + restart() + + def check_thread(self): + # Check if some files have been touched in the addons path. + # If true, check if the touched file belongs to an installed module + # in any of the database used in the registry manager. + while 1: + while self.notifier.check_events(1000): + self.notifier.read_events() + self.notifier.process_events() + l = self.files.keys() + self.files.clear() + self.process_data(l) + self.process_python(l) + + def run(self): + t = threading.Thread(target=self.check_thread) + t.setDaemon(True) + t.start() + _logger.info('AutoReload watcher running') + +#---------------------------------------------------------- +# Servers: Threaded, Gevented and Prefork +#---------------------------------------------------------- + +class CommonServer(object): + def __init__(self, app): + # TODO Change the xmlrpc_* options to http_* + self.app = app + # config + self.interface = config['xmlrpc_interface'] or '0.0.0.0' + self.port = config['xmlrpc_port'] + # runtime + self.pid = os.getpid() + + def dumpstacks(self): + """ Signal handler: dump a stack trace for each existing thread.""" + # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696 + # modified for python 2.5 compatibility + threads_info = dict([(th.ident, {'name': th.name, + 'uid': getattr(th,'uid','n/a')}) + for th in threading.enumerate()]) + code = [] + for threadId, stack in sys._current_frames().items(): + thread_info = threads_info.get(threadId) + code.append("\n# Thread: %s (id:%s) (uid:%s)" % \ + (thread_info and thread_info['name'] or 'n/a', + threadId, + thread_info and thread_info['uid'] or 'n/a')) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + _logger.info("\n".join(code)) + + def close_socket(self, sock): + """ Closes a socket instance cleanly + :param sock: the network socket to close + :type sock: socket.socket + """ + try: + sock.shutdown(socket.SHUT_RDWR) + except socket.error, e: + # On OSX, socket shutdowns both sides if any side closes it + # causing an error 57 'Socket is not connected' on shutdown + # of the other side (or something), see + # http://bugs.python.org/issue4397 + # note: stdlib fixed test, not behavior + if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']: + raise + sock.close() + +class ThreadedServer(CommonServer): + def __init__(self, app): + super(ThreadedServer, self).__init__(app) + self.main_thread_id = threading.currentThread().ident + # Variable keeping track of the number of calls to the signal handler defined + # below. This variable is monitored by ``quit_on_signals()``. + self.quit_signals_received = 0 + + #self.socket = None + self.httpd = None + + def signal_handler(self, sig, frame): + if sig in [signal.SIGINT,signal.SIGTERM]: + # shutdown on kill -INT or -TERM + self.quit_signals_received += 1 + if self.quit_signals_received > 1: + # logging.shutdown was already called at this point. + sys.stderr.write("Forced shutdown.\n") + os._exit(0) + elif sig == signal.SIGHUP: + # restart on kill -HUP + openerp.phoenix = True + self.quit_signals_received += 1 + elif sig == signal.SIGQUIT: + # dump stacks on kill -3 + self.dumpstacks() + + def cron_thread(self, number): + while True: + time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style + registries = openerp.modules.registry.RegistryManager.registries + _logger.debug('cron%d polling for jobs', number) + for db_name, registry in registries.items(): + while True and registry.ready: + acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) + if not acquired: + break + + def cron_spawn(self): + """ Start the above runner function in a daemon thread. + + The thread is a typical daemon thread: it will never quit and must be + terminated when the main process exits - with no consequence (the processing + threads it spawns are not marked daemon). + + """ + # Force call to strptime just before starting the cron thread + # to prevent time.strptime AttributeError within the thread. + # See: http://bugs.python.org/issue7980 + datetime.datetime.strptime('2012-01-01', '%Y-%m-%d') + for i in range(openerp.tools.config['max_cron_threads']): + def target(): + self.cron_thread(i) + t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i) + t.setDaemon(True) + t.start() + _logger.debug("cron%d started!" % i) + + def http_thread(self): + def app(e,s): + return self.app(e,s) + self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app) + self.httpd.serve_forever() + + def http_spawn(self): + threading.Thread(target=self.http_thread).start() + _logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port) + + def start(self): + _logger.debug("Setting signal handlers") + if os.name == 'posix': + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGCHLD, self.signal_handler) + signal.signal(signal.SIGHUP, self.signal_handler) + signal.signal(signal.SIGQUIT, self.signal_handler) + elif os.name == 'nt': + import win32api + win32api.SetConsoleCtrlHandler(lambda sig: signal_handler(sig, None), 1) + self.cron_spawn() + self.http_spawn() + + def stop(self): + """ Shutdown the WSGI server. Wait for non deamon threads. + """ + _logger.info("Initiating shutdown") + _logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") + + self.httpd.shutdown() + self.close_socket(self.httpd.socket) + + # Manually join() all threads before calling sys.exit() to allow a second signal + # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. + # threading.Thread.join() should not mask signals (at least in python 2.5). + me = threading.currentThread() + _logger.debug('current thread: %r', me) + for thread in threading.enumerate(): + _logger.debug('process %r (%r)', thread, thread.isDaemon()) + if thread != me and not thread.isDaemon() and thread.ident != self.main_thread_id: + while thread.isAlive(): + _logger.debug('join and sleep') + # Need a busyloop here as thread.join() masks signals + # and would prevent the forced shutdown. + thread.join(0.05) + time.sleep(0.05) + + _logger.debug('--') + openerp.modules.registry.RegistryManager.delete_all() + logging.shutdown() + + def run(self): + """ Start the http server and the cron thread then wait for a signal. + + The first SIGINT or SIGTERM signal will initiate a graceful shutdown while + a second one if any will force an immediate exit. + """ + self.start() + + # Wait for a first signal to be handled. (time.sleep will be interrupted + # by the signal handler.) The try/except is for the win32 case. + try: + while self.quit_signals_received == 0: + time.sleep(60) + except KeyboardInterrupt: + pass + + self.stop() + + def reload(self): + os.kill(self.pid, signal.SIGHUP) + +class GeventServer(CommonServer): + def __init__(self, app): + super(GeventServer, self).__init__(app) + self.port = config['longpolling_port'] + self.httpd = None + + def watch_parent(self, beat=4): + import gevent + ppid = os.getppid() + while True: + if ppid != os.getppid(): + pid = os.getpid() + _logger.info("LongPolling (%s) Parent changed", pid) + # suicide !! + os.kill(pid, signal.SIGTERM) + return + gevent.sleep(beat) + + def start(self): + import gevent + from gevent.wsgi import WSGIServer + gevent.spawn(self.watch_parent) + self.httpd = WSGIServer((self.interface, self.port), self.app) + _logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port) + self.httpd.serve_forever() + + def stop(self): + import gevent + self.httpd.stop() + gevent.shutdown() + + def run(self): + self.start() + self.stop() + +class PreforkServer(CommonServer): + """ Multiprocessing inspired by (g)unicorn. + PreforkServer (aka Multicorn) currently uses accept(2) as dispatching + method between workers but we plan to replace it by a more intelligent + dispatcher to will parse the first HTTP request line. + """ + def __init__(self, app): + # config + self.address = (config['xmlrpc_interface'] or '0.0.0.0', config['xmlrpc_port']) + self.population = config['workers'] + self.timeout = config['limit_time_real'] + self.limit_request = config['limit_request'] + # working vars + self.beat = 4 + self.app = app + self.pid = os.getpid() + self.socket = None + self.workers_http = {} + self.workers_cron = {} + self.workers = {} + self.generation = 0 + self.queue = [] + self.long_polling_pid = None + + def pipe_new(self): + pipe = os.pipe() + for fd in pipe: + # non_blocking + flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + # close_on_exec + flags = fcntl.fcntl(fd, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(fd, fcntl.F_SETFD, flags) + return pipe + + def pipe_ping(self, pipe): + try: + os.write(pipe[1], '.') + except IOError, e: + if e.errno not in [errno.EAGAIN, errno.EINTR]: + raise + + def signal_handler(self, sig, frame): + if len(self.queue) < 5 or sig == signal.SIGCHLD: + self.queue.append(sig) + self.pipe_ping(self.pipe) + else: + _logger.warn("Dropping signal: %s", sig) + + def worker_spawn(self, klass, workers_registry): + self.generation += 1 + worker = klass(self) + pid = os.fork() + if pid != 0: + worker.pid = pid + self.workers[pid] = worker + workers_registry[pid] = worker + return worker + else: + worker.run() + sys.exit(0) + + def long_polling_spawn(self): + nargs = stripped_sys_argv('--pidfile','--workers') + cmd = nargs[0] + cmd = os.path.join(os.path.dirname(cmd), "openerp-gevent") + nargs[0] = cmd + popen = subprocess.Popen(nargs) + self.long_polling_pid = popen.pid + + def worker_pop(self, pid): + if pid in self.workers: + _logger.debug("Worker (%s) unregistered",pid) + try: + self.workers_http.pop(pid,None) + self.workers_cron.pop(pid,None) + u = self.workers.pop(pid) + u.close() + except OSError: + return + + def worker_kill(self, pid, sig): + try: + os.kill(pid, sig) + except OSError, e: + if e.errno == errno.ESRCH: + self.worker_pop(pid) + + def process_signals(self): + while len(self.queue): + sig = self.queue.pop(0) + if sig in [signal.SIGINT,signal.SIGTERM]: + raise KeyboardInterrupt + elif sig == signal.SIGHUP: + # restart on kill -HUP + openerp.phoenix = True + raise KeyboardInterrupt + elif sig == signal.SIGQUIT: + # dump stacks on kill -3 + self.dumpstacks() + elif sig == signal.SIGTTIN: + # increase number of workers + self.population += 1 + elif sig == signal.SIGTTOU: + # decrease number of workers + self.population -= 1 + + def process_zombie(self): + # reap dead workers + while 1: + try: + wpid, status = os.waitpid(-1, os.WNOHANG) + if not wpid: + break + if (status >> 8) == 3: + msg = "Critial worker error (%s)" + _logger.critical(msg, wpid) + raise Exception(msg % wpid) + self.worker_pop(wpid) + except OSError, e: + if e.errno == errno.ECHILD: + break + raise + + def process_timeout(self): + now = time.time() + for (pid, worker) in self.workers.items(): + if (worker.watchdog_timeout is not None) and \ + (now - worker.watchdog_time >= worker.watchdog_timeout): + _logger.error("Worker (%s) timeout", pid) + self.worker_kill(pid, signal.SIGKILL) + + def process_spawn(self): + while len(self.workers_http) < self.population: + self.worker_spawn(WorkerHTTP, self.workers_http) + while len(self.workers_cron) < config['max_cron_threads']: + self.worker_spawn(WorkerCron, self.workers_cron) + if not self.long_polling_pid: + self.long_polling_spawn() + + def sleep(self): + try: + # map of fd -> worker + fds = dict([(w.watchdog_pipe[0],w) for k,w in self.workers.items()]) + fd_in = fds.keys() + [self.pipe[0]] + # check for ping or internal wakeups + ready = select.select(fd_in, [], [], self.beat) + # update worker watchdogs + for fd in ready[0]: + if fd in fds: + fds[fd].watchdog_time = time.time() + try: + # empty pipe + while os.read(fd, 1): + pass + except OSError, e: + if e.errno not in [errno.EAGAIN]: + raise + except select.error, e: + if e[0] not in [errno.EINTR]: + raise + + def start(self): + # wakeup pipe, python doesnt throw EINTR when a syscall is interrupted + # by a signal simulating a pseudo SA_RESTART. We write to a pipe in the + # signal handler to overcome this behaviour + self.pipe = self.pipe_new() + # set signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGHUP, self.signal_handler) + signal.signal(signal.SIGCHLD, self.signal_handler) + signal.signal(signal.SIGQUIT, self.signal_handler) + signal.signal(signal.SIGTTIN, self.signal_handler) + signal.signal(signal.SIGTTOU, self.signal_handler) + # listen to socket + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setblocking(0) + self.socket.bind(self.address) + self.socket.listen(8*self.population) + + def stop(self, graceful=True): + if self.long_polling_pid is not None: + self.worker_kill(self.long_polling_pid, signal.SIGKILL) # FIXME make longpolling process handle SIGTERM correctly + self.long_polling_pid = None + if graceful: + _logger.info("Stopping gracefully") + limit = time.time() + self.timeout + for pid in self.workers.keys(): + self.worker_kill(pid, signal.SIGTERM) + while self.workers and time.time() < limit: + self.process_zombie() + time.sleep(0.1) + else: + _logger.info("Stopping forcefully") + for pid in self.workers.keys(): + self.worker_kill(pid, signal.SIGTERM) + self.socket.close() + + def run(self): + self.start() + _logger.debug("Multiprocess starting") + while 1: + try: + #_logger.debug("Multiprocess beat (%s)",time.time()) + self.process_signals() + self.process_zombie() + self.process_timeout() + self.process_spawn() + self.sleep() + except KeyboardInterrupt: + _logger.debug("Multiprocess clean stop") + self.stop() + break + except Exception,e: + _logger.exception(e) + self.stop(False) + sys.exit(-1) + +class Worker(object): + """ Workers """ + def __init__(self, multi): + self.multi = multi + self.watchdog_time = time.time() + self.watchdog_pipe = multi.pipe_new() + # Can be set to None if no watchdog is desired. + self.watchdog_timeout = multi.timeout + self.ppid = os.getpid() + self.pid = None + self.alive = True + # should we rename into lifetime ? + self.request_max = multi.limit_request + self.request_count = 0 + + def setproctitle(self, title=""): + setproctitle('openerp: %s %s %s' % (self.__class__.__name__, self.pid, title)) + + def close(self): + os.close(self.watchdog_pipe[0]) + os.close(self.watchdog_pipe[1]) + + def signal_handler(self, sig, frame): + self.alive = False + + def sleep(self): + try: + ret = select.select([self.multi.socket], [], [], self.multi.beat) + except select.error, e: + if e[0] not in [errno.EINTR]: + raise + + def process_limit(self): + # If our parent changed sucide + if self.ppid != os.getppid(): + _logger.info("Worker (%s) Parent changed", self.pid) + self.alive = False + # check for lifetime + if self.request_count >= self.request_max: + _logger.info("Worker (%d) max request (%s) reached.", self.pid, self.request_count) + self.alive = False + # Reset the worker if it consumes too much memory (e.g. caused by a memory leak). + rss, vms = psutil.Process(os.getpid()).get_memory_info() + if vms > config['limit_memory_soft']: + _logger.info('Worker (%d) virtual memory limit (%s) reached.', self.pid, vms) + self.alive = False # Commit suicide after the request. + + # VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space + soft, hard = resource.getrlimit(resource.RLIMIT_AS) + resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard)) + + # SIGXCPU (exceeded CPU time) signal handler will raise an exception. + r = resource.getrusage(resource.RUSAGE_SELF) + cpu_time = r.ru_utime + r.ru_stime + def time_expired(n, stack): + _logger.info('Worker (%d) CPU time limit (%s) reached.', config['limit_time_cpu']) + # We dont suicide in such case + raise Exception('CPU time limit exceeded.') + signal.signal(signal.SIGXCPU, time_expired) + soft, hard = resource.getrlimit(resource.RLIMIT_CPU) + resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + config['limit_time_cpu'], hard)) + + def process_work(self): + pass + + def start(self): + self.pid = os.getpid() + self.setproctitle() + _logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid) + # Reseed the random number generator + random.seed() + # Prevent fd inherientence close_on_exec + flags = fcntl.fcntl(self.multi.socket, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(self.multi.socket, fcntl.F_SETFD, flags) + # reset blocking status + self.multi.socket.setblocking(0) + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, signal.SIG_DFL) + signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + def stop(self): + pass + + def run(self): + try: + self.start() + while self.alive: + self.process_limit() + self.multi.pipe_ping(self.watchdog_pipe) + self.sleep() + self.process_work() + _logger.info("Worker (%s) exiting. request_count: %s.", self.pid, self.request_count) + self.stop() + except Exception,e: + _logger.exception("Worker (%s) Exception occured, exiting..." % self.pid) + # should we use 3 to abort everything ? + sys.exit(1) + +class WorkerHTTP(Worker): + """ HTTP Request workers """ + def process_request(self, client, addr): + client.setblocking(1) + client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # Prevent fd inherientence close_on_exec + flags = fcntl.fcntl(client, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(client, fcntl.F_SETFD, flags) + # do request using BaseWSGIServerNoBind monkey patched with socket + self.server.socket = client + # tolerate broken pipe when the http client closes the socket before + # receiving the full reply + try: + self.server.process_request(client,addr) + except IOError, e: + if e.errno != errno.EPIPE: + raise + self.request_count += 1 + + def process_work(self): + try: + client, addr = self.multi.socket.accept() + self.process_request(client, addr) + except socket.error, e: + if e[0] not in (errno.EAGAIN, errno.ECONNABORTED): + raise + + def start(self): + Worker.start(self) + self.server = BaseWSGIServerNoBind(self.multi.app) + +class WorkerCron(Worker): + """ Cron workers """ + + def __init__(self, multi): + super(WorkerCron, self).__init__(multi) + # process_work() below process a single database per call. + # The variable db_index is keeping track of the next database to + # process. + self.db_index = 0 + + def sleep(self): + # Really sleep once all the databases have been processed. + if self.db_index == 0: + interval = SLEEP_INTERVAL + self.pid % 10 # chorus effect + time.sleep(interval) + + def _db_list(self): + if config['db_name']: + db_names = config['db_name'].split(',') + else: + db_names = openerp.service.db.exp_list(True) + return db_names + + def process_work(self): + rpc_request = logging.getLogger('openerp.netsvc.rpc.request') + rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) + _logger.debug("WorkerCron (%s) polling for jobs", self.pid) + db_names = self._db_list() + if len(db_names): + self.db_index = (self.db_index + 1) % len(db_names) + db_name = db_names[self.db_index] + self.setproctitle(db_name) + if rpc_request_flag: + start_time = time.time() + start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() + + import openerp.addons.base as base + base.ir.ir_cron.ir_cron._acquire_job(db_name) + openerp.modules.registry.RegistryManager.delete(db_name) + + # dont keep cursors in multi database mode + if len(db_names) > 1: + openerp.sql_db.close_db(db_name) + if rpc_request_flag: + end_time = time.time() + end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() + logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (db_name, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) + _logger.debug("WorkerCron (%s) %s", self.pid, logline) + + self.request_count += 1 + if self.request_count >= self.request_max and self.request_max < len(db_names): + _logger.error("There are more dabatases to process than allowed " + "by the `limit_request` configuration variable: %s more.", + len(db_names) - self.request_max) + else: + self.db_index = 0 + + def start(self): + os.nice(10) # mommy always told me to be nice with others... + Worker.start(self) + self.multi.socket.close() + + # chorus effect: make cron workers do not all start at first database + mct = config['max_cron_threads'] + p = float(self.pid % mct) / mct + self.db_index = int(len(self._db_list()) * p) + +#---------------------------------------------------------- +# start/stop public api +#---------------------------------------------------------- + +server = None + +def load_server_wide_modules(): + for m in openerp.conf.server_wide_modules: + try: + openerp.modules.module.load_openerp_module(m) + except Exception: + msg = '' + if m == 'web': + msg = """ +The `web` module is provided by the addons found in the `openerp-web` project. +Maybe you forgot to add those addons in your addons_path configuration.""" + _logger.exception('Failed to load server-wide module `%s`.%s', m, msg) + +def _reexec(updated_modules=None): + """reexecute openerp-server process with (nearly) the same arguments""" + if openerp.tools.osutil.is_running_as_nt_service(): + subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True) + exe = os.path.basename(sys.executable) + args = stripped_sys_argv() + args += ["-u", ','.join(updated_modules)] + if not args or args[0] != exe: + args.insert(0, exe) + os.execv(sys.executable, args) + +def start(): + """ Start the openerp http server and cron processor. + """ + global server + load_server_wide_modules() + if config['workers']: + openerp.multi_process = True + server = PreforkServer(openerp.service.wsgi_server.application) + elif openerp.evented: + server = GeventServer(openerp.service.wsgi_server.application) + else: + server = ThreadedServer(openerp.service.wsgi_server.application) + + if config['auto_reload']: + autoreload = AutoReload(server) + autoreload.run() + + server.run() + + # like the legend of the phoenix, all ends with beginnings + if getattr(openerp, 'phoenix', False): + modules = [] + if config['auto_reload']: + modules = autoreload.modules.keys() + _reexec(modules) + sys.exit(0) + +def restart(): + """ Restart the server + """ + if os.name == 'nt': + # run in a thread to let the current thread return response to the caller. + threading.Thread(target=_reexec).start() + else: + os.kill(server.pid, signal.SIGHUP) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/workers.py b/openerp/service/workers.py deleted file mode 100644 index c8b8b84bca2..00000000000 --- a/openerp/service/workers.py +++ /dev/null @@ -1,448 +0,0 @@ -#----------------------------------------------------------- -# Multicorn, multiprocessing inspired by gunicorn -# TODO rename class: Multicorn -> Arbiter ? -#----------------------------------------------------------- -import errno -import fcntl -import logging -import os -import psutil -import random -import resource -import select -import signal -import socket -import sys -import time -import subprocess -import os.path - -import werkzeug.serving -try: - from setproctitle import setproctitle -except ImportError: - setproctitle = lambda x: None - -import openerp -import openerp.tools.config as config -from openerp.tools.misc import stripped_sys_argv - -_logger = logging.getLogger(__name__) - -class Multicorn(object): - """ Multiprocessing inspired by (g)unicorn. - Multicorn currently uses accept(2) as dispatching method between workers - but we plan to replace it by a more intelligent dispatcher to will parse - the first HTTP request line. - """ - def __init__(self, app): - # config - self.address = (config['xmlrpc_interface'] or '0.0.0.0', config['xmlrpc_port']) - self.population = config['workers'] - self.timeout = config['limit_time_real'] - self.limit_request = config['limit_request'] - # working vars - self.beat = 4 - self.app = app - self.pid = os.getpid() - self.socket = None - self.workers_http = {} - self.workers_cron = {} - self.workers = {} - self.generation = 0 - self.queue = [] - self.long_polling_pid = None - - def pipe_new(self): - pipe = os.pipe() - for fd in pipe: - # non_blocking - flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK - fcntl.fcntl(fd, fcntl.F_SETFL, flags) - # close_on_exec - flags = fcntl.fcntl(fd, fcntl.F_GETFD) | fcntl.FD_CLOEXEC - fcntl.fcntl(fd, fcntl.F_SETFD, flags) - return pipe - - def pipe_ping(self, pipe): - try: - os.write(pipe[1], '.') - except IOError, e: - if e.errno not in [errno.EAGAIN, errno.EINTR]: - raise - - def signal_handler(self, sig, frame): - if len(self.queue) < 5 or sig == signal.SIGCHLD: - self.queue.append(sig) - self.pipe_ping(self.pipe) - else: - _logger.warn("Dropping signal: %s", sig) - - def worker_spawn(self, klass, workers_registry): - self.generation += 1 - worker = klass(self) - pid = os.fork() - if pid != 0: - worker.pid = pid - self.workers[pid] = worker - workers_registry[pid] = worker - return worker - else: - worker.run() - sys.exit(0) - - def long_polling_spawn(self): - nargs = stripped_sys_argv('--pidfile') - cmd = nargs[0] - cmd = os.path.join(os.path.dirname(cmd), "openerp-long-polling") - nargs[0] = cmd - popen = subprocess.Popen(nargs) - self.long_polling_pid = popen.pid - - def worker_pop(self, pid): - if pid in self.workers: - _logger.debug("Worker (%s) unregistered",pid) - try: - self.workers_http.pop(pid,None) - self.workers_cron.pop(pid,None) - u = self.workers.pop(pid) - u.close() - except OSError: - return - - def worker_kill(self, pid, sig): - try: - os.kill(pid, sig) - except OSError, e: - if e.errno == errno.ESRCH: - self.worker_pop(pid) - - def process_signals(self): - while len(self.queue): - sig = self.queue.pop(0) - if sig in [signal.SIGINT,signal.SIGTERM]: - raise KeyboardInterrupt - - def process_zombie(self): - # reap dead workers - while 1: - try: - wpid, status = os.waitpid(-1, os.WNOHANG) - if not wpid: - break - if (status >> 8) == 3: - msg = "Critial worker error (%s)" - _logger.critical(msg, wpid) - raise Exception(msg % wpid) - self.worker_pop(wpid) - except OSError, e: - if e.errno == errno.ECHILD: - break - raise - - def process_timeout(self): - now = time.time() - for (pid, worker) in self.workers.items(): - if (worker.watchdog_timeout is not None) and \ - (now - worker.watchdog_time >= worker.watchdog_timeout): - _logger.error("Worker (%s) timeout", pid) - self.worker_kill(pid, signal.SIGKILL) - - def process_spawn(self): - while len(self.workers_http) < self.population: - self.worker_spawn(WorkerHTTP, self.workers_http) - while len(self.workers_cron) < config['max_cron_threads']: - self.worker_spawn(WorkerCron, self.workers_cron) - if not self.long_polling_pid: - self.long_polling_spawn() - - def sleep(self): - try: - # map of fd -> worker - fds = dict([(w.watchdog_pipe[0],w) for k,w in self.workers.items()]) - fd_in = fds.keys() + [self.pipe[0]] - # check for ping or internal wakeups - ready = select.select(fd_in, [], [], self.beat) - # update worker watchdogs - for fd in ready[0]: - if fd in fds: - fds[fd].watchdog_time = time.time() - try: - # empty pipe - while os.read(fd, 1): - pass - except OSError, e: - if e.errno not in [errno.EAGAIN]: - raise - except select.error, e: - if e[0] not in [errno.EINTR]: - raise - - def start(self): - # wakeup pipe, python doesnt throw EINTR when a syscall is interrupted - # by a signal simulating a pseudo SA_RESTART. We write to a pipe in the - # signal handler to overcome this behaviour - self.pipe = self.pipe_new() - # set signal - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, self.signal_handler) - signal.signal(signal.SIGCHLD, self.signal_handler) - # listen to socket - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.setblocking(0) - self.socket.bind(self.address) - self.socket.listen(8*self.population) - - def stop(self, graceful=True): - if self.long_polling_pid is not None: - self.worker_kill(self.long_polling_pid, signal.SIGKILL) # FIXME make longpolling process handle SIGTERM correctly - self.long_polling_pid = None - if graceful: - _logger.info("Stopping gracefully") - limit = time.time() + self.timeout - for pid in self.workers.keys(): - self.worker_kill(pid, signal.SIGTERM) - while self.workers and time.time() < limit: - self.process_zombie() - time.sleep(0.1) - else: - _logger.info("Stopping forcefully") - for pid in self.workers.keys(): - self.worker_kill(pid, signal.SIGTERM) - self.socket.close() - openerp.cli.server.quit_signals_received = 1 - - def run(self): - self.start() - _logger.debug("Multiprocess starting") - while 1: - try: - #_logger.debug("Multiprocess beat (%s)",time.time()) - self.process_signals() - self.process_zombie() - self.process_timeout() - self.process_spawn() - self.sleep() - except KeyboardInterrupt: - _logger.debug("Multiprocess clean stop") - self.stop() - break - except Exception,e: - _logger.exception(e) - self.stop(False) - sys.exit(-1) - -class Worker(object): - """ Workers """ - def __init__(self, multi): - self.multi = multi - self.watchdog_time = time.time() - self.watchdog_pipe = multi.pipe_new() - # Can be set to None if no watchdog is desired. - self.watchdog_timeout = multi.timeout - self.ppid = os.getpid() - self.pid = None - self.alive = True - # should we rename into lifetime ? - self.request_max = multi.limit_request - self.request_count = 0 - - def setproctitle(self, title=""): - setproctitle('openerp: %s %s %s' % (self.__class__.__name__, self.pid, title)) - - def close(self): - os.close(self.watchdog_pipe[0]) - os.close(self.watchdog_pipe[1]) - - def signal_handler(self, sig, frame): - self.alive = False - - def sleep(self): - try: - ret = select.select([self.multi.socket], [], [], self.multi.beat) - except select.error, e: - if e[0] not in [errno.EINTR]: - raise - - def process_limit(self): - # If our parent changed sucide - if self.ppid != os.getppid(): - _logger.info("Worker (%s) Parent changed", self.pid) - self.alive = False - # check for lifetime - if self.request_count >= self.request_max: - _logger.info("Worker (%d) max request (%s) reached.", self.pid, self.request_count) - self.alive = False - # Reset the worker if it consumes too much memory (e.g. caused by a memory leak). - rss, vms = psutil.Process(os.getpid()).get_memory_info() - if vms > config['limit_memory_soft']: - _logger.info('Worker (%d) virtual memory limit (%s) reached.', self.pid, vms) - self.alive = False # Commit suicide after the request. - - # VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space - soft, hard = resource.getrlimit(resource.RLIMIT_AS) - resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard)) - - # SIGXCPU (exceeded CPU time) signal handler will raise an exception. - r = resource.getrusage(resource.RUSAGE_SELF) - cpu_time = r.ru_utime + r.ru_stime - def time_expired(n, stack): - _logger.info('Worker (%d) CPU time limit (%s) reached.', config['limit_time_cpu']) - # We dont suicide in such case - raise Exception('CPU time limit exceeded.') - signal.signal(signal.SIGXCPU, time_expired) - soft, hard = resource.getrlimit(resource.RLIMIT_CPU) - resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + config['limit_time_cpu'], hard)) - - def process_work(self): - pass - - def start(self): - self.pid = os.getpid() - self.setproctitle() - _logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid) - # Reseed the random number generator - random.seed() - # Prevent fd inherientence close_on_exec - flags = fcntl.fcntl(self.multi.socket, fcntl.F_GETFD) | fcntl.FD_CLOEXEC - fcntl.fcntl(self.multi.socket, fcntl.F_SETFD, flags) - # reset blocking status - self.multi.socket.setblocking(0) - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, signal.SIG_DFL) - signal.signal(signal.SIGCHLD, signal.SIG_DFL) - - def stop(self): - pass - - def run(self): - try: - self.start() - while self.alive: - self.process_limit() - self.multi.pipe_ping(self.watchdog_pipe) - self.sleep() - self.process_work() - _logger.info("Worker (%s) exiting. request_count: %s.", self.pid, self.request_count) - self.stop() - except Exception,e: - _logger.exception("Worker (%s) Exception occured, exiting..." % self.pid) - # should we use 3 to abort everything ? - sys.exit(1) - -class WorkerHTTP(Worker): - """ HTTP Request workers """ - def process_request(self, client, addr): - client.setblocking(1) - client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - # Prevent fd inherientence close_on_exec - flags = fcntl.fcntl(client, fcntl.F_GETFD) | fcntl.FD_CLOEXEC - fcntl.fcntl(client, fcntl.F_SETFD, flags) - # do request using WorkerBaseWSGIServer monkey patched with socket - self.server.socket = client - # tolerate broken pipe when the http client closes the socket before - # receiving the full reply - try: - self.server.process_request(client,addr) - except IOError, e: - if e.errno != errno.EPIPE: - raise - self.request_count += 1 - - def process_work(self): - try: - client, addr = self.multi.socket.accept() - self.process_request(client, addr) - except socket.error, e: - if e[0] not in (errno.EAGAIN, errno.ECONNABORTED): - raise - - def start(self): - Worker.start(self) - self.server = WorkerBaseWSGIServer(self.multi.app) - -class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer): - """ werkzeug WSGI Server patched to allow using an external listen socket - """ - def __init__(self, app): - werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app) - def server_bind(self): - # we dont bind beause we use the listen socket of Multicorn#socket - # instead we close the socket - if self.socket: - self.socket.close() - def server_activate(self): - # dont listen as we use Multicorn#socket - pass - -class WorkerCron(Worker): - """ Cron workers """ - - def __init__(self, multi): - super(WorkerCron, self).__init__(multi) - # process_work() below process a single database per call. - # The variable db_index is keeping track of the next database to - # process. - self.db_index = 0 - - def sleep(self): - # Really sleep once all the databases have been processed. - if self.db_index == 0: - interval = 60 + self.pid % 10 # chorus effect - time.sleep(interval) - - def _db_list(self): - if config['db_name']: - db_names = config['db_name'].split(',') - else: - db_names = openerp.service.db.exp_list(True) - return db_names - - def process_work(self): - rpc_request = logging.getLogger('openerp.netsvc.rpc.request') - rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) - _logger.debug("WorkerCron (%s) polling for jobs", self.pid) - db_names = self._db_list() - if len(db_names): - self.db_index = (self.db_index + 1) % len(db_names) - db_name = db_names[self.db_index] - self.setproctitle(db_name) - if rpc_request_flag: - start_time = time.time() - start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() - - import openerp.addons.base as base - base.ir.ir_cron.ir_cron._acquire_job(db_name) - openerp.modules.registry.RegistryManager.delete(db_name) - - # dont keep cursors in multi database mode - if len(db_names) > 1: - openerp.sql_db.close_db(db_name) - if rpc_request_flag: - end_time = time.time() - end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() - logline = '%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (db_name, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) - _logger.debug("WorkerCron (%s) %s", self.pid, logline) - - self.request_count += 1 - if self.request_count >= self.request_max and self.request_max < len(db_names): - _logger.error("There are more dabatases to process than allowed " - "by the `limit_request` configuration variable: %s more.", - len(db_names) - self.request_max) - else: - self.db_index = 0 - - def start(self): - os.nice(10) # mommy always told me to be nice with others... - Worker.start(self) - self.multi.socket.close() - openerp.service.start_internal() - - # chorus effect: make cron workers do not all start at first database - mct = config['max_cron_threads'] - p = float(self.pid % mct) / mct - self.db_index = int(len(self._db_list()) * p) - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index acbfc67dd0b..506a07d0295 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -57,13 +57,7 @@ RPC_FAULT_CODE_WARNING = 2 RPC_FAULT_CODE_ACCESS_DENIED = 3 RPC_FAULT_CODE_ACCESS_ERROR = 4 -# The new (6.1) versioned RPC paths. -XML_RPC_PATH = '/openerp/xmlrpc' -XML_RPC_PATH_1 = '/openerp/xmlrpc/1' -JSON_RPC_PATH = '/openerp/jsonrpc' -JSON_RPC_PATH_1 = '/openerp/jsonrpc/1' - -def xmlrpc_return(start_response, service, method, params, legacy_exceptions=False): +def xmlrpc_return(start_response, service, method, params, string_faultcode=False): """ Helper to call a service's method with some params, using a wsgi-supplied ``start_response`` callback. @@ -81,14 +75,14 @@ def xmlrpc_return(start_response, service, method, params, legacy_exceptions=Fal result = openerp.netsvc.dispatch_rpc(service, method, params) response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) except Exception, e: - if legacy_exceptions: - response = xmlrpc_handle_exception_legacy(e) + if string_faultcode: + response = xmlrpc_handle_exception_string(e) else: - response = xmlrpc_handle_exception(e) + response = xmlrpc_handle_exception_int(e) start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) return [response] -def xmlrpc_handle_exception(e): +def xmlrpc_handle_exception_int(e): if isinstance(e, openerp.osv.orm.except_orm): # legacy fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, openerp.tools.ustr(e.value)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) @@ -121,7 +115,7 @@ def xmlrpc_handle_exception(e): response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) return response -def xmlrpc_handle_exception_legacy(e): +def xmlrpc_handle_exception_string(e): if isinstance(e, openerp.osv.orm.except_orm): fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + e.value, '') response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) @@ -146,82 +140,29 @@ def xmlrpc_handle_exception_legacy(e): response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) return response -def wsgi_xmlrpc_1(environ, start_response): - """ The main OpenERP WSGI handler.""" - if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH_1): - length = int(environ['CONTENT_LENGTH']) - data = environ['wsgi.input'].read(length) - - params, method = xmlrpclib.loads(data) - - path = environ['PATH_INFO'][len(XML_RPC_PATH_1):] - if path.startswith('/'): path = path[1:] - if path.endswith('/'): path = path[:-1] - path = path.split('/') - - # All routes are hard-coded. - - # No need for a db segment. - if len(path) == 1: - service = path[0] - - if service == 'common': - if method in ('server_version',): - service = 'db' - return xmlrpc_return(start_response, service, method, params) - - # A db segment must be given. - elif len(path) == 2: - service, db_name = path - params = (db_name,) + params - - return xmlrpc_return(start_response, service, method, params) - - # A db segment and a model segment must be given. - elif len(path) == 3 and path[0] == 'model': - service, db_name, model_name = path - params = (db_name,) + params[:2] + (model_name,) + params[2:] - service = 'object' - return xmlrpc_return(start_response, service, method, params) - - # The body has been read, need to raise an exception (not return None). - fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '') - response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) - start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) - return [response] - def wsgi_xmlrpc(environ, start_response): - """ WSGI handler to return the versions.""" - if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith(XML_RPC_PATH): - length = int(environ['CONTENT_LENGTH']) - data = environ['wsgi.input'].read(length) + """ Two routes are available for XML-RPC - params, method = xmlrpclib.loads(data) + /xmlrpc/ route returns faultCode as strings. This is a historic + violation of the protocol kept for compatibility. - path = environ['PATH_INFO'][len(XML_RPC_PATH):] - if path.startswith('/'): path = path[1:] - if path.endswith('/'): path = path[:-1] - path = path.split('/') - - # All routes are hard-coded. - - if len(path) == 1 and path[0] == '' and method in ('version',): - return xmlrpc_return(start_response, 'common', method, ()) - - # The body has been read, need to raise an exception (not return None). - fault = xmlrpclib.Fault(RPC_FAULT_CODE_CLIENT_ERROR, '') - response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) - start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) - return [response] - -def wsgi_xmlrpc_legacy(environ, start_response): + /xmlrpc/2/ is a new route that returns faultCode as int and is + therefore fully compliant. + """ if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'): length = int(environ['CONTENT_LENGTH']) data = environ['wsgi.input'].read(length) - path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ... + + # Distinguish betweed the 2 faultCode modes + string_faultcode = True + if environ['PATH_INFO'].startswith('/xmlrpc/2/'): + service = environ['PATH_INFO'][len('/xmlrpc/2/'):] + string_faultcode = False + else: + service = environ['PATH_INFO'][len('/xmlrpc/'):] params, method = xmlrpclib.loads(data) - return xmlrpc_return(start_response, path, method, params, True) + return xmlrpc_return(start_response, service, method, params, string_faultcode) def wsgi_webdav(environ, start_response): pi = environ['PATH_INFO'] @@ -400,10 +341,8 @@ def application_unproxied(environ, start_response): if hasattr(threading.current_thread(), 'dbname'): del threading.current_thread().dbname - openerp.service.start_internal() - # Try all handlers until one returns some result (i.e. not None). - wsgi_handlers = [wsgi_xmlrpc_1, wsgi_xmlrpc, wsgi_xmlrpc_legacy, wsgi_webdav] + wsgi_handlers = [wsgi_xmlrpc, wsgi_webdav] wsgi_handlers += module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) @@ -422,69 +361,5 @@ def application(environ, start_response): else: return application_unproxied(environ, start_response) -# The WSGI server, started by start_server(), stopped by stop_server(). -httpd = None - -def serve(interface, port, threaded): - """ Serve HTTP requests via werkzeug development server. - - Calling this function is blocking, you might want to call it in its own - thread. - """ - - global httpd - if not openerp.evented: - httpd = werkzeug.serving.make_server(interface, port, application, threaded=threaded) - else: - from gevent.wsgi import WSGIServer - httpd = WSGIServer((interface, port), application) - httpd.serve_forever() - -def start_service(): - """ Call serve() in its own thread. - - The WSGI server can be shutdown with stop_server() below. - """ - # TODO Change the xmlrpc_* options to http_* - interface = config['xmlrpc_interface'] or '0.0.0.0' - port = config['xmlrpc_port'] - _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port) - if not openerp.evented: - threading.Thread(target=serve, args=(interface, port, True)).start() - else: - serve(interface, port, True) - -def stop_service(): - """ Initiate the shutdown of the WSGI server. - - The server is supposed to have been started by start_server() above. - """ - if httpd: - if not openerp.evented: - httpd.shutdown() - close_socket(httpd.socket) - else: - import gevent - httpd.stop() - gevent.shutdown() - -def close_socket(sock): - """ Closes a socket instance cleanly - - :param sock: the network socket to close - :type sock: socket.socket - """ - try: - sock.shutdown(socket.SHUT_RDWR) - except socket.error, e: - # On OSX, socket shutdowns both sides if any side closes it - # causing an error 57 'Socket is not connected' on shutdown - # of the other side (or something), see - # http://bugs.python.org/issue4397 - # note: stdlib fixed test, not behavior - if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']: - raise - sock.close() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/common.py b/openerp/tests/common.py index f8472e81fcf..df00e61f140 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -135,11 +135,11 @@ class RpcCase(unittest2.TestCase): self.proxy.object_60 = xmlrpclib.ServerProxy(url_60 + 'object') #self.proxy.edi_60 = xmlrpclib.ServerProxy(url_60 + 'edi') - # Use the new (6.1) API. - self.proxy.url_61 = url_61 = 'http://%s:%d/openerp/xmlrpc/1/' % (HOST, PORT) - self.proxy.common_61 = xmlrpclib.ServerProxy(url_61 + 'common') - self.proxy.db_61 = xmlrpclib.ServerProxy(url_61 + 'db') - self.proxy.model_61 = xmlrpclib.ServerProxy(url_61 + 'model/' + DB) + # Use the new (8) API. + self.proxy.url_8 = url_8 = 'http://%s:%d/xmlrpc/2/' % (HOST, PORT) + self.proxy.common_8 = xmlrpclib.ServerProxy(url_8 + 'common') + self.proxy.db_8 = xmlrpclib.ServerProxy(url_8 + 'db') + self.proxy.object_8 = xmlrpclib.ServerProxy(url_8 + 'object') @classmethod def generate_database_name(cls): diff --git a/openerp/tests/test_xmlrpc.py b/openerp/tests/test_xmlrpc.py index 8c9d6911e7b..12bc828cb29 100644 --- a/openerp/tests/test_xmlrpc.py +++ b/openerp/tests/test_xmlrpc.py @@ -47,21 +47,18 @@ class test_xmlrpc(common.RpcCase): def test_xmlrpc_ir_model_search(self): """ Try a search on the object service. """ - ids = self.proxy.object_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, - 'ir.model', 'search', []) + o = self.proxy.object_60 + ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', []) assert ids - ids = self.proxy.object_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, - 'ir.model', 'search', [], {}) + ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [], {}) assert ids - def test_xmlrpc_61_ir_model_search(self): + def test_xmlrpc_8_ir_model_search(self): """ Try a search on the object service. """ - - proxy = xmlrpclib.ServerProxy(self.proxy.url_61 + 'model/' + DB + - '/ir.model') - ids = proxy.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'search', []) + o = self.proxy.object_8 + ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', []) assert ids - ids = proxy.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'search', [], {}) + ids = o.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [], {}) assert ids # This test was written to test the creation of a new RPC endpoint, not diff --git a/openerp/tools/config.py b/openerp/tools/config.py index cc3c187c775..ebfd8cea20d 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -106,7 +106,6 @@ class configmanager(object): help="specify additional addons paths (separated by commas).", action="callback", callback=self._check_addons_path, nargs=1, type="string") group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web") - group.add_option("--gevent", dest="gevent", action="store_true", my_default=False, help="Activate the GEvent mode, this also desactivate the cron.") parser.add_option_group(group) # XML-RPC / HTTP @@ -246,6 +245,7 @@ class configmanager(object): # Advanced options group = optparse.OptionGroup(parser, "Advanced options") + group.add_option('--auto-reload', dest='auto_reload', action='store_true', my_default=False, help='enable auto reload') group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode') group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, help="stop the server after its initialization") @@ -399,7 +399,7 @@ class configmanager(object): 'list_db', 'xmlrpcs', 'proxy_mode', 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', - 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'gevent' + 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'auto_reload' ] for arg in keys: diff --git a/openerpcommand/web.py b/openerpcommand/web.py index 5875978ad32..5d9d429244b 100644 --- a/openerpcommand/web.py +++ b/openerpcommand/web.py @@ -30,9 +30,9 @@ def run(args): import gevent import gevent.monkey import gevent.wsgi - import gevent_psycopg2 + import psycogreen.gevent gevent.monkey.patch_all() - gevent_psycopg2.monkey_patch() + psycogreen.gevent.patch_psycopg() import threading import openerp import openerp.cli.server diff --git a/setup.py b/setup.py index ff801a97933..4f4ffe6e505 100644 --- a/setup.py +++ b/setup.py @@ -120,7 +120,7 @@ setuptools.setup( 'feedparser', 'gdata', 'gevent', - 'gevent-psycopg2', + 'psycogreen', 'Jinja2', 'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/ 'mako',