diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py index a33dba8a9fc..7ba775c1797 100644 --- a/bin/addons/base/ir/ir_actions.py +++ b/bin/addons/base/ir/ir_actions.py @@ -286,7 +286,8 @@ class act_window_view(osv.osv): ('form', 'Form'), ('graph', 'Graph'), ('calendar', 'Calendar'), - ('gantt', 'Gantt')), string='View Type', required=True), + ('gantt', 'Gantt'), + ('gallery', 'Gallery')), string='View Type', required=True), 'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'), '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."), diff --git a/bin/addons/base/ir/ir_ui_view.py b/bin/addons/base/ir/ir_ui_view.py index e0b7d6c3252..62e4bd9dd6e 100644 --- a/bin/addons/base/ir/ir_ui_view.py +++ b/bin/addons/base/ir/ir_ui_view.py @@ -64,6 +64,7 @@ class view(osv.osv): ('calendar', 'Calendar'), ('diagram','Diagram'), ('gantt', 'Gantt'), + ('gallery', 'Gallery'), ('search','Search')), 'View Type', required=True), 'arch': fields.text('View Architecture', required=True), 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade'), diff --git a/bin/addons/base/res/res_company.py b/bin/addons/base/res/res_company.py index bbc1c78af2b..1bc0f7372ee 100644 --- a/bin/addons/base/res/res_company.py +++ b/bin/addons/base/res/res_company.py @@ -203,7 +203,7 @@ class res_company(osv.osv): return """
- + diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py index 2b1d39381cc..e49578e9cc6 100644 --- a/bin/addons/base/res/res_user.py +++ b/bin/addons/base/res/res_user.py @@ -412,8 +412,14 @@ class users(osv.osv): return self.name_get(cr, user, ids) def copy(self, cr, uid, id, default=None, context={}): - login = self.read(cr, uid, [id], ['login'])[0]['login'] - default.update({'login': login+' (copy)'}) + user2copy = self.read(cr, uid, [id], ['login','name'])[0] + if default is None: + default = {} + copy_pattern = _("%s (copy)") + default.update(login=(copy_pattern % user2copy['login']), + name=(copy_pattern % user2copy['name']), + address_id=False, # avoid sharing the address of the copied user! + ) return super(users, self).copy(cr, uid, id, default, context) def context_get(self, cr, uid, context=None): diff --git a/bin/addons/base/rng/board.rng b/bin/addons/base/rng/board.rng deleted file mode 100644 index 73477113f62..00000000000 --- a/bin/addons/base/rng/board.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/calendar.rng b/bin/addons/base/rng/calendar.rng deleted file mode 100644 index 73477113f62..00000000000 --- a/bin/addons/base/rng/calendar.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/form.rng b/bin/addons/base/rng/form.rng deleted file mode 100644 index b8a93c67549..00000000000 --- a/bin/addons/base/rng/form.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/graph.rng b/bin/addons/base/rng/graph.rng deleted file mode 100644 index 73477113f62..00000000000 --- a/bin/addons/base/rng/graph.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/inherit.rng b/bin/addons/base/rng/inherit.rng deleted file mode 100644 index 73477113f62..00000000000 --- a/bin/addons/base/rng/inherit.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/tree.rng b/bin/addons/base/rng/tree.rng deleted file mode 100644 index 73477113f62..00000000000 --- a/bin/addons/base/rng/tree.rng +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/bin/addons/base/rng/view.rng b/bin/addons/base/rng/view.rng index 9c591723a9f..c634ede8d5f 100644 --- a/bin/addons/base/rng/view.rng +++ b/bin/addons/base/rng/view.rng @@ -397,6 +397,7 @@ + @@ -440,6 +441,7 @@ + @@ -470,6 +472,7 @@ + @@ -532,6 +535,18 @@ + + + + + + + + + + + + @@ -561,6 +576,7 @@ + @@ -629,6 +645,7 @@ + diff --git a/bin/osv/orm.py b/bin/osv/orm.py index 50006603bcf..9361effbc8e 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -2215,7 +2215,7 @@ class orm(orm_template): # Take care of adding join(s) if groupby is an '_inherits'ed field groupby_list = groupby if groupby: - if groupby and isinstance(groupby, list): + if isinstance(groupby, list): groupby = groupby[0] tables, where_clause, qfield = self._inherits_join_calc(groupby, tables, where_clause) @@ -2234,7 +2234,7 @@ class orm(orm_template): flist = '' group_by = groupby if groupby: - if fget.get(groupby, False): + if fget.get(groupby): if fget[groupby]['type'] in ('date', 'datetime'): flist = "to_char(%s,'yyyy-mm') as %s " % (groupby, groupby) groupby = "to_char(%s,'yyyy-mm')" % (groupby) @@ -2256,10 +2256,7 @@ class orm(orm_template): flist += ',' flist += operator+'('+f+') as '+f - if groupby: - gb = ' group by '+groupby - else: - gb = '' + 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) alldata = {} groupby = group_by @@ -2268,7 +2265,15 @@ class orm(orm_template): if val == None: r[fld] = False alldata[r['id']] = r del r['id'] - data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context) + if groupby and fget[groupby]['type'] == 'many2one': + data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=groupby, context=context) + data_read = self.read(cr, uid, data_ids, groupby and [groupby] or ['id'], context=context) + # restore order of the search as read() uses the default _order: + data = [data_read[id] for id in data_ids] + else: + data = self.read(cr, uid, alldata.keys(), groupby and [groupby] or ['id'], context=context) + if groupby: + data.sort(lambda x,y:cmp(x[groupby],y[groupby])) for d in data: if groupby: d['__domain'] = [(groupby, '=', alldata[d['id']][groupby] or False)] + domain @@ -2314,7 +2319,7 @@ class orm(orm_template): :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: (table, where_clause, qualified_field) where ``table`` and ``where_clause`` are the updated + :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. """ @@ -3926,7 +3931,7 @@ class orm(orm_template): def _check_qorder(self, word): if not regex_order.match(word): - raise except_orm(_('AccessError'), _('Bad query.')) + raise except_orm(_('AccessError'), _('Invalid "order" specified. A valid "order" specification is a comma-separated list of valid field names (optionally followed by asc/desc for the direction)')) return True def _apply_ir_rules(self, cr, uid, where_clause, where_clause_params, tables, mode='read', model_name=None, context=None): @@ -3952,6 +3957,79 @@ class orm(orm_template): return True return False + def _generate_m2o_order_by(self, order_field, tables, where_clause): + """ + Add possibly missing JOIN 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. + """ + 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) + order_field_column = self._inherit_fields[order_field][2] + else: + qualified_field = '"%s"."%s"' % (self._table, order_field) + order_field_column = self._columns[order_field] + + assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()' + assert order_field_column._classic_write or getattr(order_field_column, 'store', False), "Many2one function/related fields must be stored to be used as ordering fields" + + # figure out the applicable order_by for the m2o + dest_model = self.pool.get(order_field_column._obj) + m2o_order = dest_model._order + if not regex_order.match(m2o_order): + # _order is complex, can't use it here, so we default to _rec_name + m2o_order = dest_model._rec_name + else: + # extract the first field name, to be able to qualify it and add desc/asc + m2o_order = m2o_order.split(",",1)[0].strip().split(" ",1)[0] + + # 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) + + + def _generate_order_by(self, order_spec, tables, where_clause): + """ + Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be + a comma-separated list of valid field names, optionally followed by an ASC or DESC direction. + + :raise" except_orm in case order_spec is malformed + """ + order_by_clause = self._order + if order_spec: + order_by_elements = [] + self._check_qorder(order_spec) + for order_part in order_spec.split(','): + order_split = order_part.strip().split(' ') + order_field = order_split[0].strip() + order_direction = order_split[1].strip() if len(order_split) == 2 else '' + if order_field in self._columns: + order_column = self._columns[order_field] + if order_column._classic_read: + order_by_clause = '"%s"."%s"' % (self._table, order_field) + elif order_column._type == 'many2one': + order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause) + 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) + elif order_column._type == 'many2one': + order_by_clause, tables, where_clause = self._generate_m2o_order_by(order_field, tables, where_clause) + else: + continue # ignore non-readable or "non-joignable" fields + order_by_elements.append("%s %s" % (order_by_clause, order_direction)) + order_by_clause = ",".join(order_by_elements) + + return order_by_clause and (' ORDER BY %s ' % order_by_clause) or '' + def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): """ Private implementation of search() method, allowing specifying the uid to use for the access right check. @@ -3982,35 +4060,17 @@ class orm(orm_template): tables = list(set(tables).union(set(previous_tables))) where = where_clause - - order_by = self._order - if order: - self._check_qorder(order) - o = order.split(' ')[0] - if (o in self._columns): - # we can only do efficient sort if the fields is stored in database - if getattr(self._columns[o], '_classic_read'): - order_by = order - elif (o in self._inherit_fields): - parent_obj = self.pool.get(self._inherit_fields[o][0]) - if getattr(parent_obj._columns[o], '_classic_read'): - # Allowing inherits'ed field for server side sorting, if they can be sorted by the dbms - inherited_tables, inherit_join, order_by = self._inherits_join_calc(o, tables, 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 '' - - if where: - where_str = " WHERE %s" % " AND ".join(where) - else: - where_str = "" + where_str = where and (" WHERE %s" % " AND ".join(where)) or '' if count: cr.execute('select count(%s.id) from ' % self._table + ','.join(tables) + 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 '+order_by+limit_str+offset_str, where_clause_params) + cr.execute('select %s.id from ' % self._table + ','.join(tables) + 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/osv.py b/bin/osv/osv.py index d75c9af564f..11db4096a9a 100644 --- a/bin/osv/osv.py +++ b/bin/osv/osv.py @@ -65,12 +65,19 @@ class osv_pool(netsvc.Service): for key in self._sql_error.keys(): if key in inst[0]: self.abortResponse(1, _('Constraint Error'), 'warning', _(self._sql_error[key])) - if inst.pgcode == errorcodes.NOT_NULL_VIOLATION: + if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION): msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set') self.logger.debug("IntegrityError", exc_info=True) try: - context = inst.pgerror.split('"public".')[1] - model_name = table = context.split('"')[1] + errortxt = inst.pgerror.replace('«','"').replace('»','"') + if '"public".' in errortxt: + context = errortxt.split('"public".')[1] + model_name = table = context.split('"')[1] + else: + last_quote_end = errortxt.rfind('"') + last_quote_begin = errortxt.rfind('"', 0, last_quote_end) + model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip() + print "MODEL", last_quote_begin, last_quote_end, model_name model = table.replace("_",".") model_obj = self.get(model) if model_obj: diff --git a/bin/report/render/rml2pdf/trml2pdf.py b/bin/report/render/rml2pdf/trml2pdf.py index 9a5fd66517b..ca47423008b 100644 --- a/bin/report/render/rml2pdf/trml2pdf.py +++ b/bin/report/render/rml2pdf/trml2pdf.py @@ -64,7 +64,7 @@ class _rml_styles(object,): for style in node.findall('paraStyle'): sname = style.get('name') self.styles[sname] = self._para_style_update(style) - + self.styles_obj[sname] = reportlab.lib.styles.ParagraphStyle(sname, self.default_style["Normal"], **self.styles[sname]) for variable in node.findall('initialize'): @@ -809,6 +809,8 @@ class _rml_template(object): if story_cnt > 0: fis.append(platypus.PageBreak()) fis += r.render(node_story) + # Reset Page Number with new story tag + fis.append(PageReset()) story_cnt += 1 if self.localcontext: fis.append(PageCount()) diff --git a/bin/tools/convert.py b/bin/tools/convert.py index a90de107b29..a8749514958 100644 --- a/bin/tools/convert.py +++ b/bin/tools/convert.py @@ -68,6 +68,24 @@ def _obj(pool, cr, uid, model_str, context=None): model = pool.get(model_str) return lambda x: model.browse(cr, uid, x, context=context) +def _fix_multiple_roots(node): + """ + Surround the children of the ``node`` element of an XML field with a + single root "data" element, to prevent having a document with multiple + roots once parsed separately. + + XML nodes should have one root only, but we'd like to support + direct multiple roots in our partial documents (like inherited view architectures). + As a convention we'll surround multiple root with a container "data" element, to be + ignored later when parsing. + """ + + if len(node) > 1: + data_node = etree.Element("data") + for child in node: + data_node.append(child) + node.append(data_node) + def _eval_xml(self, node, pool, cr, uid, idref, context=None): if context is None: context = {} @@ -115,6 +133,7 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None): if not id in idref: idref[id]=self.id_get(cr, False, id) return s % idref + _fix_multiple_roots(node) return '\n'\ +_process("".join([etree.tostring(n, encoding='utf-8') for n in node]), diff --git a/bin/tools/yaml_import.py b/bin/tools/yaml_import.py index 87fd2ff432c..814779b3f36 100644 --- a/bin/tools/yaml_import.py +++ b/bin/tools/yaml_import.py @@ -70,14 +70,16 @@ def is_url(node): def is_eval(node): return isinstance(node, yaml_tag.Eval) - + def is_ref(node): return isinstance(node, yaml_tag.Ref) \ or _is_yaml_mapping(node, yaml_tag.Ref) - + def is_ir_set(node): return _is_yaml_mapping(node, yaml_tag.IrSet) +def is_string(node): + return isinstance(node, basestring) class TestReport(object): def __init__(self): @@ -374,6 +376,14 @@ class YamlInterpreter(object): elif column._type == "many2many": ids = [self.get_id(xml_id) for xml_id in expression] value = [(6, 0, ids)] + elif column._type == "date" and is_string(expression): + # enforce ISO format for string date values, to be locale-agnostic during tests + time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT) + value = expression + elif column._type == "datetime" and is_string(expression): + # enforce ISO format for string datetime values, to be locale-agnostic during tests + time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT) + value = expression else: # scalar field if is_eval(expression): value = self.process_eval(expression)