diff options
| author | Harry (OpenERP) <hmo@tinyerp.com> | 2010-10-05 14:24:14 +0530 |
|---|---|---|
| committer | Harry (OpenERP) <hmo@tinyerp.com> | 2010-10-05 14:24:14 +0530 |
| commit | 6ba07005eb8aaf656ae4738c1528485cbdd4fdbf (patch) | |
| tree | 46b46cd447a75993364a09a20e70066c4a6f8564 /bin | |
| parent | baba6829dc1586acf86eb845eb0be727bed62b13 (diff) | |
| parent | 863766e16afe32b046e5e5bf0f7e0a6384be025b (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.sql | 3 | ||||
| -rw-r--r-- | bin/addons/base/base_data.xml | 7 | ||||
| -rw-r--r-- | bin/addons/base/base_demo.xml | 2 | ||||
| -rw-r--r-- | bin/addons/base/i18n/nl.po | 6 | ||||
| -rw-r--r-- | bin/addons/base/ir/ir_actions.py | 25 | ||||
| -rw-r--r-- | bin/addons/base/ir/ir_rule.py | 3 | ||||
| -rw-r--r-- | bin/addons/base/ir/ir_values.py | 2 | ||||
| -rw-r--r-- | bin/addons/base/res/partner/partner.py | 2 | ||||
| -rw-r--r-- | bin/addons/base/res/res_user.py | 20 | ||||
| -rw-r--r-- | bin/addons/base/security/base_security.xml | 2 | ||||
| -rw-r--r-- | bin/addons/base/security/ir.model.access.csv | 1 | ||||
| -rw-r--r-- | bin/osv/orm.py | 206 | ||||
| -rw-r--r-- | bin/osv/query.py | 122 | ||||
| -rw-r--r-- | bin/pooler.py | 3 | ||||
| -rw-r--r-- | bin/report/interface.py | 20 | ||||
| -rw-r--r-- | bin/report/printscreen/ps_list.py | 33 | ||||
| -rw-r--r-- | bin/report/report_sxw.py | 35 | ||||
| -rw-r--r-- | bin/run_tests.py | 27 | ||||
| -rw-r--r-- | bin/test/__init__.py | 22 | ||||
| -rw-r--r-- | bin/test/test_osv.py | 64 |
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) + |
