[merge] merge from lp:~xrg/openobject-server/optimize-5.0 and removed some confilts
bzr revid: hmo@tinyerp.com-20091120143104-tb6136unkdw7yfy9
This commit is contained in:
commit
dad1ff955b
|
@ -45,11 +45,17 @@ from cStringIO import StringIO
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
_ad = os.path.abspath(opj(tools.config['root_path'], 'addons')) # default addons path (base)
|
_ad = os.path.abspath(opj(tools.config['root_path'], 'addons')) # default addons path (base)
|
||||||
ad = os.path.abspath(tools.config['addons_path']) # alternate addons path
|
ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
|
||||||
|
|
||||||
sys.path.insert(1, _ad)
|
sys.path.insert(1, _ad)
|
||||||
if ad != _ad:
|
|
||||||
sys.path.insert(1, ad)
|
ad_cnt=1
|
||||||
|
for adp in ad_paths:
|
||||||
|
if adp != _ad:
|
||||||
|
sys.path.insert(ad_cnt, adp)
|
||||||
|
ad_cnt+=1
|
||||||
|
|
||||||
|
ad_paths.append(_ad) # for get_module_path
|
||||||
|
|
||||||
# Modules already loaded
|
# Modules already loaded
|
||||||
loaded = []
|
loaded = []
|
||||||
|
@ -153,11 +159,10 @@ class Node(Singleton):
|
||||||
|
|
||||||
def get_module_path(module, downloaded=False):
|
def get_module_path(module, downloaded=False):
|
||||||
"""Return the path of the given module."""
|
"""Return the path of the given module."""
|
||||||
if os.path.exists(opj(ad, module)) or os.path.exists(opj(ad, '%s.zip' % module)):
|
for adp in ad_paths:
|
||||||
return opj(ad, module)
|
if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
|
||||||
|
return opj(adp, module)
|
||||||
|
|
||||||
if os.path.exists(opj(_ad, module)) or os.path.exists(opj(_ad, '%s.zip' % module)):
|
|
||||||
return opj(_ad, module)
|
|
||||||
if downloaded:
|
if downloaded:
|
||||||
return opj(_ad, module)
|
return opj(_ad, module)
|
||||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
|
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
|
||||||
|
@ -282,7 +287,10 @@ def get_modules():
|
||||||
return os.path.isdir(name) or zipfile.is_zipfile(name)
|
return os.path.isdir(name) or zipfile.is_zipfile(name)
|
||||||
return map(clean, filter(is_really_module, os.listdir(dir)))
|
return map(clean, filter(is_really_module, os.listdir(dir)))
|
||||||
|
|
||||||
return list(set(listdir(ad) + listdir(_ad)))
|
plist = []
|
||||||
|
for ad in ad_paths:
|
||||||
|
plist.extend(listdir(ad))
|
||||||
|
return list(set(plist))
|
||||||
|
|
||||||
def get_modules_with_version():
|
def get_modules_with_version():
|
||||||
modules = get_modules()
|
modules = get_modules()
|
||||||
|
@ -310,6 +318,7 @@ def upgrade_graph(graph, cr, module_list, force=None):
|
||||||
mod_path = get_module_path(module)
|
mod_path = get_module_path(module)
|
||||||
terp_file = get_module_resource(module, '__terp__.py')
|
terp_file = get_module_resource(module, '__terp__.py')
|
||||||
if not mod_path or not terp_file:
|
if not mod_path or not terp_file:
|
||||||
|
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable' % (module))
|
||||||
cr.execute("update ir_module_module set state=%s where name=%s", ('uninstallable', module))
|
cr.execute("update ir_module_module set state=%s where name=%s", ('uninstallable', module))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -325,6 +334,7 @@ def upgrade_graph(graph, cr, module_list, force=None):
|
||||||
|
|
||||||
dependencies = dict([(p, deps) for p, deps, data in packages])
|
dependencies = dict([(p, deps) for p, deps, data in packages])
|
||||||
current, later = set([p for p, dep, data in packages]), set()
|
current, later = set([p for p, dep, data in packages]), set()
|
||||||
|
|
||||||
while packages and current > later:
|
while packages and current > later:
|
||||||
package, deps, data = packages[0]
|
package, deps, data = packages[0]
|
||||||
|
|
||||||
|
@ -394,7 +404,7 @@ def register_class(m):
|
||||||
try:
|
try:
|
||||||
zip_mod_path = mod_path + '.zip'
|
zip_mod_path = mod_path + '.zip'
|
||||||
if not os.path.isfile(zip_mod_path):
|
if not os.path.isfile(zip_mod_path):
|
||||||
fm = imp.find_module(m, [ad, _ad])
|
fm = imp.find_module(m, ad_paths)
|
||||||
try:
|
try:
|
||||||
imp.load_module(m, *fm)
|
imp.load_module(m, *fm)
|
||||||
finally:
|
finally:
|
||||||
|
@ -571,6 +581,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
|
||||||
has_updates = False
|
has_updates = False
|
||||||
modobj = None
|
modobj = None
|
||||||
|
|
||||||
|
logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
|
||||||
|
|
||||||
for package in graph:
|
for package in graph:
|
||||||
logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
|
logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
|
||||||
migrations.migrate_module(package, 'pre')
|
migrations.migrate_module(package, 'pre')
|
||||||
|
@ -751,7 +763,11 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
||||||
cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
|
cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
|
||||||
for rmod, rid in cr.fetchall():
|
for rmod, rid in cr.fetchall():
|
||||||
uid = 1
|
uid = 1
|
||||||
pool.get(rmod).unlink(cr, uid, [rid])
|
rmod_module= pool.get(rmod)
|
||||||
|
if rmod_module:
|
||||||
|
rmod_module.unlink(cr, uid, [rid])
|
||||||
|
else:
|
||||||
|
logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
|
||||||
cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
|
cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
|
||||||
cr.commit()
|
cr.commit()
|
||||||
#
|
#
|
||||||
|
|
|
@ -574,7 +574,7 @@
|
||||||
<field name="code">mh</field>
|
<field name="code">mh</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="mk" model="res.country">
|
<record id="mk" model="res.country">
|
||||||
<field name="name">Macedonia</field>
|
<field name="name">FYROM</field>
|
||||||
<field name="code">mk</field>
|
<field name="code">mk</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="ml" model="res.country">
|
<record id="ml" model="res.country">
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -114,6 +114,8 @@
|
||||||
<field name="padding"/>
|
<field name="padding"/>
|
||||||
<field name="number_increment"/>
|
<field name="number_increment"/>
|
||||||
<field name="number_next"/>
|
<field name="number_next"/>
|
||||||
|
<field name="weight" />
|
||||||
|
<field name="condition" colspan="4" />
|
||||||
<separator colspan="4" string="Legend (for prefix, suffix)"/>
|
<separator colspan="4" string="Legend (for prefix, suffix)"/>
|
||||||
<group col="8" colspan="4">
|
<group col="8" colspan="4">
|
||||||
<group>
|
<group>
|
||||||
|
|
|
@ -127,6 +127,7 @@ class report_xml(osv.osv):
|
||||||
('html', 'html'),
|
('html', 'html'),
|
||||||
('raw', 'raw'),
|
('raw', 'raw'),
|
||||||
('sxw', 'sxw'),
|
('sxw', 'sxw'),
|
||||||
|
('txt', 'txt'),
|
||||||
('odt', 'odt'),
|
('odt', 'odt'),
|
||||||
('html2html','HTML from HTML'),
|
('html2html','HTML from HTML'),
|
||||||
('mako2html','HTML from HTML(Mako)'),
|
('mako2html','HTML from HTML(Mako)'),
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ir_attachment(osv.osv):
|
||||||
ima = self.pool.get('ir.model.access')
|
ima = self.pool.get('ir.model.access')
|
||||||
if isinstance(ids, (int, long)):
|
if isinstance(ids, (int, long)):
|
||||||
ids = [ids]
|
ids = [ids]
|
||||||
cr.execute('select distinct res_model from ir_attachment where id in ('+','.join(map(str, ids))+')')
|
cr.execute('select distinct res_model from ir_attachment where id = ANY (%s)', (ids,))
|
||||||
for obj in cr.fetchall():
|
for obj in cr.fetchall():
|
||||||
if obj[0]:
|
if obj[0]:
|
||||||
ima.check(cr, uid, obj[0], mode, context=context)
|
ima.check(cr, uid, obj[0], mode, context=context)
|
||||||
|
|
|
@ -132,6 +132,11 @@ class ir_cron(osv.osv, netsvc.Agent):
|
||||||
if not check:
|
if not check:
|
||||||
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
|
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
|
||||||
|
|
||||||
|
except Exception, ex:
|
||||||
|
logger = netsvc.Logger()
|
||||||
|
logger.notifyChannel('cron', netsvc.LOG_WARNING,
|
||||||
|
'Exception in cron:'+str(ex))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
cr.commit()
|
cr.commit()
|
||||||
cr.close()
|
cr.close()
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from osv import fields,osv
|
from osv import fields,osv
|
||||||
|
from tools.safe_eval import safe_eval
|
||||||
import pooler
|
import pooler
|
||||||
|
|
||||||
class ir_sequence_type(osv.osv):
|
class ir_sequence_type(osv.osv):
|
||||||
|
@ -46,12 +47,15 @@ class ir_sequence(osv.osv):
|
||||||
'number_next': fields.integer('Next Number', required=True),
|
'number_next': fields.integer('Next Number', required=True),
|
||||||
'number_increment': fields.integer('Increment Number', required=True),
|
'number_increment': fields.integer('Increment Number', required=True),
|
||||||
'padding' : fields.integer('Number padding', required=True),
|
'padding' : fields.integer('Number padding', required=True),
|
||||||
|
'condition': fields.char('Condition', size=250, help="If set, sequence will only be used in case this python expression matches, and will precede other sequences."),
|
||||||
|
'weight': fields.integer('Weight',required=True, help="If two sequences match, the highest weight will be used.")
|
||||||
}
|
}
|
||||||
_defaults = {
|
_defaults = {
|
||||||
'active': lambda *a: True,
|
'active': lambda *a: True,
|
||||||
'number_increment': lambda *a: 1,
|
'number_increment': lambda *a: 1,
|
||||||
'number_next': lambda *a: 1,
|
'number_next': lambda *a: 1,
|
||||||
'padding' : lambda *a : 0,
|
'padding' : lambda *a : 0,
|
||||||
|
'weight' : lambda *a: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _process(self, s):
|
def _process(self, s):
|
||||||
|
@ -70,10 +74,29 @@ class ir_sequence(osv.osv):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_id(self, cr, uid, sequence_id, test='id=%s', context=None):
|
def get_id(self, cr, uid, sequence_id, test='id=%s', context=None):
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
try:
|
try:
|
||||||
cr.execute('SELECT id, number_next, prefix, suffix, padding FROM ir_sequence WHERE '+test+' AND active=%s FOR UPDATE', (sequence_id, True))
|
cr.execute('SELECT id, number_next, prefix, suffix, padding, condition \
|
||||||
res = cr.dictfetchone()
|
FROM ir_sequence \
|
||||||
if res:
|
WHERE '+test+' AND active=%s ORDER BY weight DESC, length(COALESCE(condition,\'\')) DESC \
|
||||||
|
FOR UPDATE', (sequence_id, True))
|
||||||
|
for res in cr.dictfetchall():
|
||||||
|
if res['condition']:
|
||||||
|
print "ir_seq: %s has condition:" %res['id'], res['condition'],
|
||||||
|
try:
|
||||||
|
bo = safe_eval(res['condition'],context)
|
||||||
|
if not bo:
|
||||||
|
print "not matched"
|
||||||
|
continue
|
||||||
|
except Exception,e:
|
||||||
|
# it would be normal to have exceptions, because
|
||||||
|
# the domain may contain errors
|
||||||
|
print "Exception.\ne:",e
|
||||||
|
print "Context:", context
|
||||||
|
continue
|
||||||
|
print "Matched!"
|
||||||
|
|
||||||
cr.execute('UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s AND active=%s', (res['id'], True))
|
cr.execute('UPDATE ir_sequence SET number_next=number_next+number_increment WHERE id=%s AND active=%s', (res['id'], True))
|
||||||
if res['number_next']:
|
if res['number_next']:
|
||||||
return self._process(res['prefix']) + '%%0%sd' % res['padding'] % res['number_next'] + self._process(res['suffix'])
|
return self._process(res['prefix']) + '%%0%sd' % res['padding'] % res['number_next'] + self._process(res['suffix'])
|
||||||
|
@ -83,8 +106,8 @@ class ir_sequence(osv.osv):
|
||||||
cr.commit()
|
cr.commit()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get(self, cr, uid, code):
|
def get(self, cr, uid, code, context = None):
|
||||||
return self.get_id(cr, uid, code, test='code=%s')
|
return self.get_id(cr, uid, code, test='code=%s',context=context)
|
||||||
ir_sequence()
|
ir_sequence()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -249,8 +249,7 @@ class ir_values(osv.osv):
|
||||||
if r[2].has_key('groups_id'):
|
if r[2].has_key('groups_id'):
|
||||||
groups = r[2]['groups_id']
|
groups = r[2]['groups_id']
|
||||||
if len(groups) > 0:
|
if len(groups) > 0:
|
||||||
group_ids = ','.join([ str(x) for x in r[2]['groups_id']])
|
cr.execute("SELECT count(*) FROM res_groups_users_rel WHERE gid = ANY(%s) AND uid=%s",(groups, uid))
|
||||||
cr.execute("select count(*) from res_groups_users_rel where gid in (%s) and uid='%s'" % (group_ids, uid))
|
|
||||||
gr_ids = cr.fetchall()
|
gr_ids = cr.fetchall()
|
||||||
if not gr_ids[0][0] > 0:
|
if not gr_ids[0][0] > 0:
|
||||||
res2.remove(r)
|
res2.remove(r)
|
||||||
|
|
|
@ -81,7 +81,10 @@ def graph_get(cr, graph, wkf_id, nested=False, workitem={}):
|
||||||
start = cr.fetchone()[0]
|
start = cr.fetchone()[0]
|
||||||
cr.execute("select 'subflow.'||name,id from wkf_activity where flow_stop=True and wkf_id=%s", (wkf_id,))
|
cr.execute("select 'subflow.'||name,id from wkf_activity where flow_stop=True and wkf_id=%s", (wkf_id,))
|
||||||
stop = cr.fetchall()
|
stop = cr.fetchall()
|
||||||
|
if (stop):
|
||||||
stop = (stop[0][1], dict(stop))
|
stop = (stop[0][1], dict(stop))
|
||||||
|
else:
|
||||||
|
stop = ("stop",{})
|
||||||
return ((start,{}),stop)
|
return ((start,{}),stop)
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,7 +146,8 @@ showpage'''
|
||||||
else:
|
else:
|
||||||
inst_id = inst_id[0]
|
inst_id = inst_id[0]
|
||||||
graph = pydot.Dot(fontsize='16', label="""\\\n\\nWorkflow: %s\\n OSV: %s""" % (wkfinfo['name'],wkfinfo['osv']),
|
graph = pydot.Dot(fontsize='16', label="""\\\n\\nWorkflow: %s\\n OSV: %s""" % (wkfinfo['name'],wkfinfo['osv']),
|
||||||
size='10.7, 7.3', center='1', ratio='auto', rotate='90', rankdir='LR'
|
size='7.3, 10.1', center='1', ratio='auto', rotate='0', rankdir='TB',
|
||||||
|
ordering='out'
|
||||||
)
|
)
|
||||||
graph_instance_get(cr, graph, inst_id, data.get('nested', False))
|
graph_instance_get(cr, graph, inst_id, data.get('nested', False))
|
||||||
ps_string = graph.create(prog='dot', format='ps')
|
ps_string = graph.create(prog='dot', format='ps')
|
||||||
|
|
|
@ -233,6 +233,12 @@
|
||||||
<para style="terp_tblheader_Details">Object: [[ object.model ]] [[ objdoc(object.model) ]]</para>
|
<para style="terp_tblheader_Details">Object: [[ object.model ]] [[ objdoc(object.model) ]]</para>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<para style="terp_default_9">[[ repeatIn(objdoc2(object.model), 'sline') ]]</para>
|
||||||
|
<para style="terp_default_9"> [[ sline ]] </para>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</blockTable>
|
</blockTable>
|
||||||
<section>
|
<section>
|
||||||
<para style="terp_default_1">
|
<para style="terp_default_1">
|
||||||
|
|
|
@ -29,11 +29,38 @@ class ir_module_reference_print(report_sxw.rml_parse):
|
||||||
'time': time,
|
'time': time,
|
||||||
'findobj': self._object_find,
|
'findobj': self._object_find,
|
||||||
'objdoc': self._object_doc,
|
'objdoc': self._object_doc,
|
||||||
|
'objdoc2': self._object_doc2,
|
||||||
'findflds': self._fields_find,
|
'findflds': self._fields_find,
|
||||||
})
|
})
|
||||||
def _object_doc(self, obj):
|
def _object_doc(self, obj):
|
||||||
modobj = self.pool.get(obj)
|
modobj = self.pool.get(obj)
|
||||||
return modobj.__doc__
|
strdocs= modobj.__doc__
|
||||||
|
if not strdocs:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
strdocs=strdocs.strip().splitlines(True)
|
||||||
|
res = ''
|
||||||
|
for stre in strdocs:
|
||||||
|
if not stre or stre.isspace():
|
||||||
|
break
|
||||||
|
res += stre
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _object_doc2(self, obj):
|
||||||
|
modobj = self.pool.get(obj)
|
||||||
|
strdocs= modobj.__doc__
|
||||||
|
if not strdocs:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
strdocs=strdocs.strip().splitlines(True)
|
||||||
|
res = []
|
||||||
|
fou = False
|
||||||
|
for stre in strdocs:
|
||||||
|
if fou:
|
||||||
|
res.append(stre.strip())
|
||||||
|
elif not stre or stre.isspace():
|
||||||
|
fou = True
|
||||||
|
return res
|
||||||
|
|
||||||
def _object_find(self, module):
|
def _object_find(self, module):
|
||||||
ids2 = self.pool.get('ir.model.data').search(self.cr, self.uid, [('module','=',module), ('model','=','ir.model')])
|
ids2 = self.pool.get('ir.model.data').search(self.cr, self.uid, [('module','=',module), ('model','=','ir.model')])
|
||||||
|
|
|
@ -80,6 +80,7 @@ class wizard_export_lang(osv.osv_memory):
|
||||||
) ),
|
) ),
|
||||||
}
|
}
|
||||||
_defaults = { 'state': lambda *a: 'choose',
|
_defaults = { 'state': lambda *a: 'choose',
|
||||||
|
'name': lambda *a: 'lang.tar.gz'
|
||||||
}
|
}
|
||||||
wizard_export_lang()
|
wizard_export_lang()
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_country_state" model="ir.actions.act_window">
|
<record id="action_country_state" model="ir.actions.act_window">
|
||||||
<field name="name">States</field>
|
<field name="name">Fed. States</field>
|
||||||
<field name="type">ir.actions.act_window</field>
|
<field name="type">ir.actions.act_window</field>
|
||||||
<field name="res_model">res.country.state</field>
|
<field name="res_model">res.country.state</field>
|
||||||
<field name="view_type">form</field>
|
<field name="view_type">form</field>
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ir_property(osv.osv):
|
||||||
}
|
}
|
||||||
def unlink(self, cr, uid, ids, context={}):
|
def unlink(self, cr, uid, ids, context={}):
|
||||||
if ids:
|
if ids:
|
||||||
cr.execute('delete from ir_model_fields where id in (select fields_id from ir_property where (fields_id is not null) and (id in ('+','.join(map(str, ids))+')))')
|
cr.execute('DELETE FROM ir_model_fields WHERE id IN (SELECT fields_id FROM ir_property WHERE (fields_id IS NOT NULL) AND (id = ANY (%s)))', (ids,))
|
||||||
res = super(ir_property, self).unlink(cr, uid, ids, context)
|
res = super(ir_property, self).unlink(cr, uid, ids, context)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
|
@ -108,11 +108,11 @@ class res_company(osv.osv):
|
||||||
<pageGraphics>
|
<pageGraphics>
|
||||||
<fill color="black"/>
|
<fill color="black"/>
|
||||||
<stroke color="black"/>
|
<stroke color="black"/>
|
||||||
<setFont name="Helvetica" size="8"/>
|
<setFont name="DejaVu Sans" size="8"/>
|
||||||
<drawString x="1.3cm" y="28.3cm"> [[ formatLang(time.strftime("%Y-%m-%d"), date=True) ]] [[ time.strftime("%H:%M") ]]</drawString>
|
<drawString x="1.3cm" y="28.3cm"> [[ formatLang(time.strftime("%Y-%m-%d"), date=True) ]] [[ time.strftime("%H:%M") ]]</drawString>
|
||||||
<setFont name="Helvetica-Bold" size="10"/>
|
<setFont name="DejaVu Sans Bold" size="10"/>
|
||||||
<drawString x="9.8cm" y="28.3cm">[[ company.partner_id.name ]]</drawString>
|
<drawString x="9.8cm" y="28.3cm">[[ company.partner_id.name ]]</drawString>
|
||||||
<setFont name="Helvetica" size="8"/>
|
<setFont name="DejaVu Sans" size="8"/>
|
||||||
<drawRightString x="19.7cm" y="28.3cm"><pageNumber/> / </drawRightString>
|
<drawRightString x="19.7cm" y="28.3cm"><pageNumber/> / </drawRightString>
|
||||||
<drawString x="19.8cm" y="28.3cm"><pageCount/></drawString>
|
<drawString x="19.8cm" y="28.3cm"><pageCount/></drawString>
|
||||||
<stroke color="#000000"/>
|
<stroke color="#000000"/>
|
||||||
|
@ -131,7 +131,7 @@ class res_company(osv.osv):
|
||||||
<pageGraphics>
|
<pageGraphics>
|
||||||
<!-- You Logo - Change X,Y,Width and Height -->
|
<!-- You Logo - Change X,Y,Width and Height -->
|
||||||
<image x="1.3cm" y="27.6cm" height="40.0" >[[company.logo]]</image>
|
<image x="1.3cm" y="27.6cm" height="40.0" >[[company.logo]]</image>
|
||||||
<setFont name="Helvetica" size="8"/>
|
<setFont name="DejaVu Sans" size="8"/>
|
||||||
<fill color="black"/>
|
<fill color="black"/>
|
||||||
<stroke color="black"/>
|
<stroke color="black"/>
|
||||||
<lines>1.3cm 27.7cm 20cm 27.7cm</lines>
|
<lines>1.3cm 27.7cm 20cm 27.7cm</lines>
|
||||||
|
|
|
@ -71,6 +71,14 @@ class groups(osv.osv):
|
||||||
aid.write({'groups_id': [(4, gid)]})
|
aid.write({'groups_id': [(4, gid)]})
|
||||||
return gid
|
return gid
|
||||||
|
|
||||||
|
def copy(self, cr, uid, id, default={}, context={}, done_list=[], local=False):
|
||||||
|
group = self.browse(cr, uid, id, context=context)
|
||||||
|
default = default.copy()
|
||||||
|
if not 'name' in default:
|
||||||
|
default['name'] = group['name']
|
||||||
|
default['name'] = default['name'] + _(' (copy)')
|
||||||
|
return super(groups, self).copy(cr, uid, id, default, context=context)
|
||||||
|
|
||||||
groups()
|
groups()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<rng:choice>
|
<rng:choice>
|
||||||
<rng:ref name="notebook"/>
|
<rng:ref name="notebook"/>
|
||||||
<rng:ref name="field"/>
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="shortcut"/>
|
||||||
<rng:ref name="group"/>
|
<rng:ref name="group"/>
|
||||||
<rng:ref name="button"/>
|
<rng:ref name="button"/>
|
||||||
<rng:ref name="hpaned" />
|
<rng:ref name="hpaned" />
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
<rng:zeroOrMore>
|
<rng:zeroOrMore>
|
||||||
<rng:choice>
|
<rng:choice>
|
||||||
<rng:ref name="field"/>
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="shortcut"/>
|
||||||
<rng:ref name="separator"/>
|
<rng:ref name="separator"/>
|
||||||
<rng:ref name="tree"/>
|
<rng:ref name="tree"/>
|
||||||
<rng:ref name="group"/>
|
<rng:ref name="group"/>
|
||||||
|
@ -184,6 +186,7 @@
|
||||||
<rng:element name="properties"><rng:empty/></rng:element>
|
<rng:element name="properties"><rng:empty/></rng:element>
|
||||||
<rng:ref name="label" />
|
<rng:ref name="label" />
|
||||||
<rng:ref name="field"/>
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="shortcut"/>
|
||||||
<rng:ref name="group"/>
|
<rng:ref name="group"/>
|
||||||
<rng:ref name="separator"/>
|
<rng:ref name="separator"/>
|
||||||
<rng:ref name="button"/>
|
<rng:ref name="button"/>
|
||||||
|
@ -222,6 +225,7 @@
|
||||||
<rng:ref name="separator"/>
|
<rng:ref name="separator"/>
|
||||||
<rng:ref name="button"/>
|
<rng:ref name="button"/>
|
||||||
<rng:ref name="field"/>
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="shortcut"/>
|
||||||
<rng:ref name="label" />
|
<rng:ref name="label" />
|
||||||
<rng:ref name="group" />
|
<rng:ref name="group" />
|
||||||
<rng:ref name="filter"/>
|
<rng:ref name="filter"/>
|
||||||
|
@ -239,6 +243,7 @@
|
||||||
<rng:zeroOrMore>
|
<rng:zeroOrMore>
|
||||||
<rng:choice>
|
<rng:choice>
|
||||||
<rng:ref name="any"/>
|
<rng:ref name="any"/>
|
||||||
|
<rng:ref name="button"/>
|
||||||
</rng:choice>
|
</rng:choice>
|
||||||
</rng:zeroOrMore>
|
</rng:zeroOrMore>
|
||||||
</rng:element>
|
</rng:element>
|
||||||
|
@ -351,6 +356,7 @@
|
||||||
<rng:ref name="separator"/>
|
<rng:ref name="separator"/>
|
||||||
<rng:ref name="button"/>
|
<rng:ref name="button"/>
|
||||||
<rng:ref name="field"/>
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="shortcut"/>
|
||||||
<rng:ref name="label" />
|
<rng:ref name="label" />
|
||||||
<rng:ref name="group" />
|
<rng:ref name="group" />
|
||||||
<rng:ref name="filter"/>
|
<rng:ref name="filter"/>
|
||||||
|
@ -473,6 +479,15 @@
|
||||||
</rng:element>
|
</rng:element>
|
||||||
</rng:define>
|
</rng:define>
|
||||||
|
|
||||||
|
<rng:define name="shortcut">
|
||||||
|
<rng:element name="shortcut">
|
||||||
|
<rng:optional><rng:attribute name="attrs"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="name" /></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="key"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="goto" /></rng:optional>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
|
||||||
<rng:start>
|
<rng:start>
|
||||||
<rng:choice>
|
<rng:choice>
|
||||||
<rng:ref name="form" />
|
<rng:ref name="form" />
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
<rng:attribute name="string"/>
|
<rng:attribute name="string"/>
|
||||||
<rng:attribute name="model"/>
|
<rng:attribute name="model"/>
|
||||||
<rng:attribute name="name"/>
|
<rng:attribute name="name"/>
|
||||||
|
<rng:optional><rng:attribute name="report_type"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="multi"/></rng:optional>
|
<rng:optional><rng:attribute name="multi"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="menu"/></rng:optional>
|
<rng:optional><rng:attribute name="menu"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="keyword"/></rng:optional>
|
<rng:optional><rng:attribute name="keyword"/></rng:optional>
|
||||||
|
@ -118,6 +119,7 @@
|
||||||
<rng:optional><rng:attribute name="type"/></rng:optional>
|
<rng:optional><rng:attribute name="type"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="ref"/></rng:optional>
|
<rng:optional><rng:attribute name="ref"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="eval"/></rng:optional>
|
<rng:optional><rng:attribute name="eval"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="domain"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="search"/></rng:optional>
|
<rng:optional><rng:attribute name="search"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="model"/></rng:optional>
|
<rng:optional><rng:attribute name="model"/></rng:optional>
|
||||||
<rng:optional><rng:attribute name="use"/></rng:optional>
|
<rng:optional><rng:attribute name="use"/></rng:optional>
|
||||||
|
|
353
bin/netsvc.py
353
bin/netsvc.py
|
@ -36,17 +36,23 @@ import time
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
import release
|
import release
|
||||||
|
|
||||||
SERVICES = {}
|
|
||||||
GROUPS = {}
|
|
||||||
|
|
||||||
class Service(object):
|
class Service(object):
|
||||||
|
""" Base class for *Local* services
|
||||||
|
|
||||||
|
Functionality here is trusted, no authentication.
|
||||||
|
"""
|
||||||
|
_services = {}
|
||||||
def __init__(self, name, audience=''):
|
def __init__(self, name, audience=''):
|
||||||
SERVICES[name] = self
|
Service._services[name] = self
|
||||||
self.__name = name
|
self.__name = name
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
|
|
||||||
def joinGroup(self, name):
|
def joinGroup(self, name):
|
||||||
GROUPS.setdefault(name, {})[self.__name] = self
|
raise Exception("No group for local services")
|
||||||
|
#GROUPS.setdefault(name, {})[self.__name] = self
|
||||||
|
|
||||||
|
def service_exist(self,name):
|
||||||
|
return Service._services.has_key(name)
|
||||||
|
|
||||||
def exportMethod(self, method):
|
def exportMethod(self, method):
|
||||||
if callable(method):
|
if callable(method):
|
||||||
|
@ -58,11 +64,16 @@ class Service(object):
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
class LocalService(Service):
|
class LocalService(object):
|
||||||
|
""" Proxy for local services.
|
||||||
|
|
||||||
|
Any instance of this class will behave like the single instance
|
||||||
|
of Service(name)
|
||||||
|
"""
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.__name = name
|
self.__name = name
|
||||||
try:
|
try:
|
||||||
self._service = SERVICES[name]
|
self._service = Service._services[name]
|
||||||
for method_name, method_definition in self._service._methods.items():
|
for method_name, method_definition in self._service._methods.items():
|
||||||
setattr(self, method_name, method_definition)
|
setattr(self, method_name, method_definition)
|
||||||
except KeyError, keyError:
|
except KeyError, keyError:
|
||||||
|
@ -71,19 +82,55 @@ class LocalService(Service):
|
||||||
def __call__(self, method, *params):
|
def __call__(self, method, *params):
|
||||||
return getattr(self, method)(*params)
|
return getattr(self, method)(*params)
|
||||||
|
|
||||||
def service_exist(name):
|
class ExportService(object):
|
||||||
return SERVICES.get(name, False)
|
""" Proxy for exported services.
|
||||||
|
|
||||||
|
All methods here should take an AuthProxy as their first parameter. It
|
||||||
|
will be appended by the calling framework.
|
||||||
|
|
||||||
|
Note that this class has no direct proxy, capable of calling
|
||||||
|
eservice.method(). Rather, the proxy should call
|
||||||
|
dispatch(method,auth,params)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_services = {}
|
||||||
|
_groups = {}
|
||||||
|
|
||||||
|
def __init__(self, name, audience=''):
|
||||||
|
ExportService._services[name] = self
|
||||||
|
self.__name = name
|
||||||
|
|
||||||
|
def joinGroup(self, name):
|
||||||
|
ExportService._groups.setdefault(name, {})[self.__name] = self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getService(cls,name):
|
||||||
|
return cls._services[name]
|
||||||
|
|
||||||
|
def dispatch(self, method, auth, params):
|
||||||
|
raise Exception("stub dispatch at %s" % self.__name)
|
||||||
|
|
||||||
|
def new_dispatch(self,method,auth,params):
|
||||||
|
raise Exception("stub dispatch at %s" % self.__name)
|
||||||
|
|
||||||
|
def abortResponse(self, error, description, origin, details):
|
||||||
|
if not tools.config['debug_mode']:
|
||||||
|
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
LOG_NOTSET = 'notset'
|
LOG_NOTSET = 'notset'
|
||||||
LOG_DEBUG_RPC = 'debug_rpc'
|
LOG_DEBUG_RPC = 'debug_rpc'
|
||||||
LOG_DEBUG = 'debug'
|
LOG_DEBUG = 'debug'
|
||||||
|
LOG_DEBUG2 = 'debug2'
|
||||||
LOG_INFO = 'info'
|
LOG_INFO = 'info'
|
||||||
LOG_WARNING = 'warn'
|
LOG_WARNING = 'warn'
|
||||||
LOG_ERROR = 'error'
|
LOG_ERROR = 'error'
|
||||||
LOG_CRITICAL = 'critical'
|
LOG_CRITICAL = 'critical'
|
||||||
|
|
||||||
# add new log level below DEBUG
|
# add new log level below DEBUG
|
||||||
logging.DEBUG_RPC = logging.DEBUG - 1
|
logging.DEBUG2 = logging.DEBUG - 1
|
||||||
|
logging.DEBUG_RPC = logging.DEBUG2 - 1
|
||||||
|
|
||||||
def init_logger():
|
def init_logger():
|
||||||
import os
|
import os
|
||||||
|
@ -94,7 +141,6 @@ def init_logger():
|
||||||
# create a format for log messages and dates
|
# create a format for log messages and dates
|
||||||
formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
|
formatter = logging.Formatter('[%(asctime)s] %(levelname)s:%(name)s:%(message)s')
|
||||||
|
|
||||||
logging_to_stdout = False
|
|
||||||
if tools.config['syslog']:
|
if tools.config['syslog']:
|
||||||
# SysLog Handler
|
# SysLog Handler
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
|
@ -112,15 +158,18 @@ def init_logger():
|
||||||
dirname = os.path.dirname(logf)
|
dirname = os.path.dirname(logf)
|
||||||
if dirname and not os.path.isdir(dirname):
|
if dirname and not os.path.isdir(dirname):
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
|
if tools.config['logrotate'] is not False:
|
||||||
handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
|
handler = logging.handlers.TimedRotatingFileHandler(logf,'D',1,30)
|
||||||
|
elif os.name == 'posix':
|
||||||
|
handler = logging.handlers.WatchedFileHandler(logf)
|
||||||
|
else:
|
||||||
|
handler = logging.handlers.FileHandler(logf)
|
||||||
except Exception, ex:
|
except Exception, ex:
|
||||||
sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
|
sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
logging_to_stdout = True
|
|
||||||
else:
|
else:
|
||||||
# Normal Handler on standard output
|
# Normal Handler on standard output
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
logging_to_stdout = True
|
|
||||||
|
|
||||||
|
|
||||||
# tell the handler to use this format
|
# tell the handler to use this format
|
||||||
|
@ -128,9 +177,9 @@ def init_logger():
|
||||||
|
|
||||||
# add the handler to the root logger
|
# add the handler to the root logger
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.setLevel(tools.config['log_level'] or '0')
|
logger.setLevel(int(tools.config['log_level'] or '0'))
|
||||||
|
|
||||||
if logging_to_stdout and os.name != 'nt':
|
if (not isinstance(handler, logging.FileHandler)) and os.name != 'nt':
|
||||||
# change color of level names
|
# change color of level names
|
||||||
# uses of ANSI color codes
|
# uses of ANSI color codes
|
||||||
# see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
|
# see http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
|
||||||
|
@ -141,6 +190,7 @@ def init_logger():
|
||||||
|
|
||||||
mapping = {
|
mapping = {
|
||||||
'DEBUG_RPC': ('blue', 'white'),
|
'DEBUG_RPC': ('blue', 'white'),
|
||||||
|
'DEBUG2': ('green', 'white'),
|
||||||
'DEBUG': ('blue', 'default'),
|
'DEBUG': ('blue', 'default'),
|
||||||
'INFO': ('green', 'default'),
|
'INFO': ('green', 'default'),
|
||||||
'WARNING': ('yellow', 'default'),
|
'WARNING': ('yellow', 'default'),
|
||||||
|
@ -160,6 +210,10 @@ class Logger(object):
|
||||||
|
|
||||||
log = logging.getLogger(tools.ustr(name))
|
log = logging.getLogger(tools.ustr(name))
|
||||||
|
|
||||||
|
if level == LOG_DEBUG2 and not hasattr(log, level):
|
||||||
|
fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG2, msg, *args, **kwargs)
|
||||||
|
setattr(log, LOG_DEBUG2, fct)
|
||||||
|
|
||||||
if level == LOG_DEBUG_RPC and not hasattr(log, level):
|
if level == LOG_DEBUG_RPC and not hasattr(log, level):
|
||||||
fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
|
fct = lambda msg, *args, **kwargs: log.log(logging.DEBUG_RPC, msg, *args, **kwargs)
|
||||||
setattr(log, LOG_DEBUG_RPC, fct)
|
setattr(log, LOG_DEBUG_RPC, fct)
|
||||||
|
@ -169,17 +223,33 @@ class Logger(object):
|
||||||
if isinstance(msg, Exception):
|
if isinstance(msg, Exception):
|
||||||
msg = tools.exception_to_unicode(msg)
|
msg = tools.exception_to_unicode(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
msg = tools.ustr(msg).strip()
|
msg = tools.ustr(msg).strip()
|
||||||
|
if level in (LOG_ERROR,LOG_CRITICAL) and tools.config.get_misc('debug','env_info',True):
|
||||||
if level in (LOG_ERROR,LOG_CRITICAL):
|
msg = common().exp_get_server_environment() + "\n" + msg
|
||||||
msg = common().get_server_environment() + '\n' + msg
|
|
||||||
|
|
||||||
result = msg.split('\n')
|
result = msg.split('\n')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
result = msg.strip().split('\n')
|
||||||
|
try:
|
||||||
if len(result)>1:
|
if len(result)>1:
|
||||||
for idx, s in enumerate(result):
|
for idx, s in enumerate(result):
|
||||||
level_method('[%02d]: %s' % (idx+1, s,))
|
level_method('[%02d]: %s' % (idx+1, s,))
|
||||||
elif result:
|
elif result:
|
||||||
level_method(result[0])
|
level_method(result[0])
|
||||||
|
except IOError,e:
|
||||||
|
# TODO: perhaps reset the logger streams?
|
||||||
|
#if logrotate closes our files, we end up here..
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
# better ignore the exception and carry on..
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_loglevel(self, level):
|
||||||
|
log = logging.getLogger()
|
||||||
|
log.setLevel(logging.INFO) # make sure next msg is printed
|
||||||
|
log.info("Log level changed to %s" % logging.getLevelName(level))
|
||||||
|
log.setLevel(level)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
logging.shutdown()
|
logging.shutdown()
|
||||||
|
@ -194,7 +264,7 @@ class Agent(object):
|
||||||
def setAlarm(self, fn, dt, db_name, *args, **kwargs):
|
def setAlarm(self, fn, dt, db_name, *args, **kwargs):
|
||||||
wait = dt - time.time()
|
wait = dt - time.time()
|
||||||
if wait > 0:
|
if wait > 0:
|
||||||
self._logger.notifyChannel('timers', LOG_DEBUG, "Job scheduled in %s seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
|
self._logger.notifyChannel('timers', LOG_DEBUG, "Job scheduled in %.3g seconds for %s.%s" % (wait, fn.im_class.__name__, fn.func_name))
|
||||||
timer = threading.Timer(wait, fn, args, kwargs)
|
timer = threading.Timer(wait, fn, args, kwargs)
|
||||||
timer.start()
|
timer.start()
|
||||||
self._timers.setdefault(db_name, []).append(timer)
|
self._timers.setdefault(db_name, []).append(timer)
|
||||||
|
@ -218,10 +288,66 @@ class Agent(object):
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
class xmlrpc(object):
|
class Server:
|
||||||
class RpcGateway(object):
|
""" Generic interface for all servers with an event loop etc.
|
||||||
def __init__(self, name):
|
Override this to impement http, net-rpc etc. servers.
|
||||||
self.name = name
|
|
||||||
|
Servers here must have threaded behaviour. start() must not block,
|
||||||
|
there is no run().
|
||||||
|
"""
|
||||||
|
__is_started = False
|
||||||
|
__servers = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if Server.__is_started:
|
||||||
|
raise Exception('All instances of servers must be inited before the startAll()')
|
||||||
|
Server.__servers.append(self)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
print "called stub Server.start"
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
print "called stub Server.stop"
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
""" This function should return statistics about the server """
|
||||||
|
return "%s: No statistics" % str(self.__class__)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def startAll(cls):
|
||||||
|
if cls.__is_started:
|
||||||
|
return
|
||||||
|
Logger().notifyChannel("services", LOG_INFO,
|
||||||
|
"Starting %d services" % len(cls.__servers))
|
||||||
|
for srv in cls.__servers:
|
||||||
|
srv.start()
|
||||||
|
cls.__is_started = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def quitAll(cls):
|
||||||
|
if not cls.__is_started:
|
||||||
|
return
|
||||||
|
Logger().notifyChannel("services", LOG_INFO,
|
||||||
|
"Stopping %d services" % len(cls.__servers))
|
||||||
|
for srv in cls.__servers:
|
||||||
|
srv.stop()
|
||||||
|
cls.__is_started = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def allStats(cls):
|
||||||
|
res = ''
|
||||||
|
if cls.__is_started:
|
||||||
|
res += "Servers started\n"
|
||||||
|
else:
|
||||||
|
res += "Servers stopped\n"
|
||||||
|
for srv in cls.__servers:
|
||||||
|
try:
|
||||||
|
res += srv.stats() + "\n"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return res
|
||||||
|
|
||||||
class OpenERPDispatcherException(Exception):
|
class OpenERPDispatcherException(Exception):
|
||||||
def __init__(self, exception, traceback):
|
def __init__(self, exception, traceback):
|
||||||
|
@ -234,14 +360,19 @@ class OpenERPDispatcher:
|
||||||
Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
|
Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
|
||||||
|
|
||||||
def dispatch(self, service_name, method, params):
|
def dispatch(self, service_name, method, params):
|
||||||
if service_name not in GROUPS['web-services']:
|
|
||||||
raise Exception('Access Denied for Service :'+service_name)
|
|
||||||
try:
|
try:
|
||||||
self.log('service', service_name)
|
self.log('service', service_name)
|
||||||
self.log('method', method)
|
self.log('method', method)
|
||||||
self.log('params', params)
|
self.log('params', params)
|
||||||
result = LocalService(service_name)(method, *params)
|
if hasattr(self,'auth_provider'):
|
||||||
|
auth = self.auth_provider
|
||||||
|
else:
|
||||||
|
auth = None
|
||||||
|
result = ExportService.getService(service_name).dispatch(method, auth, params)
|
||||||
self.log('result', result)
|
self.log('result', result)
|
||||||
|
# We shouldn't marshall None,
|
||||||
|
if result == None:
|
||||||
|
result = False
|
||||||
return result
|
return result
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.log('exception', tools.exception_to_unicode(e))
|
self.log('exception', tools.exception_to_unicode(e))
|
||||||
|
@ -255,174 +386,4 @@ class OpenERPDispatcher:
|
||||||
pdb.post_mortem(tb[2])
|
pdb.post_mortem(tb[2])
|
||||||
raise OpenERPDispatcherException(e, tb_s)
|
raise OpenERPDispatcherException(e, tb_s)
|
||||||
|
|
||||||
class GenericXMLRPCRequestHandler(OpenERPDispatcher):
|
|
||||||
def _dispatch(self, method, params):
|
|
||||||
try:
|
|
||||||
service_name = self.path.split("/")[-1]
|
|
||||||
return self.dispatch(service_name, method, params)
|
|
||||||
except OpenERPDispatcherException, e:
|
|
||||||
raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
|
|
||||||
|
|
||||||
class SSLSocket(object):
|
|
||||||
def __init__(self, socket):
|
|
||||||
if not hasattr(socket, 'sock_shutdown'):
|
|
||||||
from OpenSSL import SSL
|
|
||||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
|
||||||
ctx.use_privatekey_file(tools.config['secure_pkey_file'])
|
|
||||||
ctx.use_certificate_file(tools.config['secure_cert_file'])
|
|
||||||
self.socket = SSL.Connection(ctx, socket)
|
|
||||||
else:
|
|
||||||
self.socket = socket
|
|
||||||
|
|
||||||
def shutdown(self, how):
|
|
||||||
return self.socket.sock_shutdown(how)
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self.socket, name)
|
|
||||||
|
|
||||||
class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
|
||||||
rpc_paths = map(lambda s: '/xmlrpc/%s' % s, GROUPS.get('web-services', {}).keys())
|
|
||||||
|
|
||||||
class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
|
|
||||||
def setup(self):
|
|
||||||
self.connection = SSLSocket(self.request)
|
|
||||||
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
|
|
||||||
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
|
|
||||||
|
|
||||||
class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer):
|
|
||||||
def server_bind(self):
|
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
|
|
||||||
|
|
||||||
class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer):
|
|
||||||
def __init__(self, server_address, HandlerClass, logRequests=1):
|
|
||||||
SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass, logRequests)
|
|
||||||
self.socket = SSLSocket(socket.socket(self.address_family, self.socket_type))
|
|
||||||
self.server_bind()
|
|
||||||
self.server_activate()
|
|
||||||
|
|
||||||
class HttpDaemon(threading.Thread):
|
|
||||||
def __init__(self, interface, port, secure=False):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.__port = port
|
|
||||||
self.__interface = interface
|
|
||||||
self.secure = bool(secure)
|
|
||||||
handler_class = (SimpleXMLRPCRequestHandler, SecureXMLRPCRequestHandler)[self.secure]
|
|
||||||
server_class = (SimpleThreadedXMLRPCServer, SecureThreadedXMLRPCServer)[self.secure]
|
|
||||||
|
|
||||||
if self.secure:
|
|
||||||
from OpenSSL.SSL import Error as SSLError
|
|
||||||
else:
|
|
||||||
class SSLError(Exception): pass
|
|
||||||
try:
|
|
||||||
self.server = server_class((interface, port), handler_class, 0)
|
|
||||||
except SSLError, e:
|
|
||||||
Logger().notifyChannel('xml-rpc-ssl', LOG_CRITICAL, "Can not load the certificate and/or the private key files")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception, e:
|
|
||||||
Logger().notifyChannel('xml-rpc', LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def attach(self, path, gw):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
if os.name != 'nt':
|
|
||||||
self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 )
|
|
||||||
self.server.socket.close()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.server.register_introspection_functions()
|
|
||||||
|
|
||||||
self.running = True
|
|
||||||
while self.running:
|
|
||||||
self.server.handle_request()
|
|
||||||
return True
|
|
||||||
|
|
||||||
# If the server need to be run recursively
|
|
||||||
#
|
|
||||||
#signal.signal(signal.SIGALRM, self.my_handler)
|
|
||||||
#signal.alarm(6)
|
|
||||||
#while True:
|
|
||||||
# self.server.handle_request()
|
|
||||||
#signal.alarm(0) # Disable the alarm
|
|
||||||
|
|
||||||
import tiny_socket
|
|
||||||
class TinySocketClientThread(threading.Thread, OpenERPDispatcher):
|
|
||||||
def __init__(self, sock, threads):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.sock = sock
|
|
||||||
self.threads = threads
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import select
|
|
||||||
self.running = True
|
|
||||||
try:
|
|
||||||
ts = tiny_socket.mysocket(self.sock)
|
|
||||||
except:
|
|
||||||
self.sock.close()
|
|
||||||
self.threads.remove(self)
|
|
||||||
return False
|
|
||||||
while self.running:
|
|
||||||
try:
|
|
||||||
msg = ts.myreceive()
|
|
||||||
except:
|
|
||||||
self.sock.close()
|
|
||||||
self.threads.remove(self)
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
result = self.dispatch(msg[0], msg[1], msg[2:])
|
|
||||||
ts.mysend(result)
|
|
||||||
except OpenERPDispatcherException, e:
|
|
||||||
new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling
|
|
||||||
ts.mysend(new_e, exception=True, traceback=e.traceback)
|
|
||||||
|
|
||||||
self.sock.close()
|
|
||||||
self.threads.remove(self)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
|
|
||||||
class TinySocketServerThread(threading.Thread):
|
|
||||||
def __init__(self, interface, port, secure=False):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.__port = port
|
|
||||||
self.__interface = interface
|
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
self.socket.bind((self.__interface, self.__port))
|
|
||||||
self.socket.listen(5)
|
|
||||||
self.threads = []
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import select
|
|
||||||
try:
|
|
||||||
self.running = True
|
|
||||||
while self.running:
|
|
||||||
(clientsocket, address) = self.socket.accept()
|
|
||||||
ct = TinySocketClientThread(clientsocket, self.threads)
|
|
||||||
self.threads.append(ct)
|
|
||||||
ct.start()
|
|
||||||
self.socket.close()
|
|
||||||
except Exception, e:
|
|
||||||
self.socket.close()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.running = False
|
|
||||||
for t in self.threads:
|
|
||||||
t.stop()
|
|
||||||
try:
|
|
||||||
if hasattr(socket, 'SHUT_RDWR'):
|
|
||||||
self.socket.shutdown(socket.SHUT_RDWR)
|
|
||||||
else:
|
|
||||||
self.socket.shutdown(2)
|
|
||||||
self.socket.close()
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -36,6 +36,7 @@ GNU Public Licence.
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import pwd
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# ubuntu 8.04 has obsoleted `pyxml` package and installs here.
|
# ubuntu 8.04 has obsoleted `pyxml` package and installs here.
|
||||||
# the path needs to be updated before any `import xml`
|
# the path needs to be updated before any `import xml`
|
||||||
|
@ -48,11 +49,16 @@ if os.path.exists(_oldxml1):
|
||||||
elif os.path.exists(_oldxml2):
|
elif os.path.exists(_oldxml2):
|
||||||
sys.path.insert(0,_oldxml2)
|
sys.path.insert(0,_oldxml2)
|
||||||
|
|
||||||
|
|
||||||
import release
|
import release
|
||||||
__author__ = release.author
|
__author__ = release.author
|
||||||
__version__ = release.version
|
__version__ = release.version
|
||||||
|
|
||||||
|
# We DON't log this using the standard logger, because we might mess
|
||||||
|
# with the logfile's permissions. Just do a quick exit here.
|
||||||
|
if pwd.getpwuid(os.getuid())[0] == 'root' :
|
||||||
|
sys.stderr.write("Attempted to run OpenERP server as root. This is not good, aborting.\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# get logger
|
# get logger
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -105,6 +111,17 @@ import addons
|
||||||
# Load and update databases if requested
|
# Load and update databases if requested
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
|
||||||
|
import service.http_server
|
||||||
|
|
||||||
|
if not ( tools.config["stop_after_init"] or \
|
||||||
|
tools.config["translate_in"] or \
|
||||||
|
tools.config["translate_out"] ):
|
||||||
|
service.http_server.init_servers()
|
||||||
|
service.http_server.init_xmlrpc()
|
||||||
|
|
||||||
|
import service.netrpc_server
|
||||||
|
service.netrpc_server.init_servers()
|
||||||
|
|
||||||
if tools.config['db_name']:
|
if tools.config['db_name']:
|
||||||
for db in tools.config['db_name'].split(','):
|
for db in tools.config['db_name'].split(','):
|
||||||
pooler.get_db_and_pool(db, update_module=tools.config['init'] or tools.config['update'])
|
pooler.get_db_and_pool(db, update_module=tools.config['init'] or tools.config['update'])
|
||||||
|
@ -145,36 +162,9 @@ if tools.config["stop_after_init"]:
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Launch Server
|
# Launch Servers
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
|
||||||
if tools.config['xmlrpc']:
|
|
||||||
port = int(tools.config['port'])
|
|
||||||
interface = tools.config["interface"]
|
|
||||||
secure = tools.config["secure"]
|
|
||||||
|
|
||||||
httpd = netsvc.HttpDaemon(interface, port, secure)
|
|
||||||
|
|
||||||
xml_gw = netsvc.xmlrpc.RpcGateway('web-services')
|
|
||||||
httpd.attach("/xmlrpc", xml_gw)
|
|
||||||
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
|
||||||
"starting XML-RPC%s services, port %s" %
|
|
||||||
((tools.config['secure'] and ' Secure' or ''), port))
|
|
||||||
|
|
||||||
#
|
|
||||||
#if tools.config["soap"]:
|
|
||||||
# soap_gw = netsvc.xmlrpc.RpcGateway('web-services')
|
|
||||||
# httpd.attach("/soap", soap_gw )
|
|
||||||
# logger.notifyChannel("web-services", netsvc.LOG_INFO, 'starting SOAP services, port '+str(port))
|
|
||||||
#
|
|
||||||
|
|
||||||
if tools.config['netrpc']:
|
|
||||||
netport = int(tools.config['netport'])
|
|
||||||
netinterface = tools.config["netinterface"]
|
|
||||||
tinySocket = netsvc.TinySocketServerThread(netinterface, netport, False)
|
|
||||||
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
|
||||||
"starting NET-RPC service, port %d" % (netport,))
|
|
||||||
|
|
||||||
LST_SIGNALS = ['SIGINT', 'SIGTERM']
|
LST_SIGNALS = ['SIGINT', 'SIGTERM']
|
||||||
if os.name == 'posix':
|
if os.name == 'posix':
|
||||||
LST_SIGNALS.extend(['SIGUSR1','SIGQUIT'])
|
LST_SIGNALS.extend(['SIGUSR1','SIGQUIT'])
|
||||||
|
@ -189,11 +179,8 @@ def handler(signum, _):
|
||||||
:param signum: the signal number
|
:param signum: the signal number
|
||||||
:param _:
|
:param _:
|
||||||
"""
|
"""
|
||||||
if tools.config['netrpc']:
|
|
||||||
tinySocket.stop()
|
|
||||||
if tools.config['xmlrpc']:
|
|
||||||
httpd.stop()
|
|
||||||
netsvc.Agent.quit()
|
netsvc.Agent.quit()
|
||||||
|
netsvc.Server.quitAll()
|
||||||
if tools.config['pidfile']:
|
if tools.config['pidfile']:
|
||||||
os.unlink(tools.config['pidfile'])
|
os.unlink(tools.config['pidfile'])
|
||||||
logger.notifyChannel('shutdown', netsvc.LOG_INFO,
|
logger.notifyChannel('shutdown', netsvc.LOG_INFO,
|
||||||
|
@ -210,16 +197,14 @@ if tools.config['pidfile']:
|
||||||
fd.write(pidtext)
|
fd.write(pidtext)
|
||||||
fd.close()
|
fd.close()
|
||||||
|
|
||||||
|
|
||||||
|
netsvc.Server.startAll()
|
||||||
|
|
||||||
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
'the server is running, waiting for connections...')
|
'the server is running, waiting for connections...')
|
||||||
|
|
||||||
if tools.config['netrpc']:
|
|
||||||
tinySocket.start()
|
|
||||||
if tools.config['xmlrpc']:
|
|
||||||
httpd.start()
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(60)
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ class expression(object):
|
||||||
return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
|
return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
|
||||||
|
|
||||||
def _is_leaf(self, element, internal=False):
|
def _is_leaf(self, element, internal=False):
|
||||||
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
|
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
|
||||||
INTERNAL_OPS = OPS + ('inselect',)
|
INTERNAL_OPS = OPS + ('inselect',)
|
||||||
return (isinstance(element, tuple) or isinstance(element, list)) \
|
return (isinstance(element, tuple) or isinstance(element, list)) \
|
||||||
and len(element) == 3 \
|
and len(element) == 3 \
|
||||||
|
@ -44,6 +44,7 @@ class expression(object):
|
||||||
or (internal and element[1] in INTERNAL_OPS))
|
or (internal and element[1] in INTERNAL_OPS))
|
||||||
|
|
||||||
def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
|
def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
|
||||||
|
# todo: merge into parent query as sub-query
|
||||||
res = []
|
res = []
|
||||||
if ids:
|
if ids:
|
||||||
if op in ['<','>','>=','<=']:
|
if op in ['<','>','>=','<=']:
|
||||||
|
@ -56,8 +57,7 @@ class expression(object):
|
||||||
subids = ids[i:i+cr.IN_MAX]
|
subids = ids[i:i+cr.IN_MAX]
|
||||||
cr.execute('SELECT "%s"' \
|
cr.execute('SELECT "%s"' \
|
||||||
' FROM "%s"' \
|
' FROM "%s"' \
|
||||||
' WHERE "%s" in (%s)' % (s, f, w, ','.join(['%s']*len(subids))),
|
' WHERE "%s" = ANY (%%s)' % (s, f, w), (subids,))
|
||||||
subids)
|
|
||||||
res.extend([r[0] for r in cr.fetchall()])
|
res.extend([r[0] for r in cr.fetchall()])
|
||||||
else:
|
else:
|
||||||
cr.execute('SELECT distinct("%s")' \
|
cr.execute('SELECT distinct("%s")' \
|
||||||
|
@ -236,6 +236,8 @@ class expression(object):
|
||||||
if operator == 'child_of':
|
if operator == 'child_of':
|
||||||
if isinstance(right, basestring):
|
if isinstance(right, basestring):
|
||||||
ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], 'like', limit=None)]
|
ids2 = [x[0] for x in field_obj.name_search(cr, uid, right, [], 'like', limit=None)]
|
||||||
|
elif isinstance(right, (int, long)):
|
||||||
|
ids2 = list([right])
|
||||||
else:
|
else:
|
||||||
ids2 = list(right)
|
ids2 = list(right)
|
||||||
|
|
||||||
|
@ -343,6 +345,18 @@ class expression(object):
|
||||||
query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
|
query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
|
||||||
elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
|
elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
|
||||||
query = '%s.%s IS NOT NULL' % (table._table, left)
|
query = '%s.%s IS NOT NULL' % (table._table, left)
|
||||||
|
elif (operator == '=?'):
|
||||||
|
op = '='
|
||||||
|
if (right is False or right is None):
|
||||||
|
return ( 'TRUE',[])
|
||||||
|
if left in table._columns:
|
||||||
|
format = table._columns[left]._symbol_set[0]
|
||||||
|
query = '(%s.%s %s %s)' % (table._table, left, op, format)
|
||||||
|
params = table._columns[left]._symbol_set[1](right)
|
||||||
|
else:
|
||||||
|
query = "(%s.%s %s '%%s')" % (table._table, left, op)
|
||||||
|
params = right
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if left == 'id':
|
if left == 'id':
|
||||||
query = '%s.id %s %%s' % (table._table, operator)
|
query = '%s.id %s %%s' % (table._table, operator)
|
||||||
|
|
|
@ -459,7 +459,7 @@ class one2many(_column):
|
||||||
elif act[0] == 6:
|
elif act[0] == 6:
|
||||||
obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
|
obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
|
||||||
ids2 = act[2] or [0]
|
ids2 = act[2] or [0]
|
||||||
cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id not in ('+','.join(map(str, ids2))+')', (id,))
|
cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
|
||||||
ids3 = map(lambda x:x[0], cr.fetchall())
|
ids3 = map(lambda x:x[0], cr.fetchall())
|
||||||
obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
|
obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
|
||||||
return result
|
return result
|
||||||
|
@ -504,7 +504,6 @@ class many2many(_column):
|
||||||
return res
|
return res
|
||||||
for id in ids:
|
for id in ids:
|
||||||
res[id] = []
|
res[id] = []
|
||||||
ids_s = ','.join(map(str, ids))
|
|
||||||
limit_str = self._limit is not None and ' limit %d' % self._limit or ''
|
limit_str = self._limit is not None and ' limit %d' % self._limit or ''
|
||||||
obj = obj.pool.get(self._obj)
|
obj = obj.pool.get(self._obj)
|
||||||
|
|
||||||
|
@ -514,10 +513,10 @@ class many2many(_column):
|
||||||
|
|
||||||
cr.execute('SELECT '+self._rel+'.'+self._id2+','+self._rel+'.'+self._id1+' \
|
cr.execute('SELECT '+self._rel+'.'+self._id2+','+self._rel+'.'+self._id1+' \
|
||||||
FROM '+self._rel+' , '+obj._table+' \
|
FROM '+self._rel+' , '+obj._table+' \
|
||||||
WHERE '+self._rel+'.'+self._id1+' in ('+ids_s+') \
|
WHERE '+self._rel+'.'+self._id1+' = ANY (%s) \
|
||||||
AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+d1
|
AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+d1
|
||||||
+limit_str+' order by '+obj._table+'.'+obj._order+' offset %s',
|
+limit_str+' order by '+obj._table+'.'+obj._order+' offset %s',
|
||||||
d2+[offset])
|
[ids,]+d2+[offset])
|
||||||
for r in cr.fetchall():
|
for r in cr.fetchall():
|
||||||
res[r[1]].append(r[0])
|
res[r[1]].append(r[0])
|
||||||
return res
|
return res
|
||||||
|
@ -585,6 +584,16 @@ class many2many(_column):
|
||||||
obj.datas[id][name] = act[2]
|
obj.datas[id][name] = act[2]
|
||||||
|
|
||||||
|
|
||||||
|
def get_nice_size(a):
|
||||||
|
(x,y) = a
|
||||||
|
if isinstance(y, (int,long)):
|
||||||
|
size = y
|
||||||
|
elif y:
|
||||||
|
y = len(y)
|
||||||
|
else:
|
||||||
|
y = 0
|
||||||
|
return (x, tools.human_size(size))
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Function fields
|
# Function fields
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
@ -665,7 +674,7 @@ class function(_column):
|
||||||
|
|
||||||
if self._type == 'binary' and context.get('bin_size', False):
|
if self._type == 'binary' and context.get('bin_size', False):
|
||||||
# convert the data returned by the function with the size of that data...
|
# convert the data returned by the function with the size of that data...
|
||||||
res = dict(map(lambda (x, y): (x, tools.human_size(len(y or ''))), res.items()))
|
res = dict(map( get_nice_size, res.items()))
|
||||||
return res
|
return res
|
||||||
get_memory = get
|
get_memory = get
|
||||||
|
|
||||||
|
|
|
@ -94,10 +94,10 @@ class browse_null(object):
|
||||||
self.id = False
|
self.id = False
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
return False
|
return None
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return False # XXX: return self ?
|
return None # XXX: return self ?
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
return False
|
return False
|
||||||
|
@ -165,7 +165,7 @@ class browse_record(object):
|
||||||
col = self._table._columns[name]
|
col = self._table._columns[name]
|
||||||
elif name in self._table._inherit_fields:
|
elif name in self._table._inherit_fields:
|
||||||
col = self._table._inherit_fields[name][2]
|
col = self._table._inherit_fields[name][2]
|
||||||
elif hasattr(self._table, name):
|
elif hasattr(self._table, str(name)):
|
||||||
if isinstance(getattr(self._table, name), (types.MethodType, types.LambdaType, types.FunctionType)):
|
if isinstance(getattr(self._table, name), (types.MethodType, types.LambdaType, types.FunctionType)):
|
||||||
return lambda *args, **argv: getattr(self._table, name)(self._cr, self._uid, [self._id], *args, **argv)
|
return lambda *args, **argv: getattr(self._table, name)(self._cr, self._uid, [self._id], *args, **argv)
|
||||||
else:
|
else:
|
||||||
|
@ -173,7 +173,7 @@ class browse_record(object):
|
||||||
else:
|
else:
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error: field '%s' does not exist in object '%s' !" % (name, self._table._name))
|
logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error: field '%s' does not exist in object '%s' !" % (name, self._table._name))
|
||||||
return False
|
return None
|
||||||
|
|
||||||
# if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
|
# if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
|
||||||
if col._prefetch:
|
if col._prefetch:
|
||||||
|
@ -205,6 +205,9 @@ class browse_record(object):
|
||||||
d[n].set_value(self._cr, self._uid, d[n], self, f, lang_obj)
|
d[n].set_value(self._cr, self._uid, d[n], self, f, lang_obj)
|
||||||
|
|
||||||
|
|
||||||
|
if not datas:
|
||||||
|
# Where did those ids come from? Perhaps old entries in ir_model_data?
|
||||||
|
raise except_orm('NoDataError', 'Field %s in %s%s'%(name,self._table_name,str(ids)))
|
||||||
# create browse records for 'remote' objects
|
# create browse records for 'remote' objects
|
||||||
for data in datas:
|
for data in datas:
|
||||||
for n, f in ffields:
|
for n, f in ffields:
|
||||||
|
@ -225,6 +228,12 @@ class browse_record(object):
|
||||||
elif f._type in ('one2many', 'many2many') and len(data[n]):
|
elif f._type in ('one2many', 'many2many') and len(data[n]):
|
||||||
data[n] = self._list_class([browse_record(self._cr, self._uid, id, self._table.pool.get(f._obj), self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process) for id in data[n]], self._context)
|
data[n] = self._list_class([browse_record(self._cr, self._uid, id, self._table.pool.get(f._obj), self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process) for id in data[n]], self._context)
|
||||||
self._data[data['id']].update(data)
|
self._data[data['id']].update(data)
|
||||||
|
if not name in self._data[self._id]:
|
||||||
|
#how did this happen?
|
||||||
|
logger = netsvc.Logger()
|
||||||
|
logger.notifyChannel("browse_record", netsvc.LOG_ERROR,"Ffields: %s, datas: %s"%(str(fffields),str(datas)))
|
||||||
|
logger.notifyChannel("browse_record", netsvc.LOG_ERROR,"Data: %s, Table: %s"%(str(self._data[self._id]),str(self._table)))
|
||||||
|
raise AttributeError(_('Unknown attribute %s in %s ') % (str(name),self._table_name))
|
||||||
return self._data[self._id][name]
|
return self._data[self._id][name]
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
@ -662,8 +671,10 @@ class orm_template(object):
|
||||||
else:
|
else:
|
||||||
module, xml_id = current_module, line[i]
|
module, xml_id = current_module, line[i]
|
||||||
id = ir_model_data_obj._get_id(cr, uid, module, xml_id)
|
id = ir_model_data_obj._get_id(cr, uid, module, xml_id)
|
||||||
res_id = ir_model_data_obj.read(cr, uid, [id],
|
res_res_id = ir_model_data_obj.read(cr, uid, [id],
|
||||||
['res_id'])[0]['res_id']
|
['res_id'])
|
||||||
|
if res_res_id:
|
||||||
|
res_id = res_res_id[0]['res_id']
|
||||||
row[field[-1][:-3]] = res_id or False
|
row[field[-1][:-3]] = res_id or False
|
||||||
continue
|
continue
|
||||||
if (len(field) == len(prefix)+1) and \
|
if (len(field) == len(prefix)+1) and \
|
||||||
|
@ -951,6 +962,7 @@ class orm_template(object):
|
||||||
and getattr(self._columns[f], arg):
|
and getattr(self._columns[f], arg):
|
||||||
res[f][arg] = getattr(self._columns[f], arg)
|
res[f][arg] = getattr(self._columns[f], arg)
|
||||||
|
|
||||||
|
#TODO: optimize
|
||||||
res_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'field', context.get('lang', False) or 'en_US')
|
res_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'field', context.get('lang', False) or 'en_US')
|
||||||
if res_trans:
|
if res_trans:
|
||||||
res[f]['string'] = res_trans
|
res[f]['string'] = res_trans
|
||||||
|
@ -1337,8 +1349,8 @@ class orm_template(object):
|
||||||
# otherwise, build some kind of default view
|
# otherwise, build some kind of default view
|
||||||
if view_type == 'form':
|
if view_type == 'form':
|
||||||
res = self.fields_get(cr, user, context=context)
|
res = self.fields_get(cr, user, context=context)
|
||||||
xml = '''<?xml version="1.0" encoding="utf-8"?>''' \
|
xml = '<?xml version="1.0" encoding="utf-8"?> ' \
|
||||||
'''<form string="%s">''' % (self._description,)
|
'<form string="%s">' % (self._description,)
|
||||||
for x in res:
|
for x in res:
|
||||||
if res[x]['type'] not in ('one2many', 'many2many'):
|
if res[x]['type'] not in ('one2many', 'many2many'):
|
||||||
xml += '<field name="%s"/>' % (x,)
|
xml += '<field name="%s"/>' % (x,)
|
||||||
|
@ -1349,19 +1361,27 @@ class orm_template(object):
|
||||||
_rec_name = self._rec_name
|
_rec_name = self._rec_name
|
||||||
if _rec_name not in self._columns:
|
if _rec_name not in self._columns:
|
||||||
_rec_name = self._columns.keys()[0]
|
_rec_name = self._columns.keys()[0]
|
||||||
xml = '''<?xml version="1.0" encoding="utf-8"?>''' \
|
xml = '<?xml version="1.0" encoding="utf-8"?>' \
|
||||||
'''<tree string="%s"><field name="%s"/></tree>''' \
|
'<tree string="%s"><field name="%s"/></tree>' \
|
||||||
% (self._description, self._rec_name)
|
% (self._description, self._rec_name)
|
||||||
elif view_type == 'calendar':
|
elif view_type == 'calendar':
|
||||||
xml = self.__get_default_calendar_view()
|
xml = self.__get_default_calendar_view()
|
||||||
else:
|
else:
|
||||||
raise except_orm(_('Invalid Architecture!'),_("There is no view of type '%s' defined for the structure!") % view_type)
|
xml = '<?xml version="1.0"?>' # what happens here, graph case?
|
||||||
|
# raise except_orm(_('Invalid Architecture!'),_("There is no view of type '%s' defined for the structure!") % view_type)
|
||||||
result['arch'] = etree.fromstring(encode(xml))
|
result['arch'] = etree.fromstring(encode(xml))
|
||||||
result['name'] = 'default'
|
result['name'] = 'default'
|
||||||
result['field_parent'] = False
|
result['field_parent'] = False
|
||||||
result['view_id'] = 0
|
result['view_id'] = 0
|
||||||
|
|
||||||
xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=context)
|
try:
|
||||||
|
doc = dom.minidom.parseString(encode(result['arch']))
|
||||||
|
except Exception, ex:
|
||||||
|
logger = netsvc.Logger()
|
||||||
|
logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Wrong arch in %s (%s):\n %s' % (result['name'], view_type, result['arch'] ))
|
||||||
|
raise except_orm('Error',
|
||||||
|
('Invalid xml in view %s(%d) of %s: %s' % (result['name'], result['view_id'], self._name, str(ex))))
|
||||||
|
xarch, xfields = self.__view_look_dom_arch(cr, user, doc, view_id, context=context)
|
||||||
result['arch'] = xarch
|
result['arch'] = xarch
|
||||||
result['fields'] = xfields
|
result['fields'] = xfields
|
||||||
|
|
||||||
|
@ -1441,6 +1461,7 @@ class orm_template(object):
|
||||||
self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'read', context=context)
|
self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'read', context=context)
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = self._columns.keys() + self._inherit_fields.keys()
|
fields = self._columns.keys() + self._inherit_fields.keys()
|
||||||
|
#FIXME: collect all calls to _get_source into one SQL call.
|
||||||
for lang in langs:
|
for lang in langs:
|
||||||
res[lang] = {'code': lang}
|
res[lang] = {'code': lang}
|
||||||
for f in fields:
|
for f in fields:
|
||||||
|
@ -1462,6 +1483,7 @@ class orm_template(object):
|
||||||
|
|
||||||
def write_string(self, cr, uid, id, langs, vals, context=None):
|
def write_string(self, cr, uid, id, langs, vals, context=None):
|
||||||
self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'write', context=context)
|
self.pool.get('ir.model.access').check(cr, uid, 'ir.translation', 'write', context=context)
|
||||||
|
#FIXME: try to only call the translation in one SQL
|
||||||
for lang in langs:
|
for lang in langs:
|
||||||
for field in vals:
|
for field in vals:
|
||||||
if field in self._columns:
|
if field in self._columns:
|
||||||
|
@ -1706,7 +1728,7 @@ class orm_memory(orm_template):
|
||||||
if id in self.datas:
|
if id in self.datas:
|
||||||
del self.datas[id]
|
del self.datas[id]
|
||||||
if len(ids):
|
if len(ids):
|
||||||
cr.execute('delete from wkf_instance where res_type=%s and res_id in ('+','.join(map(str, ids))+')', (self._name, ))
|
cr.execute('delete from wkf_instance where res_type=%s and res_id = ANY (%s)', (self._name,ids))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def perm_read(self, cr, user, ids, context=None, details=True):
|
def perm_read(self, cr, user, ids, context=None, details=True):
|
||||||
|
@ -1882,6 +1904,20 @@ class orm(orm_template):
|
||||||
"AND c.oid=a.attrelid " \
|
"AND c.oid=a.attrelid " \
|
||||||
"AND a.atttypid=t.oid", (self._table, k))
|
"AND a.atttypid=t.oid", (self._table, k))
|
||||||
res = cr.dictfetchall()
|
res = cr.dictfetchall()
|
||||||
|
if not res and hasattr(f,'oldname'):
|
||||||
|
cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN a.atttypmod-4 ELSE a.attlen END as size " \
|
||||||
|
"FROM pg_class c,pg_attribute a,pg_type t " \
|
||||||
|
"WHERE c.relname=%s " \
|
||||||
|
"AND a.attname=%s " \
|
||||||
|
"AND c.oid=a.attrelid " \
|
||||||
|
"AND a.atttypid=t.oid", (self._table, f.oldname))
|
||||||
|
res_old = cr.dictfetchall()
|
||||||
|
logger.notifyChannel('orm', netsvc.LOG_DEBUG, 'trying to rename %s(%s) to %s'% (self._table, f.oldname, k))
|
||||||
|
if res_old and len(res_old)==1:
|
||||||
|
cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % ( self._table,f.oldname, k))
|
||||||
|
res = res_old
|
||||||
|
res[0]['attname'] = k
|
||||||
|
|
||||||
if not res:
|
if not res:
|
||||||
if not isinstance(f, fields.function) or f.store:
|
if not isinstance(f, fields.function) or f.store:
|
||||||
|
|
||||||
|
@ -1929,7 +1965,7 @@ class orm(orm_template):
|
||||||
f_pg_type = f_pg_def['typname']
|
f_pg_type = f_pg_def['typname']
|
||||||
f_pg_size = f_pg_def['size']
|
f_pg_size = f_pg_def['size']
|
||||||
f_pg_notnull = f_pg_def['attnotnull']
|
f_pg_notnull = f_pg_def['attnotnull']
|
||||||
if isinstance(f, fields.function) and not f.store:
|
if isinstance(f, fields.function) and not f.store and (not hasattr(f,'nodrop') or not f.nodrop):
|
||||||
logger.notifyChannel('orm', netsvc.LOG_INFO, 'column %s (%s) in table %s removed: converted to a function !\n' % (k, f.string, self._table))
|
logger.notifyChannel('orm', netsvc.LOG_INFO, 'column %s (%s) in table %s removed: converted to a function !\n' % (k, f.string, self._table))
|
||||||
cr.execute('ALTER TABLE "%s" DROP COLUMN "%s"'% (self._table, k))
|
cr.execute('ALTER TABLE "%s" DROP COLUMN "%s"'% (self._table, k))
|
||||||
cr.commit()
|
cr.commit()
|
||||||
|
@ -2033,7 +2069,7 @@ class orm(orm_template):
|
||||||
cr.execute('ALTER TABLE "' + self._table + '" ADD FOREIGN KEY ("' + k + '") REFERENCES "' + ref + '" ON DELETE ' + f.ondelete)
|
cr.execute('ALTER TABLE "' + self._table + '" ADD FOREIGN KEY ("' + k + '") REFERENCES "' + ref + '" ON DELETE ' + f.ondelete)
|
||||||
cr.commit()
|
cr.commit()
|
||||||
else:
|
else:
|
||||||
logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error !")
|
logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error, column %s->%s has multiple instances !"%(self._table,k))
|
||||||
for order,f,k in todo_update_store:
|
for order,f,k in todo_update_store:
|
||||||
todo_end.append((order, self._update_store, (f, k)))
|
todo_end.append((order, self._update_store, (f, k)))
|
||||||
|
|
||||||
|
@ -2243,9 +2279,11 @@ class orm(orm_template):
|
||||||
self.pool.get('ir.model.access').check(cr, user, self._name, 'read', context=context)
|
self.pool.get('ir.model.access').check(cr, user, self._name, 'read', context=context)
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = self._columns.keys() + self._inherit_fields.keys()
|
fields = self._columns.keys() + self._inherit_fields.keys()
|
||||||
select = ids
|
|
||||||
if isinstance(ids, (int, long)):
|
if isinstance(ids, (int, long)):
|
||||||
select = [ids]
|
select = [ids]
|
||||||
|
else:
|
||||||
|
select = map(int,ids)
|
||||||
|
|
||||||
select = map(lambda x: isinstance(x,dict) and x['id'] or x, select)
|
select = map(lambda x: isinstance(x,dict) and x['id'] or x, select)
|
||||||
result = self._read_flat(cr, user, select, fields, context, load)
|
result = self._read_flat(cr, user, select, fields, context, load)
|
||||||
|
|
||||||
|
@ -2292,18 +2330,16 @@ class orm(orm_template):
|
||||||
for i in range(0, len(ids), cr.IN_MAX):
|
for i in range(0, len(ids), cr.IN_MAX):
|
||||||
sub_ids = ids[i:i+cr.IN_MAX]
|
sub_ids = ids[i:i+cr.IN_MAX]
|
||||||
if d1:
|
if d1:
|
||||||
cr.execute('SELECT %s FROM \"%s\" WHERE id IN (%s) AND %s ORDER BY %s' % \
|
cr.execute('SELECT %s FROM \"%s\" WHERE id = ANY (%%s) AND %s ORDER BY %s' % \
|
||||||
(','.join(fields_pre2 + ['id']), self._table,
|
(','.join(fields_pre2 + ['id']), self._table, d1,
|
||||||
','.join(['%s' for x in sub_ids]), d1,
|
self._order),[sub_ids,]+d2)
|
||||||
self._order),sub_ids + d2)
|
|
||||||
if not cr.rowcount == len({}.fromkeys(sub_ids)):
|
if not cr.rowcount == len({}.fromkeys(sub_ids)):
|
||||||
raise except_orm(_('AccessError'),
|
raise except_orm(_('AccessError'),
|
||||||
_('You try to bypass an access rule (Document type: %s).') % self._description)
|
_('You try to bypass an access rule (Document type: %s).') % self._description)
|
||||||
else:
|
else:
|
||||||
cr.execute('SELECT %s FROM \"%s\" WHERE id IN (%s) ORDER BY %s' % \
|
cr.execute('SELECT %s FROM \"%s\" WHERE id = ANY (%%s) ORDER BY %s' % \
|
||||||
(','.join(fields_pre2 + ['id']), self._table,
|
(','.join(fields_pre2 + ['id']), self._table,
|
||||||
','.join(['%s' for x in sub_ids]),
|
self._order), (sub_ids,))
|
||||||
self._order), sub_ids)
|
|
||||||
res.extend(cr.dictfetchall())
|
res.extend(cr.dictfetchall())
|
||||||
else:
|
else:
|
||||||
res = map(lambda x: {'id': x}, ids)
|
res = map(lambda x: {'id': x}, ids)
|
||||||
|
@ -2313,6 +2349,7 @@ class orm(orm_template):
|
||||||
continue
|
continue
|
||||||
if self._columns[f].translate:
|
if self._columns[f].translate:
|
||||||
ids = map(lambda x: x['id'], res)
|
ids = map(lambda x: x['id'], res)
|
||||||
|
#TODO: optimize out of this loop
|
||||||
res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context.get('lang', False) or 'en_US', ids)
|
res_trans = self.pool.get('ir.translation')._get_ids(cr, user, self._name+','+f, 'model', context.get('lang', False) or 'en_US', ids)
|
||||||
for r in res:
|
for r in res:
|
||||||
r[f] = res_trans.get(r['id'], False) or r[f]
|
r[f] = res_trans.get(r['id'], False) or r[f]
|
||||||
|
@ -2628,6 +2665,7 @@ class orm(orm_template):
|
||||||
'where id in ('+ids_str+')', upd1)
|
'where id in ('+ids_str+')', upd1)
|
||||||
|
|
||||||
if totranslate:
|
if totranslate:
|
||||||
|
# TODO: optimize
|
||||||
for f in direct:
|
for f in direct:
|
||||||
if self._columns[f].translate:
|
if self._columns[f].translate:
|
||||||
src_trans = self.pool.get(self._name).read(cr,user,ids,[f])
|
src_trans = self.pool.get(self._name).read(cr,user,ids,[f])
|
||||||
|
@ -3145,6 +3183,7 @@ class orm(orm_template):
|
||||||
elif ftype in ('one2many', 'one2one'):
|
elif ftype in ('one2many', 'one2one'):
|
||||||
res = []
|
res = []
|
||||||
rel = self.pool.get(fields[f]['relation'])
|
rel = self.pool.get(fields[f]['relation'])
|
||||||
|
if data[f] != False:
|
||||||
for rel_id in data[f]:
|
for rel_id in data[f]:
|
||||||
# the lines are first duplicated using the wrong (old)
|
# the lines are first duplicated using the wrong (old)
|
||||||
# parent but then are reassigned to the correct one thanks
|
# parent but then are reassigned to the correct one thanks
|
||||||
|
@ -3157,6 +3196,7 @@ class orm(orm_template):
|
||||||
data[f] = [(6, 0, data[f])]
|
data[f] = [(6, 0, data[f])]
|
||||||
|
|
||||||
trans_obj = self.pool.get('ir.translation')
|
trans_obj = self.pool.get('ir.translation')
|
||||||
|
#TODO: optimize translations
|
||||||
trans_name=''
|
trans_name=''
|
||||||
for f in fields:
|
for f in fields:
|
||||||
trans_flag=True
|
trans_flag=True
|
||||||
|
@ -3205,7 +3245,7 @@ class orm(orm_template):
|
||||||
sub_ids_parent = ids_parent[i:i+cr.IN_MAX]
|
sub_ids_parent = ids_parent[i:i+cr.IN_MAX]
|
||||||
cr.execute('SELECT distinct "'+parent+'"'+
|
cr.execute('SELECT distinct "'+parent+'"'+
|
||||||
' FROM "'+self._table+'" ' \
|
' FROM "'+self._table+'" ' \
|
||||||
'WHERE id in ('+','.join(map(str, sub_ids_parent))+')')
|
'WHERE id = ANY(%s)',(sub_ids_parent,))
|
||||||
ids_parent2.extend(filter(None, map(lambda x: x[0], cr.fetchall())))
|
ids_parent2.extend(filter(None, map(lambda x: x[0], cr.fetchall())))
|
||||||
ids_parent = ids_parent2
|
ids_parent = ids_parent2
|
||||||
for i in ids_parent:
|
for i in ids_parent:
|
||||||
|
|
|
@ -48,7 +48,7 @@ def toxml(val):
|
||||||
|
|
||||||
class report_int(netsvc.Service):
|
class report_int(netsvc.Service):
|
||||||
def __init__(self, name, audience='*'):
|
def __init__(self, name, audience='*'):
|
||||||
assert not netsvc.service_exist(name), 'The report "%s" already exist!' % name
|
assert not self.service_exist(name), 'The report "%s" already exist!' % name
|
||||||
super(report_int, self).__init__(name, audience)
|
super(report_int, self).__init__(name, audience)
|
||||||
if name[0:7]<>'report.':
|
if name[0:7]<>'report.':
|
||||||
raise Exception, 'ConceptionError, bad report name, should start with "report."'
|
raise Exception, 'ConceptionError, bad report name, should start with "report."'
|
||||||
|
@ -56,7 +56,7 @@ class report_int(netsvc.Service):
|
||||||
self.id = 0
|
self.id = 0
|
||||||
self.name2 = '.'.join(name.split('.')[1:])
|
self.name2 = '.'.join(name.split('.')[1:])
|
||||||
self.title = None
|
self.title = None
|
||||||
self.joinGroup('report')
|
#self.joinGroup('report')
|
||||||
self.exportMethod(self.create)
|
self.exportMethod(self.create)
|
||||||
|
|
||||||
def create(self, cr, uid, ids, datas, context=None):
|
def create(self, cr, uid, ids, datas, context=None):
|
||||||
|
@ -80,6 +80,7 @@ class report_rml(report_int):
|
||||||
'html': self.create_html,
|
'html': self.create_html,
|
||||||
'raw': self.create_raw,
|
'raw': self.create_raw,
|
||||||
'sxw': self.create_sxw,
|
'sxw': self.create_sxw,
|
||||||
|
'txt': self.create_txt,
|
||||||
'odt': self.create_odt,
|
'odt': self.create_odt,
|
||||||
'html2html' : self.create_html2html,
|
'html2html' : self.create_html2html,
|
||||||
'makohtml2html' :self.create_makohtml2html,
|
'makohtml2html' :self.create_makohtml2html,
|
||||||
|
@ -204,11 +205,17 @@ class report_rml(report_int):
|
||||||
obj.render()
|
obj.render()
|
||||||
return obj.get()
|
return obj.get()
|
||||||
|
|
||||||
|
def create_txt(self, rml,localcontext, logo=None, title=None):
|
||||||
|
obj = render.rml2txt(rml, localcontext, self.bin_datas)
|
||||||
|
obj.render()
|
||||||
|
return obj.get().encode('utf-8')
|
||||||
|
|
||||||
def create_html2html(self, rml, localcontext = None, logo=None, title=None):
|
def create_html2html(self, rml, localcontext = None, logo=None, title=None):
|
||||||
obj = render.html2html(rml, localcontext, self.bin_datas)
|
obj = render.html2html(rml, localcontext, self.bin_datas)
|
||||||
obj.render()
|
obj.render()
|
||||||
return obj.get()
|
return obj.get()
|
||||||
|
|
||||||
|
|
||||||
def create_raw(self,rml, localcontext = None, logo=None, title=None):
|
def create_raw(self,rml, localcontext = None, logo=None, title=None):
|
||||||
obj = render.odt2odt(etree.XML(rml),localcontext)
|
obj = render.odt2odt(etree.XML(rml),localcontext)
|
||||||
obj.render()
|
obj.render()
|
||||||
|
@ -237,8 +244,9 @@ def register_all(db):
|
||||||
cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
|
cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
|
||||||
result = cr.dictfetchall()
|
result = cr.dictfetchall()
|
||||||
cr.close()
|
cr.close()
|
||||||
|
svcs = netsvc.Service._services
|
||||||
for r in result:
|
for r in result:
|
||||||
if netsvc.service_exist('report.'+r['report_name']):
|
if svcs.has_key('report.'+r['report_name']):
|
||||||
continue
|
continue
|
||||||
if r['report_rml'] or r['report_rml_content_data']:
|
if r['report_rml'] or r['report_rml_content_data']:
|
||||||
report_sxw('report.'+r['report_name'], r['model'],
|
report_sxw('report.'+r['report_name'], r['model'],
|
||||||
|
@ -250,4 +258,3 @@ def register_all(db):
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
from simple import simple
|
from simple import simple
|
||||||
from rml import rml, rml2html, odt2odt, html2html, makohtml2html
|
from rml import rml, rml2html, rml2txt, odt2odt , html2html, makohtml2html
|
||||||
from render import render
|
from render import render
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import render
|
import render
|
||||||
import rml2pdf
|
import rml2pdf
|
||||||
import rml2html as htmlizer
|
import rml2html as htmlizer
|
||||||
|
import rml2txt as txtizer
|
||||||
import odt2odt as odt
|
import odt2odt as odt
|
||||||
import html2html as html
|
import html2html as html
|
||||||
import makohtml2html as makohtml
|
import makohtml2html as makohtml
|
||||||
|
@ -50,6 +51,16 @@ class rml2html(render.render):
|
||||||
def _render(self):
|
def _render(self):
|
||||||
return htmlizer.parseString(self.rml,self.localcontext)
|
return htmlizer.parseString(self.rml,self.localcontext)
|
||||||
|
|
||||||
|
class rml2txt(render.render):
|
||||||
|
def __init__(self, rml, localcontext= None, datas={}):
|
||||||
|
super(rml2txt, self).__init__(datas)
|
||||||
|
self.rml = rml
|
||||||
|
self.localcontext = localcontext
|
||||||
|
self.output_type = 'txt'
|
||||||
|
|
||||||
|
def _render(self):
|
||||||
|
return txtizer.parseString(self.rml, self.localcontext)
|
||||||
|
|
||||||
class odt2odt(render.render):
|
class odt2odt(render.render):
|
||||||
def __init__(self, rml, localcontext = None, datas = {}):
|
def __init__(self, rml, localcontext = None, datas = {}):
|
||||||
render.render.__init__(self, datas)
|
render.render.__init__(self, datas)
|
||||||
|
|
|
@ -454,7 +454,7 @@ if __name__=="__main__":
|
||||||
rml2html_help()
|
rml2html_help()
|
||||||
print parseString(file(sys.argv[1], 'r').read()),
|
print parseString(file(sys.argv[1], 'r').read()),
|
||||||
else:
|
else:
|
||||||
print 'Usage: trml2pdf input.rml >output.pdf'
|
print 'Usage: rml2html input.rml >output.html'
|
||||||
print 'Try \'trml2pdf --help\' for more information.'
|
print 'Try \'rml2html --help\' for more information.'
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,49 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2004-2009 P. Christeas, Tiny SPRL (<http://tiny.be>).
|
||||||
|
# All Rights Reserved
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
CustomTTFonts = [ ('Helvetica',"DejaVu Sans", "DejaVuSans.ttf", 'normal'),
|
||||||
|
('Helvetica',"DejaVu Sans Bold", "DejaVuSans-Bold.ttf", 'bold'),
|
||||||
|
('Helvetica',"DejaVu Sans Oblique", "DejaVuSans-Oblique.ttf", 'italic'),
|
||||||
|
('Helvetica',"DejaVu Sans BoldOblique", "DejaVuSans-BoldOblique.ttf", 'bolditalic'),
|
||||||
|
('Times',"Liberation Serif", "LiberationSerif-Regular.ttf", 'normal'),
|
||||||
|
('Times',"Liberation Serif Bold", "LiberationSerif-Bold.ttf", 'bold'),
|
||||||
|
('Times',"Liberation Serif Italic", "LiberationSerif-Italic.ttf", 'italic'),
|
||||||
|
('Times',"Liberation Serif BoldItalic", "LiberationSerif-BoldItalic.ttf", 'bolditalic'),
|
||||||
|
('Times-Roman',"Liberation Serif", "LiberationSerif-Regular.ttf", 'normal'),
|
||||||
|
('Times-Roman',"Liberation Serif Bold", "LiberationSerif-Bold.ttf", 'bold'),
|
||||||
|
('Times-Roman',"Liberation Serif Italic", "LiberationSerif-Italic.ttf", 'italic'),
|
||||||
|
('Times-Roman',"Liberation Serif BoldItalic", "LiberationSerif-BoldItalic.ttf", 'bolditalic'),
|
||||||
|
('ZapfDingbats',"DejaVu Serif", "DejaVuSerif.ttf", 'normal'),
|
||||||
|
('ZapfDingbats',"DejaVu Serif Bold", "DejaVuSerif-Bold.ttf", 'bold'),
|
||||||
|
('ZapfDingbats',"DejaVu Serif Italic", "DejaVuSerif-Italic.ttf", 'italic'),
|
||||||
|
('ZapfDingbats',"DejaVu Serif BoldItalic", "DejaVuSerif-BoldItalic.ttf", 'bolditalic'),
|
||||||
|
('Courier',"FreeMono", "FreeMono.ttf", 'normal'),
|
||||||
|
('Courier',"FreeMono Bold", "FreeMonoBold.ttf", 'bold'),
|
||||||
|
('Courier',"FreeMono Oblique", "FreeMonoOblique.ttf", 'italic'),
|
||||||
|
('Courier',"FreeMono BoldOblique", "FreeMonoBoldOblique.ttf", 'bolditalic'),]
|
||||||
|
|
||||||
|
def SetCustomFonts(rmldoc):
|
||||||
|
for name, font, fname, mode in CustomTTFonts:
|
||||||
|
rmldoc.setTTFontMapping(name, font,fname, mode)
|
||||||
|
|
||||||
|
#eof
|
|
@ -163,6 +163,26 @@ class _rml_doc(object):
|
||||||
addMapping(name, 1, 0, name) #bold
|
addMapping(name, 1, 0, name) #bold
|
||||||
addMapping(name, 1, 1, name) #italic and bold
|
addMapping(name, 1, 1, name) #italic and bold
|
||||||
|
|
||||||
|
def setTTFontMapping(self,face, fontname,filename, mode='all'):
|
||||||
|
from reportlab.lib.fonts import addMapping
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
|
||||||
|
pdfmetrics.registerFont(TTFont(fontname, filename ))
|
||||||
|
if (mode == 'all'):
|
||||||
|
addMapping(face, 0, 0, fontname) #normal
|
||||||
|
addMapping(face, 0, 1, fontname) #italic
|
||||||
|
addMapping(face, 1, 0, fontname) #bold
|
||||||
|
addMapping(face, 1, 1, fontname) #italic and bold
|
||||||
|
elif (mode== 'normal') or (mode == 'regular'):
|
||||||
|
addMapping(face, 0, 0, fontname) #normal
|
||||||
|
elif (mode == 'italic'):
|
||||||
|
addMapping(face, 0, 1, fontname) #italic
|
||||||
|
elif (mode == 'bold'):
|
||||||
|
addMapping(face, 1, 0, fontname) #bold
|
||||||
|
elif (mode == 'bolditalic'):
|
||||||
|
addMapping(face, 1, 1, fontname) #italic and bold
|
||||||
|
|
||||||
def _textual_image(self, node):
|
def _textual_image(self, node):
|
||||||
rc = ''
|
rc = ''
|
||||||
for n in node.getchildren():
|
for n in node.getchildren():
|
||||||
|
@ -775,6 +795,12 @@ class _rml_template(object):
|
||||||
def parseNode(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
def parseNode(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
||||||
node = etree.XML(rml)
|
node = etree.XML(rml)
|
||||||
r = _rml_doc(node, localcontext, images, path, title=title)
|
r = _rml_doc(node, localcontext, images, path, title=title)
|
||||||
|
#try to override some font mappings
|
||||||
|
try:
|
||||||
|
from customfonts import SetCustomFonts
|
||||||
|
SetCustomFonts(r)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
fp = cStringIO.StringIO()
|
fp = cStringIO.StringIO()
|
||||||
r.render(fp)
|
r.render(fp)
|
||||||
return fp.getvalue()
|
return fp.getvalue()
|
||||||
|
@ -782,6 +808,14 @@ def parseNode(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
||||||
def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
||||||
node = etree.XML(rml)
|
node = etree.XML(rml)
|
||||||
r = _rml_doc(node, localcontext, images, path, title=title)
|
r = _rml_doc(node, localcontext, images, path, title=title)
|
||||||
|
|
||||||
|
#try to override some font mappings
|
||||||
|
try:
|
||||||
|
from customfonts import SetCustomFonts
|
||||||
|
SetCustomFonts(r)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if fout:
|
if fout:
|
||||||
fp = file(fout,'wb')
|
fp = file(fout,'wb')
|
||||||
r.render(fp)
|
r.render(fp)
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
from rml2txt import parseString, parseNode
|
||||||
|
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -0,0 +1,551 @@
|
||||||
|
#!/bin/env python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Copyright (C) 2005, Fabien Pinckaers, UCL, FSA
|
||||||
|
# Copyright (C) 2008, P. Christeas
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import StringIO
|
||||||
|
import copy
|
||||||
|
from lxml import etree
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
Font_size= 10.0
|
||||||
|
|
||||||
|
def verbose(text):
|
||||||
|
sys.stderr.write(text+"\n");
|
||||||
|
|
||||||
|
class textbox():
|
||||||
|
"""A box containing plain text.
|
||||||
|
It can have an offset, in chars.
|
||||||
|
Lines can be either text strings, or textbox'es, recursively.
|
||||||
|
"""
|
||||||
|
def __init__(self,x=0, y=0):
|
||||||
|
self.posx = x
|
||||||
|
self.posy = y
|
||||||
|
self.lines = []
|
||||||
|
self.curline = ''
|
||||||
|
self.endspace = False
|
||||||
|
|
||||||
|
def newline(self):
|
||||||
|
if isinstance(self.curline, textbox):
|
||||||
|
self.lines.extend(self.curline.renderlines())
|
||||||
|
else:
|
||||||
|
self.lines.append(self.curline)
|
||||||
|
self.curline = ''
|
||||||
|
|
||||||
|
def fline(self):
|
||||||
|
if isinstance(self.curline, textbox):
|
||||||
|
self.lines.extend(self.curline.renderlines())
|
||||||
|
elif len(self.curline):
|
||||||
|
self.lines.append(self.curline)
|
||||||
|
self.curline = ''
|
||||||
|
|
||||||
|
def appendtxt(self,txt):
|
||||||
|
"""Append some text to the current line.
|
||||||
|
Mimic the HTML behaviour, where all whitespace evaluates to
|
||||||
|
a single space """
|
||||||
|
if not txt:
|
||||||
|
return
|
||||||
|
bs = es = False
|
||||||
|
if txt[0].isspace():
|
||||||
|
bs = True
|
||||||
|
if txt[len(txt)-1].isspace():
|
||||||
|
es = True
|
||||||
|
if bs and not self.endspace:
|
||||||
|
self.curline += " "
|
||||||
|
self.curline += txt.strip().replace("\n"," ").replace("\t"," ")
|
||||||
|
if es:
|
||||||
|
self.curline += " "
|
||||||
|
self.endspace = es
|
||||||
|
|
||||||
|
def rendertxt(self,xoffset=0):
|
||||||
|
result = ''
|
||||||
|
lineoff = ""
|
||||||
|
for i in range(self.posy):
|
||||||
|
result +="\n"
|
||||||
|
for i in range(self.posx+xoffset):
|
||||||
|
lineoff+=" "
|
||||||
|
for l in self.lines:
|
||||||
|
result+= lineoff+ l +"\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
def renderlines(self,pad=0):
|
||||||
|
"""Returns a list of lines, from the current object
|
||||||
|
pad: all lines must be at least pad characters.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
lineoff = ""
|
||||||
|
for i in range(self.posx):
|
||||||
|
lineoff+=" "
|
||||||
|
for l in self.lines:
|
||||||
|
lpad = ""
|
||||||
|
if pad and len(l) < pad :
|
||||||
|
for i in range(pad - len(l)):
|
||||||
|
lpad += " "
|
||||||
|
#elif pad and len(l) > pad ?
|
||||||
|
result.append(lineoff+ l+lpad)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def haplines(self,arr,offset,cc= ''):
|
||||||
|
""" Horizontaly append lines
|
||||||
|
"""
|
||||||
|
while (len(self.lines) < len(arr)):
|
||||||
|
self.lines.append("")
|
||||||
|
|
||||||
|
for i in range(len(self.lines)):
|
||||||
|
while (len(self.lines[i]) < offset):
|
||||||
|
self.lines[i] += " "
|
||||||
|
for i in range(len(arr)):
|
||||||
|
self.lines[i] += cc +arr[i]
|
||||||
|
|
||||||
|
|
||||||
|
class _flowable(object):
|
||||||
|
def __init__(self, template, doc,localcontext):
|
||||||
|
self._tags = {
|
||||||
|
'1title': self._tag_title,
|
||||||
|
'1spacer': self._tag_spacer,
|
||||||
|
'para': self._tag_para,
|
||||||
|
'font': self._tag_font,
|
||||||
|
'section': self._tag_section,
|
||||||
|
'1nextFrame': self._tag_next_frame,
|
||||||
|
'blockTable': self._tag_table,
|
||||||
|
'1pageBreak': self._tag_page_break,
|
||||||
|
'1setNextTemplate': self._tag_next_template,
|
||||||
|
}
|
||||||
|
self.template = template
|
||||||
|
self.doc = doc
|
||||||
|
self.localcontext = localcontext
|
||||||
|
self.nitags = []
|
||||||
|
self.tbox = None
|
||||||
|
|
||||||
|
def warn_nitag(self,tag):
|
||||||
|
if tag not in self.nitags:
|
||||||
|
verbose("Unknown tag \"%s\", please implement it." % tag)
|
||||||
|
self.nitags.append(tag)
|
||||||
|
|
||||||
|
def _tag_page_break(self, node):
|
||||||
|
return "\f"
|
||||||
|
|
||||||
|
def _tag_next_template(self, node):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _tag_next_frame(self, node):
|
||||||
|
result=self.template.frame_stop()
|
||||||
|
result+='\n'
|
||||||
|
result+=self.template.frame_start()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _tag_title(self, node):
|
||||||
|
node.tagName='h1'
|
||||||
|
return node.toxml()
|
||||||
|
|
||||||
|
def _tag_spacer(self, node):
|
||||||
|
length = 1+int(utils.unit_get(node.get('length')))/35
|
||||||
|
return "\n"*length
|
||||||
|
|
||||||
|
def _tag_table(self, node):
|
||||||
|
self.tb.fline()
|
||||||
|
saved_tb = self.tb
|
||||||
|
self.tb = None
|
||||||
|
sizes = None
|
||||||
|
if node.get('colWidths'):
|
||||||
|
sizes = map(lambda x: utils.unit_get(x), node.get('colWidths').split(','))
|
||||||
|
trs = []
|
||||||
|
for n in utils._child_get(node,self):
|
||||||
|
if n.tag == 'tr':
|
||||||
|
tds = []
|
||||||
|
for m in utils._child_get(n,self):
|
||||||
|
if m.tag == 'td':
|
||||||
|
self.tb = textbox()
|
||||||
|
self.rec_render_cnodes(m)
|
||||||
|
tds.append(self.tb)
|
||||||
|
self.tb = None
|
||||||
|
if len(tds):
|
||||||
|
trs.append(tds)
|
||||||
|
|
||||||
|
if not sizes:
|
||||||
|
verbose("computing table sizes..")
|
||||||
|
for tds in trs:
|
||||||
|
trt = textbox()
|
||||||
|
off=0
|
||||||
|
for i in range(len(tds)):
|
||||||
|
p = int(sizes[i]/Font_size)
|
||||||
|
trl = tds[i].renderlines(pad=p)
|
||||||
|
trt.haplines(trl,off)
|
||||||
|
off += sizes[i]/Font_size
|
||||||
|
saved_tb.curline = trt
|
||||||
|
saved_tb.fline()
|
||||||
|
|
||||||
|
self.tb = saved_tb
|
||||||
|
return
|
||||||
|
|
||||||
|
def _tag_para(self, node):
|
||||||
|
#TODO: styles
|
||||||
|
self.rec_render_cnodes(node)
|
||||||
|
self.tb.newline()
|
||||||
|
|
||||||
|
def _tag_section(self, node):
|
||||||
|
#TODO: styles
|
||||||
|
self.rec_render_cnodes(node)
|
||||||
|
self.tb.newline()
|
||||||
|
|
||||||
|
def _tag_font(self, node):
|
||||||
|
"""We do ignore fonts.."""
|
||||||
|
self.rec_render_cnodes(node)
|
||||||
|
|
||||||
|
def rec_render_cnodes(self,node):
|
||||||
|
self.tb.appendtxt(utils._process_text(self, node.text or ''))
|
||||||
|
for n in utils._child_get(node,self):
|
||||||
|
self.rec_render(n)
|
||||||
|
self.tb.appendtxt(utils._process_text(self, node.tail or ''))
|
||||||
|
|
||||||
|
def rec_render(self,node):
|
||||||
|
""" Recursive render: fill outarr with text of current node
|
||||||
|
"""
|
||||||
|
if node.tag != None:
|
||||||
|
if node.tag in self._tags:
|
||||||
|
self._tags[node.tag](node)
|
||||||
|
else:
|
||||||
|
self.warn_nitag(node.tag)
|
||||||
|
|
||||||
|
def render(self, node):
|
||||||
|
self.tb= textbox()
|
||||||
|
#result = self.template.start()
|
||||||
|
#result += self.template.frame_start()
|
||||||
|
self.rec_render_cnodes(node)
|
||||||
|
#result += self.template.frame_stop()
|
||||||
|
#result += self.template.end()
|
||||||
|
result = self.tb.rendertxt()
|
||||||
|
del self.tb
|
||||||
|
return result
|
||||||
|
|
||||||
|
class _rml_tmpl_tag(object):
|
||||||
|
def __init__(self, *args):
|
||||||
|
pass
|
||||||
|
def tag_start(self):
|
||||||
|
return ''
|
||||||
|
def tag_end(self):
|
||||||
|
return False
|
||||||
|
def tag_stop(self):
|
||||||
|
return ''
|
||||||
|
def tag_mergeable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class _rml_tmpl_frame(_rml_tmpl_tag):
|
||||||
|
def __init__(self, posx, width):
|
||||||
|
self.width = width
|
||||||
|
self.posx = posx
|
||||||
|
def tag_start(self):
|
||||||
|
return "frame start"
|
||||||
|
return '<table border="0" width="%d"><tr><td width="%d"> </td><td>' % (self.width+self.posx,self.posx)
|
||||||
|
def tag_end(self):
|
||||||
|
return True
|
||||||
|
def tag_stop(self):
|
||||||
|
return "frame stop"
|
||||||
|
return '</td></tr></table><br/>'
|
||||||
|
def tag_mergeable(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# An awfull workaround since I don't really understand the semantic behind merge.
|
||||||
|
def merge(self, frame):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _rml_tmpl_draw_string(_rml_tmpl_tag):
|
||||||
|
def __init__(self, node, style):
|
||||||
|
self.posx = utils.unit_get(node.get('x'))
|
||||||
|
self.posy = utils.unit_get(node.get('y'))
|
||||||
|
aligns = {
|
||||||
|
'drawString': 'left',
|
||||||
|
'drawRightString': 'right',
|
||||||
|
'drawCentredString': 'center'
|
||||||
|
}
|
||||||
|
align = aligns[node.localName]
|
||||||
|
self.pos = [(self.posx, self.posy, align, utils.text_get(node), style.get('td'), style.font_size_get('td'))]
|
||||||
|
|
||||||
|
def tag_start(self):
|
||||||
|
return "draw string \"%s\" @(%d,%d)..\n" %("txt",self.posx,self.posy)
|
||||||
|
self.pos.sort()
|
||||||
|
res = '\\table ...'
|
||||||
|
posx = 0
|
||||||
|
i = 0
|
||||||
|
for (x,y,align,txt, style, fs) in self.pos:
|
||||||
|
if align=="left":
|
||||||
|
pos2 = len(txt)*fs
|
||||||
|
res+='<td width="%d"></td><td style="%s" width="%d">%s</td>' % (x - posx, style, pos2, txt)
|
||||||
|
posx = x+pos2
|
||||||
|
if align=="right":
|
||||||
|
res+='<td width="%d" align="right" style="%s">%s</td>' % (x - posx, style, txt)
|
||||||
|
posx = x
|
||||||
|
if align=="center":
|
||||||
|
res+='<td width="%d" align="center" style="%s">%s</td>' % ((x - posx)*2, style, txt)
|
||||||
|
posx = 2*x-posx
|
||||||
|
i+=1
|
||||||
|
res+='\\table end'
|
||||||
|
return res
|
||||||
|
def merge(self, ds):
|
||||||
|
self.pos+=ds.pos
|
||||||
|
|
||||||
|
class _rml_tmpl_draw_lines(_rml_tmpl_tag):
|
||||||
|
def __init__(self, node, style):
|
||||||
|
coord = [utils.unit_get(x) for x in utils.text_get(node).split(' ')]
|
||||||
|
self.ok = False
|
||||||
|
self.posx = coord[0]
|
||||||
|
self.posy = coord[1]
|
||||||
|
self.width = coord[2]-coord[0]
|
||||||
|
self.ok = coord[1]==coord[3]
|
||||||
|
self.style = style
|
||||||
|
self.style = style.get('hr')
|
||||||
|
|
||||||
|
def tag_start(self):
|
||||||
|
return "draw lines..\n"
|
||||||
|
if self.ok:
|
||||||
|
return '<table border="0" cellpadding="0" cellspacing="0" width="%d"><tr><td width="%d"></td><td><hr width="100%%" style="margin:0px; %s"></td></tr></table>' % (self.posx+self.width,self.posx,self.style)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
class _rml_stylesheet(object):
|
||||||
|
def __init__(self, stylesheet, doc):
|
||||||
|
self.doc = doc
|
||||||
|
self.attrs = {}
|
||||||
|
self._tags = {
|
||||||
|
'fontSize': lambda x: ('font-size',str(utils.unit_get(x))+'px'),
|
||||||
|
'alignment': lambda x: ('text-align',str(x))
|
||||||
|
}
|
||||||
|
result = ''
|
||||||
|
for ps in stylesheet.findall('paraStyle'):
|
||||||
|
attr = {}
|
||||||
|
attrs = ps.attributes
|
||||||
|
for i in range(attrs.length):
|
||||||
|
name = attrs.item(i).localName
|
||||||
|
attr[name] = ps.get(name)
|
||||||
|
attrs = []
|
||||||
|
for a in attr:
|
||||||
|
if a in self._tags:
|
||||||
|
attrs.append("%s:%s" % self._tags[a](attr[a]))
|
||||||
|
if len(attrs):
|
||||||
|
result += "p."+attr['name']+" {"+'; '.join(attrs)+"}\n"
|
||||||
|
self.result = result
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
class _rml_draw_style(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.style = {}
|
||||||
|
self._styles = {
|
||||||
|
'fill': lambda x: {'td': {'color':x.get('color')}},
|
||||||
|
'setFont': lambda x: {'td': {'font-size':x.get('size')+'px'}},
|
||||||
|
'stroke': lambda x: {'hr': {'color':x.get('color')}},
|
||||||
|
}
|
||||||
|
def update(self, node):
|
||||||
|
if node.localName in self._styles:
|
||||||
|
result = self._styles[node.localName](node)
|
||||||
|
for key in result:
|
||||||
|
if key in self.style:
|
||||||
|
self.style[key].update(result[key])
|
||||||
|
else:
|
||||||
|
self.style[key] = result[key]
|
||||||
|
def font_size_get(self,tag):
|
||||||
|
size = utils.unit_get(self.style.get('td', {}).get('font-size','16'))
|
||||||
|
return size
|
||||||
|
|
||||||
|
def get(self,tag):
|
||||||
|
if not tag in self.style:
|
||||||
|
return ""
|
||||||
|
return ';'.join(['%s:%s' % (x[0],x[1]) for x in self.style[tag].items()])
|
||||||
|
|
||||||
|
class _rml_template(object):
|
||||||
|
def __init__(self, localcontext, out, node, doc, images={}, path='.', title=None):
|
||||||
|
self.localcontext = localcontext
|
||||||
|
self.frame_pos = -1
|
||||||
|
self.frames = []
|
||||||
|
self.template_order = []
|
||||||
|
self.page_template = {}
|
||||||
|
self.loop = 0
|
||||||
|
self._tags = {
|
||||||
|
'drawString': _rml_tmpl_draw_string,
|
||||||
|
'drawRightString': _rml_tmpl_draw_string,
|
||||||
|
'drawCentredString': _rml_tmpl_draw_string,
|
||||||
|
'lines': _rml_tmpl_draw_lines
|
||||||
|
}
|
||||||
|
self.style = _rml_draw_style()
|
||||||
|
for pt in node.findall('pageTemplate'):
|
||||||
|
frames = {}
|
||||||
|
id = pt.get('id')
|
||||||
|
self.template_order.append(id)
|
||||||
|
for tmpl in pt.findall('frame'):
|
||||||
|
posy = int(utils.unit_get(tmpl.get('y1'))) #+utils.unit_get(tmpl.get('height')))
|
||||||
|
posx = int(utils.unit_get(tmpl.get('x1')))
|
||||||
|
frames[(posy,posx,tmpl.get('id'))] = _rml_tmpl_frame(posx, utils.unit_get(tmpl.get('width')))
|
||||||
|
for tmpl in node.findall('pageGraphics'):
|
||||||
|
for n in tmpl.getchildren():
|
||||||
|
if n.nodeType==n.ELEMENT_NODE:
|
||||||
|
if n.localName in self._tags:
|
||||||
|
t = self._tags[n.localName](n, self.style)
|
||||||
|
frames[(t.posy,t.posx,n.localName)] = t
|
||||||
|
else:
|
||||||
|
self.style.update(n)
|
||||||
|
keys = frames.keys()
|
||||||
|
keys.sort()
|
||||||
|
keys.reverse()
|
||||||
|
self.page_template[id] = []
|
||||||
|
for key in range(len(keys)):
|
||||||
|
if key>0 and keys[key-1][0] == keys[key][0]:
|
||||||
|
if type(self.page_template[id][-1]) == type(frames[keys[key]]):
|
||||||
|
if self.page_template[id][-1].tag_mergeable():
|
||||||
|
self.page_template[id][-1].merge(frames[keys[key]])
|
||||||
|
continue
|
||||||
|
self.page_template[id].append(frames[keys[key]])
|
||||||
|
self.template = self.template_order[0]
|
||||||
|
|
||||||
|
def _get_style(self):
|
||||||
|
return self.style
|
||||||
|
|
||||||
|
def set_next_template(self):
|
||||||
|
self.template = self.template_order[(self.template_order.index(name)+1) % self.template_order]
|
||||||
|
self.frame_pos = -1
|
||||||
|
|
||||||
|
def set_template(self, name):
|
||||||
|
self.template = name
|
||||||
|
self.frame_pos = -1
|
||||||
|
|
||||||
|
def frame_start(self):
|
||||||
|
result = ''
|
||||||
|
frames = self.page_template[self.template]
|
||||||
|
ok = True
|
||||||
|
while ok:
|
||||||
|
self.frame_pos += 1
|
||||||
|
if self.frame_pos>=len(frames):
|
||||||
|
self.frame_pos=0
|
||||||
|
self.loop=1
|
||||||
|
ok = False
|
||||||
|
continue
|
||||||
|
f = frames[self.frame_pos]
|
||||||
|
result+=f.tag_start()
|
||||||
|
ok = not f.tag_end()
|
||||||
|
if ok:
|
||||||
|
result+=f.tag_stop()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def frame_stop(self):
|
||||||
|
frames = self.page_template[self.template]
|
||||||
|
f = frames[self.frame_pos]
|
||||||
|
result=f.tag_stop()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
return "template end\n"
|
||||||
|
result = ''
|
||||||
|
while not self.loop:
|
||||||
|
result += self.frame_start()
|
||||||
|
result += self.frame_stop()
|
||||||
|
return result
|
||||||
|
|
||||||
|
class _rml_doc(object):
|
||||||
|
def __init__(self, node, localcontext, images={}, path='.', title=None):
|
||||||
|
self.localcontext = localcontext
|
||||||
|
self.etree = node
|
||||||
|
self.filename = self.etree.get('filename')
|
||||||
|
self.result = ''
|
||||||
|
|
||||||
|
def render(self, out):
|
||||||
|
#el = self.etree.findall('docinit')
|
||||||
|
#if el:
|
||||||
|
#self.docinit(el)
|
||||||
|
|
||||||
|
#el = self.etree.findall('stylesheet')
|
||||||
|
#self.styles = _rml_styles(el,self.localcontext)
|
||||||
|
|
||||||
|
el = self.etree.findall('template')
|
||||||
|
self.result =""
|
||||||
|
if len(el):
|
||||||
|
pt_obj = _rml_template(self.localcontext, out, el[0], self)
|
||||||
|
stories = utils._child_get(self.etree, self, 'story')
|
||||||
|
for story in stories:
|
||||||
|
if self.result:
|
||||||
|
self.result += '\f'
|
||||||
|
f = _flowable(pt_obj,story,self.localcontext)
|
||||||
|
self.result += f.render(story)
|
||||||
|
del f
|
||||||
|
else:
|
||||||
|
self.result = "<cannot render w/o template>"
|
||||||
|
self.result += '\n'
|
||||||
|
out.write( self.result)
|
||||||
|
|
||||||
|
def parseNode(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
||||||
|
node = etree.XML(rml)
|
||||||
|
r = _rml_doc(node, localcontext, images, path, title=title)
|
||||||
|
fp = StringIO.StringIO()
|
||||||
|
r.render(fp)
|
||||||
|
return fp.getvalue()
|
||||||
|
|
||||||
|
def parseString(rml, localcontext = {},fout=None, images={}, path='.',title=None):
|
||||||
|
node = etree.XML(rml)
|
||||||
|
r = _rml_doc(node, localcontext, images, path, title=title)
|
||||||
|
if fout:
|
||||||
|
fp = file(fout,'wb')
|
||||||
|
r.render(fp)
|
||||||
|
fp.close()
|
||||||
|
return fout
|
||||||
|
else:
|
||||||
|
fp = StringIO.StringIO()
|
||||||
|
r.render(fp)
|
||||||
|
return fp.getvalue()
|
||||||
|
|
||||||
|
def trml2pdf_help():
|
||||||
|
print 'Usage: rml2txt input.rml >output.html'
|
||||||
|
print 'Render the standard input (RML) and output an TXT file'
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
if len(sys.argv)>1:
|
||||||
|
if sys.argv[1]=='--help':
|
||||||
|
trml2pdf_help()
|
||||||
|
print parseString(file(sys.argv[1], 'r').read()).encode('iso8859-7')
|
||||||
|
else:
|
||||||
|
print 'Usage: trml2txt input.rml >output.pdf'
|
||||||
|
print 'Try \'trml2txt --help\' for more information.'
|
||||||
|
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# trml2pdf - An RML to PDF converter
|
||||||
|
# Copyright (C) 2003, Fabien Pinckaers, UCL, FSA
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
import re
|
||||||
|
import reportlab
|
||||||
|
import reportlab.lib.units
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
_regex = re.compile('\[\[(.+?)\]\]')
|
||||||
|
|
||||||
|
def _child_get(node, self=None, tagname=None):
|
||||||
|
for n in node:
|
||||||
|
if self and self.localcontext and n.get('rml_loop', False):
|
||||||
|
oldctx = self.localcontext
|
||||||
|
for ctx in eval(n.get('rml_loop'),{}, self.localcontext):
|
||||||
|
self.localcontext.update(ctx)
|
||||||
|
if (tagname is None) or (n.tag==tagname):
|
||||||
|
if n.get('rml_except', False):
|
||||||
|
try:
|
||||||
|
eval(n.get('rml_except'), {}, self.localcontext)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if n.get('rml_tag'):
|
||||||
|
try:
|
||||||
|
(tag,attr) = eval(n.get('rml_tag'),{}, self.localcontext)
|
||||||
|
n2 = copy.copy(n)
|
||||||
|
n2.tag = tag
|
||||||
|
n2.attrib.update(attr)
|
||||||
|
yield n2
|
||||||
|
except:
|
||||||
|
yield n
|
||||||
|
else:
|
||||||
|
yield n
|
||||||
|
self.localcontext = oldctx
|
||||||
|
continue
|
||||||
|
if self and self.localcontext and n.get('rml_except', False):
|
||||||
|
try:
|
||||||
|
eval(n.get('rml_except'), {}, self.localcontext)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if (tagname is None) or (n.tag==tagname):
|
||||||
|
yield n
|
||||||
|
|
||||||
|
def _process_text(self, txt):
|
||||||
|
if not self.localcontext:
|
||||||
|
return txt
|
||||||
|
if not txt:
|
||||||
|
return ''
|
||||||
|
result = ''
|
||||||
|
sps = _regex.split(txt)
|
||||||
|
while sps:
|
||||||
|
# This is a simple text to translate
|
||||||
|
result += self.localcontext.get('translate', lambda x:x)(sps.pop(0))
|
||||||
|
if sps:
|
||||||
|
try:
|
||||||
|
txt2 = eval(sps.pop(0),self.localcontext)
|
||||||
|
except:
|
||||||
|
txt2 = ''
|
||||||
|
if type(txt2) == type(0) or type(txt2) == type(0.0):
|
||||||
|
txt2 = str(txt2)
|
||||||
|
if type(txt2)==type('') or type(txt2)==type(u''):
|
||||||
|
result += txt2
|
||||||
|
return result
|
||||||
|
|
||||||
|
def text_get(node):
|
||||||
|
rc = ''
|
||||||
|
for node in node.getchildren():
|
||||||
|
rc = rc + node.text
|
||||||
|
return rc
|
||||||
|
|
||||||
|
units = [
|
||||||
|
(re.compile('^(-?[0-9\.]+)\s*in$'), reportlab.lib.units.inch),
|
||||||
|
(re.compile('^(-?[0-9\.]+)\s*cm$'), reportlab.lib.units.cm),
|
||||||
|
(re.compile('^(-?[0-9\.]+)\s*mm$'), reportlab.lib.units.mm),
|
||||||
|
(re.compile('^(-?[0-9\.]+)\s*$'), 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
def unit_get(size):
|
||||||
|
global units
|
||||||
|
if size:
|
||||||
|
for unit in units:
|
||||||
|
res = unit[0].search(size, 0)
|
||||||
|
if res:
|
||||||
|
return unit[1]*float(res.group(1))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tuple_int_get(node, attr_name, default=None):
|
||||||
|
if not node.get(attr_name):
|
||||||
|
return default
|
||||||
|
res = [int(x) for x in node.get(attr_name).split(',')]
|
||||||
|
return res
|
||||||
|
|
||||||
|
def bool_get(value):
|
||||||
|
return (str(value)=="1") or (value.lower()=='yes')
|
||||||
|
|
||||||
|
def attr_get(node, attrs, dict={}):
|
||||||
|
res = {}
|
||||||
|
for name in attrs:
|
||||||
|
if node.get(name):
|
||||||
|
res[name] = unit_get(node.get(name))
|
||||||
|
for key in dict:
|
||||||
|
if node.get(key):
|
||||||
|
if dict[key]=='str':
|
||||||
|
res[key] = str(node.get(key))
|
||||||
|
elif dict[key]=='bool':
|
||||||
|
res[key] = bool_get(node.get(key))
|
||||||
|
elif dict[key]=='int':
|
||||||
|
res[key] = int(node.get(key))
|
||||||
|
elif dict[key]=='unit':
|
||||||
|
res[key] = unit_get(node.get(key))
|
||||||
|
return res
|
||||||
|
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -67,7 +67,8 @@ class simple(render.render):
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__=='__main__':
|
||||||
import time
|
import time
|
||||||
s = simple('''<test>
|
s = simple()
|
||||||
|
s.xml = '''<test>
|
||||||
<author-list>
|
<author-list>
|
||||||
<author>
|
<author>
|
||||||
<name>Fabien Pinckaers</name>
|
<name>Fabien Pinckaers</name>
|
||||||
|
@ -79,8 +80,9 @@ if __name__=='__main__':
|
||||||
</author>
|
</author>
|
||||||
No other
|
No other
|
||||||
</author-list>
|
</author-list>
|
||||||
</test>''')
|
</test>'''
|
||||||
print s.render()
|
if s.render():
|
||||||
|
print s.get()
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -354,14 +354,14 @@ class report_sxw(report_rml, preprocess.report):
|
||||||
report_type = report_xml.report_type
|
report_type = report_xml.report_type
|
||||||
if report_type in ['sxw','odt']:
|
if report_type in ['sxw','odt']:
|
||||||
fnct = self.create_source_odt
|
fnct = self.create_source_odt
|
||||||
elif report_type in ['pdf','raw','html']:
|
elif report_type in ['pdf','raw','txt','html']:
|
||||||
fnct = self.create_source_pdf
|
fnct = self.create_source_pdf
|
||||||
elif report_type=='html2html':
|
elif report_type=='html2html':
|
||||||
fnct = self.create_source_html2html
|
fnct = self.create_source_html2html
|
||||||
elif report_type=='mako2html':
|
elif report_type=='mako2html':
|
||||||
fnct = self.create_source_mako2html
|
fnct = self.create_source_mako2html
|
||||||
else:
|
else:
|
||||||
raise 'Unknown Report Type'
|
raise Exception('Unknown Report Type: '+report_type)
|
||||||
return fnct(cr, uid, ids, data, report_xml, context)
|
return fnct(cr, uid, ids, data, report_xml, context)
|
||||||
|
|
||||||
def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
|
def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
|
||||||
|
|
|
@ -0,0 +1,319 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright P. Christeas <p_christ@hol.gr> 2008,2009
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# WARNING: This program as such is intended to be used by professional
|
||||||
|
# programmers who take the whole responsability of assessing all potential
|
||||||
|
# consequences resulting from its eventual inadequacies and bugs
|
||||||
|
# End users who are looking for a ready-to-use solution with commercial
|
||||||
|
# garantees and support are strongly adviced to contract a Free Software
|
||||||
|
# Service Company
|
||||||
|
#
|
||||||
|
# This program is Free Software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
""" This file contains instance of the http server.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
from websrv_lib import *
|
||||||
|
import netsvc
|
||||||
|
import threading
|
||||||
|
import tools
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
|
||||||
|
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
except ImportError:
|
||||||
|
fcntl = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ssl import SSLError
|
||||||
|
except ImportError:
|
||||||
|
class SSLError(Exception): pass
|
||||||
|
|
||||||
|
class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
|
||||||
|
""" A threaded httpd server, with all the necessary functionality for us.
|
||||||
|
|
||||||
|
It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
|
||||||
|
will be available to the request handler
|
||||||
|
"""
|
||||||
|
encoding = None
|
||||||
|
allow_none = False
|
||||||
|
allow_reuse_address = 1
|
||||||
|
_send_traceback_header = False
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
def __init__(self, addr, requestHandler,
|
||||||
|
logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
|
||||||
|
self.logRequests = logRequests
|
||||||
|
|
||||||
|
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||||
|
HTTPServer.__init__(self, addr, requestHandler)
|
||||||
|
|
||||||
|
# [Bug #1222790] If possible, set close-on-exec flag; if a
|
||||||
|
# method spawns a subprocess, the subprocess shouldn't have
|
||||||
|
# the listening socket open.
|
||||||
|
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
|
||||||
|
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
|
||||||
|
flags |= fcntl.FD_CLOEXEC
|
||||||
|
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
|
||||||
|
|
||||||
|
def handle_error(self, request, client_address):
|
||||||
|
""" Override the error handler
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
netsvc.Logger().notifyChannel("init", netsvc.LOG_ERROR,"Server error in request from %s:\n%s" %
|
||||||
|
(client_address,traceback.format_exc()))
|
||||||
|
|
||||||
|
class MultiHandler2(MultiHTTPHandler):
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args)
|
||||||
|
|
||||||
|
def log_error(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('http',netsvc.LOG_ERROR,format % args)
|
||||||
|
|
||||||
|
|
||||||
|
class SecureMultiHandler2(SecureMultiHTTPHandler):
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('https',netsvc.LOG_DEBUG,format % args)
|
||||||
|
|
||||||
|
def getcert_fnames(self):
|
||||||
|
tc = tools.config
|
||||||
|
fcert = tc.get_misc('httpsd','sslcert', 'ssl/server.cert')
|
||||||
|
fkey = tc.get_misc('httpsd','sslkey', 'ssl/server.key')
|
||||||
|
return (fcert,fkey)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args)
|
||||||
|
|
||||||
|
def log_error(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('http',netsvc.LOG_ERROR,format % args)
|
||||||
|
|
||||||
|
class HttpDaemon(threading.Thread, netsvc.Server):
|
||||||
|
def __init__(self, interface, port):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
netsvc.Server.__init__(self)
|
||||||
|
self.__port = port
|
||||||
|
self.__interface = interface
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.server = ThreadedHTTPServer((interface, port), MultiHandler2)
|
||||||
|
self.server.vdirs = []
|
||||||
|
self.server.logRequests = True
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
|
"starting HTTP service at %s port %d" % (interface or '0.0.0.0', port,))
|
||||||
|
except Exception, e:
|
||||||
|
netsvc.Logger().notifyChannel('httpd', netsvc.LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,))
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def attach(self, path, gw):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if os.name != 'nt':
|
||||||
|
self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 )
|
||||||
|
self.server.socket.close()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
#self.server.register_introspection_functions()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
self.server.handle_request()
|
||||||
|
return True
|
||||||
|
|
||||||
|
class HttpSDaemon(threading.Thread, netsvc.Server):
|
||||||
|
def __init__(self, interface, port):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
netsvc.Server.__init__(self)
|
||||||
|
self.__port = port
|
||||||
|
self.__interface = interface
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.server = ThreadedHTTPServer((interface, port), SecureMultiHandler2)
|
||||||
|
self.server.vdirs = []
|
||||||
|
self.server.logRequests = True
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
|
"starting HTTPS service at %s port %d" % (interface or '0.0.0.0', port,))
|
||||||
|
except SSLError, e:
|
||||||
|
netsvc.Logger().notifyChannel('httpd-ssl', netsvc.LOG_CRITICAL, "Can not load the certificate and/or the private key files")
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
netsvc.Logger().notifyChannel('httpd-ssl', netsvc.LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def attach(self, path, gw):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if os.name != 'nt':
|
||||||
|
self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 )
|
||||||
|
self.server.socket.close()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
#self.server.register_introspection_functions()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
self.server.handle_request()
|
||||||
|
return True
|
||||||
|
|
||||||
|
httpd = None
|
||||||
|
httpsd = None
|
||||||
|
|
||||||
|
def init_servers():
|
||||||
|
global httpd, httpsd
|
||||||
|
if tools.config.get_misc('httpd','enable', True):
|
||||||
|
httpd = HttpDaemon(tools.config.get_misc('httpd','interface', ''), \
|
||||||
|
tools.config.get_misc('httpd','port', 8069))
|
||||||
|
|
||||||
|
if tools.config.get_misc('httpsd','enable', False):
|
||||||
|
httpsd = HttpSDaemon(tools.config.get_misc('httpsd','interface', ''), \
|
||||||
|
tools.config.get_misc('httpsd','port', 8071))
|
||||||
|
|
||||||
|
def reg_http_service(hts, secure_only = False):
|
||||||
|
""" Register some handler to httpd.
|
||||||
|
hts must be an HTTPDir
|
||||||
|
"""
|
||||||
|
global httpd, httpsd
|
||||||
|
if not isinstance(hts, HTTPDir):
|
||||||
|
raise Exception("Wrong class for http service")
|
||||||
|
|
||||||
|
if httpd and not secure_only:
|
||||||
|
httpd.server.vdirs.append(hts)
|
||||||
|
|
||||||
|
if httpsd:
|
||||||
|
httpsd.server.vdirs.append(hts)
|
||||||
|
|
||||||
|
if (not httpd) and (not httpsd):
|
||||||
|
netsvc.Logger().notifyChannel('httpd',netsvc.LOG_WARNING,"No httpd available to register service %s" % hts.path)
|
||||||
|
return
|
||||||
|
|
||||||
|
import SimpleXMLRPCServer
|
||||||
|
class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
||||||
|
rpc_paths = []
|
||||||
|
protocol_version = 'HTTP/1.1'
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
try:
|
||||||
|
service_name = self.path.split("/")[-1]
|
||||||
|
return self.dispatch(service_name, method, params)
|
||||||
|
except netsvc.OpenERPDispatcherException, e:
|
||||||
|
raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
netsvc.Logger().notifyChannel('xmlrpc',netsvc.LOG_DEBUG_RPC,format % args)
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.connection = dummyconn()
|
||||||
|
if not len(XMLRPCRequestHandler.rpc_paths):
|
||||||
|
XMLRPCRequestHandler.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def init_xmlrpc():
|
||||||
|
if not tools.config.get_misc('xmlrpc','enable', True):
|
||||||
|
return
|
||||||
|
reg_http_service(HTTPDir('/xmlrpc/',XMLRPCRequestHandler))
|
||||||
|
# Example of http file serving:
|
||||||
|
# reg_http_service(HTTPDir('/test/',HTTPHandler))
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
|
"Registered XML-RPC over HTTP")
|
||||||
|
|
||||||
|
|
||||||
|
class OerpAuthProxy(AuthProxy):
|
||||||
|
""" Require basic authentication..
|
||||||
|
|
||||||
|
This is a copy of the BasicAuthProxy, which however checks/caches the db
|
||||||
|
as well.
|
||||||
|
"""
|
||||||
|
def __init__(self,provider):
|
||||||
|
AuthProxy.__init__(self,provider)
|
||||||
|
self.auth_creds = {}
|
||||||
|
self.auth_tries = 0
|
||||||
|
self.last_auth = None
|
||||||
|
|
||||||
|
def checkRequest(self,handler,path = '/'):
|
||||||
|
if self.auth_creds:
|
||||||
|
return True
|
||||||
|
auth_str = handler.headers.get('Authorization',False)
|
||||||
|
try:
|
||||||
|
db = handler.get_db_from_path(path)
|
||||||
|
print "Got db:",db
|
||||||
|
except:
|
||||||
|
if path.startswith('/'):
|
||||||
|
path = path[1:]
|
||||||
|
psp= path.split('/')
|
||||||
|
if len(psp)>1:
|
||||||
|
db = psp[0]
|
||||||
|
else:
|
||||||
|
#FIXME!
|
||||||
|
self.provider.log("Wrong path: %s, failing auth" %path)
|
||||||
|
raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
|
||||||
|
|
||||||
|
if auth_str and auth_str.startswith('Basic '):
|
||||||
|
auth_str=auth_str[len('Basic '):]
|
||||||
|
(user,passwd) = base64.decodestring(auth_str).split(':')
|
||||||
|
self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
|
||||||
|
acd = self.provider.authenticate(db,user,passwd,handler.client_address)
|
||||||
|
if acd != False:
|
||||||
|
self.auth_creds[db] = acd
|
||||||
|
self.last_auth=db
|
||||||
|
return True
|
||||||
|
if self.auth_tries > 5:
|
||||||
|
self.provider.log("Failing authorization after 5 requests w/o password")
|
||||||
|
raise AuthRejectedExc("Authorization failed.")
|
||||||
|
self.auth_tries += 1
|
||||||
|
raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
|
||||||
|
|
||||||
|
import security
|
||||||
|
class OpenERPAuthProvider(AuthProvider):
|
||||||
|
def __init__(self,realm = 'OpenERP User'):
|
||||||
|
self.realm = realm
|
||||||
|
|
||||||
|
def setupAuth(self, multi, handler):
|
||||||
|
if not multi.sec_realms.has_key(self.realm):
|
||||||
|
multi.sec_realms[self.realm] = OerpAuthProxy(self)
|
||||||
|
handler.auth_proxy = multi.sec_realms[self.realm]
|
||||||
|
|
||||||
|
def authenticate(self, db, user, passwd, client_address):
|
||||||
|
try:
|
||||||
|
uid = security.login(db,user,passwd)
|
||||||
|
if uid is False:
|
||||||
|
return False
|
||||||
|
return (user, passwd, db, uid)
|
||||||
|
except Exception,e:
|
||||||
|
netsvc.Logger().notifyChannel("auth",netsvc.LOG_DEBUG,"Fail auth:"+ str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
netsvc.Logger().notifyChannel("auth",netsvc.LOG_INFO,msg)
|
||||||
|
|
||||||
|
#eof
|
|
@ -0,0 +1,163 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright P. Christeas <p_christ@hol.gr> 2008,2009
|
||||||
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
""" This file contains instance of the net-rpc server
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import netsvc
|
||||||
|
import threading
|
||||||
|
import tools
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import tiny_socket
|
||||||
|
class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
|
||||||
|
def __init__(self, sock, threads):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.sock = sock
|
||||||
|
# Only at the server side, use a big timeout: close the
|
||||||
|
# clients connection when they're idle for 20min.
|
||||||
|
self.sock.settimeout(1200)
|
||||||
|
self.threads = threads
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
if hasattr(socket, 'SHUT_RDWR'):
|
||||||
|
self.socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
else:
|
||||||
|
self.socket.shutdown(2)
|
||||||
|
except: pass
|
||||||
|
# That should garbage-collect and close it, too
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# import select
|
||||||
|
self.running = True
|
||||||
|
try:
|
||||||
|
ts = tiny_socket.mysocket(self.sock)
|
||||||
|
except:
|
||||||
|
self.threads.remove(self)
|
||||||
|
self.running = False
|
||||||
|
return False
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
msg = ts.myreceive()
|
||||||
|
except:
|
||||||
|
self.threads.remove(self)
|
||||||
|
self.running = False
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
result = self.dispatch(msg[0], msg[1], msg[2:])
|
||||||
|
ts.mysend(result)
|
||||||
|
except netsvc.OpenERPDispatcherException, e:
|
||||||
|
try:
|
||||||
|
new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling
|
||||||
|
ts.mysend(new_e, exception=True, traceback=e.traceback)
|
||||||
|
except:
|
||||||
|
self.running = False
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
# this code should not be reachable, therefore we warn
|
||||||
|
netsvc.Logger().notifyChannel("net-rpc", netsvc.LOG_WARNING, "exception: %" % str(e))
|
||||||
|
break
|
||||||
|
|
||||||
|
self.threads.remove(self)
|
||||||
|
self.running = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
|
||||||
|
class TinySocketServerThread(threading.Thread,netsvc.Server):
|
||||||
|
def __init__(self, interface, port, secure=False):
|
||||||
|
threading.Thread.__init__(self, name="Net-RPC socket")
|
||||||
|
netsvc.Server.__init__(self)
|
||||||
|
self.__port = port
|
||||||
|
self.__interface = interface
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.socket.bind((self.__interface, self.__port))
|
||||||
|
self.socket.listen(5)
|
||||||
|
self.threads = []
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
|
"starting NET-RPC service at %s port %d" % (interface or '0.0.0.0', port,))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# import select
|
||||||
|
try:
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
(clientsocket, address) = self.socket.accept()
|
||||||
|
ct = TinySocketClientThread(clientsocket, self.threads)
|
||||||
|
clientsocket = None
|
||||||
|
self.threads.append(ct)
|
||||||
|
ct.start()
|
||||||
|
lt = len(self.threads)
|
||||||
|
if (lt > 10) and (lt % 10 == 0):
|
||||||
|
# Not many threads should be serving at the same time, so log
|
||||||
|
# their abuse.
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_DEBUG,
|
||||||
|
"Netrpc: %d threads" % len(self.threads))
|
||||||
|
self.socket.close()
|
||||||
|
except Exception, e:
|
||||||
|
netsvc.Logger().notifyChannel("web-services", netsvc.LOG_WARNING,
|
||||||
|
"Netrpc: closing because of exception %s" % str(e))
|
||||||
|
self.socket.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
for t in self.threads:
|
||||||
|
t.stop()
|
||||||
|
try:
|
||||||
|
if hasattr(socket, 'SHUT_RDWR'):
|
||||||
|
self.socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
else:
|
||||||
|
self.socket.shutdown(2)
|
||||||
|
self.socket.close()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stats(self):
|
||||||
|
res = "Net-RPC: " + ( (self.running and "running") or "stopped")
|
||||||
|
i = 0
|
||||||
|
for t in self.threads:
|
||||||
|
i += 1
|
||||||
|
res += "\nNet-RPC #%d: %s " % (i, t.name)
|
||||||
|
if t.isAlive():
|
||||||
|
res += "running"
|
||||||
|
else:
|
||||||
|
res += "finished"
|
||||||
|
if t.sock:
|
||||||
|
res += ", socket"
|
||||||
|
return res
|
||||||
|
|
||||||
|
netrpcd = None
|
||||||
|
|
||||||
|
def init_servers():
|
||||||
|
global netrpcd
|
||||||
|
if tools.config.get_misc('netrpcd','enable', True):
|
||||||
|
netrpcd = TinySocketServerThread(tools.config.get_misc('netrpcd','interface', ''), \
|
||||||
|
tools.config.get_misc('netrpcd','port', 8070))
|
|
@ -24,6 +24,13 @@ import tools
|
||||||
|
|
||||||
_uid_cache = {}
|
_uid_cache = {}
|
||||||
|
|
||||||
|
# When rejecting a password, we need to give as little info as possible
|
||||||
|
class ExceptionNoTb(Exception):
|
||||||
|
def __init__(self, msg ):
|
||||||
|
# self.message = msg # No need in Python 2.6
|
||||||
|
self.traceback = ('','','')
|
||||||
|
self.args = (msg, '')
|
||||||
|
|
||||||
def login(db, login, password):
|
def login(db, login, password):
|
||||||
cr = pooler.get_db(db).cursor()
|
cr = pooler.get_db(db).cursor()
|
||||||
if password:
|
if password:
|
||||||
|
@ -41,7 +48,7 @@ def check_super(passwd):
|
||||||
if passwd == tools.config['admin_passwd']:
|
if passwd == tools.config['admin_passwd']:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise Exception('AccessDenied')
|
raise ExceptionNoTb('AccessDenied')
|
||||||
|
|
||||||
def check(db, uid, passwd):
|
def check(db, uid, passwd):
|
||||||
cached_pass = _uid_cache.get(db, {}).get(uid)
|
cached_pass = _uid_cache.get(db, {}).get(uid)
|
||||||
|
@ -55,7 +62,7 @@ def check(db, uid, passwd):
|
||||||
res = cr.fetchone()[0]
|
res = cr.fetchone()[0]
|
||||||
cr.close()
|
cr.close()
|
||||||
if not bool(res):
|
if not bool(res):
|
||||||
raise Exception('AccessDenied')
|
raise ExceptionNoTb('AccessDenied')
|
||||||
if res:
|
if res:
|
||||||
if _uid_cache.has_key(db):
|
if _uid_cache.has_key(db):
|
||||||
ulist = _uid_cache[db]
|
ulist = _uid_cache[db]
|
||||||
|
@ -73,8 +80,9 @@ def access(db, uid, passwd, sec_level, ids):
|
||||||
res = cr.fetchone()
|
res = cr.fetchone()
|
||||||
cr.close()
|
cr.close()
|
||||||
if not res:
|
if not res:
|
||||||
raise Exception('Bad username or password')
|
raise ExceptionNoTb('Bad username or password')
|
||||||
return res[0]
|
return res[0]
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -40,29 +40,36 @@ import tools
|
||||||
import locale
|
import locale
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
|
|
||||||
class db(netsvc.Service):
|
class db(netsvc.ExportService):
|
||||||
def __init__(self, name="db"):
|
def __init__(self, name="db"):
|
||||||
netsvc.Service.__init__(self, name)
|
netsvc.ExportService.__init__(self, name)
|
||||||
self.joinGroup("web-services")
|
self.joinGroup("web-services")
|
||||||
self.exportMethod(self.create)
|
|
||||||
self.exportMethod(self.get_progress)
|
|
||||||
self.exportMethod(self.drop)
|
|
||||||
self.exportMethod(self.dump)
|
|
||||||
self.exportMethod(self.restore)
|
|
||||||
self.exportMethod(self.rename)
|
|
||||||
self.exportMethod(self.list)
|
|
||||||
self.exportMethod(self.list_lang)
|
|
||||||
self.exportMethod(self.change_admin_password)
|
|
||||||
self.exportMethod(self.server_version)
|
|
||||||
self.exportMethod(self.migrate_databases)
|
|
||||||
self.actions = {}
|
self.actions = {}
|
||||||
self.id = 0
|
self.id = 0
|
||||||
self.id_protect = threading.Semaphore()
|
self.id_protect = threading.Semaphore()
|
||||||
|
|
||||||
self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
|
self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
|
||||||
|
|
||||||
def create(self, password, db_name, demo, lang, user_password='admin'):
|
def dispatch(self, method, auth, params):
|
||||||
security.check_super(password)
|
if method in [ 'create', 'get_progress', 'drop', 'dump',
|
||||||
|
'restore', 'rename',
|
||||||
|
'change_admin_password', 'migrate_databases' ]:
|
||||||
|
passwd = params[0]
|
||||||
|
params = params[1:]
|
||||||
|
security.check_super(passwd)
|
||||||
|
elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
|
||||||
|
# params = params
|
||||||
|
# No security check for these methods
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise KeyError("Method not found: %s" % method)
|
||||||
|
fn = getattr(self, 'exp_'+method)
|
||||||
|
return fn(*params)
|
||||||
|
|
||||||
|
def new_dispatch(self,method,auth,params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exp_create(self, db_name, demo, lang, user_password='admin'):
|
||||||
self.id_protect.acquire()
|
self.id_protect.acquire()
|
||||||
self.id += 1
|
self.id += 1
|
||||||
id = self.id
|
id = self.id
|
||||||
|
@ -130,8 +137,7 @@ class db(netsvc.Service):
|
||||||
self.actions[id]['thread'] = create_thread
|
self.actions[id]['thread'] = create_thread
|
||||||
return id
|
return id
|
||||||
|
|
||||||
def get_progress(self, password, id):
|
def exp_get_progress(self, id):
|
||||||
security.check_super(password)
|
|
||||||
if self.actions[id]['thread'].isAlive():
|
if self.actions[id]['thread'].isAlive():
|
||||||
# return addons.init_progress[db_name]
|
# return addons.init_progress[db_name]
|
||||||
return (min(self.actions[id].get('progress', 0),0.95), [])
|
return (min(self.actions[id].get('progress', 0),0.95), [])
|
||||||
|
@ -146,8 +152,7 @@ class db(netsvc.Service):
|
||||||
del self.actions[id]
|
del self.actions[id]
|
||||||
raise Exception, e
|
raise Exception, e
|
||||||
|
|
||||||
def drop(self, password, db_name):
|
def exp_drop(self, db_name):
|
||||||
security.check_super(password)
|
|
||||||
sql_db.close_db(db_name)
|
sql_db.close_db(db_name)
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
|
@ -179,8 +184,7 @@ class db(netsvc.Service):
|
||||||
if os.name == 'nt' and self._pg_psw_env_var_is_set:
|
if os.name == 'nt' and self._pg_psw_env_var_is_set:
|
||||||
os.environ['PGPASSWORD'] = ''
|
os.environ['PGPASSWORD'] = ''
|
||||||
|
|
||||||
def dump(self, password, db_name):
|
def exp_dump(self, db_name):
|
||||||
security.check_super(password)
|
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
self._set_pg_psw_env_var()
|
self._set_pg_psw_env_var()
|
||||||
|
@ -209,8 +213,7 @@ class db(netsvc.Service):
|
||||||
|
|
||||||
return base64.encodestring(data)
|
return base64.encodestring(data)
|
||||||
|
|
||||||
def restore(self, password, db_name, data):
|
def exp_restore(self, db_name, data):
|
||||||
security.check_super(password)
|
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
self._set_pg_psw_env_var()
|
self._set_pg_psw_env_var()
|
||||||
|
@ -260,8 +263,7 @@ class db(netsvc.Service):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def rename(self, password, old_name, new_name):
|
def exp_rename(self, old_name, new_name):
|
||||||
security.check_super(password)
|
|
||||||
sql_db.close_db(old_name)
|
sql_db.close_db(old_name)
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
|
@ -287,14 +289,14 @@ class db(netsvc.Service):
|
||||||
sql_db.close_db('template1')
|
sql_db.close_db('template1')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def db_exist(self, db_name):
|
def exp_db_exist(self, db_name):
|
||||||
try:
|
try:
|
||||||
db = sql_db.db_connect(db_name)
|
db = sql_db.db_connect(db_name)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def list(self):
|
def exp_list(self):
|
||||||
db = sql_db.db_connect('template1')
|
db = sql_db.db_connect('template1')
|
||||||
cr = db.cursor()
|
cr = db.cursor()
|
||||||
try:
|
try:
|
||||||
|
@ -323,27 +325,25 @@ class db(netsvc.Service):
|
||||||
res.sort()
|
res.sort()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def change_admin_password(self, old_password, new_password):
|
def exp_change_admin_password(self, new_password):
|
||||||
security.check_super(old_password)
|
|
||||||
tools.config['admin_passwd'] = new_password
|
tools.config['admin_passwd'] = new_password
|
||||||
tools.config.save()
|
tools.config.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def list_lang(self):
|
def exp_list_lang(self):
|
||||||
return tools.scan_languages()
|
return tools.scan_languages()
|
||||||
|
|
||||||
def server_version(self):
|
def exp_server_version(self):
|
||||||
""" Return the version of the server
|
""" Return the version of the server
|
||||||
Used by the client to verify the compatibility with its own version
|
Used by the client to verify the compatibility with its own version
|
||||||
"""
|
"""
|
||||||
return release.version
|
return release.version
|
||||||
|
|
||||||
def migrate_databases(self, password, databases):
|
def exp_migrate_databases(self,databases):
|
||||||
|
|
||||||
from osv.orm import except_orm
|
from osv.orm import except_orm
|
||||||
from osv.osv import except_osv
|
from osv.osv import except_osv
|
||||||
|
|
||||||
security.check_super(password)
|
|
||||||
l = netsvc.Logger()
|
l = netsvc.Logger()
|
||||||
for db in databases:
|
for db in databases:
|
||||||
try:
|
try:
|
||||||
|
@ -362,63 +362,74 @@ class db(netsvc.Service):
|
||||||
return True
|
return True
|
||||||
db()
|
db()
|
||||||
|
|
||||||
class common(netsvc.Service):
|
class _ObjectService(netsvc.ExportService):
|
||||||
|
"A common base class for those who have fn(db, uid, password,...) "
|
||||||
|
|
||||||
|
def common_dispatch(self, method, auth, params):
|
||||||
|
(db, uid, passwd ) = params[0:3]
|
||||||
|
params = params[3:]
|
||||||
|
security.check(db,uid,passwd)
|
||||||
|
cr = pooler.get_db(db).cursor()
|
||||||
|
fn = getattr(self, 'exp_'+method)
|
||||||
|
res = fn(cr, uid, *params)
|
||||||
|
cr.commit()
|
||||||
|
cr.close()
|
||||||
|
return res
|
||||||
|
|
||||||
|
class common(_ObjectService):
|
||||||
def __init__(self,name="common"):
|
def __init__(self,name="common"):
|
||||||
netsvc.Service.__init__(self,name)
|
_ObjectService.__init__(self,name)
|
||||||
self.joinGroup("web-services")
|
self.joinGroup("web-services")
|
||||||
self.exportMethod(self.ir_get)
|
|
||||||
self.exportMethod(self.ir_set)
|
|
||||||
self.exportMethod(self.ir_del)
|
|
||||||
self.exportMethod(self.about)
|
|
||||||
self.exportMethod(self.login)
|
|
||||||
self.exportMethod(self.logout)
|
|
||||||
self.exportMethod(self.timezone_get)
|
|
||||||
self.exportMethod(self.get_available_updates)
|
|
||||||
self.exportMethod(self.get_migration_scripts)
|
|
||||||
self.exportMethod(self.get_server_environment)
|
|
||||||
self.exportMethod(self.login_message)
|
|
||||||
|
|
||||||
def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False):
|
def dispatch(self, method, auth, params):
|
||||||
security.check(db, uid, password)
|
logger = netsvc.Logger()
|
||||||
cr = pooler.get_db(db).cursor()
|
if method in [ 'ir_set','ir_del', 'ir_get' ]:
|
||||||
|
return self.common_dispatch(method,auth,params)
|
||||||
|
if method == 'login':
|
||||||
|
# At this old dispatcher, we do NOT update the auth proxy
|
||||||
|
res = security.login(params[0], params[1], params[2])
|
||||||
|
msg = res and 'successful login' or 'bad login or password'
|
||||||
|
# TODO log the client ip address..
|
||||||
|
logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
|
||||||
|
return res or False
|
||||||
|
elif method == 'logout':
|
||||||
|
if auth:
|
||||||
|
auth.logout(params[1])
|
||||||
|
logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
|
||||||
|
return True
|
||||||
|
elif method in ['about', 'timezone_get', 'get_server_environment', 'login_message', 'get_stats' ]:
|
||||||
|
pass
|
||||||
|
elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel']:
|
||||||
|
passwd = params[0]
|
||||||
|
params = params[1:]
|
||||||
|
security.check_super(passwd)
|
||||||
|
else:
|
||||||
|
raise Exception("Method not found: %s" % method)
|
||||||
|
|
||||||
|
fn = getattr(self, 'exp_'+method)
|
||||||
|
return fn(*params)
|
||||||
|
|
||||||
|
|
||||||
|
def new_dispatch(self,method,auth,params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exp_ir_set(self, cr, uid, keys, args, name, value, replace=True, isobject=False):
|
||||||
res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
|
res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def ir_del(self, db, uid, password, id):
|
def exp_ir_del(self, cr, uid, id):
|
||||||
security.check(db, uid, password)
|
|
||||||
cr = pooler.get_db(db).cursor()
|
|
||||||
res = ir.ir_del(cr,uid, id)
|
res = ir.ir_del(cr,uid, id)
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None):
|
def exp_ir_get(self, cr, uid, keys, args=None, meta=None, context=None):
|
||||||
if not args:
|
if not args:
|
||||||
args=[]
|
args=[]
|
||||||
if not context:
|
if not context:
|
||||||
context={}
|
context={}
|
||||||
security.check(db, uid, password)
|
|
||||||
cr = pooler.get_db(db).cursor()
|
|
||||||
res = ir.ir_get(cr,uid, keys, args, meta, context)
|
res = ir.ir_get(cr,uid, keys, args, meta, context)
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def login(self, db, login, password):
|
def exp_about(self, extended=False):
|
||||||
res = security.login(db, login, password)
|
|
||||||
logger = netsvc.Logger()
|
|
||||||
msg = res and 'successful login' or 'bad login or password'
|
|
||||||
logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower()))
|
|
||||||
return res or False
|
|
||||||
|
|
||||||
def logout(self, db, login, password):
|
|
||||||
logger = netsvc.Logger()
|
|
||||||
logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def about(self, extended=False):
|
|
||||||
"""Return information about the OpenERP Server.
|
"""Return information about the OpenERP Server.
|
||||||
|
|
||||||
@param extended: if True then return version info
|
@param extended: if True then return version info
|
||||||
|
@ -438,12 +449,11 @@ GNU Public Licence.
|
||||||
return info, release.version
|
return info, release.version
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def timezone_get(self, db, login, password):
|
def exp_timezone_get(self, db, login, password):
|
||||||
return time.tzname[0]
|
return time.tzname[0]
|
||||||
|
|
||||||
|
|
||||||
def get_available_updates(self, password, contract_id, contract_password):
|
def exp_get_available_updates(self, contract_id, contract_password):
|
||||||
security.check_super(password)
|
|
||||||
import tools.maintenance as tm
|
import tools.maintenance as tm
|
||||||
try:
|
try:
|
||||||
rc = tm.remote_contract(contract_id, contract_password)
|
rc = tm.remote_contract(contract_id, contract_password)
|
||||||
|
@ -456,8 +466,7 @@ GNU Public Licence.
|
||||||
self.abortResponse(1, 'Migration Error', 'warning', str(e))
|
self.abortResponse(1, 'Migration Error', 'warning', str(e))
|
||||||
|
|
||||||
|
|
||||||
def get_migration_scripts(self, password, contract_id, contract_password):
|
def exp_get_migration_scripts(self, contract_id, contract_password):
|
||||||
security.check_super(password)
|
|
||||||
l = netsvc.Logger()
|
l = netsvc.Logger()
|
||||||
import tools.maintenance as tm
|
import tools.maintenance as tm
|
||||||
try:
|
try:
|
||||||
|
@ -529,12 +538,7 @@ GNU Public Licence.
|
||||||
l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
|
l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_server_environment(self):
|
def exp_get_server_environment(self):
|
||||||
try:
|
|
||||||
rev_id = os.popen('bzr revision-info').read()
|
|
||||||
except Exception,e:
|
|
||||||
rev_id = 'Exception: %s\n' % (tools.ustr(e))
|
|
||||||
|
|
||||||
os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
|
os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
|
||||||
if not os_lang:
|
if not os_lang:
|
||||||
os_lang = 'NOT SET'
|
os_lang = 'NOT SET'
|
||||||
|
@ -553,43 +557,48 @@ GNU Public Licence.
|
||||||
'Operating System Architecture : %s\n' \
|
'Operating System Architecture : %s\n' \
|
||||||
'Operating System Locale : %s\n'\
|
'Operating System Locale : %s\n'\
|
||||||
'Python Version : %s\n'\
|
'Python Version : %s\n'\
|
||||||
'OpenERP-Server Version : %s\n'\
|
'OpenERP-Server Version : %s'\
|
||||||
'Last revision No. & ID : %s'\
|
|
||||||
%(platform.release(), platform.version(), platform.architecture()[0],
|
%(platform.release(), platform.version(), platform.architecture()[0],
|
||||||
os_lang, platform.python_version(),release.version,rev_id)
|
os_lang, platform.python_version(),release.version)
|
||||||
return environment
|
return environment
|
||||||
|
|
||||||
|
|
||||||
def login_message(self):
|
def exp_login_message(self):
|
||||||
return tools.config.get('login_message', False)
|
return tools.config.get('login_message', False)
|
||||||
|
|
||||||
|
def exp_set_loglevel(self,loglevel):
|
||||||
|
l = netsvc.Logger()
|
||||||
|
l.set_loglevel(int(loglevel))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def exp_get_stats(self):
|
||||||
|
import threading
|
||||||
|
res = "OpenERP server: %d threads\n" % threading.active_count()
|
||||||
|
res += netsvc.Server.allStats()
|
||||||
|
return res
|
||||||
|
|
||||||
common()
|
common()
|
||||||
|
|
||||||
class objects_proxy(netsvc.Service):
|
class objects_proxy(netsvc.ExportService):
|
||||||
def __init__(self, name="object"):
|
def __init__(self, name="object"):
|
||||||
netsvc.Service.__init__(self,name)
|
netsvc.ExportService.__init__(self,name)
|
||||||
self.joinGroup('web-services')
|
self.joinGroup('web-services')
|
||||||
self.exportMethod(self.execute)
|
|
||||||
self.exportMethod(self.exec_workflow)
|
|
||||||
self.exportMethod(self.obj_list)
|
|
||||||
|
|
||||||
def exec_workflow(self, db, uid, passwd, object, method, id):
|
def dispatch(self, method, auth, params):
|
||||||
|
(db, uid, passwd ) = params[0:3]
|
||||||
|
params = params[3:]
|
||||||
|
if method not in ['execute','exec_workflow','obj_list']:
|
||||||
|
raise KeyError("Method not supported %s" % method)
|
||||||
security.check(db,uid,passwd)
|
security.check(db,uid,passwd)
|
||||||
service = netsvc.LocalService("object_proxy")
|
ls = netsvc.LocalService('object_proxy')
|
||||||
res = service.exec_workflow(db, uid, object, method, id)
|
fn = getattr(ls, method)
|
||||||
|
res = fn(db, uid, *params)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def execute(self, db, uid, passwd, object, method, *args):
|
|
||||||
security.check(db, uid, passwd)
|
|
||||||
service = netsvc.LocalService("object_proxy")
|
|
||||||
res = service.execute(db, uid, object, method, *args)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def obj_list(self, db, uid, passwd):
|
def new_dispatch(self,method,auth,params):
|
||||||
security.check(db, uid, passwd)
|
pass
|
||||||
service = netsvc.LocalService("object_proxy")
|
|
||||||
res = service.obj_list()
|
|
||||||
return res
|
|
||||||
objects_proxy()
|
objects_proxy()
|
||||||
|
|
||||||
|
|
||||||
|
@ -604,26 +613,36 @@ objects_proxy()
|
||||||
# Wizard datas: {}
|
# Wizard datas: {}
|
||||||
# TODO: change local request to OSE request/reply pattern
|
# TODO: change local request to OSE request/reply pattern
|
||||||
#
|
#
|
||||||
class wizard(netsvc.Service):
|
class wizard(netsvc.ExportService):
|
||||||
def __init__(self, name='wizard'):
|
def __init__(self, name='wizard'):
|
||||||
netsvc.Service.__init__(self,name)
|
netsvc.ExportService.__init__(self,name)
|
||||||
self.joinGroup('web-services')
|
self.joinGroup('web-services')
|
||||||
self.exportMethod(self.execute)
|
|
||||||
self.exportMethod(self.create)
|
|
||||||
self.id = 0
|
self.id = 0
|
||||||
self.wiz_datas = {}
|
self.wiz_datas = {}
|
||||||
self.wiz_name = {}
|
self.wiz_name = {}
|
||||||
self.wiz_uid = {}
|
self.wiz_uid = {}
|
||||||
|
|
||||||
|
def dispatch(self, method, auth, params):
|
||||||
|
(db, uid, passwd ) = params[0:3]
|
||||||
|
params = params[3:]
|
||||||
|
if method not in ['execute','create']:
|
||||||
|
raise KeyError("Method not supported %s" % method)
|
||||||
|
security.check(db,uid,passwd)
|
||||||
|
fn = getattr(self, 'exp_'+method)
|
||||||
|
res = fn(db, uid, *params)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def new_dispatch(self,method,auth,params):
|
||||||
|
pass
|
||||||
|
|
||||||
def _execute(self, db, uid, wiz_id, datas, action, context):
|
def _execute(self, db, uid, wiz_id, datas, action, context):
|
||||||
self.wiz_datas[wiz_id].update(datas)
|
self.wiz_datas[wiz_id].update(datas)
|
||||||
wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
|
wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
|
||||||
return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
|
return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
|
||||||
|
|
||||||
def create(self, db, uid, passwd, wiz_name, datas=None):
|
def exp_create(self, db, uid, wiz_name, datas=None):
|
||||||
if not datas:
|
if not datas:
|
||||||
datas={}
|
datas={}
|
||||||
security.check(db, uid, passwd)
|
|
||||||
#FIXME: this is not thread-safe
|
#FIXME: this is not thread-safe
|
||||||
self.id += 1
|
self.id += 1
|
||||||
self.wiz_datas[self.id] = {}
|
self.wiz_datas[self.id] = {}
|
||||||
|
@ -631,10 +650,9 @@ class wizard(netsvc.Service):
|
||||||
self.wiz_uid[self.id] = uid
|
self.wiz_uid[self.id] = uid
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None):
|
def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
|
||||||
if not context:
|
if not context:
|
||||||
context={}
|
context={}
|
||||||
security.check(db, uid, passwd)
|
|
||||||
|
|
||||||
if wiz_id in self.wiz_uid:
|
if wiz_id in self.wiz_uid:
|
||||||
if self.wiz_uid[wiz_id] == uid:
|
if self.wiz_uid[wiz_id] == uid:
|
||||||
|
@ -658,22 +676,33 @@ class ExceptionWithTraceback(Exception):
|
||||||
self.traceback = tb
|
self.traceback = tb
|
||||||
self.args = (msg, tb)
|
self.args = (msg, tb)
|
||||||
|
|
||||||
class report_spool(netsvc.Service):
|
class report_spool(netsvc.ExportService):
|
||||||
def __init__(self, name='report'):
|
def __init__(self, name='report'):
|
||||||
netsvc.Service.__init__(self, name)
|
netsvc.ExportService.__init__(self, name)
|
||||||
self.joinGroup('web-services')
|
self.joinGroup('web-services')
|
||||||
self.exportMethod(self.report)
|
|
||||||
self.exportMethod(self.report_get)
|
|
||||||
self._reports = {}
|
self._reports = {}
|
||||||
self.id = 0
|
self.id = 0
|
||||||
self.id_protect = threading.Semaphore()
|
self.id_protect = threading.Semaphore()
|
||||||
|
|
||||||
def report(self, db, uid, passwd, object, ids, datas=None, context=None):
|
def dispatch(self, method, auth, params):
|
||||||
|
(db, uid, passwd ) = params[0:3]
|
||||||
|
params = params[3:]
|
||||||
|
if method not in ['report','report_get']:
|
||||||
|
raise KeyError("Method not supported %s" % method)
|
||||||
|
security.check(db,uid,passwd)
|
||||||
|
fn = getattr(self, 'exp_' + method)
|
||||||
|
res = fn(db, uid, *params)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def new_dispatch(self,method,auth,params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exp_report(self, db, uid, object, ids, datas=None, context=None):
|
||||||
if not datas:
|
if not datas:
|
||||||
datas={}
|
datas={}
|
||||||
if not context:
|
if not context:
|
||||||
context={}
|
context={}
|
||||||
security.check(db, uid, passwd)
|
|
||||||
|
|
||||||
self.id_protect.acquire()
|
self.id_protect.acquire()
|
||||||
self.id += 1
|
self.id += 1
|
||||||
|
@ -729,8 +758,7 @@ class report_spool(netsvc.Service):
|
||||||
del self._reports[report_id]
|
del self._reports[report_id]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def report_get(self, db, uid, passwd, report_id):
|
def exp_report_get(self, db, uid, report_id):
|
||||||
security.check(db, uid, passwd)
|
|
||||||
|
|
||||||
if report_id in self._reports:
|
if report_id in self._reports:
|
||||||
if self._reports[report_id]['uid'] == uid:
|
if self._reports[report_id]['uid'] == uid:
|
||||||
|
|
|
@ -0,0 +1,423 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright P. Christeas <p_christ@hol.gr> 2008,2009
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# WARNING: This program as such is intended to be used by professional
|
||||||
|
# programmers who take the whole responsability of assessing all potential
|
||||||
|
# consequences resulting from its eventual inadequacies and bugs
|
||||||
|
# End users who are looking for a ready-to-use solution with commercial
|
||||||
|
# garantees and support are strongly adviced to contract a Free Software
|
||||||
|
# Service Company
|
||||||
|
#
|
||||||
|
# This program is Free Software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
""" Framework for generic http servers
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import base64
|
||||||
|
import SocketServer
|
||||||
|
from BaseHTTPServer import *
|
||||||
|
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
||||||
|
|
||||||
|
class AuthRequiredExc(Exception):
|
||||||
|
def __init__(self,atype,realm):
|
||||||
|
Exception.__init__(self)
|
||||||
|
self.atype = atype
|
||||||
|
self.realm = realm
|
||||||
|
|
||||||
|
class AuthRejectedExc(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthProvider:
|
||||||
|
def __init__(self,realm):
|
||||||
|
self.realm = realm
|
||||||
|
|
||||||
|
def setupAuth(self, multi,handler):
|
||||||
|
""" Attach an AuthProxy object to handler
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def authenticate(self, user, passwd, client_address):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
print msg
|
||||||
|
|
||||||
|
class BasicAuthProvider(AuthProvider):
|
||||||
|
def setupAuth(self, multi, handler):
|
||||||
|
if not multi.sec_realms.has_key(self.realm):
|
||||||
|
multi.sec_realms[self.realm] = BasicAuthProxy(self)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthProxy:
|
||||||
|
""" This class will hold authentication information for a handler,
|
||||||
|
i.e. a connection
|
||||||
|
"""
|
||||||
|
def __init__(self, provider):
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
def checkRequest(self,handler,path = '/'):
|
||||||
|
""" Check if we are allowed to process that request
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BasicAuthProxy(AuthProxy):
|
||||||
|
""" Require basic authentication..
|
||||||
|
"""
|
||||||
|
def __init__(self,provider):
|
||||||
|
AuthProxy.__init__(self,provider)
|
||||||
|
self.auth_creds = None
|
||||||
|
self.auth_tries = 0
|
||||||
|
|
||||||
|
def checkRequest(self,handler,path = '/'):
|
||||||
|
if self.auth_creds:
|
||||||
|
return True
|
||||||
|
auth_str = handler.headers.get('Authorization',False)
|
||||||
|
if auth_str and auth_str.startswith('Basic '):
|
||||||
|
auth_str=auth_str[len('Basic '):]
|
||||||
|
(user,passwd) = base64.decodestring(auth_str).split(':')
|
||||||
|
self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd))
|
||||||
|
self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address)
|
||||||
|
if self.auth_creds:
|
||||||
|
return True
|
||||||
|
if self.auth_tries > 5:
|
||||||
|
self.provider.log("Failing authorization after 5 requests w/o password")
|
||||||
|
raise AuthRejectedExc("Authorization failed.")
|
||||||
|
self.auth_tries += 1
|
||||||
|
raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPHandler(SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self,request, client_address, server):
|
||||||
|
SimpleHTTPRequestHandler.__init__(self,request,client_address,server)
|
||||||
|
# print "Handler for %s inited" % str(client_address)
|
||||||
|
self.protocol_version = 'HTTP/1.1'
|
||||||
|
self.connection = dummyconn()
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
""" Classes here should NOT handle inside their constructor
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class HTTPDir:
|
||||||
|
""" A dispatcher class, like a virtual folder in httpd
|
||||||
|
"""
|
||||||
|
def __init__(self,path,handler, auth_provider = None):
|
||||||
|
self.path = path
|
||||||
|
self.handler = handler
|
||||||
|
self.auth_provider = auth_provider
|
||||||
|
|
||||||
|
def matches(self, request):
|
||||||
|
""" Test if some request matches us. If so, return
|
||||||
|
the matched path. """
|
||||||
|
if request.startswith(self.path):
|
||||||
|
return self.path
|
||||||
|
return False
|
||||||
|
|
||||||
|
class noconnection:
|
||||||
|
""" a class to use instead of the real connection
|
||||||
|
"""
|
||||||
|
def makefile(self, mode, bufsize):
|
||||||
|
return None
|
||||||
|
|
||||||
|
class dummyconn:
|
||||||
|
def shutdown(self, tru):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _quote_html(html):
|
||||||
|
return html.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
class FixSendError:
|
||||||
|
#error_message_format = """ """
|
||||||
|
def send_error(self, code, message=None):
|
||||||
|
#overriden from BaseHTTPRequestHandler, we also send the content-length
|
||||||
|
try:
|
||||||
|
short, long = self.responses[code]
|
||||||
|
except KeyError:
|
||||||
|
short, long = '???', '???'
|
||||||
|
if message is None:
|
||||||
|
message = short
|
||||||
|
explain = long
|
||||||
|
self.log_error("code %d, message %s", code, message)
|
||||||
|
# using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
|
||||||
|
content = (self.error_message_format %
|
||||||
|
{'code': code, 'message': _quote_html(message), 'explain': explain})
|
||||||
|
self.send_response(code, message)
|
||||||
|
self.send_header("Content-Type", self.error_content_type)
|
||||||
|
self.send_header('Connection', 'close')
|
||||||
|
self.send_header('Content-Length', len(content) or 0)
|
||||||
|
self.end_headers()
|
||||||
|
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
|
||||||
|
""" this is a multiple handler, that will dispatch each request
|
||||||
|
to a nested handler, iff it matches
|
||||||
|
|
||||||
|
The handler will also have *one* dict of authentication proxies,
|
||||||
|
groupped by their realm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
protocol_version = "HTTP/1.1"
|
||||||
|
default_request_version = "HTTP/0.9" # compatibility with py2.5
|
||||||
|
|
||||||
|
auth_required_msg = """ <html><head><title>Authorization required</title></head>
|
||||||
|
<body>You must authenticate to use this service</body><html>\r\r"""
|
||||||
|
|
||||||
|
def __init__(self, request, client_address, server):
|
||||||
|
self.in_handlers = {}
|
||||||
|
self.sec_realms = {}
|
||||||
|
SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
|
||||||
|
self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
|
||||||
|
|
||||||
|
def _handle_one_foreign(self,fore, path, auth_provider):
|
||||||
|
""" This method overrides the handle_one_request for *children*
|
||||||
|
handlers. It is required, since the first line should not be
|
||||||
|
read again..
|
||||||
|
|
||||||
|
"""
|
||||||
|
fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
|
||||||
|
if not fore.parse_request(): # An error code has been sent, just exit
|
||||||
|
return
|
||||||
|
self.request_version = fore.request_version
|
||||||
|
if auth_provider and auth_provider.realm:
|
||||||
|
try:
|
||||||
|
self.sec_realms[auth_provider.realm].checkRequest(fore,path)
|
||||||
|
except AuthRequiredExc,ae:
|
||||||
|
if self.request_version != 'HTTP/1.1':
|
||||||
|
self.log_error("Cannot require auth at %s",self.request_version)
|
||||||
|
self.send_error(401)
|
||||||
|
return
|
||||||
|
self._get_ignore_body(fore) # consume any body that came, not loose sync with input
|
||||||
|
self.send_response(401,'Authorization required')
|
||||||
|
self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
|
||||||
|
self.send_header('Connection', 'keep-alive')
|
||||||
|
self.send_header('Content-Type','text/html')
|
||||||
|
self.send_header('Content-Length',len(self.auth_required_msg))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(self.auth_required_msg)
|
||||||
|
return
|
||||||
|
except AuthRejectedExc,e:
|
||||||
|
self.log_error("Rejected auth: %s" % e.args[0])
|
||||||
|
self.send_error(401,e.args[0])
|
||||||
|
self.close_connection = 1
|
||||||
|
return
|
||||||
|
mname = 'do_' + fore.command
|
||||||
|
if not hasattr(fore, mname):
|
||||||
|
fore.send_error(501, "Unsupported method (%r)" % fore.command)
|
||||||
|
return
|
||||||
|
fore.close_connection = 0
|
||||||
|
method = getattr(fore, mname)
|
||||||
|
method()
|
||||||
|
if fore.close_connection:
|
||||||
|
# print "Closing connection because of handler"
|
||||||
|
self.close_connection = fore.close_connection
|
||||||
|
|
||||||
|
def parse_rawline(self):
|
||||||
|
"""Parse a request (internal).
|
||||||
|
|
||||||
|
The request should be stored in self.raw_requestline; the results
|
||||||
|
are in self.command, self.path, self.request_version and
|
||||||
|
self.headers.
|
||||||
|
|
||||||
|
Return True for success, False for failure; on failure, an
|
||||||
|
error is sent back.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.command = None # set in case of error on the first line
|
||||||
|
self.request_version = version = self.default_request_version
|
||||||
|
self.close_connection = 1
|
||||||
|
requestline = self.raw_requestline
|
||||||
|
if requestline[-2:] == '\r\n':
|
||||||
|
requestline = requestline[:-2]
|
||||||
|
elif requestline[-1:] == '\n':
|
||||||
|
requestline = requestline[:-1]
|
||||||
|
self.requestline = requestline
|
||||||
|
words = requestline.split()
|
||||||
|
if len(words) == 3:
|
||||||
|
[command, path, version] = words
|
||||||
|
if version[:5] != 'HTTP/':
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
base_version_number = version.split('/', 1)[1]
|
||||||
|
version_number = base_version_number.split(".")
|
||||||
|
# RFC 2145 section 3.1 says there can be only one "." and
|
||||||
|
# - major and minor numbers MUST be treated as
|
||||||
|
# separate integers;
|
||||||
|
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
|
||||||
|
# turn is lower than HTTP/12.3;
|
||||||
|
# - Leading zeros MUST be ignored by recipients.
|
||||||
|
if len(version_number) != 2:
|
||||||
|
raise ValueError
|
||||||
|
version_number = int(version_number[0]), int(version_number[1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
if version_number >= (1, 1):
|
||||||
|
self.close_connection = 0
|
||||||
|
if version_number >= (2, 0):
|
||||||
|
self.send_error(505,
|
||||||
|
"Invalid HTTP Version (%s)" % base_version_number)
|
||||||
|
return False
|
||||||
|
elif len(words) == 2:
|
||||||
|
[command, path] = words
|
||||||
|
self.close_connection = 1
|
||||||
|
if command != 'GET':
|
||||||
|
self.send_error(400,
|
||||||
|
"Bad HTTP/0.9 request type (%r)" % command)
|
||||||
|
return False
|
||||||
|
elif not words:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.send_error(400, "Bad request syntax (%r)" % requestline)
|
||||||
|
return False
|
||||||
|
self.request_version = version
|
||||||
|
self.command, self.path, self.version = command, path, version
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_one_request(self):
|
||||||
|
"""Handle a single HTTP request.
|
||||||
|
Dispatch to the correct handler.
|
||||||
|
"""
|
||||||
|
self.request.setblocking(True)
|
||||||
|
self.raw_requestline = self.rfile.readline()
|
||||||
|
if not self.raw_requestline:
|
||||||
|
self.close_connection = 1
|
||||||
|
# self.log_message("no requestline, connection closed?")
|
||||||
|
return
|
||||||
|
if not self.parse_rawline():
|
||||||
|
self.log_message("Could not parse rawline.")
|
||||||
|
return
|
||||||
|
# self.parse_request(): # Do NOT parse here. the first line should be the only
|
||||||
|
for vdir in self.server.vdirs:
|
||||||
|
p = vdir.matches(self.path)
|
||||||
|
if p == False:
|
||||||
|
continue
|
||||||
|
npath = self.path[len(p):]
|
||||||
|
if not npath.startswith('/'):
|
||||||
|
npath = '/' + npath
|
||||||
|
|
||||||
|
if not self.in_handlers.has_key(p):
|
||||||
|
self.in_handlers[p] = vdir.handler(noconnection(),self.client_address,self.server)
|
||||||
|
if vdir.auth_provider:
|
||||||
|
vdir.auth_provider.setupAuth(self, self.in_handlers[p])
|
||||||
|
hnd = self.in_handlers[p]
|
||||||
|
hnd.rfile = self.rfile
|
||||||
|
hnd.wfile = self.wfile
|
||||||
|
self.rlpath = self.raw_requestline
|
||||||
|
self._handle_one_foreign(hnd,npath, vdir.auth_provider)
|
||||||
|
# print "Handled, closing = ", self.close_connection
|
||||||
|
return
|
||||||
|
# if no match:
|
||||||
|
self.send_error(404, "Path not found: %s" % self.path)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_ignore_body(self,fore):
|
||||||
|
if not fore.headers.has_key("content-length"):
|
||||||
|
return
|
||||||
|
max_chunk_size = 10*1024*1024
|
||||||
|
size_remaining = int(fore.headers["content-length"])
|
||||||
|
got = ''
|
||||||
|
while size_remaining:
|
||||||
|
chunk_size = min(size_remaining, max_chunk_size)
|
||||||
|
got = fore.rfile.read(chunk_size)
|
||||||
|
size_remaining -= len(got)
|
||||||
|
|
||||||
|
|
||||||
|
class SecureMultiHTTPHandler(MultiHTTPHandler):
|
||||||
|
def getcert_fnames(self):
|
||||||
|
""" Return a pair with the filenames of ssl cert,key
|
||||||
|
|
||||||
|
Override this to direct to other filenames
|
||||||
|
"""
|
||||||
|
return ('server.cert','server.key')
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
import ssl
|
||||||
|
certfile, keyfile = self.getcert_fnames()
|
||||||
|
try:
|
||||||
|
self.connection = ssl.wrap_socket(self.request,
|
||||||
|
server_side=True,
|
||||||
|
certfile=certfile,
|
||||||
|
keyfile=keyfile,
|
||||||
|
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||||
|
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||||
|
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
||||||
|
self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
|
||||||
|
except:
|
||||||
|
self.request.shutdown(socket.SHUT_RDWR)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
# With ssl connections, closing the filehandlers alone may not
|
||||||
|
# work because of ref counting. We explicitly tell the socket
|
||||||
|
# to shutdown.
|
||||||
|
MultiHTTPHandler.finish(self)
|
||||||
|
try:
|
||||||
|
self.connection.shutdown(socket.SHUT_RDWR)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import threading
|
||||||
|
class ConnThreadingMixIn:
|
||||||
|
"""Mix-in class to handle each _connection_ in a new thread.
|
||||||
|
|
||||||
|
This is necessary for persistent connections, where multiple
|
||||||
|
requests should be handled synchronously at each connection, but
|
||||||
|
multiple connections can run in parallel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Decides how threads will act upon termination of the
|
||||||
|
# main process
|
||||||
|
daemon_threads = False
|
||||||
|
|
||||||
|
def _handle_request_noblock(self):
|
||||||
|
"""Start a new thread to process the request."""
|
||||||
|
t = threading.Thread(target = self._handle_request2)
|
||||||
|
if self.daemon_threads:
|
||||||
|
t.setDaemon (1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _handle_request2(self):
|
||||||
|
"""Handle one request, without blocking.
|
||||||
|
|
||||||
|
I assume that select.select has returned that the socket is
|
||||||
|
readable before this function was called, so there should be
|
||||||
|
no risk of blocking in get_request().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
request, client_address = self.get_request()
|
||||||
|
except socket.error:
|
||||||
|
return
|
||||||
|
if self.verify_request(request, client_address):
|
||||||
|
try:
|
||||||
|
self.process_request(request, client_address)
|
||||||
|
except:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
#eof
|
|
@ -54,10 +54,12 @@ from mx import DateTime as mdt
|
||||||
re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$');
|
re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$');
|
||||||
re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$');
|
re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$');
|
||||||
|
|
||||||
def log(msg, lvl=netsvc.LOG_DEBUG):
|
def log(msg, lvl=netsvc.LOG_DEBUG2):
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
logger.notifyChannel('sql', lvl, msg)
|
logger.notifyChannel('sql', lvl, msg)
|
||||||
|
|
||||||
|
sql_counter = 0
|
||||||
|
|
||||||
class Cursor(object):
|
class Cursor(object):
|
||||||
IN_MAX = 1000
|
IN_MAX = 1000
|
||||||
sql_from_log = {}
|
sql_from_log = {}
|
||||||
|
@ -71,7 +73,7 @@ class Cursor(object):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
if self.__closed:
|
if self.__closed:
|
||||||
raise psycopg2.ProgrammingError('Unable to use the cursor after having closing it')
|
raise psycopg2.ProgrammingError('Unable to use the cursor after having closed it')
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
@ -107,7 +109,7 @@ class Cursor(object):
|
||||||
self.count+=1
|
self.count+=1
|
||||||
if '%d' in query or '%f' in query:
|
if '%d' in query or '%f' in query:
|
||||||
log(query, netsvc.LOG_WARNING)
|
log(query, netsvc.LOG_WARNING)
|
||||||
log("SQL queries mustn't contain %d or %f anymore. Use only %s", netsvc.LOG_WARNING)
|
log("SQL queries cannot contain %d or %f anymore. Use only %s", netsvc.LOG_WARNING)
|
||||||
if params:
|
if params:
|
||||||
query = query.replace('%d', '%s').replace('%f', '%s')
|
query = query.replace('%d', '%s').replace('%f', '%s')
|
||||||
|
|
||||||
|
@ -117,6 +119,10 @@ class Cursor(object):
|
||||||
try:
|
try:
|
||||||
params = params or None
|
params = params or None
|
||||||
res = self._obj.execute(query, params)
|
res = self._obj.execute(query, params)
|
||||||
|
except psycopg2.ProgrammingError, pe:
|
||||||
|
logger= netsvc.Logger()
|
||||||
|
logger.notifyChannel('sql_db', netsvc.LOG_ERROR, "Programming error: %s, in query %s" % (pe, query))
|
||||||
|
raise
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log("bad query: %s" % self._obj.query)
|
log("bad query: %s" % self._obj.query)
|
||||||
log(e)
|
log(e)
|
||||||
|
@ -138,19 +144,20 @@ class Cursor(object):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def print_log(self):
|
def print_log(self):
|
||||||
|
global sql_counter
|
||||||
|
sql_counter += self.count
|
||||||
def process(type):
|
def process(type):
|
||||||
sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log}
|
sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log}
|
||||||
if not sqllogs[type]:
|
sum = 0
|
||||||
return
|
if sqllogs[type]:
|
||||||
sqllogitems = sqllogs[type].items()
|
sqllogitems = sqllogs[type].items()
|
||||||
sqllogitems.sort(key=lambda k: k[1][1])
|
sqllogitems.sort(key=lambda k: k[1][1])
|
||||||
sum = 0
|
|
||||||
log("SQL LOG %s:" % (type,))
|
log("SQL LOG %s:" % (type,))
|
||||||
for r in sqllogitems:
|
for r in sqllogitems:
|
||||||
log("table: %s: %s/%s" %(r[0], str(r[1][1]), r[1][0]))
|
log("table: %s: %s/%s" %(r[0], str(r[1][1]), r[1][0]))
|
||||||
sum+= r[1][1]
|
sum+= r[1][1]
|
||||||
log("SUM:%s/%d" % (sum, self.count))
|
|
||||||
sqllogs[type].clear()
|
sqllogs[type].clear()
|
||||||
|
log("SUM %s:%s/%d [%d]" % (type, sum, self.count,sql_counter))
|
||||||
process('from')
|
process('from')
|
||||||
process('into')
|
process('into')
|
||||||
self.count = 0
|
self.count = 0
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
# X.509 Certificate options
|
||||||
|
#
|
||||||
|
# DN options
|
||||||
|
|
||||||
|
# The organization of the subject.
|
||||||
|
organization = "Acme inc."
|
||||||
|
|
||||||
|
# The organizational unit of the subject.
|
||||||
|
unit = "dept."
|
||||||
|
|
||||||
|
# The locality of the subject.
|
||||||
|
# locality =
|
||||||
|
|
||||||
|
# The state of the certificate owner.
|
||||||
|
state = "Attiki"
|
||||||
|
|
||||||
|
# The country of the subject. Two letter code.
|
||||||
|
country = GR
|
||||||
|
|
||||||
|
# The common name of the certificate owner.
|
||||||
|
cn = "Some company"
|
||||||
|
|
||||||
|
# A user id of the certificate owner.
|
||||||
|
#uid = "clauper"
|
||||||
|
|
||||||
|
# If the supported DN OIDs are not adequate you can set
|
||||||
|
# any OID here.
|
||||||
|
# For example set the X.520 Title and the X.520 Pseudonym
|
||||||
|
# by using OID and string pairs.
|
||||||
|
#dn_oid = "2.5.4.12" "Dr." "2.5.4.65" "jackal"
|
||||||
|
|
||||||
|
# This is deprecated and should not be used in new
|
||||||
|
# certificates.
|
||||||
|
# pkcs9_email = "none@none.org"
|
||||||
|
|
||||||
|
# The serial number of the certificate
|
||||||
|
serial = 001
|
||||||
|
|
||||||
|
# In how many days, counting from today, this certificate will expire.
|
||||||
|
expiration_days = 700
|
||||||
|
|
||||||
|
# X.509 v3 extensions
|
||||||
|
|
||||||
|
# A dnsname in case of a WWW server.
|
||||||
|
#dns_name = "www.none.org"
|
||||||
|
#dns_name = "www.morethanone.org"
|
||||||
|
|
||||||
|
# An IP address in case of a server.
|
||||||
|
#ip_address = "192.168.1.1"
|
||||||
|
|
||||||
|
# An email in case of a person
|
||||||
|
email = "none@none.org"
|
||||||
|
|
||||||
|
# An URL that has CRLs (certificate revocation lists)
|
||||||
|
# available. Needed in CA certificates.
|
||||||
|
#crl_dist_points = "http://www.getcrl.crl/getcrl/"
|
||||||
|
|
||||||
|
# Whether this is a CA certificate or not
|
||||||
|
#ca
|
||||||
|
|
||||||
|
# Whether this certificate will be used for a TLS client
|
||||||
|
#tls_www_client
|
||||||
|
|
||||||
|
# Whether this certificate will be used for a TLS server
|
||||||
|
tls_www_server
|
||||||
|
|
||||||
|
# Whether this certificate will be used to sign data (needed
|
||||||
|
# in TLS DHE ciphersuites).
|
||||||
|
#signing_key
|
||||||
|
|
||||||
|
# Whether this certificate will be used to encrypt data (needed
|
||||||
|
# in TLS RSA ciphersuites). Note that it is prefered to use different
|
||||||
|
# keys for encryption and signing.
|
||||||
|
encryption_key
|
||||||
|
|
||||||
|
# Whether this key will be used to sign other certificates.
|
||||||
|
#cert_signing_key
|
||||||
|
|
||||||
|
# Whether this key will be used to sign CRLs.
|
||||||
|
#crl_signing_key
|
||||||
|
|
||||||
|
# Whether this key will be used to sign code.
|
||||||
|
#code_signing_key
|
||||||
|
|
||||||
|
# Whether this key will be used to sign OCSP data.
|
||||||
|
#ocsp_signing_key
|
||||||
|
|
||||||
|
# Whether this key will be used for time stamping.
|
||||||
|
#time_stamping_key
|
|
@ -33,11 +33,13 @@ class Myexception(Exception):
|
||||||
class mysocket:
|
class mysocket:
|
||||||
def __init__(self, sock=None):
|
def __init__(self, sock=None):
|
||||||
if sock is None:
|
if sock is None:
|
||||||
self.sock = socket.socket(
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
else:
|
else:
|
||||||
self.sock = sock
|
self.sock = sock
|
||||||
self.sock.settimeout(120)
|
# self.sock.settimeout(120)
|
||||||
|
# prepare this socket for long operations: it may block for infinite
|
||||||
|
# time, but should exit as soon as the net is down
|
||||||
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||||
def connect(self, host, port=False):
|
def connect(self, host, port=False):
|
||||||
if not port:
|
if not port:
|
||||||
protocol, buf = host.split('//')
|
protocol, buf = host.split('//')
|
||||||
|
@ -61,8 +63,8 @@ class mysocket:
|
||||||
buf=''
|
buf=''
|
||||||
while len(buf) < 8:
|
while len(buf) < 8:
|
||||||
chunk = self.sock.recv(8 - len(buf))
|
chunk = self.sock.recv(8 - len(buf))
|
||||||
if chunk == '':
|
if not chunk:
|
||||||
raise RuntimeError, "socket connection broken"
|
raise socket.timeout
|
||||||
buf += chunk
|
buf += chunk
|
||||||
size = int(buf)
|
size = int(buf)
|
||||||
buf = self.sock.recv(1)
|
buf = self.sock.recv(1)
|
||||||
|
@ -73,8 +75,8 @@ class mysocket:
|
||||||
msg = ''
|
msg = ''
|
||||||
while len(msg) < size:
|
while len(msg) < size:
|
||||||
chunk = self.sock.recv(size-len(msg))
|
chunk = self.sock.recv(size-len(msg))
|
||||||
if chunk == '':
|
if not chunk:
|
||||||
raise RuntimeError, "socket connection broken"
|
raise socket.timeout
|
||||||
msg = msg + chunk
|
msg = msg + chunk
|
||||||
msgio = cStringIO.StringIO(msg)
|
msgio = cStringIO.StringIO(msg)
|
||||||
unpickler = cPickle.Unpickler(msgio)
|
unpickler = cPickle.Unpickler(msgio)
|
||||||
|
|
|
@ -66,6 +66,7 @@ class configmanager(object):
|
||||||
'import_partial': "",
|
'import_partial': "",
|
||||||
'pidfile': None,
|
'pidfile': None,
|
||||||
'logfile': None,
|
'logfile': None,
|
||||||
|
'logrotate': '1',
|
||||||
'smtp_server': 'localhost',
|
'smtp_server': 'localhost',
|
||||||
'smtp_user': False,
|
'smtp_user': False,
|
||||||
'smtp_port':25,
|
'smtp_port':25,
|
||||||
|
@ -82,6 +83,8 @@ class configmanager(object):
|
||||||
'list_db' : True,
|
'list_db' : True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.misc = {}
|
||||||
|
|
||||||
hasSSL = check_ssl()
|
hasSSL = check_ssl()
|
||||||
|
|
||||||
self._LOGLEVELS = dict([(getattr(netsvc, 'LOG_%s' % x), getattr(logging, x))
|
self._LOGLEVELS = dict([(getattr(netsvc, 'LOG_%s' % x), getattr(logging, x))
|
||||||
|
@ -131,6 +134,8 @@ class configmanager(object):
|
||||||
# Logging Group
|
# Logging Group
|
||||||
group = optparse.OptionGroup(parser, "Logging Configuration")
|
group = optparse.OptionGroup(parser, "Logging Configuration")
|
||||||
group.add_option("--logfile", dest="logfile", help="file where the server log will be stored")
|
group.add_option("--logfile", dest="logfile", help="file where the server log will be stored")
|
||||||
|
group.add_option("--no-logrotate", dest="logrotate", action="store_false",
|
||||||
|
default=None, help="do not rotate the logfile")
|
||||||
group.add_option("--syslog", action="store_true", dest="syslog",
|
group.add_option("--syslog", action="store_true", dest="syslog",
|
||||||
default=False, help="Send the log to the syslog server")
|
default=False, help="Send the log to the syslog server")
|
||||||
group.add_option('--log-level', dest='log_level', type='choice', choices=self._LOGLEVELS.keys(),
|
group.add_option('--log-level', dest='log_level', type='choice', choices=self._LOGLEVELS.keys(),
|
||||||
|
@ -220,20 +225,19 @@ class configmanager(object):
|
||||||
keys = ['interface', 'port', 'db_name', 'db_user', 'db_password', 'db_host',
|
keys = ['interface', 'port', 'db_name', 'db_user', 'db_password', 'db_host',
|
||||||
'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout',
|
'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout',
|
||||||
'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'price_accuracy',
|
'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'price_accuracy',
|
||||||
'netinterface', 'netport', 'db_maxconn', 'import_partial', 'addons_path']
|
'netinterface', 'netport', 'db_maxconn', 'import_partial', 'addons_path',
|
||||||
|
'netrpc', 'xmlrpc', 'syslog', 'without_demo']
|
||||||
|
|
||||||
if hasSSL:
|
if hasSSL:
|
||||||
keys.extend(['smtp_ssl', 'secure_cert_file', 'secure_pkey_file'])
|
keys.extend(['smtp_ssl', 'secure_cert_file', 'secure_pkey_file'])
|
||||||
|
keys.append('secure')
|
||||||
|
|
||||||
for arg in keys:
|
for arg in keys:
|
||||||
if getattr(opt, arg):
|
if getattr(opt, arg):
|
||||||
self.options[arg] = getattr(opt, arg)
|
self.options[arg] = getattr(opt, arg)
|
||||||
|
|
||||||
keys = ['language', 'translate_out', 'translate_in', 'debug_mode',
|
keys = ['language', 'translate_out', 'translate_in', 'debug_mode',
|
||||||
'stop_after_init', 'without_demo', 'netrpc', 'xmlrpc', 'syslog']
|
'stop_after_init', 'logrotate']
|
||||||
|
|
||||||
if hasSSL and not self.options['secure']:
|
|
||||||
keys.append('secure')
|
|
||||||
|
|
||||||
for arg in keys:
|
for arg in keys:
|
||||||
if getattr(opt, arg) is not None:
|
if getattr(opt, arg) is not None:
|
||||||
|
@ -337,6 +341,18 @@ class configmanager(object):
|
||||||
if value=='False' or value=='false':
|
if value=='False' or value=='false':
|
||||||
value = False
|
value = False
|
||||||
self.options[name] = value
|
self.options[name] = value
|
||||||
|
#parse the other sections, as well
|
||||||
|
for sec in p.sections():
|
||||||
|
if sec == 'options':
|
||||||
|
continue
|
||||||
|
if not self.misc.has_key(sec):
|
||||||
|
self.misc[sec]= {}
|
||||||
|
for (name, value) in p.items(sec):
|
||||||
|
if value=='True' or value=='true':
|
||||||
|
value = True
|
||||||
|
if value=='False' or value=='false':
|
||||||
|
value = False
|
||||||
|
self.misc[sec][name] = value
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
except ConfigParser.NoSectionError:
|
except ConfigParser.NoSectionError:
|
||||||
|
@ -354,6 +370,10 @@ class configmanager(object):
|
||||||
else:
|
else:
|
||||||
p.set('options', opt, self.options[opt])
|
p.set('options', opt, self.options[opt])
|
||||||
|
|
||||||
|
for sec in self.misc.keys():
|
||||||
|
for opt in self.misc[sec].keys():
|
||||||
|
p.set(sec,opt,self.misc[sec][opt])
|
||||||
|
|
||||||
# try to create the directories and write the file
|
# try to create the directories and write the file
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(os.path.dirname(self.rcfile)):
|
if not os.path.exists(os.path.dirname(self.rcfile)):
|
||||||
|
@ -371,6 +391,9 @@ class configmanager(object):
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
return self.options.get(key, default)
|
return self.options.get(key, default)
|
||||||
|
|
||||||
|
def get_misc(self, sect, key, default=None):
|
||||||
|
return self.misc.get(sect,{}).get(key, default)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
self.options[key] = value
|
self.options[key] = value
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,12 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None):
|
||||||
all_timezones=[]
|
all_timezones=[]
|
||||||
pytz=pytzclass()
|
pytz=pytzclass()
|
||||||
idref2['pytz'] = pytz
|
idref2['pytz'] = pytz
|
||||||
|
try:
|
||||||
return eval(a_eval, idref2)
|
return eval(a_eval, idref2)
|
||||||
|
except:
|
||||||
|
logger = netsvc.Logger()
|
||||||
|
logger.notifyChannel("init", netsvc.LOG_WARNING, 'could eval(%s) for %s in %s, please get back and fix it!' % (a_eval,node.getAttribute('name'),context))
|
||||||
|
return ""
|
||||||
if t == 'xml':
|
if t == 'xml':
|
||||||
def _process(s, idref):
|
def _process(s, idref):
|
||||||
m = re.findall('[^%]%\((.*?)\)[ds]', s)
|
m = re.findall('[^%]%\((.*?)\)[ds]', s)
|
||||||
|
@ -231,12 +236,12 @@ class xml_import(object):
|
||||||
id = xml_id
|
id = xml_id
|
||||||
if '.' in xml_id:
|
if '.' in xml_id:
|
||||||
module, id = xml_id.split('.', 1)
|
module, id = xml_id.split('.', 1)
|
||||||
assert '.' not in id, """The ID reference "%s" must contains
|
assert '.' not in id, """The ID reference "%s" must contain
|
||||||
maximum one dot. They are used to refer to other modules ID, in the
|
maximum one dot. They are used to refer to other modules ID, in the
|
||||||
form: module.record_id""" % (xml_id,)
|
form: module.record_id""" % (xml_id,)
|
||||||
if module != self.module:
|
if module != self.module:
|
||||||
modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
|
modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
|
||||||
assert modcnt == 1, """The ID "%s" refer to an uninstalled module""" % (xml_id,)
|
assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
|
||||||
|
|
||||||
if len(id) > 64:
|
if len(id) > 64:
|
||||||
self.logger.notifyChannel('init', netsvc.LOG_ERROR, 'id: %s is to long (max: 64)'% (id,))
|
self.logger.notifyChannel('init', netsvc.LOG_ERROR, 'id: %s is to long (max: 64)'% (id,))
|
||||||
|
@ -274,8 +279,11 @@ form: module.record_id""" % (xml_id,)
|
||||||
res['report_sxw_content'] = sxw_content
|
res['report_sxw_content'] = sxw_content
|
||||||
if rec.get('header'):
|
if rec.get('header'):
|
||||||
res['header'] = eval(rec.get('header',''))
|
res['header'] = eval(rec.get('header',''))
|
||||||
|
if rec.get('report_type'):
|
||||||
|
res['report_type'] = rec.get('report_type','')
|
||||||
res['multi'] = rec.get('multi','') and eval(rec.get('multi',''))
|
res['multi'] = rec.get('multi','') and eval(rec.get('multi',''))
|
||||||
xml_id = rec.get('id','').encode('utf8')
|
xml_id = rec.get('id','').encode('utf8')
|
||||||
|
|
||||||
self._test_xml_id(xml_id)
|
self._test_xml_id(xml_id)
|
||||||
|
|
||||||
if rec.get('groups'):
|
if rec.get('groups'):
|
||||||
|
|
|
@ -173,8 +173,8 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
|
||||||
|
|
||||||
@return: fileobject if pathinfo is False else (fileobject, filepath)
|
@return: fileobject if pathinfo is False else (fileobject, filepath)
|
||||||
"""
|
"""
|
||||||
|
import addons
|
||||||
adp = os.path.normcase(os.path.abspath(config['addons_path']))
|
adps = addons.ad_paths
|
||||||
rtp = os.path.normcase(os.path.abspath(config['root_path']))
|
rtp = os.path.normcase(os.path.abspath(config['root_path']))
|
||||||
|
|
||||||
if name.replace(os.path.sep, '/').startswith('addons/'):
|
if name.replace(os.path.sep, '/').startswith('addons/'):
|
||||||
|
@ -189,6 +189,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
|
||||||
|
|
||||||
subdir2 = (subdir2 != 'addons' or None) and subdir2
|
subdir2 = (subdir2 != 'addons' or None) and subdir2
|
||||||
|
|
||||||
|
for adp in adps:
|
||||||
try:
|
try:
|
||||||
if subdir2:
|
if subdir2:
|
||||||
fn = os.path.join(adp, subdir2, name)
|
fn = os.path.join(adp, subdir2, name)
|
||||||
|
@ -382,6 +383,18 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
|
||||||
def write(self, s):
|
def write(self, s):
|
||||||
self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
|
self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
|
||||||
|
|
||||||
|
smtp_server = config['smtp_server']
|
||||||
|
if smtp_server.startswith('maildir:/'):
|
||||||
|
from mailbox import Maildir
|
||||||
|
maildir_path = smtp_server[8:]
|
||||||
|
try:
|
||||||
|
mdir = Maildir(maildir_path,factory=None, create = True)
|
||||||
|
mdir.add(msg.as_string(True))
|
||||||
|
return True
|
||||||
|
except Exception,e:
|
||||||
|
netsvc.Logger().notifyChannel('email_send (maildir)', netsvc.LOG_ERROR, e)
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
oldstderr = smtplib.stderr
|
oldstderr = smtplib.stderr
|
||||||
s = smtplib.SMTP()
|
s = smtplib.SMTP()
|
||||||
|
@ -392,8 +405,7 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
|
||||||
smtplib.stderr = WriteToLogger()
|
smtplib.stderr = WriteToLogger()
|
||||||
|
|
||||||
s.set_debuglevel(int(bool(debug))) # 0 or 1
|
s.set_debuglevel(int(bool(debug))) # 0 or 1
|
||||||
|
s.connect(smtp_server, config['smtp_port'])
|
||||||
s.connect(config['smtp_server'], config['smtp_port'])
|
|
||||||
if ssl:
|
if ssl:
|
||||||
s.ehlo()
|
s.ehlo()
|
||||||
s.starttls()
|
s.starttls()
|
||||||
|
@ -737,14 +749,14 @@ def ustr(value):
|
||||||
return unicode(value, getlocale()[1])
|
return unicode(value, getlocale()[1])
|
||||||
|
|
||||||
def exception_to_unicode(e):
|
def exception_to_unicode(e):
|
||||||
if hasattr(e, 'message'):
|
if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
|
||||||
return ustr(e.message)
|
return ustr(e.message)
|
||||||
if hasattr(e, 'args'):
|
if hasattr(e, 'args'):
|
||||||
return "\n".join((ustr(a) for a in e.args))
|
return "\n".join((ustr(a) for a in e.args))
|
||||||
try:
|
try:
|
||||||
return ustr(e)
|
return ustr(e)
|
||||||
except:
|
except:
|
||||||
return u"Unknow message"
|
return u"Unknown message"
|
||||||
|
|
||||||
|
|
||||||
# to be compatible with python 2.4
|
# to be compatible with python 2.4
|
||||||
|
@ -794,7 +806,7 @@ def get_languages():
|
||||||
'cs_CZ': u'Czech / Čeština',
|
'cs_CZ': u'Czech / Čeština',
|
||||||
'da_DK': u'Danish / Dansk',
|
'da_DK': u'Danish / Dansk',
|
||||||
'de_DE': u'German / Deutsch',
|
'de_DE': u'German / Deutsch',
|
||||||
'el_EL': u'Greek / Ελληνικά',
|
'el_GR': u'Greek / Ελληνικά',
|
||||||
'en_CA': u'English (CA)',
|
'en_CA': u'English (CA)',
|
||||||
'en_GB': u'English (UK)',
|
'en_GB': u'English (UK)',
|
||||||
'en_US': u'English (US)',
|
'en_US': u'English (US)',
|
||||||
|
@ -843,11 +855,11 @@ def get_user_companies(cr, user):
|
||||||
def _get_company_children(cr, ids):
|
def _get_company_children(cr, ids):
|
||||||
if not ids:
|
if not ids:
|
||||||
return []
|
return []
|
||||||
cr.execute('SELECT id FROM res_company WHERE parent_id = any(array[%s])' %(','.join([str(x) for x in ids]),))
|
cr.execute('SELECT id FROM res_company WHERE parent_id = ANY (%s)', (ids,))
|
||||||
res=[x[0] for x in cr.fetchall()]
|
res=[x[0] for x in cr.fetchall()]
|
||||||
res.extend(_get_company_children(cr, res))
|
res.extend(_get_company_children(cr, res))
|
||||||
return res
|
return res
|
||||||
cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %s AND comp.id = u.company_id' % (user,))
|
cr.execute('SELECT comp.id FROM res_company AS comp, res_users AS u WHERE u.id = %s AND comp.id = u.company_id', (user,))
|
||||||
compids=[cr.fetchone()[0]]
|
compids=[cr.fetchone()[0]]
|
||||||
compids.extend(_get_company_children(cr, compids))
|
compids.extend(_get_company_children(cr, compids))
|
||||||
return compids
|
return compids
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright P. Christeas <p_christ@hol.gr> 2008,2009
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# WARNING: This program as such is intended to be used by professional
|
||||||
|
# programmers who take the whole responsability of assessing all potential
|
||||||
|
# consequences resulting from its eventual inadequacies and bugs
|
||||||
|
# End users who are looking for a ready-to-use solution with commercial
|
||||||
|
# garantees and support are strongly adviced to contract a Free Software
|
||||||
|
# Service Company
|
||||||
|
#
|
||||||
|
# This program is Free Software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# 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 General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
__export_bis = {}
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def __init_ebis():
|
||||||
|
global __export_bis
|
||||||
|
|
||||||
|
_evars = [ 'abs', 'all', 'any', 'basestring' , 'bool',
|
||||||
|
'chr', 'cmp','complex', 'dict', 'divmod', 'enumerate',
|
||||||
|
'float', 'frozenset', 'getattr', 'hasattr', 'hash',
|
||||||
|
'hex', 'id','int', 'iter', 'len', 'list', 'long', 'map', 'max',
|
||||||
|
'min', 'oct', 'ord','pow', 'range', 'reduce', 'repr',
|
||||||
|
'reversed', 'round', 'set', 'setattr', 'slice','sorted', 'str',
|
||||||
|
'sum', 'tuple','type', 'unichr','unicode', 'xrange',
|
||||||
|
'True','False', 'None', 'NotImplemented', 'Ellipsis', ]
|
||||||
|
|
||||||
|
if sys.version_info[0:2] >= (2,6):
|
||||||
|
_evars.extend(['bin', 'format', 'next'])
|
||||||
|
for v in _evars:
|
||||||
|
__export_bis[v] = __builtins__[v]
|
||||||
|
|
||||||
|
|
||||||
|
__init_ebis()
|
||||||
|
|
||||||
|
|
||||||
|
def safe_eval(expr,sglobals,slocals = None):
|
||||||
|
""" A little safer version of eval().
|
||||||
|
This one, will use fewer builtin functions, so that only
|
||||||
|
arithmetic and logic expressions can really work """
|
||||||
|
|
||||||
|
global __export_bis
|
||||||
|
|
||||||
|
if not sglobals.has_key('__builtins__'):
|
||||||
|
# we copy, because we wouldn't want successive calls to safe_eval
|
||||||
|
# to be able to alter the builtins.
|
||||||
|
sglobals['__builtins__'] = __export_bis.copy()
|
||||||
|
|
||||||
|
return eval(expr,sglobals,slocals)
|
||||||
|
|
||||||
|
#eof
|
|
@ -133,9 +133,12 @@ class GettextAlias(object):
|
||||||
return source
|
return source
|
||||||
|
|
||||||
cr = frame.f_locals.get('cr')
|
cr = frame.f_locals.get('cr')
|
||||||
|
try:
|
||||||
lang = (frame.f_locals.get('context') or {}).get('lang', False)
|
lang = (frame.f_locals.get('context') or {}).get('lang', False)
|
||||||
if not (lang and cr):
|
if not (lang and cr):
|
||||||
return source
|
return source
|
||||||
|
except:
|
||||||
|
return source
|
||||||
|
|
||||||
cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, 'code', source))
|
cr.execute('select value from ir_translation where lang=%s and type=%s and src=%s', (lang, 'code', source))
|
||||||
res_trans = cr.fetchone()
|
res_trans = cr.fetchone()
|
||||||
|
@ -151,6 +154,7 @@ class TinyPoFile(object):
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
self.buffer.seek(0)
|
self.buffer.seek(0)
|
||||||
self.lines = self._get_lines()
|
self.lines = self._get_lines()
|
||||||
|
self.lines_count = len(self.lines);
|
||||||
|
|
||||||
self.first = True
|
self.first = True
|
||||||
self.tnrs= []
|
self.tnrs= []
|
||||||
|
@ -165,6 +169,9 @@ class TinyPoFile(object):
|
||||||
lines.append('') # ensure that the file ends with at least an empty line
|
lines.append('') # ensure that the file ends with at least an empty line
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
def cur_line(self):
|
||||||
|
return (self.lines_count - len(self.lines))
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
def unquote(str):
|
def unquote(str):
|
||||||
return str[1:-1].replace("\\n", "\n") \
|
return str[1:-1].replace("\\n", "\n") \
|
||||||
|
@ -178,14 +185,31 @@ class TinyPoFile(object):
|
||||||
else:
|
else:
|
||||||
tmp_tnrs = []
|
tmp_tnrs = []
|
||||||
line = None
|
line = None
|
||||||
|
fuzzy = False
|
||||||
while (not line):
|
while (not line):
|
||||||
if 0 == len(self.lines):
|
if 0 == len(self.lines):
|
||||||
raise StopIteration()
|
raise StopIteration()
|
||||||
line = self.lines.pop(0).strip()
|
line = self.lines.pop(0).strip()
|
||||||
if line.startswith('#:'):
|
if line.startswith('#:'):
|
||||||
tmp_tnrs.append( line[2:].strip().split(':') )
|
if ' ' in line[2:].strip():
|
||||||
if line.startswith('#'):
|
for lpart in line[2:].strip().split(' '):
|
||||||
line = None
|
tmp_tnrs.append(lpart.strip().split(':',2))
|
||||||
|
else:
|
||||||
|
tmp_tnrs.append( line[2:].strip().split(':',2) )
|
||||||
|
elif line.startswith('#,') and (line[2:].strip() == 'fuzzy'):
|
||||||
|
fuzzy = True
|
||||||
|
line = self.lines.pop(0).strip()
|
||||||
|
while not line:
|
||||||
|
# allow empty lines between comments and msgid
|
||||||
|
line = self.lines.pop(0).strip()
|
||||||
|
if line.startswith('#~ '):
|
||||||
|
while line.startswith('#~ ') or not line.strip():
|
||||||
|
if 0 == len(self.lines):
|
||||||
|
raise StopIteration()
|
||||||
|
line = self.lines.pop(0)
|
||||||
|
# This has been a deprecated entry, don't return anything
|
||||||
|
return self.next()
|
||||||
|
|
||||||
|
|
||||||
if not line.startswith('msgid'):
|
if not line.startswith('msgid'):
|
||||||
raise Exception("malformed file: bad line: %s" % line)
|
raise Exception("malformed file: bad line: %s" % line)
|
||||||
|
@ -202,7 +226,7 @@ class TinyPoFile(object):
|
||||||
|
|
||||||
while not line.startswith('msgstr'):
|
while not line.startswith('msgstr'):
|
||||||
if not line:
|
if not line:
|
||||||
raise Exception('malformed file')
|
raise Exception('malformed file at %d'% self.cur_line())
|
||||||
source += unquote(line)
|
source += unquote(line)
|
||||||
line = self.lines.pop(0).strip()
|
line = self.lines.pop(0).strip()
|
||||||
|
|
||||||
|
@ -212,12 +236,15 @@ class TinyPoFile(object):
|
||||||
trad += unquote(line)
|
trad += unquote(line)
|
||||||
line = self.lines.pop(0).strip()
|
line = self.lines.pop(0).strip()
|
||||||
|
|
||||||
if tmp_tnrs:
|
if tmp_tnrs and not fuzzy:
|
||||||
type, name, res_id = tmp_tnrs.pop(0)
|
type, name, res_id = tmp_tnrs.pop(0)
|
||||||
for t, n, r in tmp_tnrs:
|
for t, n, r in tmp_tnrs:
|
||||||
self.tnrs.append((t, n, r, source, trad))
|
self.tnrs.append((t, n, r, source, trad))
|
||||||
|
|
||||||
self.first = False
|
self.first = False
|
||||||
|
|
||||||
|
if name == None:
|
||||||
|
return self.next()
|
||||||
return type, name, res_id, source, trad
|
return type, name, res_id, source, trad
|
||||||
|
|
||||||
def write_infos(self, modules):
|
def write_infos(self, modules):
|
||||||
|
@ -251,8 +278,9 @@ class TinyPoFile(object):
|
||||||
def write(self, modules, tnrs, source, trad):
|
def write(self, modules, tnrs, source, trad):
|
||||||
def quote(s):
|
def quote(s):
|
||||||
return '"%s"' % s.replace('"','\\"') \
|
return '"%s"' % s.replace('"','\\"') \
|
||||||
.replace('\\ ','\\\\ ') \
|
.replace('\n', '\\n"\n"') \
|
||||||
.replace('\n', '\\n"\n"')
|
.replace(' \\ ',' \\\\ ')
|
||||||
|
|
||||||
|
|
||||||
plurial = len(modules) > 1 and 's' or ''
|
plurial = len(modules) > 1 and 's' or ''
|
||||||
self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
|
self.buffer.write("#. module%s: %s\n" % (plurial, ', '.join(modules)))
|
||||||
|
@ -404,11 +432,14 @@ def trans_generate(lang, modules, dbname=None):
|
||||||
|
|
||||||
query = 'SELECT name, model, res_id, module' \
|
query = 'SELECT name, model, res_id, module' \
|
||||||
' FROM ir_model_data'
|
' FROM ir_model_data'
|
||||||
if not 'all' in modules:
|
query_param = None
|
||||||
|
if 'all_installed' in modules:
|
||||||
|
query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
|
||||||
|
elif not 'all' in modules:
|
||||||
query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
|
query += ' WHERE module IN (%s)' % ','.join(['%s']*len(modules))
|
||||||
|
query_param = modules
|
||||||
query += ' ORDER BY module, model, name'
|
query += ' ORDER BY module, model, name'
|
||||||
|
|
||||||
query_param = not 'all' in modules and modules or None
|
|
||||||
cr.execute(query, query_param)
|
cr.execute(query, query_param)
|
||||||
|
|
||||||
_to_translate = []
|
_to_translate = []
|
||||||
|
@ -443,8 +474,8 @@ def trans_generate(lang, modules, dbname=None):
|
||||||
push_translation(module, 'view', encode(obj.model), 0, t)
|
push_translation(module, 'view', encode(obj.model), 0, t)
|
||||||
elif model=='ir.actions.wizard':
|
elif model=='ir.actions.wizard':
|
||||||
service_name = 'wizard.'+encode(obj.wiz_name)
|
service_name = 'wizard.'+encode(obj.wiz_name)
|
||||||
if netsvc.SERVICES.get(service_name):
|
if netsvc.Service._services.get(service_name):
|
||||||
obj2 = netsvc.SERVICES[service_name]
|
obj2 = netsvc.Service._services[service_name]
|
||||||
for state_name, state_def in obj2.states.iteritems():
|
for state_name, state_def in obj2.states.iteritems():
|
||||||
if 'result' in state_def:
|
if 'result' in state_def:
|
||||||
result = state_def['result']
|
result = state_def['result']
|
||||||
|
@ -459,6 +490,9 @@ def trans_generate(lang, modules, dbname=None):
|
||||||
}
|
}
|
||||||
|
|
||||||
# export fields
|
# export fields
|
||||||
|
if not result.has_key('fields'):
|
||||||
|
logger.notifyChannel("db",netsvc.LOG_WARNING,"res has no fields: %r" % result)
|
||||||
|
continue
|
||||||
for field_name, field_def in result['fields'].iteritems():
|
for field_name, field_def in result['fields'].iteritems():
|
||||||
res_name = name + ',' + field_name
|
res_name = name + ',' + field_name
|
||||||
|
|
||||||
|
@ -483,7 +517,11 @@ def trans_generate(lang, modules, dbname=None):
|
||||||
push_translation(module, 'wizard_button', res_name, 0, button_label)
|
push_translation(module, 'wizard_button', res_name, 0, button_label)
|
||||||
|
|
||||||
elif model=='ir.model.fields':
|
elif model=='ir.model.fields':
|
||||||
|
try:
|
||||||
field_name = encode(obj.name)
|
field_name = encode(obj.name)
|
||||||
|
except AttributeError, exc:
|
||||||
|
logger.notifyChannel("db", netsvc.LOG_ERROR, "name error in %s: %s" % (xml_name,str(exc)))
|
||||||
|
continue
|
||||||
objmodel = pool.get(obj.model)
|
objmodel = pool.get(obj.model)
|
||||||
if not objmodel or not field_name in objmodel._columns:
|
if not objmodel or not field_name in objmodel._columns:
|
||||||
continue
|
continue
|
||||||
|
@ -540,14 +578,29 @@ def trans_generate(lang, modules, dbname=None):
|
||||||
for field_name,field_def in pool.get(model)._columns.items():
|
for field_name,field_def in pool.get(model)._columns.items():
|
||||||
if field_def.translate:
|
if field_def.translate:
|
||||||
name = model + "," + field_name
|
name = model + "," + field_name
|
||||||
|
try:
|
||||||
trad = getattr(obj, field_name) or ''
|
trad = getattr(obj, field_name) or ''
|
||||||
|
except:
|
||||||
|
trad = ''
|
||||||
push_translation(module, 'model', name, xml_name, encode(trad))
|
push_translation(module, 'model', name, xml_name, encode(trad))
|
||||||
|
|
||||||
# parse source code for _() calls
|
# parse source code for _() calls
|
||||||
def get_module_from_path(path):
|
def get_module_from_path(path,mod_paths=None):
|
||||||
relative_addons_path = tools.config['addons_path'][len(tools.config['root_path'])+1:]
|
if not mod_paths:
|
||||||
if path.startswith(relative_addons_path) and (os.path.dirname(path) != relative_addons_path):
|
# First, construct a list of possible paths
|
||||||
path = path[len(relative_addons_path)+1:]
|
def_path = os.path.abspath(os.path.join(tools.config['root_path'], 'addons')) # default addons path (base)
|
||||||
|
ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
|
||||||
|
mod_paths=[def_path]
|
||||||
|
for adp in ad_paths:
|
||||||
|
mod_paths.append(adp)
|
||||||
|
if not adp.startswith('/'):
|
||||||
|
mod_paths.append(os.path.join(def_path,adp))
|
||||||
|
elif adp.startswith(def_path):
|
||||||
|
mod_paths.append(adp[len(def_path)+1:])
|
||||||
|
|
||||||
|
for mp in mod_paths:
|
||||||
|
if path.startswith(mp) and (os.path.dirname(path) != mp):
|
||||||
|
path = path[len(mp)+1:]
|
||||||
return path.split(os.path.sep)[0]
|
return path.split(os.path.sep)[0]
|
||||||
return 'base' # files that are not in a module are considered as being in 'base' module
|
return 'base' # files that are not in a module are considered as being in 'base' module
|
||||||
|
|
||||||
|
@ -707,6 +760,8 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
|
||||||
|
|
||||||
# if the resource id (res_id) is in that list, use it,
|
# if the resource id (res_id) is in that list, use it,
|
||||||
# otherwise use the whole list
|
# otherwise use the whole list
|
||||||
|
if not ids:
|
||||||
|
ids = []
|
||||||
ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
|
ids = (dic['res_id'] in ids) and [dic['res_id']] or ids
|
||||||
for id in ids:
|
for id in ids:
|
||||||
dic['res_id'] = id
|
dic['res_id'] = id
|
||||||
|
|
|
@ -43,7 +43,7 @@ class interface(netsvc.Service):
|
||||||
states = {}
|
states = {}
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
assert not netsvc.service_exist('wizard.'+name), 'The wizard "%s" already exists!'%name
|
assert not self.service_exist('wizard.'+name), 'The wizard "%s" already exists!'%name
|
||||||
super(interface, self).__init__('wizard.'+name)
|
super(interface, self).__init__('wizard.'+name)
|
||||||
self.exportMethod(self.execute)
|
self.exportMethod(self.execute)
|
||||||
self.wiz_name = name
|
self.wiz_name = name
|
||||||
|
|
|
@ -27,9 +27,8 @@ import pooler
|
||||||
|
|
||||||
def create(cr, ident, wkf_id):
|
def create(cr, ident, wkf_id):
|
||||||
(uid,res_type,res_id) = ident
|
(uid,res_type,res_id) = ident
|
||||||
cr.execute("select nextval('wkf_instance_id_seq')")
|
cr.execute('insert into wkf_instance (res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s) RETURNING id', (res_type,res_id,uid,wkf_id))
|
||||||
id_new = cr.fetchone()[0]
|
id_new = cr.fetchone()[0]
|
||||||
cr.execute('insert into wkf_instance (id,res_type,res_id,uid,wkf_id) values (%s,%s,%s,%s,%s)', (id_new,res_type,res_id,uid,wkf_id))
|
|
||||||
cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (wkf_id,))
|
cr.execute('select * from wkf_activity where flow_start=True and wkf_id=%s', (wkf_id,))
|
||||||
res = cr.dictfetchall()
|
res = cr.dictfetchall()
|
||||||
stack = []
|
stack = []
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
ADMIN_PASSWD='admin'
|
||||||
|
method_1() {
|
||||||
|
cat '-' << EOF
|
||||||
|
<xml>
|
||||||
|
<methodCall>
|
||||||
|
<methodName>set_loglevel</methodName>
|
||||||
|
<params>
|
||||||
|
<param><value><string>$ADMIN_PASSWD</string></value>
|
||||||
|
</param>
|
||||||
|
<param>
|
||||||
|
<value><string>$1</string></value>
|
||||||
|
</param>
|
||||||
|
</params>
|
||||||
|
</methodCall>
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
LEVEL=10
|
||||||
|
|
||||||
|
if [ -n "$1" ] ; then LEVEL=$1 ; fi
|
||||||
|
|
||||||
|
method_1 $LEVEL | POST -c 'text/xml' http://localhost:8069/xmlrpc/common
|
||||||
|
#eof
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
7
|
|
@ -0,0 +1,35 @@
|
||||||
|
Source: openerp-server
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Jimmy Angelakos <vyruss@hellug.gr>
|
||||||
|
Uploaders: Daniel Baumann <daniel@debian.org>
|
||||||
|
Build-Depends: debhelper (>= 7), po-debconf, python-dev, quilt
|
||||||
|
Build-Depends-Indep: python-libxslt1, python-lxml, python-psycopg2
|
||||||
|
Standards-Version: 3.8.3
|
||||||
|
Homepage: http://www.openerp.com/
|
||||||
|
Vcs-Browser: http://git.debian-maintainers.org/?p=open-object/openerp-server.git
|
||||||
|
Vcs-Git: git://git.debian-maintainers.org/git/open-object/openerp-server.git
|
||||||
|
XSBC-Maintainer-Homepage: http://open-object.debian-maintainers.org/
|
||||||
|
|
||||||
|
Package: openerp-server
|
||||||
|
Section: net
|
||||||
|
Architecture: all
|
||||||
|
Depends:
|
||||||
|
${misc:Depends}, adduser, python, python-libxslt1, python-lxml,
|
||||||
|
python-psycopg2, python-pydot, python-pychart, python-reportlab, python-tz
|
||||||
|
Conflicts: tinyerp-server
|
||||||
|
Replaces: tinyerp-server
|
||||||
|
Recommends:
|
||||||
|
graphviz, ghostscript, postgresql, postgresql-client, python-imaging,
|
||||||
|
python-matplotlib, python-openssl, python-pyparsing
|
||||||
|
Suggests: openerp-client
|
||||||
|
Description: Enterprise Resource Management (server)
|
||||||
|
Open ERP, previously known as TinyERP, is a complete ERP and CRM. The main
|
||||||
|
features are accounting (analytic and financial), stock management, sales and
|
||||||
|
purchases management, tasks automation, marketing campaigns, help desk, POS,
|
||||||
|
etc. Technical features include a distributed server, flexible workflows, an
|
||||||
|
object database, a dynamic GUI, customizable reports, and NET-RPC and XML-RPC
|
||||||
|
interfaces.
|
||||||
|
.
|
||||||
|
This package contains the Open ERP server, install openerp-client package for
|
||||||
|
the client.
|
|
@ -0,0 +1,245 @@
|
||||||
|
Author: Tiny.be <info@tiny.be>
|
||||||
|
Download: http://www.openerp.com/
|
||||||
|
|
||||||
|
Files: *
|
||||||
|
Copyright: (C) 2004-2009 Tiny.be <info@tiny.be>
|
||||||
|
License: GPL-3+
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-3 file.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
bin/addons/gen_graph.sh
|
||||||
|
doc/migrate/*
|
||||||
|
Copyright: (C) 2004-2008 Tiny.be <info@tiny.be>
|
||||||
|
License: GPL-2+
|
||||||
|
This program is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU General Public License
|
||||||
|
as published by the Free Software Foundation; either version 2
|
||||||
|
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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-2 file.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
bin/addons/account/report/general_ledger.py
|
||||||
|
bin/addons/account/report/general_ledger_landscape.py
|
||||||
|
bin/addons/account/wizard/wizard_statement_from_invoice.py
|
||||||
|
Copyright: (C) 2005-2008 CamptoCamp
|
||||||
|
License: GPL-2+
|
||||||
|
This program is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU General Public License
|
||||||
|
as published by the Free Software Foundation; either version 2
|
||||||
|
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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-2 file.
|
||||||
|
|
||||||
|
Files: bin/addons/auction/barcode/*
|
||||||
|
Copyright: (C) 2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||||
|
License: BSD
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
.
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. All advertising materials mentioning features or use of this software
|
||||||
|
must display the following acknowledgement:
|
||||||
|
This product includes software developed by Tyler C. Sarna.
|
||||||
|
4. Neither the name of the author nor the names of contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||||
|
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Files:
|
||||||
|
bin/addons/base_report_designer/wizard/tiny_sxw2rml/normalized_oo2rml.xsl
|
||||||
|
bin/addons/base_report_designer/wizard/tiny_sxw2rml/tiny_sxw2rml.py
|
||||||
|
Copyright: (C) 2005 Martin Simon
|
||||||
|
License: LGPL-2.1
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Library General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
.
|
||||||
|
This library 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
|
||||||
|
Library General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU Library General Public
|
||||||
|
License along with this library; if not, write to the Free
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU Library General Public License
|
||||||
|
can be found in /usr/share/common-licenses/LGPL-2.1 file.
|
||||||
|
|
||||||
|
Files: bin/addons/document/ftpserver/*
|
||||||
|
Copyright:
|
||||||
|
(C) 2007 Giampaolo Rodola <g.rodola@gmail.com>
|
||||||
|
(C) 2008 Fabien Pinckaers <fp@tinyerp.com>
|
||||||
|
License: MIT
|
||||||
|
Permission to use, copy, modify, and distribute this software and
|
||||||
|
its documentation for any purpose and without fee is hereby
|
||||||
|
granted, provided that the above copyright notice appear in all
|
||||||
|
copies and that both that copyright notice and this permission
|
||||||
|
notice appear in supporting documentation, and that the name of
|
||||||
|
Giampaolo Rodola' not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
.
|
||||||
|
Giampaolo Rodola' DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||||
|
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
|
||||||
|
NO EVENT Giampaolo Rodola' BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||||
|
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||||
|
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
Files: bin/addons/hr_holidays/*
|
||||||
|
Copyright:
|
||||||
|
(C) 2004-2008 Tiny.be <info@tiny.be>
|
||||||
|
(C) 2005-2006 Axelor SARL. <http://www.axelor.com/>
|
||||||
|
License: GPL-2+
|
||||||
|
This program is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU General Public License
|
||||||
|
as published by the Free Software Foundation; either version 2
|
||||||
|
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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-2 file.
|
||||||
|
|
||||||
|
Files: bin/addons/l10n_chart_uk_minimal/*
|
||||||
|
Copyright:
|
||||||
|
(C) 2004-2008 Tiny.be <info@tiny.be>
|
||||||
|
(C) 2004-2008 Seath Solutions Ltd.
|
||||||
|
License: GPL-2+
|
||||||
|
This program is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU General Public License
|
||||||
|
as published by the Free Software Foundation; either version 2
|
||||||
|
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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-2 file.
|
||||||
|
|
||||||
|
Files: bin/tools/threadinglocal.py
|
||||||
|
Copyright: (C) 2004-2005 CherryPy Team <team@cherrypy.org>
|
||||||
|
License: BSD
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
.
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the CherryPy Team nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Files: bin/tools/decimal.py
|
||||||
|
Copyright: (C) 2004 Python Software Foundation.
|
||||||
|
License: other
|
||||||
|
This file is distributed under the Python Software License
|
||||||
|
(http://www.python.org/2.3/license.html).
|
||||||
|
.
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
Files: debian/*
|
||||||
|
Copyright: (C) 2005-2009 Daniel Baumann <daniel@debian.org>
|
||||||
|
License: GPL-3+
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
.
|
||||||
|
On Debian systems, the complete text of the GNU General Public License
|
||||||
|
can be found in /usr/share/common-licenses/GPL-3 file.
|
|
@ -0,0 +1,55 @@
|
||||||
|
# /etc/openerp-server.conf(5) - configuration file for openerp-server(1)
|
||||||
|
|
||||||
|
[options]
|
||||||
|
# Enable the debugging mode (default False).
|
||||||
|
verbose = False
|
||||||
|
debug_mode = False
|
||||||
|
|
||||||
|
# The file where the server pid will be stored (default False).
|
||||||
|
#pidfile = /var/run/openerp.pid
|
||||||
|
|
||||||
|
# The file where the server log will be stored (default False).
|
||||||
|
logfile = /var/log/openerp-server.log
|
||||||
|
|
||||||
|
# The unix account on behalf openerp is running.
|
||||||
|
process_user = openerp
|
||||||
|
|
||||||
|
# The IP address on which the server will bind.
|
||||||
|
# If empty, it will bind on all interfaces (default empty).
|
||||||
|
interface = localhost
|
||||||
|
|
||||||
|
# The TCP port on which the server will listen (default 8069).
|
||||||
|
#port = 8070
|
||||||
|
|
||||||
|
# Enable debug mode (default False).
|
||||||
|
debug_mode = False
|
||||||
|
|
||||||
|
# Launch server over https instead of http (default False).
|
||||||
|
secure = False
|
||||||
|
|
||||||
|
# Specify the SMTP server for sending email (default localhost).
|
||||||
|
smtp_server = localhost
|
||||||
|
|
||||||
|
# Specify the SMTP user for sending email (default False).
|
||||||
|
smtp_user = False
|
||||||
|
|
||||||
|
# Specify the SMTP password for sending email (default False).
|
||||||
|
smtp_password = False
|
||||||
|
|
||||||
|
# Specify the database name.
|
||||||
|
#db_name = openerp
|
||||||
|
|
||||||
|
# Specify the database user name (default None).
|
||||||
|
db_user = openerp
|
||||||
|
|
||||||
|
# Specify the database password for db_user (default None).
|
||||||
|
db_password =
|
||||||
|
|
||||||
|
# Specify the database host (default localhost).
|
||||||
|
db_host =
|
||||||
|
|
||||||
|
# Specify the database port (default None).
|
||||||
|
db_port = 5432
|
||||||
|
|
||||||
|
# Specify the price accuracy.
|
||||||
|
#price_accuracy =
|
|
@ -0,0 +1,49 @@
|
||||||
|
openerp-server for Debian
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Open ERP uses a PostgreSQL database to store its data. With the first generation
|
||||||
|
of packages, you have to setup this database manually. Here is a short
|
||||||
|
explanation how to achieve this (you need to execute all commands as root):
|
||||||
|
|
||||||
|
0. Making sure, PostgreSQL is running
|
||||||
|
|
||||||
|
# /etc/init.d/postgresql restart
|
||||||
|
|
||||||
|
Note that depending on the version of PostgreSQL installed on your system,
|
||||||
|
the above syvinit script could also be named postgresql-VERSION (whereas
|
||||||
|
'VERSION' needs to be replace with a version number).
|
||||||
|
|
||||||
|
1. Creating the database user
|
||||||
|
|
||||||
|
# su - postgres -c "createuser -q --createdb --no-createrole --pwprompt openerp"
|
||||||
|
|
||||||
|
Note: If you want to run the database as another user than 'openerp', you
|
||||||
|
need to replace 'openerp' above with the user you want instead, and you
|
||||||
|
need to adjust 'db_user = openerp' in /etc/openerp-server.conf too.
|
||||||
|
|
||||||
|
3. Restarting openerp-server
|
||||||
|
|
||||||
|
# /etc/init.d/openerp-server restart
|
||||||
|
|
||||||
|
|
||||||
|
4. Initializing the database
|
||||||
|
|
||||||
|
Now you can connect with Open ERP client to the database and initialize it.
|
||||||
|
|
||||||
|
Now, you're finish. Please be aware of the following things:
|
||||||
|
|
||||||
|
* openerp-server has by default two accounts:
|
||||||
|
- User: admin; password: admin
|
||||||
|
- User: demo; password; demo
|
||||||
|
|
||||||
|
* openerp-server listens by default on port 8070. If you need to change this,
|
||||||
|
edit /etc/openerp-server.conf and replace 'port = 8070' with
|
||||||
|
'port = <your port>'.
|
||||||
|
|
||||||
|
* openerp-server in the upstreams configuration listens by default to *all*
|
||||||
|
interfaces. For security reasons, we do restrict it in the Debian packages
|
||||||
|
to listen only on localhost. If you need to change this, edit
|
||||||
|
/etc/openerp-server.conf and replace 'interface = localhost' with
|
||||||
|
'interface = <your ip>'.
|
||||||
|
|
||||||
|
-- Daniel Baumann <daniel@debian.org> Fri, 1 Jun 2007 12:00:00 +0200
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
db_input low openerp-server/username || true
|
||||||
|
db_go
|
||||||
|
|
||||||
|
db_stop
|
|
@ -0,0 +1,2 @@
|
||||||
|
doc/migrate
|
||||||
|
doc/tests
|
|
@ -0,0 +1 @@
|
||||||
|
debian/openerp-server.preseed
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
### BEGIN INIT INFO
|
||||||
|
# Provides: openerp-server
|
||||||
|
# Required-Start: $syslog
|
||||||
|
# Required-Stop: $syslog
|
||||||
|
# Should-Start: $network
|
||||||
|
# Should-Stop: $network
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: Enterprise Resource Management software
|
||||||
|
# Description: Open ERP is a complete ERP and CRM software.
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
PATH=/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
DAEMON=/usr/bin/openerp-server
|
||||||
|
NAME=openerp-server
|
||||||
|
DESC=openerp-server
|
||||||
|
|
||||||
|
USER=openerp
|
||||||
|
|
||||||
|
test -x ${DAEMON} || exit 0
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "${1}" in
|
||||||
|
start)
|
||||||
|
echo -n "Starting ${DESC}: "
|
||||||
|
|
||||||
|
start-stop-daemon --start --quiet --pidfile /var/run/${NAME}.pid \
|
||||||
|
--chuid ${USER} --background --make-pidfile \
|
||||||
|
--exec ${DAEMON} -- --config=/etc/openerp-server.conf
|
||||||
|
|
||||||
|
echo "${NAME}."
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
echo -n "Stopping ${DESC}: "
|
||||||
|
|
||||||
|
start-stop-daemon --stop --quiet --pidfile /var/run/${NAME}.pid \
|
||||||
|
--oknodo
|
||||||
|
|
||||||
|
echo "${NAME}."
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart|force-reload)
|
||||||
|
echo -n "Restarting ${DESC}: "
|
||||||
|
|
||||||
|
start-stop-daemon --stop --quiet --pidfile /var/run/${NAME}.pid \
|
||||||
|
--oknodo
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
start-stop-daemon --start --quiet --pidfile /var/run/${NAME}.pid \
|
||||||
|
--chuid ${USER} --background --make-pidfile \
|
||||||
|
--exec ${DAEMON} -- --config=/etc/openerp-server.conf
|
||||||
|
|
||||||
|
echo "${NAME}."
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
N=/etc/init.d/${NAME}
|
||||||
|
echo "Usage: ${NAME} {start|stop|restart|force-reload}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
|
@ -0,0 +1 @@
|
||||||
|
debian/local/openerp-server.conf /etc
|
|
@ -0,0 +1,2 @@
|
||||||
|
/usr/share/man/man5/openerp_serverrc.5.gz /usr/share/man/man5/openerp-server.conf.5.gz
|
||||||
|
/var/lib/openerp-server/filestore /usr/lib/openerp-server/filestore
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Add-on directories needs data directories, and sometimes they are
|
||||||
|
# (intentionally) empty, hence overriting the lintian warning.
|
||||||
|
openerp-server: package-contains-empty-directory
|
||||||
|
# Add-on directoires contain images sometimes, but it's nothing that is
|
||||||
|
# worthwile to be splittet out to /usr/share as it's all manual work for each
|
||||||
|
# release.
|
||||||
|
openerp-server: image-file-in-usr-lib
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
CONFFILE="/etc/openerp-server.conf"
|
||||||
|
LOGFILE="/var/log/openerp-server.log"
|
||||||
|
|
||||||
|
case "${1}" in
|
||||||
|
configure)
|
||||||
|
db_version 2.0
|
||||||
|
|
||||||
|
db_get openerp-server/username
|
||||||
|
_USERNAME="${RET:-openerp}"
|
||||||
|
|
||||||
|
db_stop
|
||||||
|
|
||||||
|
if ! getent passwd | grep -q "^${_USERNAME}"
|
||||||
|
then
|
||||||
|
adduser --system --no-create-home --quiet --gecos 'Open ERP server' --group ${_USERNAME}
|
||||||
|
else
|
||||||
|
echo "Open ERP user (${_USERNAME}) already exists, doing nothing."
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i -e "s|^process_user.*$|process_user = ${_USERNAME}|" ${CONFFILE}
|
||||||
|
|
||||||
|
# Creating log file
|
||||||
|
touch ${LOGFILE}
|
||||||
|
chown ${_USERNAME}:adm ${LOGFILE}
|
||||||
|
chmod 0640 ${LOGFILE}
|
||||||
|
|
||||||
|
# Creating local storage directory
|
||||||
|
mkdir -p /var/lib/openerp-server/filestore
|
||||||
|
|
||||||
|
# Setting ownership and permissions
|
||||||
|
chmod 0640 ${CONFFILE}
|
||||||
|
chown ${_USERNAME}:${_USERNAME} ${CONFFILE}
|
||||||
|
chown ${_USERNAME}:${_USERNAME} /var/lib/openerp-server -R
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
|
||||||
|
echo "* Open ERP uses a PostgreSQL database to store its data. With the first *"
|
||||||
|
echo "* generation of packages, you have to setup this database manually. *"
|
||||||
|
echo "* Please read /usr/share/doc/openerp-server/README.Debian how to do it. *"
|
||||||
|
echo "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
|
||||||
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "postinst called with unknown argument \`{$1}'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "${1}" in
|
||||||
|
remove)
|
||||||
|
_USERNAME="openerp"
|
||||||
|
_GROUPNAME="openerp"
|
||||||
|
|
||||||
|
if [ -x /usr/sbin/deluser ]
|
||||||
|
then
|
||||||
|
deluser --quiet --system ${_USERNAME}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x /usr/sbin/delgroup ]
|
||||||
|
then
|
||||||
|
delgroup --quiet --system --only-if-empty ${_GROUPNAME} || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /usr/lib/openerp-server/addons/__init__.pyc
|
||||||
|
rmdir --ignore-fail-on-non-empty /usr/lib/openerp-server/addons || true
|
||||||
|
rmdir --ignore-fail-on-non-empty /usr/lib/openerp-server || true
|
||||||
|
;;
|
||||||
|
|
||||||
|
purge)
|
||||||
|
rm -rf /var/lib/openerp-server
|
||||||
|
;;
|
||||||
|
|
||||||
|
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "postrm called with unknown argument \`${1}'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
|
@ -0,0 +1,5 @@
|
||||||
|
################################################################################
|
||||||
|
## openerp-server
|
||||||
|
|
||||||
|
#openerp-server openerp-server/username string openerp
|
||||||
|
################################################################################
|
|
@ -0,0 +1,9 @@
|
||||||
|
Template: openerp-server/username
|
||||||
|
Type: string
|
||||||
|
Default: openerp
|
||||||
|
_Description: Dedicated system account for the Open ERP server:
|
||||||
|
The Open ERP server must use a dedicated account for its operation so that
|
||||||
|
the system's security is not compromised by running it with superuser
|
||||||
|
privileges.
|
||||||
|
.
|
||||||
|
Please choose that account's username.
|
|
@ -0,0 +1,15 @@
|
||||||
|
Author: Daniel Baumann <daniel@debian.org>
|
||||||
|
Description: Disable modules check to make it buildable without X11.
|
||||||
|
|
||||||
|
diff -Naurp openerp-server.orig/setup.py openerp-server/setup.py
|
||||||
|
--- openerp-server.orig/setup.py 2009-06-08 15:51:20.000000000 +0000
|
||||||
|
+++ openerp-server/setup.py 2009-07-26 10:27:49.000000000 +0000
|
||||||
|
@@ -121,7 +121,7 @@ def data_files():
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
-check_modules()
|
||||||
|
+#check_modules()
|
||||||
|
|
||||||
|
f = file('openerp-server','w')
|
||||||
|
start_script = """#!/bin/sh\necho "OpenERP Setup - The content of this file is generated at the install stage\n" """
|
|
@ -0,0 +1,15 @@
|
||||||
|
Author: Brian DeRocher <brian@derocher.org>
|
||||||
|
Description: Correct SQL syntax in migrate script (Closes: #467517).
|
||||||
|
|
||||||
|
diff -Naurp openerp-server.orig/doc/migrate/3.4.0-4.0.0/pre.py openerp-server/doc/migrate/3.4.0-4.0.0/pre.py
|
||||||
|
--- openerp-server.orig/doc/migrate/3.4.0-4.0.0/pre.py 2008-11-03 21:33:56.000000000 +0000
|
||||||
|
+++ openerp-server/doc/migrate/3.4.0-4.0.0/pre.py 2008-11-09 09:09:49.000000000 +0000
|
||||||
|
@@ -123,7 +123,7 @@ cr.commit()
|
||||||
|
|
||||||
|
for line in (
|
||||||
|
"ALTER TABLE ir_module_module ADD demo BOOLEAN",
|
||||||
|
- "ALTER TABLE ir_module_module SET demo DEFAULT False",
|
||||||
|
+ "ALTER TABLE ir_module_module alter column demo set DEFAULT False",
|
||||||
|
"DELETE FROM ir_values WHERE VALUE LIKE '%,False'",
|
||||||
|
"""UPDATE ir_ui_view set arch='<?xml version="1.0"?><tree string="Menu" toolbar="1"><field icon="icon" name="name"/></tree>' where name='ir.ui.menu.tree' and type='tree' and field_parent='child_id'""",
|
||||||
|
):
|
|
@ -0,0 +1,12 @@
|
||||||
|
Author: Daniel Baumann <daniel@debian.org>
|
||||||
|
Description: Correcting shebang.
|
||||||
|
|
||||||
|
diff -Naurp openerp-server.orig/bin/addons/document/ftpserver/ftpserver.py openerp-server/bin/addons/document/ftpserver/ftpserver.py
|
||||||
|
--- openerp-server.orig/bin/addons/document/ftpserver/ftpserver.py 2009-05-30 10:14:37.000000000 +0000
|
||||||
|
+++ openerp-server/bin/addons/document/ftpserver/ftpserver.py 2009-05-30 11:02:00.000000000 +0000
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-#!/usr/bin/env python
|
||||||
|
+#!/usr/bin/python
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# ftpserver.py
|
||||||
|
#
|
|
@ -0,0 +1 @@
|
||||||
|
[type: gettext/rfc822deb] openerp-server.templates
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Czech translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Vítězslav Kotrla <vitezslav.kotrla@gmail.com>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-05-30 08:24+0000\n"
|
||||||
|
"PO-Revision-Date: 2009-06-21 08:20+0200\n"
|
||||||
|
"Last-Translator: Vítězslav Kotrla <vitezslav.kotrla@gmail.com>\n"
|
||||||
|
"Language-Team: Czech <debian-l10n-czech@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Vyhrazený systémový účet, pod kterým bude běžet Open ERP server:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that the "
|
||||||
|
"system's security is not compromised by running it with superuser privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Pokud by byl Open ERP server spuštěn se superuživatelskými oprávněními, mohlo by dojít "
|
||||||
|
"ke kompromitaci zabezpečení systému. Proto musí Open ERP server pro svoji činnost používat "
|
||||||
|
"vyhrazený neprivilegovaný účet."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Zvolte prosím název totoho účtu."
|
|
@ -0,0 +1,40 @@
|
||||||
|
# German translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Kai Wasserbäch <debian@carbon-project.org>
|
||||||
|
# Copyright (C) 2009 Helge Kreutzmann <debian@helgefjell.de>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-06 19:01+0200\n"
|
||||||
|
"Last-Translator: Helge Kreutzmann <debian@helgefjell.de>\n"
|
||||||
|
"Language-Team: German <debian-l10n-german@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Eigenes Systemkonto für den Open ERP-Server:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Der Open ERP-Server muss ein eigenes Konto für den Betrieb verwenden, um die "
|
||||||
|
"Sicherheit des Systems nicht durch das Betreiben mit Superuser-Rechten zu "
|
||||||
|
"kompromittieren."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Bitte wählen Sie den Benutzernamen dieses Kontos."
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Spanish translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Software in the Public Interest
|
||||||
|
# 2009 Fernando González de Requena <fgrequena@gmail.com>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-03-30 22:35+0200\n"
|
||||||
|
"Last-Translator: Fernando González de Requena <fgrequena@gmail.com>\n"
|
||||||
|
"Language-Team: Spanish <debian-l10n-spanish@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Cuenta del sistema dedicada para el servidor Open ERP:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"El servidor de Open ERP debe utilizar una cuenta dedicada para su "
|
||||||
|
"funcionamiento, de tal modo que la seguridad del sistema no se vea "
|
||||||
|
"comprometida por su utilización con privilegios de administración."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Elija un nombre de usuario para esa cuenta."
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Finnish translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Esko Arajärvi
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-07 22:19+0300\n"
|
||||||
|
"Last-Translator: Esko Arajärvi <edu@iki.fi>\n"
|
||||||
|
"Language-Team: Finnish <debian-l10n-finnish@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Lokalize 0.3\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Dedikoitu järjestelmätunnus Open ERP-taustaohjelman ajamiseen:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Open ERP-palvelimen tulee käyttää dedikoitua tunnusta toiminnoissaan, jotta "
|
||||||
|
"järjestelmän turvallisuus ei vaarannu kuten käytettäessä "
|
||||||
|
"pääkäyttäjäoikeuksia."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Valitse kyseisen tunnuksen nimi."
|
|
@ -0,0 +1,39 @@
|
||||||
|
# French translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Steve Petruzzello <dlist@bluewin.ch>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-03-26 01:12+0100\n"
|
||||||
|
"Last-Translator: Steve Petruzzello <dlist@bluewin.ch>\n"
|
||||||
|
"Language-Team: French <debian-l10n-french@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Identifiant dédié pour le serveur Open ERP:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Le serveur Open ERP doit être exécuté avec un identifiant spécifique, "
|
||||||
|
"différent du superutilisateur, afin de ne pas compromettre la sécurité du "
|
||||||
|
"système."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Veuillez choisir cet identifiant."
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Galizian translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Marce Villarino <mvillarino@gmail.com>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-15 21:32+0200\n"
|
||||||
|
"Last-Translator: marce villarino <mvillarino@users.sourceforge.net>\n"
|
||||||
|
"Language-Team: Galician <proxecto@trasno.ent>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Lokalize 0.2\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Conta de usuario do sistema adicada ao servidor Open ERP:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"O servidor Open ERP debe empregar unha conta adicada a el para que a "
|
||||||
|
"seguridade do sistema non se poña en perigo ao executalo con privilexios de "
|
||||||
|
"administrador."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Escolla o nome de usuario desa conta."
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Italian translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Vincenzo Campanella <vinz65@gmail.com>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-03-26 08:52+0100\n"
|
||||||
|
"Last-Translator: Vincenzo Campanella <vinz65@gmail.com>\n"
|
||||||
|
"Language-Team: Italian <tp@lists.linux.it>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Account di sistema dedicato per il server di Open ERP:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Il server Open ERP deve utilizzare un account dedicato per eseguire le "
|
||||||
|
"proprie operazioni, in modo che la sicurezza del sistema non rischi di "
|
||||||
|
"essere compromessa eseguendolo con privilegi di superutente."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Scegliere il nome utente di tale account."
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Japanese translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Hideki Yamane <henrich@debian.or.jp>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-09 19:20+0900\n"
|
||||||
|
"Last-Translator: Hideki Yamane (Debian-JP) <henrich@debian.or.jp>\n"
|
||||||
|
"Language-Team: Japanese <debian-japanese@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Open ERP デーモン専用のシステムアカウント:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Open ERP サーバはその動作について専用のアカウントを使うようになっているため、"
|
||||||
|
"管理者特権で動作していてもシステムのセキュリティは侵害されません。"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "アカウントのユーザ名を選んでください。"
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Portuguese translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Américo Monteiro <a_monteiro@netcabo.pt>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-03-26 19:12+0000\n"
|
||||||
|
"Last-Translator: Américo Monteiro <a_monteiro@netcabo.pt>\n"
|
||||||
|
"Language-Team: Portuguese <traduz@debianpt.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Conta dedicada do sistema para o servidor Open ERP:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"O servidor Open ERP tem que usar uma conta dedicada para as suas operações, "
|
||||||
|
"isto para que a segurança do sistema não seja comprometida ao corrê-lo com "
|
||||||
|
"privilégios de superutilizador."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Por favor escolha o nome dessa conta."
|
|
@ -0,0 +1,43 @@
|
||||||
|
# German translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Sergey Alyoshin <alyoshin.s@gmail.com>
|
||||||
|
# Copyright (C) 2009 Yuri Kozlov <yuray@komyakino.ru>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server 5.0.1-0-2\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-08 21:08+0400\n"
|
||||||
|
"Last-Translator: Yuri Kozlov <yuray@komyakino.ru>\n"
|
||||||
|
"Language-Team: Russian <debian-l10n-russian@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%"
|
||||||
|
"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Специально выделенная системная учётная запись для Open ERP службы:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Для улучшения безопасности системы Open ERP сервер должен использовать "
|
||||||
|
"специально выделенную учётную запись, а не запускаться с правами "
|
||||||
|
"суперпользователя."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Укажите имя такой учётной записи."
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Swedish translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Martin Bagge <brother@bsnet.se>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-04-07 18:36+0100\n"
|
||||||
|
"Last-Translator: Martin Bagge <brother@bsnet.se>\n"
|
||||||
|
"Language-Team: swedish <debian-l10n-swedish@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: KBabel 1.11.4\n"
|
||||||
|
"X-Poedit-Language: Swedish\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "Dedikerat systemkonto för Open ERP server:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Open ERP servern måste ha ett dedikerat konto för att fungera på ett säkert "
|
||||||
|
"sätt, att använda privilegier från en superanvändare är inte säkert."
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "Ange kontots användarnamn."
|
|
@ -0,0 +1,38 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=CHARSET\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr ""
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Simplified Chinese translation of openerp-server debconf templates.
|
||||||
|
# Copyright (C) 2009 Deng Xiyue <manphiz-guest@users.alioth.debian.org>
|
||||||
|
# This file is distributed under the same license as the openerp-server package.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: openerp-server\n"
|
||||||
|
"Report-Msgid-Bugs-To: openerp-server@packages.debian.org\n"
|
||||||
|
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
|
||||||
|
"PO-Revision-Date: 2009-03-26 16:48+0800\n"
|
||||||
|
"Last-Translator: Deng Xiyue <manphiz-guest@users.alioth.debian.org>\n"
|
||||||
|
"Language-Team: Debian Chinese GB <debian-chinese-gb@lists.debian.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Dedicated system account for the Open ERP server:"
|
||||||
|
msgstr "指定操作 Open ERP 守护进程的专用系统帐户:"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid ""
|
||||||
|
"The Open ERP server must use a dedicated account for its operation so that "
|
||||||
|
"the system's security is not compromised by running it with superuser "
|
||||||
|
"privileges."
|
||||||
|
msgstr ""
|
||||||
|
"Open ERP 服务器必须使用一个专用的账户来进行操作,这样就不会因为使用超级用户权"
|
||||||
|
"限运行而破坏系统的安全。"
|
||||||
|
|
||||||
|
#. Type: string
|
||||||
|
#. Description
|
||||||
|
#: ../openerp-server.templates:1001
|
||||||
|
msgid "Please choose that account's username."
|
||||||
|
msgstr "请选择账户的用户名。"
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
SHELL := sh -e
|
||||||
|
|
||||||
|
|
||||||
|
update:
|
||||||
|
# Needs: shell-helper
|
||||||
|
|
||||||
|
cd debian; \
|
||||||
|
debconf-create-preseed *.config; \
|
||||||
|
|
||||||
|
for FILE in debian/*.preseed; \
|
||||||
|
do \
|
||||||
|
grep -v preseed $$FILE > $$FILE.tmp; \
|
||||||
|
mv $$FILE.tmp $$FILE; \
|
||||||
|
echo $$FILE >> debian/`basename $$FILE .preseed`.examples; \
|
||||||
|
done
|
||||||
|
|
||||||
|
clean: clean-patched unpatch
|
||||||
|
clean-patched: patch
|
||||||
|
dh_testdir
|
||||||
|
dh_testroot
|
||||||
|
rm -f build-stamp
|
||||||
|
|
||||||
|
NO_CHECK_MODULES=1 python setup.py clean
|
||||||
|
rm -rf build openerp-server
|
||||||
|
-find $(CURDIR) -type f -name "*.pyc" | xargs rm -f
|
||||||
|
|
||||||
|
debconf-updatepo
|
||||||
|
|
||||||
|
dh_clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
install: patch
|
||||||
|
dh_testdir
|
||||||
|
dh_testroot
|
||||||
|
dh_prep
|
||||||
|
dh_installdirs
|
||||||
|
|
||||||
|
NO_CHECK_MODULES=1 python setup.py install --no-compile --prefix=$(CURDIR)/debian/openerp-server/usr
|
||||||
|
|
||||||
|
# Adjusting program location
|
||||||
|
sed -i -e 's|cd .*python.*/site-packages|cd /usr/lib|' debian/openerp-server/usr/bin/openerp-server
|
||||||
|
mv debian/openerp-server/usr/lib/python*/site-packages/openerp-server debian/openerp-server/usr/lib
|
||||||
|
rm -rf debian/openerp-server/usr/lib/python*
|
||||||
|
|
||||||
|
# Fixing permissions
|
||||||
|
find debian/openerp-server/usr/lib/openerp-server/addons -type f -print0 | xargs -0 chmod 0644
|
||||||
|
|
||||||
|
# Removing double files
|
||||||
|
rm -rf debian/openerp-server/usr/share/doc/openerp-server-*
|
||||||
|
|
||||||
|
binary: binary-indep
|
||||||
|
|
||||||
|
binary-arch:
|
||||||
|
|
||||||
|
binary-indep: install
|
||||||
|
dh_testdir
|
||||||
|
dh_testroot
|
||||||
|
dh_installchangelogs doc/Changelog
|
||||||
|
dh_installdocs
|
||||||
|
dh_installexamples
|
||||||
|
dh_install
|
||||||
|
dh_installinit --update-rcd-params='defaults 21'
|
||||||
|
dh_installdebconf
|
||||||
|
dh_lintian
|
||||||
|
dh_link
|
||||||
|
dh_compress
|
||||||
|
dh_fixperms
|
||||||
|
dh_installdeb
|
||||||
|
dh_gencontrol
|
||||||
|
dh_md5sums
|
||||||
|
dh_builddeb
|
||||||
|
|
||||||
|
patch:
|
||||||
|
|
||||||
|
unpatch:
|
||||||
|
|
||||||
|
.PHONY: clean build install binary binary-arch binary-indep patch unpatch
|
|
@ -0,0 +1,951 @@
|
||||||
|
4.2.1
|
||||||
|
Bugfixes
|
||||||
|
Fix context for source_count function
|
||||||
|
Create stock move on production for products without BOM lines
|
||||||
|
Add IBAN fields in bank view
|
||||||
|
Fix uninitialize variable in import data
|
||||||
|
Update due date on invoice when payment term change
|
||||||
|
Fix store on field function that have type many2one or one2one
|
||||||
|
Request summary must be truncate
|
||||||
|
Partner event name must be truncate
|
||||||
|
Remove parent field on partner contact view
|
||||||
|
Fix icon type on journal period
|
||||||
|
Remove exception on the size of char field
|
||||||
|
Fix reference on move line that comes from invoice (Customer != Supplier)
|
||||||
|
Add function search on sheet_id of timesheet_sheet
|
||||||
|
Don't return 0 for balance account if there is no fiscal year
|
||||||
|
Fix set to draft for expense, now really set to draft
|
||||||
|
Add product and partner in the recursive call of tax compute
|
||||||
|
Don't compute balance account for inactive account
|
||||||
|
Fix bad encoding in log message on report_sxw
|
||||||
|
Fix overdue report for refund lines
|
||||||
|
Don't start server in non secure mode if secure mode have been set
|
||||||
|
Fix default value of move line if move_id is not find
|
||||||
|
Fix _product_partner_ref for cannot concatenate 'str' and 'bool' objects
|
||||||
|
Add partner_id in the context of SO for browsing the product
|
||||||
|
Fix multi-tax code on invoice
|
||||||
|
Fix tax definition for Belgium chart
|
||||||
|
Remove compute debit/credit on inactive account
|
||||||
|
Fix the way the tax are rounded for invoice with tax included prices
|
||||||
|
Fix SO to use the right uom and price to create invoice
|
||||||
|
Fix on_chnage uos on SO to return the id not the browse record
|
||||||
|
Add condition on the button "Sending goods>Packing to be invoiced" to show
|
||||||
|
only customer packings
|
||||||
|
Fix zero division error when the quantity is zero on an invoice line
|
||||||
|
Fix duplicate timesheet line that have been invoiced
|
||||||
|
Fix invoice report for bad removeParentNode tag
|
||||||
|
Fix priority for product view
|
||||||
|
Fix tax line computation when encoding account lines manually
|
||||||
|
Fix refund supplier invoice to have the same journal
|
||||||
|
New chinese translation
|
||||||
|
Pass context to action_done on stock move
|
||||||
|
Add product_uom change on sale order line
|
||||||
|
Fix demo data for working time UOM
|
||||||
|
Fix _sheet function in timesheet_sheet when called with a list of non
|
||||||
|
unique id
|
||||||
|
Remove commit inside function validate on account move
|
||||||
|
Use one function to post account move
|
||||||
|
Fix computation of sale/purchase amount in segmentation module
|
||||||
|
Use standar uom converion in analytic lines
|
||||||
|
Add journal_id in context for account move line search in payment module
|
||||||
|
Fix wrong id used by pricelist based on partner form
|
||||||
|
Use partner reference from SO/PO for invoice name if there is one
|
||||||
|
Make analysis analytic module include child accounts
|
||||||
|
|
||||||
|
4.2.0
|
||||||
|
Summary:
|
||||||
|
Add new view graph
|
||||||
|
REPORT_INTRASTAT: new module
|
||||||
|
KERNEL: add netrpc (speed improvement)
|
||||||
|
REPORT_STOCK: add report on stock by stock location and production lots
|
||||||
|
HR_TIMESHEET_INVOICE: add final invoice
|
||||||
|
MULTI_COMPANY_ACCOUNT: new module
|
||||||
|
ADD modules publication tools
|
||||||
|
KERNEL: add timezone
|
||||||
|
KERNEL: add concurnecy check
|
||||||
|
BASE: allow to specify many view_id in act_window
|
||||||
|
BASE: add ir.rules (acces base on record fields)
|
||||||
|
KERNEL: add search_count on objects
|
||||||
|
KERNEL: add assert tools (unit test)
|
||||||
|
KERNEL: improve workflow speed
|
||||||
|
KERNEL: move some modules to extra_addons
|
||||||
|
Bugfixes:
|
||||||
|
Fix pooler for multi-db
|
||||||
|
REPORT_ANALYTIC: new reports
|
||||||
|
BOARD_ACCOUNT: new dashboard for accountants
|
||||||
|
PURCHASE: allow multiple pickings for the same purchase order
|
||||||
|
STOCK: When refunding picking: confirm & Assign the newly generated picking
|
||||||
|
PRODUCT: add average price
|
||||||
|
STOCK: Fix workflow for stock
|
||||||
|
TOOLS: Fix export translate for wizard
|
||||||
|
KERNEL: add id in import_data
|
||||||
|
BASE: add history rate to currency
|
||||||
|
ACCOUNT: partner_id is now required for an invoice
|
||||||
|
HR_TIMESHEET: add exception if employee haven't product
|
||||||
|
I18N: new fr_CH file
|
||||||
|
HR_EXPENSE: fix domain
|
||||||
|
ACCOUNT: Fix invoice with currency and payment term
|
||||||
|
ACCOUNT: Fix currency
|
||||||
|
KERNEL: add pidfile
|
||||||
|
ACCOUNT,PURCHASE,SALE: use partner lang for description
|
||||||
|
Model Acces: Unlink permission (delete) is now available
|
||||||
|
KERNEL: Remove set for python2.3
|
||||||
|
HR: add id to Attendance menu
|
||||||
|
PRODUCT: add dimension to packaging
|
||||||
|
ACCOUNT: new cash_discount on payment term
|
||||||
|
KERNEL: Add price accuracy
|
||||||
|
BASE: Function to remove installed modules
|
||||||
|
REPORT_SALE: fix for sale without line
|
||||||
|
PURCHASE: remove use of currency
|
||||||
|
KERNEL: fix set without values
|
||||||
|
PURCHASE: fix domain pricelist
|
||||||
|
INVOICE: use date for currency rate
|
||||||
|
KERNEL: Fix import many2many by id
|
||||||
|
KERNEL: run the cron
|
||||||
|
ACCOUNT: bank statment line now have a ref t othe corresponding invoice
|
||||||
|
ACCOUNT: Add possibilitty to include tax amount in base amount for the computation of the next taxes
|
||||||
|
ACCOUNT: Add product in tax compute python code
|
||||||
|
KERNEL: use reportlab 2.0
|
||||||
|
BASE: fix import the same lang
|
||||||
|
ACCOUNT: fix tax code
|
||||||
|
ACCOUNT: define tax account for invoice and refund
|
||||||
|
ACCOUNT: add supplier tax to product
|
||||||
|
ACCOUNT: don't overwrite tax_code on the creation for account line
|
||||||
|
PURCHASE: use partner code for report order
|
||||||
|
KERNEL: fix pooler netsvc for multi-db
|
||||||
|
TOOLS: add ref to function tag
|
||||||
|
PRODUCT: fix digits on volume and weight, add weight_net
|
||||||
|
ACCOUNT: split to new module account_cash_discount
|
||||||
|
ORM : error message on python constraints are now displayed correctly
|
||||||
|
ACCOUNT: add partner to tax compute context
|
||||||
|
KERNEL: improve logger
|
||||||
|
PROJECT: add check_recursion for project
|
||||||
|
HR_TIMESHEET_INVOICE: improve create invoice
|
||||||
|
ACCOUNT: add product_id to analytic line create by invoice
|
||||||
|
KERNEL: fix the inheritance mechanism
|
||||||
|
KERNEL: Fix use always basename for cvs file
|
||||||
|
BASE: fix IBAN len to 27
|
||||||
|
INVOICE: fix invoice number for analytic
|
||||||
|
REPORT: add replace tag for custom header
|
||||||
|
ACCOUNT: add ref to analytic line
|
||||||
|
BASE: prevent exception in ir_cron
|
||||||
|
SALE: fix uos for tax_amount
|
||||||
|
MRP: fix dbname in _procure_confirm
|
||||||
|
HR_EXPENSE: add domain to analytic_account
|
||||||
|
KERNEL: use 0 instead of False for fix on _fnct_read
|
||||||
|
SUBSCRIPTION: add required to model
|
||||||
|
HR_TIMESHEET: add rounding on report
|
||||||
|
SALE: Fix cancel invoice and recreate invoice, now cancel also the order lines
|
||||||
|
STOCK-DELIVERY: add wizard invoice_onshipping from delivery to stock
|
||||||
|
STOCK: use tax from sale for invoice
|
||||||
|
BASE: improve copy of res.partner
|
||||||
|
ACCOUNT: pay only invoice if not in state draft
|
||||||
|
REPORT: fix rml translation, translate before eval
|
||||||
|
PRODUCT_EXTENDED: don't use seller price for bom price
|
||||||
|
ACCOUNT_TAX_INCLUDE: fix right amount in account move generate with tax_include
|
||||||
|
BASE: improve workflow print
|
||||||
|
SALE: fix workflow error when create invoice from wizard
|
||||||
|
MRP: Use company currency for Product Cost Structure
|
||||||
|
BASE: prevent recursion in company
|
||||||
|
KERNEL: Fix deleted property and many2one
|
||||||
|
KERNEL: allow directory for import csv
|
||||||
|
KERNEL: add store option to fields function
|
||||||
|
ACCOUNT: use property_account_tax on on_change_product
|
||||||
|
KERNEL: add right-click for translate label
|
||||||
|
KERNEL: fix log of backtrace
|
||||||
|
KERNEL: fix search on xxx2many
|
||||||
|
BASE: use tool to call popen.pipe2
|
||||||
|
KERNEL: fix print workflow on win32
|
||||||
|
BASE: fix US states
|
||||||
|
KERNEL: use python 2.3 format_exception
|
||||||
|
ACCOUNT: add multi-company into base accounting
|
||||||
|
KERNEL: check return code for exec_pg_command_pipe
|
||||||
|
KERNEL: fix search with active args
|
||||||
|
KERNEL: improve _sql_contsraints, now insert if doesn't exist
|
||||||
|
KERNEL: remove old inheritor and add _constraints and _sql_constraints to the fields inherited
|
||||||
|
CRM: bugfix mailgate
|
||||||
|
PURCHASE: fix the UOM for purchase line and improve update price unit
|
||||||
|
ACCOUNT: new invoice view
|
||||||
|
KERNEL,BASE: allow to create zip modules
|
||||||
|
BASE: add right-to-left
|
||||||
|
KERNEL: copy now ignore technical values ('create_date', 'create_uid', 'write_date' and 'write_uid')
|
||||||
|
ACCOUNT_TAX_INCLUDE: Now the module manage correctly the case when the taxes defined on the product differ from the taxes defined on the invoice line
|
||||||
|
ALL: fix colspan 3 -> 4
|
||||||
|
KERNEL: use context for search
|
||||||
|
ACCOUNT: improve speed of analytic account
|
||||||
|
ACCOUNT: fix search debit/credit on partner
|
||||||
|
ACCOUNT: fix refund invoice if no product_id nor uos_id on lines
|
||||||
|
MRP: fix scheduler location of product to produce and method, date of automatic orderpoint
|
||||||
|
KERNEL: many2many : fix unlink and link action
|
||||||
|
MRP: add default product_uom from context and add link from product to bom
|
||||||
|
PROJECT: improve speed for function fields
|
||||||
|
ALL: remove bad act_window name
|
||||||
|
KERNEL: modification for compatibility with postgres 7.4
|
||||||
|
KERNEL: fix size for selection field
|
||||||
|
KERNEL: fix compatibility for python2.5
|
||||||
|
KERNEL: add new win32 build script
|
||||||
|
KERNEL: add test for duplicate report and wizard
|
||||||
|
ACCOUNT: force round amount fixed in payment term
|
||||||
|
KERNEL: fix print screen
|
||||||
|
CRM: Better ergonomy
|
||||||
|
SERVER: add sum tag on tree view that display sum of the selected lines
|
||||||
|
KERNEL: allow subfield query on one2many
|
||||||
|
KERNEL: fix create_date and write_date as there are timestamp now
|
||||||
|
SERVER: improve language
|
||||||
|
KERNEL: fix search on fields function of type one2many, many2many
|
||||||
|
ACCOUNT: fix pay invoice to use period
|
||||||
|
ACCOUNT: add check recursion in account.tax.code
|
||||||
|
MRP: fix compute cycle for workcenter
|
||||||
|
BASE: add constraint uniq module name
|
||||||
|
BASE: improve update module list
|
||||||
|
ACCOUNT: add round to last payment term
|
||||||
|
KERNEL: don't modify the args of the call
|
||||||
|
KERNEL: don't use mutable as default value in function defintion
|
||||||
|
KERNEL: fix orm for sql query with reserved words
|
||||||
|
|
||||||
|
16/03/2007
|
||||||
|
4.0.3
|
||||||
|
Summary:
|
||||||
|
Improve the migration scripts
|
||||||
|
Some bugfixes
|
||||||
|
Print workflow on win32 (with ghostscript)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
BASE: Fix "set default value"
|
||||||
|
HR_TIMESHEET_INVOICE: Improve invoice on timesheet
|
||||||
|
ACCOUNT: Fix tax amount
|
||||||
|
KERNEL: correct the delete for property
|
||||||
|
PURCHASE: fix the journal for invoice created by PO
|
||||||
|
KERNEL: fix the migration for id removed
|
||||||
|
Add id to some menuitem
|
||||||
|
BASE: prevent exception in ir_cron when the DB is dropped
|
||||||
|
HR: Fix sign-in/sign-out, the user is now allowed to provide a date in
|
||||||
|
the future
|
||||||
|
SALE: fix uos for the tax amount
|
||||||
|
MRP: fix wrong dbname in _procure_confirm
|
||||||
|
HR_EXPENSE: add domain to analytic_account
|
||||||
|
ACCOUNT: fix debit_get
|
||||||
|
SUBSCRIPTION: model is required now
|
||||||
|
HR_TIMESHEET: add rounding value to report
|
||||||
|
SALE: Fix cancel and recreate invoice, now cancel also the order lines
|
||||||
|
STOCK: use the tax define in sale for the invoice
|
||||||
|
ACCOUNT: add test to pay only if invoice not in state draft
|
||||||
|
KERNEL: root have access to all records
|
||||||
|
REPORT: fix rml translation to translate before the eval
|
||||||
|
ACCOUNT_TAX_INCLUDE: Use the right amount in account mmove generate
|
||||||
|
with tax_include
|
||||||
|
BASE: Improve the workflow print
|
||||||
|
SALE: Fix workflow error when creating invoice from the wizard
|
||||||
|
PRODUCT_EXTENDED: don't use pricelist to compute standard price
|
||||||
|
MRP: Use company currency for product cost structure
|
||||||
|
KERNEL: fix where clause when deleting false items
|
||||||
|
ACCOUNT: product source account depend on the invoice type now
|
||||||
|
ACCOUNT: use the property account tax for the on_change_product
|
||||||
|
ACCOUNT: use the invoice date for the date of analytic line
|
||||||
|
ACCOUNT: Fix the pay invoice when multi-currency
|
||||||
|
HR_TIMESHEET_PROJECT: use the right product
|
||||||
|
STOCK: Fix to assign picking with product consumable and call the
|
||||||
|
workflow
|
||||||
|
STOCK: Fix the split lot production
|
||||||
|
PURCHASE: fix workflow for purchase with manual invoice to not set
|
||||||
|
invoice and paid
|
||||||
|
DELIVERY: can use any type of journal for invoice
|
||||||
|
KERNEL: fix search on xxx2many
|
||||||
|
ACCOUNT: add id to sequence record
|
||||||
|
KERNEL: set properly the demo flag for module installed
|
||||||
|
KERNEL: Fix print workflow on win32
|
||||||
|
LETTER: fix print letter
|
||||||
|
|
||||||
|
Migration:
|
||||||
|
Fix migration for postreSQL 7.4
|
||||||
|
Fix the default value of demo in module
|
||||||
|
Fix migration of account_uos to product_uos
|
||||||
|
|
||||||
|
Wed Jan 17 15:06:07 CET 2007
|
||||||
|
4.0.2
|
||||||
|
Summary:
|
||||||
|
Improve the migration
|
||||||
|
Some bugfixes
|
||||||
|
Improve tax
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
Fix tax for invoice, refund, etc
|
||||||
|
SALE: fix view priority
|
||||||
|
PURCHASE: wizard may crash on some data
|
||||||
|
BASE: Fix import the same lang
|
||||||
|
BASE: start the cron
|
||||||
|
PURCHASE: fix domain for pricelist
|
||||||
|
KERNEL: fix object set without values
|
||||||
|
REPORT_SALE: fix for sale without line
|
||||||
|
KERNEL: add pidfile
|
||||||
|
BASE: remove 'set' for python2.3 compliant
|
||||||
|
Migration:
|
||||||
|
Migrate hr_timesheet user_id
|
||||||
|
|
||||||
|
Fri Dec 22 12:01:26 CET 2006
|
||||||
|
4.0.1
|
||||||
|
Summary:
|
||||||
|
Improve the migration
|
||||||
|
Some bugfixes
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
HR_EXPENSE: Fix domain
|
||||||
|
HR_TIMESHEET: Fix employee without product
|
||||||
|
TOOLS: Fix export translate
|
||||||
|
BASE: fix for concurrency of sequence number
|
||||||
|
MRP: fix report
|
||||||
|
CRM: fix graph report
|
||||||
|
KERNEL: fix instance of osv_pool
|
||||||
|
KERNEL: fix setup.py
|
||||||
|
|
||||||
|
|
||||||
|
Mon Dec 4 18:01:55 CET 2006
|
||||||
|
4.0.0
|
||||||
|
Summary:
|
||||||
|
Some bugfixes
|
||||||
|
|
||||||
|
Tue Nov 28 14:44:20 CET 2006
|
||||||
|
4.0.0-rc1
|
||||||
|
Summary:
|
||||||
|
This is a stable version (RC1) with lots of new features. Main
|
||||||
|
Improvements were:
|
||||||
|
Accounting: more functions, new modules, more stable
|
||||||
|
Much more better ergonomy
|
||||||
|
Lots of simplification to allows non IT people to use and
|
||||||
|
configure Tiny ERP: manage database, step by step configuration
|
||||||
|
menu, auto-installers, better help, ...
|
||||||
|
|
||||||
|
New:
|
||||||
|
Skill management module
|
||||||
|
ACCOUNT:
|
||||||
|
New and simpler bank statement form
|
||||||
|
New reports:
|
||||||
|
on Timesheets (analytic accounting)
|
||||||
|
Theorical revenue based on time spent
|
||||||
|
Global timesheet report by month
|
||||||
|
Chart of accounts
|
||||||
|
Different taxes methods supported
|
||||||
|
Gross (brut)
|
||||||
|
Net
|
||||||
|
Fixed amount
|
||||||
|
INVOICE:
|
||||||
|
invoice on shipping (manufacturing industry)
|
||||||
|
invoice on timesheet (services)
|
||||||
|
PURCHASE:
|
||||||
|
different invoicing control method (on order, on shipping,
|
||||||
|
manual)
|
||||||
|
Support of prices tax included /excluded in sales orders
|
||||||
|
New modules:
|
||||||
|
Sale_journal, stock_journal for bigger industries:
|
||||||
|
Divide works in different journals
|
||||||
|
New invoicing method from partner, to so, to picking
|
||||||
|
Daily, Monthly (grouped by partner or not)
|
||||||
|
New modules for prices with taxes included / excluded
|
||||||
|
New chart of accounts supported:
|
||||||
|
l10n_be/ l10n_chart_be_frnl/
|
||||||
|
l10n_chart_id/ l10n_chart_uk/
|
||||||
|
l10n_ca-qc/ l10n_chart_br/
|
||||||
|
l10n_chart_it/ l10n_chart_us_general/
|
||||||
|
l10n_ch/ l10n_chart_ca_en/
|
||||||
|
l10n_chart_it_cc2424/ l10n_chart_us_manufacturing/
|
||||||
|
l10n_ch_pcpbl_association/ l10n_chart_ca_fr/
|
||||||
|
l10n_chart_la/ l10n_chart_us_service/
|
||||||
|
l10n_ch_pcpbl_independant/ l10n_chart_ch_german/
|
||||||
|
l10n_chart_nl/ l10n_chart_us_ucoa/
|
||||||
|
l10n_ch_pcpbl_menage/ l10n_chart_cn/
|
||||||
|
l10n_chart_nl_standard/ l10n_chart_us_ucoa_ez/
|
||||||
|
l10n_ch_pcpbl_plangen/ l10n_chart_cn_traditional/
|
||||||
|
l10n_chart_no/ l10n_chart_ve/
|
||||||
|
l10n_ch_pcpbl_plangensimpl/ l10n_chart_co/
|
||||||
|
l10n_chart_pa/ l10n_fr/
|
||||||
|
l10n_ch_vat_brut/ l10n_chart_cz/
|
||||||
|
l10n_chart_pl/ l10n_se/
|
||||||
|
l10n_ch_vat_forfait/ l10n_chart_da/
|
||||||
|
l10n_chart_sp/ l10n_simple/
|
||||||
|
l10n_ch_vat_net/ l10n_chart_de_datev_skr03/
|
||||||
|
l10n_chart_sw/
|
||||||
|
l10n_chart_at/ l10n_chart_de_skr03/
|
||||||
|
l10n_chart_sw_church/
|
||||||
|
l10n_chart_au/ l10n_chart_hu/
|
||||||
|
l10n_chart_sw_food/
|
||||||
|
Step by step configuration menu
|
||||||
|
Setup wizard on first connection
|
||||||
|
Select a company profile, auto-install language, demo data, ...
|
||||||
|
|
||||||
|
Imrovements:
|
||||||
|
KERNEL: Demo data improved
|
||||||
|
Better import / export system
|
||||||
|
KERNEL: Multi-database management system
|
||||||
|
Backup, Restore, Create, Drop from the client
|
||||||
|
PRODUCT/PRODUCT_EXTD: Eavily change the product form, use the new
|
||||||
|
object to compute the pricelist
|
||||||
|
REPORTS:
|
||||||
|
Better Sale order, purchase order, invocies and customers reports
|
||||||
|
ACCOUNT: Support of taxes in accounts
|
||||||
|
management of the VAT taxes for most european countries:
|
||||||
|
Support of VAT codes in invoices
|
||||||
|
Better computation of default values in accounting entries
|
||||||
|
Preferences in partners, override products
|
||||||
|
Bugfix when closing a fiscal year
|
||||||
|
Better ergonomy when writting entries
|
||||||
|
New Module Management System:
|
||||||
|
Install / Upgrade new modules directly from the client
|
||||||
|
Install new languages
|
||||||
|
KERNEL:
|
||||||
|
Ability to add select=True at the object level for postgresql indexes
|
||||||
|
Bugfix in search in some inherited objects
|
||||||
|
Added the ability to call methods from a browse object
|
||||||
|
KERNEL+BASE: changed the way the migration system works for menuitems:
|
||||||
|
now you can change a menuitem defined elsewhere. And this will work
|
||||||
|
whether that menuitem has an id or not (it use the name of the
|
||||||
|
menuitem to find it)
|
||||||
|
KERNEL:
|
||||||
|
Installing a module from the client
|
||||||
|
Better Windows Auto-Installer
|
||||||
|
DELIVERY:
|
||||||
|
Delivery and invoicing on picking list
|
||||||
|
KERNEL:
|
||||||
|
Distinction between active (by default) and installable
|
||||||
|
ACCOUNT/PROJECT: Added support for the type of invoicing
|
||||||
|
CRM:
|
||||||
|
eMAil gateway
|
||||||
|
Management of different departments and sections
|
||||||
|
Rule system
|
||||||
|
About 20 new statistics reporting
|
||||||
|
eCommerce interface:
|
||||||
|
Better Joomla (virtuemart, OSCommerce) support
|
||||||
|
Joomla is now fully functionnal
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
ACCOUNT: tree view on reporting analytic account
|
||||||
|
KERNEL: Fix the bug that happened when mixing active and child_of
|
||||||
|
search
|
||||||
|
KERNEL: Check for the existance of active when computing child_of
|
||||||
|
PRODUCT: production computation with different UoM
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Fri Oct 6 14:44:05 CEST 2006
|
||||||
|
Server 3.4.2
|
||||||
|
Improvements:
|
||||||
|
BASE: changed workflow print system so that it handles inexisting
|
||||||
|
workflows more gracefully (patch from Geoff Gardiner)
|
||||||
|
MRP: new view to take into account the orderpoint exceptions
|
||||||
|
MRP: made menu title more explicit
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
ACCOUNT: fixed typo in invoice + changed sxw file so that it is in
|
||||||
|
sync with the rml file
|
||||||
|
DELIVERY: fixed taxes on delivery line (patch from Brice Vissière)
|
||||||
|
PROJECT: skip tasks without user in Gantt charts (it crashed the report)
|
||||||
|
PRODUCT: fixed bug when no active pricelist version was found
|
||||||
|
PRODUCT_EXTENDED: correct recursive computation of the price
|
||||||
|
SALE: get product price from price list even when quantity is set after
|
||||||
|
the product is set
|
||||||
|
STOCK: fixed partial picking
|
||||||
|
|
||||||
|
Packaging:
|
||||||
|
Changed migration script so that it works on PostgreSQL 7.4
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Tue Sep 12 15:10:31 CEST 2006
|
||||||
|
Server 3.4.1
|
||||||
|
Bugfixes:
|
||||||
|
ACCOUNT: fixed a bug which prevented to reconcile posted moves.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Mon Sep 11 16:12:10 CEST 2006
|
||||||
|
Server 3.4.0 (changes since 3.3.0)
|
||||||
|
New modules:
|
||||||
|
ESALE_JOOMLA: integration with Joomla CMS
|
||||||
|
HR_TIMESHEET_ICAL: import iCal to automatically complete timesheet
|
||||||
|
based on outlook meetings
|
||||||
|
PARTNER_LDAP: adds partner synchronization with an LDAP server
|
||||||
|
SALE_REBATE: adds rebates to sale orders
|
||||||
|
|
||||||
|
4 new modules for reporting using postgresql views:
|
||||||
|
REPORT_CRM: reporting on CRM cases: by month, user, ...
|
||||||
|
REPORT_PROJECT: reporting on projects: tasks closed by project, user,
|
||||||
|
month, ...
|
||||||
|
REPORT_PURCHASE: reporting on purchases
|
||||||
|
REPORT_SALE: reporting on sales by periods and by product, category of
|
||||||
|
product, ...
|
||||||
|
|
||||||
|
New features:
|
||||||
|
KERNEL: Tiny ERP server and client may now communicate through HTTPS.
|
||||||
|
To launch the server with HTTPS, use the -S or --secure option
|
||||||
|
Note that if the server runs on HTTPS, the clients MUST connect
|
||||||
|
with the "secure" option checked.
|
||||||
|
KERNEL: the server can now run as a service on Windows
|
||||||
|
Printscreen function (Tree view print)
|
||||||
|
KERNEL: added a new --stop-after-init option which stops the server
|
||||||
|
just before it starts listening
|
||||||
|
KERNEL: added support for a new forcecreate attribute on XML record
|
||||||
|
fields: it is useful for records are in a data node marked as
|
||||||
|
"noupdate" but the record still needs to be added if it doesn't
|
||||||
|
exit yet. The typical use for that is when you add a new record
|
||||||
|
to a noupdate file/node.
|
||||||
|
KERNEL: manage SQL constraints with human-readable error message on the
|
||||||
|
client side, eg: Unique constraints
|
||||||
|
KERNEL: added a new system to be able to specify the tooltip for each
|
||||||
|
field in the definition of the field (by using the new help=""
|
||||||
|
attribute)
|
||||||
|
ACCOUNT: new report: aged trial balance system
|
||||||
|
ACCOUNT: added a wizard to pay an invoice from the invoice form
|
||||||
|
BASE: print on a module to print the reference guide using introspection
|
||||||
|
HR: added report on attendance errors
|
||||||
|
PRODUCT: products now support multi-Level variants
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
KERNEL: speed improvement in many parts of the system thanks to some
|
||||||
|
optimizations and a new caching system
|
||||||
|
KERNEL: New property system which replace the, now deprecated, ir_set
|
||||||
|
system. This leads to better migration of properties, more
|
||||||
|
practical use of them (they can be used like normal fields),
|
||||||
|
they can be translated, they are "multi-company aware", and
|
||||||
|
you can specify access rights for them on a per field basis.
|
||||||
|
KERNEL: Under windows, the server looks for its configuration file in
|
||||||
|
the "etc" sub directory (relative to the installation path).
|
||||||
|
This was needed so that the server can be run as a windows
|
||||||
|
service (using the SYSTEM profile).
|
||||||
|
KERNEL: added ability to import CSV files from the __terp__.py file
|
||||||
|
KERNEL: force freeing cursor when closing them, so that they are
|
||||||
|
available again immediately and not when garbage collected.
|
||||||
|
KERNEL: automatically drop not null/required constraints from removed
|
||||||
|
fields (ie which are in the database but not in the object)
|
||||||
|
KERNEL: added a command-line option to specify which smtp server to use
|
||||||
|
to send emails.
|
||||||
|
KERNEL: made browse_record hashable
|
||||||
|
ALL: removed shortcuts for the demo user.
|
||||||
|
ACCOUNT: better invoice report
|
||||||
|
ACCOUNT: Modifs for account chart, removed old stock_income account type
|
||||||
|
ACCOUNT: made the test_paid method on invoices more tolerant to buggy
|
||||||
|
data (open invoices without move/movelines)
|
||||||
|
ACCOUNT: better bank statement reconciliation system
|
||||||
|
ACCOUNT: accounting entries encoding improved a lot (using journal)
|
||||||
|
ACCOUNT: Adding a date and max Qty field in analytic accounts for
|
||||||
|
support contract
|
||||||
|
ACCOUNT: Adding the View type to analytic account / cost account
|
||||||
|
ACCOUNT: changed test_paid so that the workflow works even if there is
|
||||||
|
no move line
|
||||||
|
ACCOUNT: Cleanup credit/debit and balance computation methods. Should
|
||||||
|
be faster too.
|
||||||
|
ACCOUNT: use the normal sequence (from the journal) for the name of
|
||||||
|
moves generated from invoices instead of the longer name.
|
||||||
|
ACCOUNT: print Payment delay in invoices
|
||||||
|
ACCOUNT: account chart show subtotals
|
||||||
|
ACCOUNT: Subtotal in view accounts
|
||||||
|
ACCOUNT: Replaced some Typo: moves-> entries, Transaction -> entry
|
||||||
|
ACCOUNT: added quantities in analytic accounts view, and modified
|
||||||
|
cost ledger report for partners/customers
|
||||||
|
ACCOUNT: added default value for the currency field in invoices
|
||||||
|
ACCOUNT: added the comment/notes field on the invoice report
|
||||||
|
BASE: added menuitem (and action) to access partner functions (in the
|
||||||
|
definitions menu)
|
||||||
|
BASE: better demo data
|
||||||
|
BASE: duplicating a menu item now duplicates its action and submenus
|
||||||
|
BASE: Bank Details on Partners
|
||||||
|
CRM: View on all actions made on cases (used by our ISO9002 customer
|
||||||
|
to manage corrections to actions)
|
||||||
|
CRM: fixed wizard to create a sale order from a case
|
||||||
|
CRM: search on non active case, not desactivated by default
|
||||||
|
CRM: Case ID in fields with search
|
||||||
|
HR_TIMESHEET: new "sign_in, sign_out" using projects. It fills
|
||||||
|
timesheets and attendance at the same time.
|
||||||
|
HR_TIMESHEET: added cost unit to employee demo data
|
||||||
|
MRP: improvement in the scheduler
|
||||||
|
MRP: purchase order lines' description generated from a procurement
|
||||||
|
defaults to the product name instead of procurement name
|
||||||
|
MRP: Better traceability
|
||||||
|
MRP: Better view for procurement in exception
|
||||||
|
MRP: Added production delay in product forms. Use this delay for
|
||||||
|
average production delay for one product
|
||||||
|
MRP: dates scheduler, better computation
|
||||||
|
MRP: added constraint for non 0 BoM lines
|
||||||
|
PRODUCT: Better pricelist system (on template or variant of product)
|
||||||
|
PRODUCT_EXTENDED: Compute the price only if there is a supplier
|
||||||
|
PROJECT: when a task is closed, use the task's customer to warn the
|
||||||
|
customer if it is set, otherwise use the project contact.
|
||||||
|
PROJECT: better system to automatically send an email to the customer
|
||||||
|
when a task is closed or reopened.
|
||||||
|
PURCHASE: date_planned <= current_time line in red
|
||||||
|
PURCHASE: better purchase order report
|
||||||
|
PURCHASE: better purchase order duplication: you can now duplicate non
|
||||||
|
draft purchase orders and the new one will become draft.
|
||||||
|
SALE: better sale order report
|
||||||
|
SALE: better demo data for sale orders
|
||||||
|
SALE: better view for buttons in sale.order
|
||||||
|
SALE: select product => description = product name instead of code
|
||||||
|
SALE: warehouse field in shop is now required
|
||||||
|
SCRUM: lots of improvements for better useability
|
||||||
|
STOCK: allows to confirm empty picking lists.
|
||||||
|
STOCK: speed up stock computation methods
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
KERNEL: fix a huge bug in the search method for objects involving
|
||||||
|
"old-style" inheritance (inherits) which prevented some records
|
||||||
|
to be accessible in some cases. Most notable example was some
|
||||||
|
products were not accessible in the sale order lines if you had
|
||||||
|
more products in your database than the limit of your search
|
||||||
|
(80 by default).
|
||||||
|
KERNEL: fixed bug which caused OO (sxw) reports to behave badly (crash
|
||||||
|
on Windows and not print correctly on Linux) when data
|
||||||
|
contained XML entities (&, <, >)
|
||||||
|
KERNEL: reports are now fully concurrency compliant
|
||||||
|
KERNEL: fixed bug which caused menuitems without id to cause havoc on
|
||||||
|
update. The menuitems themselves were not created (which is
|
||||||
|
correct) but they created a bad "default" action for all
|
||||||
|
menuitems without action (such as all "menu folders").
|
||||||
|
KERNEL: fix a small security issue: we should check the password of the
|
||||||
|
user when a user asks for the result of a report (in addition
|
||||||
|
to the user id and id of that report)
|
||||||
|
KERNEL: bugfix in view inheritancy
|
||||||
|
KERNEL: fixed duplicating resource with a state field whose selection
|
||||||
|
doesn't contain a 'draft' value (for example project tasks). It
|
||||||
|
now uses the default value of the resource for that field.
|
||||||
|
KERNEL: fixed updating many2many fields using the (4, id) syntax
|
||||||
|
KERNEL: load/save the --logfile option correctly in the config file
|
||||||
|
KERNEL: fixed duplicating a resource with many2many fields
|
||||||
|
ALL: all properties should be inside a data tag with "noupdate" and
|
||||||
|
should have a forcecreate attribute.
|
||||||
|
ACCOUNT: fixed rounding bug in tax computation method
|
||||||
|
ACCOUNT: bugfix in balance and aged balance reports
|
||||||
|
ACCOUNT: fixing precision in function fields methods
|
||||||
|
ACCOUNT: fixed creation of account move lines without using the client
|
||||||
|
interface
|
||||||
|
ACCOUNT: fixed duplicating invoices
|
||||||
|
ACCOUNT: fixed opening an invoices whose description contained non
|
||||||
|
ASCII chars at specific position
|
||||||
|
ACCOUNT: small bugfixes in all accounting reports
|
||||||
|
ACCOUNT: fixed crash when --without-demo due to missing payment.term
|
||||||
|
ACCOUNT: fixed bug in automatic reconciliation
|
||||||
|
ACCOUNT: pass the address to the tax computation method so that it is
|
||||||
|
available in the tax "python applicable code"
|
||||||
|
BASE: allows to delete a request which has a history (it now deletes the
|
||||||
|
history as well as the request)
|
||||||
|
BASE: override copy method for users so that we can duplicate them
|
||||||
|
BASE: fixed bug when the user search for a partner by hitting on an
|
||||||
|
empty many2one field (it searched for a partner with ref=='')
|
||||||
|
BASE: making ir.sequence call thread-safe.
|
||||||
|
CRM: fixed a bug which introduced an invalid case state when closing a
|
||||||
|
case (Thanks to Leigh Willard)
|
||||||
|
HR: added domain to category tree view so that they are not displayed
|
||||||
|
twice
|
||||||
|
HR_TIMESHEET: fixed print graph
|
||||||
|
HR_TIMESHEET: fixed printing timesheet report
|
||||||
|
HR_TIMESHEET: Remove a timesheet entry removes the analytic line
|
||||||
|
MRP: bugfix on "force reservation"
|
||||||
|
MRP: fixed bugs in some reports and MRP scheduler when a partner has
|
||||||
|
no address
|
||||||
|
MRP: fix Force production button if no product available
|
||||||
|
MRP: when computing lots of procurements, the scheduler could raise
|
||||||
|
locking error at the database level. Fixed.
|
||||||
|
PRODUCT: added missing context to compute product list price
|
||||||
|
PRODUCT: fixed field type of qty_available and virtual_available
|
||||||
|
(integer->float). This prevented these fields to be displayed
|
||||||
|
in forms.
|
||||||
|
PROJECT: fixed the view of unassigned task (form and list) instead of
|
||||||
|
form only.
|
||||||
|
PURCHASE: fixed merging orders that made inventory errors when coming
|
||||||
|
from a procurement (orderpoint).
|
||||||
|
PURCHASE: fix bug which prevented to make a purchase order with
|
||||||
|
"manual" lines (ie without product)
|
||||||
|
PURCHASE: fix wizard to group purchase orders in several ways:
|
||||||
|
- only group orders if they are to the same location
|
||||||
|
- only group lines if they are the same except for qty and unit
|
||||||
|
- fix the workflow redirect method so that procurement are not
|
||||||
|
canceled when we merge orders
|
||||||
|
SALE: fixed duplicating a confirmed sale order
|
||||||
|
SALE: fixed making sale orders with "manual" lines (without product)
|
||||||
|
STOCK: future stock prevision bugfix (for move when date_planned < now)
|
||||||
|
STOCK: better view for stock.move
|
||||||
|
STOCK: fixed partial pickings (waiting for a production)
|
||||||
|
Miscellaneous minor bugfixes
|
||||||
|
|
||||||
|
Packaging:
|
||||||
|
Fixed bug in setup.py which didn't copy csv files nor some sub-
|
||||||
|
directories
|
||||||
|
Added a script to migrate a 3.3.0 server to 3.4.0 (you should read the
|
||||||
|
README file in doc/migrate/3.3.0-3.4.0)
|
||||||
|
Removed OsCommerce module
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Fri May 19 10:16:18 CEST 2006
|
||||||
|
Server 3.3.0
|
||||||
|
New features:
|
||||||
|
NEW MODULE: hr_timesheet_project
|
||||||
|
Automatically maps projects and tasks to analytic account
|
||||||
|
So that hours spent closing tasks are automatically encoded
|
||||||
|
KERNEL: Added a logfile and a pidfile option (patch from Dan Horak)
|
||||||
|
STOCK: Added support for revisions of tracking numbers
|
||||||
|
STOCK: Added support for revision of production lots
|
||||||
|
STOCK: Added a "splitting and tracking lines" wizard
|
||||||
|
PRODUCT_EXTENDED: Added a method to compute the cost of a product
|
||||||
|
automatically from the cost of its parts
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
ALL: Small improvements in wizards (order of buttons)
|
||||||
|
PRODUCT: Remove packaging info from supplierinfo
|
||||||
|
PROJECT: Better task view (moved unused fields to other tab)
|
||||||
|
SALE: Keep formating for sale order lines' notes in the sale order report
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
KERNEL: Fixed bug which caused field names with non ascii chars didn't work
|
||||||
|
in list mode on Windows
|
||||||
|
KERNEL: Fix concurrency issue with UpdatableStr with the use of
|
||||||
|
threading.local
|
||||||
|
KERNEL: Removed browse_record __unicode__ method... It made the sale order
|
||||||
|
report crash when using product names with non ASCII characters
|
||||||
|
KERNEL: Fixed bug which caused the translation export to fail when the server
|
||||||
|
was not launched from the directory its source is.
|
||||||
|
BASE: Updating a menuitem now takes care its parent menus
|
||||||
|
BASE: Fixed a cursor locking issue with updates
|
||||||
|
BASE: Fixed viewing sequence types as a tree/list
|
||||||
|
HR: Month field needs to be required in the "hours spent" report
|
||||||
|
PURCHASE: fixed group purchase order wizard:
|
||||||
|
- if there were orders from several different suppliers, it created a purchase
|
||||||
|
order for only the first supplier but canceled other orders, even those which
|
||||||
|
weren't merged in the created order (closes bugzilla #236)
|
||||||
|
- doesn't trash "manual" lines (ie lines with no product)
|
||||||
|
- pay attentions to unit factors when adding several lines together
|
||||||
|
MRP: fixed workcenter load report (prints only the selected workcenters) and
|
||||||
|
does't crash if the user didn't select all workcenters
|
||||||
|
|
||||||
|
Miscellaneous:
|
||||||
|
Removed pydot from required dependencies
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Server 3.3.0-rc1
|
||||||
|
================
|
||||||
|
|
||||||
|
Changelog for Users
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
New module: OS Commerce
|
||||||
|
Integration with Tiny ERP and OS Commerce
|
||||||
|
Synchronisation 100% automated with eSale;
|
||||||
|
Import of categories of products
|
||||||
|
Export of products (with photos support)
|
||||||
|
Import of Orders (with the eslae module)
|
||||||
|
Export of stock level
|
||||||
|
Import of OSCommerce Taxes
|
||||||
|
Multiple shop allowed with different rules/products
|
||||||
|
Simple Installation
|
||||||
|
|
||||||
|
New Module: HR_TIMESHEET
|
||||||
|
Management by affair, timesheets creates analytic entries in the
|
||||||
|
accounting to get costs and revenue of each affairs. Affairs are
|
||||||
|
structured in trees.
|
||||||
|
|
||||||
|
New Module: Account Follow Up
|
||||||
|
Multi-Level and configurable Follows ups for the accounting module
|
||||||
|
|
||||||
|
New module; Productivity Analysis of users
|
||||||
|
A module to compare productivity of users of Tiny ERP
|
||||||
|
Generic module, you can compare everything (sales, products, partners,
|
||||||
|
...)
|
||||||
|
|
||||||
|
New Modules for localisations:
|
||||||
|
Accounting localisations for be, ca, fr, de, ch, sw
|
||||||
|
Fix: corrected encoding (latin1 to utf8) of Swedish account tree XML file
|
||||||
|
|
||||||
|
New Module - Sandwich
|
||||||
|
Allows employees to order the lunch
|
||||||
|
Keeps employees preferences
|
||||||
|
|
||||||
|
New Module TOOLS:
|
||||||
|
Email automatic importation/integration in the ERP
|
||||||
|
|
||||||
|
New Module EDI:
|
||||||
|
Import of EDI sale orders
|
||||||
|
Export of shippings
|
||||||
|
|
||||||
|
Multi-Company:
|
||||||
|
Tiny ERP is now fully multi-company !
|
||||||
|
New Company and configuration can be made in the client side.
|
||||||
|
|
||||||
|
ACCOUNTING:
|
||||||
|
Better Entries > Standard Entries (Editable Tree, like in Excel)
|
||||||
|
Automatic creation of lines
|
||||||
|
Journal centralised or not
|
||||||
|
Counterpart of lines in one line or one counterpart per entry
|
||||||
|
Analytic accounting recoded from scratch
|
||||||
|
5 new reports
|
||||||
|
Completly integrated with:
|
||||||
|
production,
|
||||||
|
hr_timesheet > Management by affairs
|
||||||
|
sales & purchases,
|
||||||
|
Tasks.
|
||||||
|
Added unreconciliation functionnalities
|
||||||
|
Added account tree fast rendering
|
||||||
|
Better tax computation system supporting worldwide specific countries
|
||||||
|
Better subscription system
|
||||||
|
Wizard to close a period
|
||||||
|
Wizard to clase a fiscal year
|
||||||
|
Very powerfull, simple and complete multi-currency system
|
||||||
|
in pricelists, sale order, purchases, ...
|
||||||
|
Added required fields in currencies (currency code)
|
||||||
|
Added decimal support
|
||||||
|
Better search on accounts (on code, shortcut or name)
|
||||||
|
Added constraint;
|
||||||
|
on users
|
||||||
|
on group
|
||||||
|
on accounts in a journal
|
||||||
|
added menuitem for automatic reconciliation; Multi-Levels
|
||||||
|
added factor to analytic units
|
||||||
|
added form view for budget items dotations
|
||||||
|
made number of digits in quantity field of the budget spread wizard coherent with the object field
|
||||||
|
fixed journal on purchase invoices/refunds (SugarCRM #6)
|
||||||
|
Better bank statement reconciliation
|
||||||
|
Fixed some reports
|
||||||
|
|
||||||
|
STOCK:
|
||||||
|
Better view for location (using localisation of locations; posx, posy, posz)
|
||||||
|
|
||||||
|
MARKETING:
|
||||||
|
fixed small bug when a partner has no adress
|
||||||
|
state field of marketing partner set as readonly
|
||||||
|
fixed marketing steps form view
|
||||||
|
better history view
|
||||||
|
disabled completely send sms wizard
|
||||||
|
fixed send email wizard
|
||||||
|
good priority -> high priority
|
||||||
|
fixed 'call again later' button
|
||||||
|
|
||||||
|
NETWORK:
|
||||||
|
added tree view for login/password
|
||||||
|
|
||||||
|
HR:
|
||||||
|
added holiday_status (=type of ...) to expense claim form view
|
||||||
|
|
||||||
|
BASE (partner):
|
||||||
|
fixed email_send and _email_send methods
|
||||||
|
removed partner without addresses from demo data
|
||||||
|
Added a date field in the partner form
|
||||||
|
|
||||||
|
MRP:
|
||||||
|
New report: workcenter futur loads
|
||||||
|
Analytic entries when production done.
|
||||||
|
SCHEDULER: better error msg in the generated request
|
||||||
|
Allows services in BoMs (for eg, subcontracting)
|
||||||
|
|
||||||
|
Project/Service Management:
|
||||||
|
create orders from tasks; bugfixes
|
||||||
|
Completly integrated with the rest of the ERP
|
||||||
|
Services can now be MTO/MTS, Buy (subcontracting), produce (task), ...
|
||||||
|
Services can be used anywhere (sale.order, bom, ...)
|
||||||
|
See this graph;
|
||||||
|
http://tiny.be/download/flux/flux_procurement.png
|
||||||
|
tasks sorted by ... AND id, so that the order is not random
|
||||||
|
within a priority
|
||||||
|
|
||||||
|
Automatic translations of all wizards
|
||||||
|
|
||||||
|
Scrum Project Management
|
||||||
|
Better Ergonomy; click on a sprint to view tasks
|
||||||
|
Planned, Effetive hours and progress in backlog, project and sprint
|
||||||
|
Better Burndown Chart computation
|
||||||
|
Better (simpler) view of tasks
|
||||||
|
|
||||||
|
Better demo Data
|
||||||
|
In All modules, eth converted to english
|
||||||
|
|
||||||
|
PRODUCT:
|
||||||
|
computing the weight of the packaging
|
||||||
|
Added last order date
|
||||||
|
Alternative suppliers (with delay, prefs, ...) for one product
|
||||||
|
|
||||||
|
PRICELISTS:
|
||||||
|
much more powerfull system
|
||||||
|
views simplified
|
||||||
|
one pricelist per usage: sale, order, pvc
|
||||||
|
price_type on product_view
|
||||||
|
Multi-Currency pricelist (EUR pricelist can depend on a $ one)
|
||||||
|
|
||||||
|
HR-TIMESHEET: fixed bugs in hours report:
|
||||||
|
sum all lines for the same day instead of displaying only the first one
|
||||||
|
it now uses the analytic unit factor, so that mixing hours and days has some sense
|
||||||
|
close cursor
|
||||||
|
|
||||||
|
SALE:
|
||||||
|
invoices generated from a sale order are pre-computed (taxes are computed)
|
||||||
|
|
||||||
|
new invoicing functionnality;
|
||||||
|
invoice on order quantities or,
|
||||||
|
invoice on shipped quantities
|
||||||
|
|
||||||
|
Invoice on a sale.order or a sale.order.line
|
||||||
|
|
||||||
|
added default value for uos_qty in sale order lines (default to 1)
|
||||||
|
|
||||||
|
|
||||||
|
Changelog for Developers
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
New option --debug, that opens a python interpreter when an exception
|
||||||
|
occurs on the server side.
|
||||||
|
|
||||||
|
Better wizard system. Arguements self, cr, uid, context are passed in all
|
||||||
|
functions of the wizard like normal objects. All wizards converted.
|
||||||
|
|
||||||
|
Speed improvements in many views; partners, sale.order, ...
|
||||||
|
less requests from client to server when opening a form
|
||||||
|
|
||||||
|
Better translation system, wizard terms are exported.
|
||||||
|
|
||||||
|
Script to render module dependency graph
|
||||||
|
|
||||||
|
KERNEL+ALL: pass context to methods computing a selection.
|
||||||
|
|
||||||
|
Modification for actions and view definitions:
|
||||||
|
Actions Window:
|
||||||
|
New field: view_mode = 'tree,form' or 'form,tree' -> default='form,tree'
|
||||||
|
New role of view_type: tree (with shortcuts), form (others with switch button)
|
||||||
|
If you need a form that opens in list mode:
|
||||||
|
view_mode = 'tree,form' or 'tree'
|
||||||
|
view_type = form
|
||||||
|
You can define a view in a view (for example sale.order.line in
|
||||||
|
sale.order)
|
||||||
|
less requests on the client side, no need to define 2 views
|
||||||
|
|
||||||
|
Better command-line option message
|
||||||
|
|
||||||
|
Fixed bug which prevented to search for names using non ASCII
|
||||||
|
chars in many2one or many2many fields
|
||||||
|
|
||||||
|
Report Engine: bugfix for concurrency
|
||||||
|
|
||||||
|
Support of SQL constraints
|
||||||
|
Uniq, check, ...
|
||||||
|
Good error message in the client side (check an account entry with
|
||||||
|
credit and debit >0)
|
||||||
|
|
||||||
|
Fixed: when an exception was raised, the cursor wasn't closed and this
|
||||||
|
could cause a freeze in some cases
|
||||||
|
|
||||||
|
Sequence can contains code: %(year)s, ... for prefix, suffix
|
||||||
|
EX: ORDER %(year)/0005
|
||||||
|
|
||||||
|
Bugfixes for automatic migration system
|
||||||
|
|
||||||
|
bugfix on default value with creation of inherits
|
||||||
|
|
||||||
|
Improvement in report_sxw; you can redefine preprocess to do some
|
||||||
|
preprocessing before printing
|
||||||
|
|
||||||
|
Barcode support enabled by default
|
||||||
|
|
||||||
|
Fixed OpenOffice reports when the server is not launched from the
|
||||||
|
directory the code reside
|
||||||
|
|
||||||
|
Print workflow use a pipe instead of using a temporary file (now workflows
|
||||||
|
works on Windows Servers)
|
||||||
|
|
||||||
|
Inheritancy improved (multiple arguments: replace, inside, after, before)
|
||||||
|
|
||||||
|
Lots of small bugfixes
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
Installation Steps
|
||||||
|
------------------
|
||||||
|
|
||||||
|
1. Check that all the required dependencies are installed.
|
||||||
|
|
||||||
|
2. Create a postgresql database.
|
||||||
|
|
||||||
|
The default database name is "terp". If you want to use another name, you
|
||||||
|
will need to provide it when launching the server (by using the commandline
|
||||||
|
option --database).
|
||||||
|
|
||||||
|
To create a postgresql database named "terp" using the following command:
|
||||||
|
$ createdb --encoding=UNICODE terp
|
||||||
|
|
||||||
|
If it is the first time you use postgresql you might need to create a new user
|
||||||
|
to the postgres system using the following commands (where myusername is your
|
||||||
|
unix user name):
|
||||||
|
|
||||||
|
$ su -
|
||||||
|
# su - postgres
|
||||||
|
$ createuser openerp
|
||||||
|
Shall the new user be allowed to create databases? (y/n) y
|
||||||
|
Shall the new user be allowed to create more new users? (y/n) y
|
||||||
|
CREATE USER
|
||||||
|
$ logout
|
||||||
|
# logout
|
||||||
|
|
||||||
|
3. Launch service daemon by "service openerp-server start".
|
||||||
|
|
||||||
|
The first time it is run, the server will initialise the database with all the default values.
|
||||||
|
|
||||||
|
4. Connect to the server using the GUI client.
|
||||||
|
|
||||||
|
There are two accounts by default:
|
||||||
|
* login: admin, password:admin
|
||||||
|
* login: demo, password:demo
|
|
@ -0,0 +1,31 @@
|
||||||
|
Important note for OpenERP build >= 5.0.1-11xrg:
|
||||||
|
|
||||||
|
THE USERNAME HAS CHANGED!!
|
||||||
|
|
||||||
|
Former user "tinyerp" is now called "openerp".
|
||||||
|
|
||||||
|
|
||||||
|
For that, you will have to make sure the following files are chowned
|
||||||
|
to the new user:
|
||||||
|
|
||||||
|
/var/log/openerp
|
||||||
|
/var/spool/openerp
|
||||||
|
/var/run/openerp
|
||||||
|
/etc/openerp
|
||||||
|
/etc/openerp/cert.cfg
|
||||||
|
/etc/openerp-server.conf
|
||||||
|
/etc/logrotate.d/openerp-server
|
||||||
|
|
||||||
|
Then, rename the user in the postgres database:
|
||||||
|
|
||||||
|
psql -U postgres postgres
|
||||||
|
|
||||||
|
ALTER ROLE tinyerp RENAME TO openerp;
|
||||||
|
|
||||||
|
Then, edit your openerp-server.conf to depict the change:
|
||||||
|
- db_user = tinyerp
|
||||||
|
+ db_user = openerp
|
||||||
|
|
||||||
|
Good luck!
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
[options]
|
||||||
|
without_demo = True
|
||||||
|
; This is the password that allows database operations:
|
||||||
|
; admin_passwd = admin
|
||||||
|
upgrade = False
|
||||||
|
verbose = False
|
||||||
|
netrpc = True
|
||||||
|
xmlrpc = True
|
||||||
|
port = 8069
|
||||||
|
interface =
|
||||||
|
db_host = False
|
||||||
|
db_port = False
|
||||||
|
; Please uncomment the following line *after* you have created the
|
||||||
|
; database. It activates the auto module check on startup.
|
||||||
|
; db_name = terp
|
||||||
|
db_user = openerp
|
||||||
|
db_password = False
|
||||||
|
; Uncomment these for xml-rpc over SSL
|
||||||
|
; secure = True
|
||||||
|
; secure_cert_file = /etc/openerp/server.cert
|
||||||
|
; secure_pkey_file = /etc/openerp/server.key
|
||||||
|
root_path = None
|
||||||
|
soap = False
|
||||||
|
translate_modules = ['all']
|
||||||
|
demo = {}
|
||||||
|
addons_path = None
|
||||||
|
reportgz = False
|
|
@ -0,0 +1,147 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# openerp-server This shell script takes care of starting and stopping
|
||||||
|
# OpenERP server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 95 05
|
||||||
|
# description: OpenERP server
|
||||||
|
#
|
||||||
|
# pidfile: /var/run/openerp-server.pid
|
||||||
|
# config: /etc/openerp-server.conf
|
||||||
|
|
||||||
|
### BEGIN INIT INFO
|
||||||
|
# Provides: openerp-server
|
||||||
|
# Required-Start: postgresql
|
||||||
|
# Required-Stop: postgresql
|
||||||
|
# Should-Start: $network harddrake
|
||||||
|
# Default-Start: 345
|
||||||
|
# Short-Description: Launches the OpenERP server.
|
||||||
|
# Description: This startup script launches the OpenERP server.
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
# Source function library.
|
||||||
|
. /etc/rc.d/init.d/functions
|
||||||
|
|
||||||
|
PIDFILE=/var/run/openerp/openerp-server.pid
|
||||||
|
LOCKFILE=/var/lock/subsys/openerp-server
|
||||||
|
LOGFILE=/var/log/openerp/openerp-server.log
|
||||||
|
|
||||||
|
OPTS="--pidfile=$PIDFILE --logfile=$LOGFILE"
|
||||||
|
|
||||||
|
prog="openerp-server"
|
||||||
|
desc="OpenERP Server Daemon"
|
||||||
|
|
||||||
|
# check if the openerp-server conf file is present, then use it
|
||||||
|
if [ -f /etc/openerp-server.conf ]; then
|
||||||
|
OPTS="$OPTS -c /etc/openerp-server.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source function library
|
||||||
|
if [ -f /etc/init.d/functions ] ; then
|
||||||
|
. /etc/init.d/functions
|
||||||
|
elif [ -f /etc/rc.d/init.d/functions ] ; then
|
||||||
|
. /etc/rc.d/init.d/functions
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check the existence of the openerp-server script
|
||||||
|
[ -z "/usr/bin/openerp-server" ] && exit 0
|
||||||
|
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if [ -d /etc/openerp/start.d ] ; then
|
||||||
|
echo -n $"Preparing $desc: "
|
||||||
|
run-parts --exit-on-error /etc/openerp/start.d
|
||||||
|
RETVAL=$?
|
||||||
|
echo
|
||||||
|
[ $RETVAL -ne 0 ] && return $RETVAL
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n $"Starting $desc ($prog): "
|
||||||
|
daemon --user openerp --check openerp-server \
|
||||||
|
"/usr/bin/setsid /usr/bin/openerp-server \
|
||||||
|
-c /etc/openerp-server.conf \
|
||||||
|
--pidfile=$PIDFILE \
|
||||||
|
--logfile=$LOGFILE &"
|
||||||
|
|
||||||
|
RETVAL=$?
|
||||||
|
echo
|
||||||
|
[ $RETVAL -eq 0 ] && touch $LOCKFILE
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping $desc ($prog): "
|
||||||
|
kill -TERM `cat $PIDFILE` > /dev/null 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
rm -f $LOCKFILE
|
||||||
|
|
||||||
|
echo_success
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo_failure
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
if [ -d /etc/openerp/stop.d ] ; then
|
||||||
|
echo -n $"Clearing $desc: "
|
||||||
|
run-parts /etc/openerp/stop.d
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
condrestart() {
|
||||||
|
[ -e $LOCKFILE ] && restart || :
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
if [ -f $PIDFILE ] ; then
|
||||||
|
checkpid `cat $PIDFILE`
|
||||||
|
RETVAL=$?
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
echo $"$prog is running..."
|
||||||
|
else
|
||||||
|
echo $"$prog is stopped"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo $"$prog is stopped"
|
||||||
|
fi
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart|reload)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
|
||||||
|
condrestart)
|
||||||
|
condrestart
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
|
||||||
|
probe)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 {start|stop|status|restart|condrestart|reload}"
|
||||||
|
exit 1
|
||||||
|
esac
|
|
@ -0,0 +1,5 @@
|
||||||
|
/var/log/openerp/*.log {
|
||||||
|
copytruncate
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ADMIN_PASSWD='admin'
|
||||||
|
method_1() {
|
||||||
|
cat '-' << EOF
|
||||||
|
<xml>
|
||||||
|
<methodCall>
|
||||||
|
<methodName>get_stats</methodName>
|
||||||
|
<params>
|
||||||
|
</params>
|
||||||
|
</methodCall>
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
LEVEL=10
|
||||||
|
|
||||||
|
if [ -n "$1" ] ; then LEVEL=$1 ; fi
|
||||||
|
|
||||||
|
method_1 $LEVEL | POST -c 'text/xml' http://localhost:8069/xmlrpc/common
|
||||||
|
#eof
|
|
@ -0,0 +1,587 @@
|
||||||
|
"""HTTP server base class.
|
||||||
|
|
||||||
|
Note: the class in this module doesn't implement any HTTP request; see
|
||||||
|
SimpleHTTPServer for simple implementations of GET, HEAD and POST
|
||||||
|
(including CGI scripts). It does, however, optionally implement HTTP/1.1
|
||||||
|
persistent connections, as of version 0.3.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
- BaseHTTPRequestHandler: HTTP request handler base class
|
||||||
|
- test: test function
|
||||||
|
|
||||||
|
XXX To do:
|
||||||
|
|
||||||
|
- log requests even later (to capture byte count)
|
||||||
|
- log user-agent header and other interesting goodies
|
||||||
|
- send error log to separate file
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# See also:
|
||||||
|
#
|
||||||
|
# HTTP Working Group T. Berners-Lee
|
||||||
|
# INTERNET-DRAFT R. T. Fielding
|
||||||
|
# <draft-ietf-http-v10-spec-00.txt> H. Frystyk Nielsen
|
||||||
|
# Expires September 8, 1995 March 8, 1995
|
||||||
|
#
|
||||||
|
# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt
|
||||||
|
#
|
||||||
|
# and
|
||||||
|
#
|
||||||
|
# Network Working Group R. Fielding
|
||||||
|
# Request for Comments: 2616 et al
|
||||||
|
# Obsoletes: 2068 June 1999
|
||||||
|
# Category: Standards Track
|
||||||
|
#
|
||||||
|
# URL: http://www.faqs.org/rfcs/rfc2616.html
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
# ---------
|
||||||
|
#
|
||||||
|
# Here's a quote from the NCSA httpd docs about log file format.
|
||||||
|
#
|
||||||
|
# | The logfile format is as follows. Each line consists of:
|
||||||
|
# |
|
||||||
|
# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb
|
||||||
|
# |
|
||||||
|
# | host: Either the DNS name or the IP number of the remote client
|
||||||
|
# | rfc931: Any information returned by identd for this person,
|
||||||
|
# | - otherwise.
|
||||||
|
# | authuser: If user sent a userid for authentication, the user name,
|
||||||
|
# | - otherwise.
|
||||||
|
# | DD: Day
|
||||||
|
# | Mon: Month (calendar name)
|
||||||
|
# | YYYY: Year
|
||||||
|
# | hh: hour (24-hour format, the machine's timezone)
|
||||||
|
# | mm: minutes
|
||||||
|
# | ss: seconds
|
||||||
|
# | request: The first line of the HTTP request as sent by the client.
|
||||||
|
# | ddd: the status code returned by the server, - if not available.
|
||||||
|
# | bbbb: the total number of bytes sent,
|
||||||
|
# | *not including the HTTP/1.0 header*, - if not available
|
||||||
|
# |
|
||||||
|
# | You can determine the name of the file accessed through request.
|
||||||
|
#
|
||||||
|
# (Actually, the latter is only true if you know the server configuration
|
||||||
|
# at the time the request was made!)
|
||||||
|
|
||||||
|
__version__ = "0.3"
|
||||||
|
|
||||||
|
__all__ = ["HTTPServer", "BaseHTTPRequestHandler"]
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import socket # For gethostbyaddr()
|
||||||
|
import mimetools
|
||||||
|
import SocketServer
|
||||||
|
|
||||||
|
# Default error message template
|
||||||
|
DEFAULT_ERROR_MESSAGE = """\
|
||||||
|
<head>
|
||||||
|
<title>Error response</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Error response</h1>
|
||||||
|
<p>Error code %(code)d.
|
||||||
|
<p>Message: %(message)s.
|
||||||
|
<p>Error code explanation: %(code)s = %(explain)s.
|
||||||
|
</body>
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_ERROR_CONTENT_TYPE = "text/html"
|
||||||
|
|
||||||
|
def _quote_html(html):
|
||||||
|
return html.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
class HTTPServer(SocketServer.TCPServer):
|
||||||
|
|
||||||
|
allow_reuse_address = 1 # Seems to make sense in testing environment
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
"""Override server_bind to store the server name."""
|
||||||
|
SocketServer.TCPServer.server_bind(self)
|
||||||
|
host, port = self.socket.getsockname()[:2]
|
||||||
|
self.server_name = socket.getfqdn(host)
|
||||||
|
self.server_port = port
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler):
|
||||||
|
|
||||||
|
"""HTTP request handler base class.
|
||||||
|
|
||||||
|
The following explanation of HTTP serves to guide you through the
|
||||||
|
code as well as to expose any misunderstandings I may have about
|
||||||
|
HTTP (so you don't need to read the code to figure out I'm wrong
|
||||||
|
:-).
|
||||||
|
|
||||||
|
HTTP (HyperText Transfer Protocol) is an extensible protocol on
|
||||||
|
top of a reliable stream transport (e.g. TCP/IP). The protocol
|
||||||
|
recognizes three parts to a request:
|
||||||
|
|
||||||
|
1. One line identifying the request type and path
|
||||||
|
2. An optional set of RFC-822-style headers
|
||||||
|
3. An optional data part
|
||||||
|
|
||||||
|
The headers and data are separated by a blank line.
|
||||||
|
|
||||||
|
The first line of the request has the form
|
||||||
|
|
||||||
|
<command> <path> <version>
|
||||||
|
|
||||||
|
where <command> is a (case-sensitive) keyword such as GET or POST,
|
||||||
|
<path> is a string containing path information for the request,
|
||||||
|
and <version> should be the string "HTTP/1.0" or "HTTP/1.1".
|
||||||
|
<path> is encoded using the URL encoding scheme (using %xx to signify
|
||||||
|
the ASCII character with hex code xx).
|
||||||
|
|
||||||
|
The specification specifies that lines are separated by CRLF but
|
||||||
|
for compatibility with the widest range of clients recommends
|
||||||
|
servers also handle LF. Similarly, whitespace in the request line
|
||||||
|
is treated sensibly (allowing multiple spaces between components
|
||||||
|
and allowing trailing whitespace).
|
||||||
|
|
||||||
|
Similarly, for output, lines ought to be separated by CRLF pairs
|
||||||
|
but most clients grok LF characters just fine.
|
||||||
|
|
||||||
|
If the first line of the request has the form
|
||||||
|
|
||||||
|
<command> <path>
|
||||||
|
|
||||||
|
(i.e. <version> is left out) then this is assumed to be an HTTP
|
||||||
|
0.9 request; this form has no optional headers and data part and
|
||||||
|
the reply consists of just the data.
|
||||||
|
|
||||||
|
The reply form of the HTTP 1.x protocol again has three parts:
|
||||||
|
|
||||||
|
1. One line giving the response code
|
||||||
|
2. An optional set of RFC-822-style headers
|
||||||
|
3. The data
|
||||||
|
|
||||||
|
Again, the headers and data are separated by a blank line.
|
||||||
|
|
||||||
|
The response code line has the form
|
||||||
|
|
||||||
|
<version> <responsecode> <responsestring>
|
||||||
|
|
||||||
|
where <version> is the protocol version ("HTTP/1.0" or "HTTP/1.1"),
|
||||||
|
<responsecode> is a 3-digit response code indicating success or
|
||||||
|
failure of the request, and <responsestring> is an optional
|
||||||
|
human-readable string explaining what the response code means.
|
||||||
|
|
||||||
|
This server parses the request and the headers, and then calls a
|
||||||
|
function specific to the request type (<command>). Specifically,
|
||||||
|
a request SPAM will be handled by a method do_SPAM(). If no
|
||||||
|
such method exists the server sends an error response to the
|
||||||
|
client. If it exists, it is called with no arguments:
|
||||||
|
|
||||||
|
do_SPAM()
|
||||||
|
|
||||||
|
Note that the request name is case sensitive (i.e. SPAM and spam
|
||||||
|
are different requests).
|
||||||
|
|
||||||
|
The various request details are stored in instance variables:
|
||||||
|
|
||||||
|
- client_address is the client IP address in the form (host,
|
||||||
|
port);
|
||||||
|
|
||||||
|
- command, path and version are the broken-down request line;
|
||||||
|
|
||||||
|
- headers is an instance of mimetools.Message (or a derived
|
||||||
|
class) containing the header information;
|
||||||
|
|
||||||
|
- rfile is a file object open for reading positioned at the
|
||||||
|
start of the optional input data part;
|
||||||
|
|
||||||
|
- wfile is a file object open for writing.
|
||||||
|
|
||||||
|
IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING!
|
||||||
|
|
||||||
|
The first thing to be written must be the response line. Then
|
||||||
|
follow 0 or more header lines, then a blank line, and then the
|
||||||
|
actual data (if any). The meaning of the header lines depends on
|
||||||
|
the command executed by the server; in most cases, when data is
|
||||||
|
returned, there should be at least one header line of the form
|
||||||
|
|
||||||
|
Content-type: <type>/<subtype>
|
||||||
|
|
||||||
|
where <type> and <subtype> should be registered MIME types,
|
||||||
|
e.g. "text/html" or "text/plain".
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The Python system version, truncated to its first component.
|
||||||
|
sys_version = "Python/" + sys.version.split()[0]
|
||||||
|
|
||||||
|
# The server software version. You may want to override this.
|
||||||
|
# The format is multiple whitespace-separated strings,
|
||||||
|
# where each string is of the form name[/version].
|
||||||
|
server_version = "BaseHTTP/" + __version__
|
||||||
|
|
||||||
|
# The default request version. This only affects responses up until
|
||||||
|
# the point where the request line is parsed, so it mainly decides what
|
||||||
|
# the client gets back when sending a malformed request line.
|
||||||
|
# Most web servers default to HTTP 0.9, i.e. don't send a status line.
|
||||||
|
default_request_version = "HTTP/0.9"
|
||||||
|
|
||||||
|
def parse_request(self):
|
||||||
|
"""Parse a request (internal).
|
||||||
|
|
||||||
|
The request should be stored in self.raw_requestline; the results
|
||||||
|
are in self.command, self.path, self.request_version and
|
||||||
|
self.headers.
|
||||||
|
|
||||||
|
Return True for success, False for failure; on failure, an
|
||||||
|
error is sent back.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.command = None # set in case of error on the first line
|
||||||
|
self.request_version = version = self.default_request_version
|
||||||
|
self.close_connection = 1
|
||||||
|
requestline = self.raw_requestline
|
||||||
|
if requestline[-2:] == '\r\n':
|
||||||
|
requestline = requestline[:-2]
|
||||||
|
elif requestline[-1:] == '\n':
|
||||||
|
requestline = requestline[:-1]
|
||||||
|
self.requestline = requestline
|
||||||
|
words = requestline.split()
|
||||||
|
if len(words) == 3:
|
||||||
|
[command, path, version] = words
|
||||||
|
if version[:5] != 'HTTP/':
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
base_version_number = version.split('/', 1)[1]
|
||||||
|
version_number = base_version_number.split(".")
|
||||||
|
# RFC 2145 section 3.1 says there can be only one "." and
|
||||||
|
# - major and minor numbers MUST be treated as
|
||||||
|
# separate integers;
|
||||||
|
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
|
||||||
|
# turn is lower than HTTP/12.3;
|
||||||
|
# - Leading zeros MUST be ignored by recipients.
|
||||||
|
if len(version_number) != 2:
|
||||||
|
raise ValueError
|
||||||
|
version_number = int(version_number[0]), int(version_number[1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
self.send_error(400, "Bad request version (%r)" % version)
|
||||||
|
return False
|
||||||
|
if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1":
|
||||||
|
self.close_connection = 0
|
||||||
|
if version_number >= (2, 0):
|
||||||
|
self.send_error(505,
|
||||||
|
"Invalid HTTP Version (%s)" % base_version_number)
|
||||||
|
return False
|
||||||
|
elif len(words) == 2:
|
||||||
|
[command, path] = words
|
||||||
|
self.close_connection = 1
|
||||||
|
if command != 'GET':
|
||||||
|
self.send_error(400,
|
||||||
|
"Bad HTTP/0.9 request type (%r)" % command)
|
||||||
|
return False
|
||||||
|
elif not words:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.send_error(400, "Bad request syntax (%r)" % requestline)
|
||||||
|
return False
|
||||||
|
self.command, self.path, self.request_version = command, path, version
|
||||||
|
|
||||||
|
# Examine the headers and look for a Connection directive
|
||||||
|
self.headers = self.MessageClass(self.rfile, 0)
|
||||||
|
|
||||||
|
conntype = self.headers.get('Connection', "")
|
||||||
|
if conntype.lower() == 'close':
|
||||||
|
self.close_connection = 1
|
||||||
|
elif (conntype.lower() == 'keep-alive' and
|
||||||
|
self.protocol_version >= "HTTP/1.1"):
|
||||||
|
self.close_connection = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_one_request(self):
|
||||||
|
"""Handle a single HTTP request.
|
||||||
|
|
||||||
|
You normally don't need to override this method; see the class
|
||||||
|
__doc__ string for information on how to handle specific HTTP
|
||||||
|
commands such as GET and POST.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.raw_requestline = self.rfile.readline()
|
||||||
|
if not self.raw_requestline:
|
||||||
|
self.close_connection = 1
|
||||||
|
return
|
||||||
|
if not self.parse_request(): # An error code has been sent, just exit
|
||||||
|
return
|
||||||
|
mname = 'do_' + self.command
|
||||||
|
if not hasattr(self, mname):
|
||||||
|
self.send_error(501, "Unsupported method (%r)" % self.command)
|
||||||
|
return
|
||||||
|
method = getattr(self, mname)
|
||||||
|
method()
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
"""Handle multiple requests if necessary."""
|
||||||
|
self.close_connection = 1
|
||||||
|
|
||||||
|
self.handle_one_request()
|
||||||
|
while not self.close_connection:
|
||||||
|
self.handle_one_request()
|
||||||
|
|
||||||
|
def send_error(self, code, message=None):
|
||||||
|
"""Send and log an error reply.
|
||||||
|
|
||||||
|
Arguments are the error code, and a detailed message.
|
||||||
|
The detailed message defaults to the short entry matching the
|
||||||
|
response code.
|
||||||
|
|
||||||
|
This sends an error response (so it must be called before any
|
||||||
|
output has been generated), logs the error, and finally sends
|
||||||
|
a piece of HTML explaining the error to the user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
short, long = self.responses[code]
|
||||||
|
except KeyError:
|
||||||
|
short, long = '???', '???'
|
||||||
|
if message is None:
|
||||||
|
message = short
|
||||||
|
explain = long
|
||||||
|
self.log_error("code %d, message %s", code, message)
|
||||||
|
# using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
|
||||||
|
content = (self.error_message_format %
|
||||||
|
{'code': code, 'message': _quote_html(message), 'explain': explain})
|
||||||
|
self.send_response(code, message)
|
||||||
|
self.send_header("Content-Type", self.error_content_type)
|
||||||
|
self.send_header('Connection', 'close')
|
||||||
|
self.end_headers()
|
||||||
|
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
error_message_format = DEFAULT_ERROR_MESSAGE
|
||||||
|
error_content_type = DEFAULT_ERROR_CONTENT_TYPE
|
||||||
|
|
||||||
|
def send_response(self, code, message=None):
|
||||||
|
"""Send the response header and log the response code.
|
||||||
|
|
||||||
|
Also send two standard headers with the server software
|
||||||
|
version and the current date.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.log_request(code)
|
||||||
|
if message is None:
|
||||||
|
if code in self.responses:
|
||||||
|
message = self.responses[code][0]
|
||||||
|
else:
|
||||||
|
message = ''
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("%s %d %s\r\n" %
|
||||||
|
(self.protocol_version, code, message))
|
||||||
|
# print (self.protocol_version, code, message)
|
||||||
|
self.send_header('Server', self.version_string())
|
||||||
|
self.send_header('Date', self.date_time_string())
|
||||||
|
|
||||||
|
def send_header(self, keyword, value):
|
||||||
|
"""Send a MIME header."""
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("%s: %s\r\n" % (keyword, value))
|
||||||
|
|
||||||
|
if keyword.lower() == 'connection':
|
||||||
|
if value.lower() == 'close':
|
||||||
|
self.close_connection = 1
|
||||||
|
elif value.lower() == 'keep-alive':
|
||||||
|
self.close_connection = 0
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
"""Send the blank line ending the MIME headers."""
|
||||||
|
if self.request_version != 'HTTP/0.9':
|
||||||
|
self.wfile.write("\r\n")
|
||||||
|
|
||||||
|
def log_request(self, code='-', size='-'):
|
||||||
|
"""Log an accepted request.
|
||||||
|
|
||||||
|
This is called by send_response().
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.log_message('"%s" %s %s',
|
||||||
|
self.requestline, str(code), str(size))
|
||||||
|
|
||||||
|
def log_error(self, format, *args):
|
||||||
|
"""Log an error.
|
||||||
|
|
||||||
|
This is called when a request cannot be fulfilled. By
|
||||||
|
default it passes the message on to log_message().
|
||||||
|
|
||||||
|
Arguments are the same as for log_message().
|
||||||
|
|
||||||
|
XXX This should go to the separate error log.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.log_message(format, *args)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""Log an arbitrary message.
|
||||||
|
|
||||||
|
This is used by all other logging functions. Override
|
||||||
|
it if you have specific logging wishes.
|
||||||
|
|
||||||
|
The first argument, FORMAT, is a format string for the
|
||||||
|
message to be logged. If the format string contains
|
||||||
|
any % escapes requiring parameters, they should be
|
||||||
|
specified as subsequent arguments (it's just like
|
||||||
|
printf!).
|
||||||
|
|
||||||
|
The client host and current date/time are prefixed to
|
||||||
|
every message.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
sys.stderr.write("%s - - [%s] %s\n" %
|
||||||
|
(self.address_string(),
|
||||||
|
self.log_date_time_string(),
|
||||||
|
format%args))
|
||||||
|
|
||||||
|
def version_string(self):
|
||||||
|
"""Return the server software version string."""
|
||||||
|
return self.server_version + ' ' + self.sys_version
|
||||||
|
|
||||||
|
def date_time_string(self, timestamp=None):
|
||||||
|
"""Return the current date and time formatted for a message header."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = time.time()
|
||||||
|
year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
|
||||||
|
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
|
||||||
|
self.weekdayname[wd],
|
||||||
|
day, self.monthname[month], year,
|
||||||
|
hh, mm, ss)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def log_date_time_string(self):
|
||||||
|
"""Return the current time formatted for logging."""
|
||||||
|
now = time.time()
|
||||||
|
year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
|
||||||
|
s = "%02d/%3s/%04d %02d:%02d:%02d" % (
|
||||||
|
day, self.monthname[month], year, hh, mm, ss)
|
||||||
|
return s
|
||||||
|
|
||||||
|
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||||
|
|
||||||
|
monthname = [None,
|
||||||
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||||
|
|
||||||
|
def address_string(self):
|
||||||
|
"""Return the client address formatted for logging.
|
||||||
|
|
||||||
|
This version looks up the full hostname using gethostbyaddr(),
|
||||||
|
and tries to find a name that contains at least one dot.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
host, port = self.client_address[:2]
|
||||||
|
return socket.getfqdn(host)
|
||||||
|
|
||||||
|
# Essentially static class variables
|
||||||
|
|
||||||
|
# The version of the HTTP protocol we support.
|
||||||
|
# Set this to HTTP/1.1 to enable automatic keepalive
|
||||||
|
protocol_version = "HTTP/1.0"
|
||||||
|
|
||||||
|
# The Message-like class used to parse headers
|
||||||
|
MessageClass = mimetools.Message
|
||||||
|
|
||||||
|
# Table mapping response codes to messages; entries have the
|
||||||
|
# form {code: (shortmessage, longmessage)}.
|
||||||
|
# See RFC 2616.
|
||||||
|
responses = {
|
||||||
|
100: ('Continue', 'Request received, please continue'),
|
||||||
|
101: ('Switching Protocols',
|
||||||
|
'Switching to new protocol; obey Upgrade header'),
|
||||||
|
|
||||||
|
200: ('OK', 'Request fulfilled, document follows'),
|
||||||
|
201: ('Created', 'Document created, URL follows'),
|
||||||
|
202: ('Accepted',
|
||||||
|
'Request accepted, processing continues off-line'),
|
||||||
|
203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
|
||||||
|
204: ('No Content', 'Request fulfilled, nothing follows'),
|
||||||
|
205: ('Reset Content', 'Clear input form for further input.'),
|
||||||
|
206: ('Partial Content', 'Partial content follows.'),
|
||||||
|
|
||||||
|
300: ('Multiple Choices',
|
||||||
|
'Object has several resources -- see URI list'),
|
||||||
|
301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
|
||||||
|
302: ('Found', 'Object moved temporarily -- see URI list'),
|
||||||
|
303: ('See Other', 'Object moved -- see Method and URL list'),
|
||||||
|
304: ('Not Modified',
|
||||||
|
'Document has not changed since given time'),
|
||||||
|
305: ('Use Proxy',
|
||||||
|
'You must use proxy specified in Location to access this '
|
||||||
|
'resource.'),
|
||||||
|
307: ('Temporary Redirect',
|
||||||
|
'Object moved temporarily -- see URI list'),
|
||||||
|
|
||||||
|
400: ('Bad Request',
|
||||||
|
'Bad request syntax or unsupported method'),
|
||||||
|
401: ('Unauthorized',
|
||||||
|
'No permission -- see authorization schemes'),
|
||||||
|
402: ('Payment Required',
|
||||||
|
'No payment -- see charging schemes'),
|
||||||
|
403: ('Forbidden',
|
||||||
|
'Request forbidden -- authorization will not help'),
|
||||||
|
404: ('Not Found', 'Nothing matches the given URI'),
|
||||||
|
405: ('Method Not Allowed',
|
||||||
|
'Specified method is invalid for this server.'),
|
||||||
|
406: ('Not Acceptable', 'URI not available in preferred format.'),
|
||||||
|
407: ('Proxy Authentication Required', 'You must authenticate with '
|
||||||
|
'this proxy before proceeding.'),
|
||||||
|
408: ('Request Timeout', 'Request timed out; try again later.'),
|
||||||
|
409: ('Conflict', 'Request conflict.'),
|
||||||
|
410: ('Gone',
|
||||||
|
'URI no longer exists and has been permanently removed.'),
|
||||||
|
411: ('Length Required', 'Client must specify Content-Length.'),
|
||||||
|
412: ('Precondition Failed', 'Precondition in headers is false.'),
|
||||||
|
413: ('Request Entity Too Large', 'Entity is too large.'),
|
||||||
|
414: ('Request-URI Too Long', 'URI is too long.'),
|
||||||
|
415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
|
||||||
|
416: ('Requested Range Not Satisfiable',
|
||||||
|
'Cannot satisfy request range.'),
|
||||||
|
417: ('Expectation Failed',
|
||||||
|
'Expect condition could not be satisfied.'),
|
||||||
|
|
||||||
|
500: ('Internal Server Error', 'Server got itself in trouble'),
|
||||||
|
501: ('Not Implemented',
|
||||||
|
'Server does not support this operation'),
|
||||||
|
502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
|
||||||
|
503: ('Service Unavailable',
|
||||||
|
'The server cannot process the request due to a high load'),
|
||||||
|
504: ('Gateway Timeout',
|
||||||
|
'The gateway server did not receive a timely response'),
|
||||||
|
505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test(HandlerClass = BaseHTTPRequestHandler,
|
||||||
|
ServerClass = HTTPServer, protocol="HTTP/1.0"):
|
||||||
|
"""Test the HTTP request handler class.
|
||||||
|
|
||||||
|
This runs an HTTP server on port 8000 (or the first command line
|
||||||
|
argument).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if sys.argv[1:]:
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
else:
|
||||||
|
port = 8000
|
||||||
|
server_address = ('', port)
|
||||||
|
|
||||||
|
HandlerClass.protocol_version = protocol
|
||||||
|
httpd = ServerClass(server_address, HandlerClass)
|
||||||
|
|
||||||
|
sa = httpd.socket.getsockname()
|
||||||
|
print "Serving HTTP on", sa[0], "port", sa[1], "..."
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
|
@ -0,0 +1,611 @@
|
||||||
|
"""Simple XML-RPC Server.
|
||||||
|
|
||||||
|
This module can be used to create simple XML-RPC servers
|
||||||
|
by creating a server and either installing functions, a
|
||||||
|
class instance, or by extending the SimpleXMLRPCServer
|
||||||
|
class.
|
||||||
|
|
||||||
|
It can also be used to handle XML-RPC requests in a CGI
|
||||||
|
environment using CGIXMLRPCRequestHandler.
|
||||||
|
|
||||||
|
A list of possible usage patterns follows:
|
||||||
|
|
||||||
|
1. Install functions:
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_function(pow)
|
||||||
|
server.register_function(lambda x,y: x+y, 'add')
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
2. Install an instance:
|
||||||
|
|
||||||
|
class MyFuncs:
|
||||||
|
def __init__(self):
|
||||||
|
# make all of the string functions available through
|
||||||
|
# string.func_name
|
||||||
|
import string
|
||||||
|
self.string = string
|
||||||
|
def _listMethods(self):
|
||||||
|
# implement this method so that system.listMethods
|
||||||
|
# knows to advertise the strings methods
|
||||||
|
return list_public_methods(self) + \
|
||||||
|
['string.' + method for method in list_public_methods(self.string)]
|
||||||
|
def pow(self, x, y): return pow(x, y)
|
||||||
|
def add(self, x, y) : return x + y
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_introspection_functions()
|
||||||
|
server.register_instance(MyFuncs())
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
3. Install an instance with custom dispatch method:
|
||||||
|
|
||||||
|
class Math:
|
||||||
|
def _listMethods(self):
|
||||||
|
# this method must be present for system.listMethods
|
||||||
|
# to work
|
||||||
|
return ['add', 'pow']
|
||||||
|
def _methodHelp(self, method):
|
||||||
|
# this method must be present for system.methodHelp
|
||||||
|
# to work
|
||||||
|
if method == 'add':
|
||||||
|
return "add(2,3) => 5"
|
||||||
|
elif method == 'pow':
|
||||||
|
return "pow(x, y[, z]) => number"
|
||||||
|
else:
|
||||||
|
# By convention, return empty
|
||||||
|
# string if no help is available
|
||||||
|
return ""
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
if method == 'pow':
|
||||||
|
return pow(*params)
|
||||||
|
elif method == 'add':
|
||||||
|
return params[0] + params[1]
|
||||||
|
else:
|
||||||
|
raise 'bad method'
|
||||||
|
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_introspection_functions()
|
||||||
|
server.register_instance(Math())
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
4. Subclass SimpleXMLRPCServer:
|
||||||
|
|
||||||
|
class MathServer(SimpleXMLRPCServer):
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
try:
|
||||||
|
# We are forcing the 'export_' prefix on methods that are
|
||||||
|
# callable through XML-RPC to prevent potential security
|
||||||
|
# problems
|
||||||
|
func = getattr(self, 'export_' + method)
|
||||||
|
except AttributeError:
|
||||||
|
raise Exception('method "%s" is not supported' % method)
|
||||||
|
else:
|
||||||
|
return func(*params)
|
||||||
|
|
||||||
|
def export_add(self, x, y):
|
||||||
|
return x + y
|
||||||
|
|
||||||
|
server = MathServer(("localhost", 8000))
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
5. CGI script:
|
||||||
|
|
||||||
|
server = CGIXMLRPCRequestHandler()
|
||||||
|
server.register_function(pow)
|
||||||
|
server.handle_request()
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Written by Brian Quinlan (brian@sweetapp.com).
|
||||||
|
# Based on code written by Fredrik Lundh.
|
||||||
|
|
||||||
|
import xmlrpclib
|
||||||
|
from xmlrpclib import Fault
|
||||||
|
import SocketServer
|
||||||
|
import BaseHTTPServer
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
except ImportError:
|
||||||
|
fcntl = None
|
||||||
|
|
||||||
|
def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
|
||||||
|
"""resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
|
||||||
|
|
||||||
|
Resolves a dotted attribute name to an object. Raises
|
||||||
|
an AttributeError if any attribute in the chain starts with a '_'.
|
||||||
|
|
||||||
|
If the optional allow_dotted_names argument is false, dots are not
|
||||||
|
supported and this function operates similar to getattr(obj, attr).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if allow_dotted_names:
|
||||||
|
attrs = attr.split('.')
|
||||||
|
else:
|
||||||
|
attrs = [attr]
|
||||||
|
|
||||||
|
for i in attrs:
|
||||||
|
if i.startswith('_'):
|
||||||
|
raise AttributeError(
|
||||||
|
'attempt to access private attribute "%s"' % i
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
obj = getattr(obj,i)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def list_public_methods(obj):
|
||||||
|
"""Returns a list of attribute strings, found in the specified
|
||||||
|
object, which represent callable attributes"""
|
||||||
|
|
||||||
|
return [member for member in dir(obj)
|
||||||
|
if not member.startswith('_') and
|
||||||
|
hasattr(getattr(obj, member), '__call__')]
|
||||||
|
|
||||||
|
def remove_duplicates(lst):
|
||||||
|
"""remove_duplicates([2,2,2,1,3,3]) => [3,1,2]
|
||||||
|
|
||||||
|
Returns a copy of a list without duplicates. Every list
|
||||||
|
item must be hashable and the order of the items in the
|
||||||
|
resulting list is not defined.
|
||||||
|
"""
|
||||||
|
u = {}
|
||||||
|
for x in lst:
|
||||||
|
u[x] = 1
|
||||||
|
|
||||||
|
return u.keys()
|
||||||
|
|
||||||
|
class SimpleXMLRPCDispatcher:
|
||||||
|
"""Mix-in class that dispatches XML-RPC requests.
|
||||||
|
|
||||||
|
This class is used to register XML-RPC method handlers
|
||||||
|
and then to dispatch them. There should never be any
|
||||||
|
reason to instantiate this class directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, allow_none, encoding):
|
||||||
|
self.funcs = {}
|
||||||
|
self.instance = None
|
||||||
|
self.allow_none = allow_none
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def register_instance(self, instance, allow_dotted_names=False):
|
||||||
|
"""Registers an instance to respond to XML-RPC requests.
|
||||||
|
|
||||||
|
Only one instance can be installed at a time.
|
||||||
|
|
||||||
|
If the registered instance has a _dispatch method then that
|
||||||
|
method will be called with the name of the XML-RPC method and
|
||||||
|
its parameters as a tuple
|
||||||
|
e.g. instance._dispatch('add',(2,3))
|
||||||
|
|
||||||
|
If the registered instance does not have a _dispatch method
|
||||||
|
then the instance will be searched to find a matching method
|
||||||
|
and, if found, will be called. Methods beginning with an '_'
|
||||||
|
are considered private and will not be called by
|
||||||
|
SimpleXMLRPCServer.
|
||||||
|
|
||||||
|
If a registered function matches a XML-RPC request, then it
|
||||||
|
will be called instead of the registered instance.
|
||||||
|
|
||||||
|
If the optional allow_dotted_names argument is true and the
|
||||||
|
instance does not have a _dispatch method, method names
|
||||||
|
containing dots are supported and resolved, as long as none of
|
||||||
|
the name segments start with an '_'.
|
||||||
|
|
||||||
|
*** SECURITY WARNING: ***
|
||||||
|
|
||||||
|
Enabling the allow_dotted_names options allows intruders
|
||||||
|
to access your module's global variables and may allow
|
||||||
|
intruders to execute arbitrary code on your machine. Only
|
||||||
|
use this option on a secure, closed network.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.instance = instance
|
||||||
|
self.allow_dotted_names = allow_dotted_names
|
||||||
|
|
||||||
|
def register_function(self, function, name = None):
|
||||||
|
"""Registers a function to respond to XML-RPC requests.
|
||||||
|
|
||||||
|
The optional name argument can be used to set a Unicode name
|
||||||
|
for the function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
name = function.__name__
|
||||||
|
self.funcs[name] = function
|
||||||
|
|
||||||
|
def register_introspection_functions(self):
|
||||||
|
"""Registers the XML-RPC introspection methods in the system
|
||||||
|
namespace.
|
||||||
|
|
||||||
|
see http://xmlrpc.usefulinc.com/doc/reserved.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.funcs.update({'system.listMethods' : self.system_listMethods,
|
||||||
|
'system.methodSignature' : self.system_methodSignature,
|
||||||
|
'system.methodHelp' : self.system_methodHelp})
|
||||||
|
|
||||||
|
def register_multicall_functions(self):
|
||||||
|
"""Registers the XML-RPC multicall method in the system
|
||||||
|
namespace.
|
||||||
|
|
||||||
|
see http://www.xmlrpc.com/discuss/msgReader$1208"""
|
||||||
|
|
||||||
|
self.funcs.update({'system.multicall' : self.system_multicall})
|
||||||
|
|
||||||
|
def _marshaled_dispatch(self, data, dispatch_method = None):
|
||||||
|
"""Dispatches an XML-RPC method from marshalled (XML) data.
|
||||||
|
|
||||||
|
XML-RPC methods are dispatched from the marshalled (XML) data
|
||||||
|
using the _dispatch method and the result is returned as
|
||||||
|
marshalled data. For backwards compatibility, a dispatch
|
||||||
|
function can be provided as an argument (see comment in
|
||||||
|
SimpleXMLRPCRequestHandler.do_POST) but overriding the
|
||||||
|
existing method through subclassing is the prefered means
|
||||||
|
of changing method dispatch behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
params, method = xmlrpclib.loads(data)
|
||||||
|
|
||||||
|
# generate response
|
||||||
|
if dispatch_method is not None:
|
||||||
|
response = dispatch_method(method, params)
|
||||||
|
else:
|
||||||
|
response = self._dispatch(method, params)
|
||||||
|
# wrap response in a singleton tuple
|
||||||
|
response = (response,)
|
||||||
|
response = xmlrpclib.dumps(response, methodresponse=1,
|
||||||
|
allow_none=self.allow_none, encoding=self.encoding)
|
||||||
|
except Fault, fault:
|
||||||
|
response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
|
||||||
|
encoding=self.encoding)
|
||||||
|
except:
|
||||||
|
# report exception back to server
|
||||||
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||||
|
response = xmlrpclib.dumps(
|
||||||
|
xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
|
||||||
|
encoding=self.encoding, allow_none=self.allow_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def system_listMethods(self):
|
||||||
|
"""system.listMethods() => ['add', 'subtract', 'multiple']
|
||||||
|
|
||||||
|
Returns a list of the methods supported by the server."""
|
||||||
|
|
||||||
|
methods = self.funcs.keys()
|
||||||
|
if self.instance is not None:
|
||||||
|
# Instance can implement _listMethod to return a list of
|
||||||
|
# methods
|
||||||
|
if hasattr(self.instance, '_listMethods'):
|
||||||
|
methods = remove_duplicates(
|
||||||
|
methods + self.instance._listMethods()
|
||||||
|
)
|
||||||
|
# if the instance has a _dispatch method then we
|
||||||
|
# don't have enough information to provide a list
|
||||||
|
# of methods
|
||||||
|
elif not hasattr(self.instance, '_dispatch'):
|
||||||
|
methods = remove_duplicates(
|
||||||
|
methods + list_public_methods(self.instance)
|
||||||
|
)
|
||||||
|
methods.sort()
|
||||||
|
return methods
|
||||||
|
|
||||||
|
def system_methodSignature(self, method_name):
|
||||||
|
"""system.methodSignature('add') => [double, int, int]
|
||||||
|
|
||||||
|
Returns a list describing the signature of the method. In the
|
||||||
|
above example, the add method takes two integers as arguments
|
||||||
|
and returns a double result.
|
||||||
|
|
||||||
|
This server does NOT support system.methodSignature."""
|
||||||
|
|
||||||
|
# See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
|
||||||
|
|
||||||
|
return 'signatures not supported'
|
||||||
|
|
||||||
|
def system_methodHelp(self, method_name):
|
||||||
|
"""system.methodHelp('add') => "Adds two integers together"
|
||||||
|
|
||||||
|
Returns a string containing documentation for the specified method."""
|
||||||
|
|
||||||
|
method = None
|
||||||
|
if method_name in self.funcs:
|
||||||
|
method = self.funcs[method_name]
|
||||||
|
elif self.instance is not None:
|
||||||
|
# Instance can implement _methodHelp to return help for a method
|
||||||
|
if hasattr(self.instance, '_methodHelp'):
|
||||||
|
return self.instance._methodHelp(method_name)
|
||||||
|
# if the instance has a _dispatch method then we
|
||||||
|
# don't have enough information to provide help
|
||||||
|
elif not hasattr(self.instance, '_dispatch'):
|
||||||
|
try:
|
||||||
|
method = resolve_dotted_attribute(
|
||||||
|
self.instance,
|
||||||
|
method_name,
|
||||||
|
self.allow_dotted_names
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Note that we aren't checking that the method actually
|
||||||
|
# be a callable object of some kind
|
||||||
|
if method is None:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
import pydoc
|
||||||
|
return pydoc.getdoc(method)
|
||||||
|
|
||||||
|
def system_multicall(self, call_list):
|
||||||
|
"""system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
|
||||||
|
[[4], ...]
|
||||||
|
|
||||||
|
Allows the caller to package multiple XML-RPC calls into a single
|
||||||
|
request.
|
||||||
|
|
||||||
|
See http://www.xmlrpc.com/discuss/msgReader$1208
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for call in call_list:
|
||||||
|
method_name = call['methodName']
|
||||||
|
params = call['params']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# XXX A marshalling error in any response will fail the entire
|
||||||
|
# multicall. If someone cares they should fix this.
|
||||||
|
results.append([self._dispatch(method_name, params)])
|
||||||
|
except Fault, fault:
|
||||||
|
results.append(
|
||||||
|
{'faultCode' : fault.faultCode,
|
||||||
|
'faultString' : fault.faultString}
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||||
|
results.append(
|
||||||
|
{'faultCode' : 1,
|
||||||
|
'faultString' : "%s:%s" % (exc_type, exc_value)}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _dispatch(self, method, params):
|
||||||
|
"""Dispatches the XML-RPC method.
|
||||||
|
|
||||||
|
XML-RPC calls are forwarded to a registered function that
|
||||||
|
matches the called XML-RPC method name. If no such function
|
||||||
|
exists then the call is forwarded to the registered instance,
|
||||||
|
if available.
|
||||||
|
|
||||||
|
If the registered instance has a _dispatch method then that
|
||||||
|
method will be called with the name of the XML-RPC method and
|
||||||
|
its parameters as a tuple
|
||||||
|
e.g. instance._dispatch('add',(2,3))
|
||||||
|
|
||||||
|
If the registered instance does not have a _dispatch method
|
||||||
|
then the instance will be searched to find a matching method
|
||||||
|
and, if found, will be called.
|
||||||
|
|
||||||
|
Methods beginning with an '_' are considered private and will
|
||||||
|
not be called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
func = None
|
||||||
|
try:
|
||||||
|
# check to see if a matching function has been registered
|
||||||
|
func = self.funcs[method]
|
||||||
|
except KeyError:
|
||||||
|
if self.instance is not None:
|
||||||
|
# check for a _dispatch method
|
||||||
|
if hasattr(self.instance, '_dispatch'):
|
||||||
|
return self.instance._dispatch(method, params)
|
||||||
|
else:
|
||||||
|
# call instance method directly
|
||||||
|
try:
|
||||||
|
func = resolve_dotted_attribute(
|
||||||
|
self.instance,
|
||||||
|
method,
|
||||||
|
self.allow_dotted_names
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if func is not None:
|
||||||
|
return func(*params)
|
||||||
|
else:
|
||||||
|
raise Exception('method "%s" is not supported' % method)
|
||||||
|
|
||||||
|
class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
"""Simple XML-RPC request handler class.
|
||||||
|
|
||||||
|
Handles all HTTP POST requests and attempts to decode them as
|
||||||
|
XML-RPC requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Class attribute listing the accessible path components;
|
||||||
|
# paths not on this list will result in a 404 error.
|
||||||
|
rpc_paths = ('/', '/RPC2')
|
||||||
|
|
||||||
|
def is_rpc_path_valid(self):
|
||||||
|
if self.rpc_paths:
|
||||||
|
return self.path in self.rpc_paths
|
||||||
|
else:
|
||||||
|
# If .rpc_paths is empty, just assume all paths are legal
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
"""Handles the HTTP POST request.
|
||||||
|
|
||||||
|
Attempts to interpret all HTTP POST requests as XML-RPC calls,
|
||||||
|
which are forwarded to the server's _dispatch method for handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check that the path is legal
|
||||||
|
if not self.is_rpc_path_valid():
|
||||||
|
self.report_404()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get arguments by reading body of request.
|
||||||
|
# We read this in chunks to avoid straining
|
||||||
|
# socket.read(); around the 10 or 15Mb mark, some platforms
|
||||||
|
# begin to have problems (bug #792570).
|
||||||
|
max_chunk_size = 10*1024*1024
|
||||||
|
size_remaining = int(self.headers["content-length"])
|
||||||
|
L = []
|
||||||
|
while size_remaining:
|
||||||
|
chunk_size = min(size_remaining, max_chunk_size)
|
||||||
|
L.append(self.rfile.read(chunk_size))
|
||||||
|
size_remaining -= len(L[-1])
|
||||||
|
data = ''.join(L)
|
||||||
|
|
||||||
|
# In previous versions of SimpleXMLRPCServer, _dispatch
|
||||||
|
# could be overridden in this class, instead of in
|
||||||
|
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
|
||||||
|
# check to see if a subclass implements _dispatch and dispatch
|
||||||
|
# using that method if present.
|
||||||
|
response = self.server._marshaled_dispatch(
|
||||||
|
data, getattr(self, '_dispatch', None)
|
||||||
|
)
|
||||||
|
except Exception, e: # This should only happen if the module is buggy
|
||||||
|
# internal error, report as HTTP server error
|
||||||
|
self.send_response(500)
|
||||||
|
|
||||||
|
# Send information about the exception if requested
|
||||||
|
if hasattr(self.server, '_send_traceback_header') and \
|
||||||
|
self.server._send_traceback_header:
|
||||||
|
self.send_header("X-exception", str(e))
|
||||||
|
self.send_header("X-traceback", traceback.format_exc())
|
||||||
|
|
||||||
|
self.end_headers()
|
||||||
|
else:
|
||||||
|
# got a valid XML RPC response
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/xml")
|
||||||
|
self.send_header("Content-length", str(len(response)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(response)
|
||||||
|
|
||||||
|
# shut down the connection
|
||||||
|
self.wfile.flush()
|
||||||
|
self.connection.shutdown(1)
|
||||||
|
|
||||||
|
def report_404 (self):
|
||||||
|
# Report a 404 error
|
||||||
|
self.send_response(404)
|
||||||
|
response = 'No such page'
|
||||||
|
self.send_header("Content-type", "text/plain")
|
||||||
|
self.send_header("Content-length", str(len(response)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(response)
|
||||||
|
# shut down the connection
|
||||||
|
self.wfile.flush()
|
||||||
|
self.connection.shutdown(1)
|
||||||
|
|
||||||
|
def log_request(self, code='-', size='-'):
|
||||||
|
"""Selectively log an accepted request."""
|
||||||
|
|
||||||
|
if self.server.logRequests:
|
||||||
|
BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
|
||||||
|
|
||||||
|
class SimpleXMLRPCServer(SocketServer.TCPServer,
|
||||||
|
SimpleXMLRPCDispatcher):
|
||||||
|
"""Simple XML-RPC server.
|
||||||
|
|
||||||
|
Simple XML-RPC server that allows functions and a single instance
|
||||||
|
to be installed to handle requests. The default implementation
|
||||||
|
attempts to dispatch XML-RPC calls to the functions or instance
|
||||||
|
installed in the server. Override the _dispatch method inhereted
|
||||||
|
from SimpleXMLRPCDispatcher to change this behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
# Warning: this is for debugging purposes only! Never set this to True in
|
||||||
|
# production code, as will be sending out sensitive information (exception
|
||||||
|
# and stack trace details) when exceptions are raised inside
|
||||||
|
# SimpleXMLRPCRequestHandler.do_POST
|
||||||
|
_send_traceback_header = False
|
||||||
|
|
||||||
|
def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
|
||||||
|
logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
|
||||||
|
self.logRequests = logRequests
|
||||||
|
|
||||||
|
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||||
|
SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
|
||||||
|
|
||||||
|
# [Bug #1222790] If possible, set close-on-exec flag; if a
|
||||||
|
# method spawns a subprocess, the subprocess shouldn't have
|
||||||
|
# the listening socket open.
|
||||||
|
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
|
||||||
|
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
|
||||||
|
flags |= fcntl.FD_CLOEXEC
|
||||||
|
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
|
||||||
|
|
||||||
|
class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
|
||||||
|
"""Simple handler for XML-RPC data passed through CGI."""
|
||||||
|
|
||||||
|
def __init__(self, allow_none=False, encoding=None):
|
||||||
|
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
|
||||||
|
|
||||||
|
def handle_xmlrpc(self, request_text):
|
||||||
|
"""Handle a single XML-RPC request"""
|
||||||
|
|
||||||
|
response = self._marshaled_dispatch(request_text)
|
||||||
|
|
||||||
|
print 'Content-Type: text/xml'
|
||||||
|
print 'Content-Length: %d' % len(response)
|
||||||
|
print
|
||||||
|
sys.stdout.write(response)
|
||||||
|
|
||||||
|
def handle_get(self):
|
||||||
|
"""Handle a single HTTP GET request.
|
||||||
|
|
||||||
|
Default implementation indicates an error because
|
||||||
|
XML-RPC uses the POST method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = 400
|
||||||
|
message, explain = \
|
||||||
|
BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
|
||||||
|
|
||||||
|
response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
|
||||||
|
{
|
||||||
|
'code' : code,
|
||||||
|
'message' : message,
|
||||||
|
'explain' : explain
|
||||||
|
}
|
||||||
|
print 'Status: %d %s' % (code, message)
|
||||||
|
print 'Content-Type: text/html'
|
||||||
|
print 'Content-Length: %d' % len(response)
|
||||||
|
print
|
||||||
|
sys.stdout.write(response)
|
||||||
|
|
||||||
|
def handle_request(self, request_text = None):
|
||||||
|
"""Handle a single XML-RPC request passed through a CGI post method.
|
||||||
|
|
||||||
|
If no XML data is given then it is read from stdin. The resulting
|
||||||
|
XML-RPC response is printed to stdout along with the correct HTTP
|
||||||
|
headers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request_text is None and \
|
||||||
|
os.environ.get('REQUEST_METHOD', None) == 'GET':
|
||||||
|
self.handle_get()
|
||||||
|
else:
|
||||||
|
# POST data is normally available through stdin
|
||||||
|
if request_text is None:
|
||||||
|
request_text = sys.stdin.read()
|
||||||
|
|
||||||
|
self.handle_xmlrpc(request_text)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print 'Running XML-RPC server on port 8000'
|
||||||
|
server = SimpleXMLRPCServer(("localhost", 8000))
|
||||||
|
server.register_function(pow)
|
||||||
|
server.register_function(lambda x,y: x+y, 'add')
|
||||||
|
server.serve_forever()
|
|
@ -0,0 +1,681 @@
|
||||||
|
"""Generic socket server classes.
|
||||||
|
|
||||||
|
This module tries to capture the various aspects of defining a server:
|
||||||
|
|
||||||
|
For socket-based servers:
|
||||||
|
|
||||||
|
- address family:
|
||||||
|
- AF_INET{,6}: IP (Internet Protocol) sockets (default)
|
||||||
|
- AF_UNIX: Unix domain sockets
|
||||||
|
- others, e.g. AF_DECNET are conceivable (see <socket.h>
|
||||||
|
- socket type:
|
||||||
|
- SOCK_STREAM (reliable stream, e.g. TCP)
|
||||||
|
- SOCK_DGRAM (datagrams, e.g. UDP)
|
||||||
|
|
||||||
|
For request-based servers (including socket-based):
|
||||||
|
|
||||||
|
- client address verification before further looking at the request
|
||||||
|
(This is actually a hook for any processing that needs to look
|
||||||
|
at the request before anything else, e.g. logging)
|
||||||
|
- how to handle multiple requests:
|
||||||
|
- synchronous (one request is handled at a time)
|
||||||
|
- forking (each request is handled by a new process)
|
||||||
|
- threading (each request is handled by a new thread)
|
||||||
|
|
||||||
|
The classes in this module favor the server type that is simplest to
|
||||||
|
write: a synchronous TCP/IP server. This is bad class design, but
|
||||||
|
save some typing. (There's also the issue that a deep class hierarchy
|
||||||
|
slows down method lookups.)
|
||||||
|
|
||||||
|
There are five classes in an inheritance diagram, four of which represent
|
||||||
|
synchronous servers of four types:
|
||||||
|
|
||||||
|
+------------+
|
||||||
|
| BaseServer |
|
||||||
|
+------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-----------+ +------------------+
|
||||||
|
| TCPServer |------->| UnixStreamServer |
|
||||||
|
+-----------+ +------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-----------+ +--------------------+
|
||||||
|
| UDPServer |------->| UnixDatagramServer |
|
||||||
|
+-----------+ +--------------------+
|
||||||
|
|
||||||
|
Note that UnixDatagramServer derives from UDPServer, not from
|
||||||
|
UnixStreamServer -- the only difference between an IP and a Unix
|
||||||
|
stream server is the address family, which is simply repeated in both
|
||||||
|
unix server classes.
|
||||||
|
|
||||||
|
Forking and threading versions of each type of server can be created
|
||||||
|
using the ForkingMixIn and ThreadingMixIn mix-in classes. For
|
||||||
|
instance, a threading UDP server class is created as follows:
|
||||||
|
|
||||||
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
||||||
|
|
||||||
|
The Mix-in class must come first, since it overrides a method defined
|
||||||
|
in UDPServer! Setting the various member variables also changes
|
||||||
|
the behavior of the underlying server mechanism.
|
||||||
|
|
||||||
|
To implement a service, you must derive a class from
|
||||||
|
BaseRequestHandler and redefine its handle() method. You can then run
|
||||||
|
various versions of the service by combining one of the server classes
|
||||||
|
with your request handler class.
|
||||||
|
|
||||||
|
The request handler class must be different for datagram or stream
|
||||||
|
services. This can be hidden by using the request handler
|
||||||
|
subclasses StreamRequestHandler or DatagramRequestHandler.
|
||||||
|
|
||||||
|
Of course, you still have to use your head!
|
||||||
|
|
||||||
|
For instance, it makes no sense to use a forking server if the service
|
||||||
|
contains state in memory that can be modified by requests (since the
|
||||||
|
modifications in the child process would never reach the initial state
|
||||||
|
kept in the parent process and passed to each child). In this case,
|
||||||
|
you can use a threading server, but you will probably have to use
|
||||||
|
locks to avoid two requests that come in nearly simultaneous to apply
|
||||||
|
conflicting changes to the server state.
|
||||||
|
|
||||||
|
On the other hand, if you are building e.g. an HTTP server, where all
|
||||||
|
data is stored externally (e.g. in the file system), a synchronous
|
||||||
|
class will essentially render the service "deaf" while one request is
|
||||||
|
being handled -- which may be for a very long time if a client is slow
|
||||||
|
to reqd all the data it has requested. Here a threading or forking
|
||||||
|
server is appropriate.
|
||||||
|
|
||||||
|
In some cases, it may be appropriate to process part of a request
|
||||||
|
synchronously, but to finish processing in a forked child depending on
|
||||||
|
the request data. This can be implemented by using a synchronous
|
||||||
|
server and doing an explicit fork in the request handler class
|
||||||
|
handle() method.
|
||||||
|
|
||||||
|
Another approach to handling multiple simultaneous requests in an
|
||||||
|
environment that supports neither threads nor fork (or where these are
|
||||||
|
too expensive or inappropriate for the service) is to maintain an
|
||||||
|
explicit table of partially finished requests and to use select() to
|
||||||
|
decide which request to work on next (or whether to handle a new
|
||||||
|
incoming request). This is particularly important for stream services
|
||||||
|
where each client can potentially be connected for a long time (if
|
||||||
|
threads or subprocesses cannot be used).
|
||||||
|
|
||||||
|
Future work:
|
||||||
|
- Standard classes for Sun RPC (which uses either UDP or TCP)
|
||||||
|
- Standard mix-in classes to implement various authentication
|
||||||
|
and encryption schemes
|
||||||
|
- Standard framework for select-based multiplexing
|
||||||
|
|
||||||
|
XXX Open problems:
|
||||||
|
- What to do with out-of-band data?
|
||||||
|
|
||||||
|
BaseServer:
|
||||||
|
- split generic "request" functionality out into BaseServer class.
|
||||||
|
Copyright (C) 2000 Luke Kenneth Casson Leighton <lkcl@samba.org>
|
||||||
|
|
||||||
|
example: read entries from a SQL database (requires overriding
|
||||||
|
get_request() to return a table entry from the database).
|
||||||
|
entry is processed by a RequestHandlerClass.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Author of the BaseServer patch: Luke Kenneth Casson Leighton
|
||||||
|
|
||||||
|
# XXX Warning!
|
||||||
|
# There is a test suite for this module, but it cannot be run by the
|
||||||
|
# standard regression test.
|
||||||
|
# To run it manually, run Lib/test/test_socketserver.py.
|
||||||
|
|
||||||
|
__version__ = "0.4"
|
||||||
|
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
except ImportError:
|
||||||
|
import dummy_threading as threading
|
||||||
|
|
||||||
|
__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
|
||||||
|
"ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
|
||||||
|
"StreamRequestHandler","DatagramRequestHandler",
|
||||||
|
"ThreadingMixIn", "ForkingMixIn"]
|
||||||
|
if hasattr(socket, "AF_UNIX"):
|
||||||
|
__all__.extend(["UnixStreamServer","UnixDatagramServer",
|
||||||
|
"ThreadingUnixStreamServer",
|
||||||
|
"ThreadingUnixDatagramServer"])
|
||||||
|
|
||||||
|
class BaseServer:
|
||||||
|
|
||||||
|
"""Base class for server classes.
|
||||||
|
|
||||||
|
Methods for the caller:
|
||||||
|
|
||||||
|
- __init__(server_address, RequestHandlerClass)
|
||||||
|
- serve_forever(poll_interval=0.5)
|
||||||
|
- shutdown()
|
||||||
|
- handle_request() # if you do not use serve_forever()
|
||||||
|
- fileno() -> int # for select()
|
||||||
|
|
||||||
|
Methods that may be overridden:
|
||||||
|
|
||||||
|
- server_bind()
|
||||||
|
- server_activate()
|
||||||
|
- get_request() -> request, client_address
|
||||||
|
- handle_timeout()
|
||||||
|
- verify_request(request, client_address)
|
||||||
|
- server_close()
|
||||||
|
- process_request(request, client_address)
|
||||||
|
- close_request(request)
|
||||||
|
- handle_error()
|
||||||
|
|
||||||
|
Methods for derived classes:
|
||||||
|
|
||||||
|
- finish_request(request, client_address)
|
||||||
|
|
||||||
|
Class variables that may be overridden by derived classes or
|
||||||
|
instances:
|
||||||
|
|
||||||
|
- timeout
|
||||||
|
- address_family
|
||||||
|
- socket_type
|
||||||
|
- allow_reuse_address
|
||||||
|
|
||||||
|
Instance variables:
|
||||||
|
|
||||||
|
- RequestHandlerClass
|
||||||
|
- socket
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
timeout = None
|
||||||
|
|
||||||
|
def __init__(self, server_address, RequestHandlerClass):
|
||||||
|
"""Constructor. May be extended, do not override."""
|
||||||
|
self.server_address = server_address
|
||||||
|
self.RequestHandlerClass = RequestHandlerClass
|
||||||
|
self.__is_shut_down = threading.Event()
|
||||||
|
self.__serving = False
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
"""Called by constructor to activate the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def serve_forever(self, poll_interval=0.5):
|
||||||
|
"""Handle one request at a time until shutdown.
|
||||||
|
|
||||||
|
Polls for shutdown every poll_interval seconds. Ignores
|
||||||
|
self.timeout. If you need to do periodic tasks, do them in
|
||||||
|
another thread.
|
||||||
|
"""
|
||||||
|
self.__serving = True
|
||||||
|
self.__is_shut_down.clear()
|
||||||
|
while self.__serving:
|
||||||
|
# XXX: Consider using another file descriptor or
|
||||||
|
# connecting to the socket to wake this up instead of
|
||||||
|
# polling. Polling reduces our responsiveness to a
|
||||||
|
# shutdown request and wastes cpu at all other times.
|
||||||
|
r, w, e = select.select([self], [], [], poll_interval)
|
||||||
|
if r:
|
||||||
|
self._handle_request_noblock()
|
||||||
|
self.__is_shut_down.set()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Stops the serve_forever loop.
|
||||||
|
|
||||||
|
Blocks until the loop has finished. This must be called while
|
||||||
|
serve_forever() is running in another thread, or it will
|
||||||
|
deadlock.
|
||||||
|
"""
|
||||||
|
self.__serving = False
|
||||||
|
self.__is_shut_down.wait()
|
||||||
|
|
||||||
|
# The distinction between handling, getting, processing and
|
||||||
|
# finishing a request is fairly arbitrary. Remember:
|
||||||
|
#
|
||||||
|
# - handle_request() is the top-level call. It calls
|
||||||
|
# select, get_request(), verify_request() and process_request()
|
||||||
|
# - get_request() is different for stream or datagram sockets
|
||||||
|
# - process_request() is the place that may fork a new process
|
||||||
|
# or create a new thread to finish the request
|
||||||
|
# - finish_request() instantiates the request handler class;
|
||||||
|
# this constructor will handle the request all by itself
|
||||||
|
|
||||||
|
def handle_request(self):
|
||||||
|
"""Handle one request, possibly blocking.
|
||||||
|
|
||||||
|
Respects self.timeout.
|
||||||
|
"""
|
||||||
|
# Support people who used socket.settimeout() to escape
|
||||||
|
# handle_request before self.timeout was available.
|
||||||
|
timeout = self.socket.gettimeout()
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.timeout
|
||||||
|
elif self.timeout is not None:
|
||||||
|
timeout = min(timeout, self.timeout)
|
||||||
|
fd_sets = select.select([self], [], [], timeout)
|
||||||
|
if not fd_sets[0]:
|
||||||
|
self.handle_timeout()
|
||||||
|
return
|
||||||
|
self._handle_request_noblock()
|
||||||
|
|
||||||
|
def _handle_request_noblock(self):
|
||||||
|
"""Handle one request, without blocking.
|
||||||
|
|
||||||
|
I assume that select.select has returned that the socket is
|
||||||
|
readable before this function was called, so there should be
|
||||||
|
no risk of blocking in get_request().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
request, client_address = self.get_request()
|
||||||
|
except socket.error:
|
||||||
|
return
|
||||||
|
if self.verify_request(request, client_address):
|
||||||
|
try:
|
||||||
|
self.process_request(request, client_address)
|
||||||
|
except:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def handle_timeout(self):
|
||||||
|
"""Called if no new request arrives within self.timeout.
|
||||||
|
|
||||||
|
Overridden by ForkingMixIn.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def verify_request(self, request, client_address):
|
||||||
|
"""Verify the request. May be overridden.
|
||||||
|
|
||||||
|
Return True if we should proceed with this request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Call finish_request.
|
||||||
|
|
||||||
|
Overridden by ForkingMixIn and ThreadingMixIn.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def server_close(self):
|
||||||
|
"""Called to clean-up the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish_request(self, request, client_address):
|
||||||
|
"""Finish one request by instantiating RequestHandlerClass."""
|
||||||
|
self.RequestHandlerClass(request, client_address, self)
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
"""Called to clean up an individual request."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_error(self, request, client_address):
|
||||||
|
"""Handle an error gracefully. May be overridden.
|
||||||
|
|
||||||
|
The default is to print a traceback and continue.
|
||||||
|
|
||||||
|
"""
|
||||||
|
print '-'*40
|
||||||
|
print 'Exception happened during processing of request from',
|
||||||
|
print client_address
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc() # XXX But this goes to stderr!
|
||||||
|
print '-'*40
|
||||||
|
|
||||||
|
|
||||||
|
class TCPServer(BaseServer):
|
||||||
|
|
||||||
|
"""Base class for various socket-based server classes.
|
||||||
|
|
||||||
|
Defaults to synchronous IP stream (i.e., TCP).
|
||||||
|
|
||||||
|
Methods for the caller:
|
||||||
|
|
||||||
|
- __init__(server_address, RequestHandlerClass, bind_and_activate=True)
|
||||||
|
- serve_forever(poll_interval=0.5)
|
||||||
|
- shutdown()
|
||||||
|
- handle_request() # if you don't use serve_forever()
|
||||||
|
- fileno() -> int # for select()
|
||||||
|
|
||||||
|
Methods that may be overridden:
|
||||||
|
|
||||||
|
- server_bind()
|
||||||
|
- server_activate()
|
||||||
|
- get_request() -> request, client_address
|
||||||
|
- handle_timeout()
|
||||||
|
- verify_request(request, client_address)
|
||||||
|
- process_request(request, client_address)
|
||||||
|
- close_request(request)
|
||||||
|
- handle_error()
|
||||||
|
|
||||||
|
Methods for derived classes:
|
||||||
|
|
||||||
|
- finish_request(request, client_address)
|
||||||
|
|
||||||
|
Class variables that may be overridden by derived classes or
|
||||||
|
instances:
|
||||||
|
|
||||||
|
- timeout
|
||||||
|
- address_family
|
||||||
|
- socket_type
|
||||||
|
- request_queue_size (only for stream sockets)
|
||||||
|
- allow_reuse_address
|
||||||
|
|
||||||
|
Instance variables:
|
||||||
|
|
||||||
|
- server_address
|
||||||
|
- RequestHandlerClass
|
||||||
|
- socket
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
address_family = socket.AF_INET
|
||||||
|
|
||||||
|
socket_type = socket.SOCK_STREAM
|
||||||
|
|
||||||
|
request_queue_size = 5
|
||||||
|
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
|
||||||
|
"""Constructor. May be extended, do not override."""
|
||||||
|
BaseServer.__init__(self, server_address, RequestHandlerClass)
|
||||||
|
self.socket = socket.socket(self.address_family,
|
||||||
|
self.socket_type)
|
||||||
|
if bind_and_activate:
|
||||||
|
self.server_bind()
|
||||||
|
self.server_activate()
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
"""Called by constructor to bind the socket.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.allow_reuse_address:
|
||||||
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.socket.bind(self.server_address)
|
||||||
|
self.server_address = self.socket.getsockname()
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
"""Called by constructor to activate the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.socket.listen(self.request_queue_size)
|
||||||
|
|
||||||
|
def server_close(self):
|
||||||
|
"""Called to clean-up the server.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
"""Return socket file number.
|
||||||
|
|
||||||
|
Interface required by select().
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.socket.fileno()
|
||||||
|
|
||||||
|
def get_request(self):
|
||||||
|
"""Get the request and client address from the socket.
|
||||||
|
|
||||||
|
May be overridden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.socket.accept()
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
"""Called to clean up an individual request."""
|
||||||
|
request.close()
|
||||||
|
|
||||||
|
|
||||||
|
class UDPServer(TCPServer):
|
||||||
|
|
||||||
|
"""UDP server class."""
|
||||||
|
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
socket_type = socket.SOCK_DGRAM
|
||||||
|
|
||||||
|
max_packet_size = 8192
|
||||||
|
|
||||||
|
def get_request(self):
|
||||||
|
data, client_addr = self.socket.recvfrom(self.max_packet_size)
|
||||||
|
return (data, self.socket), client_addr
|
||||||
|
|
||||||
|
def server_activate(self):
|
||||||
|
# No need to call listen() for UDP.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close_request(self, request):
|
||||||
|
# No need to close anything.
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ForkingMixIn:
|
||||||
|
|
||||||
|
"""Mix-in class to handle each request in a new process."""
|
||||||
|
|
||||||
|
timeout = 300
|
||||||
|
active_children = None
|
||||||
|
max_children = 40
|
||||||
|
|
||||||
|
def collect_children(self):
|
||||||
|
"""Internal routine to wait for children that have exited."""
|
||||||
|
if self.active_children is None: return
|
||||||
|
while len(self.active_children) >= self.max_children:
|
||||||
|
# XXX: This will wait for any child process, not just ones
|
||||||
|
# spawned by this library. This could confuse other
|
||||||
|
# libraries that expect to be able to wait for their own
|
||||||
|
# children.
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(0, options=0)
|
||||||
|
except os.error:
|
||||||
|
pid = None
|
||||||
|
if pid not in self.active_children: continue
|
||||||
|
self.active_children.remove(pid)
|
||||||
|
|
||||||
|
# XXX: This loop runs more system calls than it ought
|
||||||
|
# to. There should be a way to put the active_children into a
|
||||||
|
# process group and then use os.waitpid(-pgid) to wait for any
|
||||||
|
# of that set, but I couldn't find a way to allocate pgids
|
||||||
|
# that couldn't collide.
|
||||||
|
for child in self.active_children:
|
||||||
|
try:
|
||||||
|
pid, status = os.waitpid(child, os.WNOHANG)
|
||||||
|
except os.error:
|
||||||
|
pid = None
|
||||||
|
if not pid: continue
|
||||||
|
try:
|
||||||
|
self.active_children.remove(pid)
|
||||||
|
except ValueError, e:
|
||||||
|
raise ValueError('%s. x=%d and list=%r' % (e.message, pid,
|
||||||
|
self.active_children))
|
||||||
|
|
||||||
|
def handle_timeout(self):
|
||||||
|
"""Wait for zombies after self.timeout seconds of inactivity.
|
||||||
|
|
||||||
|
May be extended, do not override.
|
||||||
|
"""
|
||||||
|
self.collect_children()
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Fork a new subprocess to process the request."""
|
||||||
|
self.collect_children()
|
||||||
|
pid = os.fork()
|
||||||
|
if pid:
|
||||||
|
# Parent process
|
||||||
|
if self.active_children is None:
|
||||||
|
self.active_children = []
|
||||||
|
self.active_children.append(pid)
|
||||||
|
self.close_request(request)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Child process.
|
||||||
|
# This must never return, hence os._exit()!
|
||||||
|
try:
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
os._exit(0)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
finally:
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadingMixIn:
|
||||||
|
"""Mix-in class to handle each request in a new thread."""
|
||||||
|
|
||||||
|
# Decides how threads will act upon termination of the
|
||||||
|
# main process
|
||||||
|
daemon_threads = False
|
||||||
|
|
||||||
|
def process_request_thread(self, request, client_address):
|
||||||
|
"""Same as in BaseServer but as a thread.
|
||||||
|
|
||||||
|
In addition, exception handling is done here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.finish_request(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
except:
|
||||||
|
self.handle_error(request, client_address)
|
||||||
|
self.close_request(request)
|
||||||
|
|
||||||
|
def process_request(self, request, client_address):
|
||||||
|
"""Start a new thread to process the request."""
|
||||||
|
t = threading.Thread(target = self.process_request_thread,
|
||||||
|
args = (request, client_address))
|
||||||
|
if self.daemon_threads:
|
||||||
|
t.setDaemon (1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
|
||||||
|
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
|
||||||
|
|
||||||
|
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
|
||||||
|
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
|
||||||
|
|
||||||
|
if hasattr(socket, 'AF_UNIX'):
|
||||||
|
|
||||||
|
class UnixStreamServer(TCPServer):
|
||||||
|
address_family = socket.AF_UNIX
|
||||||
|
|
||||||
|
class UnixDatagramServer(UDPServer):
|
||||||
|
address_family = socket.AF_UNIX
|
||||||
|
|
||||||
|
class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
|
||||||
|
|
||||||
|
class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
|
||||||
|
|
||||||
|
class BaseRequestHandler:
|
||||||
|
|
||||||
|
"""Base class for request handler classes.
|
||||||
|
|
||||||
|
This class is instantiated for each request to be handled. The
|
||||||
|
constructor sets the instance variables request, client_address
|
||||||
|
and server, and then calls the handle() method. To implement a
|
||||||
|
specific service, all you need to do is to derive a class which
|
||||||
|
defines a handle() method.
|
||||||
|
|
||||||
|
The handle() method can find the request as self.request, the
|
||||||
|
client address as self.client_address, and the server (in case it
|
||||||
|
needs access to per-server information) as self.server. Since a
|
||||||
|
separate instance is created for each request, the handle() method
|
||||||
|
can define arbitrary other instance variariables.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, client_address, server):
|
||||||
|
self.request = request
|
||||||
|
self.client_address = client_address
|
||||||
|
self.server = server
|
||||||
|
try:
|
||||||
|
self.setup()
|
||||||
|
self.handle()
|
||||||
|
self.finish()
|
||||||
|
finally:
|
||||||
|
sys.exc_traceback = None # Help garbage collection
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# The following two classes make it possible to use the same service
|
||||||
|
# class for stream or datagram servers.
|
||||||
|
# Each class sets up these instance variables:
|
||||||
|
# - rfile: a file object from which receives the request is read
|
||||||
|
# - wfile: a file object to which the reply is written
|
||||||
|
# When the handle() method returns, wfile is flushed properly
|
||||||
|
|
||||||
|
|
||||||
|
class StreamRequestHandler(BaseRequestHandler):
|
||||||
|
|
||||||
|
"""Define self.rfile and self.wfile for stream sockets."""
|
||||||
|
|
||||||
|
# Default buffer sizes for rfile, wfile.
|
||||||
|
# We default rfile to buffered because otherwise it could be
|
||||||
|
# really slow for large data (a getc() call per byte); we make
|
||||||
|
# wfile unbuffered because (a) often after a write() we want to
|
||||||
|
# read and we need to flush the line; (b) big writes to unbuffered
|
||||||
|
# files are typically optimized by stdio even when big reads
|
||||||
|
# aren't.
|
||||||
|
rbufsize = -1
|
||||||
|
wbufsize = 0
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.connection = self.request
|
||||||
|
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||||
|
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
if not self.wfile.closed:
|
||||||
|
self.wfile.flush()
|
||||||
|
self.wfile.close()
|
||||||
|
self.rfile.close()
|
||||||
|
|
||||||
|
|
||||||
|
class DatagramRequestHandler(BaseRequestHandler):
|
||||||
|
|
||||||
|
# XXX Regrettably, I cannot get this working on Linux;
|
||||||
|
# s.recvfrom() doesn't return a meaningful client address.
|
||||||
|
|
||||||
|
"""Define self.rfile and self.wfile for datagram sockets."""
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
try:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from StringIO import StringIO
|
||||||
|
self.packet, self.socket = self.request
|
||||||
|
self.rfile = StringIO(self.packet)
|
||||||
|
self.wfile = StringIO()
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
self.socket.sendto(self.wfile.getvalue(), self.client_address)
|
64
setup.py
64
setup.py
|
@ -73,11 +73,35 @@ def check_modules():
|
||||||
if not ok:
|
if not ok:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def find_addons():
|
def _find_addons():
|
||||||
for (dp, dn, names) in os.walk(opj('bin', 'addons')):
|
for (dp, dn, names) in os.walk(opj('bin', 'addons')):
|
||||||
if '__terp__.py' in names:
|
if '__terp__.py' in names:
|
||||||
modname = dp.replace(os.path.sep, '.').replace('bin', 'openerp-server', 1)
|
modname = os.path.basename(dp)
|
||||||
yield modname
|
yield (modname, dp)
|
||||||
|
#look for extra modules
|
||||||
|
try:
|
||||||
|
empath = os.getenv('EXTRA_MODULES_PATH','../addons/')
|
||||||
|
f = open(opj(empath,'server_modules.list'),'r')
|
||||||
|
# print 'Getting modules from:' , opj(empath,'server_modules.list')
|
||||||
|
mods = f.readlines()
|
||||||
|
for mname in mods:
|
||||||
|
mname = mname.strip()
|
||||||
|
if not mname:
|
||||||
|
continue
|
||||||
|
if os.path.exists(opj(empath,mname,'__terp__.py')):
|
||||||
|
yield ( mname, opj(empath,mname) )
|
||||||
|
else:
|
||||||
|
print "Module %s specified, but no valid path." % mname
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
__found_addons = None
|
||||||
|
|
||||||
|
# Cache the results of _find_addons() and return them
|
||||||
|
def find_addons(found_addons = None):
|
||||||
|
if not found_addons:
|
||||||
|
found_addons = _find_addons()
|
||||||
|
return found_addons
|
||||||
|
|
||||||
def data_files():
|
def data_files():
|
||||||
'''Build list of data files to be installed'''
|
'''Build list of data files to be installed'''
|
||||||
|
@ -108,20 +132,26 @@ def data_files():
|
||||||
opj('bin', 'server.pkey'),
|
opj('bin', 'server.pkey'),
|
||||||
opj('bin', 'server.cert')]))
|
opj('bin', 'server.cert')]))
|
||||||
|
|
||||||
for addon in find_addons():
|
if sys.version_info[0:2] == (2,5):
|
||||||
addonname = addon.split('.')[-1]
|
files.append((openerp_site_packages, [ opj('python25-compat','BaseHTTPServer.py'),
|
||||||
add_path = addon.replace('.', os.path.sep).replace('openerp-server', 'bin', 1)
|
opj('python25-compat','SimpleXMLRPCServer.py'),
|
||||||
addon_path = opj('lib', 'python%s' % py_short_version, 'site-packages', add_path.replace('bin', 'openerp-server', 1))
|
opj('python25-compat','SocketServer.py')]))
|
||||||
|
|
||||||
|
for (addonname, add_path) in find_addons():
|
||||||
|
addon_path = opj('lib', 'python%s' % py_short_version, 'site-packages', 'openerp-server','addons', addonname)
|
||||||
pathfiles = []
|
pathfiles = []
|
||||||
for root, dirs, innerfiles in os.walk(add_path):
|
for root, dirs, innerfiles in os.walk(add_path):
|
||||||
innerfiles = filter(lambda file: os.path.splitext(file)[1] not in ('.pyc', '.pyd', '.pyo'), innerfiles)
|
innerfiles = filter(lambda fil: os.path.splitext(fil)[1] not in ('.pyc', '.pyd', '.pyo'), innerfiles)
|
||||||
if innerfiles:
|
if innerfiles:
|
||||||
res = os.path.normpath(opj(addon_path, root.replace(opj('bin','addons', addonname), '.')))
|
res = os.path.normpath(opj(addon_path, root.replace(opj(add_path), '.')))
|
||||||
pathfiles.extend(((res, map(lambda file: opj(root, file), innerfiles)),))
|
pathfiles.extend(((res, map(lambda fil: opj(root, fil), innerfiles)),))
|
||||||
files.extend(pathfiles)
|
files.extend(pathfiles)
|
||||||
|
|
||||||
|
# for tup in files:
|
||||||
|
# print "Files:", tup[0], tup[1]
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
if not os.getenv('NO_CHECK_MODULES',False) :
|
||||||
check_modules()
|
check_modules()
|
||||||
|
|
||||||
f = file('openerp-server','w')
|
f = file('openerp-server','w')
|
||||||
|
@ -129,6 +159,13 @@ start_script = """#!/bin/sh\necho "OpenERP Setup - The content of this file is g
|
||||||
f.write(start_script)
|
f.write(start_script)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
def find_package_dirs():
|
||||||
|
res = {}
|
||||||
|
for (mod, path) in find_addons():
|
||||||
|
res ['openerp-server.addons.'+ mod ] = path
|
||||||
|
res ['openerp-server'] = 'bin'
|
||||||
|
return res
|
||||||
|
|
||||||
class openerp_server_install(install):
|
class openerp_server_install(install):
|
||||||
def run(self):
|
def run(self):
|
||||||
# create startup script
|
# create startup script
|
||||||
|
@ -179,12 +216,13 @@ setup(name = name,
|
||||||
'openerp-server.report.render',
|
'openerp-server.report.render',
|
||||||
'openerp-server.report.render.rml2pdf',
|
'openerp-server.report.render.rml2pdf',
|
||||||
'openerp-server.report.render.rml2html',
|
'openerp-server.report.render.rml2html',
|
||||||
|
'openerp-server.report.render.rml2txt',
|
||||||
|
'openerp-server.report.render.html2html',
|
||||||
'openerp-server.wizard',
|
'openerp-server.wizard',
|
||||||
'openerp-server.report.render.odt2odt',
|
'openerp-server.report.render.odt2odt',
|
||||||
'openerp-server.report.render.html2html',
|
|
||||||
'openerp-server.workflow'] + \
|
'openerp-server.workflow'] + \
|
||||||
list(find_addons()),
|
list(map( lambda (a, p): 'openerp-server.addons.'+ a ,find_addons())),
|
||||||
package_dir = {'openerp-server': 'bin'},
|
package_dir = find_package_dirs(),
|
||||||
console = [ { "script" : "bin\\openerp-server.py", "icon_resources" : [ (1,"pixmaps\\openerp-icon.ico") ] } ],
|
console = [ { "script" : "bin\\openerp-server.py", "icon_resources" : [ (1,"pixmaps\\openerp-icon.ico") ] } ],
|
||||||
options = options,
|
options = options,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
SELECT model, res_id, module FROM ir_model_data
|
||||||
|
WHERE model = 'ir.actions.act_window'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM ir_act_window WHERE id = ir_model_data.res_id);
|
||||||
|
|
||||||
|
|
||||||
|
SELECT model, res_id, module FROM ir_model_data
|
||||||
|
WHERE model = 'ir.ui.menu'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM ir_ui_menu WHERE id = ir_model_data.res_id);
|
||||||
|
|
||||||
|
SELECT model, res_id, module FROM ir_model_data
|
||||||
|
WHERE model = 'ir.ui.view'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM ir_ui_view WHERE id = ir_model_data.res_id);
|
||||||
|
|
||||||
|
DONT DELETE FROM ir_model_data
|
||||||
|
WHERE model = 'ir.actions.act_window'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM ir_act_window WHERE id = ir_model_data.res_id);
|
||||||
|
|
||||||
|
DONT DELETE FROM ir_model_data
|
||||||
|
WHERE model = 'ir.ui.menu'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM ir_ui_menu WHERE id = ir_model_data.res_id);
|
||||||
|
-- Other cleanups:
|
||||||
|
-- DELETE from ir_model_data where module = 'audittrail' AND model = 'ir.ui.view' AND NOT EXISTS( SELECT 1 FROM ir_ui_view WHERE ir_ui_view.id = ir_model_data.res_id);
|
||||||
|
-- DELETE from ir_model_data where module = 'audittrail' AND model = 'ir.ui.menu' AND NOT EXISTS( SELECT 1 FROM ir_ui_menu WHERE id = ir_model_data.res_id);
|
Loading…
Reference in New Issue