From e77abb81fe3af1bad710a199b5aeb4b684754518 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 10 Nov 2011 16:46:14 +0100 Subject: [PATCH 001/443] [IMP] Use babel's locale-aware date formatting when formatting dates in read_group titles bzr revid: xmo@openerp.com-20111110154614-ok0i1w11xvace41y --- openerp/osv/orm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index a89074d322d..a93f4f4a620 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -54,6 +54,8 @@ import time import traceback import types import warnings + +import babel.dates from lxml import etree import fields @@ -2471,7 +2473,9 @@ class BaseModel(object): dt = datetime.datetime.strptime(alldata[d['id']][groupby][:7], '%Y-%m') days = calendar.monthrange(dt.year, dt.month)[1] - d[groupby] = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d').strftime('%B %Y') + date_value = datetime.datetime.strptime(d[groupby][:10], '%Y-%m-%d') + d[groupby] = babel.dates.format_date( + date_value, format='MMMM yyyy', locale=context.get('lang', 'en_US')) d['__domain'] = [(groupby, '>=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-01', '%Y-%m-%d').strftime('%Y-%m-%d') or False),\ (groupby, '<=', alldata[d['id']][groupby] and datetime.datetime.strptime(alldata[d['id']][groupby][:7] + '-' + str(days), '%Y-%m-%d').strftime('%Y-%m-%d') or False)] + domain del alldata[d['id']][groupby] From 63ee6a74a91a630f5b5049f10ae67291df85f760 Mon Sep 17 00:00:00 2001 From: "Divyesh Makwana (Open ERP)" Date: Mon, 9 Jul 2012 17:09:54 +0530 Subject: [PATCH 002/443] [IMP] project : column project_task_history_cumulative.project_id does not exist. lp bug: https://launchpad.net/bugs/1022509 fixed bzr revid: mdi@tinyerp.com-20120709113954-l17wlurxovz6z05i --- addons/project/report/project_cumulative.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/addons/project/report/project_cumulative.xml b/addons/project/report/project_cumulative.xml index 3f7f3b26d96..535fbaf5f87 100644 --- a/addons/project/report/project_cumulative.xml +++ b/addons/project/report/project_cumulative.xml @@ -9,7 +9,6 @@ - @@ -63,9 +62,7 @@ - - From e2ba0eb95c38a16839836deeaa9a73b58221e9aa Mon Sep 17 00:00:00 2001 From: "Divyesh Makwana (Open ERP)" Date: Mon, 9 Jul 2012 18:42:59 +0530 Subject: [PATCH 003/443] [IMP] project : Revert the unneccessary changes. bzr revid: mdi@tinyerp.com-20120709131259-c9fl7g3hgwhxutgb --- addons/project/report/project_cumulative.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/project/report/project_cumulative.xml b/addons/project/report/project_cumulative.xml index 535fbaf5f87..177e945bc93 100644 --- a/addons/project/report/project_cumulative.xml +++ b/addons/project/report/project_cumulative.xml @@ -9,6 +9,7 @@ + From 79b29e17c4966f0be4f6bc42397c156e6e2d7e49 Mon Sep 17 00:00:00 2001 From: "pankita shah (Open ERP)" Date: Wed, 25 Jul 2012 12:59:52 +0530 Subject: [PATCH 004/443] [FIX]remove ProgrammingError in event module bzr revid: shp@tinyerp.com-20120725072952-mcnyappum33w1smy --- addons/event/report/report_event_registration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py index 17472e0cc95..59515f87a00 100644 --- a/addons/event/report/report_event_registration.py +++ b/addons/event/report/report_event_registration.py @@ -80,6 +80,7 @@ class report_event_registration(osv.osv): LEFT JOIN event_registration r ON (e.id=r.event_id) + where r.id is not null GROUP BY event_id, From 7096a336c084bab9f12aa1d13ae96e82cbf12d7d Mon Sep 17 00:00:00 2001 From: Date: Tue, 28 Aug 2012 16:22:02 +0200 Subject: [PATCH 005/443] [FIX] a default value (created using "Set Default" wizard) should never be shared between companies, but assigned on the user's company bzr revid: guewen.baconnier@camptocamp.com-20120828142202-9lwdicrdgc2spp6s --- addons/web/static/src/js/view_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index b64f34f3aaf..60e3d8361bc 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1007,7 +1007,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM field_to_set, self.fields[field_to_set].get_value(), all_users, - false, + true, condition || false ]).then(function () { d.close(); }); }} From 9805c665c89784bdaa5234668ed79ca2280cb124 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 19 Sep 2012 13:40:47 +0200 Subject: [PATCH 006/443] [ADD] big bit on new import: pretty much everything but o2m bzr revid: xmo@openerp.com-20120919114047-w4paoim95oxr91zb --- openerp/addons/base/ir/__init__.py | 1 + openerp/addons/base/ir/ir_fields.py | 193 ++++ openerp/osv/fields.py | 42 +- openerp/osv/orm.py | 226 +++- .../tests/addons/test_impex/tests/__init__.py | 3 +- .../addons/test_impex/tests/test_load.py | 987 ++++++++++++++++++ openerp/tests/test_misc.py | 38 +- openerp/tools/misc.py | 34 + setup.py | 1 + 9 files changed, 1507 insertions(+), 18 deletions(-) create mode 100644 openerp/addons/base/ir/ir_fields.py create mode 100644 openerp/tests/addons/test_impex/tests/test_load.py diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index bc4f3eecfbf..ba8b785f39d 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -40,6 +40,7 @@ import wizard import ir_config_parameter import osv_memory_autovacuum import ir_mail_server +import ir_fields # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py new file mode 100644 index 00000000000..f73067c9f6c --- /dev/null +++ b/openerp/addons/base/ir/ir_fields.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +import functools +import operator +import warnings +from openerp.osv import orm, fields +from openerp.tools.translate import _ + +class ConversionNotFound(ValueError): pass + +class ir_fields_converter(orm.Model): + _name = 'ir.fields.converter' + + def to_field(self, cr, uid, model, column, fromtype=str, context=None): + """ Fetches a converter for the provided column object, from the + specified type. + + A converter is simply a callable taking a value of type ``fromtype`` + (or a composite of ``fromtype``, e.g. list or dict) and returning a + value acceptable for a write() on the column ``column``. + + By default, tries to get a method on itself with a name matching the + pattern ``_$fromtype_$column._type`` and returns it. + + :param cr: openerp cursor + :param uid: ID of user calling the converter + :param column: column object to generate a value for + :type column: :class:`fields._column` + :param type fromtype: type to convert to something fitting for ``column`` + :param context: openerp request context + :return: a function (fromtype -> column.write_type), if a converter is found + :rtype: Callable | None + """ + # FIXME: return None + converter = getattr( + self, '_%s_to_%s' % (fromtype.__name__, column._type)) + if not converter: return None + + return functools.partial( + converter, cr, uid, model, column, context=context) + + def _str_to_boolean(self, cr, uid, model, column, value, context=None): + return value.lower() not in ('', '0', 'false', 'off') + + def _str_to_integer(self, cr, uid, model, column, value, context=None): + if not value: return False + return int(value) + + def _str_to_float(self, cr, uid, model, column, value, context=None): + if not value: return False + return float(value) + + def _str_to_char(self, cr, uid, model, column, value, context=None): + return value or False + + def _str_to_text(self, cr, uid, model, column, value, context=None): + return value or False + + def _get_translations(self, cr, uid, types, src, context): + Translations = self.pool['ir.translation'] + tnx_ids = Translations.search( + cr, uid, [('type', 'in', types), ('src', '=', src)], context=context) + tnx = Translations.read(cr, uid, tnx_ids, ['value'], context=context) + return map(operator.itemgetter('value'), tnx) + def _str_to_selection(self, cr, uid, model, column, value, context=None): + + selection = column.selection + if not isinstance(selection, (tuple, list)): + # FIXME: Don't pass context to avoid translations? + # Or just copy context & remove lang? + selection = selection(model, cr, uid) + for item, label in selection: + labels = self._get_translations( + cr, uid, ('selection', 'model'), label, context=context) + labels.append(label) + if value == unicode(item) or value in labels: + return item + raise ValueError( + _(u"Value '%s' not found in selection field '%%(field)s'") % ( + value)) + + + def db_id_for(self, cr, uid, model, column, subfield, value, context=None): + """ Finds a database id for the reference ``value`` in the referencing + subfield ``subfield`` of the provided column of the provided model. + + :param cr: OpenERP cursor + :param uid: OpenERP user id + :param model: model to which the column belongs + :param column: relational column for which references are provided + :param subfield: a relational subfield allowing building of refs to + existing records: ``None`` for a name_get/name_search, + ``id`` for an external id and ``.id`` for a database + id + :param value: value of the reference to match to an actual record + :param context: OpenERP request context + :return: a pair of the matched database identifier (if any) and the + translated user-readable name for the field + :rtype: (ID|None, unicode) + """ + id = None + RelatedModel = self.pool[column._obj] + if subfield == '.id': + field_type = _(u"database id") + try: tentative_id = int(value) + except ValueError: tentative_id = value + if RelatedModel.search(cr, uid, [('id', '=', tentative_id)], + context=context): + id = tentative_id + elif subfield == 'id': + field_type = _(u"external id") + if '.' in value: + module, xid = value.split('.', 1) + else: + module, xid = '', value + ModelData = self.pool['ir.model.data'] + try: + md_id = ModelData._get_id(cr, uid, module, xid) + model_data = ModelData.read(cr, uid, [md_id], ['res_id'], + context=context) + if model_data: + id = model_data[0]['res_id'] + except ValueError: pass # leave id is None + elif subfield is None: + field_type = _(u"name") + ids = RelatedModel.name_search( + cr, uid, name=value, operator='=', context=context) + if ids: + if len(ids) > 1: + warnings.warn( + _(u"Found multiple matches for field '%%(field)s' (%d matches)") + % (len(ids)), orm.ImportWarning) + id, _name = ids[0] + else: + raise Exception(u"Unknown sub-field '%s'" % subfield) + return id, field_type + + def _referencing_subfield(self, record): + """ Checks the record for the subfields allowing referencing (an + existing record in an other table), errors out if it finds potential + conflicts (multiple referencing subfields) or non-referencing subfields + returns the name of the correct subfield. + + :param record: + :return: the record subfield to use for referencing + :rtype: str + """ + # Can import by name_get, external id or database id + allowed_fields = set([None, 'id', '.id']) + fieldset = set(record.iterkeys()) + if fieldset - allowed_fields: + raise ValueError( + _(u"Can not create Many-To-One records indirectly, import the field separately")) + if len(fieldset) > 1: + raise ValueError( + _(u"Ambiguous specification for field '%(field)s', only provide one of name, external id or database id")) + + # only one field left possible, unpack + [subfield] = fieldset + return subfield + + def _str_to_many2one(self, cr, uid, model, column, values, context=None): + # Should only be one record, unpack + [record] = values + + subfield = self._referencing_subfield(record) + + reference = record[subfield] + id, subfield_type = self.db_id_for( + cr, uid, model, column, subfield, reference, context=context) + + if id is None: + raise ValueError( + _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") + % {'field_type': subfield_type, 'value': reference}) + return id + def _str_to_many2many(self, cr, uid, model, column, value, context=None): + [record] = value + + subfield = self._referencing_subfield(record) + + ids = [] + for reference in record[subfield].split(','): + id, subfield_type = self.db_id_for( + cr, uid, model, column, subfield, reference, context=context) + if id is None: + raise ValueError( + _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") + % {'field_type': subfield_type, 'value': reference}) + ids.append(id) + + return [(6, 0, ids)] + def _str_to_one2many(self, cr, uid, model, column, value, context=None): + return value diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 037d2a90986..36c6600a7e5 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1588,19 +1588,32 @@ def field_to_dict(model, cr, user, field, context=None): class column_info(object): - """Struct containing details about an osv column, either one local to - its model, or one inherited via _inherits. + """ Struct containing details about an osv column, either one local to + its model, or one inherited via _inherits. - :attr name: name of the column - :attr column: column instance, subclass of osv.fields._column - :attr parent_model: if the column is inherited, name of the model - that contains it, None for local columns. - :attr parent_column: the name of the column containing the m2o - relationship to the parent model that contains - this column, None for local columns. - :attr original_parent: if the column is inherited, name of the original - parent model that contains it i.e in case of multilevel - inheritence, None for local columns. + .. attribute:: name + + name of the column + + .. attribute:: column + + column instance, subclass of :class:`_column` + + .. attribute:: parent_model + + if the column is inherited, name of the model that contains it, + ``None`` for local columns. + + .. attribute:: parent_column + + the name of the column containing the m2o relationship to the + parent model that contains this column, ``None`` for local columns. + + .. attribute:: original_parent + + if the column is inherited, name of the original parent model that + contains it i.e in case of multilevel inheritance, ``None`` for + local columns. """ def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None): self.name = name @@ -1609,5 +1622,10 @@ class column_info(object): self.parent_column = parent_column self.original_parent = original_parent + def __str__(self): + return '%s(%s, %s, %s, %s, %s)' % ( + self.__name__, self.name, self.column, + self.parent_model, self.parent_column, self.original_parent) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 9d3b92a3d31..57cc5823dd9 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -52,13 +52,17 @@ import re import simplejson import time import types + +import psycopg2 from lxml import etree +import warnings import fields import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.config import config +from openerp.tools.misc import CountingStream from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp import SUPERUSER_ID @@ -1242,7 +1246,7 @@ class BaseModel(object): * The last item is currently unused, with no specific semantics :param fields: list of fields to import - :param data: data to import + :param datas: data to import :param mode: 'init' or 'update' for record creation :param current_module: module name :param noupdate: flag for record creation @@ -1438,6 +1442,199 @@ class BaseModel(object): self._parent_store_compute(cr) return position, 0, 0, 0 + def load(self, cr, uid, fields, data, context=None): + """ + + :param cr: cursor for the request + :param int uid: ID of the user attempting the data import + :param fields: list of fields to import, at the same index as the corresponding data + :type fields: list(str) + :param data: row-major matrix of data to import + :type data: list(list(str)) + :param dict context: + :returns: + """ + cr.execute('SAVEPOINT model_load') + messages = [] + + fields = map(fix_import_export_id_paths, fields) + ModelData = self.pool['ir.model.data'] + + mode = 'init' + current_module = '' + noupdate = False + + ids = [] + for id, xid, record, info in self._convert_records(cr, uid, + self._extract_records(cr, uid, fields, data, + context=context, log=messages.append), + context=context, log=messages.append): + cr.execute('SAVEPOINT model_load_save') + try: + ids.append(ModelData._update(cr, uid, self._name, + current_module, record, mode=mode, xml_id=xid, + noupdate=noupdate, res_id=id, context=context)) + cr.execute('RELEASE SAVEPOINT model_load_save') + except psycopg2.Error, e: + # Failed to write, log to messages, rollback savepoint (to + # avoid broken transaction) and keep going + cr.execute('ROLLBACK TO SAVEPOINT model_load_save') + messages.append(dict(info, type="error", message=str(e))) + if any(message['type'] == 'error' for message in messages): + cr.execute('ROLLBACK TO SAVEPOINT model_load') + return False, messages + return ids, messages + def _extract_records(self, cr, uid, fields_, data, + context=None, log=lambda a: None): + """ Generates record dicts from the data iterable. + + The result is a generator of dicts mapping field names to raw + (unconverted, unvalidated) values. + + For relational fields, if sub-fields were provided the value will be + a list of sub-records + + The following sub-fields may be set on the record (by key): + * None is the name_get for the record (to use with name_create/name_search) + * "id" is the External ID for the record + * ".id" is the Database ID for the record + + :param ImportLogger logger: + """ + columns = dict((k, v.column) for k, v in self._all_columns.iteritems()) + # Fake columns to avoid special cases in extractor + columns[None] = fields.char('rec_name') + columns['id'] = fields.char('External ID') + columns['.id'] = fields.integer('Database ID') + + # m2o fields can't be on multiple lines so exclude them from the + # is_relational field rows filter, but special-case it later on to + # be handled with relational fields (as it can have subfields) + is_relational = lambda field: columns[field]._type in ('one2many', 'many2many', 'many2one') + get_o2m_values = itemgetter_tuple( + [index for index, field in enumerate(fields_) + if columns[field[0]]._type == 'one2many']) + get_nono2m_values = itemgetter_tuple( + [index for index, field in enumerate(fields_) + if columns[field[0]]._type != 'one2many']) + # Checks if the provided row has any non-empty non-relational field + def only_o2m_values(row, f=get_nono2m_values, g=get_o2m_values): + return any(g(row)) and not any(f(row)) + + rows = CountingStream(data) + while True: + row = next(rows, None) + if row is None: return + record_row_index = rows.index + + # copy non-relational fields to record dict + record = dict((field[0], value) + for field, value in itertools.izip(fields_, row) + if not is_relational(field[0])) + + # Get all following rows which have relational values attached to + # the current record (no non-relational values) + # WARNING: replaces existing ``rows`` + record_span, _rows = span(only_o2m_values, rows) + # stitch record row back on for relational fields + record_span = itertools.chain([row], record_span) + for relfield in set( + field[0] for field in fields_ + if is_relational(field[0])): + column = columns[relfield] + # FIXME: how to not use _obj without relying on fields_get? + Model = self.pool[column._obj] + + # copy stream to reuse for next relational field + fieldrows, record_span = itertools.tee(record_span) + # get only cells for this sub-field, should be strictly + # non-empty, field path [None] is for name_get column + indices, subfields = zip(*((index, field[1:] or [None]) + for index, field in enumerate(fields_) + if field[0] == relfield)) + + # return all rows which have at least one value for the + # subfields of relfield + relfield_data = filter(any, map(itemgetter_tuple(indices), fieldrows)) + record[relfield] = [subrecord + for subrecord, _subinfo in Model._extract_records( + cr, uid, subfields, relfield_data, + context=context, log=log)] + # Ensure full consumption of the span (and therefore advancement of + # ``rows``) even if there are no relational fields. Needs two as + # the code above stiched the row back on (so first call may only + # get the stiched row without advancing the underlying operator row + # itself) + next(record_span, None) + next(record_span, None) + + # old rows consumption (by iterating the span) should be done here, + # at this point the old ``rows`` is 1 past `span` (either on the + # next record row or past ``StopIteration``, so wrap new ``rows`` + # (``_rows``) in a counting stream indexed 1-before the old + # ``rows`` + rows = CountingStream(_rows, rows.index - 1) + yield record, {'rows': {'from': record_row_index,'to': rows.index}} + def _convert_records(self, cr, uid, records, + context=None, log=lambda a: None): + """ Converts records from the source iterable (recursive dicts of + strings) into forms which can be written to the database (via + self.create or (ir.model.data)._update) + + :param ImportLogger parent_logger: + :returns: a list of triplets of (id, xid, record) + :rtype: list((int|None, str|None, dict)) + """ + Converter = self.pool['ir.fields.converter'] + columns = dict((k, v.column) for k, v in self._all_columns.iteritems()) + converters = dict( + (k, Converter.to_field(cr, uid, self, column, context=context)) + for k, column in columns.iteritems()) + + stream = CountingStream(records) + for record, extras in stream: + dbid = False + xid = False + converted = {} + # name_get/name_create + if None in record: pass + # xid + if 'id' in record: + xid = record['id'] + # dbid + if '.id' in record: + try: + dbid = int(record['.id']) + except ValueError: + # in case of overridden id column + dbid = record['.id'] + if not self.search(cr, uid, [('id', '=', dbid)], context=context): + log(dict(extras, + type='error', + record=stream.index, + field='.id', + message=_(u"Unknown database identifier '%s'") % dbid)) + dbid = False + + for field, strvalue in record.iteritems(): + if field in (None, 'id', '.id'): continue + + message_base = dict(extras, record=stream.index, field=field) + with warnings.catch_warnings(record=True) as w: + try: + converted[field] = converters[field](strvalue) + + for warning in w: + log(dict(message_base, type='warning', + message=unicode(warning.message) % message_base)) + except ValueError, e: + log(dict(message_base, + type='error', + message=unicode(e) % message_base + )) + + yield dbid, xid, converted, dict(extras, record=stream.index) + def get_invalid_fields(self, cr, uid): return list(self._invalids) @@ -5108,5 +5305,32 @@ class AbstractModel(BaseModel): _auto = False # don't create any database backend for AbstractModels _register = False # not visible in ORM registry, meant to be python-inherited only +def span(predicate, iterable): + """ Splits the iterable between the longest prefix of ``iterable`` whose + elements satisfy ``predicate`` and the rest. + If called with a list, equivalent to:: + + takewhile(predicate, lst), dropwhile(predicate, lst) + + :param callable predicate: + :param iterable: + :rtype: (iterable, iterable) + """ + it1, it2 = itertools.tee(iterable) + return (itertools.takewhile(predicate, it1), + itertools.dropwhile(predicate, it2)) +def itemgetter_tuple(items): + """ Fixes itemgetter inconsistency (useful in some cases) of not returning + a tuple if len(items) == 1: always returns an n-tuple where n = len(items) + """ + if len(items) == 0: + return lambda a: () + if len(items) == 1: + return lambda gettable: (gettable[items[0]],) + return operator.itemgetter(*items) +class ImportWarning(Warning): + """ Used to send warnings upwards the stack during the import process + """ + pass # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_impex/tests/__init__.py b/openerp/tests/addons/test_impex/tests/__init__.py index d6af53cba1b..8cab56fcc9a 100644 --- a/openerp/tests/addons/test_impex/tests/__init__.py +++ b/openerp/tests/addons/test_impex/tests/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from . import test_export, test_import +from . import test_export, test_import, test_load fast_suite = [ ] @@ -8,6 +8,7 @@ fast_suite = [ checks = [ test_export, test_import, + test_load, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py new file mode 100644 index 00000000000..80e5cfe9c8f --- /dev/null +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -0,0 +1,987 @@ +# -*- coding: utf-8 -*- +import openerp.modules.registry +import openerp + +from openerp.tests import common +from openerp.tools.misc import mute_logger + +def message(msg, type='error', from_=0, to_=0, record=0, field='value'): + return { + 'type': type, + 'rows': {'from': from_, 'to': to_}, + 'record': record, + 'field': field, + 'message': msg + } + +def error(row, message, record=None, **kwargs): + """ Failed import of the record ``record`` at line ``row``, with the error + message ``message`` + + :param str message: + :param dict record: + """ + return ( + -1, dict(record or {}, **kwargs), + "Line %d : %s" % (row, message), + '') + +def values(seq, field='value'): + return [item[field] for item in seq] + +class ImporterCase(common.TransactionCase): + model_name = False + + def __init__(self, *args, **kwargs): + super(ImporterCase, self).__init__(*args, **kwargs) + self.model = None + + def setUp(self): + super(ImporterCase, self).setUp() + self.model = self.registry(self.model_name) + self.registry('ir.model.data').clear_caches() + + def import_(self, fields, rows, context=None): + return self.model.load( + self.cr, openerp.SUPERUSER_ID, fields, rows, context=context) + def read(self, fields=('value',), domain=(), context=None): + return self.model.read( + self.cr, openerp.SUPERUSER_ID, + self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context), + fields=fields, context=context) + def browse(self, domain=(), context=None): + return self.model.browse( + self.cr, openerp.SUPERUSER_ID, + self.model.search(self.cr, openerp.SUPERUSER_ID, domain, context=context), + context=context) + + def xid(self, record): + ModelData = self.registry('ir.model.data') + + ids = ModelData.search( + self.cr, openerp.SUPERUSER_ID, + [('model', '=', record._table_name), ('res_id', '=', record.id)]) + if ids: + d = ModelData.read( + self.cr, openerp.SUPERUSER_ID, ids, ['name', 'module'])[0] + if d['module']: + return '%s.%s' % (d['module'], d['name']) + return d['name'] + + name = dict(record.name_get())[record.id] + # fix dotted name_get results, otherwise xid lookups blow up + name = name.replace('.', '-') + ModelData.create(self.cr, openerp.SUPERUSER_ID, { + 'name': name, + 'model': record._table_name, + 'res_id': record.id, + 'module': '__test__' + }) + return '__test__.' + name + +class test_ids_stuff(ImporterCase): + model_name = 'export.integer' + + def test_create_with_id(self): + ids, messages = self.import_(['.id', 'value'], [['42', '36']]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'field': '.id', + 'message': u"Unknown database identifier '42'", + }]) + def test_create_with_xid(self): + ids, messages = self.import_(['id', 'value'], [['somexmlid', '42']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual( + 'somexmlid', + self.xid(self.browse()[0])) + + def test_update_with_id(self): + id = self.model.create(self.cr, openerp.SUPERUSER_ID, {'value': 36}) + self.assertEqual( + 36, + self.model.browse(self.cr, openerp.SUPERUSER_ID, id).value) + + ids, messages = self.import_(['.id', 'value'], [[str(id), '42']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual( + [42], # updated value to imported + values(self.read())) + + def test_update_with_xid(self): + self.import_(['id', 'value'], [['somexmlid', '36']]) + self.assertEqual([36], values(self.read())) + + self.import_(['id', 'value'], [['somexmlid', '1234567']]) + self.assertEqual([1234567], values(self.read())) + +class test_boolean_field(ImporterCase): + model_name = 'export.boolean' + + def test_empty(self): + self.assertEqual( + self.import_(['value'], []), + ([], [])) + + def test_exported(self): + ids, messages = self.import_(['value'], [['False'], ['True'], ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + records = self.read() + self.assertEqual([ + False, + True, + ], values(records)) + + def test_falses(self): + ids, messages = self.import_( + ['value'], + [[u'0'], [u'off'], + [u'false'], [u'FALSE'], + [u'OFF'], [u''], + ]) + self.assertEqual(len(ids), 6) + self.assertFalse(messages) + self.assertEqual([ + False, + False, + False, + False, + False, + False, + ], + values(self.read())) + + def test_trues(self): + ids, messages = self.import_( + ['value'], + [['no'], + ['None'], + ['nil'], + ['()'], + ['f'], + ['#f'], + # Problem: OpenOffice (and probably excel) output localized booleans + ['VRAI'], + ]) + self.assertEqual(len(ids), 7) + # FIXME: should warn for values which are not "true", "yes" or "1" + self.assertFalse(messages) + self.assertEqual( + [True] * 7, + values(self.read())) + +class test_integer_field(ImporterCase): + model_name = 'export.integer' + + def test_none(self): + self.assertEqual( + self.import_(['value'], []), + ([], [])) + + def test_empty(self): + ids, messages = self.import_(['value'], [['']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual( + [False], + values(self.read())) + + def test_zero(self): + ids, messages = self.import_(['value'], [['0']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + ids, messages = self.import_(['value'], [['-0']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + self.assertEqual([False, False], values(self.read())) + + def test_positives(self): + ids, messages = self.import_(['value'], [ + ['1'], + ['42'], + [str(2**31-1)], + ['12345678'] + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + + self.assertEqual([ + 1, 42, 2**31-1, 12345678 + ], values(self.read())) + + def test_negatives(self): + ids, messages = self.import_(['value'], [ + ['-1'], + ['-42'], + [str(-(2**31 - 1))], + [str(-(2**31))], + ['-12345678'] + ]) + self.assertEqual(len(ids), 5) + self.assertFalse(messages) + self.assertEqual([ + -1, -42, -(2**31 - 1), -(2**31), -12345678 + ], values(self.read())) + + @mute_logger('openerp.sql_db') + def test_out_of_range(self): + ids, messages = self.import_(['value'], [[str(2**31)]]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'message': "integer out of range\n" + }]) + + ids, messages = self.import_(['value'], [[str(-2**32)]]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'message': "integer out of range\n" + }]) + + def test_nonsense(self): + ids, messages = self.import_(['value'], [['zorglub']]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'field': 'value', + 'message': u"invalid literal for int() with base 10: 'zorglub'", + }]) + +class test_float_field(ImporterCase): + model_name = 'export.float' + def test_none(self): + self.assertEqual( + self.import_(['value'], []), + ([], [])) + + def test_empty(self): + ids, messages = self.import_(['value'], [['']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual( + [False], + values(self.read())) + + def test_zero(self): + ids, messages = self.import_(['value'], [['0']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + ids, messages = self.import_(['value'], [['-0']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + self.assertEqual([False, False], values(self.read())) + + def test_positives(self): + ids, messages = self.import_(['value'], [ + ['1'], + ['42'], + [str(2**31-1)], + ['12345678'], + [str(2**33)], + ['0.000001'], + ]) + self.assertEqual(len(ids), 6) + self.assertFalse(messages) + + self.assertEqual([ + 1, 42, 2**31-1, 12345678, 2.0**33, .000001 + ], values(self.read())) + + def test_negatives(self): + ids, messages = self.import_(['value'], [ + ['-1'], + ['-42'], + [str(-2**31 + 1)], + [str(-2**31)], + ['-12345678'], + [str(-2**33)], + ['-0.000001'], + ]) + self.assertEqual(len(ids), 7) + self.assertFalse(messages) + self.assertEqual([ + -1, -42, -(2**31 - 1), -(2**31), -12345678, -2.0**33, -.000001 + ], values(self.read())) + + def test_nonsense(self): + ids, messages = self.import_(['value'], [['foobar']]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'field': 'value', + 'message': u"invalid literal for float(): foobar", + }]) + +class test_string_field(ImporterCase): + model_name = 'export.string.bounded' + + def test_empty(self): + ids, messages = self.import_(['value'], [['']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual([False], values(self.read())) + + def test_imported(self): + ids, messages = self.import_(['value'], [ + [u'foobar'], + [u'foobarbaz'], + [u'Með suð í eyrum við spilum endalaust'], + [u"People 'get' types. They use them all the time. Telling " + u"someone he can't pound a nail with a banana doesn't much " + u"surprise him."] + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + self.assertEqual([ + u"foobar", + u"foobarbaz", + u"Með suð í eyrum ", + u"People 'get' typ", + ], values(self.read())) + +class test_unbound_string_field(ImporterCase): + model_name = 'export.string' + + def test_imported(self): + ids, messages = self.import_(['value'], [ + [u'í dag viðrar vel til loftárása'], + # ackbar.jpg + [u"If they ask you about fun, you tell them – fun is a filthy" + u" parasite"] + ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + self.assertEqual([ + u"í dag viðrar vel til loftárása", + u"If they ask you about fun, you tell them – fun is a filthy parasite" + ], values(self.read())) + +class test_text(ImporterCase): + model_name = 'export.text' + + def test_empty(self): + ids, messages = self.import_(['value'], [['']]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual([False], values(self.read())) + + def test_imported(self): + s = (u"Breiðskífa er notað um útgefna hljómplötu sem inniheldur " + u"stúdíóupptökur frá einum flytjanda. Breiðskífur eru oftast " + u"milli 25-80 mínútur og er lengd þeirra oft miðuð við 33⅓ " + u"snúninga 12 tommu vínylplötur (sem geta verið allt að 30 mín " + u"hvor hlið).\n\nBreiðskífur eru stundum tvöfaldar og eru þær þá" + u" gefnar út á tveimur geisladiskum eða tveimur vínylplötum.") + ids, messages = self.import_(['value'], [[s]]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + self.assertEqual([s], values(self.read())) + +class test_selection(ImporterCase): + model_name = 'export.selection' + translations_fr = [ + ("Qux", "toto"), + ("Bar", "titi"), + ("Foo", "tete"), + ] + + def test_imported(self): + ids, messages = self.import_(['value'], [ + ['Qux'], + ['Bar'], + ['Foo'], + ['2'], + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + self.assertEqual([3, 2, 1, 2], values(self.read())) + + def test_imported_translated(self): + self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { + 'name': u'Français', + 'code': 'fr_FR', + 'translatable': True, + 'date_format': '%d.%m.%Y', + 'decimal_point': ',', + 'thousand_sep': ' ', + }) + Translations = self.registry('ir.translation') + for source, value in self.translations_fr: + Translations.create(self.cr, openerp.SUPERUSER_ID, { + 'name': 'export.selection,value', + 'lang': 'fr_FR', + 'type': 'selection', + 'src': source, + 'value': value + }) + + ids, messages = self.import_(['value'], [ + ['toto'], + ['tete'], + ['titi'], + ], context={'lang': 'fr_FR'}) + self.assertEqual(len(ids), 3) + self.assertFalse(messages) + + self.assertEqual([3, 1, 2], values(self.read())) + + ids, messages = self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + def test_invalid(self): + ids, messages = self.import_(['value'], [['Baz']]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'field': 'value', + 'message': "Value 'Baz' not found in selection field 'value'", + }]) + + ids, messages = self.import_(['value'], [[42]]) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 0, 'to': 0}, + 'record': 0, + 'field': 'value', + 'message': "Value '42' not found in selection field 'value'", + }]) + +class test_selection_function(ImporterCase): + model_name = 'export.selection.function' + translations_fr = [ + ("Corge", "toto"), + ("Grault", "titi"), + ("Whee", "tete"), + ("Moog", "tutu"), + ] + + def test_imported(self): + """ import uses fields_get, so translates import label (may or may not + be good news) *and* serializes the selection function to reverse it: + import does not actually know that the selection field uses a function + """ + # NOTE: conflict between a value and a label => ? + ids, messages = self.import_(['value'], [ + ['3'], + ["Grault"], + ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + self.assertEqual( + ['3', '1'], + values(self.read())) + + def test_translated(self): + """ Expects output of selection function returns translated labels + """ + self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { + 'name': u'Français', + 'code': 'fr_FR', + 'translatable': True, + 'date_format': '%d.%m.%Y', + 'decimal_point': ',', + 'thousand_sep': ' ', + }) + Translations = self.registry('ir.translation') + for source, value in self.translations_fr: + Translations.create(self.cr, openerp.SUPERUSER_ID, { + 'name': 'export.selection,value', + 'lang': 'fr_FR', + 'type': 'selection', + 'src': source, + 'value': value + }) + ids, messages = self.import_(['value'], [ + ['toto'], + ['tete'], + ], context={'lang': 'fr_FR'}) + self.assertIs(ids, False) + self.assertEqual(messages, [{ + 'type': 'error', + 'rows': {'from': 1, 'to': 1}, + 'record': 1, + 'field': 'value', + 'message': "Value 'tete' not found in selection field 'value'", + }]) + ids, messages = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + +class test_m2o(ImporterCase): + model_name = 'export.many2one' + + def test_by_name(self): + # create integer objects + integer_id1 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + integer_id2 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 36}) + # get its name + name1 = dict(self.registry('export.integer').name_get( + self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1] + name2 = dict(self.registry('export.integer').name_get( + self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2] + + ids , messages = self.import_(['value'], [ + # import by name_get + [name1], + [name1], + [name2], + ]) + self.assertFalse(messages) + self.assertEqual(len(ids), 3) + # correct ids assigned to corresponding records + self.assertEqual([ + (integer_id1, name1), + (integer_id1, name1), + (integer_id2, name2),], + values(self.read())) + + def test_by_xid(self): + ExportInteger = self.registry('export.integer') + integer_id = ExportInteger.create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + xid = self.xid(ExportInteger.browse( + self.cr, openerp.SUPERUSER_ID, [integer_id])[0]) + + ids, messages = self.import_(['value/id'], [[xid]]) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) + b = self.browse() + self.assertEqual(42, b[0].value.value) + + def test_by_id(self): + integer_id = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + ids, messages = self.import_(['value/.id'], [[integer_id]]) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) + b = self.browse() + self.assertEqual(42, b[0].value.value) + + def test_by_names(self): + integer_id1 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + integer_id2 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + name1 = dict(self.registry('export.integer').name_get( + self.cr, openerp.SUPERUSER_ID,[integer_id1]))[integer_id1] + name2 = dict(self.registry('export.integer').name_get( + self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2] + # names should be the same + self.assertEqual(name1, name2) + + ids, messages = self.import_(['value'], [[name2]]) + self.assertEqual( + messages, + [message(u"Found multiple matches for field 'value' (2 matches)", + type='warning')]) + self.assertEqual(len(ids), 1) + self.assertEqual([ + (integer_id1, name1) + ], values(self.read())) + + def test_fail_by_implicit_id(self): + """ Can't implicitly import records by id + """ + # create integer objects + integer_id1 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 42}) + integer_id2 = self.registry('export.integer').create( + self.cr, openerp.SUPERUSER_ID, {'value': 36}) + + # Because name_search all the things. Fallback schmallback + ids, messages = self.import_(['value'], [ + # import by id, without specifying it + [integer_id1], + [integer_id2], + [integer_id1], + ]) + self.assertEqual(messages, [ + message(u"No matching record found for name '%s' in field 'value'" % id, + from_=index, to_=index, record=index) + for index, id in enumerate([integer_id1, integer_id2, integer_id1])]) + self.assertIs(ids, False) + + def test_sub_field(self): + """ Does not implicitly create the record, does not warn that you can't + import m2o subfields (at all)... + """ + ids, messages = self.import_(['value/value'], [['42']]) + self.assertEqual(messages, [ + message(u"Can not create Many-To-One records indirectly, import " + u"the field separately")]) + self.assertIs(ids, False) + + def test_fail_noids(self): + ids, messages = self.import_(['value'], [['nameisnoexist:3']]) + self.assertEqual(messages, [message( + u"No matching record found for name 'nameisnoexist:3' " + u"in field 'value'")]) + self.assertIs(ids, False) + + ids, messages = self.import_(['value/id'], [['noxidhere']]) + self.assertEqual(messages, [message( + u"No matching record found for external id 'noxidhere' " + u"in field 'value'")]) + self.assertIs(ids, False) + + ids, messages = self.import_(['value/.id'], [['66']]) + self.assertEqual(messages, [message( + u"No matching record found for database id '66' " + u"in field 'value'")]) + self.assertIs(ids, False) + + def test_fail_multiple(self): + ids, messages = self.import_( + ['value', 'value/id'], + [['somename', 'somexid']]) + self.assertEqual(messages, [message( + u"Ambiguous specification for field 'value', only provide one of " + u"name, external id or database id")]) + self.assertIs(ids, False) + +class test_m2m(ImporterCase): + model_name = 'export.many2many' + + # apparently, one and only thing which works is a + # csv_internal_sep-separated list of ids, xids, or names (depending if + # m2m/.id, m2m/id or m2m[/anythingelse] + def test_ids(self): + id1 = self.registry('export.many2many.other').create( + self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) + id2 = self.registry('export.many2many.other').create( + self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) + id3 = self.registry('export.many2many.other').create( + self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) + id4 = self.registry('export.many2many.other').create( + self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) + id5 = self.registry('export.many2many.other').create( + self.cr, openerp.SUPERUSER_ID, {'value': 99, 'str': 'record4'}) + + ids, messages = self.import_(['value/.id'], [ + ['%d,%d' % (id1, id2)], + ['%d,%d,%d' % (id1, id3, id4)], + ['%d,%d,%d' % (id1, id2, id3)], + ['%d' % id5] + ]) + self.assertFalse(messages) + self.assertEqual(len(ids), 4) + + ids = lambda records: [record.id for record in records] + + b = self.browse() + self.assertEqual(ids(b[0].value), [id1, id2]) + self.assertEqual(values(b[0].value), [3, 44]) + + self.assertEqual(ids(b[2].value), [id1, id2, id3]) + self.assertEqual(values(b[2].value), [3, 44, 84]) + + def test_noids(self): + ids, messages = self.import_(['value/.id'], [['42']]) + self.assertEqual(messages, [message( + u"No matching record found for database id '42' in field " + u"'value'")]) + self.assertIs(ids, False) + + def test_xids(self): + M2O_o = self.registry('export.many2many.other') + id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) + id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) + id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) + id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) + records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) + + ids, messages = self.import_(['value/id'], [ + ['%s,%s' % (self.xid(records[0]), self.xid(records[1]))], + ['%s' % self.xid(records[3])], + ['%s,%s' % (self.xid(records[2]), self.xid(records[1]))], + ]) + self.assertFalse(messages) + self.assertEqual(len(ids), 3) + + b = self.browse() + self.assertEqual(values(b[0].value), [3, 44]) + self.assertEqual(values(b[2].value), [44, 84]) + def test_noxids(self): + ids, messages = self.import_(['value/id'], [['noxidforthat']]) + self.assertEqual(messages, [message( + u"No matching record found for external id 'noxidforthat' " + u"in field 'value'")]) + self.assertIs(ids, False) + + def test_names(self): + M2O_o = self.registry('export.many2many.other') + id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) + id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) + id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) + id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) + records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) + + name = lambda record: dict(record.name_get())[record.id] + + ids, messages = self.import_(['value'], [ + ['%s,%s' % (name(records[1]), name(records[2]))], + ['%s,%s,%s' % (name(records[0]), name(records[1]), name(records[2]))], + ['%s,%s' % (name(records[0]), name(records[3]))], + ]) + self.assertFalse(messages) + self.assertEqual(len(ids), 3) + + b = self.browse() + self.assertEqual(values(b[1].value), [3, 44, 84]) + self.assertEqual(values(b[2].value), [3, 9]) + + def test_nonames(self): + ids, messages = self.import_(['value'], [['wherethem2mhavenonames']]) + self.assertEqual(messages, [message( + u"No matching record found for name 'wherethem2mhavenonames' in " + u"field 'value'")]) + self.assertIs(ids, False) + + def test_import_to_existing(self): + M2O_o = self.registry('export.many2many.other') + id1 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 3, 'str': 'record0'}) + id2 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 44, 'str': 'record1'}) + id3 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 84, 'str': 'record2'}) + id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) + + xid = 'myxid' + ids, messages = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) + ids, messages = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) + + b = self.browse() + self.assertEqual(len(b), 1) + # TODO: replacement of existing m2m values is correct? + self.assertEqual(values(b[0].value), [84, 9]) + +class test_o2m(ImporterCase): + model_name = 'export.one2many' + + def test_name_get(self): + # FIXME: bloody hell why can't this just name_create the record? + self.assertRaises( + IndexError, + self.import_, + ['const', 'value'], + [['5', u'Java is a DSL for taking large XML files' + u' and converting them to stack traces']]) + + def test_single(self): + ids, messages = self.import_(['const', 'value/value'], [ + ['5', '63'] + ]) + self.assertEqual(len(ids), 1) + self.assertFalse(messages) + + (b,) = self.browse() + self.assertEqual(b.const, 5) + self.assertEqual(values(b.value), [63]) + + def test_multicore(self): + ids, messages = self.import_(['const', 'value/value'], [ + ['5', '63'], + ['6', '64'], + ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + + b1, b2 = self.browse() + self.assertEqual(b1.const, 5) + self.assertEqual(values(b1.value), [63]) + self.assertEqual(b2.const, 6) + self.assertEqual(values(b2.value), [64]) + + def test_multisub(self): + ids, messages = self.import_(['const', 'value/value'], [ + ['5', '63'], + ['', '64'], + ['', '65'], + ['', '66'], + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + + (b,) = self.browse() + self.assertEqual(values(b.value), [63, 64, 65, 66]) + + def test_multi_subfields(self): + ids, messages = self.import_(['value/str', 'const', 'value/value'], [ + ['this', '5', '63'], + ['is', '', '64'], + ['the', '', '65'], + ['rhythm', '', '66'], + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + + (b,) = self.browse() + self.assertEqual(values(b.value), [63, 64, 65, 66]) + self.assertEqual( + values(b.value, 'str'), + 'this is the rhythm'.split()) + + def test_link_inline(self): + id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Bf', 'value': 109 + }) + id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Me', 'value': 262 + }) + + try: + self.import_(['const', 'value/.id'], [ + ['42', '%d,%d' % (id1, id2)] + ]) + self.fail("Should have raised a valueerror") + except ValueError, e: + # should be Exception(Database ID doesn't exist: export.one2many.child : $id1,$id2) + self.assertIs(type(e), ValueError) + self.assertEqual( + e.args[0], + "invalid literal for int() with base 10: '%d,%d'" % (id1, id2)) + + def test_link(self): + id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Bf', 'value': 109 + }) + id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Me', 'value': 262 + }) + + ids, messages = self.import_(['const', 'value/.id'], [ + ['42', str(id1)], + ['', str(id2)], + ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + + # No record values alongside id => o2m resolution skipped altogether, + # creates 2 records => remove/don't import columns sideshow columns, + # get completely different semantics + b, b1 = self.browse() + self.assertEqual(b.const, 42) + self.assertEqual(values(b.value), []) + self.assertEqual(b1.const, 4) + self.assertEqual(values(b1.value), []) + + def test_link_2(self): + O2M_c = self.registry('export.one2many.child') + id1 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Bf', 'value': 109 + }) + id2 = O2M_c.create(self.cr, openerp.SUPERUSER_ID, { + 'str': 'Me', 'value': 262 + }) + + ids, messages = self.import_(['const', 'value/.id', 'value/value'], [ + ['42', str(id1), '1'], + ['', str(id2), '2'], + ]) + self.assertEqual(len(ids), 2) + self.assertFalse(messages) + + (b,) = self.browse() + # if an id (db or xid) is provided, expectations that objects are + # *already* linked and emits UPDATE (1, id, {}). + # Noid => CREATE (0, ?, {}) + # TODO: xid ignored aside from getting corresponding db id? + self.assertEqual(b.const, 42) + self.assertEqual(values(b.value), []) + + # FIXME: updates somebody else's records? + self.assertEqual( + O2M_c.read(self.cr, openerp.SUPERUSER_ID, id1), + {'id': id1, 'str': 'Bf', 'value': 1, 'parent_id': False}) + self.assertEqual( + O2M_c.read(self.cr, openerp.SUPERUSER_ID, id2), + {'id': id2, 'str': 'Me', 'value': 2, 'parent_id': False}) + +class test_o2m_multiple(ImporterCase): + model_name = 'export.one2many.multiple' + + def test_multi_mixed(self): + ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + ['5', '11', '21'], + ['', '12', '22'], + ['', '13', '23'], + ['', '14', ''], + ]) + self.assertEqual(len(ids), 4) + self.assertFalse(messages) + # Oh yeah, that's the stuff + (b, b1, b2) = self.browse() + self.assertEqual(values(b.child1), [11]) + self.assertEqual(values(b.child2), [21]) + + self.assertEqual(values(b1.child1), [12]) + self.assertEqual(values(b1.child2), [22]) + + self.assertEqual(values(b2.child1), [13, 14]) + self.assertEqual(values(b2.child2), [23]) + + def test_multi(self): + ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + ['5', '11', '21'], + ['', '12', ''], + ['', '13', ''], + ['', '14', ''], + ['', '', '22'], + ['', '', '23'], + ]) + self.assertEqual(len(ids), 6) + self.assertFalse(messages) + # What the actual fuck? + (b, b1) = self.browse() + self.assertEqual(values(b.child1), [11, 12, 13, 14]) + self.assertEqual(values(b.child2), [21]) + self.assertEqual(values(b1.child2), [22, 23]) + + def test_multi_fullsplit(self): + ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + ['5', '11', ''], + ['', '12', ''], + ['', '13', ''], + ['', '14', ''], + ['', '', '21'], + ['', '', '22'], + ['', '', '23'], + ]) + self.assertEqual(len(ids), 7) + self.assertFalse(messages) + # oh wow + (b, b1) = self.browse() + self.assertEqual(b.const, 5) + self.assertEqual(values(b.child1), [11, 12, 13, 14]) + self.assertEqual(b1.const, 36) + self.assertEqual(values(b1.child2), [21, 22, 23]) + +# function, related, reference: written to db as-is... +# => function uses @type for value coercion/conversion diff --git a/openerp/tests/test_misc.py b/openerp/tests/test_misc.py index 7661f253b17..54ae69899a6 100644 --- a/openerp/tests/test_misc.py +++ b/openerp/tests/test_misc.py @@ -2,12 +2,12 @@ # > PYTHONPATH=. python2 openerp/tests/test_misc.py import unittest2 +from ..tools import misc -class test_misc(unittest2.TestCase): +class append_content_to_html(unittest2.TestCase): """ Test some of our generic utility functions """ def test_append_to_html(self): - from openerp.tools import append_content_to_html test_samples = [ ('some content', '--\nYours truly', True, 'some content\n
--\nYours truly
\n'), @@ -15,7 +15,37 @@ class test_misc(unittest2.TestCase): 'some content\n\n\n

