[MERGE] Sync with trunk until revision 4967 (including al cleaning)
bzr revid: tde@openerp.com-20131007081039-adyay7oy1tpx4g2k
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
##############################################################################
|
||||
|
||||
import ir
|
||||
import workflow
|
||||
import module
|
||||
import res
|
||||
import report
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
<menuitem id="menu_email" name="Email" parent="menu_custom" sequence="1"/>
|
||||
<menuitem id="menu_security" name="Security" parent="menu_custom" sequence="25"/>
|
||||
<menuitem id="menu_ir_property" name="Parameters" parent="menu_custom" sequence="24"/>
|
||||
<menuitem id="next_id_4" name="Low Level Objects" parent="menu_custom" sequence="30"/>
|
||||
|
||||
<record id="action_client_base_menu" model="ir.actions.client">
|
||||
<field name="name">Open Settings Menu</field>
|
||||
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.2 KiB |
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- Actions -->
|
||||
|
||||
<!-- ir.actions -->
|
||||
|
||||
<record id="action_view" model="ir.ui.view">
|
||||
<field name="name">ir.actions.actions</field>
|
||||
<field name="model">ir.actions.actions</field>
|
||||
|
@ -34,7 +36,6 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_sequence_actions" model="ir.actions.act_window">
|
||||
<field name="name">Actions</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
@ -46,7 +47,7 @@
|
|||
<menuitem id="next_id_6" name="Actions" parent="base.menu_custom" sequence="2"/>
|
||||
<menuitem action="ir_sequence_actions" id="menu_ir_sequence_actions" parent="next_id_6"/>
|
||||
|
||||
|
||||
<!-- ir.actions.report.xml -->
|
||||
|
||||
<record id="act_report_xml_view" model="ir.ui.view">
|
||||
<field name="name">ir.actions.report.xml</field>
|
||||
|
@ -121,7 +122,6 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_action_report_xml" model="ir.actions.act_window">
|
||||
<field name="name">Reports</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
@ -132,6 +132,8 @@
|
|||
</record>
|
||||
<menuitem action="ir_action_report_xml" id="menu_ir_action_report_xml" parent="base.next_id_6"/>
|
||||
|
||||
<!-- ir.actions.act_window -->
|
||||
|
||||
<record id="view_window_action_tree" model="ir.ui.view">
|
||||
<field name="name">ir.actions.windows.tree</field>
|
||||
<field name="model">ir.actions.act_window</field>
|
||||
|
@ -145,7 +147,6 @@
|
|||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_window_action_form" model="ir.ui.view">
|
||||
<field name="name">ir.actions.windows.form</field>
|
||||
<field name="model">ir.actions.act_window</field>
|
||||
|
@ -221,7 +222,6 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_action_window" model="ir.actions.act_window">
|
||||
<field name="name">Window Actions</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
@ -243,66 +243,7 @@
|
|||
</record>
|
||||
<menuitem action="ir_action_window" id="menu_ir_action_window" parent="base.next_id_6"/>
|
||||
|
||||
<record id="act_wizard_view_tree" model="ir.ui.view">
|
||||
<field name="name">ir.actions.wizard.tree</field>
|
||||
<field name="model">ir.actions.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Wizard">
|
||||
<field name="name"/>
|
||||
<field name="wiz_name"/>
|
||||
<field name="multi"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_wizard_view" model="ir.ui.view">
|
||||
<field name="name">ir.actions.wizard</field>
|
||||
<field name="model">ir.actions.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Wizards" version="7.0">
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="type"/>
|
||||
<field name="wiz_name"/>
|
||||
<field name="multi"/>
|
||||
</group>
|
||||
<label for="groups_id"/>
|
||||
<field name="groups_id"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_wizard_search_view" model="ir.ui.view">
|
||||
<field name="name">ir.actions.wizard.search</field>
|
||||
<field name="model">ir.actions.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Wizards">
|
||||
<field name="name"
|
||||
filter_domain="['|', '|', ('name','ilike',self), ('type','ilike',self), ('wiz_name','ilike',self)]"
|
||||
string="Wizard"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_action_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Wizards</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">ir.actions.wizard</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="search_view_id" ref="act_wizard_search_view"/>
|
||||
</record>
|
||||
|
||||
<menuitem action="ir_action_wizard" id="menu_ir_action_wizard" parent="base.next_id_6"/>
|
||||
|
||||
|
||||
<record id="property_rule" model="ir.rule">
|
||||
<field name="name">Property multi-company</field>
|
||||
<field name="model_id" ref="model_ir_property"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<!--server action view-->
|
||||
<!-- ir.actions.server -->
|
||||
|
||||
<record id="view_server_action_form" model="ir.ui.view">
|
||||
<field name="name">Server Action</field>
|
||||
|
@ -514,7 +455,6 @@
|
|||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_server_action_tree" model="ir.ui.view">
|
||||
<field name="name">Server Actions</field>
|
||||
<field name="model">ir.actions.server</field>
|
||||
|
@ -527,7 +467,6 @@
|
|||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_server_action_search" model="ir.ui.view">
|
||||
<field name="name">ir.actions.server.search</field>
|
||||
<field name="model">ir.actions.server</field>
|
||||
|
@ -542,7 +481,6 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_server_action" model="ir.actions.act_window">
|
||||
<field name="name">Server Actions</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
@ -573,7 +511,6 @@
|
|||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="config_wizard_step_view_form" model="ir.ui.view">
|
||||
<field name="model">ir.actions.todo</field>
|
||||
<field name="name">Config Wizard Steps</field>
|
||||
|
@ -602,7 +539,6 @@
|
|||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="config_wizard_step_view_search" model="ir.ui.view">
|
||||
<field name="model">ir.actions.todo</field>
|
||||
<field name="name">ir.actions.todo.select</field>
|
||||
|
@ -614,7 +550,6 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_ir_actions_todo_form" model="ir.actions.act_window">
|
||||
<field name="name">Configuration Wizards</field>
|
||||
<field name="res_model">ir.actions.todo</field>
|
||||
|
@ -622,9 +557,7 @@
|
|||
<field name="view_type">form</field>
|
||||
<field name="help">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.</field>
|
||||
</record>
|
||||
<menuitem id="menu_ir_actions_todo" name="Configuration Wizards" parent="menu_custom" sequence="20" groups="base.group_no_one"/>
|
||||
<menuitem id="menu_ir_actions_todo_form" action="act_ir_actions_todo_form" parent="menu_ir_actions_todo"/>
|
||||
|
||||
<menuitem id="menu_ir_actions_todo_form" action="act_ir_actions_todo_form" parent="base.next_id_6"/>
|
||||
<record id="action_run_ir_action_todo" model="ir.actions.server">
|
||||
<field name="name">Run Remaining Action Todo</field>
|
||||
<field name="condition">True</field>
|
||||
|
@ -638,6 +571,5 @@ if config.get('type') not in ('ir.actions.act_window_close',):
|
|||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
<field name="view_id" eval="False"/>
|
||||
<field name="search_view_id" ref="view_attachment_search"/>
|
||||
</record>
|
||||
<menuitem action="action_attachment" id="menu_action_attachment" parent="base.next_id_4"/>
|
||||
<menuitem action="action_attachment" id="menu_action_attachment" parent="base.next_id_9"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -86,8 +86,7 @@
|
|||
<field name="context">{'active_test': False}</field>
|
||||
<field name="view_id" ref="ir_cron_view_tree"/>
|
||||
</record>
|
||||
<menuitem id="menu_ir_cron" name="Scheduler" parent="menu_custom" groups="base.group_no_one" sequence="23"/>
|
||||
<menuitem id="menu_ir_cron_act" action="ir_cron_act" parent="menu_ir_cron"/>
|
||||
<menuitem id="menu_ir_cron_act" action="ir_cron_act" parent="base.next_id_6"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
@ -1,6 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- menu_create from model form -->
|
||||
<record id="view_model_menu_create" model="ir.ui.view">
|
||||
<field name="name">Create Menu</field>
|
||||
<field name="model">wizard.ir.model.menu.create</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Menu" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="menu_id" domain="[('parent_id','<>',False)]"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="menu_create" string="Create _Menu" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window context="{'model_id': active_id}" id="act_menu_create" name="Create Menu" res_model="wizard.ir.model.menu.create" target="new" view_mode="form"/>
|
||||
|
||||
<!-- model -->
|
||||
<record id="view_model_form" model="ir.ui.view">
|
||||
<field name="model">ir.model</field>
|
||||
|
|
|
@ -85,5 +85,15 @@
|
|||
<field name="search_view_id" ref="view_rule_search"/>
|
||||
</record>
|
||||
<menuitem action="action_rule" id="menu_action_rule" parent="base.menu_security" sequence="3"/>
|
||||
|
||||
|
||||
<record id="property_rule" model="ir.rule">
|
||||
<field name="name">Property multi-company</field>
|
||||
<field name="model_id" ref="model_ir_property"/>
|
||||
<field eval="True" name="global"/>
|
||||
<field name="domain_force">['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -115,27 +115,5 @@
|
|||
</record>
|
||||
<menuitem id="menu_action_ui_view_custom" action="action_ui_view_custom" parent="base.next_id_2" sequence="3"/>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<record id="shortcut_form" model="ir.ui.view">
|
||||
<field name="model">ir.ui.view_sc</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Shortcut" version="7.0">
|
||||
<group col="4">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="shortcut_tree" model="ir.ui.view">
|
||||
<field name="model">ir.ui.view_sc</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Shortcut">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import wizard_menu
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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:
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="view_model_menu_create" model="ir.ui.view">
|
||||
<field name="name">Create Menu</field>
|
||||
<field name="model">wizard.ir.model.menu.create</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create Menu" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="menu_id" domain="[('parent_id','<>',False)]"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="menu_create" string="Create _Menu" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window context="{'model_id': active_id}" id="act_menu_create" name="Create Menu" res_model="wizard.ir.model.menu.create" target="new" view_mode="form"/>
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import workflow
|
||||
import print_instance
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
#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:
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<addresses>
|
||||
<address type="fields" name="id">
|
||||
<name type="field" name="name"/>
|
||||
<role type="field" name="title"/>
|
||||
<phone type="field" name="phone"/>
|
||||
<email type="field" name="email"/>
|
||||
<mobile type="field" name="mobile"/>
|
||||
<company>Tiny sprl</company>
|
||||
<street>Rue du Vieux Chateau, 21</street>
|
||||
<zip>BE-1457</zip>
|
||||
<city>Walhain</city>
|
||||
<website>http://tiny.be</website>
|
||||
</address>
|
||||
</addresses>
|
|
@ -1,80 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
|
||||
|
||||
<xsl:variable name="initial_bottom_pos">20.5</xsl:variable>
|
||||
<xsl:variable name="initial_left_pos">0.5</xsl:variable>
|
||||
<xsl:variable name="height_increment">5.5</xsl:variable>
|
||||
<xsl:variable name="width_increment">8.5</xsl:variable>
|
||||
<xsl:variable name="frame_height">5.5cm</xsl:variable>
|
||||
<xsl:variable name="frame_width">8.5cm</xsl:variable>
|
||||
<xsl:variable name="number_columns">3</xsl:variable>
|
||||
<xsl:variable name="max_cards">8</xsl:variable>
|
||||
|
||||
<xsl:template match="/">
|
||||
<xsl:apply-templates select="addresses"/>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="addresses">
|
||||
<document>
|
||||
<template leftMargin="2.0cm" rightMargin="2.0cm" topMargin="2.0cm"
|
||||
bottomMargin="2.0cm" title="Address list" author="Generated by OpenERP, Fabien Pinckaers">
|
||||
<pageTemplate id="all">
|
||||
<pageGraphics/>
|
||||
<xsl:apply-templates select="address" mode="frames"/>
|
||||
</pageTemplate>
|
||||
</template>
|
||||
<stylesheet>
|
||||
<paraStyle name="nospace" fontName="Courier" fontSize="12" spaceBefore="0" spaceAfter="0"/>
|
||||
</stylesheet>
|
||||
<story>
|
||||
<xsl:apply-templates select="address" mode="story"/>
|
||||
</story>
|
||||
</document>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="address" mode="frames">
|
||||
<xsl:if test="position() < $max_frames + 1">
|
||||
<frame>
|
||||
<xsl:attribute name="width">
|
||||
<xsl:value-of select="$frame_width"/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name="height">
|
||||
<xsl:value-of select="$frame_height"/>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name="x1">
|
||||
<xsl:value-of select="$initial_left_pos + ((position()-1) mod $number_columns) * $width_increment"/>
|
||||
<xsl:text>cm</xsl:text>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name="y1">
|
||||
<xsl:value-of select="$initial_bottom_pos - floor((position()-1) div $number_columns) * $height_increment"/>
|
||||
<xsl:text>cm</xsl:text>
|
||||
</xsl:attribute>
|
||||
</frame>
|
||||
</xsl:if>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="address" mode="story">
|
||||
|
||||
<drawCentredString x="105mm" y="28cm" t="1">PACKINGLIST</drawCentredString>
|
||||
<image x="5mm" y="5m" file="addons/base/report/logo-tiny.png"/>
|
||||
<blockTable colWidths="3cm,3cm" style="infos">
|
||||
<tr>
|
||||
<td>Logo</td>
|
||||
<td/>
|
||||
</tr><tr>
|
||||
<td><para><xsl:value-of select="client-id"/></para></td>
|
||||
<td><para><xsl:value-of select="shipping-id"/></para></td>
|
||||
<td><para><xsl:value-of select="picking-date"/></para></td>
|
||||
<td><para><xsl:value-of select="expedition-date"/></para></td>
|
||||
<td><para><xsl:value-of select="command-number"/></para></td>
|
||||
</tr>
|
||||
</blockTable>
|
||||
|
||||
<para style="nospace"><xsl:value-of select="company"/></para>
|
||||
<para style="nospace"><xsl:value-of select="partner_name"/></para>
|
||||
<para style="nospace"><xsl:value-of select="street"/></para>
|
||||
<para style="nospace"><xsl:value-of select="zip"/><xsl:text> </xsl:text><xsl:value-of select="city"/></para>
|
||||
<para style="nospace"><xsl:value-of select="country"/></para>
|
||||
<nextFrame/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
|
@ -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:
|
||||
|
|
|
@ -17,23 +17,6 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_config_wizard_form" model="ir.ui.view">
|
||||
<field name="name">Compabitiliby configuration wizard</field>
|
||||
<field name="model">ir.actions.configuration.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Next Configuration Step" version="7.0">
|
||||
<group>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_next" type="object" string="Continue" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_config_installer" model="ir.ui.view">
|
||||
<field name="name">Inheritable view for installer objects</field>
|
||||
<field name="model">res.config.installer</field>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<report id="res_partner_address_report" model="res.partner" name="res.partner" string="Labels" xml="base/res/report/partner_address.xml" xsl="base/res/report/partner_address.xsl" groups="base.group_no_one"/>
|
||||
<!--
|
||||
<report string="Business Cards" model="res.partner" name="res.partner.businesscard" xml="base/res/report/business_card.xml" xsl="base/res/report/business_card.xsl"/>
|
||||
-->
|
||||
<report
|
||||
id="res_partner_address_report"
|
||||
model="res.partner"
|
||||
name="res.partner"
|
||||
string="Labels"
|
||||
xml="base/res/res_partner_report_address.xml"
|
||||
xsl="base/res/res_partner_report_address.xsl"
|
||||
groups="base.group_no_one"/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="ir_ui_view_sc_partner0" model="ir.ui.view_sc">
|
||||
<field name="name">Customers</field>
|
||||
<field name="resource">ir.ui.menu</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="res_id" ref="menu_partner_form"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -35,16 +35,15 @@
|
|||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_request_link-act" model="ir.actions.act_window">
|
||||
<field name="name">Request Reference Types</field>
|
||||
<field name="name">Referencable Models</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.request.link</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="search_view_id" ref="res_request_link_search_view"/>
|
||||
</record>
|
||||
<menuitem action="res_request_link-act" id="menu_res_request_link_act" parent="base.next_id_4" />
|
||||
<menuitem action="res_request_link-act" id="menu_res_request_link_act" parent="base.next_id_9"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,6 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- change password wizard -->
|
||||
<record id="change_password_wizard_view" model="ir.ui.view">
|
||||
<field name="name">Change Password</field>
|
||||
<field name="model">change.password.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Change Password" version="7.0">
|
||||
<field name="user_ids"/>
|
||||
<footer>
|
||||
<button string="Change Password" name="change_password_button" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="change_password_wizard_user_tree_view" model="ir.ui.view">
|
||||
<field name="name">Change Password Users</field>
|
||||
<field name="model">change.password.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- the user list is editable, but one cannot add or delete rows -->
|
||||
<tree string="Users" editable="bottom" create="false" delete="false">
|
||||
<field name="user_login"/>
|
||||
<field name="new_passwd" required="True" password="True"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<act_window id="change_password_wizard_action"
|
||||
name="Change Password"
|
||||
src_model="res.users"
|
||||
res_model="change.password.wizard"
|
||||
view_type="form" view_mode="form"
|
||||
key2="client_action_multi" target="new"
|
||||
groups="base.group_erp_manager"/>
|
||||
|
||||
<!-- res.groups -->
|
||||
<record id="view_groups_search" model="ir.ui.view">
|
||||
<field name="name">res.groups.search</field>
|
||||
|
@ -277,5 +312,6 @@
|
|||
<field name="act_window_id" ref="action_res_users_my"/>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import change_password_wizard
|
|
@ -1,81 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
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})
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<!-- wizard action on res.users -->
|
||||
<act_window id="change_password_wizard_action"
|
||||
name="Change Password"
|
||||
src_model="res.users"
|
||||
res_model="change.password.wizard"
|
||||
view_type="form" view_mode="form"
|
||||
key2="client_action_multi" target="new"
|
||||
groups="base.group_erp_manager"/>
|
||||
|
||||
<!-- wizard view -->
|
||||
<record id="change_password_wizard_view" model="ir.ui.view">
|
||||
<field name="name">Change Password</field>
|
||||
<field name="model">change.password.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Change Password" version="7.0">
|
||||
<field name="user_ids"/>
|
||||
<footer>
|
||||
<button string="Change Password" name="change_password_button" type="object" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- wizard user list view -->
|
||||
<record id="change_password_wizard_user_tree_view" model="ir.ui.view">
|
||||
<field name="name">Change Password Users</field>
|
||||
<field name="model">change.password.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- the user list is editable, but one cannot add or delete rows -->
|
||||
<tree string="Users" editable="bottom" create="false" delete="false">
|
||||
<field name="user_login"/>
|
||||
<field name="new_passwd" required="True" password="True"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -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
|
||||
|
|
|
|
@ -0,0 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import workflow
|
||||
import workflow_report
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -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...')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" 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:
|
|
@ -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:
|
|
@ -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:
|
|
@ -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/<service> 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/<service> 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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|