diff --git a/MANIFEST.in b/MANIFEST.in
index 84ed45985a9..ac545d3d8d4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -13,5 +13,5 @@ recursive-include pixmaps *
recursive-include win32 *
recursive-include doc *
recursive-include bin *xml *xsl *sql *rml *sxw *csv *rng
-graft bin/addons/
+graft bin/addons
global-exclude *pyc *~
diff --git a/README b/README
index 80fb4701288..5725ce9f866 100644
--- a/README
+++ b/README
@@ -15,16 +15,3 @@ database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ..
For more information, please visit:
http://www.openerp.com
-About Tiny.be
-----------------
-
-Tiny.be is a company specialising in the development of high-level applications
-and websites. All the company products are free software, released under the
-GNU GPL license.
-
-Our main products include: OpenERP (ERP & CRM for SMB), Tiny eCommerce
-(complete eCommerce system), OpenReport (automated generation of complex
-documents), Tiny Raytracer, ...
-
-For more information, please visit:
-http://www.tiny.be
diff --git a/bin/addons/__init__.py b/bin/addons/__init__.py
index c8ef1719b3a..0b5c6dc09c5 100644
--- a/bin/addons/__init__.py
+++ b/bin/addons/__init__.py
@@ -203,14 +203,14 @@ def get_module_filetree(module, dir='.'):
return tree
-def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
- """Compress a module directory
+def zip_directory(directory, b64enc=True, src=True):
+ """Compress a directory
- @param module_directory: The module directory
+ @param directory: The directory to compress
@param base64enc: if True the function will encode the zip file with base64
@param src: Integrate the source files
- @return: a stream to store in a file-like object
+ @return: a string containing the zip file
"""
RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
@@ -225,16 +225,16 @@ def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=T
archname = StringIO()
archive = PyZipFile(archname, "w", ZIP_DEFLATED)
- archive.writepy(module_directory)
- _zippy(archive, module_directory, src=src)
+ archive.writepy(directory)
+ _zippy(archive, directory, src=src)
archive.close()
- val = archname.getvalue()
+ archive_data = archname.getvalue()
archname.close()
if b64enc:
- val = base64.encodestring(val)
+ return base64.encodestring(archive_data)
- return val
+ return archive_data
def get_module_as_zip(modulename, b64enc=True, src=True):
"""Generate a module as zip file with the source or not and can do a base64 encoding
@@ -256,7 +256,7 @@ def get_module_as_zip(modulename, b64enc=True, src=True):
if b64enc:
val = base64.encodestring(val)
else:
- val = get_module_as_zip_from_module_directory(ap, b64enc, src)
+ val = zip_directory(ap, b64enc, src)
return val
@@ -270,7 +270,18 @@ def get_module_resource(module, *args):
@return: absolute path to the resource
"""
a = get_module_path(module)
- return a and opj(a, *args) or False
+ if not a: return False
+ resource_path = opj(a, *args)
+ if zipfile.is_zipfile( a +'.zip') :
+ zip = zipfile.ZipFile( a + ".zip")
+ files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
+ resource_path = '/'.join(args)
+ if resource_path in files:
+ return opj(a, resource_path)
+ elif os.path.exists(resource_path):
+ return resource_path
+ return False
+
def get_modules():
@@ -297,9 +308,10 @@ def load_information_from_description_file(module):
"""
:param module: The name of the module (sale, purchase, ...)
"""
+
for filename in ['__openerp__.py', '__terp__.py']:
description_file = get_module_resource(module, filename)
- if os.path.isfile(description_file):
+ if description_file :
return eval(tools.file_open(description_file).read())
#TODO: refactor the logger in this file to follow the logging guidelines
@@ -332,9 +344,8 @@ def upgrade_graph(graph, cr, module_list, force=None):
for module in module_list:
mod_path = get_module_path(module)
terp_file = get_module_resource(module, '__openerp__.py')
- if not terp_file or not os.path.isfile(terp_file):
+ if not terp_file:
terp_file = get_module_resource(module, '__terp__.py')
-
if not mod_path or not terp_file:
logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module))
continue
@@ -590,7 +601,7 @@ log = logging.getLogger('init')
def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
- def process_sql_file(cr, file):
+ def process_sql_file(cr, fp):
queries = fp.read().split(';')
for query in queries:
new_query = ' '.join(query.split())
@@ -634,14 +645,14 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
def load_test(cr, module_name, id_map, mode):
cr.commit()
- if not tools.config.options['test-disable']:
+ if not tools.config.options['test_disable']:
try:
_load_data(cr, module_name, id_map, mode, 'test')
except Exception, e:
logger.notifyChannel('ERROR', netsvc.LOG_TEST, e)
pass
finally:
- if tools.config.options['test-commit']:
+ if tools.config.options['test_commit']:
cr.commit()
else:
cr.rollback()
@@ -655,7 +666,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
file = tools.file_open(pathname)
# TODO manage .csv file with noupdate == (kind == 'init')
if ext == '.sql':
- process_sql_file(cr, fp)
+ process_sql_file(cr, file)
elif ext == '.csv':
noupdate = (kind == 'init')
tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate)
@@ -757,16 +768,21 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
return has_updates
+def _check_module_names(cr, module_names):
+ mod_names = set(module_names)
+ if 'base' in mod_names:
+ # ignore dummy 'all' module
+ if 'all' in mod_names:
+ mod_names.remove('all')
+ if mod_names:
+ cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
+ if cr.dictfetchone()['count'] != len(mod_names):
+ # find out what module name(s) are incorrect:
+ cr.execute("SELECT name FROM ir_module_module")
+ incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
+ logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
+
def load_modules(db, force_demo=False, status=None, update_module=False):
-
- def check_module_name(cr, mods, state):
- for mod in mods:
- id = modobj.search(cr, 1, ['&', ('state', '=', state), ('name', '=', mod)])
- if id:
- getattr(modobj, states[state])(cr, 1, id)
- elif mod != 'all':
- logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: invalid module name!' % (mod))
-
if not status:
status = {}
cr = db.cursor()
@@ -795,16 +811,23 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if update_module:
modobj = pool.get('ir.module.module')
- states = {'installed': 'button_upgrade', 'uninstalled': 'button_install'}
logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
if ('base' in tools.config['init']) or ('base' in tools.config['update']):
modobj.update_list(cr, 1)
+ _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
+
mods = [k for k in tools.config['init'] if tools.config['init'][k]]
- check_module_name(cr, mods, 'uninstalled')
+ if mods:
+ ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
+ if ids:
+ modobj.button_install(cr, 1, ids)
mods = [k for k in tools.config['update'] if tools.config['update'][k]]
- check_module_name(cr, mods, 'installed')
+ if mods:
+ ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
+ if ids:
+ modobj.button_upgrade(cr, 1, ids)
cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
diff --git a/bin/addons/base/__openerp__.py b/bin/addons/base/__openerp__.py
index 6a02009a8de..492c397754d 100644
--- a/bin/addons/base/__openerp__.py
+++ b/bin/addons/base/__openerp__.py
@@ -26,8 +26,8 @@
'version': '1.2',
'category': 'Generic Modules/Base',
'description': """The kernel of OpenERP, needed for all installation.""",
- 'author': 'OpenERP s.a.',
- 'maintainer': 'OpenERP s.a.',
+ 'author': 'OpenERP SA',
+ 'maintainer': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': [],
'init_xml': [
@@ -43,28 +43,39 @@
'ir/wizard/wizard_menu_view.xml',
'ir/ir.xml',
'ir/workflow/workflow_view.xml',
- 'module/module_wizard.xml',
'module/module_view.xml',
- 'module/module_web_view.xml',
'module/module_data.xml',
'module/module_report.xml',
+ 'module/wizard/base_module_import_view.xml',
+ 'module/wizard/base_module_update_view.xml',
+ 'module/wizard/base_language_install_view.xml',
+ 'module/wizard/base_import_language_view.xml',
+ 'module/wizard/base_module_upgrade_view.xml',
+ 'module/wizard/base_module_configuration_view.xml',
+ 'module/wizard/base_export_language_view.xml',
+ 'module/wizard/base_update_translations_view.xml',
'res/res_request_view.xml',
'res/res_lang_view.xml',
'res/res_log_view.xml',
'res/partner/partner_report.xml',
'res/partner/partner_view.xml',
- 'res/partner/partner_wizard.xml',
'res/bank_view.xml',
'res/country_view.xml',
- 'res/res_currency_view.xml',
+ 'res/res_currency_view.xml',
'res/partner/crm_view.xml',
'res/partner/wizard/partner_sms_send_view.xml',
+ 'res/partner/wizard/partner_wizard_spam_view.xml',
+ 'res/partner/wizard/partner_clear_ids_view.xml',
+ 'res/partner/wizard/partner_wizard_ean_check_view.xml',
'res/partner/partner_data.xml',
+
'res/ir_property_view.xml',
'security/base_security.xml',
'maintenance/maintenance_view.xml',
- 'security/ir.model.access.csv'
+ 'security/ir.model.access.csv',
+ 'res/res_widget_view.xml',
+ 'res/res_widget_data.xml',
],
'demo_xml': [
'base_demo.xml',
diff --git a/bin/addons/base/base.sql b/bin/addons/base/base.sql
index bb792b78845..71bfda541c4 100644
--- a/bin/addons/base/base.sql
+++ b/bin/addons/base/base.sql
@@ -148,7 +148,6 @@ CREATE TABLE res_users (
context_lang varchar(64) default '',
-- No FK references below, will be added later by ORM
-- (when the destination rows exist)
- action_id int,
company_id int,
primary key(id)
);
@@ -160,20 +159,6 @@ CREATE TABLE res_groups (
primary key(id)
);
-create table res_roles (
- id serial NOT NULL,
- parent_id int references res_roles on delete set null,
- name varchar(64) NOT NULL,
- primary key(id)
-);
-
-CREATE TABLE res_roles_users_rel (
- uid integer NOT NULL references res_users on delete cascade,
- rid integer NOT NULL references res_roles on delete cascade
-);
-create index res_roles_users_rel_uid_idx on res_roles_users_rel (uid);
-create index res_roles_users_rel_rid_idx on res_roles_users_rel (rid);
-
CREATE TABLE res_groups_users_rel (
uid integer NOT NULL references res_users on delete cascade,
gid integer NOT NULL references res_groups on delete cascade
@@ -223,7 +208,7 @@ create table wkf_transition
trigger_expr_id varchar(128) default NULL,
signal varchar(64) default null,
- role_id int references res_roles on delete set null,
+ group_id int references res_groups on delete set null,
primary key(id)
);
@@ -302,6 +287,7 @@ CREATE TABLE ir_module_module (
certificate character varying(64),
description text,
demo boolean default False,
+ web boolean DEFAULT FALSE,
primary key(id)
);
ALTER TABLE ir_module_module add constraint name_uniq unique (name);
@@ -344,7 +330,7 @@ CREATE TABLE ir_model_data (
-- Users
---------------------------------
-insert into res_users (id,login,password,name,action_id,active,company_id) values (1,'admin',NULL,'Administrator',NULL,True,1);
+insert into res_users (id,login,password,name,active,company_id,context_lang) values (1,'admin','admin','Administrator',True,1,'en_US');
insert into ir_model_data (name,module,model,noupdate,res_id) values ('user_root','base','res.users',True,1);
-- Compatibility purpose, to remove V6.0
diff --git a/bin/addons/base/base_data.xml b/bin/addons/base/base_data.xml
index 3bc6e4d6322..b907f8d19d6 100644
--- a/bin/addons/base/base_data.xml
+++ b/bin/addons/base/base_data.xml
@@ -915,7 +915,7 @@
United Kingdom
- uk
+ gb
USA Minor Outlying Islands
@@ -1021,6 +1021,7 @@
EUR
EUR
+ €
0.01
4
@@ -1031,7 +1032,7 @@
-
+
OpenERP S.A.
@@ -1041,7 +1042,7 @@
IBAN: BE74 1262 0132 6907 - SWIFT: CPHBBE75 - VAT: BE0477.472.701
-
+
OpenERP S.A.
@@ -1051,9 +1052,8 @@
Administrator
-
-
+
@@ -1066,6 +1066,7 @@
+
@@ -1073,263 +1074,309 @@
USD
USD
+ $
0.01
4
- 1.3785
+ 1.2834
-
+
Bs
VEB
+ Bs
2.95
4
- 3132.9
+ 2768.45
-
+
CAD
CAD
+ $
0.01
4
- 1.451
+ 1.3388
-
-
+
+
CHF
CHF
+ CHF
0.01
4
- 1.644
+ 1.3086
-
+
BRL
BRL
+ R$
0.01
4
- 2.588
+ 2.2344
-
+
CNY
CNY
+ ¥
0.01
4
- 10.4311
+ 8.7556
-
-
+
+
COP
COP
+ $
0.01
4
+
+ 2933.8378
+
+
+
Kč
CZK
+ Kč
0.01
4
-
+
+ 26.5634
+
+
+
+
kr
DKK
+ kr
0.01
4
- 7.4416
+ 7.4445
-
-
+
+
Ft
HUF
+ Ft
0.01
4
+
+ 271.5621
+
+
+
Rs
IDR
+ Rs
0.01
4
-
+
+ 65.8287
+
+
+
+
+ 58.8287
+
+
+
+
Ls
LVL
+ Ls
0.01
4
- 0.71
+ 0.7086
-
-
+
+
kr
NOK
+ kr
0.01
4
- 7.93
+ 7.8668
-
-
+
+
PAB
PAB
+ B/.
0.01
4
- 1.3813
+ 1.2676
-
-
+
zł
PLN
+ zł
0.01
4
+
+ 4.1005
+
+
+
kr
SEK
+ kr
0.01
4
+
+ 10.3004
+
+
+
GBP
GBP
+ ₤
0.01
4
- 0.675
+ 0.8333
-
+
ARS
ARS
+ $
0.01
4
+
+
+ 5.0881
+
+
-
-
- 5.6
-
-
-
-
+
- INR
- Rs
+ Rs
+ INR
+ Rs
0.01
4
- 0.634
+ 59.9739
-
- 0.631
-
- 2001-01-01
-
-
- 0.632
-
- 2002-01-01
-
-
AUD
AUD
+ $
0.01
4
- 1.5266
+ 1.4070
-
+
UAH
UAH
+ ₴
0.01
4
- 10.8266
+ 10.1969
-
+
+ Reserve
+ RSV
+
diff --git a/bin/addons/base/base_demo.xml b/bin/addons/base/base_demo.xml
index 235e7e77b75..7779171fe08 100644
--- a/bin/addons/base/base_demo.xml
+++ b/bin/addons/base/base_demo.xml
@@ -6,8 +6,6 @@
demo
Demo User
Mr Demo
-
-
diff --git a/bin/addons/base/base_menu.xml b/bin/addons/base/base_menu.xml
index 5da97026e48..676fd2ff015 100644
--- a/bin/addons/base/base_menu.xml
+++ b/bin/addons/base/base_menu.xml
@@ -2,27 +2,32 @@
+
-
+ groups="base.group_no_one"/>
+
-
+
+ groups="base.group_extended" parent="base.menu_translation" sequence="3"/>
-
-
+
+
+
+
diff --git a/bin/addons/base/base_update.xml b/bin/addons/base/base_update.xml
index e1f6519de20..2448306ad79 100644
--- a/bin/addons/base/base_update.xml
+++ b/bin/addons/base/base_update.xml
@@ -79,19 +79,22 @@
+ context="{'user_id': self, 'user_preference': 1}" groups="base.group_multi_company"
+ on_change="on_change_company_id(company_id)" />
+
+
-
-
+
+
-
+
-
+
@@ -112,27 +115,40 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
@@ -147,8 +163,8 @@
-
-
+
+
@@ -159,10 +175,10 @@
search
-
-
-
-
+
+
+
+
@@ -179,7 +195,7 @@
+
+
+
+ ir.values.tree.action
+ ir.values
+ tree
+
+
+
+
+
+
+
+
+
+
+
+ ir.values.search.action
+ ir.values
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Connect Actions To Client Events
+ Client Events
ir.actions.act_window
ir.values
form
- form,tree
-
+ tree,form
+
[('key','=','action')]
{'read':'default','default_object':1}
-
+
+
+ tree
+
+
+
+
+
+
+ form
+
+
+
+
+
@@ -89,8 +155,6 @@
{'read':'default'}
-
-
@@ -102,7 +166,7 @@
-
+
@@ -137,24 +201,38 @@
+
ir.sequence.tree
ir.sequence
tree
-
-
-
-
+
-
+
+
+
+
+ ir.sequence.search
+ ir.sequence
+ search
+
+
+
+
+
+
+
+
+
+
Sequences
ir.actions.act_window
@@ -174,14 +252,38 @@
form
-
+
+
+ ir.sequence.type.tree
+ ir.sequence.type
+ tree
+
+
+
+
+
+
+
+
+
+ ir.sequence.type.search
+ ir.sequence.type
+ search
+
+
+
+
+
+
+
+
- Sequence Types
+ Sequence Codes
ir.actions.act_window
ir.sequence.type
form
@@ -200,8 +302,8 @@
form
-
-
+
+
@@ -217,6 +319,17 @@
+
+ ir.actions.actions.search
+ ir.actions.actions
+ search
+
+
+
+
+
+
+
Actions
@@ -224,9 +337,11 @@
ir.actions.actions
form
+
-
+
+
@@ -236,12 +351,17 @@
form
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -254,8 +374,8 @@
tree
-
-
+
+
@@ -263,6 +383,27 @@
+
+
+
+ ir.filters.search
+ ir.filters
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -271,6 +412,7 @@
ir.filters
form
+
@@ -278,64 +420,27 @@
-
-
- ir.actions.report.custom
- ir.actions.report.custom
- form
-
-
-
-
-
-
-
-
-
-
- ir.actions.report.custom.tree
- ir.actions.report.custom
- tree
-
-
-
-
-
-
-
-
-
- Report Custom
- ir.actions.act_window
- ir.actions.report.custom
- form
-
-
-
-
ir.actions.report.xml
ir.actions.report.xml
form
-
+
-
+
-
+
-
+
-
-
@@ -350,6 +455,7 @@
+
@@ -374,13 +480,34 @@
+
+ ir.actions.report.xml.search
+ ir.actions.report.xml
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Report Xml
+ Reports
ir.actions.act_window
ir.actions.report.xml
form
+
@@ -450,8 +577,6 @@
-
-
@@ -459,11 +584,31 @@
+
+ ir.actions.windows.search
+ ir.actions.act_window
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Window Actions
ir.actions.act_window
ir.actions.act_window
form
+
@@ -498,20 +643,42 @@
form
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ir.actions.wizard.search
+ ir.actions.wizard
+ search
+
+
+
+
+
+
+
+
+
Wizards
ir.actions.act_window
ir.actions.wizard
form
+
@@ -520,7 +687,6 @@
@@ -539,10 +705,11 @@
ir.actions.act_window
res.company
form
+ Create and manage the companies that will be managed by OpenERP from here. Shops or subsidiaries can be created and maintained from here.
-
+
Users
ir.actions.act_window
@@ -550,6 +717,7 @@
form
+ Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system.
@@ -591,51 +759,11 @@
ir.actions.act_window
res.groups
form
+ A group is a set of functional areas that will be assigned to the user in order to give them access and rights to specific applications and tasks in the system. You can create custom groups or edit the ones existing by default in order to customize the view of the menu that users will be able to see. Whether they can have a read, write, create and delete access right can be managed from here.
-
- res.roles.form
- res.roles
- form
-
-
-
-
-
-
-
-
- res.roles.tree
- res.roles
- tree
- child_id
-
-
-
-
-
-
-
- Roles Structure
- ir.actions.act_window
- res.roles
- tree
-
-
-
-
-
- Roles
- ir.actions.act_window
- res.roles
- form
-
-
-
ir.ui.view
@@ -643,45 +771,83 @@
form
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ir.ui.view.tree
ir.ui.view
tree
+
-
-
+
+
+ ir.ui.view.search
+ ir.ui.view
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Views
ir.actions.act_window
ir.ui.view
+ Views allows you to personalize each view of OpenERP. You can add new fields, move fields, rename them or delete the ones that you do not need.
@@ -692,14 +858,15 @@
form
-
-
+
+
+
+
-
-
+
@@ -736,11 +903,50 @@
+
+
+
+ ir.attachment.search
+ ir.attachment
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Attachments
@@ -748,109 +954,10 @@
ir.attachment
form
+
-
-
- ir.report.custom.fields.tree
- ir.report.custom.fields
- tree
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ir.report.custom.fields
- ir.report.custom.fields
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ir.report.custom
- ir.report.custom
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Custom Report
- ir.actions.act_window
- ir.report.custom
- tree
-
-
-
-
-
ir.model.form
@@ -859,9 +966,10 @@
-
-
-
+
+
+
+
@@ -900,14 +1008,16 @@
-
+
-
-
-
-
+
+
@@ -923,7 +1033,7 @@
-
+
@@ -939,10 +1049,41 @@
+
+
+
+ ir.model.search
+ ir.model
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -951,34 +1092,33 @@
form
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
-
+
+
-
+
@@ -986,7 +1126,6 @@
-
ir.model.fields.tree
ir.model.fields
@@ -995,22 +1134,58 @@
+
+
+
+
+ ir.model.fields.search
+ ir.model.fields
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ir.model.data.form
ir.model.data
form
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -1022,16 +1197,40 @@
ir.model.data
tree
-
+
-
+
-
+
+ ir.model.data.search
+ ir.model.data
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Objects
ir.model
@@ -1052,12 +1251,12 @@
- Module data
+ Object Identifiers
ir.model.data
form
-
@@ -1068,7 +1267,7 @@
search
-
@@ -1085,12 +1284,19 @@
form
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -1115,7 +1321,7 @@
form
-
+
@@ -1299,7 +1593,7 @@
-
+
@@ -1314,8 +1608,8 @@
-
-
+
+
+
+ Record rules
+ ir.rule
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Record Rules
ir.rule
form
+
-
+
Property multi-company
@@ -1371,10 +1690,10 @@
-
-
-
-
+
+
+
+
@@ -1385,12 +1704,12 @@
-
-
+
+
-
+
@@ -1409,9 +1728,9 @@
-
+
-
+
@@ -1457,6 +1776,25 @@
+
+ ir.actions.server.search
+ ir.actions.server
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Server Actions
ir.actions.act_window
@@ -1464,31 +1802,10 @@
form
tree,form
+
{'key':'server_action'}
-
-
-
- ir.model.fields.tree
- ir.model.fields
- tree
-
-
-
-
-
-
-
-
-
-
-
- Access Controls Grid
- ir.model.grid
- form
- tree,form
-
-
+
ir.actions.todo
@@ -1497,30 +1814,55 @@
-
-
-
+
+
+
+
+
+
ir.actions.todo
Config Wizard Steps
form
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ir.actions.todo
+ ir.actions.todo.select
+ search
+
+
+
+
+
+
+
+
+
+
Configuration Wizards
ir.actions.todo
@@ -1529,13 +1871,22 @@
-
-
+
+
+
+ AutoVacuum osv_memory objects
+ 30
+ minutes
+ -1
+ True
+
+ osv_memory.autovacuum
+ power_on
+ ()
+
+
diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py
index 3b51af26f53..84419d25b51 100644
--- a/bin/addons/base/ir/ir_actions.py
+++ b/bin/addons/base/ir/ir_actions.py
@@ -28,15 +28,16 @@ from tools.translate import _
import netsvc
import re
import copy
-import sys
+import os
from xml import dom
+from report.report_sxw import report_sxw, report_rml
class actions(osv.osv):
_name = 'ir.actions.actions'
_table = 'ir_actions'
_columns = {
'name': fields.char('Action Name', required=True, size=64),
- 'type': fields.char('Action Type', required=True, size=32),
+ 'type': fields.char('Action Type', required=True, size=32,readonly=True),
'usage': fields.char('Action Usage', size=32),
}
_defaults = {
@@ -44,23 +45,6 @@ class actions(osv.osv):
}
actions()
-class report_custom(osv.osv):
- _name = 'ir.actions.report.custom'
- _table = 'ir_act_report_custom'
- _sequence = 'ir_actions_id_seq'
- _columns = {
- 'name': fields.char('Report Name', size=64, required=True, translate=True),
- 'type': fields.char('Report Type', size=32, required=True),
- 'model':fields.char('Object', size=64, required=True),
- 'report_id': fields.integer('Report Ref.', required=True),
- 'usage': fields.char('Action Usage', size=32),
- 'multi': fields.boolean('On multiple doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view.")
- }
- _defaults = {
- 'multi': lambda *a: False,
- 'type': lambda *a: 'ir.actions.report.custom',
- }
-report_custom()
class report_xml(osv.osv):
@@ -89,6 +73,27 @@ class report_xml(osv.osv):
res[report.id] = False
return res
+ def register_all(self, cr):
+ """Report registration handler that may be overridden by subclasses to
+ add their own kinds of report services.
+ Loads all reports with no manual loaders (auto==True) and
+ registers the appropriate services to implement them.
+ """
+ opj = os.path.join
+ cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
+ result = cr.dictfetchall()
+ svcs = netsvc.Service._services
+ for r in result:
+ if svcs.has_key('report.'+r['report_name']):
+ continue
+ if r['report_rml'] or r['report_rml_content_data']:
+ report_sxw('report.'+r['report_name'], r['model'],
+ opj('addons',r['report_rml'] or '/'), header=r['header'])
+ if r['report_xsl']:
+ report_rml('report.'+r['report_name'], r['model'],
+ opj('addons',r['report_xml']),
+ r['report_xsl'] and opj('addons',r['report_xsl']))
+
_name = 'ir.actions.report.xml'
_table = 'ir_act_report_xml'
_sequence = 'ir_actions_id_seq'
@@ -98,7 +103,7 @@ class report_xml(osv.osv):
'type': fields.char('Action Type', size=32, required=True),
'report_name': fields.char('Service Name', size=64, required=True),
'usage': fields.char('Action Usage', size=32),
- 'report_type': fields.selection([ ('pdf','pdf'), ('html','html'), ('raw','raw'), ('sxw','sxw'), ('txt','txt'), ('odt','odt'), ('html2html','HTML from HTML'), ('mako2html','HTML from Mako'), ], string='Report Type', required=True),
+ 'report_type': fields.char('Report Type', size=32, required=True, help="Report Type, e.g. pdf, html, raw, sxw, odt, html2html, mako2html, ..."),
'groups_id': fields.many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', 'Groups'),
'multi': fields.boolean('On multiple doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view."),
'attachment': fields.char('Save As Attachment Prefix', size=128, help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.'),
@@ -109,9 +114,13 @@ class report_xml(osv.osv):
'report_xsl': fields.char('XSL path', size=256),
'report_xml': fields.char('XML path', size=256, help=''),
- 'report_rml': fields.char('RML path', size=256, help="The .rml path of the file or NULL if the content is in report_rml_content"),
- 'report_sxw': fields.function(_report_sxw, method=True, type='char', string='SXW path'),
+ # Pending deprecation... to be replaced by report_file as this object will become the default report object (not so specific to RML anymore)
+ 'report_rml': fields.char('Main report file path', size=256, help="The path to the main report file (depending on Report Type) or NULL if the content is in another data field"),
+ # temporary related field as report_rml is pending deprecation - this field will replace report_rml after v6.0
+ 'report_file': fields.related('report_rml', type="char", size=256, required=False, readonly=False, string='Report file', help="The path to the main report file (depending on Report Type) or NULL if the content is in another field", store=True),
+
+ 'report_sxw': fields.function(_report_sxw, method=True, type='char', string='SXW path'),
'report_sxw_content_data': fields.binary('SXW content'),
'report_rml_content_data': fields.binary('RML content'),
'report_sxw_content': fields.function(_report_content, fnct_inv=_report_content_inv, method=True, type='binary', string='SXW content',),
@@ -210,12 +219,8 @@ class act_window(osv.osv):
return res
def _get_help_status(self, cr, uid, ids, name, arg, context={}):
- cr.execute(""" SELECT action.id,
- CASE WHEN r.uid IS NULL THEN True ELSE False END
- AS help_status FROM ir_act_window action
- LEFT JOIN ir_act_window_user_rel r ON
- (action.id = r.act_id AND (r.uid IS NULL or r.uid= %s)) WHERE action.id = ANY(%s)""",(uid,ids,))
- return dict(cr.fetchall())
+ activate_tips = self.pool.get('res.users').browse(cr, uid, uid).menu_tips
+ return dict([(id, activate_tips) for id in ids])
_columns = {
'name': fields.char('Action Name', size=64, translate=True),
@@ -245,15 +250,16 @@ class act_window(osv.osv):
'search_view_id': fields.many2one('ir.ui.view', 'Search View Ref.'),
'filter': fields.boolean('Filter'),
'auto_search':fields.boolean('Auto Search'),
- 'default_user_ids': fields.many2many('res.users', 'ir_act_window_user_rel', 'act_id', 'uid', 'Users'),
'search_view' : fields.function(_search_view, type='text', method=True, string='Search View'),
'menus': fields.char('Menus', size=4096),
'help': fields.text('Action description',
- help='Optional help text for the users with a description of the target view, such as its usage and purpose.'),
- 'display_help':fields.function(_get_help_status, type='boolean', method=True, string='Display Help',
- help='It gives the status if the help message has to be displayed or not when a user performs an action')
-
+ help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
+ translate=True),
+ 'display_menu_tip':fields.function(_get_help_status, type='boolean', method=True, string='Display Menu Tips',
+ help='It gives the status if the tip has to be displayed or not when a user executes an action'),
+ 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"),
}
+
_defaults = {
'type': lambda *a: 'ir.actions.act_window',
'view_type': lambda *a: 'form',
@@ -262,14 +268,10 @@ class act_window(osv.osv):
'limit': lambda *a: 80,
'target': lambda *a: 'current',
'auto_refresh': lambda *a: 0,
- 'auto_search':lambda *a: True
+ 'auto_search':lambda *a: True,
+ 'multi': False,
}
- def _auto_init(self, cr, context={}):
- super(act_window, self)._auto_init(cr, context)
- cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_action_uid_index\'')
- if not cr.fetchone():
- cr.execute('CREATE INDEX act_window_action_uid_index ON ir_act_window_user_rel (act_id, uid)')
- cr.commit()
+
act_window()
class act_window_view(osv.osv):
@@ -509,14 +511,21 @@ class actions_server(osv.osv):
return obj
- def merge_message(self, cr, uid, keystr, action, context):
+ def merge_message(self, cr, uid, keystr, action, context=None):
+ if context is None:
+ context = {}
logger = netsvc.Logger()
def merge(match):
obj_pool = self.pool.get(action.model_id.model)
id = context.get('active_id')
obj = obj_pool.browse(cr, uid, id)
exp = str(match.group()[2:-2]).strip()
- result = eval(exp, {'object':obj, 'context': context,'time':time})
+ result = eval(exp,
+ {
+ 'object': obj,
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'time': time,
+ })
if result in (None, False):
return str("--------")
return tools.ustr(result)
@@ -533,13 +542,16 @@ class actions_server(osv.osv):
# False : Finnished correctly
# ACTION_ID : Action to launch
- def run(self, cr, uid, ids, context={}):
+ # FIXME: refactor all the eval() calls in run()!
+ def run(self, cr, uid, ids, context=None):
logger = netsvc.Logger()
+ if context is None:
+ context = {}
for action in self.browse(cr, uid, ids, context):
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
- 'context':context,
+ 'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
'time':time,
'cr': cr,
@@ -557,26 +569,19 @@ class actions_server(osv.osv):
.read(cr, uid, action.action_id.id, context=context)
if action.state=='code':
- if config['server_actions_allow_code']:
- localdict = {
- 'self': self.pool.get(action.model_id.model),
- 'context': context,
- 'time': time,
- 'ids': ids,
- 'cr': cr,
- 'uid': uid,
- 'object':obj,
- 'obj': obj,
- }
- eval(action.code, localdict, mode="exec")
- if 'action' in localdict:
- return localdict['action']
- else:
- netsvc.Logger().notifyChannel(
- self._name, netsvc.LOG_ERROR,
- "%s is a `code` server action, but "
- "it isn't allowed in this configuration.\n\n"
- "See server options to enable it"%action)
+ localdict = {
+ 'self': self.pool.get(action.model_id.model),
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'time': time,
+ 'ids': ids,
+ 'cr': cr,
+ 'uid': uid,
+ 'object':obj,
+ 'obj': obj,
+ }
+ eval(action.code, localdict, mode="exec", nocopy=True) # nocopy allows to return 'action'
+ if 'action' in localdict:
+ return localdict['action']
if action.state == 'email':
user = config['email_from']
@@ -628,9 +633,9 @@ class actions_server(osv.osv):
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
cxt = {
- 'context':context,
+ 'context': dict(context), # copy context to prevent side-effects of eval
'object': obj,
- 'time':time,
+ 'time': time,
'cr': cr,
'pool' : self.pool,
'uid' : uid
@@ -648,7 +653,11 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- cxt = {'context':context, 'object': obj, 'time':time}
+ cxt = {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ }
expr = eval(euq, cxt)
else:
expr = exp.value
@@ -684,7 +693,12 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- expr = eval(euq, {'context':context, 'object': obj, 'time':time})
+ expr = eval(euq,
+ {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ })
else:
expr = exp.value
res[exp.col1.name] = expr
@@ -703,7 +717,12 @@ class actions_server(osv.osv):
if exp.type == 'equation':
obj_pool = self.pool.get(action.model_id.model)
obj = obj_pool.browse(cr, uid, context['active_id'], context=context)
- expr = eval(euq, {'context':context, 'object': obj, 'time':time})
+ expr = eval(euq,
+ {
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ 'object': obj,
+ 'time': time,
+ })
else:
expr = exp.value
res[exp.col1.name] = expr
@@ -733,7 +752,7 @@ act_window_close()
TODO_STATES = [('open', 'To Do'),
('done', 'Done'),
('skip','Skipped'),
- ('cancel','Cancel')]
+ ('cancel','Cancelled')]
class ir_actions_todo(osv.osv):
_name = 'ir.actions.todo'
@@ -749,11 +768,26 @@ class ir_actions_todo(osv.osv):
'note':fields.text('Text', translate=True),
}
_defaults={
- 'state': lambda *a: 'open',
- 'sequence': lambda *a: 10,
- 'restart': lambda *a: 'always',
+ 'state': 'open',
+ 'sequence': 10,
+ 'restart': 'onskip',
}
_order="sequence,id"
+
+ def action_launch(self, cr, uid, ids, context=None):
+ """ Launch Action of Wizard"""
+ if context is None:
+ context = {}
+ wizard_id = ids and ids[0] or False
+ wizard = self.browse(cr, uid, wizard_id, context=context)
+ res = self.pool.get('ir.actions.act_window').read(cr, uid, wizard.action_id.id, ['name', 'view_type', 'view_mode', 'res_model', 'context', 'views', 'type'], context=context)
+ res.update({'target':'new', 'nodestroy': True})
+ return res
+
+ def action_open(self, cr, uid, ids, context=None):
+ """ Sets configuration wizard in TODO state"""
+ return self.write(cr, uid, ids, {'state': 'open'}, context=context)
+
ir_actions_todo()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/ir/ir_attachment.py b/bin/addons/base/ir/ir_attachment.py
index abba9222d25..fb621ea58fc 100644
--- a/bin/addons/base/ir/ir_attachment.py
+++ b/bin/addons/base/ir/ir_attachment.py
@@ -105,8 +105,8 @@ class ir_attachment(osv.osv):
'datas': fields.binary('Data'),
'datas_fname': fields.char('Filename',size=256),
'description': fields.text('Description'),
- 'res_name': fields.function(_name_get_resname, type='char',
- string='Resource Name', method=True),
+ 'res_name': fields.function(_name_get_resname, type='char', size=128,
+ string='Resource Name', method=True, store=True),
'res_model': fields.char('Resource Object',size=64, readonly=True,
help="The database object this attachment will be attached to"),
'res_id': fields.integer('Resource ID', readonly=True,
@@ -117,11 +117,13 @@ class ir_attachment(osv.osv):
'Type', help="Binary File or external URL", required=True),
'create_date': fields.datetime('Date Created', readonly=True),
- 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
+ 'create_uid': fields.many2one('res.users', 'Owner', readonly=True),
+ 'company_id': fields.many2one('res.company', 'Company'),
}
_defaults = {
'type': 'binary',
+ 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
}
def _auto_init(self, cr, context=None):
diff --git a/bin/addons/base/ir/ir_cron.py b/bin/addons/base/ir/ir_cron.py
index 905b2dc1778..06121533922 100644
--- a/bin/addons/base/ir/ir_cron.py
+++ b/bin/addons/base/ir/ir_cron.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
##############################################################################
#
-# OpenERP, Open Source Management Solution
+# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (). All Rights Reserved
# $Id$
#
@@ -47,15 +47,15 @@ class ir_cron(osv.osv, netsvc.Agent):
'name': fields.char('Name', size=60, required=True),
'user_id': fields.many2one('res.users', 'User', required=True),
'active': fields.boolean('Active'),
- 'interval_number': fields.integer('Interval Number'),
+ 'interval_number': fields.integer('Interval Number',help="Repeat every x."),
'interval_type': fields.selection( [('minutes', 'Minutes'),
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
- 'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates that the function will always be called'),
- 'doall' : fields.boolean('Repeat Missed'),
- 'nextcall' : fields.datetime('Next Call Date', required=True),
- 'model': fields.char('Object', size=64),
- 'function': fields.char('Function', size=64),
- 'args': fields.text('Arguments'),
+ 'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates no limit'),
+ 'doall' : fields.boolean('Repeat Missed', help="Enable this if you want to execute missed occurences as soon as the server restarts."),
+ 'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this scheduler"),
+ 'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"),
+ 'function': fields.char('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."),
+ 'args': fields.text('Arguments', help="Arguments to be passed to the method. e.g. (uid,)"),
'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent')
}
@@ -77,7 +77,7 @@ class ir_cron(osv.osv, netsvc.Agent):
except:
return False
return True
-
+
_constraints = [
(_check_args, 'Invalid arguments', ['args']),
]
@@ -98,7 +98,7 @@ class ir_cron(osv.osv, netsvc.Agent):
try:
db, pool = pooler.get_db_and_pool(db_name)
except:
- return False
+ return False
cr = db.cursor()
try:
if not pool._init:
@@ -107,7 +107,7 @@ class ir_cron(osv.osv, netsvc.Agent):
for job in cr.dictfetchall():
nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
numbercall = job['numbercall']
-
+
ok = False
while nextcall < now and numbercall:
if numbercall > 0:
@@ -125,12 +125,12 @@ class ir_cron(osv.osv, netsvc.Agent):
cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active and nextcall>=now()')
- next_call = cr.dictfetchone()['min_next_call']
+ next_call = cr.dictfetchone()['min_next_call']
if next_call:
next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
else:
next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day
-
+
if not check:
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
@@ -138,29 +138,31 @@ class ir_cron(osv.osv, netsvc.Agent):
logger = netsvc.Logger()
logger.notifyChannel('cron', netsvc.LOG_WARNING,
'Exception in cron:'+str(ex))
-
+
finally:
cr.commit()
cr.close()
-
+ def restart(self, dbname):
+ self.cancel(dbname)
+ self._poolJobs(dbname)
+
def create(self, cr, uid, vals, context=None):
- res = super(ir_cron, self).create(cr, uid, vals, context=context)
+ res = super(ir_cron, self).create(cr, uid, vals, context=context)
cr.commit()
- self.cancel(cr.dbname)
- self._poolJobs(cr.dbname)
+ self.restart(cr.dbname)
return res
+
def write(self, cr, user, ids, vals, context=None):
res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
cr.commit()
- self.cancel(cr.dbname)
- self._poolJobs(cr.dbname)
+ self.restart(cr.dbname)
return res
+
def unlink(self, cr, uid, ids, context=None):
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
cr.commit()
- self.cancel(cr.dbname)
- self._poolJobs(cr.dbname)
+ self.restart(cr.dbname)
return res
ir_cron()
diff --git a/bin/addons/base/ir/ir_exports.py b/bin/addons/base/ir/ir_exports.py
index 48f4141d0b5..39477f51fbe 100644
--- a/bin/addons/base/ir/ir_exports.py
+++ b/bin/addons/base/ir/ir_exports.py
@@ -26,7 +26,7 @@ class ir_exports(osv.osv):
_name = "ir.exports"
_columns = {
'name': fields.char('Export Name', size=128),
- 'resource': fields.char('Resource', size=128),
+ 'resource': fields.char('Resource', size=128, select=True),
'export_fields': fields.one2many('ir.exports.line', 'export_id',
'Export ID'),
}
diff --git a/bin/addons/base/ir/ir_filters.py b/bin/addons/base/ir/ir_filters.py
index 172b178dddf..0146b8dd85b 100644
--- a/bin/addons/base/ir/ir_filters.py
+++ b/bin/addons/base/ir/ir_filters.py
@@ -20,6 +20,7 @@
##############################################################################
from osv import osv, fields
+from tools.translate import _
class ir_filters(osv.osv):
'''
@@ -37,12 +38,29 @@ class ir_filters(osv.osv):
my_acts = self.read(cr, uid, act_ids, ['name', 'domain','context'])
return my_acts
+ def create_or_replace(self, cr, uid, vals, context=None):
+ filter_id = None
+ lower_name = vals['name'].lower()
+ matching_filters = [x for x in self.get_filters(cr, uid, vals['model_id'])
+ if x['name'].lower() == lower_name]
+ if matching_filters:
+ self.write(cr, uid, matching_filters[0]['id'], vals, context)
+ return False
+ return self.create(cr, uid, vals, context)
+
+ def _auto_init(self, cr, context={}):
+ super(ir_filters, self)._auto_init(cr, context)
+ # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
+ cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_index'")
+ if not cr.fetchone():
+ cr.execute('CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_index" ON ir_filters (lower(name), model_id, user_id)')
+
_columns = {
'name': fields.char('Action Name', size=64, translate=True, required=True),
'user_id':fields.many2one('res.users', 'User', help='False means for every user'),
- 'domain': fields.char('Domain Value', size=250, required=True),
- 'context': fields.char('Context Value', size=250, required=True),
- 'model_id': fields.selection(_list_all_models, 'Model', required=True),
+ 'domain': fields.text('Domain Value', required=True),
+ 'context': fields.text('Context Value', required=True),
+ 'model_id': fields.selection(_list_all_models, 'Object', size=64, required=True),
}
ir_filters()
diff --git a/bin/addons/base/ir/ir_model.py b/bin/addons/base/ir/ir_model.py
index a3b361d7575..3f3700b0e45 100644
--- a/bin/addons/base/ir/ir_model.py
+++ b/bin/addons/base/ir/ir_model.py
@@ -63,12 +63,13 @@ class ir_model(osv.osv):
is_osv_mem = self._is_osv_memory(cr, uid, all_model_ids, 'osv_memory', arg=None, context=context)
return [('id', 'in', [id for id in is_osv_mem if bool(is_osv_mem[id]) == value])]
+
_columns = {
'name': fields.char('Object Name', size=64, translate=True, required=True),
'model': fields.char('Object', size=64, required=True, select=1),
'info': fields.text('Information'),
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
- 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Manually Created',readonly=True),
+ 'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
'access_ids': fields.one2many('ir.model.access', 'model_id', 'Access'),
'osv_memory': fields.function(_is_osv_memory, method=True, string='In-memory model', type='boolean',
fnct_search=_search_osv_memory,
@@ -142,103 +143,6 @@ class ir_model(osv.osv):
x_custom_model._rec_name = x_name
ir_model()
-
-class ir_model_grid(osv.osv):
- _name = 'ir.model.grid'
- _table = 'ir_model'
- _inherit = 'ir.model'
- _description = "Objects Security Grid"
-
- def create(self, cr, uid, vals, context=None):
- raise osv.except_osv('Error !', 'You cannot add an entry to this view !')
-
- def unlink(self, *args, **argv):
- raise osv.except_osv('Error !', 'You cannot delete an entry of this view !')
-
- def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
- result = super(osv.osv, self).read(cr, uid, ids, fields, context, load)
- allgr = self.pool.get('res.groups').search(cr, uid, [], context=context)
- acc_obj = self.pool.get('ir.model.access')
-
- if not isinstance(result,list):
- result=[result]
-
- for res in result:
- rules = acc_obj.search(cr, uid, [('model_id', '=', res['id'])])
- rules_br = acc_obj.browse(cr, uid, rules, context=context)
- for g in allgr:
- res['group_'+str(g)] = ''
- for rule in rules_br:
- perm_list = []
- if rule.perm_read:
- perm_list.append('r')
- if rule.perm_write:
- perm_list.append('w')
- if rule.perm_create:
- perm_list.append('c')
- if rule.perm_unlink:
- perm_list.append('u')
- perms = ",".join(perm_list)
- if rule.group_id:
- res['group_%d'%rule.group_id.id] = perms
- else:
- res['group_0'] = perms
- return result
-
- #
- # This function do not write fields from ir.model because
- # access rights may be different for managing models and
- # access rights
- #
- def write(self, cr, uid, ids, vals, context=None):
- vals_new = vals.copy()
- acc_obj = self.pool.get('ir.model.access')
- for grid in self.browse(cr, uid, ids, context=context):
- model_id = grid.id
- perms_rel = ['read','write','create','unlink']
- for val in vals:
- if not val[:6]=='group_':
- continue
- group_id = int(val[6:]) or False
- rules = acc_obj.search(cr, uid, [('model_id', '=', model_id),('group_id', '=', group_id)])
- if not rules:
- rules = [acc_obj.create(cr, uid, {
- 'name': grid.name,
- 'model_id':model_id,
- 'group_id':group_id
- }) ]
- vals2 = dict(map(lambda x: ('perm_'+x, x[0] in (vals[val] or '')), perms_rel))
- acc_obj.write(cr, uid, rules, vals2, context=context)
- return True
-
- def fields_get(self, cr, uid, fields=None, context=None):
- result = super(ir_model_grid, self).fields_get(cr, uid, fields, context)
- groups = self.pool.get('res.groups').search(cr, uid, [])
- groups_br = self.pool.get('res.groups').browse(cr, uid, groups)
- result['group_0'] = {'string': 'All Users','type': 'char','size': 7}
- for group in groups_br:
- result['group_%d'%group.id] = {'string': '%s'%group.name,'type': 'char','size': 7}
- return result
-
- def fields_view_get(self, cr, uid, view_id=None, view_type='form', context={}, toolbar=False, submenu=False):
- result = super(ir_model_grid, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
- groups = self.pool.get('res.groups').search(cr, uid, [])
- groups_br = self.pool.get('res.groups').browse(cr, uid, groups)
- cols = ['model', 'name']
- xml = '''
-<%s editable="bottom">
-
-
-
- ''' % (view_type,)
- for group in groups_br:
- xml += ''' ''' % (group.id, )
- xml += '''%s>''' % (view_type,)
- result['arch'] = xml
- result['fields'] = self.fields_get(cr, uid, cols, context)
- return result
-ir_model_grid()
-
class ir_model_fields(osv.osv):
_name = 'ir.model.fields'
_description = "Fields"
@@ -256,7 +160,7 @@ class ir_model_fields(osv.osv):
'select_level': fields.selection([('0','Not Searchable'),('1','Always Searchable'),('2','Advanced Search')],'Searchable', required=True),
'translate': fields.boolean('Translate'),
'size': fields.integer('Size'),
- 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Manually Created', required=True, readonly=True, select=1),
+ 'state': fields.selection([('manual','Custom Field'),('base','Base Field')],'Type', required=True, readonly=True, select=1),
'on_delete': fields.selection([('cascade','Cascade'),('set null','Set NULL')], 'On delete', help='On delete property for many2one fields'),
'domain': fields.char('Domain', size=256),
'groups': fields.many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id', 'Groups'),
@@ -305,7 +209,7 @@ class ir_model_fields(osv.osv):
raise except_orm(_('Error'), _("Custom fields must have a name that starts with 'x_' !"))
if vals.get('relation',False) and not self.pool.get('ir.model').search(cr, user, [('model','=',vals['relation'])]):
- raise except_orm(_('Error'), _("Model %s Does not Exist !" % vals['relation']))
+ raise except_orm(_('Error'), _("Model %s does not exist!") % vals['relation'])
if self.pool.get(vals['model']):
self.pool.get(vals['model']).__init__(self.pool, cr)
@@ -327,15 +231,13 @@ class ir_model_access(osv.osv):
'perm_read': fields.boolean('Read Access'),
'perm_write': fields.boolean('Write Access'),
'perm_create': fields.boolean('Create Access'),
- 'perm_unlink': fields.boolean('Delete Permission'),
+ 'perm_unlink': fields.boolean('Delete Access'),
}
def check_groups(self, cr, uid, group):
- res = False
grouparr = group.split('.')
if not grouparr:
return False
-
cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN (select res_id from ir_model_data where module=%s and name=%s)", (uid, grouparr[0], grouparr[1],))
return bool(cr.fetchone())
@@ -386,6 +288,12 @@ class ir_model_access(osv.osv):
else:
model_name = model
+ # osv_memory objects can be read by everyone, as they only return
+ # results that belong to the current user (except for superuser)
+ model_obj = self.pool.get(model_name)
+ if isinstance(model_obj, osv.osv_memory):
+ return True
+
# We check if a specific rule exists
cr.execute('SELECT MAX(CASE WHEN perm_' + mode + ' THEN 1 ELSE 0 END) '
' FROM ir_model_access a '
@@ -477,7 +385,7 @@ class ir_model_data(osv.osv):
'module': lambda *a: ''
}
_sql_constraints = [
- ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same id for the same module'),
+ ('module_name_uniq', 'unique(name, module)', 'You cannot have multiple records with the same id for the same module !'),
]
def __init__(self, pool, cr):
@@ -518,13 +426,11 @@ class ir_model_data(osv.osv):
return id
def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
- warning = True
model_obj = self.pool.get(model)
if not context:
context = {}
if xml_id and ('.' in xml_id):
- assert len(xml_id.split('.'))==2, _('"%s" contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id') % (xml_id)
- warning = False
+ assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id)
module, xml_id = xml_id.split('.')
if (not xml_id) and (not self.doinit):
return False
@@ -611,7 +517,6 @@ class ir_model_data(osv.osv):
return True
def ir_set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=None, xml_id=False):
- obj = self.pool.get('ir.values')
if type(models[0])==type([]) or type(models[0])==type(()):
model,res_id = models[0]
else:
@@ -649,7 +554,7 @@ class ir_model_data(osv.osv):
if model=='workflow.activity':
cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,))
wkf_todo.extend(cr.fetchall())
- cr.execute("update wkf_transition set condition='True', role_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
+ cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id))
cr.execute("delete from wkf_transition where act_to=%s", (res_id,))
for model,id in wkf_todo:
diff --git a/bin/addons/base/ir/ir_report_custom.py b/bin/addons/base/ir/ir_report_custom.py
deleted file mode 100644
index 036469fb417..00000000000
--- a/bin/addons/base/ir/ir_report_custom.py
+++ /dev/null
@@ -1,206 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-from osv import fields,osv
-from osv.orm import browse_null
-import ir
-import report.custom
-from tools.translate import _
-from tools.safe_eval import safe_eval as eval
-import netsvc
-
-class report_custom(osv.osv):
- _name = 'ir.report.custom'
- _columns = {
- 'name': fields.char('Report Name', size=64, required=True, translate=True),
- 'model_id': fields.many2one('ir.model','Object', required=True, change_default=True),
- 'type': fields.selection([('table','Tabular'),('pie','Pie Chart'),('bar','Bar Chart'),('line','Line Plot')], "Report Type", size=64, required='True'),
- 'title': fields.char("Report Title", size=64, required='True', translate=True),
- 'print_format': fields.selection((('A4','a4'),('A5','a5')), 'Print format', required=True),
- 'print_orientation': fields.selection((('landscape','Landscape'),('portrait','Portrait')), 'Print orientation', required=True, size=16),
- 'repeat_header': fields.boolean('Repeat Header'),
- 'footer': fields.char('Report Footer', size=64, required=True),
- 'sortby': fields.char('Sorted By', size=64),
- 'fields_child0': fields.one2many('ir.report.custom.fields', 'report_id','Fields', required=True),
- 'field_parent': fields.many2one('ir.model.fields','Child Field'),
- 'state': fields.selection([('unsubscribed','Unsubscribed'),('subscribed','Subscribed')], 'State', size=64),
- 'frequency': fields.selection([('Y','Yearly'),('M','Monthly'),('D','Daily')], 'Frequency', size=64),
- 'limitt': fields.char('Limit', size=9),
- 'menu_id': fields.many2one('ir.ui.menu', 'Menu')
- }
- _defaults = {
- 'print_format': lambda *a: 'A4',
- 'print_orientation': lambda *a: 'portrait',
- 'state': lambda *a: 'unsubscribed',
- 'type': lambda *a: 'table',
- 'footer': lambda *a: 'Generated by OpenERP'
- }
-
- def onchange_model_id(self, cr, uid, ids, model_id):
- if not(model_id):
- return {}
- return {'domain': {'field_parent': [('model_id','=',model_id)]}}
-
- def unsubscribe(self, cr, uid, ids, context={}):
-#TODO: should delete the ir.actions.report.custom for these reports and do an ir_del
- self.write(cr, uid, ids, {'state':'unsubscribed'})
- return True
-
- def subscribe(self, cr, uid, ids, context={}):
- for report in self.browse(cr, uid, ids):
- report.fields_child0.sort(lambda x,y : x.sequence - y.sequence)
-
- # required on field0 does not seem to work( cause we use o2m_l ?)
- if not report.fields_child0:
- raise osv.except_osv(_('Invalid operation'), _('Enter at least one field !'))
-
- if report.type in ['pie', 'bar', 'line'] and report.field_parent:
- raise osv.except_osv(_('Invalid operation'), _('Tree can only be used in tabular reports'))
-
- # Otherwise it won't build a good tree. See level.pop in custom.py.
- if report.type == 'table' and report.field_parent and report.fields_child0 and not report.fields_child0[0].groupby:
- raise osv.except_osv('Invalid operation :', 'When creating tree (field child) report, data must be group by the first field')
-
- if report.type == 'pie':
- if len(report.fields_child0) != 2:
- raise osv.except_osv(_('Invalid operation'), _('Pie charts need exactly two fields'))
- else:
- c_f = {}
- for i in range(2):
- c_f[i] = []
- tmp = report.fields_child0[i]
- for j in range(3):
- c_f[i].append((not isinstance(eval('tmp.field_child'+str(j)), browse_null) and eval('tmp.field_child'+str(j)+'.ttype')) or None)
- if not reduce(lambda x,y : x or y, map(lambda x: x in ['integer', 'float'], c_f[1])):
- raise osv.except_osv(_('Invalid operation'), _('Second field should be figures'))
-
- if report.type == 'bar':
- if len(report.fields_child0) < 2:
- raise osv.except_osv(_('Invalid operation'), _('Bar charts need at least two fields'))
- else:
- c_f = {}
- for i in range(len(report.fields_child0)):
- c_f[i] = []
- tmp = report.fields_child0[i]
- for j in range(3):
- c_f[i].append((not isinstance(eval('tmp.field_child'+str(j)), browse_null) and eval('tmp.field_child'+str(j)+'.ttype')) or None)
-
- if i == 0:
- pass
- else:
- if not reduce(lambda x,y : x or y, map(lambda x: x in ['integer', 'float'], c_f[i])):
- raise osv.except_osv(_('Invalid operation'), _('Field %d should be a figure') %(i,))
-
- if report.state=='subscribed':
- continue
-
- name = report.name
- model = report.model_id.model
-
- action_def = {'report_id':report.id, 'type':'ir.actions.report.custom', 'model':model, 'name':name}
- id = self.pool.get('ir.actions.report.custom').create(cr, uid, action_def)
- m_id = report.menu_id.id
- action = "ir.actions.report.custom,%d" % (id,)
- if not report.menu_id:
- ir.ir_set(cr, uid, 'action', 'client_print_multi', name, [(model, False)], action, False, True)
- else:
- ir.ir_set(cr, uid, 'action', 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(m_id))], action, False, True)
-
- self.write(cr, uid, [report.id], {'state':'subscribed'}, context)
- return True
-report_custom()
-
-
-class report_custom_fields(osv.osv):
- _name = 'ir.report.custom.fields'
- _columns = {
- 'name': fields.char('Name', size=64, required=True),
- 'report_id': fields.many2one('ir.report.custom', 'Report Ref', select=True),
- 'field_child0': fields.many2one('ir.model.fields', 'Field child0', required=True),
- 'fc0_operande': fields.many2one('ir.model.fields', 'Constraint'),
- 'fc0_condition': fields.char('Condition', size=64),
- 'fc0_op': fields.selection((('>','>'),('<','<'),('==','='),('in','in'),('gety,==','(year)=')), 'Relation'),
- 'field_child1': fields.many2one('ir.model.fields', 'Field child1'),
- 'fc1_operande': fields.many2one('ir.model.fields', 'Constraint'),
- 'fc1_condition': fields.char('condition', size=64),
- 'fc1_op': fields.selection((('>','>'),('<','<'),('==','='),('in','in'),('gety,==','(year)=')), 'Relation'),
- 'field_child2': fields.many2one('ir.model.fields', 'Field child2'),
- 'fc2_operande': fields.many2one('ir.model.fields', 'Constraint'),
- 'fc2_condition': fields.char('condition', size=64),
- 'fc2_op': fields.selection((('>','>'),('<','<'),('==','='),('in','in'),('gety,==','(year)=')), 'Relation'),
- 'field_child3': fields.many2one('ir.model.fields', 'Field child3'),
- 'fc3_operande': fields.many2one('ir.model.fields', 'Constraint'),
- 'fc3_condition': fields.char('condition', size=64),
- 'fc3_op': fields.selection((('>','>'),('<','<'),('==','='),('in','in'),('gety,==','(year)=')), 'Relation'),
- 'alignment': fields.selection((('left','left'),('right','right'),('center','center')), 'Alignment', required=True),
- 'sequence': fields.integer('Sequence', required=True),
- 'width': fields.integer('Fixed Width'),
- 'operation': fields.selection((('none', 'None'),('calc_sum','Calculate Sum'),('calc_avg','Calculate Average'),('calc_count','Calculate Count'),('calc_max', 'Get Max'))),
- 'groupby' : fields.boolean('Group By'),
- 'bgcolor': fields.char('Background Color', size=64),
- 'fontcolor': fields.char('Font color', size=64),
- 'cumulate': fields.boolean('Accumulate')
- }
- _defaults = {
- 'alignment': lambda *a: 'left',
- 'bgcolor': lambda *a: 'white',
- 'fontcolor': lambda *a: 'black',
- 'operation': lambda *a: 'none',
- }
- _order = "sequence"
-
- def onchange_any_field_child(self, cr, uid, ids, field_id, level):
- if not(field_id):
- return {}
- next_level_field_name = 'field_child%d' % (level+1)
- next_level_operande = 'fc%d_operande' % (level+1)
- field = self.pool.get('ir.model.fields').browse(cr, uid, [field_id])[0]
- res = self.pool.get(field.model).fields_get(cr, uid, field.name)
- if res[field.name].has_key('relation'):
- cr.execute('select id from ir_model where model=%s', (res[field.name]['relation'],))
- (id,) = cr.fetchone() or (False,)
- if id:
- return {
- 'domain': {
- next_level_field_name: [('model_id', '=', id)],
- next_level_operande: [('model_id', '=', id)]
- },
- 'required': {
- next_level_field_name: True
- }
- }
- else:
- netsvc.Logger().notifyChannel('web-services', netsvc.LOG_WARNING, _("Using a relation field which uses an unknown object"))
- return {'required': {next_level_field_name: True}}
- else:
- return {'domain': {next_level_field_name: []}}
-
- def get_field_child_onchange_method(level):
- return lambda self, cr, uid, ids, field_id: self.onchange_any_field_child(cr, uid, ids, field_id, level)
-
- onchange_field_child0 = get_field_child_onchange_method(0)
- onchange_field_child1 = get_field_child_onchange_method(1)
- onchange_field_child2 = get_field_child_onchange_method(2)
-report_custom_fields()
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/ir/ir_rule.py b/bin/addons/base/ir/ir_rule.py
index 23981458606..657dc65a12a 100644
--- a/bin/addons/base/ir/ir_rule.py
+++ b/bin/addons/base/ir/ir_rule.py
@@ -34,7 +34,7 @@ class ir_rule(osv.osv):
res = {}
for rule in self.browse(cr, uid, ids, context):
eval_user_data = {'user': self.pool.get('res.users').browse(cr, 1, uid),
- 'time':time}
+ 'time':time}
res[rule.id] = eval(rule.domain_force, eval_user_data)
return res
@@ -55,8 +55,8 @@ class ir_rule(osv.osv):
'model_id': fields.many2one('ir.model', 'Object',select=1, required=True),
'global': fields.function(_get_value, method=True, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone"),
'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'),
- 'domain_force': fields.char('Domain', size=250),
- 'domain': fields.function(_domain_force_get, method=True, string='Domain', type='char', size=250),
+ 'domain_force': fields.text('Domain'),
+ 'domain': fields.function(_domain_force_get, method=True, string='Domain', type='text'),
'perm_read': fields.boolean('Apply For Read'),
'perm_write': fields.boolean('Apply For Write'),
'perm_create': fields.boolean('Apply For Create'),
@@ -73,7 +73,7 @@ class ir_rule(osv.osv):
'global': True,
}
_sql_constraints = [
- ('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right'),
+ ('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right !'),
]
_constraints = [
(_check_model_obj, 'Rules are not supported for osv_memory objects !', ['model_id'])
@@ -103,16 +103,18 @@ class ir_rule(osv.osv):
JOIN res_groups_users_rel u_rel ON (g_rel.group_id = u_rel.gid)
WHERE u_rel.uid = %s) OR r.global)""", (model_name, uid))
ids = map(lambda x: x[0], cr.fetchall())
- for rule in self.browse(cr, uid, ids):
- for group in rule.groups:
- group_rule.setdefault(group.id, []).append(rule.id)
- if not rule.groups:
- global_rules.append(rule.id)
- dom = self.domain_create(cr, uid, global_rules)
- dom += ['|'] * (len(group_rule)-1)
- for value in group_rule.values():
- dom += self.domain_create(cr, uid, value)
- return dom
+ if ids:
+ for rule in self.browse(cr, uid, ids):
+ for group in rule.groups:
+ group_rule.setdefault(group.id, []).append(rule.id)
+ if not rule.groups:
+ global_rules.append(rule.id)
+ dom = self.domain_create(cr, uid, global_rules)
+ dom += ['|'] * (len(group_rule)-1)
+ for value in group_rule.values():
+ dom += self.domain_create(cr, uid, value)
+ return dom
+ return []
def clear_cache(self, cr, uid):
cr.execute("""SELECT DISTINCT m.model
@@ -135,7 +137,8 @@ class ir_rule(osv.osv):
def domain_get(self, cr, uid, model_name, mode='read', context={}):
dom = self._compute_domain(cr, uid, model_name, mode=mode)
if dom:
- return self.pool.get(model_name)._where_calc(cr, uid, dom, active_test=False)
+ query = self.pool.get(model_name)._where_calc(cr, uid, dom, active_test=False)
+ return query.where_clause, query.where_clause_params, query.tables
return [], [], ['"'+self.pool.get(model_name)._table+'"']
def unlink(self, cr, uid, ids, context=None):
diff --git a/bin/addons/base/ir/ir_sequence.py b/bin/addons/base/ir/ir_sequence.py
index 006fe1cce36..83ab79f20ac 100644
--- a/bin/addons/base/ir/ir_sequence.py
+++ b/bin/addons/base/ir/ir_sequence.py
@@ -27,8 +27,8 @@ import pooler
class ir_sequence_type(osv.osv):
_name = 'ir.sequence.type'
_columns = {
- 'name': fields.char('Sequence Name',size=64, required=True),
- 'code': fields.char('Sequence Code',size=32, required=True),
+ 'name': fields.char('Name',size=64, required=True),
+ 'code': fields.char('Code',size=32, required=True),
}
ir_sequence_type()
@@ -39,13 +39,13 @@ def _code_get(self, cr, uid, context={}):
class ir_sequence(osv.osv):
_name = 'ir.sequence'
_columns = {
- 'name': fields.char('Sequence Name',size=64, required=True),
- 'code': fields.selection(_code_get, 'Sequence Code',size=64, required=True),
+ 'name': fields.char('Name',size=64, required=True),
+ 'code': fields.selection(_code_get, 'Code',size=64, required=True),
'active': fields.boolean('Active'),
- 'prefix': fields.char('Prefix',size=64),
- 'suffix': fields.char('Suffix',size=64),
- 'number_next': fields.integer('Next Number', required=True),
- 'number_increment': fields.integer('Increment Number', required=True),
+ 'prefix': fields.char('Prefix',size=64, help="Prefix value of the record for the sequence"),
+ 'suffix': fields.char('Suffix',size=64, help="Suffix value of the record for the sequence"),
+ 'number_next': fields.integer('Next Number', required=True, help="Next number of this sequence"),
+ 'number_increment': fields.integer('Increment Number', required=True, help="The next number of the sequence will be incremented by this number"),
'padding' : fields.integer('Number padding', required=True, help="OpenERP will automatically adds some '0' on the left of the 'Next Number' to get the required padding size."),
'company_id': fields.many2one('res.company', 'Company'),
}
diff --git a/bin/addons/base/ir/ir_translation.py b/bin/addons/base/ir/ir_translation.py
index 571e98b818d..6f8a4affbe9 100644
--- a/bin/addons/base/ir/ir_translation.py
+++ b/bin/addons/base/ir/ir_translation.py
@@ -25,7 +25,8 @@ import tools
TRANSLATION_TYPE = [
('field', 'Field'),
('model', 'Object'),
- ('rml', 'RML'),
+ ('rml', 'RML (deprecated - use Report)'), # Pending deprecation - to be replaced by report!
+ ('report', 'Report/Template'),
('selection', 'Selection'),
('view', 'View'),
('wizard_button', 'Wizard Button'),
@@ -64,20 +65,30 @@ class ir_translation(osv.osv):
def _auto_init(self, cr, context={}):
super(ir_translation, self)._auto_init(cr, context)
+
+ # FIXME: there is a size limit on btree indexed values so we can't index src column with normal btree.
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_ltns',))
- if not cr.fetchone():
- cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (lang, type, name, src)')
+ if cr.fetchone():
+ #temporarily removed: cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)')
+ cr.execute('DROP INDEX ir_translation_ltns')
cr.commit()
+ cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_lts',))
+ if cr.fetchone():
+ #temporarily removed: cr.execute('CREATE INDEX ir_translation_lts ON ir_translation (lang, type, src)')
+ cr.execute('DROP INDEX ir_translation_lts')
+ cr.commit()
+
+ # add separate hash index on src (no size limit on values), as postgres 8.1+ is able to combine separate indexes
+ cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_src_hash_idx',))
+ if not cr.fetchone():
+ cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation using hash (src)')
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_ltn',))
if not cr.fetchone():
- cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (lang, type, name)')
+ cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
cr.commit()
- cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_lts',))
- if not cr.fetchone():
- cr.execute('CREATE INDEX ir_translation_lts ON ir_translation (lang, type, src)')
- cr.commit()
+
@tools.cache(skiparg=3, multi='ids')
def _get_ids(self, cr, uid, name, tt, lang, ids):
@@ -121,28 +132,45 @@ class ir_translation(osv.osv):
return len(ids)
@tools.cache(skiparg=3)
- def _get_source(self, cr, uid, name, tt, lang, source=None):
+ def _get_source(self, cr, uid, name, types, lang, source=None):
+ """
+ Returns the translation for the given combination of name, type, language
+ and source. All values passed to this method should be unicode (not byte strings),
+ especially ``source``.
+
+ :param name: identification of the term to translate, such as field name
+ :param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
+ :param lang: language code of the desired translation
+ :param source: optional source term to translate (should be unicode)
+ :rtype: unicode
+ :return: the request translation, or an empty unicode string if no translation was
+ found and `source` was not passed
+ """
+ # FIXME: should assert that `source` is unicode and fix all callers to always pass unicode
+ # so we can remove the string encoding/decoding.
if not lang:
- return ''
+ return u''
+ if isinstance(types, basestring):
+ types = (types,)
if source:
- #if isinstance(source, unicode):
- # source = source.encode('utf8')
cr.execute('select value ' \
'from ir_translation ' \
'where lang=%s ' \
- 'and type=%s ' \
+ 'and type in %s ' \
'and name=%s ' \
'and src=%s',
- (lang, tt, tools.ustr(name), source))
+ (lang or '', types, tools.ustr(name), source))
else:
cr.execute('select value ' \
'from ir_translation ' \
'where lang=%s ' \
- 'and type=%s ' \
+ 'and type in %s ' \
'and name=%s',
- (lang, tt, tools.ustr(name)))
+ (lang or '', types, tools.ustr(name)))
res = cr.fetchone()
- trad = res and res[0] or ''
+ trad = res and res[0] or u''
+ if source and not trad:
+ return tools.ustr(source)
return trad
def create(self, cursor, user, vals, context=None):
@@ -157,6 +185,8 @@ class ir_translation(osv.osv):
def write(self, cursor, user, ids, vals, context=None):
if not context:
context = {}
+ if isinstance(ids, (int, long)):
+ ids = [ids]
result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
self._get_source.clear_cache(cursor.dbname, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], source=trans_obj['src'])
@@ -166,6 +196,8 @@ class ir_translation(osv.osv):
def unlink(self, cursor, user, ids, context=None):
if not context:
context = {}
+ if isinstance(ids, (int, long)):
+ ids = [ids]
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
self._get_source.clear_cache(cursor.dbname, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], source=trans_obj['src'])
self._get_ids.clear_cache(cursor.dbname, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], [trans_obj['res_id']])
diff --git a/bin/addons/base/ir/ir_ui_menu.py b/bin/addons/base/ir/ir_ui_menu.py
index a0f16090c6b..f79493b4813 100644
--- a/bin/addons/base/ir/ir_ui_menu.py
+++ b/bin/addons/base/ir/ir_ui_menu.py
@@ -69,22 +69,27 @@ class ir_ui_menu(osv.osv):
def create_shortcut(self, cr, uid, values, context={}):
dataobj = self.pool.get('ir.model.data')
- menu_id = dataobj._get_id(cr, uid, 'base', 'menu_administration_shortcut', context)
- shortcut_menu_id = int(dataobj.read(cr, uid, menu_id, ['res_id'], context)['res_id'])
- action_id = self.pool.get('ir.actions.act_window').create(cr, uid, values, context)
+ new_context = context.copy()
+ for key in context:
+ if key.startswith('default_'):
+ del new_context[key]
+
+ menu_id = dataobj._get_id(cr, uid, 'base', 'menu_administration_shortcut', new_context)
+ shortcut_menu_id = int(dataobj.read(cr, uid, menu_id, ['res_id'], new_context)['res_id'])
+ action_id = self.pool.get('ir.actions.act_window').create(cr, uid, values, new_context)
menu_data = {'name':values['name'],
'sequence':10,
'action':'ir.actions.act_window,'+str(action_id),
'parent_id':shortcut_menu_id,
'icon':'STOCK_JUSTIFY_FILL'}
- menu_id = self.pool.get('ir.ui.menu').create(cr, uid, menu_data)
+ menu_id = self.pool.get('ir.ui.menu').create(cr, 1, menu_data)
sc_data= {'name':values['name'], 'sequence': 1,'res_id': menu_id }
- sc_menu_id = self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, context)
+ sc_menu_id = self.pool.get('ir.ui.view_sc').create(cr, uid, sc_data, new_context)
user_groups = set(self.pool.get('res.users').read(cr, 1, uid, ['groups_id'])['groups_id'])
key = (cr.dbname, shortcut_menu_id, tuple(user_groups))
self._cache[key] = True
- return True
+ return action_id
def search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False):
@@ -125,7 +130,6 @@ class ir_ui_menu(osv.osv):
data = menu.action
if data:
model_field = { 'ir.actions.act_window': 'res_model',
- 'ir.actions.report.custom': 'model',
'ir.actions.report.xml': 'model',
'ir.actions.wizard': 'model',
'ir.actions.server': 'model_id',
@@ -183,7 +187,7 @@ class ir_ui_menu(osv.osv):
rex=re.compile('\([0-9]+\)')
concat=rex.findall(datas['name'])
if concat:
- next_num=eval(concat[0])+1
+ next_num=int(concat[0])+1
datas['name']=rex.sub(('(%d)'%next_num),datas['name'])
else:
datas['name']=datas['name']+'(1)'
@@ -256,6 +260,8 @@ class ir_ui_menu(osv.osv):
return False
level -= 1
return True
+
+
_columns = {
'name': fields.char('Menu', size=64, required=True, translate=True),
@@ -264,7 +270,7 @@ class ir_ui_menu(osv.osv):
'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True),
'groups_id': many2many_unique('res.groups', 'ir_ui_menu_group_rel',
'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\
- "If this field is empty, Open ERP will compute visibility based on the related object's read access."),
+ "If this field is empty, OpenERP will compute visibility based on the related object's read access."),
'complete_name': fields.function(_get_full_name, method=True,
string='Complete Name', type='char', size=128),
'icon': fields.selection(tools.icons, 'Icon', size=64),
@@ -272,7 +278,6 @@ class ir_ui_menu(osv.osv):
'action': fields.function(_action, fnct_inv=_action_inv,
method=True, type='reference', string='Action',
selection=[
- ('ir.actions.report.custom', 'ir.actions.report.custom'),
('ir.actions.report.xml', 'ir.actions.report.xml'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.wizard', 'ir.actions.wizard'),
@@ -286,7 +291,7 @@ class ir_ui_menu(osv.osv):
_defaults = {
'icon' : lambda *a: 'STOCK_OPEN',
'icon_pict': lambda *a: ('stock', ('STOCK_OPEN','ICON_SIZE_MENU')),
- 'sequence' : lambda *a: 10
+ 'sequence' : lambda *a: 10,
}
_order = "sequence,id"
ir_ui_menu()
diff --git a/bin/addons/base/ir/ir_ui_view.py b/bin/addons/base/ir/ir_ui_view.py
index d990e94a84b..4c90e85a35c 100644
--- a/bin/addons/base/ir/ir_ui_view.py
+++ b/bin/addons/base/ir/ir_ui_view.py
@@ -26,17 +26,18 @@ from tools.safe_eval import safe_eval as eval
import tools
import netsvc
import os
+import logging
def _check_xml(self, cr, uid, ids, context={}):
+ logger = logging.getLogger('init')
for view in self.browse(cr, uid, ids, context):
eview = etree.fromstring(view.arch.encode('utf8'))
frng = tools.file_open(os.path.join('base','rng','view.rng'))
relaxng_doc = etree.parse(frng)
relaxng = etree.RelaxNG(relaxng_doc)
if not relaxng.validate(eview):
- logger = netsvc.Logger()
- logger.notifyChannel('init', netsvc.LOG_ERROR, 'The view does not fit the required schema !')
- logger.notifyChannel('init', netsvc.LOG_ERROR, tools.ustr(relaxng.error_log.last_error))
+ for error in relaxng.error_log:
+ logger.error(tools.ustr(error))
return False
return True
@@ -54,7 +55,7 @@ class view(osv.osv):
_columns = {
'name': fields.char('View Name',size=64, required=True),
'model': fields.char('Object', size=64, required=True),
- 'priority': fields.integer('Priority', required=True),
+ 'priority': fields.integer('Sequence', required=True),
'type': fields.selection((
('tree','Tree'),
('form','Form'),
@@ -68,7 +69,7 @@ class view(osv.osv):
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade'),
'field_parent': fields.char('Child Field',size=64),
'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="XML ID",
- method=True),
+ method=True, help="ID of the view defined in xml file"),
}
_defaults = {
'arch': '\n\n\t \n ',
@@ -181,7 +182,11 @@ class view(osv.osv):
for node in nodes_name:
results[str(node[0])] = result[node[0]]
results[str(node[0])]['name'] = node[1]
- return {'nodes': results, 'transitions': tres, 'label' : labels, 'blank_nodes': blank_nodes}
+ return {'nodes': results,
+ 'transitions': tres,
+ 'label' : labels,
+ 'blank_nodes': blank_nodes,
+ 'node_parent_field': _Model_Field,}
view()
class view_sc(osv.osv):
@@ -204,9 +209,9 @@ class view_sc(osv.osv):
'user_id': lambda obj, cr, uid, context: uid,
}
_sql_constraints = [
- ('shortcut_unique', 'unique(res_id, user_id)', 'Shortcut for this menu already exists!'),
+ ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'),
]
-
+
view_sc()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/ir/ir_values.py b/bin/addons/base/ir/ir_values.py
index d7baa6c0f37..21ee1a66f93 100644
--- a/bin/addons/base/ir/ir_values.py
+++ b/bin/addons/base/ir/ir_values.py
@@ -24,6 +24,11 @@ from osv.orm import except_orm
import pickle
from tools.translate import _
+EXCLUDED_FIELDS = set((
+ 'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml',
+ 'report_sxw_content_data', 'report_rml_content_data', 'search_view',
+ 'search_view_id'))
+
class ir_values(osv.osv):
_name = 'ir.values'
@@ -96,7 +101,7 @@ class ir_values(osv.osv):
cr.execute('CREATE INDEX ir_values_key_model_key2_index ON ir_values (key, model, key2)')
def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
- if type(value)==type(u''):
+ if isinstance(value, unicode):
value = value.encode('utf8')
if not isobject:
value = pickle.dumps(value)
@@ -104,30 +109,24 @@ class ir_values(osv.osv):
meta = pickle.dumps(meta)
ids_res = []
for model in models:
- if type(model)==type([]) or type(model)==type(()):
+ if isinstance(model, (list, tuple)):
model,res_id = model
else:
res_id=False
if replace:
+ search_criteria = [
+ ('key', '=', key),
+ ('key2', '=', key2),
+ ('model', '=', model),
+ ('res_id', '=', res_id),
+ ('user_id', '=', preserve_user and uid)
+ ]
if key in ('meta', 'default'):
- ids = self.search(cr, uid, [
- ('key', '=', key),
- ('key2', '=', key2),
- ('name', '=', name),
- ('model', '=', model),
- ('res_id', '=', res_id),
- ('user_id', '=', preserve_user and uid)
- ])
+ search_criteria.append(('name', '=', name))
else:
- ids = self.search(cr, uid, [
- ('key', '=', key),
- ('key2', '=', key2),
- ('value', '=', value),
- ('model', '=', model),
- ('res_id', '=', res_id),
- ('user_id', '=', preserve_user and uid)
- ])
- self.unlink(cr, uid, ids)
+ search_criteria.append(('value', '=', value))
+
+ self.unlink(cr, uid, self.search(cr, uid, search_criteria))
vals = {
'name': name,
'value': value,
@@ -149,8 +148,8 @@ class ir_values(osv.osv):
def get(self, cr, uid, key, key2, models, meta=False, context={}, res_id_req=False, without_user=True, key2_req=True):
result = []
for m in models:
- if type(m)==type([]) or type(m)==type(()):
- m,res_id = m
+ if isinstance(m, (list, tuple)):
+ m, res_id = m
else:
res_id=False
@@ -159,9 +158,8 @@ class ir_values(osv.osv):
if key2:
where.append('key2=%s')
params.append(key2[:200])
- else:
- if key2_req and not meta:
- where.append('key2 is null')
+ elif key2_req and not meta:
+ where.append('key2 is null')
if res_id_req and (models[-1][0]==m):
if res_id:
where.append('res_id=%s')
@@ -176,7 +174,7 @@ class ir_values(osv.osv):
where.append('res_id=%s')
params.append(res_id)
- where.append('(user_id=%s or (user_id IS NULL))')
+ where.append('(user_id=%s or (user_id IS NULL)) order by id')
params.append(uid)
clause = ' and '.join(where)
cr.execute('select id,name,value,object,meta, key from ir_values where ' + clause, params)
@@ -193,47 +191,37 @@ class ir_values(osv.osv):
keys.append(x[1])
if x[3]:
model,id = x[2].split(',')
- id = int(id)
- fields = self.pool.get(model).fields_get_keys(cr, uid)
- pos = 0
- while pos).
+# Copyright (C) 2010 OpenERP s.a. ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,27 +15,22 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-import wizard
-import netsvc
+from osv import osv
+from osv.orm import orm_memory
-class wizard_clear_ids(wizard.interface):
- def _clear_ids(self, cr, uid, data, context):
- service = netsvc.LocalService("object_proxy")
- service.execute(cr.dbname, uid, 'res.partner', 'write', data['ids'], {'ref': False})
- return {}
-
- states = {
- 'init': {
- 'actions': [_clear_ids],
- 'result': {'type':'state', 'state':'end'}
- }
- }
-wizard_clear_ids('res.partner.clear_ids')
+class osv_memory_autovacuum(osv.osv_memory):
+ _name = 'osv_memory.autovacuum'
+ def power_on(self, cr, uid, context=None):
+ for model in self.pool.obj_list():
+ obj = self.pool.get(model)
+ if isinstance(obj, orm_memory):
+ obj.vaccum(cr, uid)
+ return True
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+osv_memory_autovacuum()
diff --git a/bin/addons/base/ir/wizard/__init__.py b/bin/addons/base/ir/wizard/__init__.py
index b1a92b51272..99c6cdcd0de 100644
--- a/bin/addons/base/ir/wizard/__init__.py
+++ b/bin/addons/base/ir/wizard/__init__.py
@@ -19,6 +19,7 @@
#
##############################################################################
import wizard_menu
+import wizard_screen
import create_action
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/ir/wizard/wizard_menu.py b/bin/addons/base/ir/wizard/wizard_menu.py
index 7468b4da4b6..3b4dc797c99 100644
--- a/bin/addons/base/ir/wizard/wizard_menu.py
+++ b/bin/addons/base/ir/wizard/wizard_menu.py
@@ -23,18 +23,19 @@ from osv import fields,osv
class wizard_model_menu(osv.osv_memory):
_name = 'wizard.ir.model.menu.create'
_columns = {
- 'model_id': fields.many2one('ir.model','Object', required=True),
'menu_id': fields.many2one('ir.ui.menu', 'Parent Menu', required=True),
'name': fields.char('Menu Name', size=64, required=True),
}
- _defaults = {
- 'model_id': lambda self,cr,uid,ctx: ctx.get('model_id', False)
- }
- def menu_create(self, cr, uid, ids, context={}):
+
+ def menu_create(self, cr, uid, ids, context=None):
+ if not context:
+ context = {}
+ model_pool = self.pool.get('ir.model')
for menu in self.browse(cr, uid, ids, context):
+ model = model_pool.browse(cr, uid, context.get('model_id'), context=context)
val = {
'name': menu.name,
- 'res_model': menu.model_id.model,
+ 'res_model': model.model,
'view_type': 'form',
'view_mode': 'tree,form'
}
diff --git a/bin/addons/base/ir/wizard/wizard_menu_view.xml b/bin/addons/base/ir/wizard/wizard_menu_view.xml
index b1e9b6e5ccc..4d3e7e7a26e 100644
--- a/bin/addons/base/ir/wizard/wizard_menu_view.xml
+++ b/bin/addons/base/ir/wizard/wizard_menu_view.xml
@@ -8,14 +8,14 @@
form
+
-
-
-
+
+
diff --git a/bin/addons/base/ir/wizard/wizard_screen.py b/bin/addons/base/ir/wizard/wizard_screen.py
new file mode 100644
index 00000000000..56f104d0b13
--- /dev/null
+++ b/bin/addons/base/ir/wizard/wizard_screen.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2010 OpenERP s.a. ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+import base64
+import os
+import random
+
+import tools
+from osv import fields,osv
+
+# Simple base class for wizards that wish to use random images on the left
+# side of the form.
+class wizard_screen(osv.osv_memory):
+ _name = 'ir.wizard.screen'
+
+ def _get_image(self, cr, uid, context=None):
+ path = os.path.join('base','res','config_pixmaps','%d.png'%random.randrange(1,4))
+ file_data = tools.file_open(path,'rb').read()
+ return base64.encodestring(file_data)
+
+ def _get_image_fn(self, cr, uid, ids, name, args, context=None):
+ image = self._get_image(cr, uid, context)
+ return dict.fromkeys(ids, image) # ok to use .fromkeys() as the image is same for all
+
+ _columns = {
+ 'config_logo': fields.function(_get_image_fn, string='Image', type='binary', method=True),
+ }
+
+ _defaults = {
+ 'config_logo': _get_image
+ }
+wizard_screen()
\ No newline at end of file
diff --git a/bin/addons/base/ir/workflow/workflow.py b/bin/addons/base/ir/workflow/workflow.py
index 7518700a3c3..a117688399d 100644
--- a/bin/addons/base/ir/workflow/workflow.py
+++ b/bin/addons/base/ir/workflow/workflow.py
@@ -146,11 +146,18 @@ class wkf_transition(osv.osv):
_columns = {
'trigger_model': fields.char('Trigger Object', size=128),
'trigger_expr_id': fields.char('Trigger Expression', size=128),
- 'signal': fields.char('Signal (button Name)', size=64),
- 'role_id': fields.many2one('res.roles', 'Role Required'),
- 'condition': fields.char('Condition', required=True, size=128),
- 'act_from': fields.many2one('workflow.activity', 'Source Activity', required=True, select=True, ondelete='cascade'),
- 'act_to': fields.many2one('workflow.activity', 'Destination Activity', required=True, select=True, ondelete='cascade'),
+ 'signal': fields.char('Signal (button Name)', size=64,
+ help="When the operation of transition comes from a button pressed in the client form, "\
+ "signal tests the name of the pressed button. If signal is NULL, no button is necessary to validate this transition."),
+ 'group_id': fields.many2one('res.groups', 'Group Required',
+ help="The group that a user must have to be authorized to validate this transition."),
+ 'condition': fields.char('Condition', required=True, size=128,
+ help="Expression to be satisfied if we want the transition done."),
+ 'act_from': fields.many2one('workflow.activity', 'Source Activity', required=True, select=True, ondelete='cascade',
+ help="Source activity. When this activity is over, the condition is tested to determine if we can start the ACT_TO activity."),
+ 'act_to': fields.many2one('workflow.activity', 'Destination Activity', required=True, select=True, ondelete='cascade',
+ help="The destination activity."),
+ 'wkf_id': fields.related('act_from','wkf_id', type='many2one', relation='workflow', string='Workflow', select=True),
}
_defaults = {
'condition': lambda *a: 'True',
@@ -164,7 +171,6 @@ class wkf_instance(osv.osv):
_log_access = False
_columns = {
'wkf_id': fields.many2one('workflow', 'Workflow', ondelete='cascade', select=True),
- 'uid': fields.integer('User ID'),
'res_id': fields.integer('Resource ID', select=True),
'res_type': fields.char('Resource Object', size=64, select=True),
'state': fields.char('State', size=32, select=True),
@@ -186,7 +192,8 @@ class wkf_workitem(osv.osv):
_log_access = False
_rec_name = 'state'
_columns = {
- 'act_id': fields.many2one('workflow.activity', 'Activity', required=True, ondelete="cascade", select=True),
+ 'act_id': fields.many2one('workflow.activity', 'Activity', required=True, ondelete="restrict", select=True),
+ 'wkf_id': fields.related('act_id','wkf_id', type='many2one', relation='workflow', string='Workflow'),
'subflow_id': fields.many2one('workflow.instance', 'Subflow', ondelete="cascade", select=True),
'inst_id': fields.many2one('workflow.instance', 'Instance', required=True, ondelete="cascade", select=True),
'state': fields.char('State', size=64, select=True),
diff --git a/bin/addons/base/ir/workflow/workflow_view.xml b/bin/addons/base/ir/workflow/workflow_view.xml
index 8713bb50698..a207eea52ab 100644
--- a/bin/addons/base/ir/workflow/workflow_view.xml
+++ b/bin/addons/base/ir/workflow/workflow_view.xml
@@ -1,279 +1,382 @@
-
-
+
+
-
+
-
- workflow.form
- workflow
- form
-
-
-
-
-
-
-
-
-
-
+
+ workflow.form
+ workflow
+ form
+
+
+
+
+
+
+
+
+
+
+
+
-
- workflow.diagram
- workflow
- diagram
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- workflow.tree
- workflow
- tree
-
-
-
-
-
-
-
-
+
+ workflow.search
+ workflow
+ search
+
+
+
+
+
+
+
-
- Workflows
- workflow
- form
-
- tree,form,diagram
-
-
+
+ workflow.diagram
+ workflow
+ diagram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ workflow.tree
+ workflow
+ tree
+
+
+
+
+
+
+
+
-
+ -->
-
- workflow.activity.form
- workflow.activity
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- workflow.activity.tree
- workflow.activity
- tree
-
-
-
-
-
-
-
-
-
-
-
-
+
+ workflow.activity.form
+ workflow.activity
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ workflow.activity.tree
+ workflow.activity
+ tree
+
+
+
+
+
+
+
+
+
+
-
- Activities
- workflow.activity
- form
-
-
-
+
+ workflow.activity.search
+ workflow.activity
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Activities
+ workflow.activity
+ form
+
+
+
+
-
+
-
- workflow.transition.form
- workflow.transition
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
- workflow.transition.tree
- workflow.transition
- tree
-
-
-
-
-
-
-
-
-
-
-
-
+
+ workflow.transition.form
+ workflow.transition
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ workflow.transition.tree
+ workflow.transition
+ tree
+
+
+
+
+
+
+
+
+
+
+ workflow.transition.search
+ workflow.transition
+ search
+
+
+
+
+
+
+
+
+
-
- Transitions
- workflow.transition
- form
-
-
-
+
+ Transitions
+ workflow.transition
+ form
+
+
+
+
-
+
-
- workflow.instance.form
- workflow.instance
- form
-
-
-
-
-
-
-
-
-
-
-
- workflow.instance.tree
- workflow.instance
- tree
-
-
-
-
-
-
-
-
-
-
+
+ workflow.instance.form
+ workflow.instance
+ form
+
+
+
+
+
+
+
+
+
+
+ workflow.instance.tree
+ workflow.instance
+ tree
+
+
+
+
+
+
+
+
+
+
+ workflow.instance.search
+ workflow.instance
+ search
+
+
+
+
+
+
+
+
+
+
+
-
- Instances
- workflow.instance
- form
-
-
-
+
+ Instances
+ workflow.instance
+ form
+
+ {'search_default_active':1}
+
+
+
-
+
-
- workflow.workitem.form
- workflow.workitem
- form
-
-
-
-
-
-
-
-
-
-
- workflow.workitem.tree
- workflow.workitem
- tree
-
-
-
-
-
-
-
-
-
+
+ workflow.workitem.form
+ workflow.workitem
+ form
+
+
+
+
+
+
+
+
+
+
+
+ workflow.workitem.tree
+ workflow.workitem
+ tree
+
+
+
+
+
+
+
+
+
+
+
+ workflow.workitem.search
+ workflow.workitem
+ search
+
+
+
+
+
+
+
+
+
+
+
+
-
- Workitems
- workflow.workitem
- form
-
-
-
+
+ Workitems
+ workflow.workitem
+ form
+
+ {'search_default_active':1}
+
+
+
-
+
diff --git a/bin/addons/base/maintenance/maintenance_view.xml b/bin/addons/base/maintenance/maintenance_view.xml
index ca5cd459da4..836344a52df 100644
--- a/bin/addons/base/maintenance/maintenance_view.xml
+++ b/bin/addons/base/maintenance/maintenance_view.xml
@@ -22,11 +22,11 @@
form
-
-
-
-
-
+
+
+
+
+
@@ -34,16 +34,43 @@
+
+
+ maintenance.contract.search
+ maintenance.contract
+ search
+
+
+
+
+
+
+
+
+
+
+ maintenance.contract.calendar
+ maintenance.contract
+ calendar
+
+
+
+
+
+
+
+
- Your Maintenance Contracts
+ Maintenance Contracts
ir.actions.act_window
maintenance.contract
form
- tree,form
+ tree,form,calendar
+
form
-
-
+
@@ -79,7 +105,7 @@
-
+
diff --git a/bin/addons/base/migrations/1.2/pre-10-ir_property_rename_column.py b/bin/addons/base/migrations/1.2/pre-10-ir_property_rename_column.py
deleted file mode 100644
index 8c79e735c30..00000000000
--- a/bin/addons/base/migrations/1.2/pre-10-ir_property_rename_column.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf8 -*-
-
-__name__ = "ir.property: Rename column value to value_reference"
-
-def migrate(cr, version):
- rename_column(cr, 'ir_property', 'value', 'value_reference')
-
-def column_exists(cr, table, column):
- cr.execute("SELECT count(1)"
- " FROM pg_class c, pg_attribute a"
- " WHERE c.relname=%s"
- " AND c.oid=a.attrelid"
- " AND a.attname=%s",
- (table, column))
- return cr.fetchone()[0] != 0
-
-def rename_column(cr, table, old, new):
- if column_exists(cr, table, old) and not column_exists(cr, table, new):
- cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, old, new))
-
diff --git a/bin/addons/base/migrations/1.2/pre-10-res_address_fuction_change_type.py b/bin/addons/base/migrations/1.2/pre-10-res_address_fuction_change_type.py
deleted file mode 100644
index 96173c775a6..00000000000
--- a/bin/addons/base/migrations/1.2/pre-10-res_address_fuction_change_type.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf8 -*-
-
-__name__ = "res.partner.address: change type of 'function' field many2one to char"
-
-def migrate(cr, version):
- change_column_type(cr,'res_partner_address')
-
-def change_column_type(cr,table):
- cr.execute('SELECT id, name FROM res_partner_function')
- all_function = cr.fetchall()
- cr.execute('ALTER TABLE %s ADD COLUMN temp_function VARCHAR(64)' % table)
- for fn in all_function:
- cr.execute("UPDATE %s SET temp_function = '%s' WHERE function = %s" % (table,fn[1],fn[0]))
- cr.execute("ALTER TABLE %s DROP COLUMN function CASCADE" % table)
- cr.execute("ALTER TABLE %s RENAME COLUMN temp_function TO function" % table)
-
diff --git a/bin/addons/base/module/__init__.py b/bin/addons/base/module/__init__.py
index 93f0f0a3d1c..1b7abf2e76e 100644
--- a/bin/addons/base/module/__init__.py
+++ b/bin/addons/base/module/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,14 +15,10 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-import module_web
import module
import wizard
import report
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/module.py b/bin/addons/base/module/module.py
index 89a059eb9c5..81985332157 100644
--- a/bin/addons/base/module/module.py
+++ b/bin/addons/base/module/module.py
@@ -19,27 +19,24 @@
# along with this program. If not, see .
#
##############################################################################
-
-import tarfile
-import re
-import urllib
-import os
import imp
import logging
-import tools
-from osv import fields, osv, orm
-import zipfile
-import release
+import os
+import re
+import urllib
import zipimport
-import wizard
import addons
-import pooler
import netsvc
+import pooler
+import release
+import tools
from tools.parse_version import parse_version
from tools.translate import _
+from osv import fields, osv, orm
+
class module_category(osv.osv):
_name = "ir.module.category"
_description = "Module Category"
@@ -56,8 +53,8 @@ class module_category(osv.osv):
result = dict(cr.fetchall())
for id in ids:
cr.execute('select id from ir_module_category where parent_id=%s', (id,))
- childs = [c for c, in cr.fetchall()]
- result[id] = reduce(lambda x,y:x+y, [result.get(c, 0) for c in childs], result.get(id, 0))
+ result[id] = sum([result.get(c, 0) for (c,) in cr.fetchall()],
+ result.get(id, 0))
return result
_columns = {
@@ -72,6 +69,7 @@ module_category()
class module(osv.osv):
_name = "ir.module.module"
_description = "Module"
+ __logger = logging.getLogger('base.' + _name)
def get_module_info(self, name):
info = {}
@@ -79,8 +77,9 @@ class module(osv.osv):
info = addons.load_information_from_description_file(name)
if 'version' in info:
info['version'] = release.major_version + '.' + info['version']
- except:
- pass
+ except Exception:
+ self.__logger.debug('Error when trying to fetch informations for '
+ 'module %s', name, exc_info=True)
return info
def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context={}):
@@ -115,23 +114,27 @@ class module(osv.osv):
v = view_obj.browse(cr,uid,data_id.res_id)
aa = v.inherit_id and '* INHERIT ' or ''
res[mnames[data_id.module]]['views_by_module'] += aa + v.name + ' ('+v.type+')\n'
- except:
- pass
+ except Exception:
+ self.__logger.debug(
+ 'Unknown error while browsing ir.ui.view[%s]',
+ data_id.res_id, exc_info=True)
elif key=='ir.actions.report.xml':
res[mnames[data_id.module]]['reports_by_module'] += report_obj.browse(cr,uid,data_id.res_id).name + '\n'
elif key=='ir.ui.menu':
try:
m = menu_obj.browse(cr,uid,data_id.res_id)
res[mnames[data_id.module]]['menus_by_module'] += m.complete_name + '\n'
- except:
- pass
- except KeyError, e:
+ except Exception:
+ self.__logger.debug(
+ 'Unknown error while browsing ir.ui.menu[%s]',
+ data_id.res_id, exc_info=True)
+ except KeyError:
pass
return res
_columns = {
- 'name': fields.char("Name", size=128, readonly=True, required=True),
- 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True),
+ 'name': fields.char("Name", size=128, readonly=True, required=True, select=True),
+ 'category_id': fields.many2one('ir.module.category', 'Category', readonly=True, select=True),
'shortdesc': fields.char('Short Description', size=256, readonly=True, translate=True),
'description': fields.text("Description", readonly=True, translate=True),
'author': fields.char("Author", size=128, readonly=True),
@@ -151,8 +154,6 @@ class module(osv.osv):
'url': fields.char('URL', size=128, readonly=True),
'dependencies_id': fields.one2many('ir.module.module.dependency',
'module_id', 'Dependencies', readonly=True),
- 'web_dependencies_id': fields.one2many('ir.module.web.dependency',
- 'module_id', 'Web Dependencies', readonly=True),
'state': fields.selection([
('uninstallable','Not Installable'),
('uninstalled','Not Installed'),
@@ -160,7 +161,7 @@ class module(osv.osv):
('to upgrade','To be upgraded'),
('to remove','To be removed'),
('to install','To be installed')
- ], string='State', readonly=True),
+ ], string='State', readonly=True, select=True),
'demo': fields.boolean('Demo data'),
'license': fields.selection([
('GPL-2', 'GPL Version 2'),
@@ -175,12 +176,14 @@ class module(osv.osv):
'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
+ 'web': fields.boolean('Has a web component', readonly=True),
}
_defaults = {
'state': lambda *a: 'uninstalled',
'demo': lambda *a: False,
'license': lambda *a: 'AGPL-3',
+ 'web': False,
}
_order = 'name'
@@ -229,7 +232,7 @@ class module(osv.osv):
raise Exception('Unable to find %r in path' % (binary,))
- def state_update(self, cr, uid, ids, newstate, states_to_update, context={}, level=100):
+ def state_update(self, cr, uid, ids, newstate, states_to_update, context=None, level=100):
if level<1:
raise orm.except_orm(_('Error'), _('Recursion error in modules dependencies !'))
demo = False
@@ -237,25 +240,25 @@ class module(osv.osv):
mdemo = False
for dep in module.dependencies_id:
if dep.state == 'unknown':
- raise orm.except_orm(_('Error'), _("You try to install the module '%s' that depends on the module:'%s'.\nBut this module is not available in your system.") % (module.name, dep.name,))
+ raise orm.except_orm(_('Error'), _("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,))
ids2 = self.search(cr, uid, [('name','=',dep.name)])
if dep.state != newstate:
mdemo = self.state_update(cr, uid, ids2, newstate, states_to_update, context, level-1,) or mdemo
else:
od = self.browse(cr, uid, ids2)[0]
mdemo = od.demo or mdemo
-
- for web_mod in module.web_dependencies_id:
- if web_mod.state == 'unknown':
- raise orm.except_orm(_('Error'), _("You try to install the module '%s' that depends on the module:'%s'.\nBut this module is not available in your system.") % (module.name, dep.name,))
- ids2 = self.pool.get('ir.module.web').search(cr, uid, [('module','=',web_mod.name)])
- self.pool.get('ir.module.web').button_install(cr, uid, ids2)
-
+
terp = self.get_module_info(module.name)
try:
self._check_external_dependencies(terp)
except Exception, e:
- raise orm.except_orm(_('Error'), _('Unable %s the module "%s" because an external dependencie is not met: %s' % (newstate, module.name, e.args[0])))
+ if newstate == 'to install':
+ msg = _('Unable to install module "%s" because an external dependency is not met: %s')
+ elif newstate == 'to upgrade':
+ msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s')
+ else:
+ msg = _('Unable to process module "%s" because an external dependency is not met: %s')
+ raise orm.except_orm(_('Error'), msg % (module.name, e.args[0]))
if not module.dependencies_id:
mdemo = module.demo
if module.state in states_to_update:
@@ -340,6 +343,7 @@ class module(osv.osv):
'website': terp.get('website', ''),
'license': terp.get('license', 'GPL-2'),
'certificate': terp.get('certificate') or None,
+ 'web': terp.get('web') or False,
}
# update the list of available packages
@@ -373,7 +377,6 @@ class module(osv.osv):
id = self.create(cr, uid, dict(name=mod_name, state='uninstalled', **values))
res[1] += 1
self._update_dependencies(cr, uid, id, terp.get('depends', []))
- self._update_web_dependencies(cr, uid, id, terp.get('web_depends', []))
self._update_category(cr, uid, id, terp.get('category', 'Uncategorized'))
return res
@@ -398,7 +401,9 @@ class module(osv.osv):
fp = file(fname, 'wb')
fp.write(zipfile)
fp.close()
- except Exception, e:
+ except Exception:
+ self.__logger.exception('Error when trying to create module '
+ 'file %s', fname)
raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,))
terp = self.get_module_info(mod.name)
self.write(cr, uid, mod.id, self.get_values_from_terp(terp))
@@ -417,18 +422,6 @@ class module(osv.osv):
for d in depends:
cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (id, d))
- def _update_web_dependencies(self, cr, uid, id, depends=[]):
- web_module_pool = self.pool.get('ir.module.web')
- res = False
-
- for d in depends:
- ids = web_module_pool.search(cr, uid, [('module','=',d)])
- if len(ids) > 0:
- cr.execute("Select id from ir_module_web_dependency where module_id=%s and web_module_id=%s and name=%s", (id, ids[0], d))
- res = cr.fetchone()
- if not res:
- cr.execute('INSERT INTO ir_module_web_dependency (module_id, web_module_id, name) values (%s, %s, %s)', (id, ids[0], d))
-
def _update_category(self, cr, uid, id, category='Uncategorized'):
categs = category.split('/')
p_id = None
@@ -448,7 +441,7 @@ class module(osv.osv):
categs = categs[1:]
self.write(cr, uid, [id], {'category_id': p_id})
- def update_translations(self, cr, uid, ids, filter_lang=None):
+ def update_translations(self, cr, uid, ids, filter_lang=None, context={}):
logger = netsvc.Logger()
if not filter_lang:
pool = pooler.get_pool(cr.dbname)
@@ -475,7 +468,7 @@ class module(osv.osv):
iso_lang = iso_lang.split('_')[0]
if os.path.exists(f):
logger.notifyChannel("i18n", netsvc.LOG_INFO, 'module %s: loading translation file for language %s' % (mod.name, iso_lang))
- tools.trans_load(cr.dbname, f, lang, verbose=False)
+ tools.trans_load(cr.dbname, f, lang, verbose=False, context=context)
def check(self, cr, uid, ids, context=None):
logger = logging.getLogger('init')
@@ -491,40 +484,54 @@ class module(osv.osv):
logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
-module()
+ def list_web(self, cr, uid, context=None):
+ """ list_web(cr, uid, context) -> [module_name]
+ Lists all the currently installed modules with a web component.
-class web_module_dependency(osv.osv):
- _name = "ir.module.web.dependency"
- _description = "Web Module dependency"
-
- def _state(self, cr, uid, ids, name, args, context={}):
- result = {}
- mod_obj = self.pool.get('ir.module.web')
- for md in self.browse(cr, uid, ids):
- ids = mod_obj.search(cr, uid, [('module', '=', md.name)])
- if ids:
- result[md.id] = mod_obj.read(cr, uid, [ids[0]], ['state'])[0]['state']
+ Returns a list of addon names.
+ """
+ return [
+ module['name']
+ for module in self.browse(cr, uid,
+ self.search(cr, uid,
+ [('web', '=', True),
+ ('state', 'in', ['installed','to upgrade','to remove'])],
+ context=context),
+ context=context)]
+ def _web_dependencies(self, cr, uid, module, context=None):
+ for dependency in module.dependencies_id:
+ (parent,) = self.browse(cr, uid, self.search(cr, uid,
+ [('name', '=', dependency.name)], context=context),
+ context=context)
+ if parent.web:
+ yield parent.name
else:
- result[md.id] = 'unknown'
- return result
+ self._web_dependencies(
+ cr, uid, parent, context=context)
+ def get_web(self, cr, uid, names, context=None):
+ """ get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
-
- _columns = {
- 'name': fields.char('Name', size=128),
- 'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
- 'web_module_id': fields.many2one('ir.module.web', 'Web Module', select=True, ondelete='cascade'),
- 'state': fields.function(_state, method=True, type='selection', selection=[
- ('uninstallable','Uninstallable'),
- ('uninstalled','Not Installed'),
- ('installed','Installed'),
- ('to upgrade','To be upgraded'),
- ('to remove','To be removed'),
- ('to install','To be installed'),
- ('unknown', 'Unknown'),
- ], string='State', readonly=True),
- }
+ Returns the web content of all the named addons.
-web_module_dependency()
+ The toplevel directory of the zipped content is called 'web',
+ its final naming has to be managed by the client
+ """
+ modules = self.browse(cr, uid,
+ self.search(cr, uid, [('name', 'in', names)], context=context),
+ context=context)
+ if not modules: return []
+ self.__logger.info('Sending web content of modules %s '
+ 'to web client', names)
+ return [
+ {'name': module.name,
+ 'depends': list(self._web_dependencies(
+ cr, uid, module, context=context)),
+ 'content': addons.zip_directory(
+ addons.get_module_resource(module.name, 'web'))}
+ for module in modules
+ ]
+
+module()
class module_dependency(osv.osv):
_name = "ir.module.module.dependency"
@@ -542,7 +549,7 @@ class module_dependency(osv.osv):
return result
_columns = {
- 'name': fields.char('Name', size=128),
+ 'name': fields.char('Name', size=128, select=True),
'module_id': fields.many2one('ir.module.module', 'Module', select=True, ondelete='cascade'),
'state': fields.function(_state, method=True, type='selection', selection=[
('uninstallable','Uninstallable'),
@@ -552,8 +559,6 @@ class module_dependency(osv.osv):
('to remove','To be removed'),
('to install','To be installed'),
('unknown', 'Unknown'),
- ], string='State', readonly=True),
+ ], string='State', readonly=True, select=True),
}
module_dependency()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/module_view.xml b/bin/addons/base/module/module_view.xml
index 9bb2cdd2769..2d391cdd8e6 100644
--- a/bin/addons/base/module/module_view.xml
+++ b/bin/addons/base/module/module_view.xml
@@ -40,20 +40,21 @@
-
+
-
+
+
-
+
@@ -67,7 +68,6 @@
form
tree,form
[('category_id','=',active_id)]
-
@@ -89,19 +89,26 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -121,14 +128,6 @@
-
-
-
-
-
-
-
-
@@ -148,14 +147,15 @@
+
-
+
-
+
@@ -168,7 +168,8 @@
form
tree,form
-
+
+ List all certified modules available to configure your OpenERP. Modules that are installed are flagged as such. You can search for a specific module using the name or the description of the module. You do not need to install modules one by one, you can install many at the same time by clicking on the schedule button in this list. Then, apply the schedule upgrade from Action menu once for all the ones you have scheduled for installation.
diff --git a/bin/addons/base/module/module_web.py b/bin/addons/base/module/module_web.py
deleted file mode 100644
index 8f65dc3960d..00000000000
--- a/bin/addons/base/module/module_web.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from osv import fields, osv, orm
-
-class module_web(osv.osv):
- _name = "ir.module.web"
- _description = "Web Module"
-
- _columns = {
- 'name': fields.char("Name", size=128, readonly=True, required=True),
- 'module': fields.char("Module", size=128, readonly=True, required=True),
- 'description': fields.text("Description", readonly=True, translate=True),
- 'author': fields.char("Author", size=128, readonly=True),
- 'website': fields.char("Website", size=256, readonly=True),
- 'state': fields.selection([
- ('uninstallable','Uninstallable'),
- ('uninstalled','Not Installed'),
- ('installed','Installed')
- ], string='State', readonly=True)
- }
-
- _defaults = {
- 'state': lambda *a: 'uninstalled',
- }
- _order = 'name'
-
- _sql_constraints = [
- ('name_uniq', 'unique (name)', 'The name of the module must be unique !'),
- ]
-
- def update_module_list(self, cr, uid, modules, context={}):
-
- for module in modules:
- mod_name = module['name']
- ids = self.search(cr, uid, [('name','=',mod_name)])
- if ids:
- self.write(cr, uid, ids, module)
- else:
- self.create(cr, uid, module)
-
- def button_install(self, cr, uid, ids, context={}):
- return self.write(cr, uid, ids, {'state': 'installed'}, context)
-
- def button_uninstall(self, cr, uid, ids, context={}):
- return self.write(cr, uid, ids, {'state': 'uninstalled'}, context)
-
-module_web()
-
diff --git a/bin/addons/base/module/module_web_view.xml b/bin/addons/base/module/module_web_view.xml
deleted file mode 100644
index 32e927b70e7..00000000000
--- a/bin/addons/base/module/module_web_view.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
- ir.module.web.tree
- ir.module.web
- tree
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- web_module_list
- /openerp/modules
-
-
-
-
-
diff --git a/bin/addons/base/module/module_wizard.xml b/bin/addons/base/module/module_wizard.xml
deleted file mode 100644
index b989fc56041..00000000000
--- a/bin/addons/base/module/module_wizard.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
- Import module
- base.module.import
-
-
-
-
- Update Modules List
- module.module.update
-
-
-
-
-
-
-
-
-
-
- Load an Official Translation
- module.lang.install
-
-
-
-
- Export a Translation File
- wizard.module.lang.export
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Export a Translation File
- ir.actions.act_window
- wizard.module.lang.export
- form
- form
- new
-
-
-
-
- Import a Translation File
- module.lang.import
-
-
-
-
- Update Translations
- wizard.module.update_translations
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Resynchronise Terms
- ir.actions.act_window
- wizard.module.update_translations
- form
- form
- new
-
-
-
-
-
diff --git a/bin/addons/base/module/wizard/__init__.py b/bin/addons/base/module/wizard/__init__.py
index c0af5977b21..a4133a50113 100644
--- a/bin/addons/base/module/wizard/__init__.py
+++ b/bin/addons/base/module/wizard/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,19 +15,18 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-import wizard_update_module
-import wizard_module_upgrade
-import wizard_configuration
-import wizard_module_lang_install
-import add_new
-import wizard_export_lang
-import wizard_import_lang
-import wizard_module_import
-import wizard_update_translations
+import base_module_import
+import base_module_update
+import base_language_install
+import base_import_language
+import base_module_upgrade
+import base_module_configuration
+import base_export_language
+import base_update_translations
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/module/wizard/wizard_export_lang.py b/bin/addons/base/module/wizard/base_export_language.py
similarity index 92%
rename from bin/addons/base/module/wizard/wizard_export_lang.py
rename to bin/addons/base/module/wizard/base_export_language.py
index b59351be61b..1b43b561ac3 100644
--- a/bin/addons/base/module/wizard/wizard_export_lang.py
+++ b/bin/addons/base/module/wizard/base_export_language.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# Copyright (C) 2004-2010 Tiny SPRL ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -27,7 +27,7 @@ from osv import fields,osv
from tools.translate import _
from tools.misc import get_iso_codes
-class wizard_export_lang(osv.osv_memory):
+class base_language_export(osv.osv_memory):
def _get_languages(self, cr, uid, context):
lang_obj=pooler.get_pool(cr.dbname).get('res.lang')
@@ -67,7 +67,8 @@ class wizard_export_lang(osv.osv_memory):
buf.close()
return self.write(cr, uid, ids, {'state':'get', 'data':out, 'advice':this.advice, 'name':this.name}, context=context)
- _name = "wizard.module.lang.export"
+ _name = "base.language.export"
+ _inherit = "ir.wizard.screen"
_columns = {
'name': fields.char('Filename', 16, readonly=True),
'lang': fields.selection(_get_languages, 'Language', help='To export a new language, do not select a language.'), # not required: unset = new language
@@ -78,11 +79,11 @@ class wizard_export_lang(osv.osv_memory):
'state': fields.selection( ( ('choose','choose'), # choose language
('get','get'), # get the file
) ),
- }
- _defaults = { 'state': lambda *a: 'choose',
- 'name': lambda *a: 'lang.tar.gz'
- }
-wizard_export_lang()
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ }
+ _defaults = {
+ 'state': lambda *a: 'choose',
+ 'name': lambda *a: 'lang.tar.gz'
+ }
+base_language_export()
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_export_language_view.xml b/bin/addons/base/module/wizard/base_export_language_view.xml
new file mode 100644
index 00000000000..c908c5ce03a
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_export_language_view.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Export Translations
+ base.language.export
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Export Translation
+ ir.actions.act_window
+ base.language.export
+ form
+ form
+ new
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_import_language.py b/bin/addons/base/module/wizard/base_import_language.py
new file mode 100644
index 00000000000..6608ade03ef
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_import_language.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import tools
+import base64
+from tempfile import TemporaryFile
+from osv import osv, fields
+
+class base_language_import(osv.osv_memory):
+ """ Language Import """
+
+ _name = "base.language.import"
+ _description = "Language Import"
+ _inherit = "ir.wizard.screen"
+
+ _columns = {
+ 'name': fields.char('Language Name',size=64 , required=True),
+ 'code': fields.char('Code (eg:en__US)',size=5 , required=True),
+ 'data': fields.binary('File', required=True),
+ }
+
+ def import_lang(self, cr, uid, ids, context):
+ """
+ Import Language
+ @param cr: the current row, from the database cursor.
+ @param uid: the current user’s ID for security checks.
+ @param ids: the ID or list of IDs
+ @param context: A standard dictionary
+ """
+
+ import_data = self.browse(cr, uid, ids)[0]
+ fileobj = TemporaryFile('w+')
+ fileobj.write(base64.decodestring(import_data.data))
+
+ # now we determine the file format
+ fileobj.seek(0)
+ first_line = fileobj.readline().strip().replace('"', '').replace(' ', '')
+ fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
+ fileobj.seek(0)
+
+ tools.trans_load_data(cr.dbname, fileobj, fileformat, import_data.code, lang_name=import_data.name)
+ fileobj.close()
+ return {}
+
+base_language_import()
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_import_language_view.xml b/bin/addons/base/module/wizard/base_import_language_view.xml
new file mode 100644
index 00000000000..d313afc4080
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_import_language_view.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Import Translation
+ base.language.import
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import Translation
+ ir.actions.act_window
+ base.language.import
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_language_install.py b/bin/addons/base/module/wizard/base_language_install.py
new file mode 100644
index 00000000000..9cdae41ad2b
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_language_install.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import tools
+from osv import osv, fields
+
+class base_language_install(osv.osv_memory):
+ """ Install Language"""
+
+ _name = "base.language.install"
+ _inherit = "ir.wizard.screen"
+ _description = "Install Language"
+
+ _columns = {
+ 'lang': fields.selection(tools.scan_languages(),'Language', required=True),
+ 'overwrite': fields.boolean('Overwrite Existing Terms', help="If you check this box, your customized translations will be overwritten and replaced by the official ones."),
+ 'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
+ }
+ _defaults = {
+ 'state': 'init',
+ 'overwrite': False
+ }
+ def lang_install(self, cr, uid, ids, context):
+ language_obj = self.browse(cr, uid, ids)[0]
+ lang = language_obj.lang
+ if lang:
+ modobj = self.pool.get('ir.module.module')
+ mids = modobj.search(cr, uid, [('state', '=', 'installed')])
+ if language_obj.overwrite:
+ context = {'overwrite': True}
+ modobj.update_translations(cr, uid, mids, lang, context or {})
+ self.write(cr, uid, ids, {'state': 'done'}, context=context)
+ return False
+base_language_install()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/module/wizard/base_language_install_view.xml b/bin/addons/base/module/wizard/base_language_install_view.xml
new file mode 100644
index 00000000000..d207ea9ea29
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_language_install_view.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Load Official Translation
+ base.language.install
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Load Official Translation
+ ir.actions.act_window
+ base.language.install
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_module_configuration.py b/bin/addons/base/module/wizard/base_module_configuration.py
new file mode 100644
index 00000000000..866b22905e0
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_configuration.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from osv import osv
+from tools.translate import _
+
+class base_module_configuration(osv.osv_memory):
+
+ _name = "base.module.configuration"
+
+ def start(self, cr, uid, ids, context=None):
+ todo_ids = self.pool.get('ir.actions.todo').search(cr, uid, ['|', '|', ('restart','=','always'), ('state', '=', 'open'), '&', ('state', '=', 'skip'), ('restart', '=', 'onskip')])
+ if not todo_ids:
+ # When there is no wizard todo it will display message
+ data_obj = self.pool.get('ir.model.data')
+ result = data_obj._get_id(cr, uid, 'base', 'view_base_module_configuration_form')
+ view_id = data_obj.browse(cr, uid, result).res_id
+ value = {
+ 'name': _('System Configuration done'),
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'base.module.configuration',
+ 'view_id': [view_id],
+ 'type': 'ir.actions.act_window',
+ 'target': 'new'
+ }
+ return value
+ # Run the config wizards
+ config_pool = self.pool.get('res.config')
+ return config_pool.start(cr, uid, ids, context=context)
+
+base_module_configuration()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/module/wizard/base_module_configuration_view.xml b/bin/addons/base/module/wizard/base_module_configuration_view.xml
new file mode 100644
index 00000000000..2d0b31b7126
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_configuration_view.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ Module Configuration
+ base.module.configuration
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+ Start Configuration
+
+ code
+ action = obj.start([])
+
+
+
+
+
+
+
+ Start Configuration
+ client_action_multi
+
+ action
+ ir.actions.todo
+
+
+ Reconfigure
+ ir.ui.menu
+
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_module_import.py b/bin/addons/base/module/wizard/base_module_import.py
new file mode 100644
index 00000000000..05c3b18198d
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_import.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import os
+import tools
+
+import zipfile
+from StringIO import StringIO
+import base64
+from tools.translate import _
+from osv import osv, fields
+
+class base_module_import(osv.osv_memory):
+ """ Import Module """
+
+ _name = "base.module.import"
+ _inherit = "ir.wizard.screen"
+ _description = "Import Module"
+
+ _columns = {
+ 'module_file': fields.binary('Module .ZIP file', required=True),
+ 'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
+ 'module_name': fields.char('Module Name', size=128),
+ }
+
+ _defaults = {
+ 'state': 'init',
+ }
+
+ def importzip(self, cr, uid, ids, context):
+ (data,) = self.browse(cr, uid, ids , context=context)
+ module_data = data.module_file
+
+ val = base64.decodestring(module_data)
+ fp = StringIO()
+ fp.write(val)
+ fdata = zipfile.ZipFile(fp, 'r')
+ fname = fdata.namelist()[0]
+ module_name = os.path.split(fname)[0]
+
+ ad = tools.config['addons_path'].split(",")[-1]
+
+ fname = os.path.join(ad, module_name+'.zip')
+ try:
+ fp = file(fname, 'wb')
+ fp.write(val)
+ fp.close()
+ except IOError:
+ raise osv.except_osv(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
+
+ self.pool.get('ir.module.module').update_list(cr, uid, {'module_name': module_name,})
+ self.write(cr, uid, ids, {'state':'done', 'module_name': module_name}, context)
+ return False
+
+ def action_module_open(self, cr, uid, ids, context):
+ (data,) = self.browse(cr, uid, ids , context=context)
+ return {
+ 'domain': str([('name', '=', data.module_name)]),
+ 'name': 'Modules',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': 'ir.module.module',
+ 'type': 'ir.actions.act_window',
+ }
+
+base_module_import()
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_module_import_view.xml b/bin/addons/base/module/wizard/base_module_import_view.xml
new file mode 100644
index 00000000000..8e43789b057
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_import_view.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Module Import
+ base.module.import
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Module Import
+ ir.actions.act_window
+ base.module.import
+ form
+ form
+ new
+
+
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/add_new.py b/bin/addons/base/module/wizard/base_module_scan.py
similarity index 69%
rename from bin/addons/base/module/wizard/add_new.py
rename to bin/addons/base/module/wizard/base_module_scan.py
index c6eddab2adb..d47e091955f 100644
--- a/bin/addons/base/module/wizard/add_new.py
+++ b/bin/addons/base/module/wizard/base_module_scan.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-# Copyright (C) 2010 OpenERP s.a. ().
+# Copyright (C) 2004-2010 Tiny SPRL ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -16,34 +15,26 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-
import os
-import re
import glob
-import time
import imp
import tools
-import wizard
-import pooler
import zipfile
+from osv import osv
-module_name_re = re.compile('.*addons.(.*?).__terp__.py$')
+class base_module_scan(osv.osv_memory):
+ """ scan module """
-_info_arch = '''
-
-
-
-'''
-_info_fields = {}
+ _name = "base.module.scan"
+ _description = "scan module"
-class wizard_install_module(wizard.interface):
- def watch_dir(self, cr, uid, data, context):
- mod_obj = pooler.get_pool(cr.dbname).get('ir.module.module')
+ def watch_dir(self, cr, uid, ids, context):
+ mod_obj = self.pool.get('ir.module.module')
all_mods = mod_obj.read(cr, uid, mod_obj.search(cr, uid, []), ['name', 'state'])
known_modules = [x['name'] for x in all_mods]
ls_ad = glob.glob(os.path.join(tools.config['addons_path'], '*', '__terp__.py'))
@@ -79,18 +70,4 @@ class wizard_install_module(wizard.interface):
mod_obj.write(cr, uid, [mod['id']], {'state': 'uninstalled'})
return {}
- states = {
- 'init': {
- 'actions': [],
- 'result': {'type':'form', 'arch': _info_arch, 'fields': _info_fields, 'state':[('end','Cancel','gtk-cancel'),('addmod','Check new modules','gtk-ok')]}
- },
- 'addmod': {
- 'actions': [watch_dir],
- 'result': {'type':'state', 'state':'end'}
- },
- }
-wizard_install_module('module.module.scan')
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+base_module_scan()
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_module_scan_view.xml b/bin/addons/base/module/wizard/base_module_scan_view.xml
new file mode 100644
index 00000000000..a039c3122e1
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_scan_view.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ Module Scan
+ base.module.scan
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Module Scan
+ ir.actions.act_window
+ base.module.scan
+ form
+ form
+ new
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_module_update.py b/bin/addons/base/module/wizard/base_module_update.py
new file mode 100644
index 00000000000..a0d21840d90
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_update.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+from osv import osv, fields
+
+class base_module_update(osv.osv_memory):
+ """ Update Module """
+
+ _name = "base.module.update"
+ _description = "Update Module"
+ _inherit = "ir.wizard.screen"
+
+ _columns = {
+ 'update': fields.integer('Number of modules updated', readonly=True),
+ 'add': fields.integer('Number of modules added', readonly=True),
+ 'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
+ }
+
+ _defaults = {
+ 'state': 'init',
+ }
+
+ def update_module(self, cr, uid, ids, context=None):
+ module_obj = self.pool.get('ir.module.module')
+ update, add = module_obj.update_list(cr, uid,)
+ self.write(cr, uid, ids, {'update': update, 'add': add, 'state': 'done'}, context=context)
+ return False
+
+ def action_module_open(self, cr, uid, ids, context):
+ res = {
+ 'domain': str([]),
+ 'name': 'Modules',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': 'ir.module.module',
+ 'view_id': False,
+ 'type': 'ir.actions.act_window',
+ }
+ return res
+
+
+base_module_update()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_module_update_view.xml b/bin/addons/base/module/wizard/base_module_update_view.xml
new file mode 100644
index 00000000000..51f0c94f31c
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_update_view.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Module Update
+ base.module.update
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Module Update
+ ir.actions.act_window
+ base.module.update
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/base_module_upgrade.py b/bin/addons/base/module/wizard/base_module_upgrade.py
new file mode 100644
index 00000000000..4db4028bdb0
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_upgrade.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import pooler
+from osv import osv, fields
+
+class base_module_upgrade(osv.osv_memory):
+ """ Module Upgrade """
+
+ _name = "base.module.upgrade"
+ _description = "Module Upgrade"
+
+ _columns = {
+ 'module_info': fields.text('Modules to update',readonly=True),
+ }
+
+ def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
+ """ Changes the view dynamically
+ @param self: The object pointer.
+ @param cr: A database cursor
+ @param uid: ID of the user currently logged in
+ @param context: A standard dictionary
+ @return: New arch of view.
+ """
+ res = super(base_module_upgrade, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False)
+ record_id = context and context.get('active_id', False) or False
+ active_model = context.get('active_model')
+ if (not record_id) or (not active_model):
+ return res
+
+ ids = self.get_module_list(cr, uid, context=context)
+ if not ids:
+ res['arch'] = '''
+
+
+
+
+
+
+
+ '''
+
+ return res
+
+ def get_module_list(self, cr, uid, context=None):
+ mod_obj = self.pool.get('ir.module.module')
+ ids = mod_obj.search(cr, uid, [
+ ('state', 'in', ['to upgrade', 'to remove', 'to install'])])
+ return ids
+
+ def default_get(self, cr, uid, fields, context=None):
+ """
+ This function checks for precondition before wizard executes
+ @param self: The object pointer
+ @param cr: the current row, from the database cursor,
+ @param uid: the current user’s ID for security checks,
+ @param fields: List of fields for default value
+ @param context: A standard dictionary for contextual values
+ """
+ mod_obj = self.pool.get('ir.module.module')
+ ids = self.get_module_list(cr, uid, context=context)
+ res = mod_obj.read(cr, uid, ids, ['name','state'], context)
+ return {'module_info': '\n'.join(map(lambda x: x['name']+' : '+x['state'], res))}
+
+ def upgrade_module(self, cr, uid, ids, context):
+ pool = pooler.get_pool(cr.dbname)
+ mod_obj = self.pool.get('ir.module.module')
+ data_obj = self.pool.get('ir.model.data')
+ ids = mod_obj.search(cr, uid, [('state', 'in', ['to upgrade', 'to remove', 'to install'])])
+ unmet_packages = []
+ mod_dep_obj = pool.get('ir.module.module.dependency')
+ for mod in mod_obj.browse(cr, uid, ids):
+ depends_mod_ids = mod_dep_obj.search(cr, uid, [('module_id', '=', mod.id)])
+ for dep_mod in mod_dep_obj.browse(cr, uid, depends_mod_ids):
+ if dep_mod.state in ('unknown','uninstalled'):
+ unmet_packages.append(dep_mod.name)
+ if len(unmet_packages):
+ raise osv.except_osv('Unmet dependency !', 'Following modules are uninstalled or unknown. \n\n'+'\n'.join(unmet_packages))
+ mod_obj.download(cr, uid, ids, context=context)
+ cr.commit()
+ _, pool = pooler.restart_pool(cr.dbname, update_module=True)
+
+ id2 = data_obj._get_id(cr, uid, 'base', 'view_base_module_upgrade_install')
+ if id2:
+ id2 = data_obj.browse(cr, uid, id2, context=context).res_id
+
+ return {
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'base.module.upgrade',
+ 'views': [(id2, 'form')],
+ 'view_id': False,
+ 'type': 'ir.actions.act_window',
+ 'target': 'new',
+ }
+
+ def config(self, cr, uid, ids, context=None):
+ return self.pool.get('res.config').next(cr, uid, [], context=context)
+
+base_module_upgrade()
diff --git a/bin/addons/base/module/wizard/base_module_upgrade_view.xml b/bin/addons/base/module/wizard/base_module_upgrade_view.xml
new file mode 100644
index 00000000000..b8bb282d6ed
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_module_upgrade_view.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+ Module Upgrade
+ base.module.upgrade
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Module Upgrade
+ ir.actions.act_window
+ base.module.upgrade
+ form
+ form
+ new
+
+
+
+
+
+
+
+ Module Upgrade Install
+ base.module.upgrade
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Module Upgrade Install
+ ir.actions.act_window
+ base.module.upgrade
+ form
+ form
+
+ new
+
+
+
+
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/wizard_update_translations.py b/bin/addons/base/module/wizard/base_update_translations.py
similarity index 75%
rename from bin/addons/base/module/wizard/wizard_update_translations.py
rename to bin/addons/base/module/wizard/base_update_translations.py
index 85914ff0164..b413e9d7093 100644
--- a/bin/addons/base/module/wizard/wizard_update_translations.py
+++ b/bin/addons/base/module/wizard/base_update_translations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -24,18 +24,18 @@ import tools
import pooler
import cStringIO
-class update_translations(osv.osv_memory):
+class base_update_translations(osv.osv_memory):
def _get_languages(self, cr, uid, context):
lang_obj=pooler.get_pool(cr.dbname).get('res.lang')
ids=lang_obj.search(cr, uid, ['&', ('active', '=', True), ('translatable', '=', True),])
langs=lang_obj.browse(cr, uid, ids)
return [(lang.code, lang.name) for lang in langs]
-
+
def _get_lang_name(self, cr, uid, lang_code):
lang_obj=pooler.get_pool(cr.dbname).get('res.lang')
ids=lang_obj.search(cr, uid, [('code', '=', lang_code)])
if not ids:
- raise osv.orm_except('Bad lang')
+ raise osv.except_osv(_('No language with code "%s" exists') % lang_code)
lang = lang_obj.browse(cr, uid, ids[0])
return lang.name
def act_cancel(self, cr, uid, ids, context=None):
@@ -50,9 +50,22 @@ class update_translations(osv.osv_memory):
buf.close()
return {'type': 'ir.actions.act_window_close'}
- _name = 'wizard.module.update_translations'
+ def default_get(self, cr, uid, fields, context=None):
+ res = super(base_update_translations, self).default_get(cr, uid, fields, context=context)
+
+ if context.get('active_model') != "res.lang":
+ return res
+
+ record_id = context and context.get('active_id', False) or False
+ if record_id:
+ lang = self.pool.get('res.lang').browse(cr, uid, record_id).code
+ res.update(lang=lang)
+ return res
+
+ _name = 'base.update.translations'
+ _inherit = "ir.wizard.screen"
_columns = {
'lang': fields.selection(_get_languages, 'Language', required=True),
}
-update_translations()
+base_update_translations()
\ No newline at end of file
diff --git a/bin/addons/base/module/wizard/base_update_translations_view.xml b/bin/addons/base/module/wizard/base_update_translations_view.xml
new file mode 100644
index 00000000000..60d30440ca1
--- /dev/null
+++ b/bin/addons/base/module/wizard/base_update_translations_view.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ Synchronize Translations
+ base.update.translations
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Synchronize Translations
+ ir.actions.act_window
+ base.update.translations
+ form
+ form
+ new
+
+
+
+
+
diff --git a/bin/addons/base/module/wizard/wizard_import_lang.py b/bin/addons/base/module/wizard/wizard_import_lang.py
deleted file mode 100644
index 70f94a68f98..00000000000
--- a/bin/addons/base/module/wizard/wizard_import_lang.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import tools
-import base64
-import pooler
-from tempfile import TemporaryFile
-
-view_form="""
-
-
-
-
-
-
-
-
-
-
-
-
- """
-
-fields_form={
- 'name':{'string':'Language name', 'type':'char', 'size':64, 'required':True},
- 'code':{'string':'Code (eg:en__US)', 'type':'char', 'size':5, 'required':True},
- 'data':{'string':'File', 'type':'binary', 'required':True},
-}
-
-class wizard_import_lang(wizard.interface):
-
- def _import_lang(self, cr, uid, data, context):
- form=data['form']
- fileobj = TemporaryFile('w+')
- fileobj.write( base64.decodestring(form['data']) )
-
- # now we determine the file format
- fileobj.seek(0)
- first_line = fileobj.readline().strip().replace('"', '').replace(' ', '')
- fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
- fileobj.seek(0)
-
- tools.trans_load_data(cr.dbname, fileobj, fileformat, form['code'], lang_name=form['name'])
- fileobj.close()
- return {}
-
- states={
- 'init':{
- 'actions': [],
- 'result': {'type': 'form', 'arch': view_form, 'fields': fields_form,
- 'state':[
- ('end', 'Cancel', 'gtk-cancel'),
- ('finish', 'Ok', 'gtk-ok', True)
- ]
- }
- },
- 'finish':{
- 'actions':[],
- 'result':{'type':'action', 'action':_import_lang, 'state':'end'}
- },
- }
-wizard_import_lang('module.lang.import')
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/wizard/wizard_module_import.py b/bin/addons/base/module/wizard/wizard_module_import.py
deleted file mode 100644
index 1350a4ad0ba..00000000000
--- a/bin/addons/base/module/wizard/wizard_module_import.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-
-import wizard
-import osv
-import pooler
-import os
-import tools
-
-import zipfile
-from StringIO import StringIO
-import base64
-from tools.translate import _
-
-finish_form ='''
-
-
-
-'''
-
-ask_form ='''
-
-
-
-
-
-'''
-
-ask_fields = {
- 'module_file': {'string': 'Module .ZIP file', 'type': 'binary', 'required': True},
-}
-
-class move_module_wizard(wizard.interface):
- def importzip(self, cr, uid, data, context):
- module_obj=pooler.get_pool(cr.dbname).get('ir.module.module')
- module_data = data['form']['module_file']
-
- val =base64.decodestring(module_data)
- fp = StringIO()
- fp.write(val)
- fdata = zipfile.ZipFile(fp, 'r')
- fname = fdata.namelist()[0]
- module_name = os.path.split(fname)[0]
-
- ad = tools.config['addons_path']
-
- fname = os.path.join(ad,module_name+'.zip')
- try:
- fp = file(fname, 'wb')
- fp.write(val)
- fp.close()
- except IOError, e:
- raise wizard.except_wizard(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
-
- pooler.get_pool(cr.dbname).get('ir.module.module').update_list(cr, uid)
- return {'module_name': module_name}
-
- def _action_module_open(self, cr, uid, data, context):
- return {
- 'domain': str([('name', '=', data['form']['module_name'])]),
- 'name': 'Module List',
- 'view_type': 'form',
- 'view_mode': 'tree,form',
- 'res_model': 'ir.module.module',
- 'view_id': False,
- 'type': 'ir.actions.act_window'
- }
-
- states = {
- 'init': {
- 'actions': [],
- 'result': {
- 'type': 'form',
- 'arch': ask_form,
- 'fields': ask_fields,
- 'state': [
- ('end', 'Cancel', 'gtk-cancel'),
- ('import', 'Import module', 'gtk-ok', True)
- ]
- }
- },
- 'import': {
- 'actions': [importzip],
- 'result': {
- 'type':'form',
- 'arch':finish_form,
- 'fields':{},
- 'state':[('open_window','Close')]
- }
- },
- 'open_window': {
- 'actions': [],
- 'result': {'type': 'action', 'action': _action_module_open, 'state':'end'}
- },
- }
-move_module_wizard('base.module.import')
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/wizard/wizard_module_lang_install.py b/bin/addons/base/module/wizard/wizard_module_lang_install.py
deleted file mode 100644
index 7367bdb4baa..00000000000
--- a/bin/addons/base/module/wizard/wizard_module_lang_install.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import tools
-import pooler
-
-view_form_end = """
-
-
-
-
-
-
- """
-
-view_form = """
-
-
-
-
-
-
-
-
- """
-
-
-class wizard_lang_install(wizard.interface):
- def _lang_install(self, cr, uid, data, context):
- lang = data['form']['lang']
- if lang:
- modobj = pooler.get_pool(cr.dbname).get('ir.module.module')
- mids = modobj.search(cr, uid, [('state', '=', 'installed')])
- modobj.update_translations(cr, uid, mids, lang)
- return {}
-
- fields_form = {
- 'lang': {'string':'Language', 'type':'selection', 'selection':tools.scan_languages(),
- },
- }
-
- states = {
- 'init': {
- 'actions': [],
- 'result': {'type': 'form', 'arch': view_form, 'fields': fields_form,
- 'state': [
- ('end', 'Cancel', 'gtk-cancel'),
- ('start', 'Start installation', 'gtk-ok', True)
- ]
- }
- },
- 'start': {
- 'actions': [_lang_install],
- 'result': {'type': 'form', 'arch': view_form_end, 'fields': {},
- 'state': [
- ('end', 'Ok', 'gtk-ok', True)
- ]
- }
- },
- }
-wizard_lang_install('module.lang.install')
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/wizard/wizard_module_upgrade.py b/bin/addons/base/module/wizard/wizard_module_upgrade.py
deleted file mode 100644
index 04be1c1428d..00000000000
--- a/bin/addons/base/module/wizard/wizard_module_upgrade.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import pooler
-import os
-import tools
-
-view_form_end = """
-
-
-
-
- """
-
-view_form = """
-
-
-
-
-
-
-
-
- """
-
-view_field = {
- "module_info": {'type': 'text', 'string': 'Modules to update', 'readonly': True},
-}
-
-class wizard_info_get(wizard.interface):
- def _get_install(self, cr, uid, data, context):
- pool=pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [
- ('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- res = mod_obj.read(cr, uid, ids, ['name','state'], context)
- return {'module_info': '\n'.join(map(lambda x: x['name']+' : '+x['state'], res))}
-
- def _check_upgrade_module(self,cr,uid,data,context):
- pool = pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [
- ('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- if ids and len(ids):
- return 'next'
- else:
- return 'end'
-
- def _upgrade_module(self, cr, uid, data, context):
- pool = pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- unmet_packages = []
- mod_dep_obj = pool.get('ir.module.module.dependency')
- for mod in mod_obj.browse(cr, uid, ids):
- depends_mod_ids = mod_dep_obj.search(cr, uid, [('module_id', '=', mod.id)])
- for dep_mod in mod_dep_obj.browse(cr, uid, depends_mod_ids):
- if dep_mod.state in ('unknown','uninstalled'):
- unmet_packages.append(dep_mod.name)
- if len(unmet_packages):
- raise wizard.except_wizard('Unmet dependency !', 'Following modules are uninstalled or unknown. \n\n'+'\n'.join(unmet_packages))
- mod_obj.download(cr, uid, ids, context=context)
- cr.commit()
- db, pool = pooler.restart_pool(cr.dbname, update_module=True)
- return {}
-
- def _config(self, cr, uid, data, context=None):
- return pooler.get_pool(cr.dbname).get('res.config')\
- .next(cr, uid, [], context=context)
-
- states = {
- 'init': {
- 'actions': [],
- 'result' : {'type': 'choice', 'next_state': _check_upgrade_module }
- },
- 'next': {
- 'actions': [_get_install],
- 'result': {'type':'form', 'arch':view_form, 'fields': view_field,
- 'state':[
- ('end', 'Cancel', 'gtk-cancel'),
- ('start', 'Start update', 'gtk-ok', True)
- ]
- }
- },
- 'start': {
- 'actions': [_upgrade_module],
- 'result': {'type':'form', 'arch':view_form_end, 'fields': {},
- 'state':[
- ('end', 'Close', 'gtk-close', True),
- ('config', 'Start configuration', 'gtk-ok', True)
- ]
- }
- },
- 'end': {
- 'actions': [],
- 'result': {'type':'form', 'arch':view_form_end, 'fields': {},
- 'state':[
- ('end', 'Close', 'gtk-close', True),
- ('config', 'Start configuration', 'gtk-ok', True)
- ]
- }
- },
- 'config':{
- 'result': {
- 'type': 'action',
- 'action': _config,
- 'state': 'end',
- },
- }
- }
-wizard_info_get('module.upgrade')
-
-
-class wizard_info_get_simple(wizard.interface):
- def _get_install(self, cr, uid, data, context):
- pool=pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [
- ('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- res = mod_obj.read(cr, uid, ids, ['name','state'], context)
- return {'module_info': '\n'.join(map(lambda x: x['name']+' : '+x['state'], res)),}
-
- def _check_upgrade_module(self,cr,uid,data,context):
- pool = pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [
- ('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- if ids and len(ids):
- return 'next'
- else:
- return 'end'
-
- def _upgrade_module(self, cr, uid, data, context):
- pool = pooler.get_pool(cr.dbname)
- mod_obj = pool.get('ir.module.module')
- ids = mod_obj.search(cr, uid, [('state', 'in', ['to upgrade', 'to remove', 'to install'])])
- unmet_packages = []
- mod_dep_obj = pool.get('ir.module.module.dependency')
- for mod in mod_obj.browse(cr, uid, ids):
- depends_mod_ids = mod_dep_obj.search(cr, uid, [('module_id', '=', mod.id)])
- for dep_mod in mod_dep_obj.browse(cr, uid, depends_mod_ids):
- if dep_mod.state in ('unknown','uninstalled'):
- unmet_packages.append(dep_mod.name)
- if len(unmet_packages):
- raise wizard.except_wizard('Unmet dependencies!', 'The following modules are not installed or unavailable. \n\n'+'\n'.join(unmet_packages))
- mod_obj.download(cr, uid, ids, context=context)
- cr.commit()
- db, pool = pooler.restart_pool(cr.dbname, update_module=True)
- return {}
-
- def _config(self, cr, uid, data, context=None):
- return pooler.get_pool(cr.dbname).get('res.config')\
- .next(cr, uid, [], context=context)
-
- states = {
- 'init': {
- 'actions': [],
- 'result' : {'type': 'choice', 'next_state': _check_upgrade_module }
- },
- 'next': {
- 'actions': [_get_install],
- 'result': {'type':'form', 'arch':view_form, 'fields': view_field,
- 'state':[
- ('end', 'Cancel', 'gtk-cancel'),
- ('start', 'Start update', 'gtk-ok', True)
- ]
- }
- },
- 'start': {
- 'actions': [_upgrade_module],
- 'result': {'type':'form', 'arch':view_form_end, 'fields': {},
- 'state':[
- ('end', 'Close', 'gtk-close', True),
- ('config', 'Start configuration', 'gtk-ok', True)
- ]
- }
- },
- 'end': {
- 'actions': [],
- 'result': {'type':'form', 'arch':view_form_end, 'fields': {},
- 'state':[
- ('end', 'Close', 'gtk-close', True),
- ('config', 'Start configuration', 'gtk-ok', True)
- ]
- }
- },
- 'config':{
- 'result': {
- 'type': 'action',
- 'action': _config,
- 'state': 'end',
- },
- }
- }
-wizard_info_get_simple('module.upgrade.simple')
-
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/module/wizard/wizard_update_module.py b/bin/addons/base/module/wizard/wizard_update_module.py
deleted file mode 100644
index 22e52bb6d6d..00000000000
--- a/bin/addons/base/module/wizard/wizard_update_module.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import netsvc
-import pooler
-
-class wizard_update_module(wizard.interface):
-
- arch = '''
-
-
-
- '''
- fields = {
-
- }
-
- arch_module = '''
-
-
-
- '''
-
- fields_module = {
- 'update': {'type': 'integer', 'string': 'Number of modules updated', 'readonly': True},
- 'add': {'type': 'integer', 'string': 'Number of modules added', 'readonly': True},
- }
-
- def _update_module(self, cr, uid, data, context):
- update, add = pooler.get_pool(cr.dbname).get('ir.module.module').update_list(cr, uid)
- return {'update': update, 'add': add}
-
- def _action_module_open(self, cr, uid, data, context):
-
- res = {
- 'domain': str([]),
- 'name': 'Module List',
- 'view_type': 'form',
- 'view_mode': 'tree,form',
- 'res_model': 'ir.module.module',
- 'view_id': False,
- 'type': 'ir.actions.act_window'
- }
-
- search_view_id = pooler.get_pool(cr.dbname).get('ir.ui.view').search(cr, uid, [('name','=','ir.module.module.list.select')], context=context)
- if search_view_id:
- res.update({'search_view_id' : search_view_id[0]})
- return res
-
- states = {
- 'init': {
- 'actions': [],
- 'result': {'type': 'form', 'arch': arch, 'fields': fields,
- 'state': [
- ('end', 'Cancel', 'gtk-cancel'),
- ('update', 'Check new modules', 'gtk-ok', True)
- ]
- }
- },
- 'update': {
- 'actions': [_update_module],
- 'result': {'type': 'form', 'arch': arch_module, 'fields': fields_module,
- 'state': [
- ('open_window', 'Ok', 'gtk-ok', True)
- ]
- }
- },
- 'open_window': {
- 'actions': [],
- 'result': {'type': 'action', 'action': _action_module_open, 'state':'end'}
- }
- }
-wizard_update_module('module.module.update')
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/report/custom_default_printscreen.xsl b/bin/addons/base/report/custom_default_printscreen.xsl
new file mode 100644
index 00000000000..b8b30222953
--- /dev/null
+++ b/bin/addons/base/report/custom_default_printscreen.xsl
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.3cm 19.3cm 28.5cm 19.3cm
+
+
+
+
+
+
+
diff --git a/bin/addons/base/report/custom_new.xsl b/bin/addons/base/report/custom_new.xsl
index 98fba709682..3a988a56373 100644
--- a/bin/addons/base/report/custom_new.xsl
+++ b/bin/addons/base/report/custom_new.xsl
@@ -1,8 +1,8 @@
-
-
+
+
@@ -14,8 +14,8 @@
-
-
+
+
@@ -114,14 +114,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/bin/addons/base/report/custom_rml_printscreen.xsl b/bin/addons/base/report/custom_rml_printscreen.xsl
new file mode 100644
index 00000000000..38e3a207b6f
--- /dev/null
+++ b/bin/addons/base/report/custom_rml_printscreen.xsl
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/addons/base/res/__init__.py b/bin/addons/base/res/__init__.py
index b7a14c78a93..1536f8b2d75 100644
--- a/bin/addons/base/res/__init__.py
+++ b/bin/addons/base/res/__init__.py
@@ -33,4 +33,5 @@ import res_request
import res_lang
import res_log
import ir_property
+import res_widget
diff --git a/bin/addons/base/res/bank_view.xml b/bin/addons/base/res/bank_view.xml
index d630efbe970..55a50d02c7f 100644
--- a/bin/addons/base/res/bank_view.xml
+++ b/bin/addons/base/res/bank_view.xml
@@ -45,13 +45,14 @@
- Bank List
+ Banks
res.bank
form
tree,form
+ Manage bank records you want to be used in the system.
-
+
diff --git a/bin/addons/base/res/config_pixmaps/1.png b/bin/addons/base/res/config_pixmaps/1.png
index ed0482f6061..63bd4337369 100755
Binary files a/bin/addons/base/res/config_pixmaps/1.png and b/bin/addons/base/res/config_pixmaps/1.png differ
diff --git a/bin/addons/base/res/config_pixmaps/2.png b/bin/addons/base/res/config_pixmaps/2.png
index 430d6a12b88..80d4e3602bb 100755
Binary files a/bin/addons/base/res/config_pixmaps/2.png and b/bin/addons/base/res/config_pixmaps/2.png differ
diff --git a/bin/addons/base/res/config_pixmaps/3.png b/bin/addons/base/res/config_pixmaps/3.png
index a8d1ffa65a1..63acab34263 100755
Binary files a/bin/addons/base/res/config_pixmaps/3.png and b/bin/addons/base/res/config_pixmaps/3.png differ
diff --git a/bin/addons/base/res/country_view.xml b/bin/addons/base/res/country_view.xml
index f799ca271d0..36b3c6f875f 100644
--- a/bin/addons/base/res/country_view.xml
+++ b/bin/addons/base/res/country_view.xml
@@ -35,9 +35,10 @@
ir.actions.act_window
res.country
form
+ Display and manage the list of all countries that can be assigned to your partner records. You can create or delete countries to make sure the ones you are working on will be maintained.
-
+
@@ -77,6 +78,7 @@
res.country.state
form
+ If you are working on the American market, you can manage the different federal states you are working on from here. Each state is attached to one country.
diff --git a/bin/addons/base/res/ir_property.py b/bin/addons/base/res/ir_property.py
index 8dcd01c4897..43451f824a1 100644
--- a/bin/addons/base/res/ir_property.py
+++ b/bin/addons/base/res/ir_property.py
@@ -20,8 +20,9 @@
##############################################################################
from osv import osv,fields
-from operator import attrgetter
+from tools.misc import attrgetter
import time
+
# -------------------------------------------------------------------------
# Properties
# -------------------------------------------------------------------------
@@ -31,28 +32,27 @@ class ir_property(osv.osv):
def _models_field_get(self, cr, uid, field_key, field_value, context=None):
get = attrgetter(field_key, field_value)
-
obj = self.pool.get('ir.model.fields')
ids = obj.search(cr, uid, [('view_load','=',1)], context=context)
res = set()
for o in obj.browse(cr, uid, ids, context=context):
res.add(get(o))
- return res
+ return list(res)
def _models_get(self, cr, uid, context=None):
- return self._models_field_get(cr, uid, 'model', 'name', context)
+ return self._models_field_get(cr, uid, 'model', 'model_id.name', context)
def _models_get2(self, cr, uid, context=None):
return self._models_field_get(cr, uid, 'relation', 'relation', context)
_columns = {
- 'name': fields.char('Name', size=128),
+ 'name': fields.char('Name', size=128, select=1),
'res_id': fields.reference('Resource', selection=_models_get, size=128,
- help="If not set, act as default property"),
- 'company_id': fields.many2one('res.company', 'Company'),
- 'fields_id': fields.many2one('ir.model.fields', 'Fields', ondelete='cascade', required=True),
+ help="If not set, acts as a default value for new resources", select=1),
+ 'company_id': fields.many2one('res.company', 'Company', select=1),
+ 'fields_id': fields.many2one('ir.model.fields', 'Field', ondelete='cascade', required=True, select=1),
'value_float' : fields.float('Value'),
'value_integer' : fields.integer_big('Value'), # will contain (int, bigint)
@@ -135,15 +135,7 @@ class ir_property(osv.osv):
def create(self, cr, uid, values, context=None):
return super(ir_property, self).create(cr, uid, self._update_values(cr, uid, None, values), context=context)
- def get_by_id(self, cr, uid, record_ids, context=None):
- if isinstance(record_ids, (int, long)):
- record_ids = [record_ids]
-
- if not record_ids:
- return False
-
- record = self.browse(cr, uid, record_ids[0], context=context)
-
+ def get_by_record(self, cr, uid, record, context=None):
if record.type in ('char', 'text'):
return record.value_text
elif record.type == 'float':
@@ -162,7 +154,6 @@ class ir_property(osv.osv):
if not record.value_datetime:
return False
return time.strftime('%Y-%m-%d', time.strptime(record.value_datetime, '%Y-%m-%d %H:%M:%S'))
-
return False
def get(self, cr, uid, name, model, res_id=False, context={}):
@@ -170,7 +161,9 @@ class ir_property(osv.osv):
if domain is not None:
domain = [('res_id', '=', res_id)] + domain
nid = self.search(cr, uid, domain, context=context)
- return self.get_by_id(cr, uid, nid, context=context)
+ if not nid: return False
+ record = self.browse(cr, uid, nid[0], context=context)
+ return self.get_by_record(cr, uid, record, context=context)
return False
def _get_domain_default(self, cr, uid, prop_name, model, context=None):
diff --git a/bin/addons/base/res/ir_property_view.xml b/bin/addons/base/res/ir_property_view.xml
index c6aba4996a0..399cac47136 100644
--- a/bin/addons/base/res/ir_property_view.xml
+++ b/bin/addons/base/res/ir_property_view.xml
@@ -28,6 +28,7 @@
+
@@ -48,6 +49,7 @@
+
@@ -71,7 +73,7 @@
form
-
+
diff --git a/bin/addons/base/res/partner/crm.py b/bin/addons/base/res/partner/crm.py
index ba9dbcf1b04..6d123ea7984 100644
--- a/bin/addons/base/res/partner/crm.py
+++ b/bin/addons/base/res/partner/crm.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -38,17 +38,6 @@ class res_partner_canal(osv.osv):
}
res_partner_canal()
-#
-# Partner: State of Mind
-#
-class res_partner_som(osv.osv):
- _name = "res.partner.som"
- _columns = {
- 'name': fields.char('State of Mind',size=64, required=True),
- 'factor': fields.float('Factor', required=True)
- }
-res_partner_som()
-
def _links_get(self, cr, uid, context={}):
obj = self.pool.get('res.request.link')
ids = obj.search(cr, uid, [])
@@ -66,8 +55,8 @@ class res_partner_event(osv.osv):
}
_order = 'date desc'
_defaults = {
- 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
- 'user_id': lambda self, cr, uid, context: uid,
+ 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'user_id': lambda self, cr, uid, context: uid,
}
res_partner_event()
diff --git a/bin/addons/base/res/partner/crm_demo.xml b/bin/addons/base/res/partner/crm_demo.xml
index d88fd99f33c..03c69ef51d7 100644
--- a/bin/addons/base/res/partner/crm_demo.xml
+++ b/bin/addons/base/res/partner/crm_demo.xml
@@ -13,27 +13,6 @@
E-mail
-
-
- Fury
- 1.0
-
-
- Unhappy
- 3.0
-
-
- Normal
- 5.0
-
-
- Happy
- 7.0
-
-
- Very Happy
- 8.5
-
-
+
diff --git a/bin/addons/base/res/partner/crm_view.xml b/bin/addons/base/res/partner/crm_view.xml
index bc359ff9fc8..51944da65c8 100644
--- a/bin/addons/base/res/partner/crm_view.xml
+++ b/bin/addons/base/res/partner/crm_view.xml
@@ -29,40 +29,12 @@
res.partner.canal
form
tree,form
+ Track from where is coming your leads and opportunities by creating specific channels that will be maintained at the creation of a document in the system. Some examples of channels can be: Website, Phone Call, Reseller, etc.
-
-
-
-
- res.partner.som.tree
- res.partner.som
- tree
-
-
-
-
-
-
-
-
-
- res.partner.som.form
- res.partner.som
- form
-
-
-
-
-
-
-
-
- States of mind
- res.partner.som
- form
- tree,form
-
-
+
+
+
res.partner.event.form
@@ -93,4 +65,4 @@
-
\ No newline at end of file
+
diff --git a/bin/addons/base/res/partner/partner.py b/bin/addons/base/res/partner/partner.py
index 11a3032dafc..2c7747b4fd7 100644
--- a/bin/addons/base/res/partner/partner.py
+++ b/bin/addons/base/res/partner/partner.py
@@ -84,7 +84,7 @@ class res_partner_title(osv.osv):
_name = 'res.partner.title'
_columns = {
'name': fields.char('Title', required=True, size=46, translate=True),
- 'shortcut': fields.char('Shortcut', required=True, size=16),
+ 'shortcut': fields.char('Shortcut', required=True, size=16, translate=True),
'domain': fields.selection([('partner','Partner'),('contact','Contact')], 'Domain', required=True, size=24)
}
_order = 'name'
@@ -104,7 +104,7 @@ class res_partner(osv.osv):
_columns = {
'name': fields.char('Name', size=128, required=True, select=True),
'date': fields.date('Date', select=1),
- 'title': fields.many2one('res.partner.title','Title'),
+ 'title': fields.many2one('res.partner.title','Partner Form'),
'parent_id': fields.many2one('res.partner','Parent Partner', select=2),
'child_ids': fields.one2many('res.partner', 'parent_id', 'Partner Ref.'),
'ref': fields.char('Reference', size=64),
@@ -130,7 +130,7 @@ class res_partner(osv.osv):
'email': fields.related('address', 'email', type='char', size=240, string='E-mail'),
'company_id': fields.many2one('res.company', 'Company', select=1),
}
-
+
def _default_category(self, cr, uid, context={}):
if 'category_id' in context and context['category_id']:
return [context['category_id']]
@@ -305,7 +305,10 @@ class res_partner_address(osv.osv):
if r['name'] and (r['city'] or r['country_id']):
addr += ', '
addr += (r['country_id'] and r['country_id'][1] or '') + ' ' + (r['city'] or '') + ' ' + (r['street'] or '')
- res.append((r['id'], addr.strip() or '/'))
+ if (context.get('contact_display', 'contact')=='partner_address') and r['partner_id']:
+ res.append((r['id'], "%s: %s" % (r['partner_id'][1], addr.strip() or '/')))
+ else:
+ res.append((r['id'], addr.strip() or '/'))
return res
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
@@ -313,7 +316,7 @@ class res_partner_address(osv.osv):
args=[]
if not context:
context={}
- if context.get('contact_display', 'contact')=='partner':
+ if context.get('contact_display', 'contact')=='partner ' or context.get('contact_display', 'contact')=='partner_address ' :
ids = self.search(cr, user, [('partner_id',operator,name)], limit=limit, context=context)
else:
if not name:
@@ -389,7 +392,7 @@ class res_partner_bank(osv.osv):
_columns = {
'name': fields.char('Description', size=128),
'acc_number': fields.char('Account Number', size=64, required=False),
- 'bank': fields.many2one('res.bank', 'Bank'),
+ 'bank': fields.many2one('res.bank', 'Bank', required=True),
'owner_name': fields.char('Account Owner', size=64),
'street': fields.char('Street', size=128),
'zip': fields.char('Zip', change_default=True, size=24),
diff --git a/bin/addons/base/res/partner/partner_data.xml b/bin/addons/base/res/partner/partner_data.xml
index 642c4cc46bf..46e9faac130 100644
--- a/bin/addons/base/res/partner/partner_data.xml
+++ b/bin/addons/base/res/partner/partner_data.xml
@@ -3,8 +3,8 @@
+ Resource: res.partner.title
+ -->
partner
Corp.
@@ -49,9 +49,9 @@
-
+
Partner
diff --git a/bin/addons/base/res/partner/partner_demo.xml b/bin/addons/base/res/partner/partner_demo.xml
index e966a5308ce..6f433675767 100644
--- a/bin/addons/base/res/partner/partner_demo.xml
+++ b/bin/addons/base/res/partner/partner_demo.xml
@@ -2,8 +2,8 @@
+ Resource: res.partner.category
+ -->
Customer
@@ -62,8 +62,31 @@
+ Resource: res.partner.category for training
+ -->
+
+ Consumers
+
+
+
+ Retailers
+
+
+
+
+ Miscellaneous Suppliers
+
+
+
+
+
+ Wood Suppliers
+
+
+
+
ASUStek
@@ -179,9 +202,57 @@
+
+
+
+
+
+ Dubois sprl
+
+
+
+ Eric Dubois
+
+
+
+ Fabien Dupont
+
+
+
+ Lucie Vonck
+
+
+
+ NotSoTiny SARL
+
+
+
+ The Shelve House
+
+
+
+
+ Vicking Direct
+
+ 1
+
+
+
+ Wood y Wood Pecker
+
+ 1
+
+
+
+ ZeroOne Inc
+
+
+
+ Resource: res.partner.address
+ -->
Bruxelles
@@ -359,14 +430,143 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- ASUSTEK-Subject
-
-
+ ASUSTEK-Subject
+
+
diff --git a/bin/addons/base/res/partner/partner_view.xml b/bin/addons/base/res/partner/partner_view.xml
index 47bc2727b4d..30bf8b49b98 100644
--- a/bin/addons/base/res/partner/partner_view.xml
+++ b/bin/addons/base/res/partner/partner_view.xml
@@ -8,10 +8,7 @@
-
-
-
- res.partner.category.form
+ Partner Categories
res.partner.category
form
@@ -517,7 +519,7 @@
- res.partner.category.list
+ Partner Categories
res.partner.category
tree
@@ -574,9 +576,10 @@
ir.actions.act_window
res.partner.category
form
+ Manage the partner categories in order to better classify them for tracking and analysis purposes. A partner may belong to several categories and categories have a hierarchy structure: a partner belonging to a category also belong to his parent category.
-
+
- Address Book
+ Customers
ir.ui.menu
diff --git a/bin/addons/base/res/partner/partner_wizard.xml b/bin/addons/base/res/partner/partner_wizard.xml
deleted file mode 100644
index cc62aa9c7bc..00000000000
--- a/bin/addons/base/res/partner/partner_wizard.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/bin/addons/base/res/partner/wizard/__init__.py b/bin/addons/base/res/partner/wizard/__init__.py
index f1a85740bf4..65869c366c4 100644
--- a/bin/addons/base/res/partner/wizard/__init__.py
+++ b/bin/addons/base/res/partner/wizard/__init__.py
@@ -19,10 +19,10 @@
#
##############################################################################
-import wizard_spam
-import wizard_clear_ids
-import wizard_ean_check
import partner_sms_send
+import partner_wizard_spam
+import partner_clear_ids
+import partner_wizard_ean_check
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/res/partner/wizard/partner_clear_ids.py b/bin/addons/base/res/partner/wizard/partner_clear_ids.py
new file mode 100644
index 00000000000..880dbaed040
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_clear_ids.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import netsvc
+from osv import fields, osv
+
+class partner_clear_ids(osv.osv_memory):
+ """ Clear IDs """
+
+ _name = "partner.clear.ids"
+ _description = "Clear IDs"
+
+ def clear_ids(self, cr, uid, ids, context):
+ """
+ Clear Ids
+
+ @param cr: the current row, from the database cursor.
+ @param uid: the current user’s ID for security checks.
+ @param ids: the ID or list of IDs
+ @param context: A standard dictionary
+ """
+
+ partner_pool = self.pool.get('res.partner')
+ active_ids = context and context.get('active_ids', [])
+ res = {}
+ for partner in partner_pool.browse(cr, uid, active_ids, context=context):
+ if active_ids in partner:
+ res.update({'ref': False})
+ return res
+
+partner_clear_ids()
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/bin/addons/base/res/partner/wizard/partner_clear_ids_view.xml b/bin/addons/base/res/partner/wizard/partner_clear_ids_view.xml
new file mode 100644
index 00000000000..5bc5d747de2
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_clear_ids_view.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ Clear IDs
+ partner.clear.ids
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear IDs
+ ir.actions.act_window
+ partner.clear.ids
+ form
+ form
+ new
+
+
+
+
\ No newline at end of file
diff --git a/bin/addons/base/res/partner/wizard/partner_sms_send_view.xml b/bin/addons/base/res/partner/wizard/partner_sms_send_view.xml
index fafdc33d72c..97c418d9f81 100644
--- a/bin/addons/base/res/partner/wizard/partner_sms_send_view.xml
+++ b/bin/addons/base/res/partner/wizard/partner_sms_send_view.xml
@@ -14,13 +14,13 @@
-
+
-
+
@@ -32,7 +32,6 @@
view_mode="form"
target="new"
key2="client_action_multi"
- id="action_partner_sms_send"
- groups="base.group_extended"/>
+ id="action_partner_sms_send"/>
diff --git a/bin/addons/base/res/partner/wizard/partner_wizard_ean_check.py b/bin/addons/base/res/partner/wizard/partner_wizard_ean_check.py
new file mode 100644
index 00000000000..8ee383c18f5
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_wizard_ean_check.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import math
+from osv import osv
+from tools.misc import UpdateableStr
+import pooler
+
+class partner_wizard_ean_check(osv.osv_memory):
+ """ Ean check """
+
+ _name = "partner.wizard.ean.check"
+ _description = "Ean Check"
+
+ def is_pair(x):
+ return not x%2
+
+ def get_ean_key(string):
+ if not string or string=='':
+ return '0'
+ if len(string)!=12:
+ return '0'
+ sum = 0
+ for i in range(12):
+ if is_pair(i):
+ sum = sum + int(string[i])
+ else:
+ sum = sum + 3*int(string[i])
+ return str(int(math.ceil(sum/10.0)*10-sum))
+
+ def default_get(self, cr, fields, context):
+ partner_table = self.pool.get('res.partner')
+ ids = context.get('active_ids')
+ partners = partner_table.browse(cr, uid, ids)
+ _check_arch_lst = ['', '', ' ', ' ',' ', ' ']
+ for partner in partners:
+ if partner['ean13'] and len(partner['ean13'])>11 and len(partner['ean13'])<14:
+ _check_arch_lst.append(' ' % partner['ean13']);
+ key = get_ean_key(partner['ean13'][:12])
+ _check_arch_lst.append(' ')
+ if len(partner['ean13'])==12:
+ _check_arch_lst.append(' ');
+ else:
+ _check_arch_lst.append(' ' % partner['ean13'][12])
+ _check_arch_lst.append(' ' % key)
+ _check_arch_lst.append(' ')
+ _check_arch.string = '\n'.join(_check_arch_lst)
+
+ def update_ean(self, cr, uid, ids, context):
+ partner_table = self.pool.get('res.partner')
+ ids = context.get('active_ids')
+ partners = partner_table.browse(cr, uid, ids)
+ for partner in partners:
+ partner_table.write(cr, uid, ids, {
+ 'ean13': "%s%s" % (partner['ean13'][:12], get_ean_key(partner['ean13'][:12]))
+ })
+ return {}
+
+partner_wizard_ean_check()
\ No newline at end of file
diff --git a/bin/addons/base/res/partner/wizard/partner_wizard_ean_check_view.xml b/bin/addons/base/res/partner/wizard/partner_wizard_ean_check_view.xml
new file mode 100644
index 00000000000..9112f2ceed8
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_wizard_ean_check_view.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ Ean check
+ partner.wizard.ean.check
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ean check
+ ir.actions.act_window
+ partner.wizard.ean.check
+ form
+ form
+ new
+
+
+
+
\ No newline at end of file
diff --git a/bin/addons/base/res/partner/wizard/partner_wizard_spam.py b/bin/addons/base/res/partner/wizard/partner_wizard_spam.py
new file mode 100644
index 00000000000..f349825d588
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_wizard_spam.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import netsvc
+import tools
+from osv import fields, osv
+
+class partner_wizard_spam(osv.osv_memory):
+ """ Mass Mailing """
+
+ _name = "partner.wizard.spam"
+ _description = "Mass Mailing"
+
+ _columns = {
+ 'email_from': fields.char("Sender's email", size=256, required=True),
+ 'subject': fields.char('Subject', size=256,required=True),
+ 'text': fields.text('Message',required=True),
+ }
+
+ def mass_mail_send(self, cr, uid, ids, context):
+ """
+ Send Email
+
+ @param cr: the current row, from the database cursor.
+ @param uid: the current user’s ID for security checks.
+ @param ids: the ID or list of IDs
+ @param context: A standard dictionary
+ """
+
+ nbr = 0
+ partner_pool = self.pool.get('res.partner')
+ data = self.browse(cr, uid, ids[0], context=context)
+ event_pool = self.pool.get('res.partner.event')
+ active_ids = context and context.get('active_ids', [])
+ partners = partner_pool.browse(cr, uid, active_ids, context)
+ for partner in partners:
+ for adr in partner.address:
+ if adr.email:
+ name = adr.name or partner.name
+ to = '%s <%s>' % (name, adr.email)
+ #TODO: add some tests to check for invalid email addresses
+ #CHECKME: maybe we should use res.partner/email_send
+ tools.email_send(data.email_from, [to], data.subject, data.text,subtype='html')
+ nbr += 1
+ event_pool.create(cr, uid,
+ {'name': 'Email(s) sent through mass mailing',
+ 'partner_id': partner.id,
+ 'description': data.text })
+ #TODO: log number of message sent
+ return {'email_sent': nbr}
+
+partner_wizard_spam()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/addons/base/res/partner/wizard/partner_wizard_spam_view.xml b/bin/addons/base/res/partner/wizard/partner_wizard_spam_view.xml
new file mode 100644
index 00000000000..b7c940b5a22
--- /dev/null
+++ b/bin/addons/base/res/partner/wizard/partner_wizard_spam_view.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ Mass Mailing
+ partner.wizard.spam
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bin/addons/base/res/partner/wizard/wizard_ean_check.py b/bin/addons/base/res/partner/wizard/wizard_ean_check.py
deleted file mode 100644
index cd757c232f5..00000000000
--- a/bin/addons/base/res/partner/wizard/wizard_ean_check.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import math
-from osv import osv
-from tools.misc import UpdateableStr
-import pooler
-
-def _is_pair(x):
- return not x%2
-
-def _get_ean_key(string):
- if not string or string=='':
- return '0'
- if len(string)!=12:
- return '0'
- sum=0
- for i in range(12):
- if _is_pair(i):
- sum+=int(string[i])
- else:
- sum+=3*int(string[i])
- return str(int(math.ceil(sum/10.0)*10-sum))
-
-#FIXME: this is not concurrency safe !!!!
-_check_arch = UpdateableStr()
-_check_fields = {}
-
-def _check_key(self, cr, uid, data, context):
- partner_table=pooler.get_pool(cr.dbname).get('res.partner')
- partners = partner_table.browse(cr, uid, data['ids'])
- _check_arch_lst=['', '', ' ', ' ',' ', ' ']
- for partner in partners:
- if partner['ean13'] and len(partner['ean13'])>11 and len(partner['ean13'])<14:
- _check_arch_lst.append(' ' % partner['ean13']);
- key=_get_ean_key(partner['ean13'][:12])
- _check_arch_lst.append(' ')
- if len(partner['ean13'])==12:
- _check_arch_lst.append(' ');
- else:
- _check_arch_lst.append(' ' % partner['ean13'][12])
- _check_arch_lst.append(' ' % key)
- _check_arch_lst.append(' ')
- _check_arch.string = '\n'.join(_check_arch_lst)
- return {}
-
-def _update_ean(self, cr, uid, data, context):
- partner_table = pooler.get_pool(cr.dbname).get('res.partner')
- partners = partner_table.browse(cr, uid, data['ids'])
- for partner in partners:
- partner_table.write(cr, uid, data['ids'], {
- 'ean13': "%s%s" % (partner['ean13'][:12], _get_ean_key(partner['ean13'][:12]))
- })
- return {}
-
-class wiz_ean_check(wizard.interface):
- states = {
- 'init': {
- 'actions': [_check_key],
- 'result': {
- 'type': 'form',
- 'arch': _check_arch,
- 'fields': _check_fields,
- 'state': (('end', 'Ignore'), ('correct', 'Correct EAN13'))
- }
- },
- 'correct' : {
- 'actions': [_update_ean],
- 'result': {
- 'type': 'state',
- 'state': 'end'
- }
- }
- }
-
-wiz_ean_check('res.partner.ean13')
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/res/partner/wizard/wizard_spam.py b/bin/addons/base/res/partner/wizard/wizard_spam.py
deleted file mode 100644
index c08baa898a4..00000000000
--- a/bin/addons/base/res/partner/wizard/wizard_spam.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import wizard
-import pooler
-import tools
-
-email_send_form = '''
-
-
-
-
-
-
- '''
-
-email_send_fields = {
- 'from': {'string':"Sender's email", 'type':'char', 'size':64, 'required':True},
- 'subject': {'string':'Subject', 'type':'char', 'size':64, 'required':True},
- 'text': {'string':'Message', 'type':'text_tag', 'required':True}
-}
-
-# this sends an email to ALL the addresses of the selected partners.
-def _mass_mail_send(self, cr, uid, data, context):
- nbr = 0
- partners = pooler.get_pool(cr.dbname).get('res.partner').browse(cr, uid, data['ids'], context)
- for partner in partners:
- for adr in partner.address:
- if adr.email:
- name = adr.name or partner.name
- to = '%s <%s>' % (name, adr.email)
-#TODO: add some tests to check for invalid email addresses
-#CHECKME: maybe we should use res.partner/email_send
- tools.email_send(data['form']['from'], [to], data['form']['subject'], data['form']['text'],subtype='html')
- nbr += 1
- pooler.get_pool(cr.dbname).get('res.partner.event').create(cr, uid,
- {'name': 'Email sent through mass mailing',
- 'partner_id': partner.id,
- 'description': data['form']['text'], })
-#TODO: log number of message sent
- return {'email_sent': nbr}
-
-class part_email(wizard.interface):
- states = {
- 'init': {
- 'actions': [],
- 'result': {'type': 'form', 'arch': email_send_form, 'fields': email_send_fields, 'state':[('end','Cancel'), ('send','Send Email')]}
- },
- 'send': {
- 'actions': [_mass_mail_send],
- 'result': {'type': 'state', 'state':'end'}
- }
- }
-part_email('res.partner.spam_send')
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
diff --git a/bin/addons/base/res/res_company.py b/bin/addons/base/res/res_company.py
index 3601e57d321..a2ed3c05840 100644
--- a/bin/addons/base/res/res_company.py
+++ b/bin/addons/base/res/res_company.py
@@ -42,7 +42,7 @@ class multi_company_default(osv.osv):
'company_dest_id': fields.many2one('res.company', 'Default Company', required=True,
help='Company to store the current record'),
'object_id': fields.many2one('ir.model', 'Object', required=True,
- help='Object affect by this rules'),
+ help='Object affected by this rule'),
'expression': fields.char('Expression', size=256, required=True,
help='Expression, must be True to match\nuse context.get or user (browse)'),
'field_id': fields.many2one('ir.model.fields', 'Field', help='Select field property'),
@@ -82,6 +82,7 @@ class res_company(osv.osv):
'rml_footer2': fields.char('Report Footer 2', size=200),
'rml_header' : fields.text('RML Header'),
'rml_header2' : fields.text('RML Internal Header'),
+ 'rml_header3' : fields.text('RML Internal Header'),
'logo' : fields.binary('Logo'),
'currency_id': fields.many2one('res.currency', 'Currency', required=True),
'currency_ids': fields.one2many('res.currency', 'company_id', 'Currency'),
@@ -89,8 +90,8 @@ class res_company(osv.osv):
'account_no':fields.char('Account No.', size=64),
}
- def search(self, cr, uid, args, offset=0, limit=None, order=None,
- context=None, count=False):
+ def _search(self, cr, uid, args, offset=0, limit=None, order=None,
+ context=None, count=False, access_rights_uid=None):
if context is None:
context = {}
@@ -105,8 +106,8 @@ class res_company(osv.osv):
user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids]))
return cmp_ids
- return super(res_company, self).search(cr, uid, args, offset=offset, limit=limit, order=order,
- context=context, count=count)
+ return super(res_company, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
+ context=context, count=count, access_rights_uid=access_rights_uid)
def _company_default_get(self, cr, uid, object=False, field=False, context=None):
"""
@@ -161,9 +162,17 @@ class res_company(osv.osv):
def cache_restart(self, cr):
self._get_company_children.clear_cache(cr.dbname)
- def create(self, cr, *args, **argv):
+ def create(self, cr, uid, vals, context=None):
+ if not vals.get('name', False) or vals.get('partner_id', False):
+ self.cache_restart(cr)
+ return super(res_company, self).create(cr, uid, vals, context=context)
+ obj_partner = self.pool.get('res.partner')
+ partner_id = obj_partner.create(cr, uid, {'name': vals['name']}, context=context)
+ vals.update({'partner_id': partner_id})
self.cache_restart(cr)
- return super(res_company, self).create(cr, *args, **argv)
+ company_id = super(res_company, self).create(cr, uid, vals, context=context)
+ obj_partner.write(cr, uid, partner_id, {'company_id': company_id}, context=context)
+ return company_id
def write(self, cr, *args, **argv):
self.cache_restart(cr)
@@ -190,7 +199,23 @@ class res_company(osv.osv):
tools.config['root_path'], '..', 'pixmaps', 'openerp-header.png'),
'rb') .read().encode('base64')
-
+ def _get_header3(self,cr,uid,ids):
+ return """
+
+
+
+
+
+
+
+ [[ formatLang(time.strftime("%Y-%m-%d"), date=True) ]] [[ time.strftime("%H:%M") ]]
+
+ [[ company.partner_id.name ]]
+
+ 25 720 1085 720
+
+
+ """
def _get_header2(self,cr,uid,ids):
return """
@@ -203,9 +228,6 @@ class res_company(osv.osv):
[[ formatLang(time.strftime("%Y-%m-%d"), date=True) ]] [[ time.strftime("%H:%M") ]]
[[ company.partner_id.name ]]
-
- /
-
1.3cm 28.1cm 20cm 28.1cm
@@ -253,7 +275,8 @@ class res_company(osv.osv):
'currency_id': _get_euro,
'rml_header':_get_header,
'rml_header2': _get_header2,
- 'logo':_get_logo
+ 'rml_header3': _get_header3,
+ #'logo':_get_logo
}
_constraints = [
diff --git a/bin/addons/base/res/res_config.py b/bin/addons/base/res/res_config.py
index 1dae2c7b88f..5b3b0755f96 100644
--- a/bin/addons/base/res/res_config.py
+++ b/bin/addons/base/res/res_config.py
@@ -18,20 +18,15 @@
# along with this program. If not, see .
#
##############################################################################
-
-import os
-import base64
-import random
+import logging
from operator import attrgetter
from osv import osv, fields
-import tools
from tools.translate import _
import netsvc
import pooler
-
class res_config_configurable(osv.osv_memory):
''' Base classes for new-style configuration items
@@ -40,42 +35,42 @@ class res_config_configurable(osv.osv_memory):
their view inherit from the related res_config_view_base view.
'''
_name = 'res.config'
+ _inherit = 'ir.wizard.screen'
logger = netsvc.Logger()
+ __logger = logging.getLogger(_name)
+
+ def get_current_progress(self, cr, uid, context=None):
+ '''Return a description the current progress of configuration:
+ a tuple of (non_open_todos:int, total_todos: int)
+ '''
+ return (self.pool.get('ir.actions.todo')\
+ .search_count(cr, uid, [('state','<>','open')], context),
+ self.pool.get('ir.actions.todo')\
+ .search_count(cr, uid, [], context))
def _progress(self, cr, uid, context=None):
- total = self.pool.get('ir.actions.todo')\
- .search_count(cr, uid, [], context)
- open = self.pool.get('ir.actions.todo')\
- .search_count(cr, uid, [('state','<>','open')], context)
+ closed, total = self.get_current_progress(cr, uid, context=context)
if total:
- return round(open*100./total)
+ return round(closed*100./total)
return 100.
- def _get_image(self, cr, uid, context=None):
- file_no = str(random.randint(1,3))
- path = os.path.join('base','res','config_pixmaps/%s.png'%file_no)
- file_data = tools.file_open(path,'rb').read()
- return base64.encodestring(file_data)
-
_columns = dict(
progress = fields.float('Configuration Progress', readonly=True),
- config_logo = fields.binary('Image', readonly=True),
- )
+ )
+
_defaults = dict(
progress = _progress,
- config_logo = _get_image
- )
+ )
- def _next_action(self, cr, uid):
+ def _next_action(self, cr, uid, context=None):
todos = self.pool.get('ir.actions.todo')
- self.logger.notifyChannel('actions', netsvc.LOG_INFO,
- 'getting next %s' % todos)
+ self.__logger.info('getting next %s', todos)
active_todos = todos.search(cr, uid, [('state','=','open')],
limit=1)
- dont_skip_todo = True
if active_todos:
todo_obj = todos.browse(cr, uid, active_todos[0], context=None)
todo_groups = map(lambda x:x.id, todo_obj.groups_id)
+ dont_skip_todo = True
if todo_groups:
cr.execute("select 1 from res_groups_users_rel where uid=%s and gid IN %s",(uid, tuple(todo_groups),))
dont_skip_todo = bool(cr.fetchone())
@@ -86,32 +81,23 @@ class res_config_configurable(osv.osv_memory):
return self._next_action(cr, uid)
return None
- def _set_previous_todo(self, cr, uid, state):
+ def _set_previous_todo(self, cr, uid, state, context=None):
""" lookup the previous (which is still the next at this point)
ir.actions.todo, set it to whatever state was provided.
-
- Raises
- `LookupError`: if we couldn't find *any* previous todo
- `ValueError`: if no state is provided
- anything ir_actions_todo.write can throw
"""
# this is ultra brittle, but apart from storing the todo id
# into the res.config view, I'm not sure how to get the
# "previous" todo
- previous_todo = self._next_action(cr, uid)
+ previous_todo = self._next_action(cr, uid, context=context)
if not previous_todo:
- raise LookupError(_("Couldn't find previous ir.actions.todo"))
- if not state:
- raise ValueError(_("Can't set an ir.actions.todo's state to "
- "nothingness"))
+ self.__logger.warn(_("Couldn't find previous ir.actions.todo"))
+ return
previous_todo.write({'state':state})
- def _next(self, cr, uid):
- self.logger.notifyChannel('actions', netsvc.LOG_INFO,
- 'getting next operation')
+ def _next(self, cr, uid, context=None):
+ self.__logger.info('getting next operation')
next = self._next_action(cr, uid)
- self.logger.notifyChannel('actions', netsvc.LOG_INFO,
- 'next action is %s' % next)
+ self.__logger.info('next action is %s', next)
if next:
action = next.action_id
return {
@@ -122,9 +108,7 @@ class res_config_configurable(osv.osv_memory):
'type': action.type,
'target': action.target,
}
- self.logger.notifyChannel(
- 'actions', netsvc.LOG_INFO,
- 'all configuration actions have been executed')
+ self.__logger.info('all configuration actions have been executed')
current_user_menu = self.pool.get('res.users')\
.browse(cr, uid, uid).menu_id
@@ -133,7 +117,6 @@ class res_config_configurable(osv.osv_memory):
.read(cr, uid, current_user_menu.id)
def start(self, cr, uid, ids, context=None):
- print 'Start'
ids2 = self.pool.get('ir.actions.todo').search(cr, uid, [], context=context)
for todo in self.pool.get('ir.actions.todo').browse(cr, uid, ids2, context=context):
if (todo.restart=='always') or (todo.restart=='onskip' and (todo.state in ('skip','cancel'))):
@@ -144,7 +127,7 @@ class res_config_configurable(osv.osv_memory):
""" Returns the next todo action to execute (using the default
sort order)
"""
- return self._next(cr, uid)
+ return self._next(cr, uid, context=context)
def execute(self, cr, uid, ids, context=None):
""" Method called when the user clicks on the ``Next`` button.
@@ -179,7 +162,7 @@ class res_config_configurable(osv.osv_memory):
an action dictionary -- executes the action provided by calling
``next``.
"""
- self._set_previous_todo(cr, uid, state='done')
+ self._set_previous_todo(cr, uid, state='done', context=context)
next = self.execute(cr, uid, ids, context=None)
if next: return next
return self.next(cr, uid, ids, context=context)
@@ -192,7 +175,7 @@ class res_config_configurable(osv.osv_memory):
an action dictionary -- executes the action provided by calling
``next``.
"""
- self._set_previous_todo(cr, uid, state='skip')
+ self._set_previous_todo(cr, uid, state='skip', context=context)
next = self.cancel(cr, uid, ids, context=None)
if next: return next
return self.next(cr, uid, ids, context=context)
@@ -208,7 +191,7 @@ class res_config_configurable(osv.osv_memory):
an action dictionary -- executes the action provided by calling
``next``.
"""
- self._set_previous_todo(cr, uid, state='cancel')
+ self._set_previous_todo(cr, uid, state='cancel', context=context)
next = self.cancel(cr, uid, ids, context=None)
if next: return next
return self.next(cr, uid, ids, context=context)
@@ -309,12 +292,13 @@ class res_config_installer(osv.osv_memory):
"""
_name = 'res.config.installer'
_inherit = 'res.config'
+ __logger = logging.getLogger(_name)
_install_if = {}
def _already_installed(self, cr, uid, context=None):
""" For each module (boolean fields in a res.config.installer),
- check if it's already installed (neither uninstallable nor uninstalled)
+ check if it's already installed (either 'to install', 'to upgrade' or 'installed')
and if it is, check it by default
"""
modules = self.pool.get('ir.module.module')
@@ -325,7 +309,7 @@ class res_config_installer(osv.osv_memory):
cr, uid,
modules.search(cr, uid,
[('name','in',selectable),
- ('state','not in',['uninstallable', 'uninstalled'])],
+ ('state','in',['to install', 'installed', 'to upgrade'])],
context=context),
context=context)
@@ -404,9 +388,7 @@ class res_config_installer(osv.osv_memory):
modules = self.pool.get('ir.module.module')
to_install = list(self.modules_to_install(
cr, uid, ids, context=context))
- self.logger.notifyChannel(
- 'installer', netsvc.LOG_INFO,
- 'Selecting addons %s to install'%to_install)
+ self.__logger.info('Selecting addons %s to install', to_install)
modules.state_update(
cr, uid,
modules.search(cr, uid, [('name','in',to_install)]),
@@ -428,6 +410,7 @@ class ir_actions_configuration_wizard(osv.osv_memory):
'''
_name='ir.actions.configuration.wizard'
_inherit = 'res.config'
+ __logger = logging.getLogger(_name)
def _next_action_note(self, cr, uid, ids, context=None):
next = self._next_action(cr, uid)
@@ -447,8 +430,7 @@ class ir_actions_configuration_wizard(osv.osv_memory):
}
def execute(self, cr, uid, ids, context=None):
- self.logger.notifyChannel(
- 'configuration', netsvc.LOG_WARNING, DEPRECATION_MESSAGE)
+ self.__logger.warn(DEPRECATION_MESSAGE)
ir_actions_configuration_wizard()
diff --git a/bin/addons/base/res/res_config.xml b/bin/addons/base/res/res_config.xml
index 9ce360ee4a6..28bfa0d1911 100644
--- a/bin/addons/base/res/res_config.xml
+++ b/bin/addons/base/res/res_config.xml
@@ -6,28 +6,28 @@
form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -60,28 +60,28 @@
form
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/addons/base/res/res_currency.py b/bin/addons/base/res/res_currency.py
index 0d03c067223..1a233df4f19 100644
--- a/bin/addons/base/res/res_currency.py
+++ b/bin/addons/base/res/res_currency.py
@@ -27,26 +27,29 @@ from tools.misc import currency
from tools.translate import _
class res_currency(osv.osv):
- def _current_rate(self, cr, uid, ids, name, arg, context={}):
- res={}
+ def _current_rate(self, cr, uid, ids, name, arg, context=None):
+ if context is None:
+ context = {}
+ res = {}
if 'date' in context:
- date=context['date']
+ date = context['date']
else:
- date=time.strftime('%Y-%m-%d')
- date= date or time.strftime('%Y-%m-%d')
+ date = time.strftime('%Y-%m-%d')
+ date = date or time.strftime('%Y-%m-%d')
for id in ids:
cr.execute("SELECT currency_id, rate FROM res_currency_rate WHERE currency_id = %s AND name <= %s ORDER BY name desc LIMIT 1" ,(id, date))
if cr.rowcount:
- id, rate=cr.fetchall()[0]
- res[id]=rate
+ id, rate = cr.fetchall()[0]
+ res[id] = rate
else:
- res[id]=0
+ res[id] = 0
return res
_name = "res.currency"
_description = "Currency"
_columns = {
'name': fields.char('Currency', size=32, required=True),
'code': fields.char('Code', size=3),
+ 'symbol': fields.char('Symbol', size=3),
'rate': fields.function(_current_rate, method=True, string='Current Rate', digits=(12,6),
help='The rate of the currency to the currency of rate 1'),
'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
@@ -70,9 +73,9 @@ class res_currency(osv.osv):
if r.__contains__('rate_ids'):
rates=r['rate_ids']
if rates:
- currency_rate_obj=self.pool.get('res.currency.rate')
- currency_date=currency_rate_obj.read(cr,user,rates[0],['name'])['name']
- r['date']=currency_date
+ currency_rate_obj= self.pool.get('res.currency.rate')
+ currency_date = currency_rate_obj.read(cr,user,rates[0],['name'])['name']
+ r['date'] = currency_date
return res
def round(self, cr, uid, currency, amount):
@@ -86,7 +89,9 @@ class res_currency(osv.osv):
def is_zero(self, cr, uid, currency, amount):
return abs(self.round(cr, uid, currency, amount)) < currency.rounding
- def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount, round=True, context={}, account=None, account_invert=False):
+ def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount, round=True, context=None, account=None, account_invert=False):
+ if context is None:
+ context = {}
if not from_currency_id:
from_currency_id = to_currency_id
if not to_currency_id:
diff --git a/bin/addons/base/res/res_currency_view.xml b/bin/addons/base/res/res_currency_view.xml
index daa286ad07f..60ddf0d98e8 100644
--- a/bin/addons/base/res/res_currency_view.xml
+++ b/bin/addons/base/res/res_currency_view.xml
@@ -8,15 +8,15 @@
tree
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -26,14 +26,26 @@
form
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/addons/base/res/res_lang.py b/bin/addons/base/res/res_lang.py
index 13c4a33aa9c..1e158a175e6 100644
--- a/bin/addons/base/res/res_lang.py
+++ b/bin/addons/base/res/res_lang.py
@@ -23,7 +23,7 @@ from osv import fields, osv
from locale import localeconv
import tools
from tools.safe_eval import safe_eval as eval
-
+from tools.translate import _
class lang(osv.osv):
_name = "res.lang"
_description = "Languages"
@@ -76,6 +76,23 @@ class lang(osv.osv):
self._lang_data_get.clear_cache(cr.dbname,lang_id= lang_id)
return super(lang, self).write(cr, uid, ids, vals, context)
+ def unlink(self, cr, uid, ids, context=None):
+ if context is None:
+ context = {}
+ languages = self.read(cr, uid, ids, ['code','active'], context=context)
+ for language in languages:
+ lang = context.get('lang')
+ if language['code']=='en_US':
+ raise osv.except_osv(_('User Error'), _("Base Language 'en_US' can not be deleted !"))
+ if lang and (language['code']==lang):
+ raise osv.except_osv(_('User Error'), _("You cannot delete the language which is User's Preferred Language !"))
+ if language['active']:
+ raise osv.except_osv(_('User Error'), _("You cannot delete the language which is Active !\nPlease de-activate the language first."))
+ trans_obj = self.pool.get('ir.translation')
+ trans_ids = trans_obj.search(cr, uid, [('lang','=',language['code'])], context=context)
+ trans_obj.unlink(cr, uid, trans_ids, context=context)
+ return super(lang, self).unlink(cr, uid, ids, context=context)
+
def _group(self, cr, uid, ids, s, monetary=False, grouping=False, thousands_sep=''):
grouping = eval(grouping)
diff --git a/bin/addons/base/res/res_lang_view.xml b/bin/addons/base/res/res_lang_view.xml
index bbb2c6c9974..839915a56a5 100644
--- a/bin/addons/base/res/res_lang_view.xml
+++ b/bin/addons/base/res/res_lang_view.xml
@@ -13,6 +13,8 @@
+
@@ -22,22 +24,23 @@
form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -73,7 +76,7 @@
-
+
@@ -98,13 +101,31 @@
+
+ res.lang.search
+ res.lang
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
Languages
res.lang
form
{'active_test': False}
+
-
+
diff --git a/bin/addons/base/res/res_log.py b/bin/addons/base/res/res_log.py
index b83e380ac55..8ac53acae6b 100644
--- a/bin/addons/base/res/res_log.py
+++ b/bin/addons/base/res/res_log.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -20,25 +20,52 @@
##############################################################################
from osv import fields, osv
+import tools
-class res_log(osv.osv_memory):
+class res_log(osv.osv):
_name = 'res.log'
_columns = {
'name': fields.char('Message', size=128, help='The logging message.', required=True),
'user_id': fields.many2one('res.users','User', required=True),
'res_model': fields.char('Object', size=128),
+ 'context': fields.char('Context', size=250),
'res_id': fields.integer('Object ID'),
- 'secondary': fields.boolean('Secondary Log', help='Do not display this log if it belongs to the same object the user is working on')
+ 'secondary': fields.boolean('Secondary Log', help='Do not display this log if it belongs to the same object the user is working on'),
+ 'create_date': fields.datetime('Created Date', readonly=True),
+ 'read': fields.boolean('Read', help="If this log item has been read, get() should not send it to the client")
}
_defaults = {
- 'user_id': lambda self,cr,uid,ctx: uid
+ 'user_id': lambda self,cr,uid,ctx: uid,
+ 'context': "{}",
+ 'read': False
}
- _order='id desc'
- # TODO: do not return secondary log if same object than in the model (but unlink it)
- def get(self, cr, uid, context={}):
- ids = self.search(cr, uid, [('user_id','=',uid)], context=context)
- result = self.read(cr, uid, ids, ['name','res_model','res_id'], context=context)
- self.unlink(cr, uid, ids, context=context)
- return result
-res_log()
+ _order='create_date desc'
+ _index_name = 'res_log_uid_read'
+ def _auto_init(self, cr, context={}):
+ super(res_log, self)._auto_init(cr, context)
+ cr.execute('SELECT 1 FROM pg_indexes WHERE indexname=%s',
+ (self._index_name,))
+ if not cr.fetchone():
+ cr.execute('CREATE INDEX %s ON res_log (user_id, read)' %
+ self._index_name)
+
+ # TODO: do not return secondary log if same object than in the model (but unlink it)
+ def get(self, cr, uid, context=None):
+ unread_log_ids = self.search(cr, uid,
+ [('user_id','=',uid), ('read', '=', False)], context=context)
+ res = self.read(cr, uid, unread_log_ids,
+ ['name','res_model','res_id','context'],
+ context=context)
+ res.reverse()
+ result = []
+ res_dict = {}
+ for r in res:
+ t = (r['res_model'], r['res_id'])
+ if t not in res_dict:
+ res_dict[t] = True
+ result.insert(0,r)
+ self.write(cr, uid, unread_log_ids, {'read': True}, context=context)
+ return result
+
+res_log()
diff --git a/bin/addons/base/res/res_log_view.xml b/bin/addons/base/res/res_log_view.xml
index 046d93f1788..4fc137eb26b 100644
--- a/bin/addons/base/res/res_log_view.xml
+++ b/bin/addons/base/res/res_log_view.xml
@@ -7,11 +7,15 @@
search
+
+
+
+
@@ -21,8 +25,9 @@
tree
-
+
+
@@ -32,12 +37,14 @@
form
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -51,6 +58,6 @@
+ parent="base.menu_audit"/>
diff --git a/bin/addons/base/res/res_request.py b/bin/addons/base/res/res_request.py
index bdbc6c0bdac..eceb670d17f 100644
--- a/bin/addons/base/res/res_request.py
+++ b/bin/addons/base/res/res_request.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -50,7 +50,7 @@ class res_request(osv.osv):
return True
def request_close(self, cr, uid, ids, *args):
- self.write(cr, uid, ids, {'state':'closed', 'active':False})
+ self.write(cr, uid, ids, {'state':'closed'})
return True
def request_get(self, cr, uid):
@@ -113,7 +113,7 @@ class res_request_history(osv.osv):
'name': lambda *a: 'NoName',
'act_from': lambda obj,cr,uid,context={}: uid,
'act_to': lambda obj,cr,uid,context={}: uid,
- 'date_sent': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'date_sent': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
}
res_request_history()
diff --git a/bin/addons/base/res/res_request_view.xml b/bin/addons/base/res/res_request_view.xml
index 6fd72238306..7cec248b990 100644
--- a/bin/addons/base/res/res_request_view.xml
+++ b/bin/addons/base/res/res_request_view.xml
@@ -1,12 +1,14 @@
+
+
res.request.tree
res.request
tree
-
+
@@ -23,65 +25,104 @@
form
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ Request - Calendar
+ res.request
+ calendar
+
+
+
+
+
+
+
+
+
+ res.request.search
+ res.request
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.request.calendar
+ res.request
+ calendar
+
+
+
+
+
+
+
- My Requests
+ Requests
ir.actions.act_window
res.request
+ tree,form,calendar
+
+ {"search_default_act_to":uid}
-
-
-
-
- My Closed Requests
- ir.actions.act_window
- res.request
- [('active','=',False)]
-
-
-
-
+
+
res.request.link.form
res.request.link
form
-
-
+
+
@@ -97,17 +138,31 @@
-
+
+ res.request.link.search
+ res.request.link
+ search
+
+
+
+
+
+
+
+
- Accepted Links in Requests
+ Request Reference Types
ir.actions.act_window
res.request.link
form
+
-
-
-
+
+
+
+
+
res.request.history.tree
res.request.history
@@ -121,7 +176,7 @@
-
+
res.request.history.form
res.request.history
diff --git a/bin/addons/base/res/res_security.xml b/bin/addons/base/res/res_security.xml
index 99584de3103..c6eee754ac4 100644
--- a/bin/addons/base/res/res_security.xml
+++ b/bin/addons/base/res/res_security.xml
@@ -10,5 +10,25 @@
+
+
+
+
+ company rule
+
+
+
+ []
+
+
+
+
+ user rule
+
+
+ ['|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id])]
+
+
+
diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py
index 59351b80d71..cdd0d8329e1 100644
--- a/bin/addons/base/res/res_user.py
+++ b/bin/addons/base/res/res_user.py
@@ -28,6 +28,7 @@ import pooler
from tools.translate import _
from service import security
import netsvc
+import time
class groups(osv.osv):
_name = "res.groups"
@@ -36,7 +37,7 @@ class groups(osv.osv):
'name': fields.char('Group Name', size=64, required=True),
'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
- 'group_id', 'rule_group_id', 'Rules', domain="[('global', '<>', True)]"),
+ 'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
'comment' : fields.text('Comment',size=250),
}
@@ -46,7 +47,7 @@ class groups(osv.osv):
def copy(self, cr, uid, id, default=None, context={}):
group_name = self.read(cr, uid, [id], ['name'])[0]['name']
- default.update({'name': group_name +' (copy)'})
+ default.update({'name': _('%s (copy)')%group_name})
return super(groups, self).copy(cr, uid, id, default, context)
def write(self, cr, uid, ids, vals, context=None):
@@ -74,14 +75,6 @@ class groups(osv.osv):
aid.write({'groups_id': [(4, 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)
-
def get_extended_interface_group(self, cr, uid, context=None):
data_obj = self.pool.get('ir.model.data')
extended_group_data_id = data_obj._get_id(cr, uid, 'base', 'group_extended')
@@ -89,26 +82,6 @@ class groups(osv.osv):
groups()
-class roles(osv.osv):
- _name = "res.roles"
- _columns = {
- 'name': fields.char('Role Name', size=64, required=True),
- 'parent_id': fields.many2one('res.roles', 'Parent', select=True),
- 'child_id': fields.one2many('res.roles', 'parent_id', 'Children'),
- 'users': fields.many2many('res.users', 'res_roles_users_rel', 'rid', 'uid', 'Users'),
- }
- _defaults = {
- }
- def check(self, cr, uid, ids, role_id):
- if role_id in ids:
- return True
- cr.execute('select parent_id from res_roles where id=%s', (role_id,))
- roles = cr.fetchone()[0]
- if roles:
- return self.check(cr, uid, ids, roles)
- return False
-roles()
-
def _lang_get(self, cr, uid, context={}):
obj = self.pool.get('res.lang')
ids = obj.search(cr, uid, [('translatable','=',True)])
@@ -146,10 +119,8 @@ class users(osv.osv):
return self.WELCOME_MAIL_BODY
def get_current_company(self, cr, uid):
- res=[]
cr.execute('select company_id, res_company.name from res_users left join res_company on res_company.id = company_id where res_users.id=%s' %uid)
- res = cr.fetchall()
- return res
+ return cr.fetchall()
def send_welcome_email(self, cr, uid, id, context=None):
logger= netsvc.Logger()
@@ -203,13 +174,17 @@ class users(osv.osv):
return dict(zip(ids, ['extended' if user in extended_users else 'simple' for user in ids]))
def _email_get(self, cr, uid, ids, name, arg, context=None):
- return dict([(user.id, user.address_id.email) for user in self.browse(cr, uid, ids, context=context)])
+ # perform this as superuser because the current user is allowed to read users, and that includes
+ # the email, even without any direct read access on the res_partner_address object.
+ return dict([(user.id, user.address_id.email) for user in self.browse(cr, 1, ids)]) # no context to avoid potential security issues as superuser
def _email_set(self, cr, uid, ids, name, value, arg, context=None):
if not isinstance(ids,list):
ids = [ids]
address_obj = self.pool.get('res.partner.address')
for user in self.browse(cr, uid, ids, context=context):
+ # perform this as superuser because the current user is allowed to write to the user, and that includes
+ # the email even without any direct write access on the res_partner_address object.
if user.address_id:
address_obj.write(cr, 1, user.address_id.id, {'email': value or None}) # no context to avoid potential security issues as superuser
else:
@@ -218,7 +193,7 @@ class users(osv.osv):
return True
_columns = {
- 'name': fields.char('Name', size=64, required=True, select=True,
+ 'name': fields.char('User Name', size=64, required=True, select=True,
help="The new user's real name, used for searching"
" and most listings"),
'login': fields.char('Login', size=64, required=True,
@@ -232,10 +207,9 @@ class users(osv.osv):
'signature': fields.text('Signature', size=64),
'address_id': fields.many2one('res.partner.address', 'Address'),
'active': fields.boolean('Active'),
- 'action_id': fields.many2one('ir.actions.actions', 'Home Action'),
- 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action'),
+ 'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at logon for this user, in addition to the standard menu."),
+ 'menu_id': fields.many2one('ir.actions.actions', 'Menu Action', help="If specified, the action will replace the standard menu for this user."),
'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
- 'roles_id': fields.many2many('res.roles', 'res_roles_users_rel', 'uid', 'rid', 'Roles'),
# Special behavior for this field: res.company.search() will only return the companies
# available to the current user (should be the user's companies?), when the user_preference
@@ -254,8 +228,18 @@ class users(osv.osv):
selection=[('simple','Simplified'),('extended','Extended')],
string='Interface', help="Choose between the simplified interface and the extended one"),
'user_email': fields.function(_email_get, method=True, fnct_inv=_email_set, string='Email', type="char", size=240),
+ 'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
+ 'date': fields.datetime('Last Connection', readonly=True),
}
+ def on_change_company_id(self, cr, uid, ids, company_id):
+ return {
+ 'warning' : {
+ 'title': _("Company Switch Warning"),
+ 'message': _("Please keep in mind that documents currently displayed may not be relevant after switching to another company. If you have unsaved changes, please make sure to save and close all forms before switching to a different company. (You can click on Cancel in the User Preferences now)"),
+ }
+ }
+
def read(self,cr, uid, ids, fields=None, context=None, load='_classic_read'):
def override_password(o):
if 'password' in o and ( 'id' not in o or o['id'] != uid ):
@@ -269,15 +253,6 @@ class users(osv.osv):
result = override_password(result)
else:
result = map(override_password, result)
-
- if isinstance(result, list):
- for rec in result:
- if not rec.get('action_id',True):
- rec['action_id'] = (self._get_menu(cr, uid),'Menu')
- else:
- if not result.get('action_id',True):
- result['action_id'] = (self._get_menu(cr, uid),'Menu')
-
return result
@@ -308,10 +283,6 @@ class users(osv.osv):
self.__admin_ids[cr.dbname] = ir_model_data_obj.read(cr, 1, [mdid], ['res_id'])[0]['res_id']
return self.__admin_ids[cr.dbname]
- def _get_action(self,cr, uid, context={}):
- ids = self.pool.get('ir.ui.menu').search(cr, uid, [('usage','=','menu')])
- return ids and ids[0] or False
-
def _get_company(self,cr, uid, context=None, uid2=False):
if not uid2:
uid2 = uid
@@ -325,24 +296,33 @@ class users(osv.osv):
return [c]
return False
- def _get_menu(self,cr, uid, context={}):
- ids = self.pool.get('ir.actions.act_window').search(cr, uid, [('usage','=','menu')])
+ def _get_menu(self,cr, uid, context=None):
+ ids = self.pool.get('ir.actions.act_window').search(cr, uid, [('usage','=','menu')], context=context)
return ids and ids[0] or False
- def _get_group(self,cr, uid, context={}):
- ids = self.pool.get('res.groups').search(cr, uid, [('name','=','Employee')])
- return ids or False
+ def _get_group(self,cr, uid, context=None):
+ dataobj = self.pool.get('ir.model.data')
+ result = []
+ try:
+ dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_user')
+ result.append(group_id)
+ dummy,group_id = dataobj.get_object_reference(cr, 1, 'base', 'group_partner_manager')
+ result.append(group_id)
+ except ValueError:
+ # If these groups does not exists anymore
+ pass
+ return result
_defaults = {
'password' : lambda *a : '',
'context_lang': lambda *args: 'en_US',
'active' : lambda *a: True,
'menu_id': _get_menu,
- 'action_id': _get_menu,
'company_id': _get_company,
'company_ids': _get_companies,
'groups_id': _get_group,
'address_id': False,
+ 'menu_tips':True
}
@tools.cache()
@@ -350,7 +330,7 @@ class users(osv.osv):
return self._get_company(cr, uid, context=context, uid2=uid2)
# User can write to a few of her own fields (but not her groups for example)
- SELF_WRITEABLE_FIELDS = ['view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
+ SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
def write(self, cr, uid, ids, values, context=None):
if not hasattr(ids, '__iter__'):
@@ -360,9 +340,10 @@ class users(osv.osv):
if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
break
else:
- # check that user is not selecting an invalid company_id
- if 'company_id' not in values or (values.get('company_id') in self.read(cr, uid, uid, ['company_ids'], context=context)['company_ids']):
- uid = 1 # safe fields only, so we write as super-user to bypass access rights
+ if 'company_id' in values:
+ if not (values['company_id'] in self.read(cr, uid, uid, ['company_ids'], context=context)['company_ids']):
+ del values['company_id']
+ uid = 1 # safe fields only, so we write as super-user to bypass access rights
res = super(users, self).write(cr, uid, ids, values, context=context)
@@ -392,8 +373,14 @@ class users(osv.osv):
return self.name_get(cr, user, ids)
def copy(self, cr, uid, id, default=None, context={}):
- login = self.read(cr, uid, [id], ['login'])[0]['login']
- default.update({'login': login+' (copy)'})
+ user2copy = self.read(cr, uid, [id], ['login','name'])[0]
+ if default is None:
+ default = {}
+ copy_pattern = _("%s (copy)")
+ default.update(login=(copy_pattern % user2copy['login']),
+ name=(copy_pattern % user2copy['name']),
+ address_id=False, # avoid sharing the address of the copied user!
+ )
return super(users, self).copy(cr, uid, id, default, context)
def context_get(self, cr, uid, context=None):
@@ -419,12 +406,13 @@ class users(osv.osv):
cr = pooler.get_db(db).cursor()
cr.execute('select id from res_users where login=%s and password=%s and active', (tools.ustr(login), tools.ustr(password)))
res = cr.fetchone()
- cr.close()
+ result = False
if res:
- return res[0]
- else:
- return False
-
+ cr.execute("update res_users set date=%s where id=%s", (time.strftime('%Y-%m-%d %H:%M:%S'),res[0]))
+ cr.commit()
+ result = res[0]
+ cr.close()
+ return result
def check_super(self, passwd):
if passwd == tools.config['admin_passwd']:
return True
@@ -540,7 +528,7 @@ class res_config_view(osv.osv_memory):
'Interface', required=True ),
}
_defaults={
- 'view':lambda *args: 'simple',
+ 'view':lambda self,cr,uid,*args: self.pool.get('res.users').browse(cr, uid, uid).view or 'simple',
}
def execute(self, cr, uid, ids, context=None):
diff --git a/bin/addons/base/res/res_widget.py b/bin/addons/base/res/res_widget.py
new file mode 100644
index 00000000000..14b7338bde0
--- /dev/null
+++ b/bin/addons/base/res/res_widget.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from osv import fields,osv
+class res_widget(osv.osv):
+ _name = "res.widget"
+ _rec_name = "title"
+ _columns = {
+ 'title' : fields.char('Title', size=64, required=True, translate=True),
+ 'content': fields.text('Content', required=True),
+ }
+res_widget()
+
+class res_widget_user(osv.osv):
+ _name="res.widget.user"
+ _order = "sequence"
+ _columns = {
+ 'sequence': fields.integer('Sequence'),
+ 'user_id': fields.many2one('res.users','User', select=1),
+ 'widget_id': fields.many2one('res.widget','Widget',required=1),
+ }
+res_widget_user()
+
+class res_widget_wizard(osv.osv_memory):
+ _name = "res.widget.wizard"
+ _description = "Add a widget"
+ _columns = {
+ 'widget_id': fields.one2many("res.widget", 'Widget', required=True),
+ }
+ def widget_add(self, cr, uid, ids, context=None):
+ if context is None:
+ context = {}
+ wizard = self.read(cr, uid, ids)[0]
+ self.pool.get('res.widget.user').create(cr, uid, {'user_id':uid, 'widget_id':wizard['widget_id']})
+ return {}
+res_widget_wizard()
+
diff --git a/bin/addons/base/res/res_widget_data.xml b/bin/addons/base/res/res_widget_data.xml
new file mode 100644
index 00000000000..a8375651b8a
--- /dev/null
+++ b/bin/addons/base/res/res_widget_data.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
diff --git a/bin/addons/base/res/res_widget_view.xml b/bin/addons/base/res/res_widget_view.xml
new file mode 100644
index 00000000000..b139f774f9b
--- /dev/null
+++ b/bin/addons/base/res/res_widget_view.xml
@@ -0,0 +1,94 @@
+
+
+
+
+ res.widget.search
+ res.widget
+ search
+
+
+
+
+
+
+
+ res.widget.form
+ res.widget
+ form
+
+
+
+
+
+
+
+
+ res.widget.tree
+ res.widget
+ tree
+
+
+
+
+
+
+
+
+ Homepage Widgets
+ res.widget
+ form
+ tree,form
+
+
+
+
+
+ res.widget.user.tree
+ res.widget.user
+ tree
+
+
+
+
+
+
+
+
+
+ res.widget.user.form
+ res.widget.user
+ form
+
+
+
+
+
+
+
+
+
+ Widgets per User
+ res.widget.user
+ form
+ tree,form
+
+
+
+
+
+
diff --git a/bin/addons/base/rng/board.rng b/bin/addons/base/rng/board.rng
deleted file mode 100644
index 73477113f62..00000000000
--- a/bin/addons/base/rng/board.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/calendar.rng b/bin/addons/base/rng/calendar.rng
deleted file mode 100644
index 73477113f62..00000000000
--- a/bin/addons/base/rng/calendar.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/form.rng b/bin/addons/base/rng/form.rng
deleted file mode 100644
index b8a93c67549..00000000000
--- a/bin/addons/base/rng/form.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/graph.rng b/bin/addons/base/rng/graph.rng
deleted file mode 100644
index 73477113f62..00000000000
--- a/bin/addons/base/rng/graph.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/inherit.rng b/bin/addons/base/rng/inherit.rng
deleted file mode 100644
index 73477113f62..00000000000
--- a/bin/addons/base/rng/inherit.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/tree.rng b/bin/addons/base/rng/tree.rng
deleted file mode 100644
index 73477113f62..00000000000
--- a/bin/addons/base/rng/tree.rng
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/rng/view.rng b/bin/addons/base/rng/view.rng
index ca99be54449..9c591723a9f 100644
--- a/bin/addons/base/rng/view.rng
+++ b/bin/addons/base/rng/view.rng
@@ -1,33 +1,81 @@
-
+
+
+
+
+
+
+
+
+
+ before
+
+ after
+
+ inside
+
+ replace
+
+
+
+
+
+ attributes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -62,7 +110,7 @@
-
+
@@ -74,12 +122,12 @@
+
-
@@ -88,12 +136,10 @@
-
-
@@ -104,20 +150,18 @@
+
-
-
-
@@ -133,24 +177,25 @@
-
-
-
+
+
+
+
-
+
@@ -177,17 +222,30 @@
-
-
-
-
+
+
+
+
+
+
+ day
+ 3days
+ week
+ 3weeks
+ month
+ 3months
+ year
+ 3years
+ 5years
+
+
@@ -204,7 +262,11 @@
-
+
+
+
+
+
@@ -214,7 +276,11 @@
-
+
+
+
+
+
@@ -239,18 +305,17 @@
+
+
-
+
-
-
-
@@ -268,7 +333,7 @@
-
+
@@ -279,20 +344,19 @@
+
+
-
-
-
@@ -307,7 +371,7 @@
-
+
@@ -340,20 +404,13 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -385,9 +442,7 @@
-
-
@@ -401,10 +456,10 @@
+
-
@@ -426,39 +481,20 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -476,7 +512,7 @@
-
+
@@ -491,13 +527,15 @@
-
+
+
+
@@ -508,17 +546,14 @@
-
-
-
@@ -543,6 +578,8 @@
+
+
@@ -551,9 +588,7 @@
-
-
@@ -581,15 +616,6 @@
-
-
-
-
-
-
-
-
-
diff --git a/bin/addons/base/security/base_security.xml b/bin/addons/base/security/base_security.xml
index 0af5f181e31..ed36267031e 100644
--- a/bin/addons/base/security/base_security.xml
+++ b/bin/addons/base/security/base_security.xml
@@ -14,7 +14,7 @@
- Employee
+ Internal User
@@ -29,23 +29,29 @@
Useability / No One
+
+ Sales / Manager
+
+
+ Sales / User
+
-
- res.partner.address company
-
+
+ res.widget.user rule
+
- [('company_id','child_of',[user.company_id.id])]
+ ['|', ('user_id','=',user.id),('user_id','=',False)]
res.partner company
- [('company_id','child_of',[user.company_id.id])]
+ ['|', ('company_id','child_of',[user.company_id.id]),('company_id','=',False)]
diff --git a/bin/addons/base/security/ir.model.access.csv b/bin/addons/base/security/ir.model.access.csv
index 4ef6770e52c..62eb8ffe2b1 100644
--- a/bin/addons/base/security/ir.model.access.csv
+++ b/bin/addons/base/security/ir.model.access.csv
@@ -1,4 +1,6 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_res_widget_user_all","res_widget_user all","model_res_widget_user",,1,0,0,0
+"access_res_widget_user_group_user","res_widget_user group_user","model_res_widget_user","group_user",1,1,1,1
"access_ir_attachment_all","ir_attachment all","model_ir_attachment",,1,0,0,0
"access_ir_attachment_group_user","ir_attachment group_user","model_ir_attachment","group_user",1,1,1,1
"access_ir_cron_group_cron","ir_cron group_cron","model_ir_cron","group_system",1,1,1,1
@@ -8,26 +10,25 @@
"access_ir_exports_line_group_system","ir_exports_line group_system","model_ir_exports_line","base.group_user",1,1,1,1
"access_ir_model_group_erp_manager","ir_model group_erp_manager","model_ir_model","group_erp_manager",1,1,1,1
"access_ir_model_access_group_erp_manager","ir_model_access_group_erp_manager","model_ir_model_access","group_erp_manager",1,1,1,1
-"access_ir_model_grid_group_erp_manager","ir_model_access_grid_group_erp_manager","model_ir_model_grid","group_erp_manager",1,1,1,1
"access_ir_model_data_group_erp_manager","ir_model_data group_erp_manager","model_ir_model_data","group_erp_manager",1,1,1,1
"access_ir_model_fields_group_erp_manager","ir_model_fields group_erp_manager","model_ir_model_fields","group_erp_manager",1,1,1,1
"access_ir_model_all","ir_model_all","model_ir_model",,1,0,0,0
"access_ir_model_access_all","ir_model_access_all","model_ir_model_access",,1,0,0,0
-"access_ir_model_grid_all","ir_model_access_grid_all","model_ir_model_grid",,0,0,0,0
"access_ir_model_data_all","ir_model_data all","model_ir_model_data",,1,0,0,0
"access_ir_model_fields_all","ir_model_fields all","model_ir_model_fields",,1,0,0,0
"access_ir_module_category_group_user","ir_module_category group_user","model_ir_module_category","group_system",1,0,0,0
"access_ir_module_module_group_user","ir_module_module group_user","model_ir_module_module","group_system",1,1,1,1
+"access_ir_module_module_user","ir_module_module user","model_ir_module_module",,1,0,0,0
"access_ir_module_module_dependency_group_system","ir_module_module_dependency group_system","model_ir_module_module_dependency","group_system",1,1,1,1
"access_ir_property_group_user","ir_property group_user","model_ir_property",,1,0,0,0
"access_ir_property_group_user_manager","ir_property group_manager","model_ir_property","base.group_partner_manager",1,1,1,0
-"access_ir_report_custom_group_system","ir_report_custom group_system","model_ir_report_custom",,1,0,0,0
-"access_ir_report_custom_fields_group_system","ir_report_custom_fields group_system","model_ir_report_custom_fields",,1,0,0,0
"access_ir_rule_group_user","ir_rule group_user","model_ir_rule",,1,0,0,0
"access_ir_rule_group_erp_manager","ir_rule group_erp_manager","model_ir_rule","group_erp_manager",1,1,1,1
"access_ir_sequence_group_user","ir_sequence group_user","model_ir_sequence",,1,0,0,0
+"access_ir_sequence_group_system","ir_sequence group_system","model_ir_sequence","group_system",1,1,1,1
"access_ir_sequence_type_group_user","ir_sequence_type group_user","model_ir_sequence_type",,1,0,0,0
-"access_ir_translation_all","ir_translation all","model_ir_translation",,1,0,0,0
+"access_ir_sequence_type_group_system","ir_sequence_type group_system","model_ir_sequence_type","group_system",1,1,1,1
+"access_ir_translation_all","ir_translation all","model_ir_translation",,1,1,1,1
"access_ir_translation_group_system","ir_translation group_system","model_ir_translation","group_system",1,1,1,1
"access_ir_ui_menu_group_user","ir_ui_menu group_user","model_ir_ui_menu",,1,0,0,0
"access_ir_ui_menu_group_system","ir_ui_menu group_system","model_ir_ui_menu","group_system",1,1,1,1
@@ -68,8 +69,6 @@
"access_res_partner_category_group_partner_manager","res_partner_category group_partner_manager","model_res_partner_category","group_partner_manager",1,1,1,1
"access_res_partner_event_group_user","res_partner_event group_user","model_res_partner_event","group_user",1,0,0,0
"access_res_partner_event_group_partner_manager","res_partner_event group_partner_manager","model_res_partner_event","group_partner_manager",1,1,1,1
-"access_res_partner_som_group_user","res_partner_som group_user","model_res_partner_som","group_partner_manager",1,1,1,1
-"access_res_partner_som_group_partner_manager","res_partner_som group_partner_manager","model_res_partner_som","group_user",1,0,0,0
"access_res_partner_title_group_user","res_partner_title group_user","model_res_partner_title","group_partner_manager",1,1,1,1
"access_res_partner_title_group_partner_manager","res_partner_title group_partner_manager","model_res_partner_title",,1,0,0,0
"access_res_request_group_user","res_request group_user","model_res_request","base.group_user",1,1,1,1
@@ -82,7 +81,7 @@
"access_res_users_group_erp_manager","res_users group_erp_manager","model_res_users","group_erp_manager",1,1,1,1
"access_ir_actions_all","ir_actions_all","model_ir_actions_actions",,1,0,0,0
"access_ir_actions_group_system","ir_actions_group_system","model_ir_actions_actions","group_system",1,1,1,1
-"access_ir_actions_act_window_all","ir_actions_act_window_all","model_ir_actions_act_window",,1,0,0,0
+"access_ir_actions_act_window_all","ir_actions_act_window_all","model_ir_actions_act_window",,1,1,1,0
"access_ir_actions_act_window_system","ir_actions_act_window_system","model_ir_actions_act_window","group_system",1,1,1,1
"access_ir_actions_act_window_close_all","ir_actions_act_window_close_all","model_ir_actions_act_window_close",,1,0,0,0
"access_ir_actions_act_window_close_group_system","ir_actions_act_window_close_group_system","model_ir_actions_act_window_close","group_system",1,1,1,1
@@ -101,8 +100,6 @@
"access_workflow_triggers_all","workflow_triggers_all","model_workflow_triggers",,1,0,0,0
"access_workflow_triggers_group_system","workflow_triggers_group_system","model_workflow_triggers","group_system",1,1,1,1
"access_workflow_workitem_all","workflow_workitem_all","model_workflow_workitem",,1,1,1,1
-"access_actions_report_custom_all","ir_actions_report_custom_all","model_ir_actions_report_custom",,1,0,0,0
-"access_actions_report_custom_group_system","ir_actions_report_custom_group_system","model_ir_actions_report_custom","group_system",1,1,1,1
"access_ir_actions_act_window_view_all","ir_actions_act_window_view_all","model_ir_actions_act_window_view",,1,0,0,0
"access_ir_actions_act_window_view_group_system","ir_actions_act_window_view_group_system","model_ir_actions_act_window_view","group_system",1,1,1,1
"access_ir_actions_url_all","ir_actions_url_all","model_ir_actions_url",,1,0,0,0
@@ -113,8 +110,6 @@
"access_ir_actions_server_group_system","ir_actions_server_group_system","model_ir_actions_server","group_system",1,1,1,1
"access_res_bank_group_system","res_bank_group_system","model_res_bank","group_system",1,1,1,1
"access_res_payterm_group_system","res_payterm_group_system","model_res_payterm","group_system",1,1,1,1
-"access_res_roles_group_erpmanager","res_roles_group_erp_manager","model_res_roles","group_erp_manager",1,1,1,1
-"access_res_roles_all","res_roles_all","model_res_roles",,1,0,0,0
"access_res_bank_group_partner_manager","res_bank_group_partner_manager","model_res_bank","group_partner_manager",1,1,1,1
"access_res_bank_user","res_bank user","model_res_bank","group_user",1,0,0,0
"access_maintenance_group_user","maintenance_contract group_user","model_maintenance_contract","group_system",1,1,1,1
@@ -124,3 +119,7 @@
"access_ir_filter all","ir_filters all","model_ir_filters",,1,0,0,0
"access_ir_filter employee","ir_filters employee","model_ir_filters","group_user",1,1,1,1
"access_ir_filters","ir_filters_all","model_ir_filters",,1,1,1,1
+"access_res_partner_address","res.partner.address","model_res_partner_address","group_system",1,1,1,1
+"access_res_widget","res.widget","model_res_widget","group_erp_manager",1,1,1,1
+"access_res_widget_user","res.widget.user","model_res_widget",,1,0,0,0
+"access_res_log_all","res.log","model_res_log",,1,1,1,1
diff --git a/bin/addons/base/test/bug_lp541545.xml b/bin/addons/base/test/bug_lp541545.xml
index 4c1df7a0383..79c3c05c729 100644
--- a/bin/addons/base/test/bug_lp541545.xml
+++ b/bin/addons/base/test/bug_lp541545.xml
@@ -5,7 +5,7 @@
diff --git a/bin/addons/base/test/test_context.xml b/bin/addons/base/test/test_context.xml
index f9a6f2055a5..44751644403 100644
--- a/bin/addons/base/test/test_context.xml
+++ b/bin/addons/base/test/test_context.xml
@@ -1,20 +1,20 @@
-
-
+
+
+
+
+
+
+
+
+
-
- 0.631
-
-
- 0.632
+ model="res.currency"
+ id="INR"
+ string="Indian rupee rate in 2002"
+ context="{'date': '2010-06-01'}">
+ 59.9739
diff --git a/bin/addons/base_quality_interrogation.py b/bin/addons/base_quality_interrogation.py
index 4e9c3321dfa..fe5bc27811b 100755
--- a/bin/addons/base_quality_interrogation.py
+++ b/bin/addons/base_quality_interrogation.py
@@ -88,7 +88,7 @@ def import_translate(uri, user, pwd, dbname, translate_in):
uid = login(uri, dbname, user, pwd)
if uid:
conn = xmlrpclib.ServerProxy(uri + '/xmlrpc/wizard')
- wiz_id = execute(conn,'create',dbname, uid, pwd, 'module.lang.import')
+ wiz_id = execute(conn,'create',dbname, uid, pwd, 'base.language.import')
for trans_in in translate_in:
lang,ext = os.path.splitext(trans_in.split('/')[-1])
state = 'init'
diff --git a/bin/import_xml.rng b/bin/import_xml.rng
index 2ddc2336cc0..7b5a889a110 100644
--- a/bin/import_xml.rng
+++ b/bin/import_xml.rng
@@ -104,7 +104,8 @@
-
+
+
diff --git a/bin/netsvc.py b/bin/netsvc.py
index e859eb01ad4..7cb7e73c9df 100644
--- a/bin/netsvc.py
+++ b/bin/netsvc.py
@@ -35,6 +35,7 @@ import time
import release
from pprint import pformat
import warnings
+import heapq
class Service(object):
""" Base class for *Local* services
@@ -103,10 +104,12 @@ class ExportService(object):
_services = {}
_groups = {}
+ _logger = logging.getLogger('web-services')
def __init__(self, name, audience=''):
ExportService._services[name] = self
self.__name = name
+ self._logger.debug("Registered an exported service: %s" % name)
def joinGroup(self, name):
ExportService._groups.setdefault(name, {})[self.__name] = self
@@ -128,7 +131,9 @@ class ExportService(object):
raise
LOG_NOTSET = 'notset'
+LOG_DEBUG_SQL = 'debug_sql'
LOG_DEBUG_RPC = 'debug_rpc'
+LOG_DEBUG_RPC_ANSWER = 'debug_rpc_answer'
LOG_DEBUG = 'debug'
LOG_TEST = 'test'
LOG_INFO = 'info'
@@ -136,8 +141,12 @@ LOG_WARNING = 'warn'
LOG_ERROR = 'error'
LOG_CRITICAL = 'critical'
+logging.DEBUG_RPC_ANSWER = logging.DEBUG - 4
+logging.addLevelName(logging.DEBUG_RPC_ANSWER, 'DEBUG_RPC_ANSWER')
logging.DEBUG_RPC = logging.DEBUG - 2
logging.addLevelName(logging.DEBUG_RPC, 'DEBUG_RPC')
+logging.DEBUG_SQL = logging.DEBUG_RPC - 3
+logging.addLevelName(logging.DEBUG_SQL, 'DEBUG_SQL')
logging.TEST = logging.INFO - 5
logging.addLevelName(logging.TEST, 'TEST')
@@ -150,7 +159,9 @@ COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
COLOR_PATTERN = "%s%s%%s%s" % (COLOR_SEQ, COLOR_SEQ, RESET_SEQ)
LEVEL_COLOR_MAPPING = {
+ logging.DEBUG_SQL: (WHITE, MAGENTA),
logging.DEBUG_RPC: (BLUE, WHITE),
+ logging.DEBUG_RPC_ANSWER: (BLUE, WHITE),
logging.DEBUG: (BLUE, DEFAULT),
logging.INFO: (GREEN, DEFAULT),
logging.TEST: (WHITE, BLUE),
@@ -159,21 +170,24 @@ LEVEL_COLOR_MAPPING = {
logging.CRITICAL: (WHITE, RED),
}
-class ColoredFormatter(logging.Formatter):
+class DBFormatter(logging.Formatter):
+ def format(self, record):
+ record.dbname = getattr(threading.currentThread(), 'dbname', '?')
+ return logging.Formatter.format(self, record)
+
+class ColoredFormatter(DBFormatter):
def format(self, record):
fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno]
record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
- return logging.Formatter.format(self, record)
-
+ return DBFormatter.format(self, record)
def init_logger():
import os
from tools.translate import resetlocale
resetlocale()
- logger = logging.getLogger()
# create a format for log messages and dates
- format = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s'
+ format = '[%(asctime)s][%(dbname)s] %(levelname)s:%(name)s:%(message)s'
if tools.config['syslog']:
# SysLog Handler
@@ -182,7 +196,7 @@ def init_logger():
else:
handler = logging.handlers.SysLogHandler('/dev/log')
format = '%s %s' % (release.description, release.version) \
- + ':%(levelname)s:%(name)s:%(message)s'
+ + ':%(dbname)s:%(levelname)s:%(name)s:%(message)s'
elif tools.config['logfile']:
# LogFile Handler
@@ -197,7 +211,7 @@ def init_logger():
handler = logging.handlers.WatchedFileHandler(logf)
else:
handler = logging.handlers.FileHandler(logf)
- except Exception, ex:
+ except Exception:
sys.stderr.write("ERROR: couldn't create the logfile directory. Logging to the standard output.\n")
handler = logging.StreamHandler(sys.stdout)
else:
@@ -207,15 +221,15 @@ def init_logger():
if isinstance(handler, logging.StreamHandler) and os.isatty(handler.stream.fileno()):
formatter = ColoredFormatter(format)
else:
- formatter = logging.Formatter(format)
+ formatter = DBFormatter(format)
handler.setFormatter(formatter)
# add the handler to the root logger
+ logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(int(tools.config['log_level'] or '0'))
-
class Logger(object):
def __init__(self):
warnings.warn("The netsvc.Logger API shouldn't be used anymore, please "
@@ -255,16 +269,19 @@ class Logger(object):
level_method('[%02d]: %s' % (idx+1, s,))
elif result:
level_method(result[0])
- except IOError,e:
+ except IOError:
# TODO: perhaps reset the logger streams?
#if logrotate closes our files, we end up here..
pass
- except:
+ except Exception:
# better ignore the exception and carry on..
pass
- def set_loglevel(self, level):
- log = logging.getLogger()
+ def set_loglevel(self, level, logger=None):
+ if logger is not None:
+ log = logging.getLogger(str(logger))
+ else:
+ 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)
@@ -276,36 +293,76 @@ import tools
init_logger()
class Agent(object):
- _timers = {}
+ """Singleton that keeps track of cancellable tasks to run at a given
+ timestamp.
+ The tasks are caracterised by:
+ * a timestamp
+ * the database on which the task run
+ * the function to call
+ * the arguments and keyword arguments to pass to the function
+
+ Implementation details:
+ Tasks are stored as list, allowing the cancellation by setting
+ the timestamp to 0.
+ A heapq is used to store tasks, so we don't need to sort
+ tasks ourself.
+ """
+ __tasks = []
+ __tasks_by_db = {}
_logger = Logger()
- __logger = logging.getLogger('timer')
-
- def setAlarm(self, fn, dt, db_name, *args, **kwargs):
- wait = dt - time.time()
- if wait > 0:
- self.__logger.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.start()
- self._timers.setdefault(db_name, []).append(timer)
-
- for db in self._timers:
- for timer in self._timers[db]:
- if not timer.isAlive():
- self._timers[db].remove(timer)
+ @classmethod
+ def setAlarm(cls, function, timestamp, db_name, *args, **kwargs):
+ task = [timestamp, db_name, function, args, kwargs]
+ heapq.heappush(cls.__tasks, task)
+ cls.__tasks_by_db.setdefault(db_name, []).append(task)
@classmethod
def cancel(cls, db_name):
- """Cancel all timers for a given database. If None passed, all timers are cancelled"""
- for db in cls._timers:
- if db_name is None or db == db_name:
- for timer in cls._timers[db]:
- timer.cancel()
+ """Cancel all tasks for a given database. If None is passed, all tasks are cancelled"""
+ if db_name is None:
+ cls.__tasks, cls.__tasks_by_db = [], {}
+ else:
+ if db_name in cls.__tasks_by_db:
+ for task in cls.__tasks_by_db[db_name]:
+ task[0] = 0
@classmethod
def quit(cls):
cls.cancel(None)
+ @classmethod
+ def runner(cls):
+ """Neverending function (intended to be ran in a dedicated thread) that
+ checks every 60 seconds tasks to run.
+ """
+ current_thread = threading.currentThread()
+ while True:
+ while cls.__tasks and cls.__tasks[0][0] < time.time():
+ task = heapq.heappop(cls.__tasks)
+ timestamp, dbname, function, args, kwargs = task
+ cls.__tasks_by_db[dbname].remove(task)
+ if not timestamp:
+ # null timestamp -> cancelled task
+ continue
+ current_thread.dbname = dbname # hack hack
+ cls._logger.notifyChannel('timers', LOG_DEBUG, "Run %s.%s(*%r, **%r)" % (function.im_class.__name__, function.func_name, args, kwargs))
+ delattr(current_thread, 'dbname')
+ task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs)
+ # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
+ task_thread.setDaemon(False)
+ task_thread.start()
+ time.sleep(1)
+ time.sleep(60)
+
+agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner")
+# the agent runner is a typical daemon thread, that will never quit and must be
+# terminated when the main process exits - with no consequence (the processing
+# threads it spawns are not marked daemon)
+agent_runner.setDaemon(True)
+agent_runner.start()
+
+
import traceback
class Server:
@@ -317,6 +374,7 @@ class Server:
"""
__is_started = False
__servers = []
+ __starter_threads = []
# we don't want blocking server calls (think select()) to
# wait forever and possibly prevent exiting the process,
@@ -329,12 +387,26 @@ class Server:
__logger = logging.getLogger('server')
def __init__(self):
- if Server.__is_started:
- raise Exception('All instances of servers must be inited before the startAll()')
Server.__servers.append(self)
+ if Server.__is_started:
+ # raise Exception('All instances of servers must be inited before the startAll()')
+ # Since the startAll() won't be called again, allow this server to
+ # init and then start it after 1sec (hopefully). Register that
+ # timer thread in a list, so that we can abort the start if quitAll
+ # is called in the meantime
+ t = threading.Timer(1.0, self._late_start)
+ t.name = 'Late start timer for %s' % str(self.__class__)
+ Server.__starter_threads.append(t)
+ t.start()
def start(self):
self.__logger.debug("called stub Server.start")
+
+ def _late_start(self):
+ self.start()
+ for thr in Server.__starter_threads:
+ if thr.finished.is_set():
+ Server.__starter_threads.remove(thr)
def stop(self):
self.__logger.debug("called stub Server.stop")
@@ -357,6 +429,11 @@ class Server:
if not cls.__is_started:
return
cls.__logger.info("Stopping %d services" % len(cls.__servers))
+ for thr in cls.__starter_threads:
+ if not thr.finished.is_set():
+ thr.cancel()
+ cls.__starter_threads.remove(thr)
+
for srv in cls.__servers:
srv.stop()
cls.__is_started = False
@@ -387,22 +464,30 @@ class OpenERPDispatcherException(Exception):
self.exception = exception
self.traceback = traceback
+def replace_request_password(args):
+ # password is always 3rd argument in a request, we replace it in RPC logs
+ # so it's easier to forward logs for diagnostics/debugging purposes...
+ args = list(args)
+ if len(args) > 2:
+ args[2] = '*'
+ return args
+
class OpenERPDispatcher:
- def log(self, title, msg):
- if tools.config['log_level'] == logging.DEBUG_RPC:
- Logger().notifyChannel('%s' % title, LOG_DEBUG_RPC, pformat(msg))
+ def log(self, title, msg, channel=logging.DEBUG_RPC, depth=2):
+ logger = logging.getLogger(title)
+ if logger.isEnabledFor(channel):
+ for line in pformat(msg, depth=depth).split('\n'):
+ logger.log(channel, line)
def dispatch(self, service_name, method, params):
try:
+ logger = logging.getLogger('result')
self.log('service', service_name)
self.log('method', method)
- self.log('params', params)
+ self.log('params', replace_request_password(params), depth=(logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) and 3 or 1))
auth = getattr(self, 'auth_provider', None)
result = ExportService.getService(service_name).dispatch(method, auth, params)
- self.log('result', result)
- # We shouldn't marshall None,
- if result == None:
- result = False
+ self.log('result', result, channel=logging.DEBUG_RPC_ANSWER, depth=(logger.isEnabledFor(logging.DEBUG_SQL) and 5 or 3))
return result
except Exception, e:
self.log('exception', tools.exception_to_unicode(e))
diff --git a/bin/openerp-server.py b/bin/openerp-server.py
index c56977fe1f0..8d210fc2906 100755
--- a/bin/openerp-server.py
+++ b/bin/openerp-server.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -16,7 +16,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -27,48 +27,51 @@ OpenERP is an ERP+CRM program for small and medium businesses.
The whole source code is distributed under the terms of the
GNU Public Licence.
-(c) 2003-TODAY, Fabien Pinckaers - Tiny sprl
+(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
"""
#----------------------------------------------------------
# python imports
#----------------------------------------------------------
-import sys
+import logging
import os
import signal
-import pwd
+import sys
+import threading
+import traceback
import release
__author__ = release.author
__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)
+if os.name == 'posix':
+ import pwd
+ # 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
#----------------------------------------------------------
import netsvc
-logger = netsvc.Logger()
+logger = logging.getLogger('server')
#-----------------------------------------------------------------------
# import the tools module so that the commandline parameters are parsed
#-----------------------------------------------------------------------
import tools
-
-logger.notifyChannel("server", netsvc.LOG_INFO, "version - %s" % release.version )
+logger.info("OpenERP version - %s", release.version)
for name, value in [('addons_path', tools.config['addons_path']),
('database hostname', tools.config['db_host'] or 'localhost'),
('database port', tools.config['db_port'] or '5432'),
('database user', tools.config['db_user'])]:
- logger.notifyChannel("server", netsvc.LOG_INFO, "%s - %s" % ( name, value ))
+ logger.info("%s - %s", name, value)
# Don't allow if the connection to PostgreSQL done by postgres user
if tools.config['db_user'] == 'postgres':
- logger.notifyChannel("server", netsvc.LOG_ERROR, "%s" % ("Attempted to connect database with postgres user. This is a security flaws, aborting."))
+ logger.error("Connecting to the database as 'postgres' user is forbidden, as it present major security issues. Shutting down.")
sys.exit(1)
import time
@@ -76,7 +79,7 @@ import time
#----------------------------------------------------------
# init net service
#----------------------------------------------------------
-logger.notifyChannel("objects", netsvc.LOG_INFO, 'initialising distributed objects services')
+logger.info('initialising distributed objects services')
#---------------------------------------------------------------
# connect to the database and initialize it with base if needed
@@ -108,6 +111,7 @@ if not ( tools.config["stop_after_init"] or \
tools.config["translate_out"] ):
service.http_server.init_servers()
service.http_server.init_xmlrpc()
+ service.http_server.init_static_http()
import service.netrpc_server
service.netrpc_server.init_servers()
@@ -115,11 +119,10 @@ if not ( tools.config["stop_after_init"] or \
if tools.config['db_name']:
for dbname in tools.config['db_name'].split(','):
db,pool = pooler.get_db_and_pool(dbname, update_module=tools.config['init'] or tools.config['update'], pooljobs=False)
- if tools.config["test-file"]:
- logger.notifyChannel("init", netsvc.LOG_INFO,
- 'loading test file %s' % (tools.config["test-file"],))
+ if tools.config["test_file"]:
+ logger.info('loading test file %s', tools.config["test_file"])
cr = db.cursor()
- tools.convert_yaml_import(cr, 'base', file(tools.config["test-file"]), {}, 'test', True)
+ tools.convert_yaml_import(cr, 'base', file(tools.config["test_file"]), {}, 'test', True)
cr.rollback()
pool.get('ir.cron')._poolJobs(db.dbname)
@@ -133,16 +136,14 @@ if tools.config["translate_out"]:
msg = "language %s" % (tools.config["language"],)
else:
msg = "new language"
- logger.notifyChannel("init", netsvc.LOG_INFO,
- 'writing translation file for %s to %s' % (msg,
- tools.config["translate_out"]))
+ logger.info('writing translation file for %s to %s', msg, tools.config["translate_out"])
fileformat = os.path.splitext(tools.config["translate_out"])[-1][1:].lower()
buf = file(tools.config["translate_out"], "w")
tools.trans_export(tools.config["language"], tools.config["translate_modules"], buf, fileformat)
buf.close()
- logger.notifyChannel("init", netsvc.LOG_INFO, 'translation file written successfully')
+ logger.info('translation file written successfully')
sys.exit(0)
if tools.config["translate_in"]:
@@ -163,30 +164,64 @@ if tools.config["stop_after_init"]:
#----------------------------------------------------------
LST_SIGNALS = ['SIGINT', 'SIGTERM']
-if os.name == 'posix':
- LST_SIGNALS.extend(['SIGUSR1','SIGQUIT'])
-
SIGNALS = dict(
[(getattr(signal, sign), sign) for sign in LST_SIGNALS]
)
-def handler(signum, _):
+netsvc.quit_signals_received = 0
+
+def handler(signum, frame):
"""
:param signum: the signal number
- :param _:
+ :param frame: the interrupted stack frame or None
"""
+ netsvc.quit_signals_received += 1
+ if netsvc.quit_signals_received > 1:
+ sys.stderr.write("Forced shutdown.\n")
+ os._exit(0)
+
+def dumpstacks(signum, frame):
+ # code from http://stackoverflow.com/questions/132058/getting-stack-trace-from-a-running-python-application#answer-2569696
+ # modified for python 2.5 compatibility
+ thread_map = dict(threading._active, **threading._limbo)
+ id2name = dict([(threadId, thread.getName()) for threadId, thread in thread_map.items()])
+ code = []
+ for threadId, stack in sys._current_frames().items():
+ code.append("\n# Thread: %s(%d)" % (id2name[threadId], threadId))
+ for filename, lineno, name, line in traceback.extract_stack(stack):
+ code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
+ if line:
+ code.append(" %s" % (line.strip()))
+ logging.getLogger('dumpstacks').info("\n".join(code))
+
+for signum in SIGNALS:
+ signal.signal(signum, handler)
+
+if os.name == 'posix':
+ signal.signal(signal.SIGQUIT, dumpstacks)
+
+def quit():
netsvc.Agent.quit()
netsvc.Server.quitAll()
if tools.config['pidfile']:
os.unlink(tools.config['pidfile'])
- logger.notifyChannel('shutdown', netsvc.LOG_INFO,
- "Shutdown Server! - %s" % ( SIGNALS[signum], ))
- logger.shutdown()
- sys.exit(0)
+ logger = logging.getLogger('shutdown')
+ logger.info("Initiating OpenERP Server shutdown")
+ logger.info("Hit CTRL-C again or send a second signal to immediately terminate the server...")
+ logging.shutdown()
-for signum in SIGNALS:
- signal.signal(signum, handler)
+ # manually join() all threads before calling sys.exit() to allow a second signal
+ # to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
+ # threading.Thread.join() should not mask signals (at least in python 2.5)
+ for thread in threading.enumerate():
+ if thread != threading.currentThread() and not thread.isDaemon():
+ while thread.isAlive():
+ # need a busyloop here as thread.join() masks signals
+ # and would present the forced shutdown
+ thread.join(0.05)
+ time.sleep(0.05)
+ sys.exit(0)
if tools.config['pidfile']:
fd = open(tools.config['pidfile'], 'w')
@@ -194,13 +229,13 @@ if tools.config['pidfile']:
fd.write(pidtext)
fd.close()
-
netsvc.Server.startAll()
-logger.notifyChannel("web-services", netsvc.LOG_INFO,
- 'the server is running, waiting for connections...')
+logger.info('OpenERP server is running, waiting for connections...')
-while True:
+while netsvc.quit_signals_received == 0:
time.sleep(60)
+quit()
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/osv/expression.py b/bin/osv/expression.py
index 15e8d86ed2a..cb1280c8e3b 100644
--- a/bin/osv/expression.py
+++ b/bin/osv/expression.py
@@ -115,6 +115,7 @@ class expression(object):
if self._is_operator(e) or e == self.__DUMMY_LEAF:
continue
left, operator, right = e
+ operator = operator.lower()
working_table = table
main_table = table
fargs = left.split('.', 1)
@@ -148,7 +149,9 @@ class expression(object):
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context)
self.__exp[i] = ('id', 'in', right1)
- continue
+
+ if not isinstance(field,fields.property):
+ continue
if field._properties and ((not field.store) or field._fnct_search):
# this is a function field
@@ -235,7 +238,7 @@ class expression(object):
if isinstance(right, basestring):
res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, context=context)]
if res_ids:
- opeartor = 'in'
+ operator = 'in'
else:
if not isinstance(right, list):
res_ids = [right]
@@ -278,15 +281,47 @@ class expression(object):
dom = _rec_get(ids2, working_table, parent=left)
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
else:
- if isinstance(right, basestring): # and not isinstance(field, fields.related):
+ def _get_expression(field_obj,cr, uid, left, right, operator, context=None):
+ if context is None:
+ context = {}
c = context.copy()
c['active_test'] = False
+ #Special treatment to ill-formed domains
+ operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
+
+ dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
+ if isinstance(right,tuple):
+ right = list(right)
+ if (not isinstance(right,list)) and operator in ['not in','in']:
+ operator = dict_op[operator]
+ elif isinstance(right,list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2'])
+ operator = dict_op[operator]
res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
if not res_ids:
- self.__exp[i] = ('id','=',0)
+ return ('id','=',0)
else:
right = map(lambda x: x[0], res_ids)
- self.__exp[i] = (left, 'in', right)
+ return (left, 'in', right)
+
+ m2o_str = False
+ if right:
+ if isinstance(right, basestring): # and not isinstance(field, fields.related):
+ m2o_str = True
+ elif isinstance(right,(list,tuple)):
+ m2o_str = True
+ for ele in right:
+ if not isinstance(ele, basestring):
+ m2o_str = False
+ break
+ else:
+ new_op = '='
+ if operator in ['not like','not ilike','not in','<>','!=']:
+ new_op = '!='
+ #Is it ok to put 'left' and not 'id' ?
+ self.__exp[i] = (left,new_op,False)
+
+ if m2o_str:
+ self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
else:
# other field type
# add the time part to datetime field when it's not there:
diff --git a/bin/osv/fields.py b/bin/osv/fields.py
index c6186b9a052..a5839381f27 100644
--- a/bin/osv/fields.py
+++ b/bin/osv/fields.py
@@ -31,13 +31,13 @@
# required
# size
#
-from collections import defaultdict
+import datetime as DT
import string
import netsvc
import sys
+import warnings
from psycopg2 import Binary
-import warnings
import tools
from tools.translate import _
@@ -193,14 +193,42 @@ class float(_column):
class date(_column):
_type = 'date'
+ @staticmethod
+ def today(*args):
+ """ Returns the current date in a format fit for being a
+ default value to a ``date`` field.
+ This method should be provided as is to the _defaults dict, it
+ should not be called.
+ """
+ return DT.date.today().strftime(
+ tools.DEFAULT_SERVER_DATE_FORMAT)
class datetime(_column):
_type = 'datetime'
+ @staticmethod
+ def now(*args):
+ """ Returns the current datetime in a format fit for being a
+ default value to a ``datetime`` field.
+ This method should be provided as is to the _defaults dict, it
+ should not be called.
+ """
+ return DT.datetime.now().strftime(
+ tools.DEFAULT_SERVER_DATETIME_FORMAT)
class time(_column):
_type = 'time'
+ @staticmethod
+ def now( *args):
+ """ Returns the current time in a format fit for being a
+ default value to a ``time`` field.
+
+ This method should be proivided as is to the _defaults dict,
+ it should not be called.
+ """
+ return DT.datetime.now().strftime(
+ tools.DEFAULT_SERVER_TIME_FORMAT)
class binary(_column):
_type = 'binary'
@@ -228,7 +256,13 @@ class binary(_column):
if v['id'] == i:
val = v[name]
break
- if context.get('bin_size', False) and val:
+
+ # If client is requesting only the size of the field, we return it instead
+ # of the content. Presumably a separate request will be done to read the actual
+ # content if it's needed at some point.
+ # TODO: after 6.0 we should consider returning a dict with size and content instead of
+ # having an implicit convention for the value
+ if val and context.get('bin_size_%s' % name, context.get('bin_size')):
res[i] = tools.human_size(long(val))
else:
res[i] = val
@@ -303,14 +337,13 @@ class many2one(_column):
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
result = {}
for id in ids:
- result[id] = obj.datas[id][name]
+ result[id] = obj.datas[id].get(name, False)
return result
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
- if not context:
- context = {}
- if not values:
- values = {}
+ context = context or {}
+ values = values or {}
+
res = {}
for r in values:
res[r['id']] = r[name]
@@ -319,21 +352,14 @@ class many2one(_column):
obj = obj.pool.get(self._obj)
# build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
- from orm import except_orm
- names = {}
- for record in list(set(filter(None, res.values()))):
- try:
- record_name = dict(obj.name_get(cr, user, [record], context))
- except except_orm:
- record_name = {}
- record_name[record] = '// Access Denied //'
- names.update(record_name)
-
- for r in res.keys():
- if res[r] and res[r] in names:
- res[r] = (res[r], names[res[r]])
+ # we use uid=1 because the visibility of a many2one field value (just id and name)
+ # must be the access right of the parent form and not the linked object itself.
+ records = dict(obj.name_get(cr, 1, list(set(filter(None, res.values()))), context=context))
+ for id in res:
+ if res[id] in records:
+ res[id] = (res[id], records[res[id]])
else:
- res[r] = False
+ res[id] = False
return res
def set(self, cr, obj_src, id, field, values, user=None, context=None):
@@ -436,11 +462,14 @@ class one2many(_column):
if not values:
values = {}
- res = defaultdict(list)
+ res = {}
+ for id in ids:
+ res[id] = []
ids2 = obj.pool.get(self._obj).search(cr, user, self._domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
- res[r[self._fields_id]].append(r['id'])
+ if r[self._fields_id] in res:
+ res[r[self._fields_id]].append(r['id'])
return res
def set(self, cr, obj, id, field, values, user=None, context=None):
@@ -517,31 +546,50 @@ class many2many(_column):
return res
for id in ids:
res[id] = []
- limit_str = self._limit is not None and ' limit %d' % self._limit or ''
+ if offset:
+ warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.",
+ DeprecationWarning, stacklevel=2)
obj = obj.pool.get(self._obj)
- d1, d2, tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
- if d1:
- d1 = ' and ' + ' and '.join(d1)
- else: d1 = ''
+ # static domains are lists, and are evaluated both here and on client-side, while string
+ # domains supposed by dynamic and evaluated on client-side only (thus ignored here)
+ # FIXME: make this distinction explicit in API!
+ domain = isinstance(self._domain, list) and self._domain or []
+
+ wquery = obj._where_calc(cr, user, domain, context=context)
+ obj._apply_ir_rules(cr, user, wquery, 'read', context=context)
+ from_c, where_c, where_params = wquery.get_sql()
+ if where_c:
+ where_c = ' AND ' + where_c
+
+ if offset or self._limit:
+ order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0])
+ else:
+ order_by = ''
+
+ limit_str = ''
+ if self._limit is not None:
+ limit_str = ' LIMIT %d' % self._limit
+
query = 'SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s \
- FROM %(rel)s, %(tbl)s \
- WHERE %(rel)s.%(id1)s in %%s \
+ FROM %(rel)s, %(from_c)s \
+ WHERE %(rel)s.%(id1)s IN %%s \
AND %(rel)s.%(id2)s = %(tbl)s.id \
- %(d1)s \
+ %(where_c)s \
+ %(order_by)s \
%(limit)s \
- ORDER BY %(tbl)s.%(order)s \
OFFSET %(offset)d' \
% {'rel': self._rel,
+ 'from_c': from_c,
'tbl': obj._table,
'id1': self._id1,
'id2': self._id2,
- 'd1': d1,
+ 'where_c': where_c,
'limit': limit_str,
- 'order': obj._order,
+ 'order_by': order_by,
'offset': offset,
}
- cr.execute(query, [tuple(ids)] + d2)
+ cr.execute(query, [tuple(ids),] + where_params)
for r in cr.fetchall():
res[r[1]].append(r[0])
return res
@@ -661,7 +709,9 @@ class function(_column):
self.selectable = False
if store:
- self._classic_read = True
+ if self._type != 'many2one':
+ # m2o fields need to return tuples with name_get, not just foreign keys
+ self._classic_read = True
self._classic_write = True
if type=='binary':
self._symbol_get=lambda x:x and str(x)
@@ -754,45 +804,42 @@ class related(function):
i -= 1
return [(self._arg[0], 'in', sarg)]
- def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
- if values and field_name:
- self._field_get2(cr, uid, obj, context)
- relation = obj._name
- res = {}
- if type(ids) != type([]):
- ids=[ids]
- objlst = obj.browse(cr, uid, ids)
- for data in objlst:
- t_id = None
- t_data = data
- relation = obj._name
- for i in range(len(self.arg)):
- field_detail = self._relations[i]
- relation = field_detail['object']
- if not t_data[self.arg[i]]:
- if self._type not in ('one2many', 'many2many'):
- t_id = t_data['id']
- t_data = False
- break
- if field_detail['type'] in ('one2many', 'many2many'):
- if self._type != "many2one":
- t_id = t_data.id
- t_data = t_data[self.arg[i]][0]
- else:
- t_data = False
- break
- else:
+ def _fnct_write(self,obj,cr, uid, ids, field_name, values, args, context=None):
+ self._field_get2(cr, uid, obj, context=context)
+ if type(ids) != type([]):
+ ids=[ids]
+ objlst = obj.browse(cr, uid, ids)
+ for data in objlst:
+ t_id = data.id
+ t_data = data
+ for i in range(len(self.arg)):
+ if not t_data: break
+ field_detail = self._relations[i]
+ if not t_data[self.arg[i]]:
+ if self._type not in ('one2many', 'many2many'):
t_id = t_data['id']
- t_data = t_data[self.arg[i]]
-
- if t_id and t_data:
- obj.pool.get(field_detail['object']).write(cr,uid,[t_id],{args[-1]:values}, context=context)
+ t_data = False
+ elif field_detail['type'] in ('one2many', 'many2many'):
+ if self._type != "many2one":
+ t_id = t_data.id
+ t_data = t_data[self.arg[i]][0]
+ else:
+ t_data = False
+ else:
+ t_id = t_data['id']
+ t_data = t_data[self.arg[i]]
+ else:
+ model = obj.pool.get(self._relations[-1]['object'])
+ model.write(cr, uid, [t_id], {args[-1]: values}, context=context)
def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
self._field_get2(cr, uid, obj, context)
if not ids: return {}
relation = obj._name
- res = {}.fromkeys(ids, False)
+ if self._type in ('one2many', 'many2many'):
+ res = dict([(i, []) for i in ids])
+ else:
+ res = {}.fromkeys(ids, False)
objlst = obj.browse(cr, 1, ids, context=context)
for data in objlst:
@@ -812,11 +859,11 @@ class related(function):
break
if field_detail['type'] in ('one2many', 'many2many') and i != len(self.arg) - 1:
t_data = t_data[self.arg[i]][0]
- else:
+ elif t_data:
t_data = t_data[self.arg[i]]
if type(t_data) == type(objlst[0]):
res[data.id] = t_data.id
- else:
+ elif t_data:
res[data.id] = t_data
if self._type=='many2one':
ids = filter(None, res.values())
@@ -886,38 +933,44 @@ class serialized(_column):
super(serialized, self).__init__(string=string, **args)
+# TODO: review completly this class for speed improvement
class property(function):
def _get_default(self, obj, cr, uid, prop_name, context=None):
- from orm import browse_record
- prop = obj.pool.get('ir.property')
- domain = prop._get_domain_default(cr, uid, prop_name, obj._name, context)
- ids = prop.search(cr, uid, domain, order='company_id', context=context)
- if not ids:
- return False
+ return self._get_defaults(obj, cr, uid, [prop_name], context=None)[0][prop_name]
- default_value = prop.get_by_id(cr, uid, ids, context=context)
- if isinstance(default_value, browse_record):
- return default_value.id
- return default_value or False
+ def _get_defaults(self, obj, cr, uid, prop_name, context=None):
+ prop = obj.pool.get('ir.property')
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name), ('res_id','=',False)]
+ ids = prop.search(cr, uid, domain, context=context)
+ replaces = {}
+ default_value = {}.fromkeys(prop_name, False)
+ for prop_rec in prop.browse(cr, uid, ids, context=context):
+ if default_value.get(prop_rec.fields_id.name, False):
+ continue
+ value = prop.get_by_record(cr, uid, prop_rec, context=context) or False
+ default_value[prop_rec.fields_id.name] = value
+ if value and (prop_rec.type == 'many2one'):
+ replaces.setdefault(value._name, {})
+ replaces[value._name][value.id] = True
+ return default_value, replaces
def _get_by_id(self, obj, cr, uid, prop_name, ids, context=None):
prop = obj.pool.get('ir.property')
vids = [obj._name + ',' + str(oid) for oid in ids]
- domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
- if domain is not None:
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
+ #domain = prop._get_domain(cr, uid, prop_name, obj._name, context)
+ if vids:
domain = [('res_id', 'in', vids)] + domain
- return prop.search(cr, uid, domain, context=context)
- else:
- return []
-
+ return prop.search(cr, uid, domain, context=context)
+ # TODO: to rewrite more clean
def _fnct_write(self, obj, cr, uid, id, prop_name, id_val, obj_dest, context=None):
if context is None:
context = {}
- nids = self._get_by_id(obj, cr, uid, prop_name, [id], context)
+ nids = self._get_by_id(obj, cr, uid, [prop_name], [id], context)
if nids:
cr.execute('DELETE FROM ir_property WHERE id IN %s', (tuple(nids),))
@@ -938,29 +991,37 @@ class property(function):
'company_id': cid,
'fields_id': def_id,
'type': self._type,
- }, context=context)
+ }, context=context)
return False
def _fnct_read(self, obj, cr, uid, ids, prop_name, obj_dest, context=None):
- from orm import browse_record
properties = obj.pool.get('ir.property')
-
- default_val = self._get_default(obj, cr, uid, prop_name, context)
-
- nids = self._get_by_id(obj, cr, uid, prop_name, ids, context)
+ domain = [('fields_id.model', '=', obj._name), ('fields_id.name','in',prop_name)]
+ domain += [('res_id','in', [obj._name + ',' + str(oid) for oid in ids])]
+ nids = properties.search(cr, uid, domain, context=context)
+ default_val,replaces = self._get_defaults(obj, cr, uid, prop_name, context)
res = {}
for id in ids:
- res[id] = default_val
- for prop in properties.browse(cr, uid, nids):
- value = prop.get_by_id(context=context)
- if isinstance(value, browse_record):
- if not value.exists():
- cr.execute('DELETE FROM ir_property WHERE id=%s', (prop.id,))
- continue
- value = value.id
- res[prop.res_id.id] = value or False
+ res[id] = default_val.copy()
+
+ brs = properties.browse(cr, uid, nids, context=context)
+ for prop in brs:
+ value = properties.get_by_record(cr, uid, prop, context=context)
+ res[prop.res_id.id][prop.fields_id.name] = value or False
+ if value and (prop.type == 'many2one'):
+ replaces.setdefault(value._name, {})
+ replaces[value._name][value.id] = True
+
+ for rep in replaces:
+ replaces[rep] = dict(obj.pool.get(rep).name_get(cr, uid, replaces[rep].keys(), context=context))
+
+ for prop in prop_name:
+ for id in ids:
+ if res[id][prop] and hasattr(res[id][prop], '_name'):
+ res[id][prop] = (res[id][prop].id , replaces[res[id][prop]._name].get(res[id][prop].id, False))
+
return res
@@ -977,7 +1038,7 @@ class property(function):
# TODO remove obj_prop parameter (use many2one type)
self.field_id = {}
function.__init__(self, self._fnct_read, False, self._fnct_write,
- obj_prop, **args)
+ obj_prop, multi='properties', **args)
def restart(self):
self.field_id = {}
diff --git a/bin/osv/orm.py b/bin/osv/orm.py
index c5a47d1201f..f29d928d5ba 100644
--- a/bin/osv/orm.py
+++ b/bin/osv/orm.py
@@ -44,7 +44,6 @@ import logging
import operator
import pickle
import re
-import string
import time
import traceback
import types
@@ -55,6 +54,7 @@ from tools.config import config
from tools.translate import _
import fields
+from query import Query
import tools
from tools.safe_eval import safe_eval as eval
@@ -188,93 +188,87 @@ class browse_record(object):
# if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
if col._prefetch:
# gen the list of "local" (ie not inherited) fields which are classic or many2one
- ffields = filter(lambda x: x[1]._classic_write, self._table._columns.items())
+ fields_to_fetch = filter(lambda x: x[1]._classic_write, self._table._columns.items())
# gen the list of inherited fields
inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items())
# complete the field list with the inherited fields which are classic or many2one
- ffields += filter(lambda x: x[1]._classic_write, inherits)
+ fields_to_fetch += filter(lambda x: x[1]._classic_write, inherits)
# otherwise we fetch only that field
else:
- ffields = [(name, col)]
+ fields_to_fetch = [(name, col)]
ids = filter(lambda id: name not in self._data[id], self._data.keys())
- # read the data
- fffields = map(lambda x: x[0], ffields)
- datas = self._table.read(self._cr, self._uid, ids, fffields, context=self._context, load="_classic_write")
+ # read the results
+ field_names = map(lambda x: x[0], fields_to_fetch)
+ field_values = self._table.read(self._cr, self._uid, ids, field_names, context=self._context, load="_classic_write")
if self._fields_process:
lang = self._context.get('lang', 'en_US') or 'en_US'
- lang_obj_ids = self.pool.get('res.lang').search(self._cr, self._uid,[('code','=',lang)])
+ lang_obj_ids = self.pool.get('res.lang').search(self._cr, self._uid, [('code', '=', lang)])
if not lang_obj_ids:
raise Exception(_('Language with code "%s" is not defined in your system !\nDefine it through the Administration menu.') % (lang,))
- lang_obj = self.pool.get('res.lang').browse(self._cr, self._uid,lang_obj_ids[0])
+ lang_obj = self.pool.get('res.lang').browse(self._cr, self._uid, lang_obj_ids[0])
- for n, f in ffields:
- if f._type in self._fields_process:
- for d in datas:
- d[n] = self._fields_process[f._type](d[n])
- if d[n]:
- d[n].set_value(self._cr, self._uid, d[n], self, f, lang_obj)
+ for field_name, field_column in fields_to_fetch:
+ if field_column._type in self._fields_process:
+ for result_line in field_values:
+ result_line[field_name] = self._fields_process[field_column._type](result_line[field_name])
+ if result_line[field_name]:
+ result_line[field_name].set_value(self._cr, self._uid, result_line[field_name], self, field_column, lang_obj)
-
- if not datas:
+ if not field_values:
# Where did those ids come from? Perhaps old entries in ir_model_dat?
- self.__logger.warn("No datas found for ids %s in %s",
- ids, self)
- raise KeyError('Field %s not found in %s'%(name,self))
+ self.__logger.warn("No field_values found for ids %s in %s", ids, self)
+ raise KeyError('Field %s not found in %s'%(name, self))
# create browse records for 'remote' objects
- for data in datas:
- if len(str(data['id']).split('-')) > 1:
- data['id'] = int(str(data['id']).split('-')[0])
+ for result_line in field_values:
new_data = {}
- for n, f in ffields:
- if f._type in ('many2one', 'one2one'):
- if data[n]:
- obj = self._table.pool.get(f._obj)
- compids = False
- if type(data[n]) in (type([]),type( (1,) )):
- ids2 = data[n][0]
+ for field_name, field_column in fields_to_fetch:
+ if field_column._type in ('many2one', 'one2one'):
+ if result_line[field_name]:
+ obj = self._table.pool.get(field_column._obj)
+ if isinstance(result_line[field_name], (list, tuple)):
+ value = result_line[field_name][0]
else:
- ids2 = data[n]
- if ids2:
+ value = result_line[field_name]
+ if value:
# FIXME: this happen when a _inherits object
# overwrite a field of it parent. Need
# testing to be sure we got the right
# object and not the parent one.
- if not isinstance(ids2, browse_record):
- new_data[n] = browse_record(self._cr,
- self._uid, ids2, obj, self._cache,
+ if not isinstance(value, browse_record):
+ new_data[field_name] = browse_record(self._cr,
+ self._uid, value, obj, self._cache,
context=self._context,
list_class=self._list_class,
fields_process=self._fields_process)
else:
- new_data[n] = ids2
+ new_data[field_name] = value
else:
- new_data[n] = browse_null()
+ new_data[field_name] = browse_null()
else:
- new_data[n] = browse_null()
- elif f._type in ('one2many', 'many2many') and len(data[n]):
- new_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)
- elif f._type in ('reference'):
- if data[n]:
- if isinstance(data[n], browse_record):
- new_data[n] = data[n]
+ new_data[field_name] = browse_null()
+ elif field_column._type in ('one2many', 'many2many') and len(result_line[field_name]):
+ new_data[field_name] = self._list_class([browse_record(self._cr, self._uid, id, self._table.pool.get(field_column._obj), self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process) for id in result_line[field_name]], self._context)
+ elif field_column._type in ('reference'):
+ if result_line[field_name]:
+ if isinstance(result_line[field_name], browse_record):
+ new_data[field_name] = result_line[field_name]
else:
- ref_obj, ref_id = data[n].split(',')
+ ref_obj, ref_id = result_line[field_name].split(',')
ref_id = long(ref_id)
obj = self._table.pool.get(ref_obj)
- compids = False
- new_data[n] = browse_record(self._cr, self._uid, ref_id, obj, self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process)
+ new_data[field_name] = browse_record(self._cr, self._uid, ref_id, obj, self._cache, context=self._context, list_class=self._list_class, fields_process=self._fields_process)
else:
- new_data[n] = browse_null()
+ new_data[field_name] = browse_null()
else:
- new_data[n] = data[n]
- self._data[data['id']].update(new_data)
+ new_data[field_name] = result_line[field_name]
+ self._data[result_line['id']].update(new_data)
if not name in self._data[self._id]:
#how did this happen?
self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR,
- "Ffields: %s, datas: %s"%(fffields, datas))
+ "Fields to fetch: %s, Field values: %s"%(field_names, field_values))
self.logger.notifyChannel("browse_record", netsvc.LOG_ERROR,
- "Data: %s, Table: %s"%(self._data[self._id], self._table))
+ "Cached: %s, Table: %s"%(self._data[self._id], self._table))
raise KeyError(_('Unknown attribute %s in %s ') % (name, self))
return self._data[self._id][name]
@@ -355,7 +349,7 @@ def get_pg_type(f):
f_type = ('int4', 'INTEGER')
else:
f_type = ('varchar', 'VARCHAR(%d)' % f_size)
- elif isinstance(f, fields.function) and eval('fields.'+(f._type),globals()) in type_dict:
+ elif isinstance(f, fields.function) and eval('fields.'+(f._type), globals()) in type_dict:
t = eval('fields.'+(f._type), globals())
f_type = (type_dict[t], type_dict[t])
elif isinstance(f, fields.function) and f._type == 'float':
@@ -394,11 +388,14 @@ class orm_template(object):
CONCURRENCY_CHECK_FIELD = '__last_update'
def log(self, cr, uid, id, message, secondary=False, context=None):
- return self.pool.get('res.log').create(cr, uid, {
- 'name': message,
- 'res_model': self._name,
- 'secondary': secondary,
- 'res_id': id},
+ return self.pool.get('res.log').create(cr, uid,
+ {
+ 'name': message,
+ 'res_model': self._name,
+ 'secondary': secondary,
+ 'res_id': id,
+ 'context': context,
+ },
context=context
)
@@ -420,8 +417,8 @@ class orm_template(object):
else:
model_id = cr.fetchone()[0]
if 'module' in context:
- name_id = 'model_'+self._name.replace('.','_')
- cr.execute('select * from ir_model_data where name=%s and res_id=%s and module=%s', (name_id,model_id,context['module']))
+ name_id = 'model_'+self._name.replace('.', '_')
+ cr.execute('select * from ir_model_data where name=%s and res_id=%s and module=%s', (name_id, model_id, context['module']))
if not cr.rowcount:
cr.execute("INSERT INTO ir_model_data (name,date_init,date_update,module,model,res_id) VALUES (%s, now(), now(), %s, %s, %s)", \
(name_id, context['module'], 'ir.model', model_id)
@@ -444,17 +441,17 @@ class orm_template(object):
'relation': f._obj or '',
'view_load': (f.view_load and 1) or 0,
'select_level': tools.ustr(f.select or 0),
- 'readonly':(f.readonly and 1) or 0,
- 'required':(f.required and 1) or 0,
- 'selectable' : (f.selectable and 1) or 0,
- 'relation_field': (f._type=='one2many' and isinstance(f,fields.one2many)) and f._fields_id or '',
+ 'readonly': (f.readonly and 1) or 0,
+ 'required': (f.required and 1) or 0,
+ 'selectable': (f.selectable and 1) or 0,
+ 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '',
}
# When its a custom field,it does not contain f.select
- if context.get('field_state','base') == 'manual':
- if context.get('field_name','') == k:
- vals['select_level'] = context.get('select','0')
+ if context.get('field_state', 'base') == 'manual':
+ if context.get('field_name', '') == k:
+ vals['select_level'] = context.get('select', '0')
#setting value to let the problem NOT occur next time
- else:
+ elif k in cols:
vals['select_level'] = cols[k]['select_level']
if k not in cols:
@@ -469,7 +466,7 @@ class orm_template(object):
)""", (
id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'],
vals['relation'], bool(vals['view_load']), 'base',
- vals['select_level'],vals['relation_field']
+ vals['select_level'], vals['relation_field']
))
if 'module' in context:
name1 = 'field_' + self._table + '_' + k
@@ -491,9 +488,9 @@ class orm_template(object):
model=%s AND name=%s""", (
vals['model_id'], vals['field_description'], vals['ttype'],
vals['relation'], bool(vals['view_load']),
- vals['select_level'], bool(vals['readonly']),bool(vals['required']),bool(vals['selectable']),vals['relation_field'],vals['model'], vals['name']
+ vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], vals['model'], vals['name']
))
- continue
+ break
cr.commit()
def _auto_init(self, cr, context=None):
@@ -505,8 +502,8 @@ class orm_template(object):
msg = "The class %s has to have a _name attribute" % name
logger = netsvc.Logger()
- logger.notifyChannel('orm', netsvc.LOG_ERROR, msg )
- raise except_orm('ValueError', msg )
+ logger.notifyChannel('orm', netsvc.LOG_ERROR, msg)
+ raise except_orm('ValueError', msg)
if not self._description:
self._description = self._name
@@ -557,11 +554,10 @@ class orm_template(object):
else:
return False
-
lines = []
data = map(lambda x: '', range(len(fields)))
- done = []
for fpos in range(len(fields)):
+ done = []
f = fields[fpos]
if f:
r = row
@@ -571,11 +567,11 @@ class orm_template(object):
r = r['id']
elif f[i] == 'id':
model_data = self.pool.get('ir.model.data')
- data_ids = model_data.search(cr, uid, [('model','=',r._table_name),('res_id','=',r['id'])])
+ data_ids = model_data.search(cr, uid, [('model', '=', r._table_name), ('res_id', '=', r['id'])])
if len(data_ids):
- d = model_data.read(cr, uid, data_ids, ['name','module'])[0]
+ d = model_data.read(cr, uid, data_ids, ['name', 'module'])[0]
if d['module']:
- r = '%s.%s'%(d['module'],d['name'])
+ r = '%s.%s' % (d['module'], d['name'])
else:
r = d['name']
else:
@@ -583,7 +579,7 @@ class orm_template(object):
else:
r = r[f[i]]
# To display external name of selection field when its exported
- if not context.get('import_comp',False):# Allow external name only if its not import compatible
+ if not context.get('import_comp', False):# Allow external name only if its not import compatible
cols = False
if f[i] in self._columns.keys():
cols = self._columns[f[i]]
@@ -617,7 +613,7 @@ class orm_template(object):
data[fpos2] = lines2[0][fpos2]
if not data[fpos]:
dt = ''
- for rr in r :
+ for rr in r:
if isinstance(rr.name, browse_record):
rr = rr.name
rr_name = self.pool.get(rr._table_name).name_get(cr, uid, [rr.id], context=context)
@@ -652,33 +648,33 @@ class orm_template(object):
This method is used when exporting data via client menu
"""
- if not context:
+ if context is None:
context = {}
- imp_comp = context.get('import_comp',False)
+ imp_comp = context.get('import_comp', False)
cols = self._columns.copy()
for f in self._inherit_fields:
cols.update({f: self._inherit_fields[f][2]})
fields_to_export = map(lambda x: x.split('/'), fields_to_export)
- fields_export = fields_to_export+[]
+ fields_export = fields_to_export + []
warning = ''
warning_fields = []
for field in fields_export:
- if imp_comp and len(field)>1:
- warning_fields.append('/'.join(map(lambda x:x in cols and cols[x].string or x,field)))
- elif len (field) <=1:
- if imp_comp and cols.get(field and field[0],False):
+ if imp_comp and len(field) > 1:
+ warning_fields.append('/'.join(map(lambda x: x in cols and cols[x].string or x,field)))
+ elif len (field) <= 1:
+ if imp_comp and cols.get(field and field[0], False):
if ((isinstance(cols[field[0]], fields.function) and not cols[field[0]].store) \
or isinstance(cols[field[0]], fields.related)\
or isinstance(cols[field[0]], fields.one2many)):
- warning_fields.append('/'.join(map(lambda x:x in cols and cols[x].string or x,field)))
+ warning_fields.append('/'.join(map(lambda x: x in cols and cols[x].string or x,field)))
datas = []
if imp_comp and len(warning_fields):
- warning = 'Following columns cannot be exported since you select to be import compatible.\n%s' %('\n'.join(warning_fields))
+ warning = 'Following columns cannot be exported since you select to be import compatible.\n%s' % ('\n'.join(warning_fields))
cr.rollback()
- return {'warning' : warning}
+ return {'warning': warning}
for row in self.browse(cr, uid, ids, context):
datas += self.__export_row(cr, uid, row, fields_to_export, context)
- return {'datas':datas}
+ return {'datas': datas}
def import_data(self, cr, uid, fields, datas, mode='init', current_module='', noupdate=False, context=None, filename=None):
"""
@@ -707,7 +703,7 @@ class orm_template(object):
def _check_db_id(self, model_name, db_id):
obj_model = self.pool.get(model_name)
- ids = obj_model.search(cr, uid, [('id','=',int(db_id))])
+ ids = obj_model.search(cr, uid, [('id', '=', int(db_id))])
if not len(ids):
raise Exception(_("Database ID doesn't exist: %s : %s") %(model_name, db_id))
return True
@@ -738,28 +734,28 @@ class orm_template(object):
if (len(field)==len(prefix)+1) and field[len(prefix)].endswith(':db_id'):
# Database ID
- res = False
- if line[i]:
- field_name = field[0].split(':')[0]
- model_rel = fields_def[field_name]['relation']
+ res = False
+ if line[i]:
+ field_name = field[0].split(':')[0]
+ model_rel = fields_def[field_name]['relation']
- if fields_def[field[len(prefix)][:-6]]['type']=='many2many':
- res_id = []
- for db_id in line[i].split(config.get('csv_internal_sep')):
- try:
- _check_db_id(self, model_rel, db_id)
- res_id.append(db_id)
- except Exception,e:
- warning += [tools.exception_to_unicode(e)]
- logger.notifyChannel("import", netsvc.LOG_ERROR,
- tools.exception_to_unicode(e))
- if len(res_id):
- res = [(6, 0, res_id)]
+ if fields_def[field[len(prefix)][:-6]]['type'] == 'many2many':
+ res_id = []
+ for db_id in line[i].split(config.get('csv_internal_sep')):
+ try:
+ _check_db_id(self, model_rel, db_id)
+ res_id.append(db_id)
+ except Exception, e:
+ warning += [tools.exception_to_unicode(e)]
+ logger.notifyChannel("import", netsvc.LOG_ERROR,
+ tools.exception_to_unicode(e))
+ if len(res_id):
+ res = [(6, 0, res_id)]
else:
try:
_check_db_id(self, model_rel, line[i])
res = line[i]
- except Exception,e:
+ except Exception, e:
warning += [tools.exception_to_unicode(e)]
logger.notifyChannel("import", netsvc.LOG_ERROR,
tools.exception_to_unicode(e))
@@ -769,7 +765,7 @@ class orm_template(object):
if (len(field)==len(prefix)+1) and field[len(prefix)].endswith(':id'):
res_id = False
if line[i]:
- if fields_def[field[len(prefix)][:-3]]['type']=='many2many':
+ if fields_def[field[len(prefix)][:-3]]['type'] == 'many2many':
res_id = []
for word in line[i].split(config.get('csv_internal_sep')):
if '.' in word:
@@ -800,7 +796,7 @@ class orm_template(object):
if (len(field) == len(prefix)+1) and \
len(field[len(prefix)].split(':lang=')) == 2:
f, lang = field[len(prefix)].split(':lang=')
- translate.setdefault(lang, {})[f]=line[i] or False
+ translate.setdefault(lang, {})[f] = line[i] or False
continue
if (len(field) == len(prefix)+1) and \
(prefix == field[0:len(prefix)]):
@@ -808,22 +804,22 @@ class orm_template(object):
# XML ID
db_id = False
is_xml_id = data_id = line[i]
- d = data_id.split('.')
- module = len(d)>1 and d[0] or ''
- name = len(d)>1 and d[1] or d[0]
- data_ids = ir_model_data_obj.search(cr, uid, [('module','=',module),('model','=',model_name),('name','=',name)])
+ d = data_id.split('.')
+ module = len(d) > 1 and d[0] or ''
+ name = len(d) > 1 and d[1] or d[0]
+ data_ids = ir_model_data_obj.search(cr, uid, [('module', '=', module), ('model', '=', model_name), ('name', '=', name)])
if len(data_ids):
d = ir_model_data_obj.read(cr, uid, data_ids, ['res_id'])[0]
db_id = d['res_id']
if is_db_id and not db_id:
- data_ids = ir_model_data_obj.search(cr, uid, [('module','=',module),('model','=',model_name),('res_id','=',is_db_id)])
- if not len(data_ids):
- ir_model_data_obj.create(cr, uid, {'module':module, 'model':model_name, 'name':name, 'res_id':is_db_id})
- db_id = is_db_id
+ data_ids = ir_model_data_obj.search(cr, uid, [('module', '=', module), ('model', '=', model_name), ('res_id', '=', is_db_id)])
+ if not len(data_ids):
+ ir_model_data_obj.create(cr, uid, {'module': module, 'model': model_name, 'name': name, 'res_id': is_db_id})
+ db_id = is_db_id
if is_db_id and int(db_id) != int(is_db_id):
- warning += [_("Id is not the same than existing one: %s")%(is_db_id)]
+ warning += [_("Id is not the same than existing one: %s") % (is_db_id)]
logger.notifyChannel("import", netsvc.LOG_ERROR,
- _("Id is not the same than existing one: %s")%(is_db_id))
+ _("Id is not the same than existing one: %s") % (is_db_id))
continue
if field[len(prefix)] == "db_id":
@@ -831,25 +827,25 @@ class orm_template(object):
try:
_check_db_id(self, model_name, line[i])
data_res_id = is_db_id = int(line[i])
- except Exception,e:
+ except Exception, e:
warning += [tools.exception_to_unicode(e)]
logger.notifyChannel("import", netsvc.LOG_ERROR,
tools.exception_to_unicode(e))
continue
- data_ids = ir_model_data_obj.search(cr, uid, [('model','=',model_name),('res_id','=',line[i])])
+ data_ids = ir_model_data_obj.search(cr, uid, [('model', '=', model_name), ('res_id', '=', line[i])])
if len(data_ids):
- d = ir_model_data_obj.read(cr, uid, data_ids, ['name','module'])[0]
+ d = ir_model_data_obj.read(cr, uid, data_ids, ['name', 'module'])[0]
data_id = d['name']
if d['module']:
- data_id = '%s.%s'%(d['module'],d['name'])
+ data_id = '%s.%s' % (d['module'], d['name'])
else:
data_id = d['name']
if is_xml_id and not data_id:
data_id = is_xml_id
- if is_xml_id and is_xml_id!=data_id:
- warning += [_("Id is not the same than existing one: %s")%(line[i])]
+ if is_xml_id and is_xml_id != data_id:
+ warning += [_("Id is not the same than existing one: %s") % (line[i])]
logger.notifyChannel("import", netsvc.LOG_ERROR,
- _("Id is not the same than existing one: %s")%(line[i]))
+ _("Id is not the same than existing one: %s") % (line[i]))
continue
if fields_def[field[len(prefix)]]['type'] == 'integer':
@@ -867,7 +863,7 @@ class orm_template(object):
sel = fields_def[field[len(prefix)]]['selection'](self,
cr, uid, context)
for key, val in sel:
- if line[i] in [tools.ustr(key),tools.ustr(val)]: #Acepting key or value for selection field
+ if line[i] in [tools.ustr(key), tools.ustr(val)]: #Acepting key or value for selection field
res = key
break
if line[i] and not res:
@@ -875,9 +871,9 @@ class orm_template(object):
_("key '%s' not found in selection field '%s'") % \
(line[i], field[len(prefix)]))
- warning += [_("Key/value '%s' not found in selection field '%s'")%(line[i],field[len(prefix)])]
+ warning += [_("Key/value '%s' not found in selection field '%s'") % (line[i], field[len(prefix)])]
- elif fields_def[field[len(prefix)]]['type']=='many2one':
+ elif fields_def[field[len(prefix)]]['type'] == 'many2one':
res = False
if line[i]:
relation = fields_def[field[len(prefix)]]['relation']
@@ -885,10 +881,10 @@ class orm_template(object):
line[i], [], operator='=', context=context)
res = (res2 and res2[0][0]) or False
if not res:
- warning += [_("Relation not found: %s on '%s'")%(line[i],relation)]
+ warning += [_("Relation not found: %s on '%s'") % (line[i], relation)]
logger.notifyChannel("import", netsvc.LOG_WARNING,
- _("Relation not found: %s on '%s'")%(line[i],relation))
- elif fields_def[field[len(prefix)]]['type']=='many2many':
+ _("Relation not found: %s on '%s'") % (line[i], relation))
+ elif fields_def[field[len(prefix)]]['type'] == 'many2many':
res = []
if line[i]:
relation = fields_def[field[len(prefix)]]['relation']
@@ -897,10 +893,10 @@ class orm_template(object):
uid, word, [], operator='=', context=context)
res3 = (res2 and res2[0][0]) or False
if not res3:
- warning += [_("Relation not found: %s on '%s'")%(line[i],relation)]
+ warning += [_("Relation not found: %s on '%s'") % (line[i], relation)]
logger.notifyChannel("import",
netsvc.LOG_WARNING,
- _("Relation not found: %s on '%s'")%(line[i],relation))
+ _("Relation not found: %s on '%s'") % (line[i], relation))
else:
res.append(res3)
if len(res):
@@ -924,10 +920,10 @@ class orm_template(object):
nbrmax = max(nbrmax, max2)
warning = warning + w2
reduce(lambda x, y: x and y, newrow)
- row[field] = (reduce(lambda x, y: x or y, newrow.values()) and \
+ row[field] = newrow and (reduce(lambda x, y: x or y, newrow.values()) and \
[(0, 0, newrow)]) or []
i = max2
- while (position+i) valid = signal_next (role X)
- # draft -> cancel = signal_cancel (no role)
- #
- # valid -> running = signal_next (role Y)
- # valid -> cancel = signal_cancel (role Z)
- #
- # running -> done = signal_next (role Z)
- # running -> cancel = signal_cancel (role Z)
-
- # As we don't know the object state, in this scenario,
- # the button "signal_cancel" will be always shown as there is no restriction to cancel in draft
- # the button "signal_next" will be show if the user has any of the roles (X Y or Z)
- # The verification will be made later in workflow process...
- if roles:
- can_click = any((not role) or rolesobj.check(cr, user, user_roles, role) for (role,) in roles)
-
+ user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
+ cr.execute("""SELECT DISTINCT t.group_id
+ FROM wkf
+ INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
+ INNER JOIN wkf_transition t ON (t.act_to = a.id)
+ WHERE wkf.osv = %s
+ AND t.signal = %s
+ AND t.group_id is NOT NULL
+ """, (self._name, button.get('name')))
+ group_ids = [x[0] for x in cr.fetchall() if x[0]]
+ can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
button.set('readonly', str(int(not can_click)))
+ return node
+ def __view_look_dom_arch(self, cr, user, node, view_id, context=None):
+ fields_def = self.__view_look_dom(cr, user, node, view_id, context=context)
+ node = self._disable_workflow_buttons(cr, user, node)
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
-
- fields={}
- if node.tag=='diagram':
- if node.getchildren()[0].tag=='node':
- node_fields=self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, fields_def.keys(), context)
- if node.getchildren()[1].tag=='arrow':
- arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, fields_def.keys(), context)
- for key,value in node_fields.items():
- fields[key]=value
- for key,value in arrow_fields.items():
- fields[key]=value
+ fields = {}
+ if node.tag == 'diagram':
+ if node.getchildren()[0].tag == 'node':
+ node_fields = self.pool.get(node.getchildren()[0].get('object')).fields_get(cr, user, fields_def.keys(), context)
+ if node.getchildren()[1].tag == 'arrow':
+ arrow_fields = self.pool.get(node.getchildren()[1].get('object')).fields_get(cr, user, fields_def.keys(), context)
+ for key, value in node_fields.items():
+ fields[key] = value
+ for key, value in arrow_fields.items():
+ fields[key] = value
else:
fields = self.fields_get(cr, user, fields_def.keys(), context)
for field in fields_def:
if field == 'id':
- # sometime, the view may containt the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
+ # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
elif field in fields:
fields[field].update(fields_def[field])
@@ -1360,7 +1449,7 @@ class orm_template(object):
model = res[0][1]
res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
msg = "\n * ".join([r[0] for r in res])
- msg += "\n\nEither you wrongly customised this view, or some modules bringing those views are not compatible with your current data model"
+ msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
netsvc.Logger().notifyChannel('orm', netsvc.LOG_ERROR, msg)
raise except_orm('View error', msg)
return arch, fields
@@ -1373,37 +1462,37 @@ class orm_template(object):
'\n'
' \n'
@@ -1421,7 +1510,7 @@ class orm_template(object):
view = self.fields_view_get(cr, uid, False, 'form', context=context)
root = etree.fromstring(encode(view['arch']))
- res = etree.XML(" " % root.get("string", ""))
+ res = etree.XML(""" """ % root.get("string", ""))
node = etree.Element("group")
res.append(node)
@@ -1471,6 +1560,14 @@ class orm_template(object):
else:
for n in node.getiterator(node2.tag):
res = True
+ if node2.tag == 'field':
+ # only compare field names, a field can be only once in a given view
+ # at a given level (and for multilevel expressions, we should use xpath
+ # inheritance spec anyway)
+ if node2.get('name') == n.get('name'):
+ return n
+ else:
+ continue
for attr in node2.attrib:
if attr == 'position':
continue
@@ -1485,7 +1582,7 @@ class orm_template(object):
# End: _find(node, node2)
doc_dest = etree.fromstring(encode(inherit))
- toparse = [ doc_dest ]
+ toparse = [doc_dest]
while len(toparse):
node2 = toparse.pop(0)
@@ -1628,7 +1725,7 @@ class orm_template(object):
else:
xml = '' # what happens here, graph case?
- raise except_orm(_('Invalid Architecture!'),_("There is no view of type '%s' defined for the structure!") % view_type)
+ 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['name'] = 'default'
result['field_parent'] = False
@@ -1639,13 +1736,13 @@ class orm_template(object):
result['fields'] = xfields
if submenu:
- if context and context.get('active_id',False):
+ if context and context.get('active_id', False):
data_menu = self.pool.get('ir.ui.menu').browse(cr, user, context['active_id'], context).action
if data_menu:
act_id = data_menu.id
if act_id:
data_action = self.pool.get('ir.actions.act_window').browse(cr, user, [act_id], context)[0]
- result['submenu'] = getattr(data_action,'menus', False)
+ result['submenu'] = getattr(data_action, 'menus', False)
if toolbar:
def clean(x):
x = x[2]
@@ -1672,7 +1769,7 @@ class orm_template(object):
resprint = filter(lambda x: not x.get('multi', False), resprint)
resrelate = map(lambda x: x[2], resrelate)
- for x in resprint+resaction+resrelate:
+ for x in resprint + resaction + resrelate:
x['string'] = x['name']
result['toolbar'] = {
@@ -1680,10 +1777,6 @@ class orm_template(object):
'action': resaction,
'relate': resrelate
}
- if result['type']=='form' and result['arch'].count("default_focus")>1:
- msg = "Form View contain more than one default_focus attribute"
- netsvc.Logger().notifyChannel('orm', netsvc.LOG_ERROR, msg)
- raise except_orm('View Error !',msg)
return result
_view_look_dom_arch = __view_look_dom_arch
@@ -1696,15 +1789,116 @@ class orm_template(object):
return len(res)
return res
- def search(self, cr, user, args, offset=0, limit=None, order=None,
- context=None, count=False):
+ def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
+ """
+ Search for records based on a search domain.
+
+ :param cr: database cursor
+ :param user: current user id
+ :param args: list of tuples specifying the search domain [('field_name', 'operator', value), ...]. Pass an empty list to match all records.
+ :param offset: optional number of results to skip in the returned values (default: 0)
+ :param limit: optional max number of records to return (default: **None**)
+ :param order: optional columns to sort by (default: self._order=id )
+ :param context: optional context arguments, like lang, time zone
+ :type context: dictionary
+ :param count: optional (default: **False**), if **True**, returns only the number of records matching the criteria, not their ids
+ :return: id or list of ids of records matching the criteria
+ :rtype: integer or list of integers
+ :raise AccessError: * if user tries to bypass access rules for read on the requested object.
+
+ **Expressing a search domain (args)**
+
+ Each tuple in the search domain needs to have 3 elements, in the form: **('field_name', 'operator', value)**, where:
+
+ * **field_name** must be a valid name of field of the object model, possibly following many-to-one relationships using dot-notation, e.g 'street' or 'partner_id.country' are valid values.
+ * **operator** must be a string with a valid comparison operator from this list: ``=, !=, >, >=, <, <=, like, ilike, in, not in, child_of, parent_left, parent_right``
+ The semantics of most of these operators are obvious.
+ The ``child_of`` operator will look for records who are children or grand-children of a given record,
+ according to the semantics of this model (i.e following the relationship field named by
+ ``self._parent_name``, by default ``parent_id``.
+ * **value** must be a valid value to compare with the values of **field_name**, depending on its type.
+
+ Domain criteria can be combined using 3 logical operators than can be added between tuples: '**&**' (logical AND, default), '**|**' (logical OR), '**!**' (logical NOT).
+ These are **prefix** operators and the arity of the '**&**' and '**|**' operator is 2, while the arity of the '**!**' is just 1.
+ Be very careful about this when you combine them the first time.
+
+ Here is an example of searching for Partners named *ABC* from Belgium and Germany whose language is not english ::
+
+ [('name','=','ABC'),'!',('language.code','=','en_US'),'|',('country_id.code','=','be'),('country_id.code','=','de'))
+
+ The '&' is omitted as it is the default, and of course we could have used '!=' for the language, but what this domain really represents is::
+
+ (name is 'ABC' AND (language is NOT english) AND (country is Belgium OR Germany))
+
+ """
+ return self._search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
+
+ def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
+ """
+ Private implementation of search() method, allowing specifying the uid to use for the access right check.
+ This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors,
+ by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules!
+
+ :param access_rights_uid: optional user ID to use when checking access rights
+ (not for ir.rules, this is only for ir.model.access)
+ """
raise NotImplementedError(_('The search method is not implemented on this object !'))
def name_get(self, cr, user, ids, context=None):
- raise NotImplementedError(_('The name_get method is not implemented on this object !'))
+ """
+
+ :param cr: database cursor
+ :param user: current user id
+ :type user: integer
+ :param ids: list of ids
+ :param context: context arguments, like lang, time zone
+ :type context: dictionary
+ :return: tuples with the text representation of requested objects for to-many relationships
+
+ """
+ if not context:
+ context = {}
+ if not ids:
+ return []
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ return [(r['id'], tools.ustr(r[self._rec_name])) for r in self.read(cr, user, ids,
+ [self._rec_name], context, load='_classic_write')]
def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
- raise NotImplementedError(_('The name_search method is not implemented on this object !'))
+ """
+ Search for records and their display names according to a search domain.
+
+ :param cr: database cursor
+ :param user: current user id
+ :param name: object name to search
+ :param args: list of tuples specifying search criteria [('field_name', 'operator', 'value'), ...]
+ :param operator: operator for search criterion
+ :param context: context arguments, like lang, time zone
+ :type context: dictionary
+ :param limit: optional max number of records to return
+ :return: list of object names matching the search criteria, used to provide completion for to-many relationships
+
+ This method is equivalent of :py:meth:`~osv.osv.osv.search` on **name** + :py:meth:`~osv.osv.osv.name_get` on the result.
+ See :py:meth:`~osv.osv.osv.search` for an explanation of the possible values for the search domain specified in **args**.
+
+ """
+ return self._name_search(cr, user, name, args, operator, context, limit)
+
+ # private implementation of name_search, allows passing a dedicated user for the name_get part to
+ # solve some access rights issues
+ def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
+ if args is None:
+ args = []
+ if context is None:
+ context = {}
+ args = args[:]
+ if name:
+ args += [(self._rec_name, operator, name)]
+ access_rights_uid = name_get_uid or user
+ ids = self._search(cr, user, args, limit=limit, context=context, access_rights_uid=access_rights_uid)
+ res = self.name_get(cr, access_rights_uid, ids, context)
+ return res
def copy(self, cr, uid, id, default=None, context=None):
raise NotImplementedError(_('The copy method is not implemented on this object !'))
@@ -1755,7 +1949,37 @@ class orm_template(object):
def _check_removed_columns(self, cr, log=False):
raise NotImplementedError()
+ def _add_missing_default_values(self, cr, uid, values, context=None):
+ missing_defaults = []
+ avoid_tables = [] # avoid overriding inherited values when parent is set
+ for tables, parent_field in self._inherits.items():
+ if parent_field in values:
+ avoid_tables.append(tables)
+ for field in self._columns.keys():
+ if not field in values:
+ missing_defaults.append(field)
+ for field in self._inherit_fields.keys():
+ if (field not in values) and (self._inherit_fields[field][0] not in avoid_tables):
+ missing_defaults.append(field)
+
+ if len(missing_defaults):
+ # override defaults with the provided values, never allow the other way around
+ defaults = self.default_get(cr, uid, missing_defaults, context)
+ for dv in defaults:
+ if (dv in self._columns and self._columns[dv]._type == 'many2many') \
+ or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'many2many') \
+ and defaults[dv] and isinstance(defaults[dv][0], (int, long)):
+ defaults[dv] = [(6, 0, defaults[dv])]
+ if dv in self._columns and self._columns[dv]._type == 'one2many' \
+ or (dv in self._inherit_fields and self._inherit_fields[dv][2]._type == 'one2many') \
+ and isinstance(defaults[dv], (list, tuple)) and isinstance(defaults[dv][0], dict):
+ defaults[dv] = [(0, 0, x) for x in defaults[dv]]
+ defaults.update(values)
+ values = defaults
+ return values
+
class orm_memory(orm_template):
+
_protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
_inherit_fields = {}
_max_count = 200
@@ -1771,7 +1995,7 @@ class orm_memory(orm_template):
def _check_access(self, uid, object_id, mode):
if uid != 1 and self.datas[object_id]['internal.create_uid'] != uid:
- raise except_orm(_('AccessError'), '%s access is only allowed on your own records for osv_memory objects' % mode.capitalize())
+ raise except_orm(_('AccessError'), '%s access is only allowed on your own records for osv_memory objects except for the super-user' % mode.capitalize())
def vaccum(self, cr, uid):
self.check_id += 1
@@ -1783,7 +2007,7 @@ class orm_memory(orm_template):
if self.datas[id]['internal.date_access'] < max:
tounlink.append(id)
self.unlink(cr, 1, tounlink)
- if len(self.datas)>self._max_count:
+ if len(self.datas) > self._max_count:
sorted = map(lambda x: (x[1]['internal.date_access'], x[0]), self.datas.items())
sorted.sort()
ids = map(lambda x: x[1], sorted[:len(self.datas)-self._max_count])
@@ -1846,12 +2070,9 @@ class orm_memory(orm_template):
self.vaccum(cr, user)
self.next_id += 1
id_new = self.next_id
- default = []
- for f in self._columns.keys():
- if not f in vals:
- default.append(f)
- if len(default):
- vals.update(self.default_get(cr, user, default, context))
+
+ vals = self._add_missing_default_values(cr, user, vals, context)
+
vals2 = {}
upd_todo = []
for field in vals:
@@ -1876,71 +2097,11 @@ class orm_memory(orm_template):
wf_service.trg_create(user, self._name, id_new, cr)
return id_new
- def default_get(self, cr, uid, fields_list, context=None):
- self.view_init(cr, uid, fields_list, context)
- if not context:
- context = {}
- # we define default value for each field existing in the object
- value = dict.fromkeys(fields_list, False)
- # get the default values for the inherited fields
- for f in fields_list:
- if f in self._defaults:
- if callable(self._defaults[f]):
- value[f] = self._defaults[f](self, cr, uid, context)
- else:
- value[f] = self._defaults[f]
-
- fld_def = ((f in self._columns) and self._columns[f]) \
- or ((f in self._inherit_fields) and self._inherit_fields[f][2]) \
- or False
-
- # get the default values set by the user and override the default
- # values defined in the object
- ir_values_obj = self.pool.get('ir.values')
- res = ir_values_obj.get(cr, uid, 'default', False, [self._name])
- for id, field, field_value in res:
- if field in fields_list:
- fld_def = (field in self._columns) and self._columns[field] or self._inherit_fields[field][2]
- if fld_def._type in ('many2one', 'one2one'):
- obj = self.pool.get(fld_def._obj)
- if not obj.search(cr, uid, [('id', '=', field_value)]):
- continue
- if fld_def._type in ('many2many'):
- obj = self.pool.get(fld_def._obj)
- field_value2 = []
- for i in range(len(field_value)):
- if not obj.search(cr, uid, [('id', '=',
- field_value[i])]):
- continue
- field_value2.append(field_value[i])
- field_value = field_value2
- if fld_def._type in ('one2many'):
- obj = self.pool.get(fld_def._obj)
- field_value2 = []
- for i in range(len(field_value)):
- field_value2.append({})
- for field2 in field_value[i]:
- if obj._columns[field2]._type in ('many2one', 'one2one'):
- obj2 = self.pool.get(obj._columns[field2]._obj)
- if not obj2.search(cr, uid,
- [('id', '=', field_value[i][field2])]):
- continue
- # TODO add test for many2many and one2many
- field_value2[i][field2] = field_value[i][field2]
- field_value = field_value2
- value[field] = field_value
-
- # get the default values from the context
- for key in context or {}:
- if key.startswith('default_') and (key[8:] in fields_list):
- value[key[8:]] = context[key]
- return value
-
def _where_calc(self, cr, user, args, active_test=True, context=None):
if not context:
context = {}
args = args[:]
- res=[]
+ res = []
# if the object has a field named 'active', filter out all inactive
# records unless they were explicitely asked for
if 'active' in self._columns and (active_test and context.get('active_test', True)):
@@ -1960,27 +2121,27 @@ class orm_memory(orm_template):
res = e.exp
return res or []
- def search(self, cr, user, args, offset=0, limit=None, order=None,
- context=None, count=False):
+ def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
if not context:
context = {}
- # implicit filter on current user
- if not args:
- args = []
- args.insert(0, ('internal.create_uid', '=', user))
+ # implicit filter on current user except for superuser
+ if user != 1:
+ if not args:
+ args = []
+ args.insert(0, ('internal.create_uid', '=', user))
result = self._where_calc(cr, user, args, context=context)
- if result==[]:
+ if result == []:
return self.datas.keys()
- res=[]
- counter=0
+ res = []
+ counter = 0
#Find the value of dict
- f=False
+ f = False
if result:
for id, data in self.datas.items():
- counter=counter+1
+ counter = counter + 1
data['id'] = id
if limit and (counter > int(limit)):
break
@@ -1988,7 +2149,7 @@ class orm_memory(orm_template):
for arg in result:
if arg[1] == '=':
val = eval('data[arg[0]]'+'==' +' arg[2]', locals())
- elif arg[1] in ['<','>','in','not in','<=','>=','<>']:
+ elif arg[1] in ['<', '>', 'in', 'not in', '<=', '>=', '<>']:
val = eval('data[arg[0]]'+arg[1] +' arg[2]', locals())
elif arg[1] in ['ilike']:
val = (str(data[arg[0]]).find(str(arg[2]))!=-1)
@@ -2020,7 +2181,8 @@ class orm_memory(orm_template):
'create_date': create_date,
'write_uid': False,
'write_date': False,
- 'id': id
+ 'id': id,
+ 'xmlid' : False,
})
return result
@@ -2034,8 +2196,9 @@ class orm_memory(orm_template):
class orm(orm_template):
_sql_constraints = []
_table = None
- _protected = ['read','write','create','default_get','perm_read','unlink','fields_get','fields_view_get','search','name_get','distinct_field_get','name_search','copy','import_data','search_count', 'exists']
-
+ _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
+ __logger = logging.getLogger('orm')
+ __schema = logging.getLogger('orm.schema')
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None):
"""
Get the list of records in list view grouped by the given ``groupby`` fields
@@ -2064,109 +2227,127 @@ class orm(orm_template):
if not fields:
fields = self._columns.keys()
- (where_clause, where_params, tables) = self._where_calc(cr, uid, domain, context=context)
- dom = self.pool.get('ir.rule').domain_get(cr, uid, self._name, 'read', context=context)
- where_clause = where_clause + dom[0]
- where_params = where_params + dom[1]
- for t in dom[2]:
- if t not in tables:
- tables.append(t)
+ query = self._where_calc(cr, uid, domain, context=context)
+ self._apply_ir_rules(cr, uid, query, 'read', context=context)
# Take care of adding join(s) if groupby is an '_inherits'ed field
groupby_list = groupby
if groupby:
- if groupby and isinstance(groupby, list):
+ if isinstance(groupby, list):
groupby = groupby[0]
- tables, where_clause = self._inherits_join_calc(groupby,tables,where_clause)
+ self._inherits_join_calc(groupby, query)
- if len(where_clause):
- where_clause = ' where '+string.join(where_clause, ' and ')
- else:
- where_clause = ''
- limit_str = limit and ' limit %d' % limit or ''
- offset_str = offset and ' offset %d' % offset or ''
+ assert not groupby or groupby in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)"
fget = self.fields_get(cr, uid, fields)
- float_int_fields = filter(lambda x: fget[x]['type'] in ('float','integer'), fields)
- sum = {}
-
+ float_int_fields = filter(lambda x: fget[x]['type'] in ('float', 'integer'), fields)
flist = ''
group_by = groupby
if groupby:
- if fget.get(groupby,False) and fget[groupby]['type'] in ('date','datetime'):
- flist = "to_char(%s,'yyyy-mm') as %s "%(groupby,groupby)
- groupby = "to_char(%s,'yyyy-mm')"%(groupby)
+ if fget.get(groupby):
+ if fget[groupby]['type'] in ('date', 'datetime'):
+ flist = "to_char(%s,'yyyy-mm') as %s " % (groupby, groupby)
+ groupby = "to_char(%s,'yyyy-mm')" % (groupby)
+ else:
+ flist = groupby
else:
- flist = groupby
+ # Don't allow arbitrary values, as this would be a SQL injection vector!
+ raise except_orm(_('Invalid group_by'),
+ _('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.')%(groupby,))
fields_pre = [f for f in float_int_fields if
f == self.CONCURRENCY_CHECK_FIELD
or (f in self._columns and getattr(self._columns[f], '_classic_write'))]
for f in fields_pre:
- if f not in ['id','sequence']:
- operator = fget[f].get('group_operator','sum')
+ if f not in ['id', 'sequence']:
+ group_operator = fget[f].get('group_operator', 'sum')
if flist:
flist += ','
- flist += operator+'('+f+') as '+f
+ flist += group_operator+'('+f+') as '+f
- if groupby:
- gb = ' group by '+groupby
- else:
- gb = ''
- cr.execute('select min(%s.id) as id,' % self._table + flist + ' from ' + ','.join(tables) + where_clause + gb + limit_str + offset_str, where_params)
+ gb = groupby and (' GROUP BY '+groupby) or ''
+
+ from_clause, where_clause, where_clause_params = query.get_sql()
+ where_clause = where_clause and ' WHERE ' + where_clause
+ limit_str = limit and ' limit %d' % limit or ''
+ offset_str = offset and ' offset %d' % offset or ''
+ cr.execute('SELECT min(%s.id) AS id,' % self._table + flist + ' FROM ' + from_clause + where_clause + gb + limit_str + offset_str, where_clause_params)
alldata = {}
groupby = group_by
for r in cr.dictfetchall():
- for fld,val in r.items():
- if val == None:r[fld] = False
+ for fld, val in r.items():
+ if val == None: r[fld] = False
alldata[r['id']] = r
del r['id']
- data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context)
- today = datetime.date.today()
+ if groupby and fget[groupby]['type'] == 'many2one':
+ data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=groupby, context=context)
+ # the IDS of the records that has groupby field value = False or ''
+ # should be added too
+ data_ids += filter(lambda x:x not in data_ids, alldata.keys())
+ data = self.read(cr, uid, data_ids, groupby and [groupby] or ['id'], context=context)
+ # restore order of the search as read() uses the default _order (this is only for groups, so the size of data_read shoud be small):
+ data.sort(lambda x,y: cmp(data_ids.index(x['id']), data_ids.index(y['id'])))
+ else:
+ data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context)
+ if groupby:
+ data.sort(lambda x,y:cmp(x[groupby],y[groupby]))
for d in data:
if groupby:
- d['__domain'] = [(groupby,'=',alldata[d['id']][groupby] or False)] + domain
- if not isinstance(groupby_list,(str, unicode)):
+ d['__domain'] = [(groupby, '=', alldata[d['id']][groupby] or False)] + domain
+ if not isinstance(groupby_list, (str, unicode)):
if groupby or not context.get('group_by_no_leaf', False):
- d['__context'] = {'group_by':groupby_list[1:]}
- if groupby and fget.has_key(groupby):
- if d[groupby] and fget[groupby]['type'] in ('date','datetime'):
- dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7],'%Y-%m')
- days = calendar.monthrange(dt.year, dt.month)[1]
+ d['__context'] = {'group_by': groupby_list[1:]}
+ if groupby and groupby in fget:
+ if d[groupby] and fget[groupby]['type'] in ('date', 'datetime'):
+ dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7], '%Y-%m')
+ days = calendar.monthrange(dt.year, dt.month)[1]
- d[groupby] = datetime.datetime.strptime(d[groupby][:10],'%Y-%m-%d').strftime('%B %Y')
- d['__domain'] = [(groupby,'>=',alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01','%Y-%m-%d').strftime('%Y-%m-%d') or False),\
- (groupby,'<=',alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days),'%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain
+ d[groupby] = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d').strftime('%B %Y')
+ d['__domain'] = [(groupby, '>=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01', '%Y-%m-%d').strftime('%Y-%m-%d') or False),\
+ (groupby, '<=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days), '%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain
del alldata[d['id']][groupby]
d.update(alldata[d['id']])
del d['id']
return data
- def _inherits_join_calc(self, field, tables, where_clause):
+ def _inherits_join_add(self, parent_model_name, query):
"""
- Adds missing table select and join clause(s) for reaching
- the field coming from an '_inherits' parent table.
+ Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
- :param tables: list of table._table names enclosed in double quotes as returned
- by _where_calc()
+ :param parent_model_name: name of the parent model for which the clauses should be added
+ :param query: query object on which the JOIN should be added
+ """
+ inherits_field = self._inherits[parent_model_name]
+ parent_model = self.pool.get(parent_model_name)
+ parent_table_name = parent_model._table
+ quoted_parent_table_name = '"%s"' % parent_table_name
+ if quoted_parent_table_name not in query.tables:
+ query.tables.append(quoted_parent_table_name)
+ query.where_clause.append('("%s".%s = %s.id)' % (self._table, inherits_field, parent_table_name))
+ def _inherits_join_calc(self, field, query):
+ """
+ Adds missing table select and join clause(s) to ``query`` for reaching
+ the field coming from an '_inherits' parent table (no duplicates).
+
+ :param field: name of inherited field to reach
+ :param query: query object on which the JOIN should be added
+ :return: qualified name of field, to be used in SELECT clause
"""
current_table = self
while field in current_table._inherit_fields and not field in current_table._columns:
- parent_table = self.pool.get(current_table._inherit_fields[field][0])
- parent_table_name = parent_table._table
- if '"%s"'%parent_table_name not in tables:
- tables.append('"%s"'%parent_table_name)
- where_clause.append('(%s.%s = %s.id)' % (current_table._table, current_table._inherits[parent_table._name], parent_table_name))
+ parent_model_name = current_table._inherit_fields[field][0]
+ parent_table = self.pool.get(parent_model_name)
+ self._inherits_join_add(parent_model_name, query)
current_table = parent_table
- return (tables, where_clause)
+ return '"%s".%s' % (current_table._table, field)
def _parent_store_compute(self, cr):
if not self._parent_store:
return
logger = netsvc.Logger()
- logger.notifyChannel('orm', netsvc.LOG_INFO, 'Computing parent left and right for table %s...' % (self._table, ))
+ logger.notifyChannel('data', netsvc.LOG_INFO, 'Computing parent left and right for table %s...' % (self._table, ))
def browse_rec(root, pos=0):
# TODO: set order
where = self._parent_name+'='+str(root)
@@ -2176,14 +2357,13 @@ class orm(orm_template):
where += ' order by '+self._parent_order
cr.execute('SELECT id FROM '+self._table+' WHERE '+where)
pos2 = pos + 1
- childs = cr.fetchall()
- for id in childs:
+ for id in cr.fetchall():
pos2 = browse_rec(id[0], pos2)
- cr.execute('update '+self._table+' set parent_left=%s, parent_right=%s where id=%s', (pos,pos2,root))
- return pos2+1
+ cr.execute('update '+self._table+' set parent_left=%s, parent_right=%s where id=%s', (pos, pos2, root))
+ return pos2 + 1
query = 'SELECT id FROM '+self._table+' WHERE '+self._parent_name+' IS NULL'
if self._parent_order:
- query += ' order by '+self._parent_order
+ query += ' order by ' + self._parent_order
pos = 0
cr.execute(query)
for (root,) in cr.fetchall():
@@ -2192,7 +2372,7 @@ class orm(orm_template):
def _update_store(self, cr, f, k):
logger = netsvc.Logger()
- logger.notifyChannel('orm', netsvc.LOG_INFO, "storing computed values of fields.function '%s'" % (k,))
+ logger.notifyChannel('data', netsvc.LOG_INFO, "storing computed values of fields.function '%s'" % (k,))
ss = self._columns[k]._symbol_set
update_query = 'UPDATE "%s" SET "%s"=%s WHERE id=%%s' % (self._table, k, ss[0])
cr.execute('select id from '+self._table)
@@ -2201,17 +2381,16 @@ class orm(orm_template):
iids = ids_lst[:40]
ids_lst = ids_lst[40:]
res = f.get(cr, self, iids, k, 1, {})
- for key,val in res.items():
+ for key, val in res.items():
if f._multi:
val = val[k]
# if val is a many2one, just write the ID
- if type(val)==tuple:
+ if type(val) == tuple:
val = val[0]
if (val<>False) or (type(val)<>bool):
cr.execute(update_query, (ss[1](val), key))
def _check_removed_columns(self, cr, log=False):
- logger = netsvc.Logger()
# iterate on the database columns to drop the NOT NULL constraints
# of fields which were required but have been removed (or will be added by another module)
columns = [c for c in self._columns if not (isinstance(self._columns[c], fields.function) and not self._columns[c].store)]
@@ -2222,28 +2401,32 @@ class orm(orm_template):
" AND c.oid=a.attrelid"
" AND a.attisdropped=%s"
" AND pg_catalog.format_type(a.atttypid, a.atttypmod) NOT IN ('cid', 'tid', 'oid', 'xid')"
- " AND a.attname NOT IN %s" ,(self._table, False, tuple(columns))),
+ " AND a.attname NOT IN %s", (self._table, False, tuple(columns))),
for column in cr.dictfetchall():
if log:
- logger.notifyChannel("orm", netsvc.LOG_DEBUG, "column %s is in the table %s but not in the corresponding object %s" % (column['attname'], self._table, self._name))
+ self.__logger.debug("column %s is in the table %s but not in the corresponding object %s",
+ column['attname'], self._table, self._name)
if column['attnotnull']:
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, column['attname']))
+ self.__schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
+ self._table, column['attname'])
def _auto_init(self, cr, context=None):
if context is None:
context = {}
- store_compute = False
- logger = netsvc.Logger()
+ store_compute = False
create = False
todo_end = []
self._field_create(cr, context=context)
if getattr(self, '_auto', True):
- cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s" ,( self._table,))
+ cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,))
if not cr.rowcount:
cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id)) WITHOUT OIDS' % (self._table,))
- cr.execute("COMMENT ON TABLE \"%s\" IS '%s'" % (self._table, self._description.replace("'","''")))
+ cr.execute("COMMENT ON TABLE \"%s\" IS '%s'" % (self._table, self._description.replace("'", "''")))
create = True
+ self.__schema.debug("Table '%s': created", self._table)
+
cr.commit()
if self._parent_store:
cr.execute("""SELECT c.relname
@@ -2251,14 +2434,22 @@ class orm(orm_template):
WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid
""", (self._table, 'parent_left'))
if not cr.rowcount:
- if 'parent_left' not in self._columns:
- logger.notifyChannel('orm', netsvc.LOG_ERROR, 'create a column parent_left on object %s: fields.integer(\'Left Parent\', select=1)' % (self._table, ))
- if 'parent_right' not in self._columns:
- logger.notifyChannel('orm', netsvc.LOG_ERROR, 'create a column parent_right on object %s: fields.integer(\'Right Parent\', select=1)' % (self._table, ))
- if self._columns[self._parent_name].ondelete != 'cascade':
- logger.notifyChannel('orm', netsvc.LOG_ERROR, "The column %s on object %s must be set as ondelete='cascade'" % (self._parent_name, self._name))
cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_left" INTEGER' % (self._table,))
cr.execute('ALTER TABLE "%s" ADD COLUMN "parent_right" INTEGER' % (self._table,))
+ if 'parent_left' not in self._columns:
+ self.__logger.error('create a column parent_left on object %s: fields.integer(\'Left Parent\', select=1)',
+ self._table)
+ self.__schema.debug("Table '%s': added column '%s' with definition=%s",
+ self._table, 'parent_left', 'INTEGER')
+ if 'parent_right' not in self._columns:
+ self.__logger.error('create a column parent_right on object %s: fields.integer(\'Right Parent\', select=1)',
+ self._table)
+ self.__schema.debug("Table '%s': added column '%s' with definition=%s",
+ self._table, 'parent_right', 'INTEGER')
+ if self._columns[self._parent_name].ondelete != 'cascade':
+ self.__logger.error("The column %s on object %s must be set as ondelete='cascade'",
+ self._parent_name, self._name)
+
cr.commit()
store_compute = True
@@ -2278,19 +2469,30 @@ class orm(orm_template):
if not cr.rowcount:
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, logs[k]))
cr.commit()
+ self.__schema.debug("Table '%s': added column '%s' with definition=%s",
+ self._table, k, logs[k])
self._check_removed_columns(cr, log=False)
# iterate on the "object columns"
todo_update_store = []
update_custom_fields = context.get('update_custom_fields', False)
+
+ 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 c.oid=a.attrelid " \
+ "AND a.atttypid=t.oid", (self._table,))
+ col_data = dict(map(lambda x: (x['attname'], x),cr.dictfetchall()))
+
+
for k in self._columns:
if k in ('id', 'write_uid', 'write_date', 'create_uid', 'create_date'):
continue
- #raise _('Can not define a column %s. Reserved keyword !') % (k,)
#Not Updating Custom fields
if k.startswith('x_') and not update_custom_fields:
continue
+
f = self._columns[k]
if isinstance(f, fields.one2many):
@@ -2299,13 +2501,15 @@ class orm(orm_template):
if self.pool.get(f._obj):
if f._fields_id not in self.pool.get(f._obj)._columns.keys():
if not self.pool.get(f._obj)._inherits or (f._fields_id not in self.pool.get(f._obj)._inherit_fields.keys()):
- raise except_orm('Programming Error', ("There is no reference field '%s' found for '%s'") % (f._fields_id,f._obj,))
+ raise except_orm('Programming Error', ("There is no reference field '%s' found for '%s'") % (f._fields_id, f._obj,))
if cr.fetchone():
cr.execute("SELECT count(1) as c FROM pg_class c,pg_attribute a WHERE c.relname=%s AND a.attname=%s AND c.oid=a.attrelid", (f._obj, f._fields_id))
res = cr.fetchone()[0]
if not res:
cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY (%s) REFERENCES "%s" ON DELETE SET NULL' % (self._obj, f._fields_id, f._table))
+ self.__schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE SET NULL",
+ self._obj, f._fields_id, f._table)
elif isinstance(f, fields.many2many):
cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (f._rel,))
if not cr.dictfetchall():
@@ -2318,15 +2522,12 @@ class orm(orm_template):
cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (f._rel, f._id2, f._rel, f._id2))
cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (f._rel, self._table, ref))
cr.commit()
+ self.__schema.debug("Create table '%s': relation between '%s' and '%s'",
+ f._rel, self._table, ref)
else:
- 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, k))
- res = cr.dictfetchall()
- if not res and hasattr(f,'oldname'):
+ res = col_data.get(k, [])
+ res = res and [res] or []
+ 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 " \
@@ -2334,23 +2535,26 @@ class orm(orm_template):
"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))
+ 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
+ self.__schema.debug("Table '%s': renamed column '%s' to '%s'",
+ self._table, f.oldname, k)
-
- if len(res)==1:
+ if len(res) == 1:
f_pg_def = res[0]
f_pg_type = f_pg_def['typname']
f_pg_size = f_pg_def['size']
f_pg_notnull = f_pg_def['attnotnull']
if isinstance(f, fields.function) and not f.store and\
not getattr(f, 'nodrop', False):
- 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" CASCADE'% (self._table, k))
+ self.__logger.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" CASCADE' % (self._table, k))
cr.commit()
+ self.__schema.debug("Table '%s': dropped column '%s' with cascade",
+ self._table, k)
f_obj_type = None
else:
f_obj_type = get_pg_type(f) and get_pg_type(f)[0]
@@ -2367,23 +2571,24 @@ class orm(orm_template):
('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]),
]
if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size < f.size:
- logger.notifyChannel('orm', netsvc.LOG_INFO, "column '%s' in table '%s' changed size" % (k, self._table))
cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" VARCHAR(%d)' % (self._table, k, f.size))
cr.execute('UPDATE "%s" SET "%s"=temp_change_size::VARCHAR(%d)' % (self._table, k, f.size))
cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
cr.commit()
+ self.__schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s",
+ self._table, k, f_pg_size, f.size)
for c in casts:
if (f_pg_type==c[0]) and (f._type==c[1]):
if f_pg_type != f_obj_type:
- if f_pg_type != f_obj_type:
- logger.notifyChannel('orm', netsvc.LOG_INFO, "column '%s' in table '%s' changed type to %s." % (k, self._table, c[1]))
ok = True
cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k))
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, c[2]))
cr.execute(('UPDATE "%s" SET "%s"=temp_change_size'+c[3]) % (self._table, k))
cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,))
cr.commit()
+ self.__schema.debug("Table '%s': column '%s' changed type from %s to %s",
+ self._table, k, c[0], c[1])
break
if f_pg_type != f_obj_type:
@@ -2397,13 +2602,14 @@ class orm(orm_template):
"AND c.oid=a.attrelid ", (self._table, newname))
if not cr.fetchone()[0]:
break
- i+=1
- logger.notifyChannel('orm', netsvc.LOG_WARNING, "column '%s' in table '%s' has changed type (DB=%s, def=%s), data moved to table %s !" % (k, self._table, f_pg_type, f._type, newname))
+ i += 1
if f_pg_notnull:
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k))
cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (self._table, k, newname))
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
- cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'","''")))
+ cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'", "''")))
+ self.__schema.debug("Table '%s': column '%s' has changed type (DB=%s, def=%s), data moved to column %s !",
+ self._table, k, f_pg_type, f._type, newname)
# if the field is required and hasn't got a NOT NULL constraint
if f.required and f_pg_notnull == 0:
@@ -2423,21 +2629,39 @@ class orm(orm_template):
try:
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k))
cr.commit()
+ self.__schema.debug("Table '%s': column '%s': added NOT NULL constraint",
+ self._table, k)
except Exception:
- logger.notifyChannel('orm', netsvc.LOG_WARNING, 'unable to set a NOT NULL constraint on column %s of the %s table !\nIf you want to have it, you should update the records and execute manually:\nALTER TABLE %s ALTER COLUMN %s SET NOT NULL' % (k, self._table, self._table, k))
+ msg = "Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
+ "If you want to have it, you should update the records and execute manually:\n"\
+ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL"
+ self.__schema.warn(msg, self._table, k, self._table, k)
cr.commit()
elif not f.required and f_pg_notnull == 1:
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" DROP NOT NULL' % (self._table, k))
cr.commit()
+ self.__schema.debug("Table '%s': column '%s': dropped NOT NULL constraint",
+ self._table, k)
+ # Verify index
indexname = '%s_%s_index' % (self._table, k)
cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = %s and tablename = %s", (indexname, self._table))
res2 = cr.dictfetchall()
if not res2 and f.select:
cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k))
cr.commit()
+ if f._type == 'text':
+ # FIXME: for fields.text columns we should try creating GIN indexes instead (seems most suitable for an ERP context)
+ msg = "Table '%s': Adding (b-tree) index for text column '%s'."\
+ "This is probably useless (does not work for fulltext search) and prevents INSERTs of long texts"\
+ " because there is a length limit for indexable btree values!\n"\
+ "Use a search view instead if you simply want to make the field searchable."
+ self.__schema.warn(msg, self._table, k, f._type)
if res2 and not f.select:
cr.execute('DROP INDEX "%s_%s_index"' % (self._table, k))
cr.commit()
+ msg = "Table '%s': dropping index for column '%s' of type '%s' as it is not required anymore"
+ self.__schema.warn(msg, self._table, k, f._type)
+
if isinstance(f, fields.many2one):
ref = self.pool.get(f._obj)._table
if ref != 'ir_actions':
@@ -2462,14 +2686,17 @@ class orm(orm_template):
cr.execute('ALTER TABLE "' + self._table + '" DROP CONSTRAINT "' + res2[0]['conname'] + '"')
cr.execute('ALTER TABLE "' + self._table + '" ADD FOREIGN KEY ("' + k + '") REFERENCES "' + ref + '" ON DELETE ' + f.ondelete)
cr.commit()
- elif len(res)>1:
- logger.notifyChannel('orm', netsvc.LOG_ERROR, "Programming error, column %s->%s has multiple instances !"%(self._table,k))
+ self.__schema.debug("Table '%s': column '%s': XXX",
+ self._table, k)
+ elif len(res) > 1:
+ netsvc.Logger().notifyChannel('orm', netsvc.LOG_ERROR, "Programming error, column %s->%s has multiple instances !" % (self._table, k))
if not res:
if not isinstance(f, fields.function) or f.store:
-
# add the missing field
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, get_pg_type(f)[1]))
- cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'","''")))
+ cr.execute("COMMENT ON COLUMN %s.%s IS '%s'" % (self._table, k, f.string.replace("'", "''")))
+ self.__schema.debug("Table '%s': added column '%s' with definition=%s",
+ self._table, k, get_pg_type(f)[1])
# initialize it
if not create and k in self._defaults:
@@ -2482,15 +2709,13 @@ class orm(orm_template):
query = 'UPDATE "%s" SET "%s"=%s' % (self._table, k, ss[0])
cr.execute(query, (ss[1](default),))
cr.commit()
- logger.notifyChannel('orm', netsvc.LOG_DEBUG, 'setting default value of new column %s of table %s'% (k, self._table))
- elif not create:
- logger.notifyChannel('orm', netsvc.LOG_DEBUG, 'creating new column %s of table %s'% (k, self._table))
+ netsvc.Logger().notifyChannel('data', netsvc.LOG_DEBUG, "Table '%s': setting default value of new column %s" % (self._table, k))
if isinstance(f, fields.function):
order = 10
if f.store is not True:
order = f.store[f.store.keys()[0]][2]
- todo_update_store.append((order, f,k))
+ todo_update_store.append((order, f, k))
# and add constraints if needed
if isinstance(f, fields.many2one):
@@ -2501,16 +2726,24 @@ class orm(orm_template):
# ir_actions is inherited so foreign key doesn't work on it
if ref != 'ir_actions':
cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (self._table, k, ref, f.ondelete))
+ self.__schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s",
+ self._table, k, ref, f.ondelete)
if f.select:
cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k))
if f.required:
try:
cr.commit()
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k))
+ self.__schema.debug("Table '%s': column '%s': added a NOT NULL constraint",
+ self._table, k)
except Exception:
- logger.notifyChannel('orm', netsvc.LOG_WARNING, 'WARNING: unable to set column %s of table %s not null !\nTry to re-run: openerp-server.py --update=module\nIf it doesn\'t work, update records and execute manually:\nALTER TABLE %s ALTER COLUMN %s SET NOT NULL' % (k, self._table, self._table, k))
+ msg = "WARNING: unable to set column %s of table %s not null !\n"\
+ "Try to re-run: openerp-server.py --update=module\n"\
+ "If it doesn't work, update records and execute manually:\n"\
+ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL"
+ self.__logger.warn(msg, k, self._table, self._table, k)
cr.commit()
- 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)))
else:
@@ -2521,14 +2754,50 @@ class orm(orm_template):
for (key, con, _) in self._sql_constraints:
conname = '%s_%s' % (self._table, key)
- cr.execute("SELECT conname FROM pg_constraint where conname=%s", (conname,))
- if not cr.dictfetchall():
- query = 'ALTER TABLE "%s" ADD CONSTRAINT "%s" %s' % (self._table, conname, con,)
+
+ cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,))
+ existing_constraints = cr.dictfetchall()
+
+ sql_actions = {
+ 'drop': {
+ 'execute': False,
+ 'query': 'ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (self._table, conname, ),
+ 'msg_ok': "Table '%s': dropped constraint '%s'. Reason: its definition changed from '%%s' to '%s'" % (
+ self._table, conname, con),
+ 'msg_err': "Table '%s': unable to drop \'%s\' constraint !" % (self._table, con),
+ 'order': 1,
+ },
+ 'add': {
+ 'execute': False,
+ 'query': 'ALTER TABLE "%s" ADD CONSTRAINT "%s" %s' % (self._table, conname, con,),
+ 'msg_ok': "Table '%s': added constraint '%s' with definition=%s" % (self._table, conname, con),
+ 'msg_err': "Table '%s': unable to add \'%s\' constraint !\n If you want to have it, you should update the records and execute manually:\n%%s" % (
+ self._table, con),
+ 'order': 2,
+ },
+ }
+
+ if not existing_constraints:
+ # constraint does not exists:
+ sql_actions['add']['execute'] = True
+ sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
+ elif con.lower() not in [item['condef'].lower() for item in existing_constraints]:
+ # constraint exists but its definition has changed:
+ sql_actions['drop']['execute'] = True
+ sql_actions['drop']['msg_ok'] = sql_actions['drop']['msg_ok'] % (existing_constraints[0]['condef'].lower(), )
+ sql_actions['add']['execute'] = True
+ sql_actions['add']['msg_err'] = sql_actions['add']['msg_err'] % (sql_actions['add']['query'], )
+
+ # we need to add the constraint:
+ sql_actions = [item for item in sql_actions.values()]
+ sql_actions.sort(key=lambda x: x['order'])
+ for sql_action in [action for action in sql_actions if action['execute']]:
try:
- cr.execute(query)
+ cr.execute(sql_action['query'])
cr.commit()
+ self.__schema.debug(sql_action['msg_ok'])
except:
- logger.notifyChannel('orm', netsvc.LOG_WARNING, 'unable to add \'%s\' constraint on table %s !\n If you want to have it, you should update the records and execute manually:\n%s' % (con, self._table, query))
+ self.__schema.warn(sql_action['msg_err'])
cr.rollback()
if create:
@@ -2560,27 +2829,27 @@ class orm(orm_template):
if not f.store:
continue
if self._columns[store_field].store is True:
- sm = {self._name:(lambda self,cr, uid, ids, c={}: ids, None, 10, None)}
+ sm = {self._name: (lambda self, cr, uid, ids, c={}: ids, None, 10, None)}
else:
sm = self._columns[store_field].store
for object, aa in sm.items():
- if len(aa)==4:
- (fnct,fields2,order,length)=aa
- elif len(aa)==3:
- (fnct,fields2,order)=aa
+ if len(aa) == 4:
+ (fnct, fields2, order, length) = aa
+ elif len(aa) == 3:
+ (fnct, fields2, order) = aa
length = None
else:
raise except_orm('Error',
('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.' % (store_field, self._name)))
self.pool._store_function.setdefault(object, [])
ok = True
- for x,y,z,e,f,l in self.pool._store_function[object]:
+ for x, y, z, e, f, l in self.pool._store_function[object]:
if (x==self._name) and (y==store_field) and (e==fields2):
- if f==order:
+ if f == order:
ok = False
if ok:
self.pool._store_function[object].append( (self._name, store_field, fnct, fields2, order, length))
- self.pool._store_function[object].sort(lambda x,y: cmp(x[4],y[4]))
+ self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4]))
for (key, _, msg) in self._sql_constraints:
self.pool._sql_error[self._table+'_'+key] = msg
@@ -2615,108 +2884,19 @@ class orm(orm_template):
elif field['ttype'] == 'many2many':
_rel1 = field['relation'].replace('.', '_')
_rel2 = field['model'].replace('.', '_')
- _rel_name = 'x_%s_%s_%s_rel' %(_rel1, _rel2, field['name'])
+ _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
self._columns[field['name']] = getattr(fields, field['ttype'])(field['relation'], _rel_name, 'id1', 'id2', **attrs)
else:
self._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
-
+ self._inherits_check()
self._inherits_reload()
if not self._sequence:
- self._sequence = self._table+'_id_seq'
+ self._sequence = self._table + '_id_seq'
for k in self._defaults:
assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,)
for f in self._columns:
self._columns[f].restart()
- def default_get(self, cr, uid, fields_list, context=None):
- """
- To Get default field values of given fields list of the model
-
- :param cr: database cursor
- :param uid: current user id
- :param fields_list: list of fields to get the default value
- :type fields_list: list (example ['field1', 'field2',])
- :param context: context arguments, like lang, time zone
- :return: dictionary of the default values for fields (set on the object class, by the user preferences, or via the context)
-
- """
- if not context:
- context = {}
- value = {}
- # get the default values for the inherited fields
- for t in self._inherits.keys():
- value.update(self.pool.get(t).default_get(cr, uid, fields_list,
- context))
-
- # get the default values defined in the object
- for f in fields_list:
- if f in self._defaults:
- if callable(self._defaults[f]):
- value[f] = self._defaults[f](self, cr, uid, context)
- else:
- value[f] = self._defaults[f]
-
- fld_def = ((f in self._columns) and self._columns[f]) \
- or ((f in self._inherit_fields) and self._inherit_fields[f][2]) \
- or False
-
- if isinstance(fld_def, fields.property):
- property_obj = self.pool.get('ir.property')
- prop_value = property_obj.get(cr, uid, f, self._name, context=context)
- if prop_value:
- if isinstance(prop_value, (browse_record, browse_null)):
- value[f] = prop_value.id
- else:
- value[f] = prop_value
- else:
- if f not in value:
- value[f] = False
-
- # get the default values set by the user and override the default
- # values defined in the object
- ir_values_obj = self.pool.get('ir.values')
- res = ir_values_obj.get(cr, uid, 'default', False, [self._name])
- for id, field, field_value in res:
- if field in fields_list:
- fld_def = (field in self._columns) and self._columns[field] or self._inherit_fields[field][2]
- if fld_def._type in ('many2one', 'one2one'):
- obj = self.pool.get(fld_def._obj)
- if not obj.search(cr, uid, [('id', '=', field_value or False)]):
- continue
- if fld_def._type in ('many2many'):
- obj = self.pool.get(fld_def._obj)
- field_value2 = []
- for i in range(len(field_value)):
- if not obj.search(cr, uid, [('id', '=',
- field_value[i])]):
- continue
- field_value2.append(field_value[i])
- field_value = field_value2
- if fld_def._type in ('one2many'):
- obj = self.pool.get(fld_def._obj)
- field_value2 = []
- for i in range(len(field_value)):
- field_value2.append({})
- for field2 in field_value[i]:
- if field2 in obj._columns.keys() and obj._columns[field2]._type in ('many2one', 'one2one'):
- obj2 = self.pool.get(obj._columns[field2]._obj)
- if not obj2.search(cr, uid,
- [('id', '=', field_value[i][field2])]):
- continue
- elif field2 in obj._inherit_fields.keys() and obj._inherit_fields[field2][2]._type in ('many2one', 'one2one'):
- obj2 = self.pool.get(obj._inherit_fields[field2][2]._obj)
- if not obj2.search(cr, uid,
- [('id', '=', field_value[i][field2])]):
- continue
- # TODO add test for many2many and one2many
- field_value2[i][field2] = field_value[i][field2]
- field_value = field_value2
- value[field] = field_value
- for key in context or {}:
- if key.startswith('default_') and (key[8:] in fields_list):
- value[key[8:]] = context[key]
- return value
-
#
# Update objects that uses this one to update their _inherits fields
#
@@ -2737,6 +2917,17 @@ class orm(orm_template):
self._inherit_fields = res
self._inherits_reload_src()
+ def _inherits_check(self):
+ for table, field_name in self._inherits.items():
+ if field_name not in self._columns:
+ logging.getLogger('init').info('Missing many2one field definition for _inherits reference "%s" in "%s", using default one.' % (field_name, self._name))
+ self._columns[field_name] = fields.many2one(table, string="Automatically created field to link to parent %s" % table,
+ required=True, ondelete="cascade")
+ elif not self._columns[field_name].required or self._columns[field_name].ondelete.lower() != "cascade":
+ logging.getLogger('init').warning('Field definition for _inherits reference "%s" in "%s" must be marked as "required" with ondelete="cascade", forcing it.' % (field_name, self._name))
+ self._columns[field_name].required = True
+ self._columns[field_name].ondelete = "cascade"
+
#def __getattr__(self, name):
# """
# Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent
@@ -2781,21 +2972,6 @@ class orm(orm_template):
return super(orm, self).fields_get(cr, user, fields, context, write_access)
def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
- """
- Read records with given ids with the given fields
-
- :param cr: database cursor
- :param user: current user id
- :param ids: id or list of the ids of the records to read
- :param fields: optional list of field names to return (default: all fields would be returned)
- :type fields: list (example ['field_name_1', ...])
- :param context(optional, highly recommended): context arguments, like lang, time zone
- :return: list of dictionaries((dictionary per record asked)) with requested field values
- :rtype: [{‘name_of_the_field’: value, ...}, ...]
- :raise AccessError: * if user has no read rights on the requested object
- * if user tries to bypass access rules for read on the requested object
-
- """
if not context:
context = {}
self.pool.get('ir.model.access').check(cr, user, self._name, 'read', context=context)
@@ -2805,32 +2981,13 @@ class orm(orm_template):
select = [ids]
else:
select = 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)
for r in result:
for key, v in r.items():
if v is None:
r[key] = False
- if key in self._columns:
- column = self._columns[key]
- elif key in self._inherit_fields:
- column = self._inherit_fields[key][2]
- else:
- continue
- if v and column._type == 'reference':
- model_name, ref_id = v.split(',', 1)
- model = self.pool.get(model_name)
- if not model:
- reset = True
- else:
- cr.execute('SELECT count(1) FROM "%s" WHERE id=%%s' % (model._table,), (ref_id,))
- reset = not cr.fetchone()[0]
- if reset:
- if column._classic_write:
- query = 'UPDATE "%s" SET "%s"=NULL WHERE id=%%s' % (self._table, key)
- cr.execute(query, (r['id'],))
- r[key] = False
if isinstance(ids, (int, long, dict)):
return result and result[0] or False
@@ -2881,13 +3038,29 @@ class orm(orm_template):
cr.execute(query, [tuple(sub_ids)] + rule_params)
if cr.rowcount != len(sub_ids):
raise except_orm(_('AccessError'),
- _('You try to bypass an access rule while reading (Document type: %s).') % self._description)
+ _('Operation prohibited by access rules, or performed on an already deleted document (Operation: read, Document type: %s).')
+ % (self._description,))
else:
cr.execute(query, (tuple(sub_ids),))
res.extend(cr.dictfetchall())
else:
res = map(lambda x: {'id': x}, ids)
+# if not res:
+# res = map(lambda x: {'id': x}, ids)
+# for record in res:
+# for f in fields_to_read:
+# field_val = False
+# if f in self._columns.keys():
+# ftype = self._columns[f]._type
+# elif f in self._inherit_fields.keys():
+# ftype = self._inherit_fields[f][2]._type
+# else:
+# continue
+# if ftype in ('one2many', 'many2many'):
+# field_val = []
+# record.update({f:field_val})
+
for f in fields_pre:
if f == self.CONCURRENCY_CHECK_FIELD:
continue
@@ -2911,7 +3084,7 @@ class orm(orm_template):
del r['id']
for record in res:
- if not record[col]:# if the record is deleted from _inherits table?
+ if not record[col]: # if the record is deleted from _inherits table?
continue
record.update(res3[record[col]])
if col not in fields_to_read:
@@ -2933,13 +3106,15 @@ class orm(orm_template):
for f in fields_post:
todo.setdefault(self._columns[f]._multi, [])
todo[self._columns[f]._multi].append(f)
- for key,val in todo.items():
+ for key, val in todo.items():
if key:
res2 = self._columns[val[0]].get(cr, self, ids, val, user, context=context, values=res)
for pos in val:
for record in res:
- if isinstance(res2[record['id']], str):res2[record['id']] = eval(res2[record['id']]) #TOCHECK : why got string instend of dict in python2.6
- record[pos] = res2[record['id']][pos]
+ if isinstance(res2[record['id']], str): res2[record['id']] = eval(res2[record['id']]) #TOCHECK : why got string instend of dict in python2.6
+ multi_fields = res2.get(record['id'],{})
+ if multi_fields:
+ record[pos] = multi_fields.get(pos,[])
else:
for f in val:
res2 = self._columns[f].get(cr, self, ids, f, user, context=context, values=res)
@@ -2987,12 +3162,8 @@ class orm(orm_template):
def perm_read(self, cr, user, ids, context=None, details=True):
"""
- Read the permission for record of the given ids
+ Returns some metadata about the given records.
- :param cr: database cursor
- :param user: current user id
- :param ids: id or list of ids
- :param context: context arguments, like lang, time zone
:param details: if True, \*_uid fields are replaced with the name of the user
:return: list of ownership dictionaries for each requested record
:rtype: list of dictionaries with the following keys:
@@ -3002,7 +3173,7 @@ class orm(orm_template):
* create_date: date when the record was created
* write_uid: last user who changed the record
* write_date: date of the last change to the record
-
+ * xmlid: XML ID to use to refer to this record (if there is one), in format ``module.name``
"""
if not context:
context = {}
@@ -3012,18 +3183,25 @@ class orm(orm_template):
uniq = isinstance(ids, (int, long))
if uniq:
ids = [ids]
- fields = 'id'
+ fields = ['id']
if self._log_access:
- fields += ', create_uid, create_date, write_uid, write_date'
- query = 'SELECT %s FROM "%s" WHERE id IN %%s' % (fields, self._table)
- cr.execute(query, (tuple(ids),))
+ fields += ['create_uid', 'create_date', 'write_uid', 'write_date']
+ quoted_table = '"%s"' % self._table
+ fields_str = ",".join('%s.%s'%(quoted_table, field) for field in fields)
+ query = '''SELECT %s, __imd.module, __imd.name
+ FROM %s LEFT JOIN ir_model_data __imd
+ ON (__imd.model = %%s and __imd.res_id = %s.id)
+ WHERE %s.id IN %%s''' % (fields_str, quoted_table, quoted_table, quoted_table)
+ cr.execute(query, (self._name, tuple(ids)))
res = cr.dictfetchall()
for r in res:
for key in r:
r[key] = r[key] or False
- if key in ('write_uid', 'create_uid', 'uid') and details:
+ if details and key in ('write_uid', 'create_uid'):
if r[key]:
r[key] = self.pool.get('res.users').name_get(cr, user, [r[key]])[0]
+ r['xmlid'] = ("%(module)s.%(name)s" % r) if r['name'] else False
+ del r['name'], r['module']
if uniq:
return res[ids[0]]
return res
@@ -3031,19 +3209,23 @@ class orm(orm_template):
def _check_concurrency(self, cr, ids, context):
if not context:
return
- if context.get(self.CONCURRENCY_CHECK_FIELD) and self._log_access:
- def key(oid):
- return "%s,%s" % (self._name, oid)
- santa = "(id = %s AND %s < COALESCE(write_date, create_date, now())::timestamp)"
- for i in range(0, len(ids), cr.IN_MAX):
- sub_ids = tools.flatten(((oid, context[self.CONCURRENCY_CHECK_FIELD][key(oid)])
- for oid in ids[i:i+cr.IN_MAX]
- if key(oid) in context[self.CONCURRENCY_CHECK_FIELD]))
- if sub_ids:
- cr.execute("SELECT count(1) FROM %s WHERE %s" % (self._table, " OR ".join([santa]*(len(sub_ids)/2))), sub_ids)
- res = cr.fetchone()
- if res and res[0]:
- raise except_orm('ConcurrencyException', _('Records were modified in the meanwhile'))
+ if not (context.get(self.CONCURRENCY_CHECK_FIELD) and self._log_access):
+ return
+ check_clause = "(id = %s AND %s < COALESCE(write_date, create_date, now())::timestamp)"
+ for sub_ids in cr.split_for_in_conditions(ids):
+ ids_to_check = []
+ for id in sub_ids:
+ id_ref = "%s,%s" % (self._name, id)
+ update_date = context[self.CONCURRENCY_CHECK_FIELD].pop(id_ref, None)
+ if update_date:
+ ids_to_check.extend([id, update_date])
+ if not ids_to_check:
+ continue
+ cr.execute("SELECT id FROM %s WHERE %s" % (self._table, " OR ".join([check_clause]*(len(ids_to_check)/2))), tuple(ids_to_check))
+ res = cr.fetchone()
+ if res:
+ # mention the first one only to keep the error message readable
+ raise except_orm('ConcurrencyException', _('A document was modified since you last viewed it (%s:%d)') % (self._description, res[0]))
def check_access_rule(self, cr, uid, ids, operation, context=None):
"""Verifies that the operation given by ``operation`` is allowed for the user
@@ -3062,8 +3244,8 @@ class orm(orm_template):
[sub_ids] + where_params)
if cr.rowcount != len(sub_ids):
raise except_orm(_('AccessError'),
- _('Operation prohibited by access rules (Operation: %s, Document type: %s).')
- % (operation, self._name))
+ _('Operation prohibited by access rules, or performed on an already deleted document (Operation: %s, Document type: %s).')
+ % (operation, self._description))
def unlink(self, cr, uid, ids, context=None):
"""
@@ -3072,7 +3254,7 @@ class orm(orm_template):
:param cr: database cursor
:param uid: current user id
:param ids: id or list of ids
- :param context(optional, highly recommended): context arguments, like lang, time zone
+ :param context: (optional) context arguments, like lang, time zone
:return: True
:raise AccessError: * if user has no unlink rights on the requested object
* if user tries to bypass access rules for unlink on the requested object
@@ -3108,8 +3290,8 @@ class orm(orm_template):
'where id IN %s', (sub_ids,))
for order, object, store_ids, fields in result_store:
if object != self._name:
- obj = self.pool.get(object)
- cr.execute('select id from '+obj._table+' where id IN %s',(tuple(store_ids),))
+ obj = self.pool.get(object)
+ cr.execute('select id from '+obj._table+' where id IN %s', (tuple(store_ids),))
rids = map(lambda x: x[0], cr.fetchall())
if rids:
obj._store_set_values(cr, uid, rids, fields, context)
@@ -3124,39 +3306,46 @@ class orm(orm_template):
:param cr: database cursor
:param user: current user id
- :type user: integer (example 1)
- :param ids: id or list of ids
- :param vals: dictionary of field values to update
- :type vals: dictionary (example {'field_name': 'value', ...})
- :param context(optional, highly recommended): context arguments, like lang, time zone
+ :type user: integer
+ :param ids: object id or list of object ids to update according to **vals**
+ :param vals: field values to update, e.g {'field_name': new_field_value, ...}
+ :type vals: dictionary
+ :param context: (optional) context arguments, e.g. {'lang': 'en_us', 'tz': 'UTC', ...}
+ :type context: dictionary
:return: True
:raise AccessError: * if user has no write rights on the requested object
* if user tries to bypass access rules for write on the requested object
:raise ValidateError: if user tries to enter invalid value for a field that is not in selection
- :raise UserError: if recurssion is found
+ :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
- vals format for relational field type.
+ **Note**: The type of field values to pass in ``vals`` for relationship fields is specific:
- + many2many field :
+ + For a many2many field, a list of tuples is expected.
+ Here is the list of tuple that are accepted, with the corresponding semantics ::
- For write operation on a many2many fields a list of tuple is
- expected. The folowing tuples are accepted:
- (0, 0, { fields }) create
- (1, ID, { fields }) update (write fields to ID)
- (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete)
- (3, ID) unlink (delete the relationship between the two objects but does not delete ID)
- (4, ID) link (add a relationship)
- (5, ID) unlink all
- (6, 0, list of ids) set a list of links
+ (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
+ (1, ID, { values }) update the linked record with id = ID (write *values* on it)
+ (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
+ (3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
+ (4, ID) link to existing record with id = ID (adds a relationship)
+ (5) unlink all (like using (3,ID) for all linked records)
+ (6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
- Example:
+ Example:
+ [(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
- [(6, 0, [8, 5, 6, 4])] set the many2many to ids [8, 5, 6, 4]
+ + For a one2many field, a lits of tuples is expected.
+ Here is the list of tuple that are accepted, with the corresponding semantics ::
- + one2many field : [(0, 0, dictionary of values)] (example: [(0, 0, {'field_name':field_value, ...})])
- + many2one field : ID of related record
- + reference field : model name, id (example: 'product.product, 5')
+ (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
+ (1, ID, { values }) update the linked record with id = ID (write *values* on it)
+ (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
+ Example:
+ [(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
+
+ + For a many2one field, simply use the ID of target record, which must already exist, or ``False`` to remove the link.
+ + For a reference field, use a string with the model name, a comma, and the target object id (example: ``'product.product, 5'``)
"""
readonly = None
@@ -3175,7 +3364,7 @@ class orm(orm_template):
for group in groups:
module = group.split(".")[0]
grp = group.split(".")[1]
- cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s" \
+ cr.execute("select count(*) from res_groups_users_rel where gid IN (select res_id from ir_model_data where name=%s and module=%s and model=%s) and uid=%s", \
(grp, module, 'res.groups', user))
readonly = cr.fetchall()
if readonly[0][0] >= 1:
@@ -3267,16 +3456,19 @@ class orm(orm_template):
for sub_ids in cr.split_for_in_conditions(ids):
cr.execute('update ' + self._table + ' set ' + ','.join(upd0) + ' ' \
'where id IN %s', upd1 + [sub_ids])
+ if cr.rowcount != len(sub_ids):
+ raise except_orm(_('AccessError'),
+ _('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
if totranslate:
# TODO: optimize
for f in direct:
if self._columns[f].translate:
- src_trans = self.pool.get(self._name).read(cr,user,ids,[f])[0][f]
+ src_trans = self.pool.get(self._name).read(cr, user, ids, [f])[0][f]
if not src_trans:
src_trans = vals[f]
# Inserting value to DB
- self.write(cr, user, ids, {f:vals[f]})
+ self.write(cr, user, ids, {f: vals[f]})
self.pool.get('ir.translation')._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
@@ -3305,7 +3497,8 @@ class orm(orm_template):
for val in updend:
if self._inherit_fields[val][0] == table:
v[val] = vals[val]
- self.pool.get(table).write(cr, user, nids, v, context)
+ if v:
+ self.pool.get(table).write(cr, user, nids, v, context)
self._validate(cr, user, ids, context)
@@ -3315,7 +3508,7 @@ class orm(orm_template):
# parent_left/right (via a child_of domain)
if parents_changed:
if self.pool._init:
- self.pool._init_parent[self._name]=True
+ self.pool._init_parent[self._name] = True
else:
order = self._parent_order or self._order
parent_val = vals[self._parent_name]
@@ -3336,7 +3529,7 @@ class orm(orm_template):
for (parent_pright, parent_id) in parents:
if parent_id == id:
break
- position = parent_pright+1
+ position = parent_pright + 1
# It's the first node of the parent
if not position:
@@ -3344,7 +3537,7 @@ class orm(orm_template):
position = 1
else:
cr.execute('select parent_left from '+self._table+' where id=%s', (parent_val,))
- position = cr.fetchone()[0]+1
+ position = cr.fetchone()[0] + 1
if pleft < position <= pright:
raise except_orm(_('UserError'), _('Recursivity Detected.'))
@@ -3352,18 +3545,18 @@ class orm(orm_template):
if pleft < position:
cr.execute('update '+self._table+' set parent_left=parent_left+%s where parent_left>=%s', (distance, position))
cr.execute('update '+self._table+' set parent_right=parent_right+%s where parent_right>=%s', (distance, position))
- cr.execute('update '+self._table+' set parent_left=parent_left+%s, parent_right=parent_right+%s where parent_left>=%s and parent_left<%s', (position-pleft,position-pleft, pleft, pright))
+ cr.execute('update '+self._table+' set parent_left=parent_left+%s, parent_right=parent_right+%s where parent_left>=%s and parent_left<%s', (position-pleft, position-pleft, pleft, pright))
else:
cr.execute('update '+self._table+' set parent_left=parent_left+%s where parent_left>=%s', (distance, position))
cr.execute('update '+self._table+' set parent_right=parent_right+%s where parent_right>=%s', (distance, position))
- cr.execute('update '+self._table+' set parent_left=parent_left-%s, parent_right=parent_right-%s where parent_left>=%s and parent_left<%s', (pleft-position+distance,pleft-position+distance, pleft+distance, pright+distance))
+ cr.execute('update '+self._table+' set parent_left=parent_left-%s, parent_right=parent_right-%s where parent_left>=%s and parent_left<%s', (pleft-position+distance, pleft-position+distance, pleft+distance, pright+distance))
result += self._store_get_values(cr, user, ids, vals.keys(), context)
result.sort()
done = {}
for order, object, ids, fields in result:
- key = (object,tuple(fields))
+ key = (object, tuple(fields))
done.setdefault(key, {})
# avoid to do several times the same computation
todo = []
@@ -3387,57 +3580,34 @@ class orm(orm_template):
:param cr: database cursor
:param user: current user id
- :type user: integer (example 1)
- :param vals: dictionary for new record {'field_name': field_value, ...}
- :type vals: dictionary (example {'field_name': field_value, ...})
- :param context(optional, highly recommended): context arguments, like lang, time zone
- :type context: dictionary (example {'lang': 'en_us', ...})
+ :type user: integer
+ :param vals: field values for new record, e.g {'field_name': field_value, ...}
+ :type vals: dictionary
+ :param context: optional context arguments, e.g. {'lang': 'en_us', 'tz': 'UTC', ...}
+ :type context: dictionary
:return: id of new record created
:raise AccessError: * if user has no create rights on the requested object
* if user tries to bypass access rules for create on the requested object
:raise ValidateError: if user tries to enter invalid value for a field that is not in selection
+ :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
- vals format for relational field type.
-
- + many2many field : [(6, 0, list of ids)] (example: [(6, 0, [8, 5, 6, 4])])
- + one2many field : [(0, 0, dictionary of values)] (example: [(0, 0, {'field_name':field_value, ...})])
- + many2one field : ID of related record
- + reference field : model name, id (example: 'product.product, 5')
+ **Note**: The type of field values to pass in ``vals`` for relationship fields is specific.
+ Please see the description of the :py:meth:`~osv.osv.osv.write` method for details about the possible values and how
+ to specify them.
"""
if not context:
context = {}
self.pool.get('ir.model.access').check(cr, user, self._name, 'create', context=context)
- default = []
-
- avoid_table = []
- for (t, c) in self._inherits.items():
- if c in vals:
- avoid_table.append(t)
- for f in self._columns.keys(): # + self._inherit_fields.keys():
- if not f in vals:
- default.append(f)
-
- for f in self._inherit_fields.keys():
- if (not f in vals) and (self._inherit_fields[f][0] not in avoid_table):
- default.append(f)
-
- if len(default):
- default_values = self.default_get(cr, user, default, context)
- for dv in default_values:
- if dv in self._columns and self._columns[dv]._type == 'many2many':
- if default_values[dv] and isinstance(default_values[dv][0], (int, long)):
- default_values[dv] = [(6, 0, default_values[dv])]
-
- vals.update(default_values)
+ vals = self._add_missing_default_values(cr, user, vals, context)
tocreate = {}
for v in self._inherits:
if self._inherits[v] not in vals:
tocreate[v] = {}
else:
- tocreate[v] = {'id' : vals[self._inherits[v]]}
+ tocreate[v] = {'id': vals[self._inherits[v]]}
(upd0, upd1, upd2) = ('', '', [])
upd_todo = []
for v in vals.keys():
@@ -3469,7 +3639,7 @@ class orm(orm_template):
else:
self.pool.get(table).write(cr, user, [record_id], tocreate[table], context=context)
- upd0 += ','+self._inherits[table]
+ upd0 += ',' + self._inherits[table]
upd1 += ',%s'
upd2.append(record_id)
@@ -3543,7 +3713,7 @@ class orm(orm_template):
if self._parent_store and not context.get('defer_parent_store_computation'):
if self.pool._init:
- self.pool._init_parent[self._name]=True
+ self.pool._init_parent[self._name] = True
else:
parent = vals.get(self._parent_name, False)
if parent:
@@ -3563,7 +3733,7 @@ class orm(orm_template):
pleft = cr.fetchone()[0] or 0
cr.execute('update '+self._table+' set parent_left=parent_left+2 where parent_left>%s', (pleft,))
cr.execute('update '+self._table+' set parent_right=parent_right+2 where parent_right>%s', (pleft,))
- cr.execute('update '+self._table+' set parent_left=%s,parent_right=%s where id=%s', (pleft+1,pleft+2,id_new))
+ cr.execute('update '+self._table+' set parent_left=%s,parent_right=%s where id=%s', (pleft+1, pleft+2, id_new))
# default element in context must be remove when call a one2many or many2many
rel_context = context.copy()
@@ -3589,7 +3759,7 @@ class orm(orm_template):
message = self._description + \
" '" + \
self.name_get(cr, user, [id_new], context=context)[0][1] + \
- "' "+ _("created.")
+ "' " + _("created.")
self.log(cr, user, id_new, message, True, context=context)
wf_service = netsvc.LocalService("workflow")
wf_service.trg_create(user, self._name, id_new, cr)
@@ -3613,24 +3783,24 @@ class orm(orm_template):
result.setdefault(fncts[fnct][0], {})
# uid == 1 for accessing objects having rules defined on store fields
- ids2 = fncts[fnct][2](self,cr, 1, ids, context)
+ ids2 = fncts[fnct][2](self, cr, 1, ids, context)
for id in filter(None, ids2):
result[fncts[fnct][0]].setdefault(id, [])
result[fncts[fnct][0]][id].append(fnct)
dict = {}
for object in result:
k2 = {}
- for id,fnct in result[object].items():
+ for id, fnct in result[object].items():
k2.setdefault(tuple(fnct), [])
k2[tuple(fnct)].append(id)
- for fnct,id in k2.items():
- dict.setdefault(fncts[fnct[0]][4],[])
- dict[fncts[fnct[0]][4]].append((fncts[fnct[0]][4],object,id,map(lambda x: fncts[x][1], fnct)))
+ for fnct, id in k2.items():
+ dict.setdefault(fncts[fnct[0]][4], [])
+ dict[fncts[fnct[0]][4]].append((fncts[fnct[0]][4], object, id, map(lambda x: fncts[x][1], fnct)))
result2 = []
tmp = dict.keys()
tmp.sort()
for k in tmp:
- result2+=dict[k]
+ result2 += dict[k]
return result2
def _store_set_values(self, cr, uid, ids, fields, context):
@@ -3639,7 +3809,7 @@ class orm(orm_template):
field_flag = False
field_dict = {}
if self._log_access:
- cr.execute('select id,write_date from '+self._table+' where id IN %s',(tuple(ids),))
+ cr.execute('select id,write_date from '+self._table+' where id IN %s', (tuple(ids),))
res = cr.fetchall()
for r in res:
if r[1]:
@@ -3666,7 +3836,7 @@ class orm(orm_template):
if key:
# uid == 1 for accessing objects having rules defined on store fields
result = self._columns[val[0]].get(cr, self, ids, val, 1, context=context)
- for id,value in result.items():
+ for id, value in result.items():
if field_flag:
for f in value.keys():
if f in field_dict[id]:
@@ -3686,7 +3856,7 @@ class orm(orm_template):
upd1.append(id)
if upd0 and upd1:
cr.execute('update "' + self._table + '" set ' + \
- string.join(upd0, ',') + ' where id = %s', upd1)
+ ','.join(upd0) + ' where id = %s', upd1)
else:
for f in val:
@@ -3697,14 +3867,14 @@ class orm(orm_template):
if r in field_dict.keys():
if f in field_dict[r]:
result.pop(r)
- for id,value in result.items():
+ for id, value in result.items():
if self._columns[f]._type in ('many2one', 'one2one'):
try:
value = value[0]
except:
pass
cr.execute('update "' + self._table + '" set ' + \
- '"'+f+'"='+self._columns[f]._symbol_set[0] + ' where id = %s', (self._columns[f]._symbol_set[1](value),id))
+ '"'+f+'"='+self._columns[f]._symbol_set[0] + ' where id = %s', (self._columns[f]._symbol_set[1](value), id))
return True
#
@@ -3714,98 +3884,178 @@ class orm(orm_template):
raise NotImplementedError(_('This method does not exist anymore'))
# TODO: ameliorer avec NULL
- def _where_calc(self, cr, user, args, active_test=True, context=None):
+ def _where_calc(self, cr, user, domain, active_test=True, context=None):
+ """Computes the WHERE clause needed to implement an OpenERP domain.
+ :param domain: the domain to compute
+ :type domain: list
+ :param active_test: whether the default filtering of records with ``active``
+ field set to ``False`` should be applied.
+ :return: the query expressing the given domain as provided in domain
+ :rtype: osv.query.Query
+ """
if not context:
context = {}
- args = args[:]
+ domain = domain[:]
# if the object has a field named 'active', filter out all inactive
# records unless they were explicitely asked for
if 'active' in self._columns and (active_test and context.get('active_test', True)):
- if args:
+ if domain:
active_in_args = False
- for a in args:
+ for a in domain:
if a[0] == 'active':
active_in_args = True
if not active_in_args:
- args.insert(0, ('active', '=', 1))
+ domain.insert(0, ('active', '=', 1))
else:
- args = [('active', '=', 1)]
+ domain = [('active', '=', 1)]
- if args:
+ if domain:
import expression
- e = expression.expression(args)
+ e = expression.expression(domain)
e.parse(cr, user, self, context)
tables = e.get_tables()
- qu1, qu2 = e.to_sql()
- qu1 = qu1 and [qu1] or []
+ where_clause, where_params = e.to_sql()
+ where_clause = where_clause and [where_clause] or []
else:
- qu1, qu2, tables = [], [], ['"%s"' % self._table]
+ where_clause, where_params, tables = [], [], ['"%s"' % self._table]
- return (qu1, qu2, tables)
+ return Query(tables, where_clause, where_params)
def _check_qorder(self, word):
if not regex_order.match(word):
- raise except_orm(_('AccessError'), _('Bad query.'))
+ raise except_orm(_('AccessError'), _('Invalid "order" specified. A valid "order" specification is a comma-separated list of valid field names (optionally followed by asc/desc for the direction)'))
return True
- def search(self, cr, user, args, offset=0, limit=None, order=None,
- context=None, count=False):
+ def _apply_ir_rules(self, cr, uid, query, mode='read', context=None):
+ """Add what's missing in ``query`` to implement all appropriate ir.rules
+ (using the ``model_name``'s rules or the current model's rules if ``model_name`` is None)
+
+ :param query: the current query object
"""
- Search for record/s with or without domain
+ def apply_rule(added_clause, added_params, added_tables, parent_model=None, child_object=None):
+ if added_clause:
+ if parent_model and child_object:
+ # as inherited rules are being applied, we need to add the missing JOIN
+ # to reach the parent table (if it was not JOINed yet in the query)
+ child_object._inherits_join_add(parent_model, query)
+ query.where_clause += added_clause
+ query.where_clause_params += added_params
+ for table in added_tables:
+ if table not in query.tables:
+ query.tables.append(table)
+ return True
+ return False
- :param cr: database cursor
- :param user: current user id
- :param args: list of tuples specifying search criteria [('field_name', 'operator', 'value'), ...]
- :param offset: optional number from search starts
- :param limit: optional max number of records to return
- :param order: optional columns to sort by (default: self._order=id )
- :param context(optional, highly recommended): context arguments, like lang, time zone
- :param count: if True, returns only the number of records matching the criteria, not their ids
- :return: id or list of ids of records matching the criteria
- :rtype: integer or list of integers
- :raise AccessError: * if user tries to bypass access rules for read on the requested object.
+ # apply main rules on the object
+ rule_obj = self.pool.get('ir.rule')
+ apply_rule(*rule_obj.domain_get(cr, uid, self._name, mode, context=context))
- Operators:
- * =, !=, >, >=, <, <=, like, ilike, in, not in, child_of, parent_left, parent_right
- Prefix operators:
- * '&' (default), '|', '!'
+ # apply ir.rules from the parents (through _inherits)
+ for inherited_model in self._inherits:
+ kwargs = dict(parent_model=inherited_model, child_object=self) #workaround for python2.5
+ apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context), **kwargs)
+ def _generate_m2o_order_by(self, order_field, query):
+ """
+ Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields,
+ either native m2o fields or function/related fields that are stored, including
+ intermediate JOINs for inheritance if required.
+
+ :return: the qualified field name to use in an ORDER BY clause to sort by ``order_field``
+ """
+ if order_field not in self._columns and order_field in self._inherit_fields:
+ # also add missing joins for reaching the table containing the m2o field
+ qualified_field = self._inherits_join_calc(order_field, query)
+ order_field_column = self._inherit_fields[order_field][2]
+ else:
+ qualified_field = '"%s"."%s"' % (self._table, order_field)
+ order_field_column = self._columns[order_field]
+
+ assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()'
+ assert order_field_column._classic_write or getattr(order_field_column, 'store', False), "Many2one function/related fields must be stored to be used as ordering fields"
+
+ # figure out the applicable order_by for the m2o
+ dest_model = self.pool.get(order_field_column._obj)
+ m2o_order = dest_model._order
+ if not regex_order.match(m2o_order):
+ # _order is complex, can't use it here, so we default to _rec_name
+ m2o_order = dest_model._rec_name
+ else:
+ # extract the first field name, to be able to qualify it and add desc/asc
+ m2o_order = m2o_order.split(",",1)[0].strip().split(" ",1)[0]
+
+ # Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
+ # as we don't want to exclude results that have NULL values for the m2o
+ src_table, src_field = qualified_field.replace('"','').split('.', 1)
+ query.join((src_table, dest_model._table, src_field, 'id'), outer=True)
+ return '"%s"."%s"' % (dest_model._table, m2o_order)
+
+
+ def _generate_order_by(self, order_spec, query):
+ """
+ Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be
+ a comma-separated list of valid field names, optionally followed by an ASC or DESC direction.
+
+ :raise" except_orm in case order_spec is malformed
+ """
+ order_by_clause = self._order
+ if order_spec:
+ order_by_elements = []
+ self._check_qorder(order_spec)
+ for order_part in order_spec.split(','):
+ order_split = order_part.strip().split(' ')
+ order_field = order_split[0].strip()
+ order_direction = order_split[1].strip() if len(order_split) == 2 else ''
+ if order_field in self._columns:
+ order_column = self._columns[order_field]
+ if order_column._classic_read:
+ order_by_clause = '"%s"."%s"' % (self._table, order_field)
+ elif order_column._type == 'many2one':
+ order_by_clause = self._generate_m2o_order_by(order_field, query)
+ else:
+ continue # ignore non-readable or "non-joignable" fields
+ elif order_field in self._inherit_fields:
+ parent_obj = self.pool.get(self._inherit_fields[order_field][0])
+ order_column = parent_obj._columns[order_field]
+ if order_column._classic_read:
+ order_by_clause = self._inherits_join_calc(order_field, query)
+ elif order_column._type == 'many2one':
+ order_by_clause = self._generate_m2o_order_by(order_field, query)
+ else:
+ continue # ignore non-readable or "non-joignable" fields
+ order_by_elements.append("%s %s" % (order_by_clause, order_direction))
+ order_by_clause = ",".join(order_by_elements)
+
+ return order_by_clause and (' ORDER BY %s ' % order_by_clause) or ''
+
+ def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None):
+ """
+ Private implementation of search() method, allowing specifying the uid to use for the access right check.
+ This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors,
+ by specifying ``access_rights_uid=1`` to bypass access rights check, but not ir.rules!
+ This is ok at the security level because this method is private and not callable through XML-RPC.
+
+ :param access_rights_uid: optional user ID to use when checking access rights
+ (not for ir.rules, this is only for ir.model.access)
"""
if context is None:
context = {}
- self.pool.get('ir.model.access').check(cr, user, self._name, 'read', context=context)
- # compute the where, order by, limit and offset clauses
- (qu1, qu2, tables) = self._where_calc(cr, user, args, context=context)
- dom = self.pool.get('ir.rule').domain_get(cr, user, self._name, 'read', context=context)
- qu1 = qu1 + dom[0]
- qu2 = qu2 + dom[1]
- for t in dom[2]:
- if t not in tables:
- tables.append(t)
+ self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context)
- if len(qu1):
- qu1 = ' where '+string.join(qu1, ' and ')
- else:
- qu1 = ''
-
-
- order_by = self._order
- if order:
- self._check_qorder(order)
- o = order.split(' ')[0]
- if (o in self._columns) and getattr(self._columns[o], '_classic_write'):
- order_by = order
+ query = self._where_calc(cr, user, args, context=context)
+ self._apply_ir_rules(cr, user, query, 'read', context=context)
+ order_by = self._generate_order_by(order, query)
+ from_clause, where_clause, where_clause_params = query.get_sql()
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
-
+ where_str = where_clause and (" WHERE %s" % where_clause) or ''
if count:
- cr.execute('select count(%s.id) from ' % self._table +
- ','.join(tables) +qu1 + limit_str + offset_str, qu2)
+ cr.execute('SELECT count("%s".id) FROM ' % self._table + from_clause + where_str + limit_str + offset_str, where_clause_params)
res = cr.fetchall()
return res[0][0]
- cr.execute('select %s.id from ' % self._table + ','.join(tables) +qu1+' order by '+order_by+limit_str+offset_str, qu2)
+ cr.execute('SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str, where_clause_params)
res = cr.fetchall()
return [x[0] for x in res]
@@ -3820,66 +4070,17 @@ class orm(orm_template):
else:
return self._columns[field].search(cr, self, args, field, value, offset, limit, uid)
- def name_get(self, cr, user, ids, context=None):
- """
-
- :param cr: database cursor
- :param user: current user id
- :type user: integer (example 1)
- :param ids: list of ids
- :param context: context arguments, like lang, time zone
- :return: tuples with the text representation of requested objects for to-many relationships
-
- """
- if not context:
- context = {}
- if not ids:
- return []
- if isinstance(ids, (int, long)):
- ids = [ids]
- return [(r['id'], tools.ustr(r[self._rec_name])) for r in self.read(cr, user, ids,
- [self._rec_name], context, load='_classic_write')]
-
- # private implementation of name_search, allows passing a dedicated user for the name_get part to
- # solve some access rights issues
- def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
- if not args:
- args = []
- if not context:
- context = {}
- args = args[:]
- if name:
- args += [(self._rec_name, operator, name)]
- ids = self.search(cr, user, args, limit=limit, context=context)
- res = self.name_get(cr, name_get_uid or user, ids, context)
- return res
-
- def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
- """
-
- :param cr: database cursor
- :param user: current user id
- :param name: object name to search
- :param args: list of tuples specifying search criteria [('field_name', 'operator', 'value'), ...]
- :param operator: operator for search criterion
- :param context: context arguments, like lang, time zone
- :param limit: optional max number of records to return
- :return: list of object names matching the search criteria, used to provide completion for to-many relationships
-
- This method is equivalent of search() on name + name_get()
-
- """
- return self._name_search(cr, user, name, args, operator, context, limit)
-
def copy_data(self, cr, uid, id, default=None, context=None):
"""
Copy given record's data with all its fields values
:param cr: database cursor
:param user: current user id
- :param ids: id of the record to copy
- :param default: dictionary of field values to update before saving the duplicate object
+ :param id: id of the record to copy
+ :param default: field values to override in the original values of the copied record
+ :type default: dictionary
:param context: context arguments, like lang, time zone
+ :type context: dictionary
:return: dictionary containing all the field values
"""
@@ -3953,12 +4154,12 @@ class orm(orm_template):
# we must recursively copy the translations for o2o and o2m
if field_def['type'] in ('one2one', 'one2many'):
target_obj = self.pool.get(field_def['relation'])
- old_record, new_record = self.read(cr, uid, [old_id, new_id], [field_name], context=context)
+ old_record, new_record = self.read(cr, uid, [old_id, new_id], [field_name], context=context)
# here we rely on the order of the ids to match the translations
# as foreseen in copy_data()
- old_childs = sorted(old_record[field_name])
- new_childs = sorted(new_record[field_name])
- for (old_child, new_child) in zip(old_childs, new_childs):
+ old_children = sorted(old_record[field_name])
+ new_children = sorted(new_record[field_name])
+ for (old_child, new_child) in zip(old_children, new_children):
# recursive copy of translations here
target_obj.copy_translations(cr, uid, old_child, new_child, context=context)
# and for translatable fields we keep them for copy
@@ -3971,9 +4172,9 @@ class orm(orm_template):
if trans_name:
trans_ids = trans_obj.search(cr, uid, [
('name', '=', trans_name),
- ('res_id','=', old_id)
+ ('res_id', '=', old_id)
])
- translation_records.extend(trans_obj.read(cr,uid,trans_ids,context=context))
+ translation_records.extend(trans_obj.read(cr, uid, trans_ids, context=context))
for record in translation_records:
del record['id']
@@ -3988,9 +4189,10 @@ class orm(orm_template):
:param cr: database cursor
:param uid: current user id
:param id: id of the record to copy
- :param default: dictionary of field values to update before saving the duplicate object
- :type default: dictionary (example {'field_name': field_value, ...})
+ :param default: dictionary of field values to override in the original values of the copied record, e.g: ``{'field_name': overriden_value, ...}``
+ :type default: dictionary
:param context: context arguments, like lang, time zone
+ :type context: dictionary
:return: True
"""
@@ -4000,7 +4202,7 @@ class orm(orm_template):
return new_id
def exists(self, cr, uid, ids, context=None):
- if type(ids) in (int,long):
+ if type(ids) in (int, long):
ids = [ids]
query = 'SELECT count(1) FROM "%s"' % (self._table)
cr.execute(query + "WHERE ID IN %s", (tuple(ids),))
@@ -4008,13 +4210,15 @@ class orm(orm_template):
def check_recursion(self, cr, uid, ids, parent=None):
"""
- Check recursion in records
+ Verifies that there is no loop in a hierarchical structure of records,
+ by following the parent relationship using the **parent** field until a loop
+ is detected or until a top-level record is found.
:param cr: database cursor
:param uid: current user id
- :param ids: list of ids of records
- :param parent: parent field name
- :return: True or False based on recursion detection
+ :param ids: list of ids of records to check
+ :param parent: optional parent field name (default: ``self._parent_name = parent_id``)
+ :return: **True** if the operation can proceed safely, or **False** if an infinite loop is detected.
"""
if not parent:
@@ -4037,19 +4241,20 @@ class orm(orm_template):
"""Find out the XML ID of any database record, if there
is one. This method works as a possible implementation
for a function field, to be able to add it to any
- model object easily, referencing it as 'osv.osv.get_xml_id'.
+ model object easily, referencing it as ``osv.osv.get_xml_id``.
- get_xml_id(cr, uid, ids) -> { 'id': 'module.xml_id' }
+ **Synopsis**: ``get_xml_id(cr, uid, ids) -> { 'id': 'module.xml_id' }``
:return: the fully qualified XML ID of the given object,
- defaulting to an empty string when there's none.
+ defaulting to an empty string when there's none
+ (to be usable as a function field).
"""
result = dict.fromkeys(ids, '')
model_data_obj = self.pool.get('ir.model.data')
- data_ids = model_data_obj.search(cr,uid,
- [('model','=',self._name),('res_id','in',ids)])
- data_results = model_data_obj.read(cr,uid,data_ids,
- ['name','module','res_id'])
+ data_ids = model_data_obj.search(cr, uid,
+ [('model', '=', self._name), ('res_id', 'in', ids)])
+ data_results = model_data_obj.read(cr, uid, data_ids,
+ ['name', 'module', 'res_id'])
for record in data_results:
result[record['res_id']] = '%(module)s.%(name)s' % record
return result
diff --git a/bin/osv/osv.py b/bin/osv/osv.py
index 0086e351d05..11db4096a9a 100644
--- a/bin/osv/osv.py
+++ b/bin/osv/osv.py
@@ -32,7 +32,7 @@ import traceback
import logging
from psycopg2 import IntegrityError, errorcodes
from tools.func import wraps
-
+from tools.translate import _
module_list = []
module_class_list = {}
@@ -64,23 +64,30 @@ class osv_pool(netsvc.Service):
except IntegrityError, inst:
for key in self._sql_error.keys():
if key in inst[0]:
- self.abortResponse(1, 'Constraint Error', 'warning', self._sql_error[key])
- if inst.pgcode == errorcodes.NOT_NULL_VIOLATION:
- msg = 'Sorry, this record cannot be deleted at the moment because other records still reference it.'
+ self.abortResponse(1, _('Constraint Error'), 'warning', _(self._sql_error[key]))
+ if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
+ msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
self.logger.debug("IntegrityError", exc_info=True)
try:
- context = inst.pgerror.split('"public".')[1]
- model_name = table = context.split('"')[1]
+ errortxt = inst.pgerror.replace('«','"').replace('»','"')
+ if '"public".' in errortxt:
+ context = errortxt.split('"public".')[1]
+ model_name = table = context.split('"')[1]
+ else:
+ last_quote_end = errortxt.rfind('"')
+ last_quote_begin = errortxt.rfind('"', 0, last_quote_end)
+ model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip()
+ print "MODEL", last_quote_begin, last_quote_end, model_name
model = table.replace("_",".")
model_obj = self.get(model)
if model_obj:
model_name = model_obj._description or model_obj._name
- msg += '\n\n[object with reference: %s - %s]' % (model_name, model)
+ msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
except Exception:
pass
- self.abortResponse(1, 'Integrity Error', 'warning', msg)
+ self.abortResponse(1, _('Integrity Error'), 'warning', msg)
else:
- self.abortResponse(1, 'Integrity Error', 'warning', inst[0])
+ self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
except Exception, e:
self.logger.exception("Uncaught exception")
raise
diff --git a/bin/osv/query.py b/bin/osv/query.py
new file mode 100644
index 00000000000..ab4f4143634
--- /dev/null
+++ b/bin/osv/query.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 OpenERP S.A. http://www.openerp.com
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+
+def _quote(to_quote):
+ if '"' not in to_quote:
+ return '"%s"' % to_quote
+ return to_quote
+
+
+class Query(object):
+ """
+ Dumb implementation of a Query object, using 3 string lists so far
+ for backwards compatibility with the (table, where_clause, where_params) previously used.
+
+ TODO: To be improved after v6.0 to rewrite part of the ORM and add support for:
+ - auto-generated multiple table aliases
+ - multiple joins to the same table with different conditions
+ - dynamic right-hand-side values in domains (e.g. a.name = a.description)
+ - etc.
+ """
+
+ def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None):
+
+ # holds the list of tables joined using default JOIN.
+ # the table names are stored double-quoted (backwards compatibility)
+ self.tables = tables or []
+
+ # holds the list of WHERE clause elements, to be joined with
+ # 'AND' when generating the final query
+ self.where_clause = where_clause or []
+
+ # holds the parameters for the formatting of `where_clause`, to be
+ # passed to psycopg's execute method.
+ self.where_clause_params = where_clause_params or []
+
+ # holds table joins done explicitly, supporting outer joins. The JOIN
+ # condition should not be in `where_clause`. The dict is used as follows:
+ # self.joins = {
+ # 'table_a': [
+ # ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN'),
+ # ('table_c', 'table_a_col2', 'table_c_col', 'LEFT JOIN'),
+ # ('table_d', 'table_a_col3', 'table_d_col', 'JOIN'),
+ # ]
+ # }
+ # which should lead to the following SQL:
+ # SELECT ... FROM "table_a" LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col")
+ # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col")
+ self.joins = joins or {}
+
+ def join(self, connection, outer=False):
+ """Adds the JOIN specified in ``connection``.
+
+ :param connection: a tuple ``(lhs, table, lhs_col, col)``.
+ The join corresponds to the SQL equivalent of::
+
+ ``(lhs.lhs_col = table.col)``
+
+ :param outer: True if a LEFT OUTER JOIN should be used, if possible
+ (no promotion to OUTER JOIN is supported in case the JOIN
+ was already present in the query, as for the moment
+ implicit INNER JOINs are only connected from NON-NULL
+ columns so it would not be correct (e.g. for
+ ``_inherits`` or when a domain criterion explicitly
+ adds filtering)
+ """
+ (lhs, table, lhs_col, col) = connection
+ lhs = _quote(lhs)
+ table = _quote(table)
+ assert lhs in self.tables, "Left-hand-side table must already be part of the query!"
+ if table in self.tables:
+ # already joined, must ignore (promotion to outer and multiple joins not supported yet)
+ pass
+ else:
+ # add JOIN
+ self.tables.append(table)
+ self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
+ return self
+
+ def get_sql(self):
+ """Returns (query_from, query_where, query_params)"""
+ query_from = ''
+ tables_to_process = list(self.tables)
+
+ def add_joins_for_table(table, query_from):
+ for (dest_table, lhs_col, col, join) in self.joins.get(table,[]):
+ tables_to_process.remove(dest_table)
+ query_from += ' %s %s ON (%s."%s" = %s."%s")' % \
+ (join, dest_table, table, lhs_col, dest_table, col)
+ query_from = add_joins_for_table(dest_table, query_from)
+ return query_from
+
+ for table in tables_to_process:
+ query_from += table
+ if table in self.joins:
+ query_from = add_joins_for_table(table, query_from)
+ query_from += ','
+ query_from = query_from[:-1] # drop last comma
+ return (query_from, " AND ".join(self.where_clause), self.where_clause_params)
+
+ def __str__(self):
+ return '' % self.get_sql()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/bin/pooler.py b/bin/pooler.py
index d0f38836504..684bc6210d4 100644
--- a/bin/pooler.py
+++ b/bin/pooler.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -34,30 +34,28 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
import osv.osv
pool = osv.osv.osv_pool()
pool_dic[db_name] = pool
-
+
try:
addons.load_modules(db, force_demo, status, update_module)
- except Exception, e:
+ except Exception:
del pool_dic[db_name]
raise
cr = db.cursor()
try:
pool.init_set(cr, False)
+ pool.get('ir.actions.report.xml').register_all(cr)
cr.commit()
finally:
cr.close()
- import report
- report.interface.register_all(db)
if pooljobs:
- pool.get('ir.cron')._poolJobs(db.dbname)
+ pool.get('ir.cron').restart(db.dbname)
return db, pool
def restart_pool(db_name, force_demo=False, status=None, update_module=False):
if db_name in pool_dic:
- pool_dic[db_name].get('ir.cron').cancel(db_name)
del pool_dic[db_name]
return get_db_and_pool(db_name, force_demo, status, update_module=update_module)
diff --git a/bin/release.py b/bin/release.py
index 817994e7d46..fa806717a01 100644
--- a/bin/release.py
+++ b/bin/release.py
@@ -1,44 +1,42 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# -*- encoding: utf-8 -*-
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-TODAY Tiny SPRL (). All Rights Reserved
+# Copyright (C) 2004-TODAY OpenERP S.A. (). All Rights Reserved
+# $Id$
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
+# 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 Affero General Public License for more details.
+# GNU General Public License for more details.
#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
#
-##############################################################################
name = 'openerp-server'
-version = '6.0dev'
+version = '6.0.0-rc1'
major_version = '6.0'
description = 'OpenERP Server'
-long_desc = '''\
-OpenERP is a complete ERP and CRM. The main features are accounting (analytic
+long_desc = '''OpenERP 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 XML-RPC interfaces.
'''
-classifiers = """\
-Development Status :: 5 - Production/Stable
+classifiers = """Development Status :: 5 - Production/Stable
License :: OSI Approved :: GNU General Public License (GPL)
Programming Language :: Python
"""
url = 'http://www.openerp.com'
-author = 'Tiny.be'
-author_email = 'info@tiny.be'
+author = 'OpenERP S.A.'
+author_email = 'info@openerp.com'
support_email = 'support@openerp.com'
license = 'GPL-3'
diff --git a/bin/report/custom.py b/bin/report/custom.py
index b53be34cfdb..4b02901bdb2 100644
--- a/bin/report/custom.py
+++ b/bin/report/custom.py
@@ -73,7 +73,7 @@ class report_custom(report_int):
for cond in conditions:
if cond and cond[0]:
c = cond[0]
- temp = c[0](eval('obj.'+c[1]))
+ temp = c[0](eval('obj.'+c[1],{'obj': obj}))
if not eval('\''+temp+'\''+' '+c[2]+' '+'\''+str(c[3])+'\''):
tobreak = True
if tobreak:
@@ -87,7 +87,7 @@ class report_custom(report_int):
row_canvas[i]=False
elif len(fields[i])==1:
if not isinstance(obj, browse_null):
- row.append(str(eval('obj.'+fields[i][0])))
+ row.append(str(eval('obj.'+fields[i][0],{'obj': obj})))
else:
row.append(None)
else:
@@ -107,7 +107,7 @@ class report_custom(report_int):
else:
key = levels.keys()
for l in key:
- objs = eval('obj.'+l)
+ objs = eval('obj.'+l,{'obj': obj})
if not isinstance(objs, browse_record_list) and type(objs) <> type([]):
objs = [objs]
field_new = []
@@ -192,7 +192,7 @@ class report_custom(report_int):
def build_tree(obj, level, depth):
res = self._row_get(cr, uid,[obj], new_fields, new_cond)
level.append(depth)
- new_obj = eval('obj.'+report['field_parent'][1])
+ new_obj = eval('obj.'+report['field_parent'][1],{'obj': obj})
if not isinstance(new_obj, list) :
new_obj = [new_obj]
for o in new_obj:
diff --git a/bin/report/interface.py b/bin/report/interface.py
index 6aecfe66133..703256d8fcd 100644
--- a/bin/report/interface.py
+++ b/bin/report/interface.py
@@ -70,6 +70,7 @@ class report_rml(report_int):
def __init__(self, name, table, tmpl, xsl):
super(report_rml, self).__init__(name)
self.table = table
+ self.internal_header=False
self.tmpl = tmpl
self.xsl = xsl
self.bin_datas = {}
@@ -110,6 +111,7 @@ class report_rml(report_int):
return self.post_process_xml_data(cr, uid, xml, context)
def post_process_xml_data(self, cr, uid, xml, context=None):
+
if not context:
context={}
# find the position of the 3rd tag
@@ -136,6 +138,8 @@ class report_rml(report_int):
# TODO: The translation doesn't work for "textext tex text "
#
def create_rml(self, cr, xml, uid, context=None):
+ if self.tmpl=='' and not self.internal_header:
+ self.internal_header=True
if not context:
context={}
pool = pooler.get_pool(cr.dbname)
@@ -174,6 +178,9 @@ class report_rml(report_int):
return xml
def create_pdf(self, rml, localcontext = None, logo=None, title=None):
+ if not localcontext:
+ localcontext={}
+ localcontext.update({'internal_header':self.internal_header})
if logo:
self.bin_datas['logo'] = logo
else:
@@ -219,25 +226,5 @@ class report_rml(report_int):
obj.render()
return obj.get()
-from report_sxw import report_sxw
-
-def register_all(db):
- opj = os.path.join
- cr = db.cursor()
- cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
- result = cr.dictfetchall()
- cr.close()
- svcs = netsvc.Service._services
- for r in result:
- if svcs.has_key('report.'+r['report_name']):
- continue
- if r['report_rml'] or r['report_rml_content_data']:
- report_sxw('report.'+r['report_name'], r['model'],
- opj('addons',r['report_rml'] or '/'), header=r['header'])
- if r['report_xsl']:
- report_rml('report.'+r['report_name'], r['model'],
- opj('addons',r['report_xml']),
- r['report_xsl'] and opj('addons',r['report_xsl']))
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/report/print_xml.py b/bin/report/print_xml.py
index f349e10b1ba..3319ee0b558 100644
--- a/bin/report/print_xml.py
+++ b/bin/report/print_xml.py
@@ -254,7 +254,7 @@ class document(object):
for el_cld in node:
self.parse_node(el_cld, el, v)
else:
- # if there is no "type" attribute in the node, copy it to the xml data and parse its childs
+ # if there is no "type" attribute in the node, copy it to the xml data and parse its children
if not node.tag == etree.Comment:
if node.tag == parent.tag:
el = parent
diff --git a/bin/report/printscreen/ps_form.py b/bin/report/printscreen/ps_form.py
index 2ee22ebcaf9..227e3084bbf 100644
--- a/bin/report/printscreen/ps_form.py
+++ b/bin/report/printscreen/ps_form.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
@@ -54,17 +54,11 @@ class report_printscreen_list(report_int):
if not context:
context={}
datas['ids'] = ids
- pool = pooler.get_pool(cr.dbname)
- model_id = pool.get('ir.model').search(cr, uid, [('model','=',model._name)])
- if model_id:
- model_desc = pool.get('ir.model').browse(cr, uid, model_id, context).name
- self.title = model_desc
- else:
- model_desc = model._description
- self.title = model_desc
-
model = pool.get(datas['model'])
- result = model.fields_view_get(cr, uid, view_type='tree', context=context)
+ pool = pooler.get_pool(cr.dbname)
+ # title come from description of model which are specified in py file.
+ self.title = model._description
+ result = model.fields_view_get(cr, uid, view_type='form', context=context)
fields_order = self._parse_string(result['arch'])
rows = model.read(cr, uid, datas['ids'], result['fields'].keys() )
diff --git a/bin/report/printscreen/ps_list.py b/bin/report/printscreen/ps_list.py
index 79d4813270f..7fb20eab0dc 100644
--- a/bin/report/printscreen/ps_list.py
+++ b/bin/report/printscreen/ps_list.py
@@ -30,12 +30,14 @@ import locale
import time, os
from operator import itemgetter
from datetime import datetime
+from report import report_sxw
class report_printscreen_list(report_int):
def __init__(self, name):
report_int.__init__(self, name)
self.context = {}
self.groupby = []
+ self.cr=''
def _parse_node(self, root_node):
result = []
@@ -60,18 +62,15 @@ class report_printscreen_list(report_int):
def create(self, cr, uid, ids, datas, context=None):
if not context:
context={}
+ self.cr=cr
self.context = context
self.groupby = context.get('group_by',[])
self.groupby_no_leaf = context.get('group_by_no_leaf',False)
pool = pooler.get_pool(cr.dbname)
model = pool.get(datas['model'])
- model_id = pool.get('ir.model').search(cr, uid, [('model','=',model._name)])
- if model_id:
- model_desc = pool.get('ir.model').browse(cr, uid, model_id[0], context).name
- self.title = model_desc
- else:
- model_desc = model._description
- self.title = model_desc
+ # Title come from description of model which are specified in py file.
+ model_desc = model._description
+ self.title = model_desc
datas['ids'] = ids
model = pooler.get_pool(cr.dbname).get(datas['model'])
result = model.fields_view_get(cr, uid, view_type='tree', context=context)
@@ -83,34 +82,36 @@ class report_printscreen_list(report_int):
for rec in records:
rec['__group'] = True
rec['__no_leaf'] = self.groupby_no_leaf
+ rec['__grouped_by'] = groupby[0] if (isinstance(groupby, list) and groupby) else groupby
for f in fields_order:
if f not in rec:
rec.update({f:False})
elif isinstance(rec[f], tuple):
rec[f] = rec[f][1]
rows.append(rec)
- groupby = (rec.get('__context', {})).get('group_by',[])
- domain = rec.get('__domain', [])
- if groupby:
- get_groupby_data(groupby, domain)
+ inner_groupby = (rec.get('__context', {})).get('group_by',[])
+ inner_domain = rec.get('__domain', [])
+ if inner_groupby:
+ get_groupby_data(inner_groupby, inner_domain)
else:
if self.groupby_no_leaf:
continue
- child_ids = model.search(cr, uid, domain)
+ child_ids = model.search(cr, uid, inner_domain)
res = model.read(cr, uid, child_ids, result['fields'].keys(), context)
+ res.sort(lambda x,y: cmp(ids.index(x['id']), ids.index(y['id'])))
rows.extend(res)
dom = [('id','in',ids)]
if self.groupby_no_leaf and len(ids) and not ids[0]:
dom = datas.get('_domain',[])
get_groupby_data(self.groupby, dom)
else:
- rows = model.read(cr, uid, datas['ids'], result['fields'].keys(), context)
- ids2 = map(itemgetter('id'), rows) # getting the ids from read result
- if datas['ids'] != ids2: # sorted ids were not taken into consideration for print screen
- rows_new = []
- for id in datas['ids']:
- rows_new += [elem for elem in rows if elem['id'] == id]
- rows = rows_new
+ rows = model.read(cr, uid, datas['ids'], result['fields'].keys(), context)
+ ids2 = map(itemgetter('id'), rows) # getting the ids from read result
+ if datas['ids'] != ids2: # sorted ids were not taken into consideration for print screen
+ rows_new = []
+ for id in datas['ids']:
+ rows_new += [elem for elem in rows if elem['id'] == id]
+ rows = rows_new
res = self._create_table(uid, datas['ids'], result['fields'], fields_order, rows, context, model_desc)
return (self.obj.get(), 'pdf')
@@ -132,6 +133,10 @@ class report_printscreen_list(report_int):
_append_node('PageHeight', '%.2f' %(pageSize[1] * 2.8346,))
_append_node('report-header', title)
+ _append_node('company', pooler.get_pool(self.cr.dbname).get('res.users').browse(self.cr,uid,uid).company_id.name)
+ rpt_obj = pooler.get_pool(self.cr.dbname).get('res.users')
+ rml_obj=report_sxw.rml_parse(self.cr, uid, rpt_obj._name,context)
+ _append_node('header-date', str(rml_obj.formatLang(time.strftime("%Y-%m-%d"),date=True))+' ' + str(time.strftime("%H:%M")))
l = []
t = 0
rowcount = 0;
@@ -175,7 +180,7 @@ class report_printscreen_list(report_int):
count += 1
if fields[f]['type']=='many2one' and line[f]:
if not line.get('__group'):
- line[f]= line[f][1]
+ line[f] = line[f][1]
if fields[f]['type']=='selection' and line[f]:
for key, value in fields[f]['selection']:
if key == line[f]:
@@ -212,31 +217,41 @@ class report_printscreen_list(report_int):
d1 = datetime.strptime(line[f], '%Y-%m-%d %H:%M:%S')
new_d1 = d1.strftime(format)
line[f] = new_d1
+
+
if line.get('__group'):
col = etree.SubElement(node_line, 'col', para='group', tree='no')
else:
col = etree.SubElement(node_line, 'col', para='yes', tree='no')
+
+ # Prevent empty labels in groups
+ if f == line.get('__grouped_by') and line.get('__group') and not line[f] and not float_flag and not temp[count]:
+ col.text = line[f] = 'Undefined'
+ col.set('tree', 'undefined')
+
if line[f] != None:
col.text = tools.ustr(line[f] or '')
if float_flag:
- col.set('tree','float')
+ col.set('tree','float')
if line.get('__no_leaf') and temp[count] == 1 and f != 'id' and not line['__context']['group_by']:
tsum[count] = float(tsum[count]) + float(line[f])
if not line.get('__group') and f != 'id' and temp[count] == 1:
tsum[count] = float(tsum[count]) + float(line[f]);
else:
- col.text = '/'
+ col.text = '/'
+
node_line = etree.SubElement(lines, 'row')
for f in range(0, len(fields_order)):
col = etree.SubElement(node_line, 'col', para='group', tree='no')
- col.set('tree','float')
+ col.set('tree', 'float')
if tsum[f] != None:
- if tsum[f] >= 0.01 :
- prec = '%.2f'
- total = prec%(tsum[f])
- txt = str(total or '')
- else:
- txt = str(tsum[f] or '')
+ if tsum[f] != 0.0:
+ digits = fields[fields_order[f]].get('digits', (16, 2))
+ prec = '%%.%sf' % (digits[1], )
+ total = prec % (tsum[f], )
+ txt = str(total or '')
+ else:
+ txt = str(tsum[f] or '')
else:
txt = '/'
if f == 0:
diff --git a/bin/report/pyPdf/generic.py b/bin/report/pyPdf/generic.py
index ef9d7e186b9..f216732a85d 100644
--- a/bin/report/pyPdf/generic.py
+++ b/bin/report/pyPdf/generic.py
@@ -214,7 +214,8 @@ class FloatObject(decimal.Decimal, PdfObject):
class NumberObject(int, PdfObject):
def __init__(self, value):
- int.__init__(self, value)
+ int.__init__(self)
+ self = value
def writeToStream(self, stream, encryption_key):
stream.write(repr(self))
@@ -404,7 +405,8 @@ class NameObject(str, PdfObject):
delimiterCharacters = "(", ")", "<", ">", "[", "]", "{", "}", "/", "%"
def __init__(self, data):
- str.__init__(self, data)
+ str.__init__(self)
+ self = data
def writeToStream(self, stream, encryption_key):
stream.write(self)
diff --git a/bin/report/pyPdf/pdf.py b/bin/report/pyPdf/pdf.py
index 2963c46eba5..9bc56cf8b05 100644
--- a/bin/report/pyPdf/pdf.py
+++ b/bin/report/pyPdf/pdf.py
@@ -50,7 +50,6 @@ import utils
import warnings
from generic import *
from utils import readNonWhitespace, readUntilWhitespace, ConvertFunctionsToVirtualList
-from sets import ImmutableSet
##
# This class supports writing PDF files out, given pages produced by another
@@ -987,8 +986,8 @@ class PageObject(DictionaryObject):
# Combine /ProcSet sets.
newResources[NameObject("/ProcSet")] = ArrayObject(
- ImmutableSet(originalResources.get("/ProcSet", ArrayObject()).getObject()).union(
- ImmutableSet(page2Resources.get("/ProcSet", ArrayObject()).getObject())
+ frozenset(originalResources.get("/ProcSet", ArrayObject()).getObject()).union(
+ frozenset(page2Resources.get("/ProcSet", ArrayObject()).getObject())
)
)
diff --git a/bin/report/render/rml2pdf/trml2pdf.py b/bin/report/render/rml2pdf/trml2pdf.py
index 771a59fa28f..e9969fa4e52 100644
--- a/bin/report/render/rml2pdf/trml2pdf.py
+++ b/bin/report/render/rml2pdf/trml2pdf.py
@@ -35,9 +35,58 @@ from lxml import etree
import base64
from reportlab.platypus.doctemplate import ActionFlowable
from tools.safe_eval import safe_eval as eval
+from reportlab.lib.units import inch,cm,mm
encoding = 'utf-8'
+class NumberedCanvas(canvas.Canvas):
+ def __init__(self, *args, **kwargs):
+ canvas.Canvas.__init__(self, *args, **kwargs)
+ self._codes = []
+ self._flag=False
+ self._pageCount=0
+ self._currentPage =0
+ self._pageCounter=0
+ self.pages={}
+
+ def showPage(self):
+ self._currentPage +=1
+ if not self._flag:
+ self._pageCount += 1
+ else:
+ self.pages.update({self._currentPage:self._pageCount})
+ self._codes.append({'code': self._code, 'stack': self._codeStack})
+ self._startPage()
+ self._flag=False
+
+ def pageCount(self):
+ if self.pages.get(self._pageCounter,False):
+ self._pageNumber=0
+ self._pageCounter +=1
+ key=self._pageCounter
+ if not self.pages.get(key,False):
+ while not self.pages.get(key,False):
+ key = key + 1
+ self.setFont("Helvetica", 8)
+ self.drawRightString((self._pagesize[0]-35), (self._pagesize[1]-43),
+ "Page %(this)i of %(total)i" % {
+ 'this': self._pageNumber+1,
+ 'total': self.pages.get(key,False),
+ }
+ )
+
+ def save(self):
+ """add page info to each page (page x of y)"""
+ # reset page counter
+ self._pageNumber = 0
+ for code in self._codes:
+ self._code = code['code']
+ self._codeStack = code['stack']
+ self.pageCount()
+ canvas.Canvas.showPage(self)
+# self.restoreState()
+ self._doc.SaveToFile(self._filename, self)
+
class PageCount(platypus.Flowable):
def draw(self):
self.canv.beginForm("pageCount")
@@ -64,7 +113,7 @@ class _rml_styles(object,):
for style in node.findall('paraStyle'):
sname = style.get('name')
self.styles[sname] = self._para_style_update(style)
-
+
self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname])
for variable in node.findall('initialize'):
@@ -261,7 +310,7 @@ class _rml_canvas(object):
if n.tag == 'pageNumber':
rc += str(self.canvas.getPageNumber())
rc += utils._process_text(self, n.tail)
- return rc
+ return rc.replace('\n','')
def _drawString(self, node):
v = utils.attr_get(node, ['x','y'])
@@ -364,15 +413,18 @@ class _rml_canvas(object):
image_data = self.images[node.get('name')]
s = cStringIO.StringIO(image_data)
else:
- import base64
if self.localcontext:
- res = utils._regex.findall(node.text)
- for key in res:
- newtext = eval(key, {}, self.localcontext)
- node.text = newtext
- image_data = base64.decodestring(node.text)
- if not image_data: return False
- s = cStringIO.StringIO(image_data)
+ res = utils._regex.findall(node.text)
+ for key in res:
+ newtext = eval(key, {}, self.localcontext)
+ node.text = newtext
+ image_data = None
+ if node.text:
+ image_data = base64.decodestring(node.text)
+ if image_data:
+ s = cStringIO.StringIO(image_data)
+ else:
+ return False
else:
if node.get('file') in self.images:
s = cStringIO.StringIO(self.images[node.get('file')])
@@ -400,6 +452,7 @@ class _rml_canvas(object):
else:
args['height'] = sy * args['width'] / sx
self.canvas.drawImage(img, **args)
+# self.canvas._doc.SaveToFile(self.canvas._filename, self.canvas)
def _path(self, node):
self.path = self.canvas.beginPath()
@@ -499,8 +552,8 @@ class _rml_flowable(object):
return rc1
def _table(self, node):
- childs = utils._child_get(node,self,'tr')
- if not childs:
+ children = utils._child_get(node,self,'tr')
+ if not children:
return None
length = 0
colwidths = None
@@ -508,7 +561,7 @@ class _rml_flowable(object):
data = []
styles = []
posy = 0
- for tr in childs:
+ for tr in children:
paraStyle = None
if tr.get('style'):
st = copy.deepcopy(self.styles.table_styles[tr.get('style')])
@@ -592,7 +645,30 @@ class _rml_flowable(object):
def _textual_image(self, node):
return base64.decodestring(node.text)
+ def _pto(self, node):
+ sub_story = []
+ pto_header = None
+ pto_trailer = None
+ for node in utils._child_get(node, self):
+ if node.tag == etree.Comment:
+ node.text = ''
+ continue
+ elif node.tag=='pto_header':
+ pto_header = self.render(node)
+ elif node.tag=='pto_trailer':
+ pto_trailer = self.render(node)
+ else:
+ flow = self._flowable(node)
+ if flow:
+ if isinstance(flow,list):
+ sub_story = sub_story + flow
+ else:
+ sub_story.append(flow)
+ return platypus.flowables.PTOContainer(sub_story, trailer=pto_trailer, header=pto_header)
+
def _flowable(self, node, extra_style=None):
+ if node.tag=='pto':
+ return self._pto(node)
if node.tag=='para':
style = self.styles.para_style_get(node)
if extra_style:
@@ -759,14 +835,20 @@ class TinyDocTemplate(platypus.BaseDocTemplate):
self.handle_frameBegin()
def afterFlowable(self, flowable):
if isinstance(flowable, PageReset):
+ self.canv._pageCount=self.page
+ self.page=0
+ self.canv._flag=True
self.canv._pageNumber = 0
class _rml_template(object):
def __init__(self, localcontext, out, node, doc, images={}, path='.', title=None):
+ if not localcontext:
+ localcontext={'internal_header':True}
self.localcontext = localcontext
self.images= images
self.path = path
self.title = title
+
if not node.get('pageSize'):
pageSize = (utils.unit_get('21cm'), utils.unit_get('29.7cm'))
else:
@@ -777,6 +859,7 @@ class _rml_template(object):
self.page_templates = []
self.styles = doc.styles
self.doc = doc
+ self.image=[]
pts = node.findall('pageTemplate')
for pt in pts:
frames = []
@@ -791,6 +874,7 @@ class _rml_template(object):
except Exception: # FIXME: be even more specific, perhaps?
gr=''
if len(gr):
+# self.image=[ n for n in utils._child_get(gr[0], self) if n.tag=='image' or not self.localcontext]
drw = _rml_draw(self.localcontext,gr[0], self.doc, images=images, path=self.path, title=self.title)
self.page_templates.append( platypus.PageTemplate(frames=frames, onPage=drw.render, **utils.attr_get(pt, [], {'id':'str'}) ))
else:
@@ -799,6 +883,8 @@ class _rml_template(object):
self.doc_tmpl.addPageTemplates(self.page_templates)
def render(self, node_stories):
+ if self.localcontext and not self.localcontext.get('internal_header',False):
+ del self.localcontext['internal_header']
fis = []
r = _rml_flowable(self.doc,self.localcontext, images=self.images, path=self.path, title=self.title)
story_cnt = 0
@@ -806,10 +892,15 @@ class _rml_template(object):
if story_cnt > 0:
fis.append(platypus.PageBreak())
fis += r.render(node_story)
+ # Reset Page Number with new story tag
+ fis.append(PageReset())
story_cnt += 1
- if self.localcontext:
+ if self.localcontext and self.localcontext.get('internal_header',False):
+ self.doc_tmpl.afterFlowable(fis)
+ self.doc_tmpl.build(fis,canvasmaker=NumberedCanvas)
+ else:
fis.append(PageCount())
- self.doc_tmpl.build(fis)
+ self.doc_tmpl.build(fis)
def parseNode(rml, localcontext = {},fout=None, images={}, path='.',title=None):
node = etree.XML(rml)
diff --git a/bin/report/render/rml2pdf/utils.py b/bin/report/render/rml2pdf/utils.py
index 96df33c925f..947526fc61b 100644
--- a/bin/report/render/rml2pdf/utils.py
+++ b/bin/report/render/rml2pdf/utils.py
@@ -36,14 +36,14 @@
# 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
-from lxml import etree
import copy
import locale
-import traceback, sys
-from tools.safe_eval import safe_eval as eval
import logging
+import re
+import reportlab
+
+import tools
+from tools.safe_eval import safe_eval as eval
_regex = re.compile('\[\[(.+?)\]\]')
@@ -116,18 +116,19 @@ def _process_text(self, txt):
sps = _regex.split(txt)
while sps:
# This is a simple text to translate
- result += unicode(self.localcontext.get('translate', lambda x:x)(sps.pop(0)))
+ to_translate = tools.ustr(sps.pop(0))
+ result += tools.ustr(self.localcontext.get('translate', lambda x:x)(to_translate))
if sps:
try:
+ txt = None
expr = sps.pop(0)
- txt = eval(expr,self.localcontext)
- if txt and (isinstance(txt, unicode) or isinstance(txt, str)):
- txt = unicode(txt)
- except Exception,e:
- tb_s = reduce(lambda x, y: x+y, traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
- if type(txt)==type('') or type(txt)==type(u''):
- txt2 = str2xml(txt)
- result += unicode(txt2)
+ txt = eval(expr, self.localcontext)
+ if txt and isinstance(txt, basestring):
+ txt = tools.ustr(txt)
+ except Exception:
+ pass
+ if isinstance(txt, basestring):
+ result += str2xml(txt)
elif (txt is not None) and (txt is not False):
result += unicode(txt)
return result
@@ -176,7 +177,7 @@ def attr_get(node, attrs, dict={}):
for key in dict:
if node.get(key):
if dict[key]=='str':
- res[key] = str(node.get(key))
+ res[key] = tools.ustr(node.get(key))
elif dict[key]=='bool':
res[key] = bool_get(node.get(key))
elif dict[key]=='int':
diff --git a/bin/report/report_sxw.py b/bin/report/report_sxw.py
index 9256734ea9f..21f37dcf70e 100644
--- a/bin/report/report_sxw.py
+++ b/bin/report/report_sxw.py
@@ -19,26 +19,21 @@
#
##############################################################################
from lxml import etree
-import traceback, sys
import StringIO
import cStringIO
import base64
-import copy
-import locale
from datetime import datetime
import os
import re
import time
from interface import report_rml
import preprocess
-import ir
-import netsvc
-import osv
+import logging
import pooler
import tools
-import warnings
import zipfile
import common
+from osv.fields import float as float_class, function as function_class
DT_FORMAT = '%Y-%m-%d'
DHM_FORMAT = '%Y-%m-%d %H:%M:%S'
@@ -110,7 +105,7 @@ class _date_format(str, _format):
if self.val:
if getattr(self,'name', None):
date = datetime.strptime(self.name, DT_FORMAT)
- return date.strftime(self.lang_obj.date_format)
+ return date.strftime(str(self.lang_obj.date_format))
return self.val
class _dttime_format(str, _format):
@@ -121,8 +116,8 @@ class _dttime_format(str, _format):
def __str__(self):
if self.val and getattr(self,'name', None):
return datetime.strptime(self.name, DHM_FORMAT)\
- .strftime("%s %s"%(self.lang_obj.date_format,
- self.lang_obj.time_format))
+ .strftime("%s %s"%(str(self.lang_obj.date_format),
+ str(self.lang_obj.time_format)))
return self.val
@@ -174,6 +169,7 @@ class rml_parse(object):
self.localcontext.update(context)
self.rml_header = user.company_id.rml_header
self.rml_header2 = user.company_id.rml_header2
+ self.rml_header3 = user.company_id.rml_header3
self.logo = user.company_id.logo
self.name = name
self._node = None
@@ -239,9 +235,46 @@ class rml_parse(object):
self.default_lang[lang] = self.lang_dict.copy()
return True
- def formatLang(self, value, digits=2, date=False,date_time=False, grouping=True, monetary=False):
+ def digits_fmt(self, obj=None, f=None, dp=None):
+ digits = self.get_digits(obj, f, dp)
+ return "%%.%df" % (digits, )
+
+ def get_digits(self, obj=None, f=None, dp=None):
+ d = DEFAULT_DIGITS = 2
+ if dp:
+ decimal_precision_obj = self.pool.get('decimal.precision')
+ ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
+ if ids:
+ d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
+ elif obj and f:
+ res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
+ if isinstance(res_digits, tuple):
+ d = res_digits[1]
+ else:
+ d = res_digits(self.cr)[1]
+ elif (hasattr(obj, '_field') and\
+ isinstance(obj._field, (float_class, function_class)) and\
+ obj._field.digits):
+ d = obj._field.digits[1] or DEFAULT_DIGITS
+ return d
+
+ def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False):
+ """
+ Assuming 'Account' decimal.precision=3:
+ formatLang(value) -> digits=2 (default)
+ formatLang(value, digits=4) -> digits=4
+ formatLang(value, dp='Account') -> digits=3
+ formatLang(value, digits=5, dp='Account') -> digits=5
+ """
+ if digits is None:
+ if dp:
+ digits = self.get_digits(dp=dp)
+ else:
+ digits = self.get_digits(value)
+
if isinstance(value, (str, unicode)) and not value:
return ''
+
if not self.lang_dict_called:
self._get_lang_dict()
self.lang_dict_called = True
@@ -249,26 +282,20 @@ class rml_parse(object):
if date or date_time:
if not str(value):
return ''
+
date_format = self.lang_dict['date_format']
parse_format = DT_FORMAT
if date_time:
+ value=value.split('.')[0]
date_format = date_format + " " + self.lang_dict['time_format']
parse_format = DHM_FORMAT
-
- # filtering time.strftime('%Y-%m-%d')
-# if type(value) == type(''):
-# parse_format = DHM_FORMAT
-# if (not date_time):
-# return str(value)
-
if not isinstance(value, time.struct_time):
- try:
- date = datetime.datetime(str(value), parse_format)
- except:# sometimes it takes converted values into value, so we dont need conversion.
- return str(value)
+ return time.strftime(date_format, time.strptime(value, parse_format))
+
else:
date = datetime(*value.timetuple()[:6])
return date.strftime(date_format)
+
return self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
def repeatIn(self, lst, name,nodes_parent=False):
@@ -286,17 +313,19 @@ class rml_parse(object):
if not self._transl_regex.match(piece_list[pn]):
source_string = piece_list[pn].replace('\n', ' ').strip()
if len(source_string):
- translated_string = transl_obj._get_source(self.cr, self.uid, self.name, 'rml', lang, source_string)
+ translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
if translated_string:
piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
text = ''.join(piece_list)
return text
def _add_header(self, rml_dom, header='external'):
- if header=='external':
- rml_head = self.rml_header
- else:
+ if header=='internal':
rml_head = self.rml_header2
+ elif header=='internal landscape':
+ rml_head = self.rml_header3
+ else:
+ rml_head = self.rml_header
head_dom = etree.XML(rml_head)
for tag in head_dom:
@@ -311,6 +340,8 @@ class rml_parse(object):
def set_context(self, objects, data, ids, report_type = None):
self.localcontext['data'] = data
self.localcontext['objects'] = objects
+ self.localcontext['digits_fmt'] = self.digits_fmt
+ self.localcontext['get_digits'] = self.get_digits
self.datas = data
self.ids = ids
self.objects = objects
@@ -321,18 +352,23 @@ class rml_parse(object):
self.localcontext.update({'name_space' :common.sxw_namespace})
class report_sxw(report_rml, preprocess.report):
- def __init__(self, name, table, rml=False, parser=rml_parse, header=True, store=False):
+ def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False):
report_rml.__init__(self, name, table, rml, '')
self.name = name
self.parser = parser
self.header = header
self.store = store
+ self.internal_header=False
+ if header=='internal' or header=='internal landscape':
+ self.internal_header=True
def getObjects(self, cr, uid, ids, context):
table_obj = pooler.get_pool(cr.dbname).get(self.table)
return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
def create(self, cr, uid, ids, data, context=None):
+ if self.internal_header:
+ context.update({'internal_header':self.internal_header})
pool = pooler.get_pool(cr.dbname)
ir_obj = pool.get('ir.actions.report.xml')
report_xml_ids = ir_obj.search(cr, uid,
@@ -348,6 +384,7 @@ class report_sxw(report_rml, preprocess.report):
for key,arg in argv.items():
setattr(self, key, arg)
report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
+ report_xml.header = self.header
report_type = report_xml.report_type
if report_type in ['sxw','odt']:
fnct = self.create_source_odt
@@ -396,8 +433,8 @@ class report_sxw(report_rml, preprocess.report):
result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
if not result:
return False
- try:
- if aname:
+ if aname:
+ try:
name = aname+'.'+result[1]
pool.get('ir.attachment').create(cr, uid, {
'name': aname,
@@ -407,11 +444,9 @@ class report_sxw(report_rml, preprocess.report):
'res_id': obj.id,
}, context=context
)
- cr.commit()
- except Exception,e:
- import traceback, sys
- tb_s = reduce(lambda x, y: x+y, traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
- netsvc.Logger().notifyChannel('report', netsvc.LOG_ERROR,str(e))
+ except Exception:
+ #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
+ logging.getLogger('report').error('Could not create saved report attachment', exc_info=True)
results.append(result)
if results:
if results[0][1]=='pdf':
diff --git a/bin/addons/base/module/wizard/wizard_configuration.py b/bin/run_tests.py
similarity index 59%
rename from bin/addons/base/module/wizard/wizard_configuration.py
rename to bin/run_tests.py
index e720ba419b0..7c0ee2a2431 100644
--- a/bin/addons/base/module/wizard/wizard_configuration.py
+++ b/bin/run_tests.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# Copyright (C) 2010 OpenERP S.A. http://www.openerp.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,28 +15,13 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-import wizard
-import pooler
+import unittest
-class wizard_configuration(wizard.interface):
-
- def _config(self, cr, uid, data, context=None):
- return pooler.get_pool(cr.dbname).get('res.config')\
- .next(cr, uid, [], context=context)
-
- states = {
- 'init':{
- 'result': {
- 'type': 'action',
- 'action': _config,
- 'state': 'end',
- },
- }
- }
-wizard_configuration('module.configuration')
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+import test
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(test))
diff --git a/bin/service/http_server.py b/bin/service/http_server.py
index bab38b06609..a06334d51e5 100644
--- a/bin/service/http_server.py
+++ b/bin/service/http_server.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
-# Copyright P. Christeas 2008,2009
+# Copyright P. Christeas 2008-2010
+# Copyright 2010 OpenERP SA. (http://www.openerp.com)
#
#
# WARNING: This program as such is intended to be used by professional
@@ -34,10 +35,13 @@ import netsvc
import errno
import threading
import tools
+import posixpath
+import urllib
import os
import select
import socket
import xmlrpclib
+import logging
from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
@@ -69,6 +73,9 @@ class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer)
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
HTTPServer.__init__(self, addr, requestHandler)
+
+ self.numThreads = 0
+ self.__threadno = 0
# [Bug #1222790] If possible, set close-on-exec flag; if a
# method spawns a subprocess, the subprocess shouldn't have
@@ -81,37 +88,56 @@ class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer)
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()))
+
+ logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
-class MultiHandler2(MultiHTTPHandler):
+ def _mark_start(self, thread):
+ self.numThreads += 1
+
+ def _mark_end(self, thread):
+ self.numThreads -= 1
+
+
+ def _get_next_name(self):
+ self.__threadno += 1
+ return 'http-client-%d' % self.__threadno
+class HttpLogHandler:
+ """ helper class for uniform log handling
+ Please define self._logger at each class that is derived from this
+ """
+ _logger = None
+
def log_message(self, format, *args):
- netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args)
+ self._logger.debug(format % args) # todo: perhaps other level
def log_error(self, format, *args):
- netsvc.Logger().notifyChannel('http',netsvc.LOG_ERROR,format % args)
+ self._logger.error(format % args)
+
+ def log_exception(self, format, *args):
+ self._logger.exception(format, *args)
+
+ def log_request(self, code='-', size='-'):
+ self._logger.log(netsvc.logging.DEBUG_RPC, '"%s" %s %s',
+ self.requestline, str(code), str(size))
+
+class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
+ _logger = logging.getLogger('http')
-class SecureMultiHandler2(SecureMultiHTTPHandler):
- def log_message(self, format, *args):
- netsvc.Logger().notifyChannel('https',netsvc.LOG_DEBUG,format % args)
+class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
+ _logger = logging.getLogger('https')
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')
+ fcert = tc.get('secure_cert_file', 'server.cert')
+ fkey = tc.get('secure_pkey_file', '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 BaseHttpDaemon(threading.Thread, netsvc.Server):
+ _RealProto = '??'
+
def __init__(self, interface, port, handler):
- threading.Thread.__init__(self)
+ threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
netsvc.Server.__init__(self)
self.__port = port
self.__interface = interface
@@ -121,10 +147,11 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server):
self.server.vdirs = []
self.server.logRequests = True
self.server.timeout = self._busywait_timeout
+ logging.getLogger("web-services").info(
+ "starting %s service at %s port %d" %
+ (self._RealProto, 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,))
+ logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
raise
@property
@@ -148,29 +175,51 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server):
raise
return True
+ def stats(self):
+ res = "%sd: " % self._RealProto + ((self.running and "running") or "stopped")
+ if self.server:
+ res += ", %d threads" % (self.server.numThreads,)
+ return res
+
+ def append_svc(self, service):
+ if not isinstance(service, HTTPDir):
+ raise Exception("Wrong class for http service")
+
+ pos = len(self.server.vdirs)
+ lastpos = pos
+ while pos > 0:
+ pos -= 1
+ if self.server.vdirs[pos].matches(service.path):
+ lastpos = pos
+ # we won't break here, but search all way to the top, to
+ # ensure there is no lesser entry that will shadow the one
+ # we are inserting.
+ self.server.vdirs.insert(lastpos, service)
+
+ def list_services(self):
+ ret = []
+ for svc in self.server.vdirs:
+ ret.append( ( svc.path, str(svc.handler)) )
+
+ return ret
+
+
class HttpDaemon(BaseHttpDaemon):
+ _RealProto = 'HTTP'
def __init__(self, interface, port):
super(HttpDaemon, self).__init__(interface, port,
handler=MultiHandler2)
- netsvc.Logger().notifyChannel(
- "web-services", netsvc.LOG_INFO,
- "starting HTTP service at %s port %d" %
- (interface or '0.0.0.0', port,))
class HttpSDaemon(BaseHttpDaemon):
+ _RealProto = 'HTTPS'
def __init__(self, interface, port):
try:
super(HttpSDaemon, self).__init__(interface, port,
handler=SecureMultiHandler2)
except SSLError, e:
- netsvc.Logger().notifyChannel(
- 'httpd-ssl', netsvc.LOG_CRITICAL,
- "Can not load the certificate and/or the private key files")
+ logging.getLogger('httpsd').exception( \
+ "Can not load the certificate and/or the private key files")
raise
- netsvc.Logger().notifyChannel(
- "web-services", netsvc.LOG_INFO,
- "starting HTTPS service at %s port %d" %
- (interface or '0.0.0.0', port,))
httpd = None
httpsd = None
@@ -190,23 +239,32 @@ def reg_http_service(hts, secure_only = False):
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)
+ httpd.append_svc(hts)
if httpsd:
- httpsd.server.vdirs.append(hts)
+ httpsd.append_svc(hts)
if (not httpd) and (not httpsd):
- netsvc.Logger().notifyChannel('httpd',netsvc.LOG_WARNING,"No httpd available to register service %s" % hts.path)
+ logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
return
+def list_http_services(protocol=None):
+ global httpd, httpsd
+ if httpd and (protocol == 'http' or protocol == None):
+ return httpd.list_services()
+ elif httpsd and (protocol == 'https' or protocol == None):
+ return httpsd.list_services()
+ else:
+ raise Exception("Incorrect protocol or no http services")
+
import SimpleXMLRPCServer
-class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
rpc_paths = []
protocol_version = 'HTTP/1.1'
+ _logger = logging.getLogger('xmlrpc')
+
def _dispatch(self, method, params):
try:
service_name = self.path.split("/")[-1]
@@ -214,9 +272,6 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCSer
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
@@ -225,9 +280,7 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCSer
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
+ self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
def init_xmlrpc():
@@ -235,13 +288,57 @@ def init_xmlrpc():
# Example of http file serving:
# reg_http_service(HTTPDir('/test/',HTTPHandler))
reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler))
- netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO,
- "Registered XML-RPC over HTTP")
+ logging.getLogger("web-services").info("Registered XML-RPC over HTTP")
- if tools.config.get('xmlrpcs', False):
- reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler, True))
- netsvc.Logger().notifyChannel('web-services', netsvc.LOG_INFO,
- "Registered XML-RPC over HTTPS")
+ if tools.config.get('xmlrpcs', False) \
+ and not tools.config.get('xmlrpc', False):
+ # only register at the secure server
+ reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler), True)
+ logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only")
+
+class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
+ _logger = logging.getLogger('httpd')
+ _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
+
+ def __init__(self,request, client_address, server):
+ HTTPHandler.__init__(self,request,client_address,server)
+ document_root = tools.config.get('static_http_document_root', False)
+ assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!"
+ self.__basepath = document_root
+
+ def translate_path(self, path):
+ """Translate a /-separated PATH to the local filename syntax.
+
+ Components that mean special things to the local file system
+ (e.g. drive or directory names) are ignored. (XXX They should
+ probably be diagnosed.)
+
+ """
+ # abandon query parameters
+ path = path.split('?',1)[0]
+ path = path.split('#',1)[0]
+ path = posixpath.normpath(urllib.unquote(path))
+ words = path.split('/')
+ words = filter(None, words)
+ path = self.__basepath
+ for word in words:
+ if word in (os.curdir, os.pardir): continue
+ path = os.path.join(path, word)
+ return path
+
+def init_static_http():
+ if not tools.config.get('static_http_enable', False):
+ return
+
+ document_root = tools.config.get('static_http_document_root', False)
+ assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)"
+
+ base_path = tools.config.get('static_http_url_prefix', '/')
+
+ reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
+
+ logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
+ (document_root, base_path))
class OerpAuthProxy(AuthProxy):
""" Require basic authentication..
@@ -260,7 +357,6 @@ class OerpAuthProxy(AuthProxy):
try:
if not db:
db = handler.get_db_from_path(path)
- print "Got db:",db
except Exception:
if path.startswith('/'):
path = path[1:]
@@ -305,10 +401,10 @@ class OpenERPAuthProvider(AuthProvider):
return False
return (user, passwd, db, uid)
except Exception,e:
- netsvc.Logger().notifyChannel("auth",netsvc.LOG_DEBUG,"Fail auth:"+ str(e))
+ logging.getLogger("auth").debug("Fail auth: %s" % e )
return False
- def log(self, msg):
- netsvc.Logger().notifyChannel("auth",netsvc.LOG_INFO,msg)
+ def log(self, msg, lvl=logging.INFO):
+ logging.getLogger("auth").log(lvl,msg)
#eof
diff --git a/bin/service/netrpc_server.py b/bin/service/netrpc_server.py
index 7f204751576..9d25c98083a 100644
--- a/bin/service/netrpc_server.py
+++ b/bin/service/netrpc_server.py
@@ -36,7 +36,9 @@ import tools
class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
def __init__(self, sock, threads):
- threading.Thread.__init__(self)
+ spn = sock and sock.getpeername()
+ spn = 'netrpc-client-%s:%s' % spn[0:2]
+ threading.Thread.__init__(self, name=spn)
self.sock = sock
# Only at the server side, use a big timeout: close the
# clients connection when they're idle for 20min.
@@ -48,7 +50,8 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
try:
self.socket.shutdown(
getattr(socket, 'SHUT_RDWR', 2))
- except Exception: pass
+ except Exception:
+ pass
# That should garbage-collect and close it, too
self.sock = None
@@ -99,7 +102,7 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher):
class TinySocketServerThread(threading.Thread,netsvc.Server):
def __init__(self, interface, port, secure=False):
- threading.Thread.__init__(self, name="Net-RPC socket")
+ threading.Thread.__init__(self, name="NetRPCDaemon-%d"%port)
netsvc.Server.__init__(self)
self.__port = port
self.__interface = interface
diff --git a/bin/service/web_services.py b/bin/service/web_services.py
index 19b178c7caa..df42d8736ce 100644
--- a/bin/service/web_services.py
+++ b/bin/service/web_services.py
@@ -20,7 +20,6 @@
##############################################################################
import base64
-import logging
import os
import security
import thread
@@ -39,8 +38,6 @@ import tools
import locale
from cStringIO import StringIO
-logging.basicConfig()
-
class db(netsvc.ExportService):
def __init__(self, name="db"):
netsvc.ExportService.__init__(self, name)
@@ -305,7 +302,7 @@ class db(netsvc.ExportService):
else:
cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
res = [str(name) for (name,) in cr.fetchall()]
- except:
+ except Exception:
res = []
finally:
cr.close()
@@ -385,7 +382,8 @@ class common(_ObjectService):
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', 'check_connectivity']:
+ 'login_message','get_stats', 'check_connectivity',
+ 'list_http_services']:
pass
elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel']:
passwd = params[0]
@@ -438,11 +436,7 @@ GNU Public Licence.
return info
def exp_timezone_get(self, db, login, password):
- #timezone detection is safe in multithread, so lazy init is ok here
- if (not tools.config['timezone']):
- tools.config['timezone'] = tools.misc.detect_server_timezone()
- return tools.config['timezone']
-
+ return tools.misc.get_server_timezone()
def exp_get_available_updates(self, contract_id, contract_password):
import tools.maintenance as tm
@@ -496,7 +490,7 @@ GNU Public Licence.
try:
try:
base64_decoded = base64.decodestring(zips[module])
- except:
+ except Exception:
l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
raise
@@ -505,13 +499,13 @@ GNU Public Licence.
try:
try:
tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
- except:
+ except Exception:
l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
rmtree(module)
raise
finally:
zip_contents.close()
- except:
+ except Exception:
l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
nmp = os.path.join(backup_directory, module)
if os.path.isdir(nmp):
@@ -556,9 +550,9 @@ GNU Public Licence.
def exp_login_message(self):
return tools.config.get('login_message', False)
- def exp_set_loglevel(self,loglevel):
+ def exp_set_loglevel(self, loglevel, logger=None):
l = netsvc.Logger()
- l.set_loglevel(int(loglevel))
+ l.set_loglevel(int(loglevel), logger)
return True
def exp_get_stats(self):
@@ -567,6 +561,10 @@ GNU Public Licence.
res += netsvc.Server.allStats()
return res
+ def exp_list_http_services(self):
+ from service import http_server
+ return http_server.list_http_services()
+
def exp_check_connectivity(self):
return bool(sql_db.db_connect('template1'))
@@ -724,7 +722,10 @@ class report_spool(netsvc.ExportService):
logger = netsvc.Logger()
logger.notifyChannel('web-services', netsvc.LOG_ERROR,
'Exception: %s\n%s' % (str(exception), tb_s))
- self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
+ if hasattr(exception, 'name') and hasattr(exception, 'value'):
+ self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value))
+ else:
+ self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
self._reports[id]['state'] = True
cr.commit()
cr.close()
@@ -735,8 +736,9 @@ class report_spool(netsvc.ExportService):
def _check_report(self, report_id):
result = self._reports[report_id]
- if result['exception']:
- raise result['exception']
+ exc = result['exception']
+ if exc:
+ self.abortResponse(exc, exc.message, 'warning', exc.traceback)
res = {'state': result['state']}
if res['state']:
if tools.config['reportgz']:
@@ -756,7 +758,6 @@ class report_spool(netsvc.ExportService):
return res
def exp_report_get(self, db, uid, report_id):
-
if report_id in self._reports:
if self._reports[report_id]['uid'] == uid:
return self._check_report(report_id)
diff --git a/bin/service/websrv_lib.py b/bin/service/websrv_lib.py
index 3f44b6d8314..8565ab6ccaa 100644
--- a/bin/service/websrv_lib.py
+++ b/bin/service/websrv_lib.py
@@ -142,6 +142,9 @@ class noconnection:
def makefile(self, mode, bufsize):
return None
+ def close(self):
+ pass
+
class dummyconn:
def shutdown(self, tru):
pass
@@ -169,10 +172,45 @@ class FixSendError:
self.send_header('Connection', 'close')
self.send_header('Content-Length', len(content) or 0)
self.end_headers()
+ if hasattr(self, '_flush'):
+ self._flush()
+
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
self.wfile.write(content)
-class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
+class HttpOptions:
+ _HTTP_OPTIONS = {'Allow': ['OPTIONS' ] }
+
+ def do_OPTIONS(self):
+ """return the list of capabilities """
+
+ opts = self._HTTP_OPTIONS
+ nopts = self._prep_OPTIONS(opts)
+ if nopts:
+ opts = nopts
+
+ self.send_response(200)
+ self.send_header("Content-Length", 0)
+
+ for key, value in opts.items():
+ if isinstance(value, basestring):
+ self.send_header(key, value)
+ elif isinstance(value, (tuple, list)):
+ self.send_header(key, ', '.join(value))
+ self.end_headers()
+
+ def _prep_OPTIONS(self, opts):
+ """Prepare the OPTIONS response, if needed
+
+ Sometimes, like in special DAV folders, the OPTIONS may contain
+ extra keywords, perhaps also dependant on the request url.
+ @param the options already. MUST be copied before being altered
+ @return the updated options.
+
+ """
+ return opts
+
+class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
""" this is a multiple handler, that will dispatch each request
to a nested handler, iff it matches
@@ -208,7 +246,7 @@ class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
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)
+ self.send_error(403)
return
self._get_ignore_body(fore) # consume any body that came, not loose sync with input
self.send_response(401,'Authorization required')
@@ -221,16 +259,35 @@ class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
return
except AuthRejectedExc,e:
self.log_error("Rejected auth: %s" % e.args[0])
- self.send_error(401,e.args[0])
+ self.send_error(403,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)
+ if fore.command == 'OPTIONS':
+ self.do_OPTIONS()
+ return
+ self.send_error(501, "Unsupported method (%r)" % fore.command)
return
fore.close_connection = 0
method = getattr(fore, mname)
- method()
+ try:
+ method()
+ except (AuthRejectedExc, AuthRequiredExc):
+ raise
+ except Exception, e:
+ if hasattr(self, 'log_exception'):
+ self.log_exception("Could not run %s", mname)
+ else:
+ self.log_error("Could not run %s: %s", mname, e)
+ self.send_error(500, "Internal error")
+ # may not work if method has already sent data
+ fore.close_connection = 1
+ self.close_connection = 1
+ if hasattr(fore, '_flush'):
+ fore._flush()
+ return
+
if fore.close_connection:
# print "Closing connection because of handler"
self.close_connection = fore.close_connection
@@ -289,6 +346,7 @@ class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
[command, path] = words
self.close_connection = 1
if command != 'GET':
+ self.log_error("Junk http request: %s", self.raw_requestline)
self.send_error(400,
"Bad HTTP/0.9 request type (%r)" % command)
return False
@@ -315,6 +373,14 @@ class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
self.log_message("Could not parse rawline.")
return
# self.parse_request(): # Do NOT parse here. the first line should be the only
+
+ if self.path == '*' and self.command == 'OPTIONS':
+ # special handling of path='*', must not use any vdir at all.
+ if not self.parse_request():
+ return
+ self.do_OPTIONS()
+ return
+
for vdir in self.server.vdirs:
p = vdir.matches(self.path)
if p == False:
@@ -397,13 +463,26 @@ class ConnThreadingMixIn:
# main process
daemon_threads = False
+ def _get_next_name(self):
+ return None
+
def _handle_request_noblock(self):
"""Start a new thread to process the request."""
- t = threading.Thread(target = self._handle_request2)
+ if not threading: # happens while quitting python
+ return
+ t = threading.Thread(name=self._get_next_name(), target=self._handle_request2)
if self.daemon_threads:
t.setDaemon (1)
t.start()
+ def _mark_start(self, thread):
+ """ Mark the start of a request thread """
+ pass
+
+ def _mark_end(self, thread):
+ """ Mark the end of a request thread """
+ pass
+
def _handle_request2(self):
"""Handle one request, without blocking.
@@ -412,14 +491,17 @@ class ConnThreadingMixIn:
no risk of blocking in get_request().
"""
try:
+ self._mark_start(threading.currentThread())
request, client_address = self.get_request()
+ if self.verify_request(request, client_address):
+ try:
+ self.process_request(request, client_address)
+ except Exception:
+ self.handle_error(request, client_address)
+ self.close_request(request)
except socket.error:
return
- if self.verify_request(request, client_address):
- try:
- self.process_request(request, client_address)
- except Exception:
- self.handle_error(request, client_address)
- self.close_request(request)
+ finally:
+ self._mark_end(threading.currentThread())
#eof
diff --git a/bin/sql_db.py b/bin/sql_db.py
index 02b3e57676c..6c2c17beeb3 100644
--- a/bin/sql_db.py
+++ b/bin/sql_db.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
#
@@ -15,12 +15,13 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
__all__ = ['db_connect', 'close_db']
+from threading import currentThread
import logging
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE
from psycopg2.psycopg1 import cursor as psycopg1cursor
@@ -77,7 +78,11 @@ class Cursor(object):
def __init__(self, pool, dbname, serialized=False):
self.sql_from_log = {}
self.sql_into_log = {}
- self.sql_log = False
+
+ # default log level determined at cursor creation, could be
+ # overridden later for debugging purposes
+ self.sql_log = self.__logger.isEnabledFor(logging.DEBUG_SQL)
+
self.sql_log_count = 0
self.__closed = True # avoid the call of close() (by __del__) if an exception
# is raised by any of the following initialisations
@@ -103,13 +108,11 @@ class Cursor(object):
self._close(True)
@check
- def execute(self, query, params=None):
+ def execute(self, query, params=None, log_exceptions=True):
if '%d' in query or '%f' in query:
self.__logger.warn(query)
self.__logger.warn("SQL queries cannot contain %d or %f anymore. "
"Use only %s")
- if params:
- query = query.replace('%d', '%s').replace('%f', '%s')
if self.sql_log:
now = mdt.now()
@@ -118,17 +121,19 @@ class Cursor(object):
params = params or None
res = self._obj.execute(query, params)
except psycopg2.ProgrammingError, pe:
- self.__logger.error("Programming error: %s, in query %s", pe, query)
+ if log_exceptions or self.sql_log:
+ self.__logger.error("Programming error: %s, in query %s", pe, query)
raise
except Exception:
- self.__logger.exception("bad query: %s", self._obj.query)
+ if log_exceptions or self.sql_log:
+ self.__logger.exception("bad query: %s", self._obj.query or query)
raise
if self.sql_log:
delay = mdt.now() - now
delay = delay.seconds * 1E6 + delay.microseconds
- self.__logger.debug("query: %s", self._obj.query)
+ self.__logger.log(logging.DEBUG_SQL, "query: %s", self._obj.query)
self.sql_log_count+=1
res_from = re_from.match(query.lower())
if res_from:
@@ -159,15 +164,16 @@ class Cursor(object):
if sqllogs[type]:
sqllogitems = sqllogs[type].items()
sqllogitems.sort(key=lambda k: k[1][1])
- self.__logger.debug("SQL LOG %s:", type)
+ self.__logger.log(logging.DEBUG_SQL, "SQL LOG %s:", type)
+ sqllogitems.sort(lambda x,y: cmp(x[1][0], y[1][0]))
for r in sqllogitems:
delay = timedelta(microseconds=r[1][1])
- self.__logger.debug("table: %s: %s/%s",
+ self.__logger.log(logging.DEBUG_SQL, "table: %s: %s/%s",
r[0], delay, r[1][0])
sum+= r[1][1]
sqllogs[type].clear()
sum = timedelta(microseconds=sum)
- self.__logger.debug("SUM %s:%s/%d [%d]",
+ self.__logger.log(logging.DEBUG_SQL, "SUM %s:%s/%d [%d]",
type, sum, self.sql_log_count, sql_counter)
sqllogs[type].clear()
process('from')
@@ -251,8 +257,7 @@ class ConnectionPool(object):
return "ConnectionPool(used=%d/count=%d/max=%d)" % (used, count, self._maxconn)
def _debug(self, msg, *args):
- msg = '%r ' + msg
- self.__logger.debug(msg, self, *args)
+ self.__logger.log(logging.DEBUG_SQL, ('%r ' + msg), self, *args)
@locked
def borrow(self, dsn):
@@ -264,7 +269,7 @@ class ConnectionPool(object):
delattr(cnx, 'leaked')
self._connections.pop(i)
self._connections.append((cnx, False))
- self._debug('Free leaked connection to %r', cnx.dsn)
+ self.__logger.warn('%r: Free leaked connection to %r', self, cnx.dsn)
for i, (cnx, used) in enumerate(self._connections):
if not used and dsn_are_equals(cnx.dsn, dsn):
@@ -307,7 +312,7 @@ class ConnectionPool(object):
@locked
def close_all(self, dsn):
- self._debug('Close all connections to %r', dsn)
+ self.__logger.info('%r: Close all connections to %r', self, dsn)
for i, (cnx, used) in tools.reverse_enumerate(self._connections):
if dsn_are_equals(cnx.dsn, dsn):
cnx.close()
@@ -323,7 +328,7 @@ class Connection(object):
def cursor(self, serialized=False):
cursor_type = serialized and 'serialized ' or ''
- self.__logger.debug('create %scursor to %r', cursor_type, self.dbname)
+ self.__logger.log(logging.DEBUG_SQL, 'create %scursor to %r', cursor_type, self.dbname)
return Cursor(self._pool, self.dbname, serialized=serialized)
def serialized_cursor(self):
@@ -359,11 +364,15 @@ def dsn_are_equals(first, second):
_Pool = ConnectionPool(int(tools.config['db_maxconn']))
def db_connect(db_name):
+ currentThread().dbname = db_name
return Connection(_Pool, db_name)
def close_db(db_name):
_Pool.close_all(dsn(db_name))
tools.cache.clean_caches_for_db(db_name)
+ ct = currentThread()
+ if hasattr(ct, 'dbname'):
+ delattr(ct, 'dbname')
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/test/__init__.py b/bin/test/__init__.py
new file mode 100644
index 00000000000..86872258874
--- /dev/null
+++ b/bin/test/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 OpenERP S.A. http://www.openerp.com
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from test_osv import *
diff --git a/bin/test/test_osv.py b/bin/test/test_osv.py
new file mode 100644
index 00000000000..191ca792a2f
--- /dev/null
+++ b/bin/test/test_osv.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010 OpenERP S.A. http://www.openerp.com
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import unittest
+from osv.query import Query
+
+class QueryTestCase(unittest.TestCase):
+
+ def test_basic_query(self):
+ query = Query()
+ query.tables.extend(['"product_product"','"product_template"'])
+ query.where_clause.append("product_product.template_id = product_template.id")
+ query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
+ query.join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join
+ self.assertEquals(query.get_sql()[0].strip(),
+ """"product_product" LEFT JOIN "res_user" ON ("product_product"."user_id" = "res_user"."id"),"product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") """.strip())
+ self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
+
+ def test_query_chained_explicit_joins(self):
+ query = Query()
+ query.tables.extend(['"product_product"','"product_template"'])
+ query.where_clause.append("product_product.template_id = product_template.id")
+ query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
+ query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
+ self.assertEquals(query.get_sql()[0].strip(),
+ """"product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id")""".strip())
+ self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
+
+ def test_mixed_query_chained_explicit_implicit_joins(self):
+ query = Query()
+ query.tables.extend(['"product_product"','"product_template"'])
+ query.where_clause.append("product_product.template_id = product_template.id")
+ query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
+ query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
+ query.tables.append('"account.account"')
+ query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join
+ self.assertEquals(query.get_sql()[0].strip(),
+ """"product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id"),"account.account" """.strip())
+ self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id AND product_category.expense_account_id = account_account.id""".strip())
+
+
+ def test_raise_missing_lhs(self):
+ query = Query()
+ query.tables.append('"product_product"')
+ self.assertRaises(AssertionError, query.join, ("product_template", "product_category", "categ_id", "id"), outer=False)
+
diff --git a/bin/tools/__init__.py b/bin/tools/__init__.py
index 9b7bb84a57f..f1a0faf0f8f 100644
--- a/bin/tools/__init__.py
+++ b/bin/tools/__init__.py
@@ -29,6 +29,7 @@ from graph import graph
from amount_to_text import *
from amount_to_text_en import *
from pdf_utils import *
+from yaml_import import *
from sql import *
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/tools/config.py b/bin/tools/config.py
index 85c7f398b62..f98cdbab1b9 100644
--- a/bin/tools/config.py
+++ b/bin/tools/config.py
@@ -68,7 +68,7 @@ class configmanager(object):
'import_partial': "",
'pidfile': None,
'logfile': None,
- 'logrotate': '1',
+ 'logrotate': True,
'smtp_server': 'localhost',
'smtp_user': False,
'smtp_port':25,
@@ -82,9 +82,15 @@ class configmanager(object):
'login_message': False,
'list_db' : True,
'timezone' : False, # to override the default TZ
- 'test-file' : False,
- 'test-disable' : False,
- 'test-commit' : False,
+ 'test_file' : False,
+ 'test_report_directory' : False,
+ 'test_disable' : False,
+ 'test_commit' : False,
+ 'static_http_enable': False,
+ 'static_http_document_root': None,
+ 'static_http_url_prefix': None,
+ 'secure_cert_file': 'server.cert',
+ 'secure_pkey_file': 'server.pkey',
}
self.misc = {}
@@ -92,7 +98,7 @@ class configmanager(object):
self.has_ssl = check_ssl()
self._LOGLEVELS = dict([(getattr(netsvc, 'LOG_%s' % x), getattr(logging, x))
- for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'TEST', 'DEBUG', 'DEBUG_RPC', 'NOTSET')])
+ for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'TEST', 'DEBUG', 'DEBUG_RPC', 'DEBUG_SQL', 'DEBUG_RPC_ANSWER','NOTSET')])
version = "%s %s" % (release.description, release.version)
self.parser = parser = optparse.OptionParser(version=version)
@@ -116,23 +122,31 @@ class configmanager(object):
group.add_option("--xmlrpcs-interface", dest="xmlrpcs_interface", help="specify the TCP IP address for the XML-RPC Secure protocol")
group.add_option("--xmlrpcs-port", dest="xmlrpcs_port", help="specify the TCP port for the XML-RPC Secure protocol", type="int")
group.add_option("--no-xmlrpcs", dest="xmlrpcs", action="store_false", help="disable the XML-RPC Secure protocol")
- group.add_option("--cert-file", dest="secure_cert_file", default="server.cert", help="specify the certificate file for the SSL connection")
- group.add_option("--pkey-file", dest="secure_pkey_file", default="server.pkey", help="specify the private key file for the SSL connection")
+ group.add_option("--cert-file", dest="secure_cert_file", help="specify the certificate file for the SSL connection")
+ group.add_option("--pkey-file", dest="secure_pkey_file", help="specify the private key file for the SSL connection")
parser.add_option_group(group)
+ # NET-RPC
group = optparse.OptionGroup(parser, "NET-RPC Configuration")
group.add_option("--netrpc-interface", dest="netrpc_interface", help="specify the TCP IP address for the NETRPC protocol")
group.add_option("--netrpc-port", dest="netrpc_port", help="specify the TCP port for the NETRPC protocol", type="int")
group.add_option("--no-netrpc", dest="netrpc", action="store_false", help="disable the NETRPC protocol")
parser.add_option_group(group)
+ # Static HTTP
+ group = optparse.OptionGroup(parser, "Static HTTP service")
+ group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", default=False, help="enable static HTTP service for serving plain HTML files")
+ group.add_option("--static-http-document-root", dest="static_http_document_root", help="specify the directory containing your static HTML files (e.g '/var/www/')")
+ group.add_option("--static-http-url-prefix", dest="static_http_url_prefix", help="specify the URL root prefix where you want web browsers to access your static HTML files (e.g '/')")
+ parser.add_option_group(group)
+
parser.add_option("-i", "--init", dest="init", help="init a module (use \"all\" for all modules)")
parser.add_option("--without-demo", dest="without_demo",
help="load demo data for a module (use \"all\" for all modules)", default=False)
parser.add_option("-u", "--update", dest="update",
help="update a module (use \"all\" for all modules)")
parser.add_option("--cache-timeout", dest="cache_timeout",
- help="set the timeout for the cache system", default=100000, type="int")
+ help="set the timeout for the cache system", type="int")
parser.add_option("-t", "--timezone", dest="timezone", help="specify reference timezone for the server (e.g. Europe/Brussels")
# stops the server from launching after initialization
@@ -145,6 +159,7 @@ class configmanager(object):
# Testing Group
group = optparse.OptionGroup(parser, "Testing Configuration")
group.add_option("--test-file", dest="test_file", help="Launch a YML test file.")
+ group.add_option("--test-report-directory", dest="test_report_directory", help="If set, will save sample of all reports in this directory.")
group.add_option("--test-disable", action="store_true", dest="test_disable",
default=False, help="Disable loading test files.")
group.add_option("--test-commit", action="store_true", dest="test_commit",
@@ -155,7 +170,7 @@ class configmanager(object):
group = optparse.OptionGroup(parser, "Logging Configuration")
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")
+ help="do not rotate the logfile")
group.add_option("--syslog", action="store_true", dest="syslog",
default=False, help="Send the log to the syslog server")
group.add_option('--log-level', dest='log_level', type='choice', choices=self._LOGLEVELS.keys(),
@@ -164,12 +179,12 @@ class configmanager(object):
# SMTP Group
group = optparse.OptionGroup(parser, "SMTP Configuration")
- group.add_option('--email-from', dest='email_from', default='', help='specify the SMTP email address for sending email')
- group.add_option('--smtp', dest='smtp_server', default='', help='specify the SMTP server for sending email')
- group.add_option('--smtp-port', dest='smtp_port', default='25', help='specify the SMTP port', type="int")
- group.add_option('--smtp-ssl', dest='smtp_ssl', default='', help='specify the SMTP server support SSL or not')
- group.add_option('--smtp-user', dest='smtp_user', default='', help='specify the SMTP username for sending email')
- group.add_option('--smtp-password', dest='smtp_password', default='', help='specify the SMTP password for sending email')
+ group.add_option('--email-from', dest='email_from', help='specify the SMTP email address for sending email')
+ group.add_option('--smtp', dest='smtp_server', help='specify the SMTP server for sending email')
+ group.add_option('--smtp-port', dest='smtp_port', help='specify the SMTP port', type="int")
+ group.add_option('--smtp-ssl', dest='smtp_ssl', action='store_true', help='specify the SMTP server support SSL or not')
+ group.add_option('--smtp-user', dest='smtp_user', help='specify the SMTP username for sending email')
+ group.add_option('--smtp-password', dest='smtp_password', help='specify the SMTP password for sending email')
parser.add_option_group(group)
group = optparse.OptionGroup(parser, "Database related options")
@@ -179,7 +194,7 @@ class configmanager(object):
group.add_option("--pg_path", dest="pg_path", help="specify the pg executable path")
group.add_option("--db_host", dest="db_host", help="specify the database host")
group.add_option("--db_port", dest="db_port", help="specify the database port", type="int")
- group.add_option("--db_maxconn", dest="db_maxconn", default='64',
+ group.add_option("--db_maxconn", dest="db_maxconn", type='int',
help="specify the the maximum number of physical connections to posgresql")
group.add_option("-P", "--import-partial", dest="import_partial",
help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.", default=False)
@@ -205,10 +220,7 @@ class configmanager(object):
parser.add_option_group(group)
security = optparse.OptionGroup(parser, 'Security-related options')
- security.add_option('--no-database-list', action="store_false", dest='list_db', default=True, help="disable the ability to return the list of databases")
- security.add_option('--enable-code-actions', action='store_true',
- dest='server_actions_allow_code', default=False,
- help='Enables server actions of state "code". Warning, this is a security risk.')
+ security.add_option('--no-database-list', action="store_false", dest='list_db', help="disable the ability to return the list of databases")
parser.add_option_group(security)
def parse_config(self):
@@ -256,23 +268,25 @@ class configmanager(object):
self.options['pidfile'] = False
keys = ['xmlrpc_interface', 'xmlrpc_port', 'db_name', 'db_user', 'db_password', 'db_host',
- 'db_port', 'list_db', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout','smtp_ssl',
- 'email_from', 'smtp_server', 'smtp_user', 'smtp_password',
+ 'db_port', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout',
+ 'email_from', 'smtp_server', 'smtp_user', 'smtp_password',
'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path',
'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone',
'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs',
- 'secure_cert_file', 'secure_pkey_file'
+ 'secure_cert_file', 'secure_pkey_file',
+ 'static_http_enable', 'static_http_document_root', 'static_http_url_prefix'
]
for arg in keys:
if getattr(opt, arg):
self.options[arg] = getattr(opt, arg)
- keys = ['language', 'translate_out', 'translate_in', 'debug_mode',
- 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog',
- 'list_db', 'server_actions_allow_code']
-
- keys.append('xmlrpcs')
+ keys = [
+ 'language', 'translate_out', 'translate_in', 'debug_mode', 'smtp_ssl',
+ 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog',
+ 'list_db', 'xmlrpcs',
+ 'test_file', 'test_disable', 'test_commit', 'test_report_directory'
+ ]
for arg in keys:
if getattr(opt, arg) is not None:
@@ -295,11 +309,7 @@ class configmanager(object):
self.options['init'] = opt.init and dict.fromkeys(opt.init.split(','), 1) or {}
self.options["demo"] = not opt.without_demo and self.options['init'] or {}
- self.options["test-file"] = opt.test_file
- self.options["test-disable"] = opt.test_disable
- self.options["test-commit"] = opt.test_commit
self.options['update'] = opt.update and dict.fromkeys(opt.update.split(','), 1) or {}
-
self.options['translate_modules'] = opt.translate_modules and map(lambda m: m.strip(), opt.translate_modules.split(',')) or ['all']
self.options['translate_modules'].sort()
@@ -440,11 +450,13 @@ class configmanager(object):
# try to create the directories and write the file
try:
- if not os.path.exists(os.path.dirname(self.rcfile)):
+ rc_exists = os.path.exists(self.rcfile)
+ if not rc_exists and not os.path.exists(os.path.dirname(self.rcfile)):
os.makedirs(os.path.dirname(self.rcfile))
try:
p.write(file(self.rcfile, 'w'))
- os.chmod(self.rcfile, 0600)
+ if not rc_exists:
+ os.chmod(self.rcfile, 0600)
except IOError:
sys.stderr.write("ERROR: couldn't write the config file\n")
diff --git a/bin/tools/convert.py b/bin/tools/convert.py
index 20f54e15f11..14823984ca3 100644
--- a/bin/tools/convert.py
+++ b/bin/tools/convert.py
@@ -68,6 +68,24 @@ def _obj(pool, cr, uid, model_str, context=None):
model = pool.get(model_str)
return lambda x: model.browse(cr, uid, x, context=context)
+def _fix_multiple_roots(node):
+ """
+ Surround the children of the ``node`` element of an XML field with a
+ single root "data" element, to prevent having a document with multiple
+ roots once parsed separately.
+
+ XML nodes should have one root only, but we'd like to support
+ direct multiple roots in our partial documents (like inherited view architectures).
+ As a convention we'll surround multiple root with a container "data" element, to be
+ ignored later when parsing.
+ """
+
+ if len(node) > 1:
+ data_node = etree.Element("data")
+ for child in node:
+ data_node.append(child)
+ node.append(data_node)
+
def _eval_xml(self, node, pool, cr, uid, idref, context=None):
if context is None:
context = {}
@@ -102,13 +120,12 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None):
pytz=pytz)
if len(f_model):
idref2['obj'] = _obj(self.pool, cr, uid, f_model, context=context)
-
try:
- return unsafe_eval(a_eval, idref2)
+ return unsafe_eval(a_eval, idref2)
except Exception:
- logger = logging.getLogger('init')
- logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
- return ""
+ logger = logging.getLogger('init')
+ logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
+ return ""
if t == 'xml':
def _process(s, idref):
m = re.findall('[^%]%\((.*?)\)[ds]', s)
@@ -116,6 +133,7 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None):
if not id in idref:
idref[id]=self.id_get(cr, False, id)
return s % idref
+ _fix_multiple_roots(node)
return '\n'\
+_process("".join([etree.tostring(n, encoding='utf-8')
for n in node]),
@@ -198,6 +216,7 @@ class assertion_report(object):
return res
class xml_import(object):
+ __logger = logging.getLogger('tools.convert.xml_import')
@staticmethod
def nodeattr2bool(node, attr, default=False):
if not node.get(attr):
@@ -212,15 +231,24 @@ class xml_import(object):
def get_context(self, data_node, node, eval_dict):
data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
- if data_node_context:
- context = unsafe_eval(data_node_context, eval_dict)
- else:
- context = {}
-
node_context = node.get("context",'').encode('utf8')
- if node_context:
- context.update(unsafe_eval(node_context, eval_dict))
-
+ context = {}
+ for ctx in (data_node_context, node_context):
+ if ctx:
+ try:
+ ctx_res = unsafe_eval(ctx, eval_dict)
+ if isinstance(context, dict):
+ context.update(ctx_res)
+ else:
+ context = ctx_res
+ except NameError:
+ # Some contexts contain references that are only valid at runtime at
+ # client-side, so in that case we keep the original context string
+ # as it is. We also log it, just in case.
+ context = ctx
+ logging.getLogger("init").debug('Context value (%s) for element with id "%s" or its data node does not parse '\
+ 'at server-side, keeping original string, in case it\'s meant for client side only',
+ ctx, node.get('id','n/a'), exc_info=True)
return context
def get_uid(self, cr, uid, data_node, node):
@@ -273,7 +301,7 @@ form: module.record_id""" % (xml_id,)
for dest,f in (('name','string'),('model','model'),('report_name','name')):
res[dest] = rec.get(f,'').encode('utf8')
assert res[dest], "Attribute %s of report is empty !" % (f,)
- for field,dest in (('rml','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
+ for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
if rec.get(field):
res[dest] = rec.get(field).encode('utf8')
if rec.get('auto'):
@@ -390,26 +418,47 @@ form: module.record_id""" % (xml_id,)
if rec.get('view'):
view_id = self.id_get(cr, 'ir.actions.act_window', rec.get('view','').encode('utf-8'))
domain = rec.get('domain','').encode('utf-8') or '{}'
- context = rec.get('context','').encode('utf-8') or '{}'
res_model = rec.get('res_model','').encode('utf-8')
src_model = rec.get('src_model','').encode('utf-8')
view_type = rec.get('view_type','').encode('utf-8') or 'form'
view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
-
usage = rec.get('usage','').encode('utf-8')
limit = rec.get('limit','').encode('utf-8')
auto_refresh = rec.get('auto_refresh','').encode('utf-8')
-
- # WARNING: don't remove the unused variables and functions below as they are
- # used by the unsafe_eval() call.
- # TODO: change this into an eval call with locals and globals parameters
uid = self.uid
active_id = str("active_id") # for further reference in client/bin/tools/__init__.py
def ref(str_id):
return self.id_get(cr, None, str_id)
- context = unsafe_eval(context)
-# domain = eval(domain) # XXX need to test this line -> uid, active_id, active_ids, ...
+ # Include all locals() in eval_context, for backwards compatibility
+ eval_context = {
+ 'name': name,
+ 'xml_id': xml_id,
+ 'type': type,
+ 'view_id': view_id,
+ 'domain': domain,
+ 'res_model': res_model,
+ 'src_model': src_model,
+ 'view_type': view_type,
+ 'view_mode': view_mode,
+ 'usage': usage,
+ 'limit': limit,
+ 'auto_refresh': auto_refresh,
+ 'uid' : uid,
+ 'active_id': active_id,
+ 'ref' : ref,
+ }
+ context = self.get_context(data_node, rec, eval_context)
+
+ try:
+ domain = unsafe_eval(domain, eval_context)
+ except NameError:
+ # Some domains contain references that are only valid at runtime at
+ # client-side, so in that case we keep the original domain string
+ # as it is. We also log it, just in case.
+ logging.getLogger("init").debug('Domain value (%s) for element with id "%s" does not parse '\
+ 'at server-side, keeping original string, in case it\'s meant for client side only',
+ domain, xml_id or 'n/a', exc_info=True)
res = {
'name': name,
'type': type,
@@ -439,6 +488,8 @@ form: module.record_id""" % (xml_id,)
if rec.get('target'):
res['target'] = rec.get('target','')
+ if rec.get('multi'):
+ res['multi'] = rec.get('multi', False)
id = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
self.idref[xml_id] = int(id)
@@ -494,29 +545,35 @@ form: module.record_id""" % (xml_id,)
m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
values = {'parent_id': False}
- if not rec.get('parent'):
+ if rec.get('parent', False) is False and len(m_l) > 1:
+ # No parent attribute specified and the menu name has several menu components,
+ # try to determine the ID of the parent according to menu path
pid = False
+ res = None
+ values['name'] = m_l[-1]
+ m_l = m_l[:-1] # last part is our name, not a parent
for idx, menu_elem in enumerate(m_l):
if pid:
cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
else:
cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
res = cr.fetchone()
- if idx==len(m_l)-1:
- values = {'parent_id': pid,'name':menu_elem}
- elif res:
+ if res:
pid = res[0]
- xml_id = idx==len(m_l)-1 and rec.get('id','').encode('utf8')
- try:
- self.pool.get('ir.model.data')._update_dummy(cr, self.uid, 'ir.ui.menu', self.module, xml_id, idx==len(m_l)-1)
- except:
- self.logger.notifyChannel('init', netsvc.LOG_ERROR, "module: %s xml_id: %s" % (self.module, xml_id))
else:
# the menuitem does't exist but we are in branch (not a leaf)
self.logger.notifyChannel("init", netsvc.LOG_WARNING, 'Warning no ID for submenu %s of menu %s !' % (menu_elem, str(m_l)))
pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
+ values['parent_id'] = pid
else:
- menu_parent_id = self.id_get(cr, 'ir.ui.menu', rec.get('parent',''))
+ # The parent attribute was specified, if non-empty determine its ID, otherwise
+ # explicitly make a top-level menu
+ if rec.get('parent'):
+ menu_parent_id = self.id_get(cr, 'ir.ui.menu', rec.get('parent',''))
+ else:
+ # we get here with , explicit clear of parent, or
+ # if no parent attribute at all but menu name is not a menu path
+ menu_parent_id = False
values = {'parent_id': menu_parent_id}
if rec.get('name'):
values['name'] = rec.get('name')
@@ -713,12 +770,12 @@ form: module.record_id""" % (xml_id,)
for field in rec.findall('./field'):
#TODO: most of this code is duplicated above (in _eval_xml)...
f_name = field.get("name",'').encode('utf-8')
- f_ref = field.get("ref",'').encode('ascii')
+ f_ref = field.get("ref",'').encode('utf-8')
f_search = field.get("search",'').encode('utf-8')
- f_model = field.get("model",'').encode('ascii')
+ f_model = field.get("model",'').encode('utf-8')
if not f_model and model._columns.get(f_name,False):
f_model = model._columns[f_name]._obj
- f_use = field.get("use",'').encode('ascii') or 'id'
+ f_use = field.get("use",'').encode('utf-8') or 'id'
f_val = False
if f_search:
@@ -764,7 +821,9 @@ form: module.record_id""" % (xml_id,)
def id_get(self, cr, model, id_str):
if id_str in self.idref:
return self.idref[id_str]
- return self.model_id_get(cr, model, id_str)[1]
+ res = self.model_id_get(cr, model, id_str)
+ if res and len(res)>1: res = res[1]
+ return res
def model_id_get(self, cr, model, id_str):
model_data_obj = self.pool.get('ir.model.data')
@@ -791,7 +850,10 @@ form: module.record_id""" % (xml_id,)
try:
self._tags[rec.tag](self.cr, rec, n)
except:
- self.logger.notifyChannel("init", netsvc.LOG_ERROR, '\n'+etree.tostring(rec))
+ self.__logger.error('Parse error in %s:%d: \n%s',
+ rec.getroottree().docinfo.URL,
+ rec.sourceline,
+ etree.tostring(rec).strip())
self.cr.rollback()
raise
return True
diff --git a/bin/tools/func.py b/bin/tools/func.py
index 3168f5e6935..8cedf6715fd 100644
--- a/bin/tools/func.py
+++ b/bin/tools/func.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
##############################################################################
-#
+#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL ().
+# Copyright (C) 2010 OpenERP s.a. ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -15,17 +16,17 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
+# along with this program. If not, see .
#
##############################################################################
-__all__ = ['partial', 'wraps', 'update_wrapper']
+__all__ = ['partial', 'wraps', 'update_wrapper', 'synchronized']
try:
from functools import partial, wraps, update_wrapper
except ImportError:
# The functools module doesn't exist in python < 2.5
- # Code taken from python 2.5
+ # Code taken from python 2.5
# http://svn.python.org/view/python/tags/r254/Lib/functools.py?view=markup
def partial(fun, *args, **kwargs):
@@ -76,3 +77,19 @@ except ImportError:
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
+
+
+def synchronized(lock_attr='_lock'):
+ def decorator(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ lock = getattr(self, lock_attr)
+ try:
+ lock.acquire()
+ return func(self, *args, **kwargs)
+ finally:
+ lock.release()
+ return wrapper
+ return decorator
+
+
diff --git a/bin/tools/lru.py b/bin/tools/lru.py
new file mode 100644
index 00000000000..190fe5e4ff6
--- /dev/null
+++ b/bin/tools/lru.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# taken from http://code.activestate.com/recipes/252524-length-limited-o1-lru-cache-implementation/
+import threading
+from func import synchronized
+
+__all__ = ['LRU']
+
+class LRUNode(object):
+ __slots__ = ['prev', 'next', 'me']
+ def __init__(self, prev, me):
+ self.prev = prev
+ self.me = me
+ self.next = None
+
+class LRU:
+ """
+ Implementation of a length-limited O(1) LRU queue.
+ Built for and used by PyPE:
+ http://pype.sourceforge.net
+ Copyright 2003 Josiah Carlson.
+ """
+ def __init__(self, count, pairs=[]):
+ self._lock = threading.RLock()
+ self.count = max(count, 1)
+ self.d = {}
+ self.first = None
+ self.last = None
+ for key, value in pairs:
+ self[key] = value
+
+ @synchronized()
+ def __contains__(self, obj):
+ return obj in self.d
+
+ @synchronized()
+ def __getitem__(self, obj):
+ a = self.d[obj].me
+ self[a[0]] = a[1]
+ return a[1]
+
+ @synchronized()
+ def __setitem__(self, obj, val):
+ if obj in self.d:
+ del self[obj]
+ nobj = LRUNode(self.last, (obj, val))
+ if self.first is None:
+ self.first = nobj
+ if self.last:
+ self.last.next = nobj
+ self.last = nobj
+ self.d[obj] = nobj
+ if len(self.d) > self.count:
+ if self.first == self.last:
+ self.first = None
+ self.last = None
+ return
+ a = self.first
+ a.next.prev = None
+ self.first = a.next
+ a.next = None
+ del self.d[a.me[0]]
+ del a
+
+ @synchronized()
+ def __delitem__(self, obj):
+ nobj = self.d[obj]
+ if nobj.prev:
+ nobj.prev.next = nobj.next
+ else:
+ self.first = nobj.next
+ if nobj.next:
+ nobj.next.prev = nobj.prev
+ else:
+ self.last = nobj.prev
+ del self.d[obj]
+
+ @synchronized()
+ def __iter__(self):
+ cur = self.first
+ while cur != None:
+ cur2 = cur.next
+ yield cur.me[1]
+ cur = cur2
+
+ @synchronized()
+ def __len__(self):
+ return len(self.d)
+
+ @synchronized()
+ def iteritems(self):
+ cur = self.first
+ while cur != None:
+ cur2 = cur.next
+ yield cur.me
+ cur = cur2
+
+ @synchronized()
+ def iterkeys(self):
+ return iter(self.d)
+
+ @synchronized()
+ def itervalues(self):
+ for i,j in self.iteritems():
+ yield j
+
+ @synchronized()
+ def keys(self):
+ return self.d.keys()
+
+ @synchronized()
+ def pop(self,key):
+ v=self[key]
+ del self[key]
+ return v
+
diff --git a/bin/tools/misc.py b/bin/tools/misc.py
index b3489a8a5dc..90b4c135214 100644
--- a/bin/tools/misc.py
+++ b/bin/tools/misc.py
@@ -24,37 +24,33 @@
Miscelleanous tools used by OpenERP.
"""
-import os, time, sys
import inspect
-
-from config import config
-
-import zipfile
-import release
-import socket
import logging
+import os
import re
-from itertools import islice
-import threading
-from which import which
-
import smtplib
+import socket
+import sys
+import threading
+import time
+import zipfile
+from datetime import datetime
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.Header import Header
from email.Utils import formatdate, COMMASPACE
-from email.Utils import formatdate, COMMASPACE
from email import Encoders
-import netsvc
-
-
+from itertools import islice, izip
+from which import which
if sys.version_info[:2] < (2, 4):
from threadinglocal import local
else:
from threading import local
-from itertools import izip
+import netsvc
+from config import config
+from lru import LRU
_logger = logging.getLogger('tools')
@@ -62,9 +58,7 @@ _logger = logging.getLogger('tools')
def init_db(cr):
import addons
f = addons.get_module_resource('base', 'base.sql')
- for line in file_open(f).read().split(';'):
- if (len(line)>0) and (not line.isspace()):
- cr.execute(line)
+ cr.execute(file_open(f).read())
cr.commit()
for i in addons.get_modules():
@@ -112,11 +106,12 @@ def init_db(cr):
id = cr.fetchone()[0]
cr.execute('insert into ir_module_module \
(id, author, website, name, shortdesc, description, \
- category_id, state, certificate) \
- values (%s, %s, %s, %s, %s, %s, %s, %s, %s)', (
+ category_id, state, certificate, web) \
+ values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', (
id, info.get('author', ''),
info.get('website', ''), i, info.get('name', False),
- info.get('description', ''), p_id, state, info.get('certificate') or None))
+ info.get('description', ''), p_id, state, info.get('certificate') or None,
+ info.get('web') or False))
cr.execute('insert into ir_model_data \
(name,model,module, res_id, noupdate) values (%s,%s,%s,%s,%s)', (
'module_meta_information', 'ir.module.module', i, id, True))
@@ -205,18 +200,18 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
subdir2 = (subdir2 != 'addons' or None) and subdir2
for adp in adps:
- try:
- if subdir2:
- fn = os.path.join(adp, subdir2, name)
- else:
- fn = os.path.join(adp, name)
- fn = os.path.normpath(fn)
- fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
- if pathinfo:
- return fo, fn
- return fo
- except IOError, e:
- pass
+ try:
+ if subdir2:
+ fn = os.path.join(adp, subdir2, name)
+ else:
+ fn = os.path.join(adp, name)
+ fn = os.path.normpath(fn)
+ fo = file_open(fn, mode=mode, subdir=None, pathinfo=pathinfo)
+ if pathinfo:
+ return fo, fn
+ return fo
+ except IOError:
+ pass
if subdir:
name = os.path.join(rtp, subdir, name)
@@ -249,7 +244,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
if pathinfo:
return fo, name
return fo
- except:
+ except Exception:
name2 = os.path.normpath(os.path.join(head + '.zip', zipname))
pass
for i in (name2, name):
@@ -350,7 +345,7 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
html = ustr(html)
- from lxml.etree import Element, tostring
+ from lxml.etree import tostring
try:
from lxml.html.soupparser import fromstring
kwargs = {}
@@ -371,7 +366,6 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
url_index = []
i = 0
for link in tree.findall('.//a'):
- title = link.text
url = link.get('href')
if url:
i += 1
@@ -404,6 +398,13 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
return html
+def generate_tracking_message_id(openobject_id):
+ """Returns a string that can be used in the Message-ID RFC822 header field so we
+ can track the replies related to a given object thanks to the "In-Reply-To" or
+ "References" fields that Mail User Agents will set.
+ """
+ return "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
+
def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False, debug=False):
"""Low-level method to send directly a Message through the configured smtp server.
:param smtp_from: RFC-822 envelope FROM (not displayed to recipient)
@@ -414,8 +415,15 @@ def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False,
:return: True if the mail was delivered successfully to the smtp,
else False (+ exception logged)
"""
+ class WriteToLogger(object):
+ def __init__(self):
+ self.logger = netsvc.Logger()
+
+ def write(self, s):
+ self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
+
if openobject_id:
- message['Message-Id'] = "<%s-openobject-%s@%s>" % (time.time(), openobject_id, socket.gethostname())
+ message['Message-Id'] = generate_tracking_message_id(openobject_id)
try:
smtp_server = config['smtp_server']
@@ -424,7 +432,7 @@ def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False,
from mailbox import Maildir
maildir_path = smtp_server[8:]
mdir = Maildir(maildir_path,factory=None, create = True)
- mdir.add(msg.as_string(True))
+ mdir.add(message.as_string(True))
return True
oldstderr = smtplib.stderr
@@ -451,11 +459,11 @@ def _email_send(smtp_from, smtp_to_list, message, openobject_id=None, ssl=False,
s.quit()
if debug:
smtplib.stderr = oldstderr
- except:
+ except Exception:
# ignored, just a consequence of the previous exception
pass
- except Exception, e:
+ except Exception:
_logger.error('could not deliver email', exc_info=True)
return False
@@ -484,18 +492,14 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
"address or having configured one")
if not email_from: email_from = config.get('email_from', False)
+ email_from = ustr(email_from).encode('utf-8')
if not email_cc: email_cc = []
if not email_bcc: email_bcc = []
if not body: body = u''
- try: email_body = body.encode('utf-8')
- except (UnicodeEncodeError, UnicodeDecodeError):
- email_body = body
- try:
- email_text = MIMEText(email_body.encode('utf8') or '',_subtype=subtype,_charset='utf-8')
- except:
- email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
+ email_body = ustr(body).encode('utf-8')
+ email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
if attach: msg = MIMEMultipart()
else: msg = email_text
@@ -529,13 +533,6 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
part.add_header('Content-Disposition', 'attachment; filename="%s"' % (fname,))
msg.attach(part)
- class WriteToLogger(object):
- def __init__(self):
- self.logger = netsvc.Logger()
-
- def write(self, s):
- self.logger.notifyChannel('email_send', netsvc.LOG_DEBUG, s)
-
return _email_send(email_from, flatten([email_to, email_cc, email_bcc]), msg, openobject_id=openobject_id, ssl=ssl, debug=debug)
#----------------------------------------------------------
@@ -547,7 +544,7 @@ def sms_send(user, password, api_id, text, to):
url = "http://api.urlsms.com/SendSMS.aspx"
#url = "http://196.7.150.220/http/sendmsg"
params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
- f = urllib.urlopen(url+"?"+params)
+ urllib.urlopen(url+"?"+params)
# FIXME: Use the logger if there is an error
return True
@@ -700,7 +697,7 @@ class cache(object):
__caches = []
- def __init__(self, timeout=None, skiparg=2, multi=None):
+ def __init__(self, timeout=None, skiparg=2, multi=None, size=8192):
assert skiparg >= 2 # at least self and cr
if timeout is None:
self.timeout = config['cache_timeout']
@@ -709,7 +706,7 @@ class cache(object):
self.skiparg = skiparg
self.multi = multi
self.lasttime = time.time()
- self.cache = {}
+ self.cache = LRU(size) # TODO take size from config
self.fun = None
cache.__caches.append(self)
@@ -867,14 +864,14 @@ def ustr(value, hint_encoding='utf-8'):
try:
return unicode(value)
except Exception:
- raise UnicodeError('unable de to convert %r' % (value,))
+ raise UnicodeError('unable to convert %r' % (value,))
for ln in get_encodings(hint_encoding):
try:
return unicode(value, ln)
except Exception:
pass
- raise UnicodeError('unable de to convert %r' % (value,))
+ raise UnicodeError('unable to convert %r' % (value,))
def exception_to_unicode(e):
@@ -884,7 +881,7 @@ def exception_to_unicode(e):
return "\n".join((ustr(a) for a in e.args))
try:
return ustr(e)
- except:
+ except Exception:
return u"Unknown message"
@@ -918,9 +915,9 @@ def get_iso_codes(lang):
def get_languages():
languages={
- 'ab_RU': u'Abkhazian (RU)',
+ 'ab_RU': u'Abkhazian / аҧсуа',
'ar_AR': u'Arabic / الْعَرَبيّة',
- 'bg_BG': u'Bulgarian / български',
+ 'bg_BG': u'Bulgarian / български език',
'bs_BS': u'Bosnian / bosanski jezik',
'ca_ES': u'Catalan / Català',
'cs_CZ': u'Czech / Čeština',
@@ -931,48 +928,65 @@ def get_languages():
'en_GB': u'English (UK)',
'en_US': u'English (US)',
'es_AR': u'Spanish (AR) / Español (AR)',
+ 'es_BO': u'Spanish (BO) / Español (BO)',
+ 'es_CL': u'Spanish (CL) / Español (CL)',
+ 'es_CO': u'Spanish (CO) / Español (CO)',
+ 'es_CR': u'Spanish (CR) / Español (CR)',
+ 'es_DO': u'Spanish (DO) / Español (DO)',
+ 'es_EC': u'Spanish (EC) / Español (EC)',
'es_ES': u'Spanish / Español',
+ 'es_GT': u'Spanish (GT) / Español (GT)',
+ 'es_HN': u'Spanish (HN) / Español (HN)',
+ 'es_MX': u'Spanish (MX) / Español (MX)',
+ 'es_NI': u'Spanish (NI) / Español (NI)',
+ 'es_PA': u'Spanish (PA) / Español (PA)',
+ 'es_PE': u'Spanish (PE) / Español (PE)',
+ 'es_PR': u'Spanish (PR) / Español (PR)',
+ 'es_PY': u'Spanish (PY) / Español (PY)',
+ 'es_SV': u'Spanish (SV) / Español (SV)',
+ 'es_UY': u'Spanish (UY) / Español (UY)',
+ 'es_VE': u'Spanish (VE) / Español (VE)',
'et_EE': u'Estonian / Eesti keel',
'fa_IR': u'Persian / فارس',
- 'fi_FI': u'Finland / Suomi',
+ 'fi_FI': u'Finnish / Suomi',
'fr_BE': u'French (BE) / Français (BE)',
'fr_CH': u'French (CH) / Français (CH)',
'fr_FR': u'French / Français',
'gl_ES': u'Galician / Galego',
- 'gu_IN': u'Gujarati / India',
- 'hi_IN': u'Hindi / India',
+ 'gu_IN': u'Gujarati / ગુજરાતી',
+ 'hi_IN': u'Hindi / हिंदी',
'hr_HR': u'Croatian / hrvatski jezik',
'hu_HU': u'Hungarian / Magyar',
'id_ID': u'Indonesian / Bahasa Indonesia',
'it_IT': u'Italian / Italiano',
- 'iu_CA': u'Inuktitut / Canada',
- 'ja_JP': u'Japanese / Japan',
- 'ko_KP': u'Korean / Korea, Democratic Peoples Republic of',
- 'ko_KR': u'Korean / Korea, Republic of',
+ 'iu_CA': u'Inuktitut / ᐃᓄᒃᑎᑐᑦ',
+ 'ja_JP': u'Japanese / 日本語',
+ 'ko_KP': u'Korean (KP) / 한국어 (KP)',
+ 'ko_KR': u'Korean (KR) / 한국어 (KR)',
'lt_LT': u'Lithuanian / Lietuvių kalba',
- 'lv_LV': u'Latvian / Latvia',
- 'ml_IN': u'Malayalam / India',
- 'mn_MN': u'Mongolian / Mongolia',
- 'nb_NO': u'Norwegian Bokmål / Norway',
+ 'lv_LV': u'Latvian / latviešu valoda',
+ 'ml_IN': u'Malayalam / മലയാളം',
+ 'mn_MN': u'Mongolian / монгол',
+ 'nb_NO': u'Norwegian Bokmål / Norsk bokmål',
'nl_NL': u'Dutch / Nederlands',
- 'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
- 'oc_FR': u'Occitan (post 1500) / France',
+ 'nl_BE': u'Flemish (BE) / Vlaams (BE)',
+ 'oc_FR': u'Occitan (FR, post 1500) / Occitan',
'pl_PL': u'Polish / Język polski',
- 'pt_BR': u'Portugese (BR) / português (BR)',
- 'pt_PT': u'Portugese / português',
- 'ro_RO': u'Romanian / limba română',
+ 'pt_BR': u'Portugese (BR) / Português (BR)',
+ 'pt_PT': u'Portugese / Português',
+ 'ro_RO': u'Romanian / română',
'ru_RU': u'Russian / русский язык',
- 'si_LK': u'Sinhalese / Sri Lanka',
+ 'si_LK': u'Sinhalese / සිංහල',
'sl_SI': u'Slovenian / slovenščina',
'sk_SK': u'Slovak / Slovenský jazyk',
- 'sq_AL': u'Albanian / Shqipëri',
- 'sr_RS': u'Serbian / Serbia',
+ 'sq_AL': u'Albanian / Shqip',
+ 'sr_RS': u'Serbian / српски језик',
'sv_SE': u'Swedish / svenska',
- 'te_IN': u'Telugu / India',
+ 'te_IN': u'Telugu / తెలుగు',
'tr_TR': u'Turkish / Türkçe',
- 'vi_VN': u'Vietnam / Cộng hòa xã hội chủ nghĩa Việt Nam',
- 'uk_UA': u'Ukrainian / украї́нська мо́ва',
- 'ur_PK': u'Urdu / Pakistan',
+ 'vi_VN': u'Vietnamese / Tiếng Việt',
+ 'uk_UA': u'Ukrainian / українська',
+ 'ur_PK': u'Urdu / اردو',
'zh_CN': u'Chinese (CN) / 简体中文',
'zh_HK': u'Chinese (HK)',
'zh_TW': u'Chinese (TW) / 正體字',
@@ -982,9 +996,6 @@ def get_languages():
return languages
def scan_languages():
-# import glob
-# file_list = [os.path.splitext(os.path.basename(f))[0] for f in glob.glob(os.path.join(config['root_path'],'addons', 'base', 'i18n', '*.po'))]
-# ret = [(lang, lang_dict.get(lang, lang)) for lang in file_list]
# Now it will take all languages from get languages function without filter it with base module languages
lang_dict = get_languages()
ret = [(lang, lang_dict.get(lang, lang)) for lang in list(lang_dict)]
@@ -1042,7 +1053,6 @@ def logged(f):
@wraps(f)
def wrapper(*args, **kwargs):
- import netsvc
from pprint import pformat
vector = ['Call -> function: %r' % f]
@@ -1104,9 +1114,7 @@ def debug(what):
--log-level=debug
"""
- import netsvc
from inspect import stack
- import re
from pprint import pformat
st = stack()[1]
param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
@@ -1156,9 +1164,6 @@ icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_
])
def extract_zip_file(zip_file, outdirectory):
- import zipfile
- import os
-
zf = zipfile.ZipFile(zip_file, 'r')
out = outdirectory
for path in zf.namelist():
@@ -1181,7 +1186,6 @@ def detect_ip_addr():
"""
def _detect_ip_addr():
from array import array
- import socket
from struct import pack, unpack
try:
@@ -1222,7 +1226,7 @@ def detect_ip_addr():
try:
ip_addr = _detect_ip_addr()
- except:
+ except Exception:
ip_addr = 'localhost'
return ip_addr
@@ -1250,7 +1254,7 @@ def get_win32_timezone():
res = str(_winreg.QueryValueEx(current_tz_key,"StandardName")[0]) # [0] is value, [1] is type code
_winreg.CloseKey(current_tz_key)
_winreg.CloseKey(hklm)
- except:
+ except Exception:
pass
return res
@@ -1259,11 +1263,9 @@ def detect_server_timezone():
Defaults to UTC if no working timezone can be found.
@return: the timezone identifier as expected by pytz.timezone.
"""
- import time
- import netsvc
try:
import pytz
- except:
+ except Exception:
netsvc.Logger().notifyChannel("detect_server_timezone", netsvc.LOG_WARNING,
"Python pytz module is not available. Timezone will be set to UTC by default.")
return 'UTC'
@@ -1280,7 +1282,7 @@ def detect_server_timezone():
try:
f = open("/etc/timezone")
tz_value = f.read(128).strip()
- except:
+ except Exception:
pass
finally:
f.close()
@@ -1309,6 +1311,66 @@ def detect_server_timezone():
"No valid timezone could be detected, using default UTC timezone. You can specify it explicitly with option 'timezone' in the server configuration.")
return 'UTC'
+def get_server_timezone():
+ # timezone detection is safe in multithread, so lazy init is ok here
+ if (not config['timezone']):
+ config['timezone'] = detect_server_timezone()
+ return config['timezone']
+
+
+DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
+DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
+DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
+ DEFAULT_SERVER_DATE_FORMAT,
+ DEFAULT_SERVER_TIME_FORMAT)
+
+def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
+ tz_offset=True, ignore_unparsable_time=True):
+ """
+ Convert a source timestamp string into a destination timestamp string, attempting to apply the
+ correct offset if both the server and local timezone are recognized, or no
+ offset at all if they aren't or if tz_offset is false (i.e. assuming they are both in the same TZ).
+
+ WARNING: This method is here to allow formatting dates correctly for inclusion in strings where
+ the client would not be able to format/offset it correctly. DO NOT use it for returning
+ date fields directly, these are supposed to be handled by the client!!
+
+ @param src_tstamp_str: the str value containing the timestamp in the server timezone.
+ @param src_format: the format to use when parsing the server timestamp.
+ @param dst_format: the format to use when formatting the resulting timestamp for the local/client timezone.
+ @param dst_tz_name: name of the destination timezone (such as the 'tz' value of the client context)
+ @param ignore_unparsable_time: if True, return False if src_tstamp_str cannot be parsed
+ using src_format or formatted using dst_format.
+
+ @return: local/client formatted timestamp, expressed in the local/client timezone if possible
+ and if tz_offset is true, or src_tstamp_str if timezone offset could not be determined.
+ """
+ if not src_tstamp_str:
+ return False
+
+ res = src_tstamp_str
+ if src_format and dst_format:
+ # find out server timezone
+ server_tz = get_server_timezone()
+ try:
+ # dt_value needs to be a datetime.datetime object (so no time.struct_time or mx.DateTime.DateTime here!)
+ dt_value = datetime.strptime(src_tstamp_str, src_format)
+ if tz_offset and dst_tz_name:
+ try:
+ import pytz
+ src_tz = pytz.timezone(server_tz)
+ dst_tz = pytz.timezone(dst_tz_name)
+ src_dt = src_tz.localize(dt_value, is_dst=True)
+ dt_value = src_dt.astimezone(dst_tz)
+ except Exception:
+ pass
+ res = dt_value.strftime(dst_format)
+ except Exception:
+ # Normal ways to end up here are if strptime or strftime failed
+ if not ignore_unparsable_time:
+ return False
+ return res
+
def split_every(n, iterable, piece_maker=tuple):
"""Splits an iterable into length-n pieces. The last piece will be shorter
@@ -1337,12 +1399,31 @@ class upload_data_thread(threading.Thread):
fp = urllib.urlopen('http://www.openerp.com/scripts/survey.php', args)
fp.read()
fp.close()
- except:
+ except Exception:
pass
def upload_data(email, data, type='SURVEY'):
a = upload_data_thread(email, data, type)
a.start()
return True
+
+
+# port of python 2.6's attrgetter with support for dotted notation
+def resolve_attr(obj, attr):
+ for name in attr.split("."):
+ obj = getattr(obj, name)
+ return obj
+
+def attrgetter(*items):
+ if len(items) == 1:
+ attr = items[0]
+ def g(obj):
+ return resolve_attr(obj, attr)
+ else:
+ def g(obj):
+ return tuple(resolve_attr(obj, attr) for attr in items)
+ return g
+
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/tools/pdf_utils.py b/bin/tools/pdf_utils.py
index 0286b9cf46d..7d9bf3af700 100644
--- a/bin/tools/pdf_utils.py
+++ b/bin/tools/pdf_utils.py
@@ -33,8 +33,10 @@ without flatten, one could further edit the resulting form.
with flatten, everything is turned into text.
"""
+from __future__ import with_statement
+
import os
-import netsvc,logging
+import tempfile
HEAD="""%FDF-1.2
@@ -91,14 +93,30 @@ def write_fields(out, fields):
def extract_keys_from_pdf(filename):
# what about using 'pdftk filename dump_data_fields' and parsing the output ?
- os.system('pdftk %s generate_fdf output /tmp/toto.fdf' % filename)
- lines = file('/tmp/toto.fdf').readlines()
+ tmp_file = tempfile.mkstemp(".fdf")[1]
+ try:
+ os.system('pdftk %s generate_fdf output \"%s\"' % (filename, tmp_file))
+ with open(tmp_file, "r") as ofile:
+ lines = ofile.readlines()
+ finally:
+ try:
+ os.remove(tmp_file)
+ except Exception:
+ pass # nothing to do
return extract_keys(lines)
def fill_pdf(infile, outfile, fields):
- write_fields(file('/tmp/toto.fdf', 'w'), fields)
- os.system('pdftk %s fill_form /tmp/toto.fdf output %s flatten' % (infile, outfile))
+ tmp_file = tempfile.mkstemp(".fdf")[1]
+ try:
+ with open(tmp_file, "w") as ofile:
+ write_fields(ofile, fields)
+ os.system('pdftk %s fill_form \"%s\" output %s flatten' % (infile, tmp_file, outfile))
+ finally:
+ try:
+ os.remove(tmp_file)
+ except Exception:
+ pass # nothing to do
def testfill_pdf(infile, outfile):
keys = extract_keys_from_pdf(infile)
diff --git a/bin/tools/safe_eval.py b/bin/tools/safe_eval.py
index 0a2a99ce07d..156899908fb 100644
--- a/bin/tools/safe_eval.py
+++ b/bin/tools/safe_eval.py
@@ -35,11 +35,12 @@ condition/math builtins.
from opcode import HAVE_ARGUMENT, opmap, opname
from types import CodeType
import logging
+import os
__all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
_CONST_OPCODES = set(opmap[x] for x in [
- 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP',
+ 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
@@ -53,8 +54,12 @@ _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
_SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
- 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE',
- 'DELETE_NAME', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE','MAKE_FUNCTION',
+ 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
+ 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
+ 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
+ # New in Python 2.7 - http://bugs.python.org/issue4715 :
+ 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
+ 'POP_JUMP_IF_TRUE'
] if x in opmap))
_logger = logging.getLogger('safe_eval')
@@ -258,10 +263,15 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
'dict': dict,
'list': list,
'tuple': tuple,
- 'map' : map,
+ 'map': map,
+ 'abs': abs,
+ 'reduce': reduce,
+ 'filter': filter,
+ 'round': round,
+ 'len': len,
+ 'set' : set
}
)
-
return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/bin/tools/translate.py b/bin/tools/translate.py
index 7648470edb6..c22b2ca53ca 100644
--- a/bin/tools/translate.py
+++ b/bin/tools/translate.py
@@ -23,18 +23,19 @@ import codecs
import csv
import fnmatch
import inspect
+import itertools
import locale
import os
import re
import tarfile
import tempfile
from os.path import join
+import logging
from datetime import datetime
from lxml import etree
-import osv, tools, pooler
-import ir
+import tools, pooler
import netsvc
from tools.misc import UpdateableStr
@@ -95,7 +96,27 @@ _LOCALE2WIN32 = {
'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
'sk_SK': 'Slovak_Slovakia',
'sl_SI': 'Slovenian_Slovenia',
+ #should find more specific locales for spanish countries,
+ #but better than nothing
+ 'es_AR': 'Spanish_Spain',
+ 'es_BO': 'Spanish_Spain',
+ 'es_CL': 'Spanish_Spain',
+ 'es_CO': 'Spanish_Spain',
+ 'es_CR': 'Spanish_Spain',
+ 'es_DO': 'Spanish_Spain',
+ 'es_EC': 'Spanish_Spain',
'es_ES': 'Spanish_Spain',
+ 'es_GT': 'Spanish_Spain',
+ 'es_HN': 'Spanish_Spain',
+ 'es_MX': 'Spanish_Spain',
+ 'es_NI': 'Spanish_Spain',
+ 'es_PA': 'Spanish_Spain',
+ 'es_PE': 'Spanish_Spain',
+ 'es_PR': 'Spanish_Spain',
+ 'es_PY': 'Spanish_Spain',
+ 'es_SV': 'Spanish_Spain',
+ 'es_UY': 'Spanish_Spain',
+ 'es_VE': 'Spanish_Spain',
'sv_SE': 'Swedish_Sweden',
'ta_IN': 'English_Australia',
'th_TH': 'Thai_Thailand',
@@ -127,30 +148,55 @@ def translate(cr, name, source_type, lang, source=None):
res = res_trans and res_trans[0] or False
return res
+logger = logging.getLogger('translate')
+
class GettextAlias(object):
+
+ def _get_cr(self, frame):
+ is_new_cr = False
+ cr = frame.f_locals.get('cr')
+ if not cr:
+ s = frame.f_locals.get('self', {})
+ cr = getattr(s, 'cr', False)
+ if not cr:
+ if frame.f_globals.get('pooler', False):
+ # TODO: we should probably get rid of the 'is_new_cr' case: no cr in locals -> no translation for you
+ dbs = frame.f_globals['pooler'].pool_dic.keys()
+ if len(dbs) == 1:
+ cr = pooler.get_db(dbs[0]).cursor()
+ is_new_cr = True
+ return cr, is_new_cr
+
+ def _get_lang(self, frame):
+ lang = frame.f_locals.get('context', {}).get('lang', False)
+ if not lang:
+ args = frame.f_locals.get('args', False)
+ if args:
+ lang = args[-1].get('lang', False)
+ if not lang:
+ s = frame.f_locals.get('self', {})
+ c = getattr(s, 'localcontext', {})
+ lang = c.get('lang', False)
+ return lang
+
def __call__(self, source):
+ is_new_cr = False
+ res = source
try:
frame = inspect.stack()[1][0]
- except:
- return source
+ cr, is_new_cr = self._get_cr(frame)
+ lang = self._get_lang(frame)
+ if lang and cr:
+ cr.execute('SELECT value FROM ir_translation WHERE lang=%s AND type IN (%s, %s) AND src=%s', (lang, 'code','sql_constraint', source))
+ res_trans = cr.fetchone()
+ res = res_trans and res_trans[0] or source
+ except Exception:
+ logger.debug('translation went wrong for string %s', repr(source))
+ finally:
+ if is_new_cr:
+ cr.close()
+ return res
- cr = frame.f_locals.get('cr')
- try:
- lang = (frame.f_locals.get('context') or {}).get('lang', False)
- if not (cr and lang):
- args = frame.f_locals.get('args',False)
- if args:
- lang = args[-1].get('lang',False)
- if frame.f_globals.get('pooler',False):
- cr = pooler.get_db(frame.f_globals['pooler'].pool_dic.keys()[0]).cursor()
- if not (lang and cr):
- return source
- except:
- return source
-
- cr.execute('select value from ir_translation where lang=%s and type IN (%s,%s) and src=%s', (lang, 'code','sql_constraint', source))
- res_trans = cr.fetchone()
- return res_trans and res_trans[0] or source
_ = GettextAlias()
@@ -375,6 +421,9 @@ def trans_export(lang, modules, buffer, format, dbname=None):
if newlang:
lang = 'en_US'
trans = trans_generate(lang, modules, dbname)
+ if newlang and format!='csv':
+ for trx in trans:
+ trx[-1] = ''
modules = set([t[0] for t in trans[1:]])
_process(format, modules, trans, buffer, lang, newlang)
del trans
@@ -443,7 +492,6 @@ def trans_generate(lang, modules, dbname=None):
query = 'SELECT name, model, res_id, module' \
' FROM ir_model_data'
- query_param = None
if 'all_installed' in modules:
query += ' WHERE module IN ( SELECT name FROM ir_module_module WHERE state = \'installed\') '
query_param = None
@@ -560,7 +608,7 @@ def trans_generate(lang, modules, dbname=None):
push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
- for key, val in field_def.selection:
+ for dummy, val in field_def.selection:
push_translation(module, 'selection', name, 0, encode(val))
elif model=='ir.actions.report.xml':
@@ -569,28 +617,33 @@ def trans_generate(lang, modules, dbname=None):
if obj.report_rml:
fname = obj.report_rml
parse_func = trans_parse_rml
- report_type = "rml"
+ report_type = "report"
elif obj.report_xsl:
fname = obj.report_xsl
parse_func = trans_parse_xsl
report_type = "xsl"
- try:
- xmlstr = tools.file_open(fname).read()
- d = etree.XML(xmlstr)
- for t in parse_func(d):
- push_translation(module, report_type, name, 0, t)
- except IOError, etree.XMLSyntaxError:
- if fname:
- logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't export translation for report %s %s %s" % (name, report_type, fname))
+ if fname and obj.report_type in ('pdf', 'xsl'):
+ try:
+ d = etree.parse(tools.file_open(fname))
+ for t in parse_func(d.iter()):
+ push_translation(module, report_type, name, 0, t)
+ except (IOError, etree.XMLSyntaxError):
+ logging.getLogger("i18n").exception("couldn't export translation for report %s %s %s", name, report_type, fname)
- for constraint in pool.get(model)._constraints:
- msg = constraint[1]
+ model_obj = pool.get(model)
+ def push_constraint_msg(module, term_type, model, msg):
# Check presence of __call__ directly instead of using
# callable() because it will be deprecated as of Python 3.0
if not hasattr(msg, '__call__'):
- push_translation(module, 'constraint', model, 0, encode(msg))
+ push_translation(module, term_type, model, 0, encode(msg))
- for field_name,field_def in pool.get(model)._columns.items():
+ for constraint in model_obj._constraints:
+ push_constraint_msg(module, 'constraint', model, constraint[1])
+
+ for constraint in model_obj._sql_constraints:
+ push_constraint_msg(module, 'sql_constraint', model, constraint[2])
+
+ for field_name,field_def in model_obj._columns.items():
if field_def.translate:
name = model + "," + field_name
try:
@@ -637,22 +690,25 @@ def trans_generate(lang, modules, dbname=None):
else :
path_list = [root_path,tools.config['addons_path']]
- for path in path_list:
- for root, dirs, files in tools.osutil.walksymlinks(path):
- for fname in fnmatch.filter(files, '*.py'):
- fabsolutepath = join(root, fname)
- frelativepath = fabsolutepath[len(path):]
- module = get_module_from_path(fabsolutepath)
- is_mod_installed = module in installed_modules
- if (('all' in modules) or (module in modules)) and is_mod_installed:
- code_string = tools.file_open(fabsolutepath, subdir='').read()
- iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)',
- code_string, re.S)
+ def export_code_terms_from_file(fname, path, root, terms_type):
+ fabsolutepath = join(root, fname)
+ frelativepath = fabsolutepath[len(path):]
+ module = get_module_from_path(frelativepath)
+ is_mod_installed = module in installed_modules
+ if (('all' in modules) or (module in modules)) and is_mod_installed:
+ code_string = tools.file_open(fabsolutepath, subdir='').read()
+ iter = re.finditer('[^a-zA-Z0-9_]_\([\s]*["\'](.+?)["\'][\s]*\)', code_string, re.S)
+ if module in installed_modules:
+ frelativepath = str("addons" + frelativepath)
+ for i in iter:
+ push_translation(module, terms_type, frelativepath, 0, encode(i.group(1)))
- if module in installed_modules :
- frelativepath =str("addons"+frelativepath)
- for i in iter:
- push_translation(module, 'code', frelativepath, 0, encode(i.group(1)))
+ for path in path_list:
+ for root, dummy, files in tools.osutil.walksymlinks(path):
+ for fname in itertools.chain(fnmatch.filter(files, '*.py')):
+ export_code_terms_from_file(fname, path, root, 'code')
+ for fname in itertools.chain(fnmatch.filter(files, '*.mako')):
+ export_code_terms_from_file(fname, path, root, 'report')
out = [["module","type","name","res_id","src","value"]] # header
@@ -665,12 +721,12 @@ def trans_generate(lang, modules, dbname=None):
cr.close()
return out
-def trans_load(db_name, filename, lang, strict=False, verbose=True):
+def trans_load(db_name, filename, lang, strict=False, verbose=True, context={}):
logger = netsvc.Logger()
try:
fileobj = open(filename,'r')
fileformat = os.path.splitext(filename)[-1][1:].lower()
- r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose)
+ r = trans_load_data(db_name, fileobj, fileformat, lang, strict=strict, verbose=verbose, context=context)
fileobj.close()
return r
except IOError:
@@ -678,7 +734,7 @@ def trans_load(db_name, filename, lang, strict=False, verbose=True):
logger.notifyChannel("i18n", netsvc.LOG_ERROR, "couldn't read translation file %s" % (filename,))
return None
-def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True):
+def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=None, verbose=True, context={}):
logger = netsvc.Logger()
if verbose:
logger.notifyChannel("i18n", netsvc.LOG_INFO, 'loading translation file for language %s' % (lang))
@@ -803,7 +859,8 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
('res_id', '=', dic['res_id'])
])
if ids:
- trans_obj.write(cr, uid, ids, {'value': dic['value']})
+ if context.get('overwrite', False):
+ trans_obj.write(cr, uid, ids, {'value': dic['value']})
else:
trans_obj.create(cr, uid, dic)
else:
@@ -814,7 +871,8 @@ def trans_load_data(db_name, fileobj, fileformat, lang, strict=False, lang_name=
('src', '=', dic['src'])
])
if ids:
- trans_obj.write(cr, uid, ids, {'value': dic['value']})
+ if context.get('overwrite', False):
+ trans_obj.write(cr, uid, ids, {'value': dic['value']})
else:
trans_obj.create(cr, uid, dic)
cr.commit()
diff --git a/bin/tools/yaml_import.py b/bin/tools/yaml_import.py
index 45815bf5795..90fedbd0330 100644
--- a/bin/tools/yaml_import.py
+++ b/bin/tools/yaml_import.py
@@ -70,14 +70,16 @@ def is_url(node):
def is_eval(node):
return isinstance(node, yaml_tag.Eval)
-
+
def is_ref(node):
return isinstance(node, yaml_tag.Ref) \
or _is_yaml_mapping(node, yaml_tag.Ref)
-
+
def is_ir_set(node):
return _is_yaml_mapping(node, yaml_tag.IrSet)
+def is_string(node):
+ return isinstance(node, basestring)
class TestReport(object):
def __init__(self):
@@ -250,6 +252,25 @@ class YamlInterpreter(object):
if not success:
msg = 'Assertion "%s" FAILED\ntest: %s\n'
args = (assertion.string, test)
+ for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
+ if aop in test:
+ left, right = test.split(aop,1)
+ lmsg = ''
+ rmsg = ''
+ try:
+ lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
+ except Exception, e:
+ lmsg = ''
+
+ try:
+ rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
+ except Exception, e:
+ rmsg = ''
+
+ msg += 'values: ! %s %s %s'
+ args += ( lmsg, aop, rmsg )
+ break
+
self._log_assert_failure(assertion.severity, msg, *args)
return
else: # all tests were successful for this assertion tag (no break)
@@ -282,7 +303,7 @@ class YamlInterpreter(object):
else:
self.validate_xml_id(record.id)
if self.isnoupdate(record) and self.mode != 'init':
- id = self.pool.get('ir.model.data')._update_dummy(self.cr, self.uid, record.model, self.module, record.id)
+ id = self.pool.get('ir.model.data')._update_dummy(self.cr, 1, record.model, self.module, record.id)
# check if the resource already existed at the last update
if id:
self.id_map[record] = int(id)
@@ -293,8 +314,10 @@ class YamlInterpreter(object):
record_dict = self._create_record(model, fields)
self.logger.debug("RECORD_DICT %s" % record_dict)
- id = self.pool.get('ir.model.data')._update(self.cr, self.uid, record.model, \
- self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode)
+ #context = self.get_context(record, self.eval_context)
+ context = record.context #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
+ id = self.pool.get('ir.model.data')._update(self.cr, 1, record.model, \
+ self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
self.id_map[record.id] = int(id)
if config.get('import_partial'):
self.cr.commit()
@@ -353,6 +376,14 @@ class YamlInterpreter(object):
elif column._type == "many2many":
ids = [self.get_id(xml_id) for xml_id in expression]
value = [(6, 0, ids)]
+ elif column._type == "date" and is_string(expression):
+ # enforce ISO format for string date values, to be locale-agnostic during tests
+ time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
+ value = expression
+ elif column._type == "datetime" and is_string(expression):
+ # enforce ISO format for string datetime values, to be locale-agnostic during tests
+ time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
+ value = expression
else: # scalar field
if is_eval(expression):
value = self.process_eval(expression)
@@ -360,14 +391,14 @@ class YamlInterpreter(object):
value = expression
# raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
return value
-
+
def process_context(self, node):
self.context = node.__dict__
if node.uid:
self.uid = self.get_id(node.uid)
if node.noupdate:
self.noupdate = node.noupdate
-
+
def process_python(self, node):
def log(msg, *args):
self.logger.log(logging.TEST, msg, *args)
@@ -384,7 +415,7 @@ class YamlInterpreter(object):
return
except Exception, e:
self.logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
- raise YamlImportAbortion(e)
+ raise
else:
self.assert_report.record(True, python.severity)
@@ -538,7 +569,7 @@ class YamlInterpreter(object):
self._set_group_values(node, values)
- pid = self.pool.get('ir.model.data')._update(self.cr, self.uid, \
+ pid = self.pool.get('ir.model.data')._update(self.cr, 1, \
'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
@@ -549,7 +580,7 @@ class YamlInterpreter(object):
action_type = node.type or 'act_window'
action_id = self.get_id(node.action)
action = "ir.actions.%s,%d" % (action_type, action_id)
- self.pool.get('ir.model.data').ir_set(self.cr, self.uid, 'action', \
+ self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
def process_act_window(self, node):
@@ -576,13 +607,14 @@ class YamlInterpreter(object):
'usage': node.usage,
'limit': node.limit,
'auto_refresh': node.auto_refresh,
+ 'multi': getattr(node, 'multi', False),
}
self._set_group_values(node, values)
if node.target:
values['target'] = node.target
- id = self.pool.get('ir.model.data')._update(self.cr, self.uid, \
+ id = self.pool.get('ir.model.data')._update(self.cr, 1, \
'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
self.id_map[node.id] = int(id)
@@ -590,7 +622,7 @@ class YamlInterpreter(object):
keyword = 'client_action_relate'
value = 'ir.actions.act_window,%s' % id
replace = node.replace or True
- self.pool.get('ir.model.data').ir_set(self.cr, self.uid, 'action', keyword, \
+ self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', keyword, \
node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
# TODO add remove ir.model.data
@@ -603,7 +635,7 @@ class YamlInterpreter(object):
ids = [self.get_id(node.id)]
if len(ids):
self.pool.get(node.model).unlink(self.cr, self.uid, ids)
- self.pool.get('ir.model.data')._unlink(self.cr, self.uid, node.model, ids)
+ self.pool.get('ir.model.data')._unlink(self.cr, 1, node.model, ids)
else:
self.logger.log(logging.TEST, "Record not deleted.")
@@ -612,7 +644,7 @@ class YamlInterpreter(object):
res = {'name': node.name, 'url': node.url, 'target': node.target}
- id = self.pool.get('ir.model.data')._update(self.cr, self.uid, \
+ id = self.pool.get('ir.model.data')._update(self.cr, 1, \
"ir.actions.url", self.module, res, node.id, mode=self.mode)
self.id_map[node.id] = int(id)
# ir_set
@@ -620,7 +652,7 @@ class YamlInterpreter(object):
keyword = node.keyword or 'client_action_multi'
value = 'ir.actions.url,%s' % id
replace = node.replace or True
- self.pool.get('ir.model.data').ir_set(self.cr, self.uid, 'action', \
+ self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
keyword, node.url, ["ir.actions.url"], value, replace=replace, \
noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
@@ -635,7 +667,7 @@ class YamlInterpreter(object):
else:
value = expression
res[fieldname] = value
- self.pool.get('ir.model.data').ir_set(self.cr, self.uid, res['key'], res['key2'], \
+ self.pool.get('ir.model.data').ir_set(self.cr, 1, res['key'], res['key2'], \
res['name'], res['models'], res['value'], replace=res.get('replace',True), \
isobject=res.get('isobject', False), meta=res.get('meta',None))
@@ -644,7 +676,7 @@ class YamlInterpreter(object):
for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
values[dest] = getattr(node, f)
assert values[dest], "Attribute %s of report is empty !" % (f,)
- for field,dest in (('rml','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
+ for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
if getattr(node, field):
values[dest] = getattr(node, field)
if node.auto:
@@ -660,7 +692,7 @@ class YamlInterpreter(object):
self._set_group_values(node, values)
- id = self.pool.get('ir.model.data')._update(self.cr, self.uid, "ir.actions.report.xml", \
+ id = self.pool.get('ir.model.data')._update(self.cr, 1, "ir.actions.report.xml", \
self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
self.id_map[xml_id] = int(id)
@@ -668,7 +700,7 @@ class YamlInterpreter(object):
keyword = node.keyword or 'client_print_multi'
value = 'ir.actions.report.xml,%s' % id
replace = node.replace or True
- self.pool.get('ir.model.data').ir_set(self.cr, self.uid, 'action', \
+ self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
def process_none(self):
diff --git a/bin/tools/yaml_tag.py b/bin/tools/yaml_tag.py
index 817f7d55a3c..e12aecdcd96 100644
--- a/bin/tools/yaml_tag.py
+++ b/bin/tools/yaml_tag.py
@@ -4,6 +4,7 @@ import logging
class YamlTag(object):
"""
Superclass for constructors of custom tags defined in yaml file.
+ __str__ is overriden in subclass and used for serialization in module recorder.
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
@@ -27,6 +28,8 @@ class Record(YamlTag):
self.model = model
self.id = id
super(Record, self).__init__(**kwargs)
+ def __str__(self):
+ return '!record {model: %s, id: %s}:' % (str(self.model,), str(self.id,))
class Python(YamlTag):
def __init__(self, model, severity=logging.WARNING, name="", **kwargs):
@@ -34,6 +37,8 @@ class Python(YamlTag):
self.severity = severity
self.name = name
super(Python, self).__init__(**kwargs)
+ def __str__(self):
+ return '!python {model: %s}: |' % (str(self.model), )
class Menuitem(YamlTag):
def __init__(self, id, name, **kwargs):
@@ -42,10 +47,13 @@ class Menuitem(YamlTag):
super(Menuitem, self).__init__(**kwargs)
class Workflow(YamlTag):
- def __init__(self, model, action, **kwargs):
+ def __init__(self, model, action, ref=None, **kwargs):
self.model = model
self.action = action
+ self.ref = ref
super(Workflow, self).__init__(**kwargs)
+ def __str__(self):
+ return '!workflow {model: %s, action: %s, ref: %s}' % (str(self.model,), str(self.action,), str(self.ref,))
class ActWindow(YamlTag):
def __init__(self, **kwargs):
@@ -80,10 +88,15 @@ class Eval(YamlTag):
def __init__(self, expression):
self.expression = expression
super(Eval, self).__init__()
+ def __str__(self):
+ return '!eval %s' % str(self.expression)
class Ref(YamlTag):
- def __init__(self, *args, **kwargs):
+ def __init__(self, expr="False", *args, **kwargs):
+ self.expr = expr
super(Ref, self).__init__(*args, **kwargs)
+ def __str__(self):
+ return 'ref(%s)' % repr(self.expr)
class IrSet(YamlTag):
def __init__(self):
@@ -166,4 +179,5 @@ def add_constructors():
yaml.add_constructor(u"!eval", eval_constructor)
yaml.add_multi_constructor(u"!ref", ref_constructor)
yaml.add_constructor(u"!ir_set", ir_set_constructor)
-add_constructors()
\ No newline at end of file
+add_constructors()
+
diff --git a/bin/workflow/wkf_expr.py b/bin/workflow/wkf_expr.py
index 0150105c3ab..71a15a181f9 100644
--- a/bin/workflow/wkf_expr.py
+++ b/bin/workflow/wkf_expr.py
@@ -68,17 +68,17 @@ def execute(cr, ident, workitem, activity):
return _eval_expr(cr, ident, workitem, activity['action'])
def check(cr, workitem, ident, transition, signal):
- ok = True
- if transition['signal']:
- ok = (signal==transition['signal'])
+ if transition['signal'] and signal != transition['signal']:
+ return False
uid = ident[0]
- if transition['role_id'] and uid != 1:
+ if transition['group_id'] and uid != 1:
pool = pooler.get_pool(cr.dbname)
- user_roles = pool.get('res.users').read(cr, uid, [uid], ['roles_id'])[0]['roles_id']
- ok = ok and pool.get('res.roles').check(cr, uid, user_roles, transition['role_id'])
- ok = ok and _eval_expr(cr, ident, workitem, transition['condition'])
- return ok
+ user_groups = pool.get('res.users').read(cr, uid, [uid], ['groups_id'])[0]['groups_id']
+ if not transition['group_id'] in user_groups:
+ return False
+
+ return _eval_expr(cr, ident, workitem, transition['condition'])
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/debian/copyright b/debian/copyright
index 6a75021214c..26494c03fc4 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -162,7 +162,7 @@ License: GPL-2+
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/*
+Files: bin/addons/l10n_uk/*
Copyright:
(C) 2004-2008 Tiny.be
(C) 2004-2008 Seath Solutions Ltd.
diff --git a/debian/po/bg.po b/debian/po/bg.po
index d8f13067dda..64571e2e63f 100644
--- a/debian/po/bg.po
+++ b/debian/po/bg.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/ca.po b/debian/po/ca.po
index 77177aaa666..b549feb2080 100644
--- a/debian/po/ca.po
+++ b/debian/po/ca.po
@@ -15,7 +15,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/cs.po b/debian/po/cs.po
index 1f7fc1c4c58..4db913ca2d2 100644
--- a/debian/po/cs.po
+++ b/debian/po/cs.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/de.po b/debian/po/de.po
index 08fb34dfca1..43f5b2f2589 100644
--- a/debian/po/de.po
+++ b/debian/po/de.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/el.po b/debian/po/el.po
index ab9a69979ea..2d7be856aea 100644
--- a/debian/po/el.po
+++ b/debian/po/el.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/en_GB.po b/debian/po/en_GB.po
index d3ca66429cd..a1f484305ba 100644
--- a/debian/po/en_GB.po
+++ b/debian/po/en_GB.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/es.po b/debian/po/es.po
index 7ad0e2b4434..46d906b1248 100644
--- a/debian/po/es.po
+++ b/debian/po/es.po
@@ -15,7 +15,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/es_EC.po b/debian/po/es_EC.po
index 6d951e15c24..03035c61d26 100644
--- a/debian/po/es_EC.po
+++ b/debian/po/es_EC.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/fa.po b/debian/po/fa.po
index 436ac5811d6..331aa5d0d9d 100644
--- a/debian/po/fa.po
+++ b/debian/po/fa.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/fi.po b/debian/po/fi.po
index 72784367e2e..7dc02829e54 100644
--- a/debian/po/fi.po
+++ b/debian/po/fi.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/fr.po b/debian/po/fr.po
index 7bd2d8f0ed5..5aa65424e38 100644
--- a/debian/po/fr.po
+++ b/debian/po/fr.po
@@ -7,20 +7,20 @@ 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-11-26 07:45+0000\n"
-"Last-Translator: Steve Petruzzello \n"
+"PO-Revision-Date: 2010-10-13 07:40+0000\n"
+"Last-Translator: Quentin THEURET \n"
"Language-Team: French \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-10-14 04:44+0000\n"
"X-Generator: Launchpad (build Unknown)\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:"
+msgstr "Compte dédié au serveur Open ERP :"
#. Type: string
#. Description
@@ -30,12 +30,12 @@ msgid ""
"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."
+"Afin de ne pas compromettre la sécurité du système, les opérations du "
+"serveur Open ERP doivent être exécutée avec un compte dédié et différent du "
+"superutilisateur."
#. Type: string
#. Description
#: ../openerp-server.templates:1001
msgid "Please choose that account's username."
-msgstr "Veuillez choisir cet identifiant."
+msgstr "Veuillez choisir l'identifiant de ce compte."
diff --git a/debian/po/gl.po b/debian/po/gl.po
index 6be40ac91f6..2fa653b1970 100644
--- a/debian/po/gl.po
+++ b/debian/po/gl.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/hr.po b/debian/po/hr.po
index 00706c0c5b6..af9c295a595 100644
--- a/debian/po/hr.po
+++ b/debian/po/hr.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/it.po b/debian/po/it.po
index f2ed8ac826c..45bd312e0c1 100644
--- a/debian/po/it.po
+++ b/debian/po/it.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/ja.po b/debian/po/ja.po
index b197d925b88..20e0bc105be 100644
--- a/debian/po/ja.po
+++ b/debian/po/ja.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/ko.po b/debian/po/ko.po
index 268d893c01c..155d2266c9a 100644
--- a/debian/po/ko.po
+++ b/debian/po/ko.po
@@ -8,20 +8,20 @@ msgstr ""
"Project-Id-Version: openobject-server\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
-"PO-Revision-Date: 2010-02-16 05:47+0000\n"
-"Last-Translator: FULL NAME \n"
+"PO-Revision-Date: 2010-09-29 08:30+0000\n"
+"Last-Translator: OpenERP Administrators \n"
"Language-Team: Korean \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-30 04:38+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
#. Description
#: ../openerp-server.templates:1001
msgid "Dedicated system account for the Open ERP server:"
-msgstr ""
+msgstr "Open ERP 서버를 위한 전용 시스템 계정:"
#. Type: string
#. Description
@@ -30,10 +30,10 @@ 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 ""
+msgstr "Open ERP 서버를 운영하실때는 슈퍼유저가 아닌 전용 계정을 이용하셔야 보안을 확보할수 있습니다"
#. Type: string
#. Description
#: ../openerp-server.templates:1001
msgid "Please choose that account's username."
-msgstr ""
+msgstr "사용자 이름을 선택하세요"
diff --git a/debian/po/mn.po b/debian/po/mn.po
new file mode 100644
index 00000000000..47a4d6a3e6c
--- /dev/null
+++ b/debian/po/mn.po
@@ -0,0 +1,41 @@
+# Mongolian translation for openobject-server
+# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
+# This file is distributed under the same license as the openobject-server package.
+# FIRST AUTHOR , 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-server\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2009-08-24 22:41+0300\n"
+"PO-Revision-Date: 2010-09-29 08:02+0000\n"
+"Last-Translator: ub121 \n"
+"Language-Team: Mongolian \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2010-09-30 04:38+0000\n"
+"X-Generator: Launchpad (build Unknown)\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 "Нэвтрэх эрхийг сонгоно уу."
diff --git a/debian/po/nb.po b/debian/po/nb.po
index e47c06330de..818da6d0626 100644
--- a/debian/po/nb.po
+++ b/debian/po/nb.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/nl.po b/debian/po/nl.po
index 74e4e74742a..e1e37a63720 100644
--- a/debian/po/nl.po
+++ b/debian/po/nl.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/pl.po b/debian/po/pl.po
index 46a8c98366d..826e50fd881 100644
--- a/debian/po/pl.po
+++ b/debian/po/pl.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/pt.po b/debian/po/pt.po
index 43f085929f4..079780d23df 100644
--- a/debian/po/pt.po
+++ b/debian/po/pt.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/pt_BR.po b/debian/po/pt_BR.po
new file mode 100644
index 00000000000..88f2aa707e1
--- /dev/null
+++ b/debian/po/pt_BR.po
@@ -0,0 +1,42 @@
+# Brazilian Portuguese translation for openobject-server
+# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
+# This file is distributed under the same license as the openobject-server package.
+# FIRST AUTHOR , 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-server\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2009-08-24 22:41+0300\n"
+"PO-Revision-Date: 2010-11-07 07:06+0000\n"
+"Last-Translator: OpenERP Administrators \n"
+"Language-Team: Brazilian Portuguese \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2010-11-08 05:11+0000\n"
+"X-Generator: Launchpad (build Unknown)\n"
+
+#. Type: string
+#. Description
+#: ../openerp-server.templates:1001
+msgid "Dedicated system account for the Open ERP server:"
+msgstr "Conta do sistema dedicada para o servidor de 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 deve usar uma conta específica para o seu funcionamento "
+"de modo que a segurança do sistema não seja comprometida por executá-lo com "
+"privilégios de super usuário."
+
+#. Type: string
+#. Description
+#: ../openerp-server.templates:1001
+msgid "Please choose that account's username."
+msgstr "Escolha um usuário para a conta"
diff --git a/debian/po/ro.po b/debian/po/ro.po
index 11c923af7a4..bace70806da 100644
--- a/debian/po/ro.po
+++ b/debian/po/ro.po
@@ -8,13 +8,13 @@ msgstr ""
"Project-Id-Version: openobject-server\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2009-08-24 22:41+0300\n"
-"PO-Revision-Date: 2009-12-21 06:11+0000\n"
+"PO-Revision-Date: 2010-09-27 23:36+0000\n"
"Last-Translator: Valentin Caragea \n"
"Language-Team: Romanian \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/ru.po b/debian/po/ru.po
index 622e2dba6a9..e702c9b28e0 100644
--- a/debian/po/ru.po
+++ b/debian/po/ru.po
@@ -8,13 +8,13 @@ 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-11-26 07:45+0000\n"
-"Last-Translator: Yuri Kozlov \n"
+"PO-Revision-Date: 2010-10-21 05:57+0000\n"
+"Last-Translator: Kirill \n"
"Language-Team: Russian \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-10-22 04:38+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/sk.po b/debian/po/sk.po
index 3b89810b2e4..25895cff9b8 100644
--- a/debian/po/sk.po
+++ b/debian/po/sk.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/sr.po b/debian/po/sr.po
index f9d964e00f5..7e895bfbfe4 100644
--- a/debian/po/sr.po
+++ b/debian/po/sr.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-22 03:55+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/sv.po b/debian/po/sv.po
index 384489afe97..e4138456cb4 100644
--- a/debian/po/sv.po
+++ b/debian/po/sv.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
"X-Poedit-Language: Swedish\n"
diff --git a/debian/po/tr.po b/debian/po/tr.po
index c7bda785999..9379a4f8ef9 100644
--- a/debian/po/tr.po
+++ b/debian/po/tr.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/uk.po b/debian/po/uk.po
index 31375230c11..39ac200e9c5 100644
--- a/debian/po/uk.po
+++ b/debian/po/uk.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/zh_CN.po b/debian/po/zh_CN.po
index 7a64f4cac15..243dc2a4a40 100644
--- a/debian/po/zh_CN.po
+++ b/debian/po/zh_CN.po
@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. Type: string
diff --git a/debian/po/zh_TW.po b/debian/po/zh_TW.po
index 3e847c82ecf..0b9df2604e3 100644
--- a/debian/po/zh_TW.po
+++ b/debian/po/zh_TW.po
@@ -12,7 +12,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-07-13 03:49+0000\n"
+"X-Launchpad-Export-Date: 2010-09-29 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
"X-Poedit-Country: TAIWAN\n"
"X-Poedit-Language: Chinese\n"
diff --git a/doc/INSTALL b/doc/INSTALL
index 47a75694d46..73c33b5485d 100644
--- a/doc/INSTALL
+++ b/doc/INSTALL
@@ -21,6 +21,9 @@ for rendering workflows graphs, you need:
* graphviz
* pyparsing
+For Luxembourg localization, you also need:
+ * pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/)
+
for generating reports using non .jpg images, you need:
* Python Imaging Library for python
diff --git a/doc/openerp-server.conf b/doc/openerp-server.conf
index 3b58a008329..6442827e87c 100644
--- a/doc/openerp-server.conf
+++ b/doc/openerp-server.conf
@@ -25,3 +25,8 @@ translate_modules = ['all']
demo = {}
addons_path = None
reportgz = False
+
+; Static http parameters
+static_http_enable = False
+static_http_document_root = /var/www/html
+static_http_url_prefix = /
\ No newline at end of file
diff --git a/doc/tests/check_profile_l10n_all.py b/doc/tests/check_profile_l10n_all.py
index f558a2a5b56..719d5ce051b 100644
--- a/doc/tests/check_profile_l10n_all.py
+++ b/doc/tests/check_profile_l10n_all.py
@@ -37,7 +37,7 @@ profiles = [
'profile_manufacturing'
]
l10n_charts = [
- 'l10n_chart_uk_minimal',
+ 'l10n_uk',
False,
'l10n_be',
'l10n_fr'
diff --git a/list-services.sh b/list-services.sh
new file mode 100755
index 00000000000..a90e58479d5
--- /dev/null
+++ b/list-services.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# ADMIN_PASSWD='admin'
+method_1() {
+ cat '-' << EOF
+
+
+ list_http_services
+
+
+
+EOF
+}
+
+method_1 | POST -c 'text/xml' http://localhost:8069/xmlrpc/common
+#eof
diff --git a/setup.cfg b/setup.cfg
index c920547962b..483eb2f22e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,5 @@
+[general]
+
[sdist]
formats=gztar
@@ -6,9 +8,9 @@ formats=rpm
[bdist_rpm]
# release must exactly match 'release' as set in bin/release.py
-release=10
+release=6.0.0-rc1
-requires=python >= 2.4
+requires=python >= 2.5
#build-requires=python-devel >= 2.3
#doc_files = doc/*
diff --git a/setup.py b/setup.py
index 39fda7703e8..c1b348503a5 100755
--- a/setup.py
+++ b/setup.py
@@ -26,14 +26,15 @@
# adapted by Nicolas Évrard
#
-import imp
import sys
import os
from os.path import join, isfile, basename
import glob
-from distutils.core import setup, Command
-from distutils.command.install import install
+from pprint import pprint as pp
+
+from setuptools import setup as official_setup, find_packages
+from setuptools.command.install import install
from distutils.sysconfig import get_python_lib
has_py2exe = False
@@ -51,63 +52,57 @@ if 'bdist_rpm' in sys.argv:
# get python short version
py_short_version = '%s.%s' % sys.version_info[:2]
-required_modules = [
- ('lxml', 'lxml module: pythonic libxml2 and libxslt bindings'),
- ('mako', 'Mako template engine'),
- ('dateutil', "Extensions to the standard datetime module"),
- ('psycopg2', 'PostgreSQL module'),
- ('pychart', 'pychart module'),
- ('pydot', 'pydot module'),
- ('pytz', 'Timezone handling library for Python'),
- ('reportlab', 'reportlab module'),
- ('yaml', 'YAML parser and emitter for Python'),
- ('pywebdav', 'PyWebDAV is a standards compliant WebDAV server and library written in Python'),
-]
+# backports os.walk with followlinks from python 2.6
+def walk_followlinks(top, topdown=True, onerror=None, followlinks=False):
+ from os.path import join, isdir, islink
+ from os import listdir, error
-def check_modules():
- errors = []
- for modname, desc in required_modules:
- try:
- imp.find_module(modname)
- except ImportError:
- errors.append(
- 'Error: python module %s (%s) is required' % (modname, desc))
+ try:
+ names = listdir(top)
+ except error, err:
+ if onerror is not None:
+ onerror(err)
+ return
- if errors:
- print '\n'.join(errors)
- sys.exit(1)
+ dirs, nondirs = [], []
+ for name in names:
+ if isdir(join(top, name)):
+ dirs.append(name)
+ else:
+ nondirs.append(name)
+
+ if topdown:
+ yield top, dirs, nondirs
+ for name in dirs:
+ path = join(top, name)
+ if followlinks or not islink(path):
+ for x in walk_followlinks(path, topdown, onerror, followlinks):
+ yield x
+ if not topdown:
+ yield top, dirs, nondirs
+
+if sys.version_info < (2, 6):
+ os.walk = walk_followlinks
def find_addons():
- for root, _, names in os.walk(join('bin', 'addons')):
+ for root, _, names in os.walk(join('bin', 'addons'), followlinks=True):
if '__openerp__.py' in names or '__terp__.py' in names:
yield basename(root), root
- #look for extra modules
- try:
- empath = os.getenv('EXTRA_MODULES_PATH', '../addons/')
- for mname in open(join(empath, 'server_modules.list')):
- mname = mname.strip()
- if not mname:
- continue
-
- terp = join(empath, mname, '__openerp__.py')
- if not os.path.exists(terp):
- terp = join(empath, mname, '__terp__.py')
-
- if os.path.exists(terp):
- yield mname, join(empath, mname)
- else:
- print "Module %s specified, but no valid path." % mname
- except:
- pass
def data_files():
'''Build list of data files to be installed'''
files = []
if os.name == 'nt':
- for root, _, names in os.walk(join('bin','addons')):
- files.append((root, [join(root, name) for name in names]))
+ os.chdir('bin')
+ for (dp, dn, names) in os.walk('addons'):
+ files.append((dp, map(lambda x: join('bin', dp, x), names)))
+ os.chdir('..')
+ #for root, _, names in os.walk(join('bin','addons')):
+ # files.append((root, [join(root, name) for name in names]))
for root, _, names in os.walk('doc'):
files.append((root, [join(root, name) for name in names]))
+ #for root, _, names in os.walk('pixmaps'):
+ # files.append((root, [join(root, name) for name in names]))
files.append(('.', [join('bin', 'import_xml.rng'),
join('bin', 'server.pkey'),
join('bin', 'server.cert')]))
@@ -145,9 +140,6 @@ def data_files():
return files
-if not os.getenv('NO_CHECK_MODULES') :
- check_modules()
-
f = file('openerp-server','w')
f.write("""#!/bin/sh
echo "Error: the content of this file should have been replaced during "
@@ -173,6 +165,8 @@ class openerp_server_install(install):
f.close()
install.run(self)
+
+
options = {
"py2exe": {
"compressed": 1,
@@ -184,12 +178,16 @@ options = {
"encodings", "dateutil", "wizard", "pychart", "PIL", "pyparsing",
"pydot", "asyncore","asynchat", "reportlab", "vobject",
"HTMLParser", "select", "mako", "poplib",
- "imaplib", "smtplib", "email", "yaml","pywebdav",
+ "imaplib", "smtplib", "email", "yaml", "DAV",
],
"excludes" : ["Tkconstants","Tkinter","tcl"],
}
}
+def setup(**kwargs):
+ #pp(kwargs)
+ return official_setup(**kwargs)
+
setup(name = name,
version = version,
description = description,
@@ -201,62 +199,46 @@ setup(name = name,
license = license,
data_files = data_files(),
cmdclass = {
- 'install' : openerp_server_install,
+ 'install' : openerp_server_install,
},
scripts = ['openerp-server'],
- packages = ['openerp-server',
- 'openerp-server.addons',
- 'openerp-server.ir',
- 'openerp-server.osv',
- 'openerp-server.pychart',
- 'openerp-server.pychart.afm',
- 'openerp-server.report',
- 'openerp-server.report.printscreen',
- 'openerp-server.report.pyPdf',
- 'openerp-server.report.render',
- 'openerp-server.report.render.html2html',
- 'openerp-server.report.render.makohtml2html',
- 'openerp-server.report.render.odt2odt',
- 'openerp-server.report.render.rml2html',
- 'openerp-server.report.render.rml2pdf',
- 'openerp-server.report.render.rml2txt',
- 'openerp-server.service',
- 'openerp-server.tools',
- 'openerp-server.wizard',
- 'openerp-server.workflow'] + \
- [('openerp-server.addons.' + name)
- for name, _ in find_addons()],
+ packages = [
+ '.'.join(['openerp-server'] + package.split('.')[1:])
+ for package in find_packages()
+ ],
+ include_package_data = True,
+ package_data = {
+ '': ['*.yml', '*.xml', '*.po', '*.pot', '*.csv'],
+ },
package_dir = find_package_dirs(),
- console = [{"script": join("bin", "openerp-server.py"),
- "icon_resources": [(1,join("pixmaps","openerp-icon.ico"))]
- }],
+ console = [
+ {
+ "script": join("bin", "openerp-server.py"),
+ "icon_resources": [(1, join("pixmaps","openerp-icon.ico"))]
+ }
+ ],
options = options,
- )
-
-if has_py2exe:
- # Sometime between pytz-2008a and pytz-2008i common_timezones started to
- # include only names of zones with a corresponding data file in zoneinfo.
- # pytz installs the zoneinfo directory tree in the same directory
- # as the pytz/__init__.py file. These data files are loaded using
- # pkg_resources.resource_stream. py2exe does not copy this to library.zip so
- # resource_stream can't find the files and common_timezones is empty when
- # read in the py2exe executable.
- # This manually copies zoneinfo into the zip. See also
- # http://code.google.com/p/googletransitdatafeed/issues/detail?id=121
- import pytz
- import zipfile
- # Make sure the layout of pytz hasn't changed
- assert (pytz.__file__.endswith('__init__.pyc') or
- pytz.__file__.endswith('__init__.py')), pytz.__file__
- zoneinfo_dir = join(os.path.dirname(pytz.__file__), 'zoneinfo')
- # '..\\Lib\\pytz\\__init__.py' -> '..\\Lib'
- disk_basedir = os.path.dirname(os.path.dirname(pytz.__file__))
- zipfile_path = join(options['py2exe']['dist_dir'], 'library.zip')
- z = zipfile.ZipFile(zipfile_path, 'a')
- for absdir, directories, filenames in os.walk(zoneinfo_dir):
- assert absdir.startswith(disk_basedir), (absdir, disk_basedir)
- zip_dir = absdir[len(disk_basedir):]
- for f in filenames:
- z.write(join(absdir, f), join(zip_dir, f))
- z.close()
+ install_requires = [
+ 'lxml',
+ 'mako',
+ 'python-dateutil',
+ 'psycopg2',
+ 'pychart',
+ 'pydot',
+ 'pytz',
+ 'reportlab',
+ 'caldav',
+ 'pyyaml',
+ #'django',
+ 'pywebdav'
+ #'cx_Oracle',
+ #'mysqldb',
+ 'feedparser',
+ #'bsddb3',
+ 'egenix-mx-base'
+ ],
+ extras_require={
+ 'SSL' : ['pyopenssl'],
+ }
+)