--

\n

Yours truly

\n\n\n'), ] for html, content, flag, expected in test_samples: - self.assertEqual(append_content_to_html(html,content,flag), expected, 'append_content_to_html is broken') + self.assertEqual(misc.append_content_to_html(html,content,flag), expected, 'append_content_to_html is broken') + +class test_countingstream(unittest2.TestCase): + def test_empty_stream(self): + s = misc.CountingStream(iter([])) + self.assertEqual(s.index, -1) + self.assertIsNone(next(s, None)) + self.assertEqual(s.index, 0) + + def test_single(self): + s = misc.CountingStream(xrange(1)) + self.assertEqual(s.index, -1) + self.assertEqual(next(s, None), 0) + self.assertIsNone(next(s, None)) + self.assertEqual(s.index, 1) + + def test_full(self): + s = misc.CountingStream(xrange(42)) + for _ in s: + pass + self.assertEqual(s.index, 42) + + def test_repeated(self): + """ Once the CountingStream has stopped iterating, the index should not + increase anymore (the internal state should not be allowed to change) + """ + s = misc.CountingStream(iter([])) + self.assertIsNone(next(s, None)) + self.assertEqual(s.index, 0) + self.assertIsNone(next(s, None)) + self.assertEqual(s.index, 0) if __name__ == '__main__': - unittest2.main() \ No newline at end of file + unittest2.main() diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 9a7b3499425..1ee3caf98d9 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1220,4 +1220,38 @@ class mute_logger(object): with self: return func(*args, **kwargs) return deco + +_ph = object() +class CountingStream(object): + """ Stream wrapper counting the number of element it has yielded. Similar + role to ``enumerate``, but for use when the iteration process of the stream + isn't fully under caller control (the stream can be iterated from multiple + points including within a library) + + ``start`` allows overriding the starting index (the index before the first + item is returned). + + On each iteration (call to :meth:`~.next`), increases its :attr:`~.index` + by one. + + .. attribute:: index + + ``int``, index of the last yielded element in the stream. If the stream + has ended, will give an index 1-past the stream + """ + def __init__(self, stream, start=-1): + self.stream = iter(stream) + self.index = start + self.stopped = False + def __iter__(self): + return self + def next(self): + if self.stopped: raise StopIteration() + self.index += 1 + val = next(self.stream, _ph) + if val is _ph: + self.stopped = True + raise StopIteration() + return val + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/setup.py b/setup.py index a26b74e255d..4451bc1b710 100755 --- a/setup.py +++ b/setup.py @@ -116,6 +116,7 @@ setuptools.setup( extras_require = { 'SSL' : ['pyopenssl'], }, + tests_require = ['unittest2'], **py2exe_options() ) From c9e0cfd64aa064df9a2544597bf77579ac4e08da Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 20 Sep 2012 12:25:45 +0200 Subject: [PATCH 007/443] [ADD] force linking to existing o2m being updated bzr revid: xmo@openerp.com-20120920102545-30tkodb4s1dng5hp --- openerp/addons/base/ir/ir_fields.py | 34 ++++++++++++++-- .../addons/test_impex/tests/test_load.py | 39 +++++++------------ 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index f73067c9f6c..6b3898f3fdd 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -5,6 +5,22 @@ import warnings from openerp.osv import orm, fields from openerp.tools.translate import _ +REFERENCING_FIELDS = set([None, 'id', '.id']) +def only_ref_fields(record): + return dict((k, v) for k, v in record.iteritems() + if k in REFERENCING_FIELDS) +def exclude_ref_fields(record): + return dict((k, v) for k, v in record.iteritems() + if k not in REFERENCING_FIELDS) + +CREATE = lambda values: (0, False, values) +UPDATE = lambda id, values: (1, id, values) +DELETE = lambda id: (2, id, False) +FORGET = lambda id: (3, id, False) +LINK_TO = lambda id: (4, id, False) +DELETE_ALL = lambda: (5, False, False) +REPLACE_WITH = lambda ids: (6, False, ids) + class ConversionNotFound(ValueError): pass class ir_fields_converter(orm.Model): @@ -145,9 +161,8 @@ class ir_fields_converter(orm.Model): :rtype: str """ # Can import by name_get, external id or database id - allowed_fields = set([None, 'id', '.id']) fieldset = set(record.iterkeys()) - if fieldset - allowed_fields: + if fieldset - REFERENCING_FIELDS: raise ValueError( _(u"Can not create Many-To-One records indirectly, import the field separately")) if len(fieldset) > 1: @@ -190,4 +205,17 @@ class ir_fields_converter(orm.Model): return [(6, 0, ids)] def _str_to_one2many(self, cr, uid, model, column, value, context=None): - return value + commands = [] + for subfield, record in zip((self._referencing_subfield( + only_ref_fields(record)) + for record in value), + value): + id, subfield_type = self.db_id_for( + cr, uid, model, column, subfield, record[subfield], context=context) + writable = exclude_ref_fields(record) + if id: + commands.append(LINK_TO(id)) + commands.append(UPDATE(id, writable)) + else: + commands.append(CREATE(writable)) + return commands diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 80e5cfe9c8f..44b6c1a56df 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -869,10 +869,13 @@ class test_o2m(ImporterCase): "invalid literal for int() with base 10: '%d,%d'" % (id1, id2)) def test_link(self): - id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + """ O2M relating to an existing record (update) force a LINK_TO as well + """ + O2M = self.registry('export.one2many.child') + id1 = O2M.create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Bf', 'value': 109 }) - id2 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { + id2 = O2M.create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Me', 'value': 262 }) @@ -880,17 +883,14 @@ class test_o2m(ImporterCase): ['42', str(id1)], ['', str(id2)], ]) - self.assertEqual(len(ids), 2) self.assertFalse(messages) + self.assertEqual(len(ids), 1) - # No record values alongside id => o2m resolution skipped altogether, - # creates 2 records => remove/don't import columns sideshow columns, - # get completely different semantics - b, b1 = self.browse() + [b] = self.browse() self.assertEqual(b.const, 42) - self.assertEqual(values(b.value), []) - self.assertEqual(b1.const, 4) - self.assertEqual(values(b1.value), []) + # automatically forces link between core record and o2ms + self.assertEqual(values(b.value), [109, 262]) + self.assertEqual(values(b.value, field='parent_id'), [b, b]) def test_link_2(self): O2M_c = self.registry('export.one2many.child') @@ -905,24 +905,13 @@ class test_o2m(ImporterCase): ['42', str(id1), '1'], ['', str(id2), '2'], ]) - self.assertEqual(len(ids), 2) self.assertFalse(messages) + self.assertEqual(len(ids), 1) - (b,) = self.browse() - # if an id (db or xid) is provided, expectations that objects are - # *already* linked and emits UPDATE (1, id, {}). - # Noid => CREATE (0, ?, {}) - # TODO: xid ignored aside from getting corresponding db id? + [b] = self.browse() self.assertEqual(b.const, 42) - self.assertEqual(values(b.value), []) - - # FIXME: updates somebody else's records? - self.assertEqual( - O2M_c.read(self.cr, openerp.SUPERUSER_ID, id1), - {'id': id1, 'str': 'Bf', 'value': 1, 'parent_id': False}) - self.assertEqual( - O2M_c.read(self.cr, openerp.SUPERUSER_ID, id2), - {'id': id2, 'str': 'Me', 'value': 2, 'parent_id': False}) + self.assertEqual(values(b.value), [1, 2]) + self.assertEqual(values(b.value, field='parent_id'), [b, b]) class test_o2m_multiple(ImporterCase): model_name = 'export.one2many.multiple' From 8e841cd8f7603fc59be78b7d73283f65e4247840 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 20 Sep 2012 12:56:12 +0200 Subject: [PATCH 008/443] [FIX] non-linking o2m tests, corresponding code bzr revid: xmo@openerp.com-20120920105612-03ifizt2iv08tdhz --- openerp/addons/base/ir/ir_fields.py | 22 +++++--- openerp/tests/addons/test_impex/models.py | 6 +++ .../addons/test_impex/tests/test_load.py | 53 ++++++++----------- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 6b3898f3fdd..44351229c26 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -206,16 +206,26 @@ class ir_fields_converter(orm.Model): return [(6, 0, ids)] def _str_to_one2many(self, cr, uid, model, column, value, context=None): commands = [] - for subfield, record in zip((self._referencing_subfield( - only_ref_fields(record)) - for record in value), - value): - id, subfield_type = self.db_id_for( - cr, uid, model, column, subfield, record[subfield], context=context) + + for record in value: + id = None + refs = only_ref_fields(record) + # there are ref fields in the record + if refs: + subfield = self._referencing_subfield(refs) + reference = record[subfield] + id, subfield_type = self.db_id_for( + cr, uid, model, column, subfield, reference, context=context) + if id is None: + raise ValueError( + _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") + % {'field_type': subfield_type, 'value': reference}) + writable = exclude_ref_fields(record) if id: commands.append(LINK_TO(id)) commands.append(UPDATE(id, writable)) else: commands.append(CREATE(writable)) + return commands diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py index 455ea6b22f4..37894ccb78c 100644 --- a/openerp/tests/addons/test_impex/models.py +++ b/openerp/tests/addons/test_impex/models.py @@ -67,6 +67,12 @@ class One2ManyChild(orm.Model): def name_get(self, cr, uid, ids, context=None): return [(record.id, "%s:%s" % (self._name, record.value)) for record in self.browse(cr, uid, ids, context=context)] + def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): + return (self.name_get(cr, user, + self.search(cr, user, [['value', operator, int(name.split(':')[1])]]) + , context=context) + if isinstance(name, basestring) and name.split(':')[0] == self._name + else []) class One2ManyMultiple(orm.Model): _name = 'export.one2many.multiple' diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 44b6c1a56df..25159dcd2ba 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -786,20 +786,21 @@ class test_o2m(ImporterCase): model_name = 'export.one2many' def test_name_get(self): - # FIXME: bloody hell why can't this just name_create the record? - self.assertRaises( - IndexError, - self.import_, + s = u'Java is a DSL for taking large XML files and converting them ' \ + u'to stack traces' + ids, messages = self.import_( ['const', 'value'], - [['5', u'Java is a DSL for taking large XML files' - u' and converting them to stack traces']]) + [['5', s]]) + self.assertEqual(messages, [message( + u"No matching record found for name '%s' in field 'value'" % s)]) + self.assertIs(ids, False) def test_single(self): ids, messages = self.import_(['const', 'value/value'], [ ['5', '63'] ]) - self.assertEqual(len(ids), 1) self.assertFalse(messages) + self.assertEqual(len(ids), 1) (b,) = self.browse() self.assertEqual(b.const, 5) @@ -810,8 +811,8 @@ class test_o2m(ImporterCase): ['5', '63'], ['6', '64'], ]) - self.assertEqual(len(ids), 2) self.assertFalse(messages) + self.assertEqual(len(ids), 2) b1, b2 = self.browse() self.assertEqual(b1.const, 5) @@ -826,8 +827,8 @@ class test_o2m(ImporterCase): ['', '65'], ['', '66'], ]) - self.assertEqual(len(ids), 4) self.assertFalse(messages) + self.assertEqual(len(ids), 1) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) @@ -839,8 +840,8 @@ class test_o2m(ImporterCase): ['the', '', '65'], ['rhythm', '', '66'], ]) - self.assertEqual(len(ids), 4) self.assertFalse(messages) + self.assertEqual(len(ids), 1) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) @@ -923,18 +924,12 @@ class test_o2m_multiple(ImporterCase): ['', '13', '23'], ['', '14', ''], ]) - self.assertEqual(len(ids), 4) self.assertFalse(messages) + self.assertEqual(len(ids), 1) # Oh yeah, that's the stuff - (b, b1, b2) = self.browse() - self.assertEqual(values(b.child1), [11]) - self.assertEqual(values(b.child2), [21]) - - self.assertEqual(values(b1.child1), [12]) - self.assertEqual(values(b1.child2), [22]) - - self.assertEqual(values(b2.child1), [13, 14]) - self.assertEqual(values(b2.child2), [23]) + [b] = self.browse() + self.assertEqual(values(b.child1), [11, 12, 13, 14]) + self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi(self): ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ @@ -945,13 +940,12 @@ class test_o2m_multiple(ImporterCase): ['', '', '22'], ['', '', '23'], ]) - self.assertEqual(len(ids), 6) self.assertFalse(messages) - # What the actual fuck? - (b, b1) = self.browse() + self.assertEqual(len(ids), 1) + + [b] = self.browse() self.assertEqual(values(b.child1), [11, 12, 13, 14]) - self.assertEqual(values(b.child2), [21]) - self.assertEqual(values(b1.child2), [22, 23]) + self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi_fullsplit(self): ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ @@ -963,14 +957,13 @@ class test_o2m_multiple(ImporterCase): ['', '', '22'], ['', '', '23'], ]) - self.assertEqual(len(ids), 7) self.assertFalse(messages) - # oh wow - (b, b1) = self.browse() + self.assertEqual(len(ids), 1) + + [b] = self.browse() self.assertEqual(b.const, 5) self.assertEqual(values(b.child1), [11, 12, 13, 14]) - self.assertEqual(b1.const, 36) - self.assertEqual(values(b1.child2), [21, 22, 23]) + self.assertEqual(values(b.child2), [21, 22, 23]) # function, related, reference: written to db as-is... # => function uses @type for value coercion/conversion From fdba99aaeb3998b6f1e76e69fc77bf45b0c73ee2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 20 Sep 2012 13:09:14 +0200 Subject: [PATCH 009/443] [ADD] inline o2m LINK_TO (m2m-style) bzr revid: xmo@openerp.com-20120920110914-hy2rtivhn9cs5wuc --- openerp/addons/base/ir/ir_fields.py | 13 ++++++++-- .../addons/test_impex/tests/test_load.py | 24 ++++++++++--------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 44351229c26..ae0d33298ca 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -204,10 +204,19 @@ class ir_fields_converter(orm.Model): ids.append(id) return [(6, 0, ids)] - def _str_to_one2many(self, cr, uid, model, column, value, context=None): + def _str_to_one2many(self, cr, uid, model, column, records, context=None): commands = [] - for record in value: + if len(records) == 1 and exclude_ref_fields(records[0]) == {}: + # only one row with only ref field, field=ref1,ref2,ref3 as in + # m2o/m2m + record = records[0] + subfield = self._referencing_subfield(record) + # transform [{subfield:ref1,ref2,ref3}] into + # [{subfield:ref1},{subfield:ref2},{subfield:ref3}] + records = ({subfield:item} for item in record[subfield].split(',')) + + for record in records: id = None refs = only_ref_fields(record) # there are ref fields in the record diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 25159dcd2ba..9ce0d5628ed 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -850,6 +850,8 @@ class test_o2m(ImporterCase): 'this is the rhythm'.split()) def test_link_inline(self): + """ m2m-style specification for o2ms + """ id1 = self.registry('export.one2many.child').create(self.cr, openerp.SUPERUSER_ID, { 'str': 'Bf', 'value': 109 }) @@ -857,17 +859,17 @@ class test_o2m(ImporterCase): 'str': 'Me', 'value': 262 }) - try: - self.import_(['const', 'value/.id'], [ - ['42', '%d,%d' % (id1, id2)] - ]) - self.fail("Should have raised a valueerror") - except ValueError, e: - # should be Exception(Database ID doesn't exist: export.one2many.child : $id1,$id2) - self.assertIs(type(e), ValueError) - self.assertEqual( - e.args[0], - "invalid literal for int() with base 10: '%d,%d'" % (id1, id2)) + ids, messages = self.import_(['const', 'value/.id'], [ + ['42', '%d,%d' % (id1, id2)] + ]) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) + + [b] = self.browse() + self.assertEqual(b.const, 42) + # automatically forces link between core record and o2ms + self.assertEqual(values(b.value), [109, 262]) + self.assertEqual(values(b.value, field='parent_id'), [b, b]) def test_link(self): """ O2M relating to an existing record (update) force a LINK_TO as well From a9bc82c46dc007fa3bf0d479d6afab40530bb648 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 20 Sep 2012 17:04:43 +0200 Subject: [PATCH 010/443] [IMP] translation tests, translated acceptable values for boolean fields bzr revid: xmo@openerp.com-20120920150443-l9lna4bnkta7n2o8 --- openerp/addons/base/ir/ir_fields.py | 27 +++- .../addons/test_impex/tests/test_load.py | 144 ++++++++---------- 2 files changed, 90 insertions(+), 81 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index ae0d33298ca..10b80511c73 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import functools import operator +import itertools import warnings from openerp.osv import orm, fields from openerp.tools.translate import _ @@ -55,7 +56,29 @@ class ir_fields_converter(orm.Model): converter, cr, uid, model, column, context=context) def _str_to_boolean(self, cr, uid, model, column, value, context=None): - return value.lower() not in ('', '0', 'false', 'off') + # all translatables used for booleans + true, yes, false, no = _(u"true"), _(u"yes"), _(u"false"), _(u"no") + # potentially broken casefolding? What about locales? + trues = set(word.lower() for word in itertools.chain( + [u'1', u"true", u"yes"], # don't use potentially translated values + self._get_translations(cr, uid, ['code'], u"true", context=context), + self._get_translations(cr, uid, ['code'], u"yes", context=context), + )) + if value.lower() in trues: return True + + # potentially broken casefolding? What about locales? + falses = set(word.lower() for word in itertools.chain( + [u'', u"0", u"false", u"no"], + self._get_translations(cr, uid, ['code'], u"false", context=context), + self._get_translations(cr, uid, ['code'], u"no", context=context), + )) + if value.lower() in falses: return False + + warnings.warn( + _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'") + % (value, yes), + orm.ImportWarning) + return True def _str_to_integer(self, cr, uid, model, column, value, context=None): if not value: return False @@ -86,7 +109,7 @@ class ir_fields_converter(orm.Model): selection = selection(model, cr, uid) for item, label in selection: labels = self._get_translations( - cr, uid, ('selection', 'model'), label, context=context) + cr, uid, ('selection', 'model', 'code'), label, context=context) labels.append(label) if value == unicode(item) or value in labels: return item diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 9ce0d5628ed..28957c29555 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -79,6 +79,27 @@ class ImporterCase(common.TransactionCase): }) return '__test__.' + name + def add_translations(self, name, type, code, *tnx): + Lang = self.registry('res.lang') + if not Lang.search(self.cr, openerp.SUPERUSER_ID, [('code', '=', code)]): + Lang.create(self.cr, openerp.SUPERUSER_ID, { + 'name': code, + 'code': code, + 'translatable': True, + 'date_format': '%d.%m.%Y', + 'decimal_point': ',', + }) + Translations = self.registry('ir.translation') + for source, value in tnx: + Translations.create(self.cr, openerp.SUPERUSER_ID, { + 'name': name, + 'lang': code, + 'type': type, + 'src': source, + 'value': value, + 'state': 'translated', + }) + class test_ids_stuff(ImporterCase): model_name = 'export.integer' @@ -139,41 +160,39 @@ class test_boolean_field(ImporterCase): ], values(records)) def test_falses(self): - ids, messages = self.import_( - ['value'], - [[u'0'], [u'off'], - [u'false'], [u'FALSE'], - [u'OFF'], [u''], - ]) - self.assertEqual(len(ids), 6) + for lang, source, value in [('fr_FR', 'no', u'non'), + ('de_DE', 'no', u'nein'), + ('ru_RU', 'no', u'нет'), + ('nl_BE', 'false', u'vals'), + ('lt_LT', 'false', u'klaidingas')]: + self.add_translations('test_import.py', 'code', lang, (source, value)) + falses = [[u'0'], [u'no'], [u'false'], [u'FALSE'], [u''], + [u'non'], # no, fr + [u'nein'], # no, de + [u'нет'], # no, ru + [u'vals'], # false, nl + [u'klaidingas'], # false, lt, + ] + + ids, messages = self.import_(['value'], falses) self.assertFalse(messages) - self.assertEqual([ - False, - False, - False, - False, - False, - False, - ], - values(self.read())) + self.assertEqual(len(ids), len(falses)) + self.assertEqual([False] * len(falses), values(self.read())) def test_trues(self): - ids, messages = self.import_( - ['value'], - [['no'], - ['None'], - ['nil'], - ['()'], - ['f'], - ['#f'], - # Problem: OpenOffice (and probably excel) output localized booleans - ['VRAI'], + trues = [['None'], ['nil'], ['()'], ['f'], ['#f'], + # Problem: OpenOffice (and probably excel) output localized booleans + ['VRAI'], ['ok'], ['true'], ['yes'], ['1'], ] + ids, messages = self.import_(['value'], trues) + self.assertEqual(len(ids), 10) + self.assertEqual(messages, [ + message(u"Unknown value '%s' for boolean field 'value', assuming 'yes'" % v[0], + type='warning', from_=i, to_=i, record=i) + for i, v in enumerate(trues) + if v[0] != 'true' if v[0] != 'yes' if v[0] != '1' ]) - self.assertEqual(len(ids), 7) - # FIXME: should warn for values which are not "true", "yes" or "1" - self.assertFalse(messages) self.assertEqual( - [True] * 7, + [True] * 10, values(self.read())) class test_integer_field(ImporterCase): @@ -399,9 +418,9 @@ class test_text(ImporterCase): class test_selection(ImporterCase): model_name = 'export.selection' translations_fr = [ - ("Qux", "toto"), - ("Bar", "titi"), ("Foo", "tete"), + ("Bar", "titi"), + ("Qux", "toto"), ] def test_imported(self): @@ -416,23 +435,8 @@ class test_selection(ImporterCase): self.assertEqual([3, 2, 1, 2], values(self.read())) def test_imported_translated(self): - self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { - 'name': u'Français', - 'code': 'fr_FR', - 'translatable': True, - 'date_format': '%d.%m.%Y', - 'decimal_point': ',', - 'thousand_sep': ' ', - }) - Translations = self.registry('ir.translation') - for source, value in self.translations_fr: - Translations.create(self.cr, openerp.SUPERUSER_ID, { - 'name': 'export.selection,value', - 'lang': 'fr_FR', - 'type': 'selection', - 'src': source, - 'value': value - }) + self.add_translations( + 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) ids, messages = self.import_(['value'], [ ['toto'], @@ -474,7 +478,7 @@ class test_selection_function(ImporterCase): translations_fr = [ ("Corge", "toto"), ("Grault", "titi"), - ("Whee", "tete"), + ("Wheee", "tete"), ("Moog", "tutu"), ] @@ -483,7 +487,7 @@ class test_selection_function(ImporterCase): be good news) *and* serializes the selection function to reverse it: import does not actually know that the selection field uses a function """ - # NOTE: conflict between a value and a label => ? + # NOTE: conflict between a value and a label => pick first ids, messages = self.import_(['value'], [ ['3'], ["Grault"], @@ -497,38 +501,20 @@ class test_selection_function(ImporterCase): def test_translated(self): """ Expects output of selection function returns translated labels """ - self.registry('res.lang').create(self.cr, openerp.SUPERUSER_ID, { - 'name': u'Français', - 'code': 'fr_FR', - 'translatable': True, - 'date_format': '%d.%m.%Y', - 'decimal_point': ',', - 'thousand_sep': ' ', - }) - Translations = self.registry('ir.translation') - for source, value in self.translations_fr: - Translations.create(self.cr, openerp.SUPERUSER_ID, { - 'name': 'export.selection,value', - 'lang': 'fr_FR', - 'type': 'selection', - 'src': source, - 'value': value - }) + self.add_translations( + 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) + ids, messages = self.import_(['value'], [ - ['toto'], + ['titi'], ['tete'], ], context={'lang': 'fr_FR'}) - self.assertIs(ids, False) - self.assertEqual(messages, [{ - 'type': 'error', - 'rows': {'from': 1, 'to': 1}, - 'record': 1, - 'field': 'value', - 'message': "Value 'tete' not found in selection field 'value'", - }]) - ids, messages = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) - self.assertEqual(len(ids), 1) self.assertFalse(messages) + self.assertEqual(len(ids), 2) + self.assertEqual(values(self.read()), ['1', '2']) + + ids, messages = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) + self.assertFalse(messages) + self.assertEqual(len(ids), 1) class test_m2o(ImporterCase): model_name = 'export.many2one' From b4421c8fba5a7208a0cf38708cfaa235d9bfa497 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 12:14:04 +0200 Subject: [PATCH 011/443] [ADD] cache dict on the cursor, for request-local repeated reads bzr revid: xmo@openerp.com-20120924101404-cel6oy1akabbo7id --- openerp/sql_db.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 7b6f4469b8d..a6ebcd0300f 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -138,6 +138,16 @@ class Cursor(object): sure you use psycopg2 v2.4.2 or newer if you use PostgreSQL 9.1 and the performance hit is a concern for you. + .. attribute:: cache + + Cache dictionary with a "request" (-ish) lifecycle, only lives as + long as the cursor itself does and proactively cleared when the + cursor is closed. + + This cache should *only* be used to store repeatable reads as it + ignores rollbacks and savepoints, it should not be used to store + *any* data which may be modified during the life of the cursor. + """ IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit @@ -182,6 +192,8 @@ class Cursor(object): self._default_log_exceptions = True + self.cache = {} + def __del__(self): if not self.__closed and not self._cnx.closed: # Oops. 'self' has not been closed explicitly. @@ -279,6 +291,8 @@ class Cursor(object): if not self._obj: return + del self.cache + if self.sql_log: self.__closer = frame_codeinfo(currentframe(),3) self.print_log() From 449a86a51f62c2e3379fc649c55d89080c278fa4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 12:32:57 +0200 Subject: [PATCH 012/443] [IMP] cache boolean and selection translations for the request (cursor) to avoid fetching them over and over again bzr revid: xmo@openerp.com-20120924103257-1jgc3qhddzzi5c17 --- openerp/addons/base/ir/ir_fields.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 10b80511c73..73e748015a3 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -95,11 +95,20 @@ class ir_fields_converter(orm.Model): return value or False def _get_translations(self, cr, uid, types, src, context): + types = tuple(types) + # Cache translations so they don't have to be reloaded from scratch on + # every row of the file + tnx_cache = cr.cache.setdefault(self._name, {}) + if tnx_cache.setdefault(types, {}) and src in tnx_cache[types]: + return tnx_cache[types][src] + Translations = self.pool['ir.translation'] tnx_ids = Translations.search( cr, uid, [('type', 'in', types), ('src', '=', src)], context=context) tnx = Translations.read(cr, uid, tnx_ids, ['value'], context=context) - return map(operator.itemgetter('value'), tnx) + result = tnx_cache[types][src] = map(operator.itemgetter('value'), tnx) + return result + def _str_to_selection(self, cr, uid, model, column, value, context=None): selection = column.selection From 877e21ffdebeba412b7cb4db5fdf352adfac2ff2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 12:52:30 +0200 Subject: [PATCH 013/443] [IMP] return fields_get-style translated field strings (if available) in user-readable warning and error messages from import, rather than logical field names bzr revid: xmo@openerp.com-20120924105230-1b7157xbruy2e5zr --- openerp/osv/orm.py | 19 +++++++++++--- .../addons/test_impex/tests/test_load.py | 26 +++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 57cc5823dd9..e58d6b76a76 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1585,8 +1585,15 @@ class BaseModel(object): :returns: a list of triplets of (id, xid, record) :rtype: list((int|None, str|None, dict)) """ + if context is None: context = {} Converter = self.pool['ir.fields.converter'] columns = dict((k, v.column) for k, v in self._all_columns.iteritems()) + Translation = self.pool['ir.translation'] + field_names = dict( + (f, (Translation._get_source(cr, uid, self._name + ',' + f, 'field', + context.get('lang', False) or 'en_US') + or column.string or f)) + for f, column in columns.iteritems()) converters = dict( (k, Converter.to_field(cr, uid, self, column, context=context)) for k, column in columns.iteritems()) @@ -1619,18 +1626,24 @@ class BaseModel(object): for field, strvalue in record.iteritems(): if field in (None, 'id', '.id'): continue - message_base = dict(extras, record=stream.index, field=field) + # In warnings and error messages, use translated string as + # field name + message_base = dict( + extras, record=stream.index, field=field_names[field]) with warnings.catch_warnings(record=True) as w: try: converted[field] = converters[field](strvalue) + # In warning and error returned, use logical field name + # as field so client can reverse for warning in w: - log(dict(message_base, type='warning', + log(dict(message_base, type='warning', field=field, message=unicode(warning.message) % message_base)) except ValueError, e: log(dict(message_base, type='error', - message=unicode(e) % message_base + field=field, + message=unicode(e) % message_base, )) yield dbid, xid, converted, dict(extras, record=stream.index) diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 28957c29555..cf95f5bb6c4 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -186,7 +186,7 @@ class test_boolean_field(ImporterCase): ids, messages = self.import_(['value'], trues) self.assertEqual(len(ids), 10) self.assertEqual(messages, [ - message(u"Unknown value '%s' for boolean field 'value', assuming 'yes'" % v[0], + message(u"Unknown value '%s' for boolean field 'unknown', assuming 'yes'" % v[0], type='warning', from_=i, to_=i, record=i) for i, v in enumerate(trues) if v[0] != 'true' if v[0] != 'yes' if v[0] != '1' @@ -460,7 +460,7 @@ class test_selection(ImporterCase): 'rows': {'from': 0, 'to': 0}, 'record': 0, 'field': 'value', - 'message': "Value 'Baz' not found in selection field 'value'", + 'message': "Value 'Baz' not found in selection field 'unknown'", }]) ids, messages = self.import_(['value'], [[42]]) @@ -470,7 +470,7 @@ class test_selection(ImporterCase): 'rows': {'from': 0, 'to': 0}, 'record': 0, 'field': 'value', - 'message': "Value '42' not found in selection field 'value'", + 'message': "Value '42' not found in selection field 'unknown'", }]) class test_selection_function(ImporterCase): @@ -583,7 +583,7 @@ class test_m2o(ImporterCase): ids, messages = self.import_(['value'], [[name2]]) self.assertEqual( messages, - [message(u"Found multiple matches for field 'value' (2 matches)", + [message(u"Found multiple matches for field 'unknown' (2 matches)", type='warning')]) self.assertEqual(len(ids), 1) self.assertEqual([ @@ -607,7 +607,7 @@ class test_m2o(ImporterCase): [integer_id1], ]) self.assertEqual(messages, [ - message(u"No matching record found for name '%s' in field 'value'" % id, + message(u"No matching record found for name '%s' in field 'unknown'" % id, from_=index, to_=index, record=index) for index, id in enumerate([integer_id1, integer_id2, integer_id1])]) self.assertIs(ids, False) @@ -626,19 +626,19 @@ class test_m2o(ImporterCase): ids, messages = self.import_(['value'], [['nameisnoexist:3']]) self.assertEqual(messages, [message( u"No matching record found for name 'nameisnoexist:3' " - u"in field 'value'")]) + u"in field 'unknown'")]) self.assertIs(ids, False) ids, messages = self.import_(['value/id'], [['noxidhere']]) self.assertEqual(messages, [message( u"No matching record found for external id 'noxidhere' " - u"in field 'value'")]) + u"in field 'unknown'")]) self.assertIs(ids, False) ids, messages = self.import_(['value/.id'], [['66']]) self.assertEqual(messages, [message( u"No matching record found for database id '66' " - u"in field 'value'")]) + u"in field 'unknown'")]) self.assertIs(ids, False) def test_fail_multiple(self): @@ -646,7 +646,7 @@ class test_m2o(ImporterCase): ['value', 'value/id'], [['somename', 'somexid']]) self.assertEqual(messages, [message( - u"Ambiguous specification for field 'value', only provide one of " + u"Ambiguous specification for field 'unknown', only provide one of " u"name, external id or database id")]) self.assertIs(ids, False) @@ -690,7 +690,7 @@ class test_m2m(ImporterCase): ids, messages = self.import_(['value/.id'], [['42']]) self.assertEqual(messages, [message( u"No matching record found for database id '42' in field " - u"'value'")]) + u"'unknown'")]) self.assertIs(ids, False) def test_xids(self): @@ -716,7 +716,7 @@ class test_m2m(ImporterCase): ids, messages = self.import_(['value/id'], [['noxidforthat']]) self.assertEqual(messages, [message( u"No matching record found for external id 'noxidforthat' " - u"in field 'value'")]) + u"in field 'unknown'")]) self.assertIs(ids, False) def test_names(self): @@ -745,7 +745,7 @@ class test_m2m(ImporterCase): ids, messages = self.import_(['value'], [['wherethem2mhavenonames']]) self.assertEqual(messages, [message( u"No matching record found for name 'wherethem2mhavenonames' in " - u"field 'value'")]) + u"field 'unknown'")]) self.assertIs(ids, False) def test_import_to_existing(self): @@ -778,7 +778,7 @@ class test_o2m(ImporterCase): ['const', 'value'], [['5', s]]) self.assertEqual(messages, [message( - u"No matching record found for name '%s' in field 'value'" % s)]) + u"No matching record found for name '%s' in field 'unknown'" % s)]) self.assertIs(ids, False) def test_single(self): From a9c2cfcdb91c79cc60de909a4e23bd9e9e5f9787 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 16:57:50 +0200 Subject: [PATCH 014/443] [IMP] make help attribute available on all actions bzr revid: xmo@openerp.com-20120924145750-n0gj4bww1d83h3fy --- openerp/addons/base/ir/ir_actions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 383f78181e9..b14f4cd7e84 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -19,14 +19,11 @@ # ############################################################################## -import ast -import copy import logging import os import re import time import tools -from xml import dom import netsvc from osv import fields,osv @@ -47,6 +44,9 @@ class actions(osv.osv): 'name': fields.char('Name', size=64, required=True), 'type': fields.char('Action Type', required=True, size=32,readonly=True), 'usage': fields.char('Action Usage', size=32), + 'help': fields.text('Action description', + help='Optional help text for the users with a description of the target view, such as its usage and purpose.', + translate=True), } _defaults = { 'usage': lambda *a: False, @@ -107,6 +107,7 @@ class report_xml(osv.osv): r['report_xsl'] and opj('addons',r['report_xsl'])) _name = 'ir.actions.report.xml' + _inherit = 'ir.actions.actions' _table = 'ir_act_report_xml' _sequence = 'ir_actions_id_seq' _order = 'name' @@ -155,6 +156,7 @@ report_xml() class act_window(osv.osv): _name = 'ir.actions.act_window' _table = 'ir_act_window' + _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'name' @@ -245,9 +247,6 @@ class act_window(osv.osv): 'filter': fields.boolean('Filter'), 'auto_search':fields.boolean('Auto Search'), 'search_view' : fields.function(_search_view, type='text', string='Search View'), - 'help': fields.text('Action description', - help='Optional help text for the users with a description of the target view, such as its usage and purpose.', - translate=True), 'multi': fields.boolean('Action on Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view"), } @@ -331,6 +330,7 @@ act_wizard() class act_url(osv.osv): _name = 'ir.actions.url' _table = 'ir_act_url' + _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'name' _columns = { @@ -432,6 +432,7 @@ class actions_server(osv.osv): _name = 'ir.actions.server' _table = 'ir_act_server' + _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'sequence,name' _columns = { From f143902d1afa345c8fad935a80a6b404a9a3cfb7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 17:04:17 +0200 Subject: [PATCH 015/443] [IMP] allow converters to add data to import messages, formalize message keys bzr revid: xmo@openerp.com-20120924150417-c2y7g7vdsfz66363 --- openerp/addons/base/ir/ir_fields.py | 33 +++++++-- openerp/osv/orm.py | 70 +++++++++++++++---- .../addons/test_impex/tests/test_load.py | 32 +++------ 3 files changed, 94 insertions(+), 41 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 73e748015a3..2256542533b 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -38,6 +38,26 @@ class ir_fields_converter(orm.Model): By default, tries to get a method on itself with a name matching the pattern ``_$fromtype_$column._type`` and returns it. + Converter callables can either return a value to their caller or raise + ``ValueError``, which will be interpreted as a validation & conversion + failure. + + ValueError can have either one or two parameters. The first parameter + is mandatory, **must** be a unicode string and will be used as the + user-visible message for the error (it should be translatable and + translated). It can contain a ``field`` named format placeholder so the + caller can inject the field's translated, user-facing name (@string). + + The second parameter is optional and, if provided, must be a mapping. + This mapping will be merged into the error dictionary returned to the + client. + + If a converter can perform its function but has to make assumptions + about the data, it can send a warning to the user through signalling + an :class:`~openerp.osv.orm.ImportWarning` (via ``warnings.warn``). The + handling of a warning at the upper levels is the same as + ``ValueError`` above. + :param cr: openerp cursor :param uid: ID of user calling the converter :param column: column object to generate a value for @@ -74,10 +94,9 @@ class ir_fields_converter(orm.Model): )) if value.lower() in falses: return False - warnings.warn( + warnings.warn(orm.ImportWarning( _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'") - % (value, yes), - orm.ImportWarning) + % (value, yes))) return True def _str_to_integer(self, cr, uid, model, column, value, context=None): @@ -124,7 +143,9 @@ class ir_fields_converter(orm.Model): return item raise ValueError( _(u"Value '%s' not found in selection field '%%(field)s'") % ( - value)) + value), { + 'moreinfo': map(operator.itemgetter(1), selection) + }) def db_id_for(self, cr, uid, model, column, subfield, value, context=None): @@ -174,9 +195,9 @@ class ir_fields_converter(orm.Model): cr, uid, name=value, operator='=', context=context) if ids: if len(ids) > 1: - warnings.warn( + warnings.warn(orm.ImportWarning( _(u"Found multiple matches for field '%%(field)s' (%d matches)") - % (len(ids)), orm.ImportWarning) + % (len(ids)))) id, _name = ids[0] else: raise Exception(u"Unknown sub-field '%s'" % subfield) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index e58d6b76a76..98e24075311 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1444,6 +1444,40 @@ class BaseModel(object): def load(self, cr, uid, fields, data, context=None): """ + Attempts to load the data matrix, and returns a list of ids (or + ``False`` if there was an error and no id could be generated) and a + list of messages. + + Each message is a dictionary with the following keys: + + ``type`` + the type of message, either ``warning`` or ``error``. Any ``error`` + message indicates the import failed and was rolled back. + ``message`` + the message's actual text, which should be translated and can be + shown to the user directly + ``rows`` + a dict with 2 keys ``from`` and ``to``, indicates the range of rows + in ``data`` which generated the message + ``record`` + a single integer, for warnings the index of the record which + generated the message (can be obtained from a non-false ``ids`` + result) + ``field`` + the name of the (logical) OpenERP field for which the error or + warning was generated + ``moreinfo`` (optional) + A string, a list or a dict, leading to more information about the + warning. + + * If ``moreinfo`` is a string, it is a supplementary warnings + message which should be hidden by default + * If ``moreinfo`` is a list, it provides a number of possible or + alternative values for the string + * If ``moreinfo`` is a dict, it is an OpenERP action descriptor + which can be executed to get more information about the issues + with the field. If present, the ``help`` key serves as a label + for the action (e.g. the text of the link). :param cr: cursor for the request :param int uid: ID of the user attempting the data import @@ -1598,6 +1632,14 @@ class BaseModel(object): (k, Converter.to_field(cr, uid, self, column, context=context)) for k, column in columns.iteritems()) + def _log(base, field, exception): + type = 'warning' if isinstance(exception, Warning) else 'error' + record = dict(base, field=field, type=type, + message=unicode(exception.args[0]) % base) + if len(exception.args) > 1 and exception.args[1]: + record.update(exception.args[1]) + log(record) + stream = CountingStream(records) for record, extras in stream: dbid = False @@ -1630,21 +1672,23 @@ class BaseModel(object): # field name message_base = dict( extras, record=stream.index, field=field_names[field]) - with warnings.catch_warnings(record=True) as w: - try: + try: + with warnings.catch_warnings(record=True) as ws: converted[field] = converters[field](strvalue) - # In warning and error returned, use logical field name - # as field so client can reverse - for warning in w: - log(dict(message_base, type='warning', field=field, - message=unicode(warning.message) % message_base)) - except ValueError, e: - log(dict(message_base, - type='error', - field=field, - message=unicode(e) % message_base, - )) + for warning in ws: + # bubble non-import warnings upward + if warning.category != ImportWarning: + warnings.warn(warning.message, warning.category) + continue + w = warning.message + if isinstance(w, basestring): + # wrap warning string in an ImportWarning for + # uniform handling + w = ImportWarning(w) + _log(message_base, field, w) + except ValueError, e: + _log(message_base, field, e) yield dbid, xid, converted, dict(extras, record=stream.index) diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index cf95f5bb6c4..f97dafaaf39 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -5,14 +5,10 @@ import openerp from openerp.tests import common from openerp.tools.misc import mute_logger -def message(msg, type='error', from_=0, to_=0, record=0, field='value'): - return { - 'type': type, - 'rows': {'from': from_, 'to': to_}, - 'record': record, - 'field': field, - 'message': msg - } +def message(msg, type='error', from_=0, to_=0, record=0, field='value', **kwargs): + return dict(kwargs, + type=type, rows={'from': from_, 'to': to_}, record=record, + field=field, message=msg) def error(row, message, record=None, **kwargs): """ Failed import of the record ``record`` at line ``row``, with the error @@ -455,23 +451,15 @@ class test_selection(ImporterCase): def test_invalid(self): ids, messages = self.import_(['value'], [['Baz']]) self.assertIs(ids, False) - self.assertEqual(messages, [{ - 'type': 'error', - 'rows': {'from': 0, 'to': 0}, - 'record': 0, - 'field': 'value', - 'message': "Value 'Baz' not found in selection field 'unknown'", - }]) + self.assertEqual(messages, [message( + u"Value 'Baz' not found in selection field 'unknown'", + moreinfo="Foo Bar Qux".split())]) ids, messages = self.import_(['value'], [[42]]) self.assertIs(ids, False) - self.assertEqual(messages, [{ - 'type': 'error', - 'rows': {'from': 0, 'to': 0}, - 'record': 0, - 'field': 'value', - 'message': "Value '42' not found in selection field 'unknown'", - }]) + self.assertEqual(messages, [message( + u"Value '42' not found in selection field 'unknown'", + moreinfo="Foo Bar Qux".split())]) class test_selection_function(ImporterCase): model_name = 'export.selection.function' From 9f2e7ba7ef86808ea5f10af178e7aeb939426323 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Sep 2012 17:15:02 +0200 Subject: [PATCH 016/443] [IMP] return a dict from Model.load for easier future extensibility (if needed) rather than a tuple. also easier/cleaner to unpack on the JS side bzr revid: xmo@openerp.com-20120924151502-4robe639ctpuvb94 --- openerp/osv/orm.py | 6 +- .../addons/test_impex/tests/test_load.py | 378 +++++++++--------- 2 files changed, 192 insertions(+), 192 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 98e24075311..b824be91b96 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1486,7 +1486,7 @@ class BaseModel(object): :param data: row-major matrix of data to import :type data: list(list(str)) :param dict context: - :returns: + :returns: {ids: list(int)|False, messages: [Message]} """ cr.execute('SAVEPOINT model_load') messages = [] @@ -1516,8 +1516,8 @@ class BaseModel(object): messages.append(dict(info, type="error", message=str(e))) if any(message['type'] == 'error' for message in messages): cr.execute('ROLLBACK TO SAVEPOINT model_load') - return False, messages - return ids, messages + ids = False + return {'ids': ids, 'messages': messages} def _extract_records(self, cr, uid, fields_, data, context=None, log=lambda a: None): """ Generates record dicts from the data iterable. diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index f97dafaaf39..060b32c48f5 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -100,9 +100,9 @@ class test_ids_stuff(ImporterCase): model_name = 'export.integer' def test_create_with_id(self): - ids, messages = self.import_(['.id', 'value'], [['42', '36']]) - self.assertIs(ids, False) - self.assertEqual(messages, [{ + result = self.import_(['.id', 'value'], [['42', '36']]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [{ 'type': 'error', 'rows': {'from': 0, 'to': 0}, 'record': 0, @@ -110,9 +110,9 @@ class test_ids_stuff(ImporterCase): 'message': u"Unknown database identifier '42'", }]) def test_create_with_xid(self): - ids, messages = self.import_(['id', 'value'], [['somexmlid', '42']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['id', 'value'], [['somexmlid', '42']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual( 'somexmlid', self.xid(self.browse()[0])) @@ -123,9 +123,9 @@ class test_ids_stuff(ImporterCase): 36, self.model.browse(self.cr, openerp.SUPERUSER_ID, id).value) - ids, messages = self.import_(['.id', 'value'], [[str(id), '42']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['.id', 'value'], [[str(id), '42']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual( [42], # updated value to imported values(self.read())) @@ -143,12 +143,12 @@ class test_boolean_field(ImporterCase): def test_empty(self): self.assertEqual( self.import_(['value'], []), - ([], [])) + {'ids': [], 'messages': []}) def test_exported(self): - ids, messages = self.import_(['value'], [['False'], ['True'], ]) - self.assertEqual(len(ids), 2) - self.assertFalse(messages) + result = self.import_(['value'], [['False'], ['True'], ]) + self.assertEqual(len(result['ids']), 2) + self.assertFalse(result['messages']) records = self.read() self.assertEqual([ False, @@ -170,18 +170,18 @@ class test_boolean_field(ImporterCase): [u'klaidingas'], # false, lt, ] - ids, messages = self.import_(['value'], falses) - self.assertFalse(messages) - self.assertEqual(len(ids), len(falses)) + result = self.import_(['value'], falses) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), len(falses)) self.assertEqual([False] * len(falses), values(self.read())) def test_trues(self): trues = [['None'], ['nil'], ['()'], ['f'], ['#f'], # Problem: OpenOffice (and probably excel) output localized booleans ['VRAI'], ['ok'], ['true'], ['yes'], ['1'], ] - ids, messages = self.import_(['value'], trues) - self.assertEqual(len(ids), 10) - self.assertEqual(messages, [ + result = self.import_(['value'], trues) + self.assertEqual(len(result['ids']), 10) + self.assertEqual(result['messages'], [ message(u"Unknown value '%s' for boolean field 'unknown', assuming 'yes'" % v[0], type='warning', from_=i, to_=i, record=i) for i, v in enumerate(trues) @@ -197,69 +197,69 @@ class test_integer_field(ImporterCase): def test_none(self): self.assertEqual( self.import_(['value'], []), - ([], [])) + {'ids': [], 'messages': []}) def test_empty(self): - ids, messages = self.import_(['value'], [['']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual( [False], values(self.read())) def test_zero(self): - ids, messages = self.import_(['value'], [['0']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['0']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) - ids, messages = self.import_(['value'], [['-0']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['-0']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual([False, False], values(self.read())) def test_positives(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['1'], ['42'], [str(2**31-1)], ['12345678'] ]) - self.assertEqual(len(ids), 4) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 4) + self.assertFalse(result['messages']) self.assertEqual([ 1, 42, 2**31-1, 12345678 ], values(self.read())) def test_negatives(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['-1'], ['-42'], [str(-(2**31 - 1))], [str(-(2**31))], ['-12345678'] ]) - self.assertEqual(len(ids), 5) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 5) + self.assertFalse(result['messages']) self.assertEqual([ -1, -42, -(2**31 - 1), -(2**31), -12345678 ], values(self.read())) @mute_logger('openerp.sql_db') def test_out_of_range(self): - ids, messages = self.import_(['value'], [[str(2**31)]]) - self.assertIs(ids, False) - self.assertEqual(messages, [{ + result = self.import_(['value'], [[str(2**31)]]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [{ 'type': 'error', 'rows': {'from': 0, 'to': 0}, 'record': 0, 'message': "integer out of range\n" }]) - ids, messages = self.import_(['value'], [[str(-2**32)]]) - self.assertIs(ids, False) - self.assertEqual(messages, [{ + result = self.import_(['value'], [[str(-2**32)]]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [{ 'type': 'error', 'rows': {'from': 0, 'to': 0}, 'record': 0, @@ -267,9 +267,9 @@ class test_integer_field(ImporterCase): }]) def test_nonsense(self): - ids, messages = self.import_(['value'], [['zorglub']]) - self.assertIs(ids, False) - self.assertEqual(messages, [{ + result = self.import_(['value'], [['zorglub']]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [{ 'type': 'error', 'rows': {'from': 0, 'to': 0}, 'record': 0, @@ -282,29 +282,29 @@ class test_float_field(ImporterCase): def test_none(self): self.assertEqual( self.import_(['value'], []), - ([], [])) + {'ids': [], 'messages': []}) def test_empty(self): - ids, messages = self.import_(['value'], [['']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual( [False], values(self.read())) def test_zero(self): - ids, messages = self.import_(['value'], [['0']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['0']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) - ids, messages = self.import_(['value'], [['-0']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['-0']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual([False, False], values(self.read())) def test_positives(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['1'], ['42'], [str(2**31-1)], @@ -312,15 +312,15 @@ class test_float_field(ImporterCase): [str(2**33)], ['0.000001'], ]) - self.assertEqual(len(ids), 6) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 6) + self.assertFalse(result['messages']) self.assertEqual([ 1, 42, 2**31-1, 12345678, 2.0**33, .000001 ], values(self.read())) def test_negatives(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['-1'], ['-42'], [str(-2**31 + 1)], @@ -329,16 +329,16 @@ class test_float_field(ImporterCase): [str(-2**33)], ['-0.000001'], ]) - self.assertEqual(len(ids), 7) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 7) + self.assertFalse(result['messages']) self.assertEqual([ -1, -42, -(2**31 - 1), -(2**31), -12345678, -2.0**33, -.000001 ], values(self.read())) def test_nonsense(self): - ids, messages = self.import_(['value'], [['foobar']]) - self.assertIs(ids, False) - self.assertEqual(messages, [{ + result = self.import_(['value'], [['foobar']]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [{ 'type': 'error', 'rows': {'from': 0, 'to': 0}, 'record': 0, @@ -350,13 +350,13 @@ class test_string_field(ImporterCase): model_name = 'export.string.bounded' def test_empty(self): - ids, messages = self.import_(['value'], [['']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual([False], values(self.read())) def test_imported(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ [u'foobar'], [u'foobarbaz'], [u'Með suð í eyrum við spilum endalaust'], @@ -364,8 +364,8 @@ class test_string_field(ImporterCase): u"someone he can't pound a nail with a banana doesn't much " u"surprise him."] ]) - self.assertEqual(len(ids), 4) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 4) + self.assertFalse(result['messages']) self.assertEqual([ u"foobar", u"foobarbaz", @@ -377,14 +377,14 @@ class test_unbound_string_field(ImporterCase): model_name = 'export.string' def test_imported(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ [u'í dag viðrar vel til loftárása'], # ackbar.jpg [u"If they ask you about fun, you tell them – fun is a filthy" u" parasite"] ]) - self.assertEqual(len(ids), 2) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 2) + self.assertFalse(result['messages']) self.assertEqual([ u"í dag viðrar vel til loftárása", u"If they ask you about fun, you tell them – fun is a filthy parasite" @@ -394,9 +394,9 @@ class test_text(ImporterCase): model_name = 'export.text' def test_empty(self): - ids, messages = self.import_(['value'], [['']]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['']]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual([False], values(self.read())) def test_imported(self): @@ -406,9 +406,9 @@ class test_text(ImporterCase): u"snúninga 12 tommu vínylplötur (sem geta verið allt að 30 mín " u"hvor hlið).\n\nBreiðskífur eru stundum tvöfaldar og eru þær þá" u" gefnar út á tveimur geisladiskum eða tveimur vínylplötum.") - ids, messages = self.import_(['value'], [[s]]) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [[s]]) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) self.assertEqual([s], values(self.read())) class test_selection(ImporterCase): @@ -420,44 +420,44 @@ class test_selection(ImporterCase): ] def test_imported(self): - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['Qux'], ['Bar'], ['Foo'], ['2'], ]) - self.assertEqual(len(ids), 4) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 4) + self.assertFalse(result['messages']) self.assertEqual([3, 2, 1, 2], values(self.read())) def test_imported_translated(self): self.add_translations( 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['toto'], ['tete'], ['titi'], ], context={'lang': 'fr_FR'}) - self.assertEqual(len(ids), 3) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 3) + self.assertFalse(result['messages']) self.assertEqual([3, 1, 2], values(self.read())) - ids, messages = self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}) - self.assertEqual(len(ids), 1) - self.assertFalse(messages) + result = self.import_(['value'], [['Foo']], context={'lang': 'fr_FR'}) + self.assertEqual(len(result['ids']), 1) + self.assertFalse(result['messages']) def test_invalid(self): - ids, messages = self.import_(['value'], [['Baz']]) - self.assertIs(ids, False) - self.assertEqual(messages, [message( + result = self.import_(['value'], [['Baz']]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [message( u"Value 'Baz' not found in selection field 'unknown'", moreinfo="Foo Bar Qux".split())]) - ids, messages = self.import_(['value'], [[42]]) - self.assertIs(ids, False) - self.assertEqual(messages, [message( + result = self.import_(['value'], [[42]]) + self.assertIs(result['ids'], False) + self.assertEqual(result['messages'], [message( u"Value '42' not found in selection field 'unknown'", moreinfo="Foo Bar Qux".split())]) @@ -476,12 +476,12 @@ class test_selection_function(ImporterCase): import does not actually know that the selection field uses a function """ # NOTE: conflict between a value and a label => pick first - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['3'], ["Grault"], ]) - self.assertEqual(len(ids), 2) - self.assertFalse(messages) + self.assertEqual(len(result['ids']), 2) + self.assertFalse(result['messages']) self.assertEqual( ['3', '1'], values(self.read())) @@ -492,17 +492,17 @@ class test_selection_function(ImporterCase): self.add_translations( 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['titi'], ['tete'], ], context={'lang': 'fr_FR'}) - self.assertFalse(messages) - self.assertEqual(len(ids), 2) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 2) self.assertEqual(values(self.read()), ['1', '2']) - ids, messages = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + result = self.import_(['value'], [['Wheee']], context={'lang': 'fr_FR'}) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) class test_m2o(ImporterCase): model_name = 'export.many2one' @@ -519,14 +519,14 @@ class test_m2o(ImporterCase): name2 = dict(self.registry('export.integer').name_get( self.cr, openerp.SUPERUSER_ID,[integer_id2]))[integer_id2] - ids , messages = self.import_(['value'], [ + result = self.import_(['value'], [ # import by name_get [name1], [name1], [name2], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 3) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 3) # correct ids assigned to corresponding records self.assertEqual([ (integer_id1, name1), @@ -541,18 +541,18 @@ class test_m2o(ImporterCase): xid = self.xid(ExportInteger.browse( self.cr, openerp.SUPERUSER_ID, [integer_id])[0]) - ids, messages = self.import_(['value/id'], [[xid]]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + result = self.import_(['value/id'], [[xid]]) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) b = self.browse() self.assertEqual(42, b[0].value.value) def test_by_id(self): integer_id = self.registry('export.integer').create( self.cr, openerp.SUPERUSER_ID, {'value': 42}) - ids, messages = self.import_(['value/.id'], [[integer_id]]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + result = self.import_(['value/.id'], [[integer_id]]) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) b = self.browse() self.assertEqual(42, b[0].value.value) @@ -568,12 +568,12 @@ class test_m2o(ImporterCase): # names should be the same self.assertEqual(name1, name2) - ids, messages = self.import_(['value'], [[name2]]) + result = self.import_(['value'], [[name2]]) self.assertEqual( - messages, + result['messages'], [message(u"Found multiple matches for field 'unknown' (2 matches)", type='warning')]) - self.assertEqual(len(ids), 1) + self.assertEqual(len(result['ids']), 1) self.assertEqual([ (integer_id1, name1) ], values(self.read())) @@ -588,55 +588,55 @@ class test_m2o(ImporterCase): self.cr, openerp.SUPERUSER_ID, {'value': 36}) # Because name_search all the things. Fallback schmallback - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ # import by id, without specifying it [integer_id1], [integer_id2], [integer_id1], ]) - self.assertEqual(messages, [ + self.assertEqual(result['messages'], [ message(u"No matching record found for name '%s' in field 'unknown'" % id, from_=index, to_=index, record=index) for index, id in enumerate([integer_id1, integer_id2, integer_id1])]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_sub_field(self): """ Does not implicitly create the record, does not warn that you can't import m2o subfields (at all)... """ - ids, messages = self.import_(['value/value'], [['42']]) - self.assertEqual(messages, [ + result = self.import_(['value/value'], [['42']]) + self.assertEqual(result['messages'], [ message(u"Can not create Many-To-One records indirectly, import " u"the field separately")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_fail_noids(self): - ids, messages = self.import_(['value'], [['nameisnoexist:3']]) - self.assertEqual(messages, [message( + result = self.import_(['value'], [['nameisnoexist:3']]) + self.assertEqual(result['messages'], [message( u"No matching record found for name 'nameisnoexist:3' " u"in field 'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) - ids, messages = self.import_(['value/id'], [['noxidhere']]) - self.assertEqual(messages, [message( + result = self.import_(['value/id'], [['noxidhere']]) + self.assertEqual(result['messages'], [message( u"No matching record found for external id 'noxidhere' " u"in field 'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) - ids, messages = self.import_(['value/.id'], [['66']]) - self.assertEqual(messages, [message( + result = self.import_(['value/.id'], [['66']]) + self.assertEqual(result['messages'], [message( u"No matching record found for database id '66' " u"in field 'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_fail_multiple(self): - ids, messages = self.import_( + result = self.import_( ['value', 'value/id'], [['somename', 'somexid']]) - self.assertEqual(messages, [message( + self.assertEqual(result['messages'], [message( u"Ambiguous specification for field 'unknown', only provide one of " u"name, external id or database id")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) class test_m2m(ImporterCase): model_name = 'export.many2many' @@ -656,14 +656,14 @@ class test_m2m(ImporterCase): id5 = self.registry('export.many2many.other').create( self.cr, openerp.SUPERUSER_ID, {'value': 99, 'str': 'record4'}) - ids, messages = self.import_(['value/.id'], [ + result = self.import_(['value/.id'], [ ['%d,%d' % (id1, id2)], ['%d,%d,%d' % (id1, id3, id4)], ['%d,%d,%d' % (id1, id2, id3)], ['%d' % id5] ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 4) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 4) ids = lambda records: [record.id for record in records] @@ -675,11 +675,11 @@ class test_m2m(ImporterCase): self.assertEqual(values(b[2].value), [3, 44, 84]) def test_noids(self): - ids, messages = self.import_(['value/.id'], [['42']]) - self.assertEqual(messages, [message( + result = self.import_(['value/.id'], [['42']]) + self.assertEqual(result['messages'], [message( u"No matching record found for database id '42' in field " u"'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_xids(self): M2O_o = self.registry('export.many2many.other') @@ -689,23 +689,23 @@ class test_m2m(ImporterCase): id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) records = M2O_o.browse(self.cr, openerp.SUPERUSER_ID, [id1, id2, id3, id4]) - ids, messages = self.import_(['value/id'], [ + result = self.import_(['value/id'], [ ['%s,%s' % (self.xid(records[0]), self.xid(records[1]))], ['%s' % self.xid(records[3])], ['%s,%s' % (self.xid(records[2]), self.xid(records[1]))], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 3) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 3) b = self.browse() self.assertEqual(values(b[0].value), [3, 44]) self.assertEqual(values(b[2].value), [44, 84]) def test_noxids(self): - ids, messages = self.import_(['value/id'], [['noxidforthat']]) - self.assertEqual(messages, [message( + result = self.import_(['value/id'], [['noxidforthat']]) + self.assertEqual(result['messages'], [message( u"No matching record found for external id 'noxidforthat' " u"in field 'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_names(self): M2O_o = self.registry('export.many2many.other') @@ -717,24 +717,24 @@ class test_m2m(ImporterCase): name = lambda record: dict(record.name_get())[record.id] - ids, messages = self.import_(['value'], [ + result = self.import_(['value'], [ ['%s,%s' % (name(records[1]), name(records[2]))], ['%s,%s,%s' % (name(records[0]), name(records[1]), name(records[2]))], ['%s,%s' % (name(records[0]), name(records[3]))], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 3) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 3) b = self.browse() self.assertEqual(values(b[1].value), [3, 44, 84]) self.assertEqual(values(b[2].value), [3, 9]) def test_nonames(self): - ids, messages = self.import_(['value'], [['wherethem2mhavenonames']]) - self.assertEqual(messages, [message( + result = self.import_(['value'], [['wherethem2mhavenonames']]) + self.assertEqual(result['messages'], [message( u"No matching record found for name 'wherethem2mhavenonames' in " u"field 'unknown'")]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_import_to_existing(self): M2O_o = self.registry('export.many2many.other') @@ -744,12 +744,12 @@ class test_m2m(ImporterCase): id4 = M2O_o.create(self.cr, openerp.SUPERUSER_ID, {'value': 9, 'str': 'record3'}) xid = 'myxid' - ids, messages = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) - ids, messages = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + result = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id1, id2)]]) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) + result = self.import_(['id', 'value/.id'], [[xid, '%d,%d' % (id3, id4)]]) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) b = self.browse() self.assertEqual(len(b), 1) @@ -762,31 +762,31 @@ class test_o2m(ImporterCase): def test_name_get(self): s = u'Java is a DSL for taking large XML files and converting them ' \ u'to stack traces' - ids, messages = self.import_( + result = self.import_( ['const', 'value'], [['5', s]]) - self.assertEqual(messages, [message( + self.assertEqual(result['messages'], [message( u"No matching record found for name '%s' in field 'unknown'" % s)]) - self.assertIs(ids, False) + self.assertIs(result['ids'], False) def test_single(self): - ids, messages = self.import_(['const', 'value/value'], [ + result = self.import_(['const', 'value/value'], [ ['5', '63'] ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) (b,) = self.browse() self.assertEqual(b.const, 5) self.assertEqual(values(b.value), [63]) def test_multicore(self): - ids, messages = self.import_(['const', 'value/value'], [ + result = self.import_(['const', 'value/value'], [ ['5', '63'], ['6', '64'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 2) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 2) b1, b2 = self.browse() self.assertEqual(b1.const, 5) @@ -795,27 +795,27 @@ class test_o2m(ImporterCase): self.assertEqual(values(b2.value), [64]) def test_multisub(self): - ids, messages = self.import_(['const', 'value/value'], [ + result = self.import_(['const', 'value/value'], [ ['5', '63'], ['', '64'], ['', '65'], ['', '66'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) def test_multi_subfields(self): - ids, messages = self.import_(['value/str', 'const', 'value/value'], [ + result = self.import_(['value/str', 'const', 'value/value'], [ ['this', '5', '63'], ['is', '', '64'], ['the', '', '65'], ['rhythm', '', '66'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) (b,) = self.browse() self.assertEqual(values(b.value), [63, 64, 65, 66]) @@ -833,11 +833,11 @@ class test_o2m(ImporterCase): 'str': 'Me', 'value': 262 }) - ids, messages = self.import_(['const', 'value/.id'], [ + result = self.import_(['const', 'value/.id'], [ ['42', '%d,%d' % (id1, id2)] ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) [b] = self.browse() self.assertEqual(b.const, 42) @@ -856,12 +856,12 @@ class test_o2m(ImporterCase): 'str': 'Me', 'value': 262 }) - ids, messages = self.import_(['const', 'value/.id'], [ + result = self.import_(['const', 'value/.id'], [ ['42', str(id1)], ['', str(id2)], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) [b] = self.browse() self.assertEqual(b.const, 42) @@ -878,12 +878,12 @@ class test_o2m(ImporterCase): 'str': 'Me', 'value': 262 }) - ids, messages = self.import_(['const', 'value/.id', 'value/value'], [ + result = self.import_(['const', 'value/.id', 'value/value'], [ ['42', str(id1), '1'], ['', str(id2), '2'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) [b] = self.browse() self.assertEqual(b.const, 42) @@ -894,21 +894,21 @@ class test_o2m_multiple(ImporterCase): model_name = 'export.one2many.multiple' def test_multi_mixed(self): - ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + result = self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', '21'], ['', '12', '22'], ['', '13', '23'], ['', '14', ''], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) # Oh yeah, that's the stuff [b] = self.browse() self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi(self): - ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + result = self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', '21'], ['', '12', ''], ['', '13', ''], @@ -916,15 +916,15 @@ class test_o2m_multiple(ImporterCase): ['', '', '22'], ['', '', '23'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) [b] = self.browse() self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) def test_multi_fullsplit(self): - ids, messages = self.import_(['const', 'child1/value', 'child2/value'], [ + result = self.import_(['const', 'child1/value', 'child2/value'], [ ['5', '11', ''], ['', '12', ''], ['', '13', ''], @@ -933,8 +933,8 @@ class test_o2m_multiple(ImporterCase): ['', '', '22'], ['', '', '23'], ]) - self.assertFalse(messages) - self.assertEqual(len(ids), 1) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) [b] = self.browse() self.assertEqual(b.const, 5) From e6c8f1739a70ba8c0252ed8ace839c30f7e794dc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Sep 2012 09:42:56 +0200 Subject: [PATCH 017/443] [ADD] 'more info' action to m2o, o2m and m2m linking failures bzr revid: xmo@openerp.com-20120925074256-18puerjbfo3om265 --- openerp/addons/base/ir/ir_fields.py | 31 +++++++------- .../addons/test_impex/tests/test_load.py | 42 +++++++++---------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 2256542533b..cec74e8976e 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -201,6 +201,21 @@ class ir_fields_converter(orm.Model): id, _name = ids[0] else: raise Exception(u"Unknown sub-field '%s'" % subfield) + + if id is None: + raise ValueError( + _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") + % {'field_type': field_type, 'value': value}, { + 'moreinfo': { + 'type': 'ir.actions.act_window', + 'target': 'new', + 'res_model': column._obj, + 'view_mode': 'tree,form', + 'view_type': 'form', + 'views': [(False, 'tree', (False, 'form'))], + 'help': _(u"See all possible values") + } + }) return id, field_type def _referencing_subfield(self, record): @@ -235,12 +250,8 @@ class ir_fields_converter(orm.Model): reference = record[subfield] id, subfield_type = self.db_id_for( cr, uid, model, column, subfield, reference, context=context) - - if id is None: - raise ValueError( - _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") - % {'field_type': subfield_type, 'value': reference}) return id + def _str_to_many2many(self, cr, uid, model, column, value, context=None): [record] = value @@ -250,13 +261,9 @@ class ir_fields_converter(orm.Model): for reference in record[subfield].split(','): id, subfield_type = self.db_id_for( cr, uid, model, column, subfield, reference, context=context) - if id is None: - raise ValueError( - _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") - % {'field_type': subfield_type, 'value': reference}) ids.append(id) - return [(6, 0, ids)] + def _str_to_one2many(self, cr, uid, model, column, records, context=None): commands = [] @@ -278,10 +285,6 @@ class ir_fields_converter(orm.Model): reference = record[subfield] id, subfield_type = self.db_id_for( cr, uid, model, column, subfield, reference, context=context) - if id is None: - raise ValueError( - _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") - % {'field_type': subfield_type, 'value': reference}) writable = exclude_ref_fields(record) if id: diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 060b32c48f5..f625ecb7043 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -9,18 +9,16 @@ def message(msg, type='error', from_=0, to_=0, record=0, field='value', **kwargs return dict(kwargs, type=type, rows={'from': from_, 'to': to_}, record=record, field=field, message=msg) - -def error(row, message, record=None, **kwargs): - """ Failed import of the record ``record`` at line ``row``, with the error - message ``message`` - - :param str message: - :param dict record: - """ - return ( - -1, dict(record or {}, **kwargs), - "Line %d : %s" % (row, message), - '') +def moreaction(model): + return { + 'type': 'ir.actions.act_window', + 'target': 'new', + 'res_model': model, + 'view_mode': 'tree,form', + 'view_type': 'form', + 'views': [(False, 'tree', (False, 'form'))], + 'help': u"See all possible values" + } def values(seq, field='value'): return [item[field] for item in seq] @@ -596,7 +594,8 @@ class test_m2o(ImporterCase): ]) self.assertEqual(result['messages'], [ message(u"No matching record found for name '%s' in field 'unknown'" % id, - from_=index, to_=index, record=index) + from_=index, to_=index, record=index, + moreinfo=moreaction('export.integer')) for index, id in enumerate([integer_id1, integer_id2, integer_id1])]) self.assertIs(result['ids'], False) @@ -614,19 +613,19 @@ class test_m2o(ImporterCase): result = self.import_(['value'], [['nameisnoexist:3']]) self.assertEqual(result['messages'], [message( u"No matching record found for name 'nameisnoexist:3' " - u"in field 'unknown'")]) + u"in field 'unknown'", moreinfo=moreaction('export.integer'))]) self.assertIs(result['ids'], False) result = self.import_(['value/id'], [['noxidhere']]) self.assertEqual(result['messages'], [message( u"No matching record found for external id 'noxidhere' " - u"in field 'unknown'")]) + u"in field 'unknown'", moreinfo=moreaction('export.integer'))]) self.assertIs(result['ids'], False) result = self.import_(['value/.id'], [['66']]) self.assertEqual(result['messages'], [message( u"No matching record found for database id '66' " - u"in field 'unknown'")]) + u"in field 'unknown'", moreinfo=moreaction('export.integer'))]) self.assertIs(result['ids'], False) def test_fail_multiple(self): @@ -678,7 +677,7 @@ class test_m2m(ImporterCase): result = self.import_(['value/.id'], [['42']]) self.assertEqual(result['messages'], [message( u"No matching record found for database id '42' in field " - u"'unknown'")]) + u"'unknown'", moreinfo=moreaction('export.many2many.other'))]) self.assertIs(result['ids'], False) def test_xids(self): @@ -703,8 +702,8 @@ class test_m2m(ImporterCase): def test_noxids(self): result = self.import_(['value/id'], [['noxidforthat']]) self.assertEqual(result['messages'], [message( - u"No matching record found for external id 'noxidforthat' " - u"in field 'unknown'")]) + u"No matching record found for external id 'noxidforthat' in field" + u" 'unknown'", moreinfo=moreaction('export.many2many.other'))]) self.assertIs(result['ids'], False) def test_names(self): @@ -733,7 +732,7 @@ class test_m2m(ImporterCase): result = self.import_(['value'], [['wherethem2mhavenonames']]) self.assertEqual(result['messages'], [message( u"No matching record found for name 'wherethem2mhavenonames' in " - u"field 'unknown'")]) + u"field 'unknown'", moreinfo=moreaction('export.many2many.other'))]) self.assertIs(result['ids'], False) def test_import_to_existing(self): @@ -766,7 +765,8 @@ class test_o2m(ImporterCase): ['const', 'value'], [['5', s]]) self.assertEqual(result['messages'], [message( - u"No matching record found for name '%s' in field 'unknown'" % s)]) + u"No matching record found for name '%s' in field 'unknown'" % s, + moreinfo=moreaction('export.one2many.child'))]) self.assertIs(result['ids'], False) def test_single(self): From 359b4a44aa2d10cac6a0ce9bd623ac6b4e1a17d7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Sep 2012 12:02:32 +0200 Subject: [PATCH 018/443] [ADD] ability to convert postgres error messages to human-readable ones also convert 'violates not-null constraint' to something about fields being required bzr revid: xmo@openerp.com-20120925100232-bfmxcxda65cki5kv --- openerp/osv/orm.py | 31 ++++++++++++++++++- openerp/tests/addons/test_impex/models.py | 1 + .../addons/test_impex/tests/test_load.py | 15 +++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b824be91b96..25974760bdd 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -42,6 +42,7 @@ """ import calendar +import collections import copy import datetime import itertools @@ -1493,6 +1494,7 @@ class BaseModel(object): fields = map(fix_import_export_id_paths, fields) ModelData = self.pool['ir.model.data'] + fg = self.fields_get(cr, uid, context=context) mode = 'init' current_module = '' @@ -1509,11 +1511,16 @@ class BaseModel(object): current_module, record, mode=mode, xml_id=xid, noupdate=noupdate, res_id=id, context=context)) cr.execute('RELEASE SAVEPOINT model_load_save') + except psycopg2.Warning, e: + cr.execute('ROLLBACK TO SAVEPOINT model_load_save') + messages.append(dict(info, type='warning', message=str(e))) except psycopg2.Error, e: # Failed to write, log to messages, rollback savepoint (to # avoid broken transaction) and keep going cr.execute('ROLLBACK TO SAVEPOINT model_load_save') - messages.append(dict(info, type="error", message=str(e))) + messages.append(dict( + info, type="error", + **PGERROR_TO_OE[e.pgcode](self, fg, info, e))) if any(message['type'] == 'error' for message in messages): cr.execute('ROLLBACK TO SAVEPOINT model_load') ids = False @@ -5390,4 +5397,26 @@ class ImportWarning(Warning): """ Used to send warnings upwards the stack during the import process """ pass + + +def convert_pgerror_23502(model, fields, info, e): + m = re.match(r'^null value in column "(?P\w+)" violates ' + r'not-null constraint\n$', + str(e)) + if not m or m.group('field') not in fields: + return {'message': unicode(e)} + field = fields[m.group('field')] + return { + 'message': _(u"Missing required value for the field '%(field)s'") % { + 'field': field['string'] + }, + 'field': m.group('field'), + } + +PGERROR_TO_OE = collections.defaultdict( + # shape of mapped converters + lambda: (lambda model, fvg, info, pgerror: {'message': unicode(pgerror)}), { + # not_null_violation + '23502': convert_pgerror_23502, +}) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py index 37894ccb78c..c56ddc649e2 100644 --- a/openerp/tests/addons/test_impex/models.py +++ b/openerp/tests/addons/test_impex/models.py @@ -17,6 +17,7 @@ models = [ ('float', fields.float()), ('decimal', fields.float(digits=(16, 3))), ('string.bounded', fields.char('unknown', size=16)), + ('string.required', fields.char('unknown', size=None, required=True)), ('string', fields.char('unknown', size=None)), ('date', fields.date()), ('datetime', fields.datetime()), diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index f625ecb7043..eb777a57988 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -388,6 +388,21 @@ class test_unbound_string_field(ImporterCase): u"If they ask you about fun, you tell them – fun is a filthy parasite" ], values(self.read())) +class test_required_string_field(ImporterCase): + model_name = 'export.string.required' + + def test_empty(self): + result = self.import_(['value'], [[]]) + self.assertEqual(result['messages'], [message( + u"Missing required value for the field 'unknown'")]) + self.assertIs(result['ids'], False) + + def test_not_provided(self): + result = self.import_(['const'], [['12']]) + self.assertEqual(result['messages'], [message( + u"Missing required value for the field 'unknown'")]) + self.assertIs(result['ids'], False) + class test_text(ImporterCase): model_name = 'export.text' From d5c69fa87e9d17c92c00347af73fce53fb47dcd4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Sep 2012 15:59:55 +0200 Subject: [PATCH 019/443] [FIX] use lists instead of iterators in BaseModel._extract_records * although it does use an explicit external index, it turns out the code is less complex * the rewrapping of (many) iterators on top of one another ended up blowing Python's stack during ``next`` calls, which Python does *not* like * added a 900-ish import test file to check for these things bzr revid: xmo@openerp.com-20120925135955-oielhopegnefyctm --- openerp/addons/base/ir/ir_fields.py | 5 +- openerp/osv/orm.py | 53 +++++-------------- .../addons/test_impex/tests/test_load.py | 13 +++++ 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index cec74e8976e..03d95fc67a6 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -69,7 +69,7 @@ class ir_fields_converter(orm.Model): """ # FIXME: return None converter = getattr( - self, '_%s_to_%s' % (fromtype.__name__, column._type)) + self, '_%s_to_%s' % (fromtype.__name__, column._type), None) if not converter: return None return functools.partial( @@ -113,6 +113,9 @@ class ir_fields_converter(orm.Model): def _str_to_text(self, cr, uid, model, column, value, context=None): return value or False + def _str_to_binary(self, cr, uid, model, column, value, context=None): + return value or False + def _get_translations(self, cr, uid, types, src, context): types = tuple(types) # Cache translations so they don't have to be reloaded from scratch on diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 25974760bdd..eb7745b2f6e 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1527,7 +1527,7 @@ class BaseModel(object): return {'ids': ids, 'messages': messages} def _extract_records(self, cr, uid, fields_, data, context=None, log=lambda a: None): - """ Generates record dicts from the data iterable. + """ Generates record dicts from the data sequence. The result is a generator of dicts mapping field names to raw (unconverted, unvalidated) values. @@ -1562,12 +1562,11 @@ class BaseModel(object): def only_o2m_values(row, f=get_nono2m_values, g=get_o2m_values): return any(g(row)) and not any(f(row)) - rows = CountingStream(data) + index = 0 while True: - row = next(rows, None) - if row is None: return - record_row_index = rows.index + if index >= len(data): return + row = data[index] # copy non-relational fields to record dict record = dict((field[0], value) for field, value in itertools.izip(fields_, row) @@ -1575,10 +1574,10 @@ class BaseModel(object): # Get all following rows which have relational values attached to # the current record (no non-relational values) - # WARNING: replaces existing ``rows`` - record_span, _rows = span(only_o2m_values, rows) + record_span = itertools.takewhile( + only_o2m_values, itertools.islice(data, index + 1, None)) # stitch record row back on for relational fields - record_span = itertools.chain([row], record_span) + record_span = list(itertools.chain([row], record_span)) for relfield in set( field[0] for field in fields_ if is_relational(field[0])): @@ -1586,8 +1585,6 @@ class BaseModel(object): # FIXME: how to not use _obj without relying on fields_get? Model = self.pool[column._obj] - # copy stream to reuse for next relational field - fieldrows, record_span = itertools.tee(record_span) # get only cells for this sub-field, should be strictly # non-empty, field path [None] is for name_get column indices, subfields = zip(*((index, field[1:] or [None]) @@ -1596,26 +1593,17 @@ class BaseModel(object): # return all rows which have at least one value for the # subfields of relfield - relfield_data = filter(any, map(itemgetter_tuple(indices), fieldrows)) + relfield_data = filter(any, map(itemgetter_tuple(indices), record_span)) record[relfield] = [subrecord for subrecord, _subinfo in Model._extract_records( cr, uid, subfields, relfield_data, context=context, log=log)] - # Ensure full consumption of the span (and therefore advancement of - # ``rows``) even if there are no relational fields. Needs two as - # the code above stiched the row back on (so first call may only - # get the stiched row without advancing the underlying operator row - # itself) - next(record_span, None) - next(record_span, None) - # old rows consumption (by iterating the span) should be done here, - # at this point the old ``rows`` is 1 past `span` (either on the - # next record row or past ``StopIteration``, so wrap new ``rows`` - # (``_rows``) in a counting stream indexed 1-before the old - # ``rows`` - rows = CountingStream(_rows, rows.index - 1) - yield record, {'rows': {'from': record_row_index,'to': rows.index}} + yield record, {'rows': { + 'from': index, + 'to': index + len(record_span) - 1 + }} + index += len(record_span) def _convert_records(self, cr, uid, records, context=None, log=lambda a: None): """ Converts records from the source iterable (recursive dicts of @@ -5369,21 +5357,6 @@ class AbstractModel(BaseModel): _auto = False # don't create any database backend for AbstractModels _register = False # not visible in ORM registry, meant to be python-inherited only -def span(predicate, iterable): - """ Splits the iterable between the longest prefix of ``iterable`` whose - elements satisfy ``predicate`` and the rest. - - If called with a list, equivalent to:: - - takewhile(predicate, lst), dropwhile(predicate, lst) - - :param callable predicate: - :param iterable: - :rtype: (iterable, iterable) - """ - it1, it2 = itertools.tee(iterable) - return (itertools.takewhile(predicate, it1), - itertools.dropwhile(predicate, it2)) def itemgetter_tuple(items): """ Fixes itemgetter inconsistency (useful in some cases) of not returning a tuple if len(items) == 1: always returns an n-tuple where n = len(items) diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index eb777a57988..72442963af5 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +import json +import pkgutil + import openerp.modules.registry import openerp @@ -956,5 +959,15 @@ class test_o2m_multiple(ImporterCase): self.assertEqual(values(b.child1), [11, 12, 13, 14]) self.assertEqual(values(b.child2), [21, 22, 23]) +class test_realworld(common.TransactionCase): + def test_bigfile(self): + data = json.loads(pkgutil.get_data(self.__module__, 'contacts_big.json')) + result = self.registry('res.partner').load( + self.cr, openerp.SUPERUSER_ID, + ['name', 'mobile', 'email', 'image'], + data) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), len(data)) + # function, related, reference: written to db as-is... # => function uses @type for value coercion/conversion From e61dc509341ed2cd1222869048a81b2db209e21d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Sep 2012 17:59:15 +0200 Subject: [PATCH 020/443] [IMP] convert empty import fields to False values without going through converters simplifies the converter methods by avoiding redundant emptiness checks bzr revid: xmo@openerp.com-20120925155915-82p2s6stpww37p5n --- openerp/addons/base/ir/ir_fields.py | 8 +++----- openerp/osv/orm.py | 4 +++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 03d95fc67a6..621dd93d469 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -100,21 +100,19 @@ class ir_fields_converter(orm.Model): return True def _str_to_integer(self, cr, uid, model, column, value, context=None): - if not value: return False return int(value) def _str_to_float(self, cr, uid, model, column, value, context=None): - if not value: return False return float(value) def _str_to_char(self, cr, uid, model, column, value, context=None): - return value or False + return value def _str_to_text(self, cr, uid, model, column, value, context=None): - return value or False + return value def _str_to_binary(self, cr, uid, model, column, value, context=None): - return value or False + return value def _get_translations(self, cr, uid, types, src, context): types = tuple(types) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index eb7745b2f6e..81063deb180 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -68,7 +68,6 @@ from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp import SUPERUSER_ID from query import Query -from openerp import SUPERUSER_ID _logger = logging.getLogger(__name__) _schema = logging.getLogger(__name__ + '.schema') @@ -1662,6 +1661,9 @@ class BaseModel(object): for field, strvalue in record.iteritems(): if field in (None, 'id', '.id'): continue + if not strvalue: + converted[field] = False + continue # In warnings and error messages, use translated string as # field name From dad2475d9f68d8bafb0dcb633a7b05256a8ea0be Mon Sep 17 00:00:00 2001 From: Saurang Suthar Date: Wed, 26 Sep 2012 12:01:47 +0530 Subject: [PATCH 021/443] [IMP]sale_crm:made Convert to Quotation button invisible if opportunity is closed(Won) bzr revid: ssu@tinyerp.com-20120926063147-l4nvifaehms9lugc --- addons/sale_crm/sale_crm_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index e54ba158b9b..efee3953788 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -9,8 +9,8 @@ - From 533ac5b6f3258fda737b2e9818302657faf87e8f Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 14:34:31 +0530 Subject: [PATCH 111/443] [FIX]hr_expense: remove purchase_ok from return bzr revid: mma@tinyerp.com-20121003090431-ia11akxym2ax2nsy --- addons/hr_expense/hr_expense.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index 91dc44fed61..c685eb3f95b 100644 --- a/addons/hr_expense/hr_expense.py +++ b/addons/hr_expense/hr_expense.py @@ -243,7 +243,7 @@ class product_product(osv.osv): data_obj = self.pool.get('ir.model.data') cat_id = data_obj._get_id(cr, uid, 'hr_expense', 'cat_expense') categ_id = data_obj.browse(cr, uid, cat_id).res_id - res = {'value' : {'type':'service','procure_method':'make_to_stock','supply_method':'buy','purchase_ok':True,'sale_ok' :False,'categ_id':categ_id }} + res = {'value' : {'type':'service','procure_method':'make_to_stock','supply_method':'buy', 'sale_ok' :False,'categ_id':categ_id }} return res product_product() From 638982a0d29c429fac29b9c6e77d3722520dfb1a Mon Sep 17 00:00:00 2001 From: Saurang Suthar Date: Wed, 3 Oct 2012 14:46:21 +0530 Subject: [PATCH 112/443] [IMP]sale:rename field name Several analytic accounts on sales into Use multiple analytic accounts on sales bzr revid: ssu@tinyerp.com-20121003091621-7ga3az5hlthor6ab --- addons/sale/res_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py index 843e1fd3343..50eaf7a394a 100644 --- a/addons/sale/res_config.py +++ b/addons/sale/res_config.py @@ -125,7 +125,7 @@ class sale_configuration(osv.osv_memory): class account_config_settings(osv.osv_memory): _inherit = 'account.config.settings' _columns = { - 'module_sale_analytic_plans': fields.boolean('Several analytic accounts on sales', + 'module_sale_analytic_plans': fields.boolean('Use multiple analytic accounts on sales', help="""This allows install module sale_analytic_plans."""), 'group_analytic_account_for_sales': fields.boolean('Analytic accounting for sales', implied_group='sale.group_analytic_accounting', From b993bf588fadac427a801d18e1aef46a3be6f52e Mon Sep 17 00:00:00 2001 From: Saurang Suthar Date: Wed, 3 Oct 2012 14:56:11 +0530 Subject: [PATCH 113/443] [IMP]hr_attendance:rename field Track attendances into Attendance group allocation to users bzr revid: ssu@tinyerp.com-20121003092611-sklycsd74m4n669h --- addons/hr_attendance/res_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_attendance/res_config.py b/addons/hr_attendance/res_config.py index 0da2d183409..c32e056621d 100644 --- a/addons/hr_attendance/res_config.py +++ b/addons/hr_attendance/res_config.py @@ -25,7 +25,7 @@ class hr_attendance_config_settings(osv.osv_memory): _inherit = 'hr.config.settings' _columns = { - 'group_hr_attendance': fields.boolean('Track attendances', + 'group_hr_attendance': fields.boolean('Attendance group allocation to users', implied_group='base.group_hr_attendance', help="Allocates attendance group to all users."), } From 948841f419e54d4b08ec59169b4f6b102decb9f4 Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 3 Oct 2012 14:59:39 +0530 Subject: [PATCH 114/443] [IMP]improve help bzr revid: sgo@tinyerp.com-20121003092939-nr2u8ft9uozb24ev --- addons/sale/sale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 6c73f4513a3..b89b687db98 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -184,7 +184,7 @@ class sale_order(osv.osv): 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True), 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."), - 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."), + 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."), 'order_policy': fields.selection([ ('manual', 'On Demand'), ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, From 43951ef5b42310889ff4b5ce6f14e1bb5a3f327c Mon Sep 17 00:00:00 2001 From: "Sanjay Gohel (Open ERP)" Date: Wed, 3 Oct 2012 15:07:42 +0530 Subject: [PATCH 115/443] [IMP]rename field and improve code for removing error bzr revid: sgo@tinyerp.com-20121003093742-waibinp6s7t6ismv --- addons/crm/crm_lead.py | 2 +- addons/sale/sale.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 7e33f397e8a..cfa30a48b7b 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -776,7 +776,7 @@ class crm_lead(base_stage, format_address, osv.osv): 'default_user_id': uid, 'default_section_id': opportunity.section_id and opportunity.section_id.id or False, 'default_email_from': opportunity.email_from, - 'default_state': 'open', + 'default_state': 'needs-action', 'default_name': opportunity.name, } return res diff --git a/addons/sale/sale.py b/addons/sale/sale.py index c47bf1357c8..5f12d302f10 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -694,7 +694,7 @@ class sale_order_line(osv.osv): _description = 'Sales Order Line' _columns = { 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}), - 'name': fields.text('Product Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}), + 'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True), 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True), From 4ef3f9218bb97b66b301f84ed49661d3be79a909 Mon Sep 17 00:00:00 2001 From: "Purnendu Singh (OpenERP)" Date: Wed, 3 Oct 2012 15:12:42 +0530 Subject: [PATCH 116/443] [IMP] document: add on_change on fname so it will update the download link with the name of attached document bzr revid: psi@tinyerp.com-20121003094242-5wu6pki5cx442jte --- addons/document/document.py | 10 ++++++++-- addons/document/document_view.xml | 13 +++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/addons/document/document.py b/addons/document/document.py index 3ae783f49e7..cbc20edc044 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -36,7 +36,6 @@ DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['roo class document_file(osv.osv): _inherit = 'ir.attachment' - _rec_name = 'datas_fname' def _attach_parent_id(self, cr, uid, ids=None, context=None): @@ -149,7 +148,14 @@ class document_file(osv.osv): _sql_constraints = [ # filename_uniq is not possible in pure SQL ] - def _check_duplication(self, cr, uid, vals, ids=None, op='create'): + + def onchange_file(self, cr, uid, ids, datas_fname=False, context=None): + res = {'value':{}} + if datas_fname: + res['value'].update({'name': datas_fname}) + return res + + def _check_duplication(self, cr, uid, vals, ids=[], op='create'): name = vals.get('name', False) parent_id = vals.get('parent_id', False) res_model = vals.get('res_model', False) diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml index 8d1ff0850b3..54776c4d36e 100644 --- a/addons/document/document_view.xml +++ b/addons/document/document_view.xml @@ -228,9 +228,11 @@ + + + - @@ -339,15 +341,6 @@ - - ir.attachment.view.inherit - ir.attachment - - - - - - process.node.form From 9c2ca851e23d5f93b59668e633618a6c3cd9f678 Mon Sep 17 00:00:00 2001 From: Tejas Tank Date: Wed, 3 Oct 2012 15:13:41 +0530 Subject: [PATCH 117/443] [IMP] procurement: Products reserverd from stock & added Manufucture Order in procurement. bzr revid: tta@openerp.com-20121003094341-suosaw2sum3grn7k --- addons/mrp/mrp_view.xml | 1 + addons/procurement/procurement.py | 2 +- addons/project_mrp/project_procurement.py | 3 +-- addons/purchase/purchase.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml index a198d736bbc..7294ad043b0 100644 --- a/addons/mrp/mrp_view.xml +++ b/addons/mrp/mrp_view.xml @@ -954,6 +954,7 @@ + diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index d47c3fe350f..ab644dd08db 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -362,7 +362,7 @@ class procurement_order(osv.osv): """ Changes procurement state to Running and writes message. @return: True """ - message = _('From stock: products assigned.') + message = _('Products reserverd from stock.') self.write(cr, uid, ids, {'state': 'running', 'message': message}, context=context) self.message_post(cr, uid, ids, body=message, context=context) diff --git a/addons/project_mrp/project_procurement.py b/addons/project_mrp/project_procurement.py index f70d43426b9..5f9a94f1894 100644 --- a/addons/project_mrp/project_procurement.py +++ b/addons/project_mrp/project_procurement.py @@ -84,8 +84,7 @@ class procurement_order(osv.osv): 'project_id': project and project.id or False, 'company_id': procurement.company_id.id, },context=context) - self.write(cr, uid, [procurement.id], {'task_id': task_id, 'state': 'running', 'message':'from project: task created.'}, context=context) - self.document_send_note(cr, uid, [procurement.id], body='%s %s:%s %s' % (_("Task"), procurement.origin or '', procurement.product_id.name, _("created")), context=context) + self.write(cr, uid, [procurement.id], {'task_id': task_id, 'state': 'running', 'message':'from project: task created.'}, context=context) self.project_task_create_note(cr, uid, ids, context=context) return task_id diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index ed99b9f2a58..4dda418c2ad 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1082,8 +1082,7 @@ class procurement_order(osv.osv): } res[procurement.id] = self.create_procurement_purchase_order(cr, uid, procurement, po_vals, line_vals, context=new_context) self.write(cr, uid, [procurement.id], {'state': 'running', 'purchase_id': res[procurement.id]}) - self.document_send_note(cr, uid, [procurement.id], body='%s %s %s' % (_("Draft Purchase Order"), name or '', _("created")), context=context) - self.purchase_order_create_note(self, cr, uid, ids, context=context) + self.purchase_order_create_note(cr, uid, ids, context=context) return res def purchase_order_create_note(self, cr, uid, ids, context=None): From 60f3b375bbeb598e2ebfc24c340f357b177921f5 Mon Sep 17 00:00:00 2001 From: "Randhir Mayatra (OpenERP)" Date: Wed, 3 Oct 2012 15:20:03 +0530 Subject: [PATCH 118/443] [IMP] make changes into thehr_expense module bzr revid: rma@tinyerp.com-20121003095003-98n5tb4tcscn0o07 --- addons/hr_expense/__openerp__.py | 2 +- addons/hr_expense/hr_expense.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py index 058bf361d50..4b273a9cc81 100644 --- a/addons/hr_expense/__openerp__.py +++ b/addons/hr_expense/__openerp__.py @@ -46,7 +46,7 @@ This module also uses analytic accounting and is compatible with the invoice on 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'], - 'depends': ['hr', 'account_voucher','procurement'], + 'depends': ['hr', 'account_voucher'], 'data': [ 'security/ir.model.access.csv', 'hr_expense_data.xml', diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index 91dc44fed61..61b091d1d51 100644 --- a/addons/hr_expense/hr_expense.py +++ b/addons/hr_expense/hr_expense.py @@ -243,7 +243,7 @@ class product_product(osv.osv): data_obj = self.pool.get('ir.model.data') cat_id = data_obj._get_id(cr, uid, 'hr_expense', 'cat_expense') categ_id = data_obj.browse(cr, uid, cat_id).res_id - res = {'value' : {'type':'service','procure_method':'make_to_stock','supply_method':'buy','purchase_ok':True,'sale_ok' :False,'categ_id':categ_id }} + res = {'value' : {'type':'service','purchase_ok':True,'sale_ok' :False,'categ_id':categ_id }} return res product_product() From 18742c545d3ea3ef57cfb4a64b599ebc3b8c6f3e Mon Sep 17 00:00:00 2001 From: "Randhir Mayatra (OpenERP)" Date: Wed, 3 Oct 2012 15:39:10 +0530 Subject: [PATCH 119/443] [IMP] make changes into project module bzr revid: rma@tinyerp.com-20121003100910-96m1dczfdovlky6j --- addons/project/__openerp__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index 5f549a719b5..85e33be777a 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -39,7 +39,6 @@ 'base_setup', 'base_status', 'product', - 'procurement', 'analytic', 'board', 'mail', From bb4b253dfda091393289c08ddd9822be46b2fb4f Mon Sep 17 00:00:00 2001 From: "Ajay Chauhan (OpenERP)" Date: Wed, 3 Oct 2012 16:06:08 +0530 Subject: [PATCH 120/443] [IMP] mrp: added onchange method for product_uom field bzr revid: cha@tinyerp.com-20121003103608-1uuh3t9mw7i2scif --- addons/mrp/mrp.py | 11 +++++++++++ addons/mrp/mrp_view.xml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index cf203a60c7f..a570842a36e 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -274,6 +274,17 @@ class mrp_bom(osv.osv): return {'value': {'name': prod.name, 'product_uom': prod.uom_id.id}} return {} + def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None): + res = {'value':{}} + if not product_uom or not product_id: + return res + product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) + uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context) + if uom.category_id.id != product.uom_id.category_id.id: + res['warning'] = {'title': _('Warning'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')} + res['value'].update({'product_uom': product.uom_id.id}) + return res + def _bom_find(self, cr, uid, product_id, product_uom, properties=None): """ Finds BoM for particular product and product uom. @param product_id: Selected product. diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml index abf0f823e5c..dca41050430 100644 --- a/addons/mrp/mrp_view.xml +++ b/addons/mrp/mrp_view.xml @@ -351,7 +351,7 @@