summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorHarry (OpenERP) <hmo@tinyerp.com>2010-10-05 14:24:14 +0530
committerHarry (OpenERP) <hmo@tinyerp.com>2010-10-05 14:24:14 +0530
commit6ba07005eb8aaf656ae4738c1528485cbdd4fdbf (patch)
tree46b46cd447a75993364a09a20e70066c4a6f8564 /bin
parentbaba6829dc1586acf86eb845eb0be727bed62b13 (diff)
parent863766e16afe32b046e5e5bf0f7e0a6384be025b (diff)
[MERGE] merged from openobject-server branch
bzr revid: hmo@tinyerp.com-20101005085414-u0eni6ot4lshvmps
Diffstat (limited to 'bin')
-rw-r--r--bin/addons/base/base.sql3
-rw-r--r--bin/addons/base/base_data.xml7
-rw-r--r--bin/addons/base/base_demo.xml2
-rw-r--r--bin/addons/base/i18n/nl.po6
-rw-r--r--bin/addons/base/ir/ir_actions.py25
-rw-r--r--bin/addons/base/ir/ir_rule.py3
-rw-r--r--bin/addons/base/ir/ir_values.py2
-rw-r--r--bin/addons/base/res/partner/partner.py2
-rw-r--r--bin/addons/base/res/res_user.py20
-rw-r--r--bin/addons/base/security/base_security.xml2
-rw-r--r--bin/addons/base/security/ir.model.access.csv1
-rw-r--r--bin/osv/orm.py206
-rw-r--r--bin/osv/query.py122
-rw-r--r--bin/pooler.py3
-rw-r--r--bin/report/interface.py20
-rw-r--r--bin/report/printscreen/ps_list.py33
-rw-r--r--bin/report/report_sxw.py35
-rw-r--r--bin/run_tests.py27
-rw-r--r--bin/test/__init__.py22
-rw-r--r--bin/test/test_osv.py64
20 files changed, 419 insertions, 186 deletions
diff --git a/bin/addons/base/base.sql b/bin/addons/base/base.sql
index 84676a7f59b..eb9eb5b9d75 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)
);
@@ -345,7 +344,7 @@ CREATE TABLE ir_model_data (
-- Users
---------------------------------
-insert into res_users (id,login,password,name,action_id,active,company_id) values (1,'admin','admin','Administrator',NULL,True,1);
+insert into res_users (id,login,password,name,active,company_id) values (1,'admin','admin','Administrator',True,1);
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 5082adfbfe0..9597ac83f92 100644
--- a/bin/addons/base/base_data.xml
+++ b/bin/addons/base/base_data.xml
@@ -1053,8 +1053,6 @@
<field name="address_id" ref="main_address"/>
<field name="company_id" ref="main_company"/>
<field name="company_ids" eval="[(4, ref('main_company'))]"/>
- <field name="action_id" ref="action_menu_admin"/>
- <field name="menu_id" ref="action_menu_admin"/>
</record>
<record id="main_partner" model="res.partner">
@@ -1375,6 +1373,9 @@
<field name="currency_id" ref="UAH"/>
<field eval="time.strftime('%Y-01-01')" name="name"/>
</record>
-
+ <record id="res_bank_1" model="res.bank">
+ <field name="name">Reserve</field>
+ <field name="code">RSV</field>
+ </record>
</data>
</openerp>
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 @@
<field name="password">demo</field>
<field name="name">Demo User</field>
<field name="signature">Mr Demo</field>
- <field name="action_id" ref="action_menu_admin"/>
- <field name="menu_id" ref="action_menu_admin"/>
<field name="address_id" ref="main_address"/>
<field name="company_id" ref="main_company"/>
<field name="groups_id" eval="[(6,0,[ref('base.group_user')])]"/>
diff --git a/bin/addons/base/i18n/nl.po b/bin/addons/base/i18n/nl.po
index af960dcbd5f..534a06b7f62 100644
--- a/bin/addons/base/i18n/nl.po
+++ b/bin/addons/base/i18n/nl.po
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.0\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
-"PO-Revision-Date: 2010-09-30 12:03+0000\n"
-"Last-Translator: Douwe Wullink (Dypalio) <Unknown>\n"
+"PO-Revision-Date: 2010-10-01 11:47+0000\n"
+"Last-Translator: OpenERP Administrators <Unknown>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-10-01 08:47+0000\n"
+"X-Launchpad-Export-Date: 2010-10-02 05:05+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: base
diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py
index 37f1ab19643..074bfa04f85 100644
--- a/bin/addons/base/ir/ir_actions.py
+++ b/bin/addons/base/ir/ir_actions.py
@@ -28,7 +28,9 @@ from tools.translate import _
import netsvc
import re
import copy
+import os
from xml import dom
+from report.report_sxw import report_sxw, report_rml
class actions(osv.osv):
_name = 'ir.actions.actions'
@@ -71,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'
@@ -80,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.'),
diff --git a/bin/addons/base/ir/ir_rule.py b/bin/addons/base/ir/ir_rule.py
index 16c2c8275d9..799a411c15f 100644
--- a/bin/addons/base/ir/ir_rule.py
+++ b/bin/addons/base/ir/ir_rule.py
@@ -137,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_values.py b/bin/addons/base/ir/ir_values.py
index 1f9a54516b6..310f3552403 100644
--- a/bin/addons/base/ir/ir_values.py
+++ b/bin/addons/base/ir/ir_values.py
@@ -176,7 +176,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)
diff --git a/bin/addons/base/res/partner/partner.py b/bin/addons/base/res/partner/partner.py
index 466a8de0786..05630dea85b 100644
--- a/bin/addons/base/res/partner/partner.py
+++ b/bin/addons/base/res/partner/partner.py
@@ -392,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/res_user.py b/bin/addons/base/res/res_user.py
index 38cf1fc21a7..929a7018f7d 100644
--- a/bin/addons/base/res/res_user.py
+++ b/bin/addons/base/res/res_user.py
@@ -233,8 +233,8 @@ 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'),
@@ -279,7 +279,6 @@ class users(osv.osv):
result = override_password(result)
else:
result = map(override_password, result)
-
return result
@@ -310,10 +309,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
@@ -327,13 +322,14 @@ 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')
+ data_id = dataobj._get_id(cr, 1, 'base', 'group_user')
+ return data_id and [data_id] or False
_defaults = {
'password' : lambda *a : '',
diff --git a/bin/addons/base/security/base_security.xml b/bin/addons/base/security/base_security.xml
index 76b725b5826..d7d39ea27e9 100644
--- a/bin/addons/base/security/base_security.xml
+++ b/bin/addons/base/security/base_security.xml
@@ -14,7 +14,7 @@
</record>
<record model="res.groups" id="group_user">
- <field name="name">Employee</field>
+ <field name="name">Internal User</field>
</record>
<record model="res.groups" context="{'noadmin':True}" id="group_multi_company">
diff --git a/bin/addons/base/security/ir.model.access.csv b/bin/addons/base/security/ir.model.access.csv
index 00833f6a48e..cbbeb30fa0d 100644
--- a/bin/addons/base/security/ir.model.access.csv
+++ b/bin/addons/base/security/ir.model.access.csv
@@ -16,6 +16,7 @@
"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
diff --git a/bin/osv/orm.py b/bin/osv/orm.py
index 44287a6d485..55d2a4e226e 100644
--- a/bin/osv/orm.py
+++ b/bin/osv/orm.py
@@ -54,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
@@ -2203,35 +2204,15 @@ class orm(orm_template):
if not fields:
fields = self._columns.keys()
- # compute the where, order by, limit and offset clauses
- (where_clause, where_clause_params, tables) = self._where_calc(cr, uid, domain, context=context)
-
- # apply direct ir.rules from current model
- self._apply_ir_rules(cr, uid, where_clause, where_clause_params, tables, 'read', context=context)
-
- # then apply the ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
- for inherited_model in self._inherits:
- previous_tables = list(tables)
- if self._apply_ir_rules(cr, uid, where_clause, where_clause_params, tables, 'read', model_name=inherited_model, context=context):
- # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
- # list of table in case the inherited table was not in the list before (as that means the corresponding
- # JOIN(s) was(were) not present)
- self._inherits_join_add(inherited_model, previous_tables, where_clause)
- tables = list(set(tables).union(set(previous_tables)))
+ 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 isinstance(groupby, list):
groupby = groupby[0]
- tables, where_clause, qfield = self._inherits_join_calc(groupby, tables, where_clause)
-
- if len(where_clause):
- where_clause = ' where ' + ' and '.join(where_clause)
- else:
- where_clause = ''
- limit_str = limit and ' limit %d' % limit or ''
- offset_str = offset and ' offset %d' % offset or ''
+ self._inherits_join_calc(groupby, query)
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?)"
@@ -2263,8 +2244,13 @@ class orm(orm_template):
flist += ','
flist += operator+'('+f+') as '+f
- gb = groupby and (' group by '+groupby) or ''
- cr.execute('select min(%s.id) as id,' % self._table + flist + ' from ' + ','.join(tables) + where_clause + gb + limit_str + offset_str, where_clause_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():
@@ -2303,43 +2289,37 @@ class orm(orm_template):
del d['id']
return data
- def _inherits_join_add(self, parent_model_name, tables, where_clause):
+ def _inherits_join_add(self, parent_model_name, query):
"""
- Add missing table SELECT and JOIN clause for reaching the parent table (no duplicates)
+ Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
:param parent_model_name: name of the parent model for which the clauses should be added
- :param tables: list of table._table names enclosed in double quotes as returned
- by _where_calc()
- :param where_clause: current list of WHERE clause params
+ :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 tables:
- tables.append(quoted_parent_table_name)
- where_clause.append('("%s".%s = %s.id)' % (self._table, inherits_field, parent_table_name))
- return (tables, where_clause)
+ 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, tables, where_clause):
+ def _inherits_join_calc(self, field, query):
"""
- Adds missing table select and join clause(s) for reaching
+ Adds missing table select and join clause(s) to ``query`` for reaching
the field coming from an '_inherits' parent table (no duplicates).
- :param tables: list of table._table names enclosed in double quotes as returned
- by _where_calc()
- :param where_clause: current list of WHERE clause params
- :return: (tables, where_clause, qualified_field) where ``tables`` and ``where_clause`` are the updated
- versions of the parameters, and ``qualified_field`` is the qualified name of ``field``
- in the form ``table.field``, to be referenced in queries.
+ :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_model_name = current_table._inherit_fields[field][0]
parent_table = self.pool.get(parent_model_name)
- self._inherits_join_add(parent_model_name, tables, where_clause)
+ self._inherits_join_add(parent_model_name, query)
current_table = parent_table
- return (tables, where_clause, '"%s".%s' % (current_table._table, field))
+ return '"%s".%s' % (current_table._table, field)
def _parent_store_compute(self, cr):
if not self._parent_store:
@@ -3899,84 +3879,87 @@ 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 args: the domain to compute
- :type args: list
+ :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: tuple with 3 elements: (where_clause, where_clause_params, tables) where
- ``where_clause`` contains a list of where clause elements (to be joined with 'AND'),
- ``where_clause_params`` is a list of parameters to be passed to the db layer
- for the where_clause expansion, and ``tables`` is the list of double-quoted
- table names that need to be included in the FROM clause.
- :rtype: tuple
+ :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'), _('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 _apply_ir_rules(self, cr, uid, where_clause, where_clause_params, tables, mode='read', model_name=None, context=None):
- """Add what's missing in ``where_clause``, ``where_params``, ``tables`` to implement
- all appropriate ir.rules (on the current object but also from it's _inherits parents)
-
- :param where_clause: list with current elements of the WHERE clause (strings)
- :param where_clause_params: list with parameters for ``where_clause``
- :param tables: list with double-quoted names of the tables that are joined
- in ``where_clause``
- :param model_name: optional name of the model whose ir.rules should be applied (default:``self._name``)
- This could be useful for inheritance for example, but there is no provision to include
- the appropriate JOIN for linking the current model to the one referenced in model_name.
- :return: True if additional clauses where applied.
+ 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
"""
- added_clause, added_params, added_tables = self.pool.get('ir.rule').domain_get(cr, uid, model_name or self._name, mode, context=context)
- if added_clause:
- where_clause += added_clause
- where_clause_params += added_params
- for table in added_tables:
- if table not in tables:
- tables.append(table)
- return True
- return False
+ def apply_rule(added_clause, added_params, added_tables):
+ if added_clause:
+ 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
+
+ # 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))
+
+ # apply ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
+ for inherited_model in self._inherits:
+ if apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context)):
+ # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
+ # list of table in case the inherited table was not in the list before (as that means the corresponding
+ # JOIN(s) was(were) not present)
+ self._inherits_join_add(inherited_model, query)
- def _generate_m2o_order_by(self, order_field, tables, where_clause):
+ def _generate_m2o_order_by(self, order_field, query):
"""
- Add possibly missing JOIN and generate the ORDER BY clause for m2o fields,
+ 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
- tables, where_clause, qualified_field = self._inherits_join_calc(order_field, tables, where_clause)
+ 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)
@@ -3995,16 +3978,14 @@ class orm(orm_template):
# 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]
- # the perhaps missing join:
- quoted_model_table = '"%s"' % dest_model._table
- if quoted_model_table not in tables:
- tables.append(quoted_model_table)
- where_clause.append('%s = %s.id' % (qualified_field, quoted_model_table))
-
- return ('%s.%s' % (quoted_model_table, m2o_order), tables, where_clause)
+ # 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, tables, where_clause):
+ 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.
@@ -4024,16 +4005,16 @@ class orm(orm_template):
if order_column._classic_read:
order_by_clause = '"%s"."%s"' % (self._table, order_field)
elif order_column._type == 'many2one':
- order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ 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:
- tables, where_clause, order_by_clause = self._inherits_join_calc(order_field, tables, where_clause)
+ order_by_clause = self._inherits_join_calc(order_field, query)
elif order_column._type == 'many2one':
- order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause)
+ 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))
@@ -4054,34 +4035,21 @@ class orm(orm_template):
if context is None:
context = {}
self.pool.get('ir.model.access').check(cr, access_rights_uid or user, self._name, 'read', context=context)
- # compute the where, order by, limit and offset clauses
- (where_clause, where_clause_params, tables) = self._where_calc(cr, user, args, context=context)
- # apply direct ir.rules from current model
- self._apply_ir_rules(cr, user, where_clause, where_clause_params, tables, 'read', context=context)
-
- # then apply the ir.rules from the parents (through _inherits), adding the appropriate JOINs if needed
- for inherited_model in self._inherits:
- previous_tables = list(tables)
- if self._apply_ir_rules(cr, user, where_clause, where_clause_params, tables, 'read', model_name=inherited_model, context=context):
- # if some rules were applied, need to add the missing JOIN for them to make sense, passing the previous
- # list of table in case the inherited table was not in the list before (as that means the corresponding
- # JOIN(s) was(were) not present)
- self._inherits_join_add(inherited_model, previous_tables, where_clause)
- tables = list(set(tables).union(set(previous_tables)))
+ 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()
- where = where_clause
- order_by = self._generate_order_by(order, tables, where_clause)
limit_str = limit and ' limit %d' % limit or ''
offset_str = offset and ' offset %d' % offset or ''
- where_str = where and (" WHERE %s" % " AND ".join(where)) or ''
+ where_str = where_clause and (" WHERE %s" % where_clause) or ''
if count:
- cr.execute('select count(%s.id) from ' % self._table +
- ','.join(tables) + where_str + limit_str + offset_str, where_clause_params)
+ 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) + where_str + order_by + limit_str+offset_str, where_clause_params)
+ 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]
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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+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 '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % 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 7a572e71e0d..684bc6210d4 100644
--- a/bin/pooler.py
+++ b/bin/pooler.py
@@ -44,12 +44,11 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
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').restart(db.dbname)
return db, pool
diff --git a/bin/report/interface.py b/bin/report/interface.py
index 6aecfe66133..0ee9f432883 100644
--- a/bin/report/interface.py
+++ b/bin/report/interface.py
@@ -219,25 +219,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/printscreen/ps_list.py b/bin/report/printscreen/ps_list.py
index aa95eedeff8..7fb20eab0dc 100644
--- a/bin/report/printscreen/ps_list.py
+++ b/bin/report/printscreen/ps_list.py
@@ -105,13 +105,13 @@ class report_printscreen_list(report_int):
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')
@@ -232,7 +232,7 @@ class report_printscreen_list(report_int):
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:
@@ -240,19 +240,18 @@ class report_printscreen_list(report_int):
else:
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/report_sxw.py b/bin/report/report_sxw.py
index aeb04ed21b6..2c505e98622 100644
--- a/bin/report/report_sxw.py
+++ b/bin/report/report_sxw.py
@@ -39,6 +39,7 @@ 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'
@@ -240,9 +241,36 @@ 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):
+ if digits is None:
+ 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
@@ -250,6 +278,7 @@ 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:
@@ -258,9 +287,11 @@ class rml_parse(object):
parse_format = DHM_FORMAT
if not isinstance(value, time.struct_time):
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):
@@ -305,6 +336,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
diff --git a/bin/run_tests.py b/bin/run_tests.py
new file mode 100644
index 00000000000..7c0ee2a2431
--- /dev/null
+++ b/bin/run_tests.py
@@ -0,0 +1,27 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import unittest
+
+import test
+
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(test))
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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+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 <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+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)
+