From e77abb81fe3af1bad710a199b5aeb4b684754518 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 10 Nov 2011 16:46:14 +0100 Subject: [PATCH 001/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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/342] [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 b8578a099c462e74762bb8f8b3b0c45c6837a33a Mon Sep 17 00:00:00 2001 From: "Khushboo Bhatt (Open ERP)" Date: Wed, 3 Oct 2012 16:43:35 +0530 Subject: [PATCH 120/342] [FIX]product:string inproved bzr revid: kbh@tinyerp.com-20121003111335-ozvs174mhekgy9ax --- addons/product/product_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index 8c19aee026f..5b3af54fd7b 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -135,7 +135,7 @@ - + @@ -197,7 +197,7 @@ - + From a4da52f7f0fb96db30bf40806bc278647b3cb20b Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 16:57:46 +0530 Subject: [PATCH 121/342] [FIX]hr_expense : remove purchase_ok bzr revid: mma@tinyerp.com-20121003112746-sw2ycbrysn65hfjz --- addons/hr_expense/hr_expense.py | 2 +- addons/product/product.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index 61b091d1d51..487e4f58782 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','purchase_ok':True,'sale_ok' :False,'categ_id':categ_id }} + res = {'value' : {'type':'service','sale_ok' :False,'categ_id':categ_id }} return res product_product() diff --git a/addons/product/product.py b/addons/product/product.py index 6bf26a45d24..11a81c23b73 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -350,7 +350,6 @@ class product_template(osv.osv): 'standard_price': lambda *a: 0.0, 'sale_ok': lambda *a: 1, 'produce_delay': lambda *a: 1, - 'purchase_ok': lambda *a: 1, 'uom_id': _get_uom_id, 'uom_po_id': _get_uom_id, 'uos_coeff' : lambda *a: 1.0, From dd44dab68c94cf88b2d9e16e614741680483d508 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Wed, 3 Oct 2012 17:08:10 +0530 Subject: [PATCH 122/342] [FIX] small issues bzr revid: hmo@tinyerp.com-20121003113810-mnlg6rf50xi2ljnt --- addons/mrp/procurement.py | 8 +++++--- addons/procurement/procurement.py | 2 +- addons/project_mrp/project_procurement.py | 6 ++++-- addons/purchase/purchase.py | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index f718856fbf8..82390a2f2c1 100644 --- a/addons/mrp/procurement.py +++ b/addons/mrp/procurement.py @@ -31,7 +31,7 @@ class procurement_order(osv.osv): _columns = { 'bom_id': fields.many2one('mrp.bom', 'BoM', ondelete='cascade', select=True), 'property_ids': fields.many2many('mrp.property', 'procurement_property_rel', 'procurement_id','property_id', 'Properties'), - 'production_id': fields.many2one('mrp.production', 'Manufucture Order'), + 'production_id': fields.many2one('mrp.production', 'Manufacturing Order'), } def check_produce_product(self, cr, uid, procurement, context=None): @@ -80,8 +80,10 @@ class procurement_order(osv.osv): procurement_obj = self.pool.get('procurement.order') for procurement in procurement_obj.browse(cr, uid, ids, context=context): res_id = procurement.move_id.id + #TOFIX: split into new function to compute manufacturing lead time newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.product_id.product_tmpl_id.produce_delay or 0.0) newdate = newdate - relativedelta(days=company.manufacturing_lead) + #TOFIX: implement hook method for creating production order produce_id = production_obj.create(cr, uid, { 'origin': procurement.origin, 'product_id': procurement.product_id.id, @@ -111,8 +113,8 @@ class procurement_order(osv.osv): def production_order_create_note(self, cr, uid, ids, context=None): for procurement in self.browse(cr, uid, ids, context=context): - body = "%s %s %s" % (_("Manufacturing Order"), procurement.production_id.name, _("Created")) - self.message_post(cr, uid, ids, body=body, context=context) + body = "%s %s %s" % (_("Manufacturing Order"), procurement.production_id.name, _("Created")) + self.message_post(cr, uid, [procurement.id], body=body, context=context) procurement_order() diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index aa3b2a2a673..5f8cd3c0b56 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -104,7 +104,7 @@ class procurement_order(osv.osv): " a make to order method."), 'note': fields.text('Note'), - 'message': fields.char('Latest error', size=124, help="Exception occurred while computing procurement orders."), + 'message': fields.char('Latest error', size=124, help="Exception occurred while computing procurement orders."), #TOCHECK: is it need after OpenChatter ? 'state': fields.selection([ ('draft','Draft'), ('cancel','Cancelled'), diff --git a/addons/project_mrp/project_procurement.py b/addons/project_mrp/project_procurement.py index 1daf03dfd9f..63e72493e35 100644 --- a/addons/project_mrp/project_procurement.py +++ b/addons/project_mrp/project_procurement.py @@ -69,8 +69,10 @@ class procurement_order(osv.osv): def action_produce_assign_service(self, cr, uid, ids, context=None): project_task = self.pool.get('project.task') for procurement in self.browse(cr, uid, ids, context=context): + #TOFIX: split into new function to compute task planning hours project = self._get_project(cr, uid, procurement, context=context) planned_hours = self._convert_qty_company_hours(cr, uid, procurement, context=context) + #TOFIX: implement hook method for creating Task task_id = project_task.create(cr, uid, { 'name': '%s:%s' % (procurement.origin or '', procurement.product_id.name), 'date_deadline': procurement.date_planned, @@ -90,8 +92,8 @@ class procurement_order(osv.osv): def project_task_create_note(self, cr, uid, ids, context=None): for procurement in self.browse(cr, uid, ids, context=context): - body = "%s %s %s" % (_("Task"), procurement.task_id.name, _("Created")) - self.message_post(cr, uid, ids, body=body, context=context) + body = "%s %s %s" % (_("Task"), procurement.task_id.name, _("Created")) + self.message_post(cr, uid, [procurement.id], body=body, context=context) procurement_order() diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index a574d8e4467..6bc76f14f34 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1083,8 +1083,8 @@ class procurement_order(osv.osv): def purchase_order_create_note(self, cr, uid, ids, context=None): for procurement in self.browse(cr, uid, ids, context=context): - body = "%s %s %s" % (_("Draft Purchase Order"), procurement.purchase_id.name, _("Created")) - self.message_post(cr, uid, ids, body=body, context=context) + body = "%s %s %s" % (_("Draft Purchase Order"), procurement.purchase_id.name, _("Created")) + self.message_post(cr, uid, [procurement.id], body=body, context=context) procurement_order() From be42204adf876f675af5e7466b0d57efbb840d6c Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 17:08:46 +0530 Subject: [PATCH 123/342] [FIX]product : remove lemda from defaults bzr revid: mma@tinyerp.com-20121003113846-mjjtghoxdu8h2nq8 --- addons/procurement/procurement.py | 4 ++-- addons/product/product.py | 16 ++++++++-------- addons/purchase/purchase.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index afa4eb2af0b..3d5e166b082 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -649,8 +649,8 @@ class product_template(osv.osv): 'supply_method': fields.selection([('produce','Manufacture'),('buy','Buy')], 'Supply Method', required=True, help="Produce will generate production order or tasks, according to the product type. Buy will trigger purchase orders when requested."), } _defaults = { - 'procure_method': lambda *a: 'make_to_stock', - 'supply_method': lambda *a: 'buy', + 'procure_method': 'make_to_stock', + 'supply_method': 'buy', } product_template() diff --git a/addons/product/product.py b/addons/product/product.py index 11a81c23b73..d23cd37ae69 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -345,17 +345,17 @@ class product_template(osv.osv): _defaults = { 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'product.template', context=c), - 'list_price': lambda *a: 1, - 'cost_method': lambda *a: 'standard', - 'standard_price': lambda *a: 0.0, - 'sale_ok': lambda *a: 1, - 'produce_delay': lambda *a: 1, + 'list_price': 1, + 'cost_method': 'standard', + 'standard_price': 0.0, + 'sale_ok': 1, + 'produce_delay': 1, 'uom_id': _get_uom_id, 'uom_po_id': _get_uom_id, - 'uos_coeff' : lambda *a: 1.0, - 'mes_type' : lambda *a: 'fixed', + 'uos_coeff' : 1.0, + 'mes_type' : 'fixed', 'categ_id' : _default_category, - 'type' : lambda *a: 'consu', + 'type' : 'consu', } def _check_uom(self, cursor, user, ids, context=None): diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 6643394b148..abb45ae0909 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1102,7 +1102,7 @@ class product_template(osv.osv): 'purchase_ok': fields.boolean('Can be Purchased', help="Determine if the product is visible in the list of products within a selection from a purchase order line."), } _defaults = { - 'purchase_ok': lambda *a: 1, + 'purchase_ok': 1, } product_template() From cfb01972363037e7ffcc40d90812acaa19314b2c Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 17:12:08 +0530 Subject: [PATCH 124/342] [FIX]product : remove lemda from defaults bzr revid: mma@tinyerp.com-20121003114208-p6p0xvecuc1vfb0e --- addons/stock/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/product.py b/addons/stock/product.py index e93258b56cd..94252f80439 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -520,7 +520,7 @@ class product_template(osv.osv): } _defaults = { - 'sale_delay': lambda *a: 7, + 'sale_delay': 7, } product_template() From cc331b802c0e7c37e4168dbd144c8590c1d27af1 Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 17:13:28 +0530 Subject: [PATCH 125/342] [FIX]product : remove sale_dealy from product.template bzr revid: mma@tinyerp.com-20121003114328-9ufkrn1d84vyny5q --- addons/product/product.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/product/product.py b/addons/product/product.py index d23cd37ae69..42f7d003079 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -281,7 +281,6 @@ class product_template(osv.osv): 'description_purchase': fields.text('Purchase Description',translate=True), 'description_sale': fields.text('Sale Description',translate=True), 'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Will change the way procurements are processed. Consumable are product where you don't manage stock."), - 'sale_delay': fields.float('Customer Lead Time', help="This is the average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."), 'produce_delay': fields.float('Manufacturing Lead Time', help="Average delay in days to produce this product. This is only for the production order and, if it is a multi-level bill of material, it's only for the level of this product. Different lead times will be summed for all levels and purchase orders."), 'rental': fields.boolean('Can be Rent'), 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"), From b9b19f61eab893ea27f16dcdf5c15621bf0c7636 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Oct 2012 13:59:49 +0200 Subject: [PATCH 126/342] [FIX] if a selection label is empty, return the value in a selection import message bzr revid: xmo@openerp.com-20121003115949-sgsouhcmboascjbl --- openerp/addons/base/ir/ir_fields.py | 3 ++- openerp/tests/addons/test_impex/models.py | 2 +- openerp/tests/addons/test_impex/tests/test_load.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 417fbc9b4db..65d4a786178 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -168,7 +168,8 @@ class ir_fields_converter(orm.Model): raise ValueError( _(u"Value '%s' not found in selection field '%%(field)s'") % ( value), { - 'moreinfo': map(operator.itemgetter(1), selection) + 'moreinfo': [label or unicode(item) for item, label in selection + if label or item] }) diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py index c56ddc649e2..3c85b56fff7 100644 --- a/openerp/tests/addons/test_impex/models.py +++ b/openerp/tests/addons/test_impex/models.py @@ -22,7 +22,7 @@ models = [ ('date', fields.date()), ('datetime', fields.datetime()), ('text', fields.text()), - ('selection', fields.selection([(1, "Foo"), (2, "Bar"), (3, "Qux")])), + ('selection', fields.selection([(1, "Foo"), (2, "Bar"), (3, "Qux"), (4, '')])), ('selection.function', fields.selection(selection_fn)), # just relate to an integer ('many2one', fields.many2one('export.integer')), diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 7657709d24a..18b26a16354 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -466,13 +466,13 @@ class test_selection(ImporterCase): self.assertIs(result['ids'], False) self.assertEqual(result['messages'], [message( u"Value 'Baz' not found in selection field 'unknown'", - moreinfo="Foo Bar Qux".split())]) + moreinfo="Foo Bar Qux 4".split())]) 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())]) + moreinfo="Foo Bar Qux 4".split())]) class test_selection_function(ImporterCase): model_name = 'export.selection.function' From f6544c801fa070403ce122d47c90644b50d8db58 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Oct 2012 14:11:37 +0200 Subject: [PATCH 127/342] [IMP] make import into a full-scene client action * Correctly handle back (via history_back) * Disable buttons when no file has been loaded yet * Highlight import when a validation has yield no message bzr revid: xmo@openerp.com-20121003121137-xo7lu5m5y0s7gyo1 --- addons/base_import/static/src/js/import.js | 74 +++++++++++--------- addons/base_import/static/src/xml/import.xml | 10 +++ 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index 0ea74e43755..dd8b3288de9 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -47,16 +47,23 @@ openerp.base_import = function (instance) { this._super.apply(this, arguments); if(add_button) { this.$buttons.on('click', '.oe_list_button_import', function() { - new instance.web.DataImport(self, self.dataset).open(); + self.do_action({ + type: 'ir.actions.client', + tag: 'import', + params: { + model: self.dataset.model + } + }); return false; }); } } }); - instance.web.DataImport = instance.web.Dialog.extend({ + instance.web.client_actions.add( + 'import', 'instance.web.DataImport'); + instance.web.DataImport = instance.web.Widget.extend({ template: 'ImportView', - dialog_title: _lt("Import Data"), opts: [ {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'}, {name: 'separator', label: _lt("Separator:"), value: ','}, @@ -100,41 +107,34 @@ openerp.base_import = function (instance) { ]; }); this.do_action(_.extend(action, {target: 'new'})); + }, + // buttons + 'click .oe_import_validate': 'import_dryrun', + 'click .oe_import_import': 'do_import', + 'click .oe_import_cancel': function (e) { + e.preventDefault(); + this.exit(); } }, - init: function (parent, dataset) { - var self = this; - this._super(parent, { - buttons: [ - {text: _t("Validate"), click: self.proxy('import_dryrun'), - 'class': 'oe_import_dialog_button'}, - {text: _t("Import"), click: self.proxy('do_import'), - 'class': 'oe_import_dialog_button oe_highlight'} - ] - }); - this.res_model = parent.model; + init: function (parent, params) { + this._super.apply(this, arguments); + this.res_model = params.model; // import object id this.id = null; this.Import = new instance.web.Model('base_import.import'); }, start: function () { var self = this; - return this.Import.call('create', [{ - 'res_model': this.res_model - }]).then(function (id) { - self.id = id; - self.$('input[name=import_id]').val(id); - }); - }, - open: function () { - this._super.apply(this, arguments); - this.$el.dialog('widget').find('.ui-dialog-buttonset') - .append(_t(" or ")) - .append( - $(' + + or + Cancel + From e8a7d276bf700b3c880d4e961cfac1ea0b40c690 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Oct 2012 14:16:19 +0200 Subject: [PATCH 128/342] [IMP] highlight toggling between validation and import buttons bzr revid: xmo@openerp.com-20121003121619-e6nbd78mfa5owycp --- addons/base_import/static/src/js/import.js | 6 ++++-- addons/base_import/static/src/xml/import.xml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index dd8b3288de9..4135f303513 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -325,8 +325,10 @@ openerp.base_import = function (instance) { }); }, render_import_result: function (message) { - if (_.isEmpty(message)) { - this.$('.oe_import_import').addClass('oe_highlight'); + var no_messages = _.isEmpty(message); + this.$('.oe_import_import').toggleClass('oe_highlight', no_messages); + this.$('.oe_import_validate').toggleClass('oe_highlight', !no_messages); + if (no_messages) { message.push({ type: 'info', message: _t("Everything seems valid.") diff --git a/addons/base_import/static/src/xml/import.xml b/addons/base_import/static/src/xml/import.xml index aab9482ee81..b0eea1fcf75 100644 --- a/addons/base_import/static/src/xml/import.xml +++ b/addons/base_import/static/src/xml/import.xml @@ -4,7 +4,7 @@
""" - _header_a4 = _header_main % ('23.0cm', '27.6cm', '27.7cm', '27.7cm', '27.8cm', '27.2cm', '26.8cm', '26.0cm', '26.0cm', '25.6cm', '25.6cm', '25.5cm', '25.5cm') + _header_a4 = _header_main % ('23.0cm', '27.6cm', '27.7cm', '27.7cm', '27.8cm', '27.4cm', '25.8cm', '26.0cm', '26.0cm', '25.6cm', '25.6cm', '25.5cm', '25.5cm') _header_letter = _header_main % ('21.3cm', '25.9cm', '26.0cm', '26.0cm', '26.1cm', '25.5cm', '25.1cm', '24.3cm', '24.3cm', '23.9cm', '23.9cm', '23.8cm', '23.8cm') def onchange_paper_format(self, cr, uid, ids, paper_format, context=None): From 8169ad64cffae017db28b3cc4c03c712a34711f9 Mon Sep 17 00:00:00 2001 From: "Foram Katharotiya (OpenERP)" Date: Wed, 3 Oct 2012 17:56:37 +0530 Subject: [PATCH 131/342] [IMP] display applicant's name above the subject in kanban of application bzr revid: fka@tinyerp.com-20121003122637-74o8xxuxoxav876x --- addons/hr_recruitment/hr_recruitment_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index 08e5f31a2d2..ccc91da2b0e 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -289,6 +289,7 @@
+


Mobile:
From 7c9ad497897f41ff20823740f671e7d9cc5238b8 Mon Sep 17 00:00:00 2001 From: "Foram Katharotiya (OpenERP)" Date: Wed, 3 Oct 2012 18:19:11 +0530 Subject: [PATCH 132/342] [IMP] in Payslips: change journal_id label bzr revid: fka@tinyerp.com-20121003124911-o792xmzwj6tswt0e --- addons/hr_payroll_account/hr_payroll_account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/hr_payroll_account/hr_payroll_account.py b/addons/hr_payroll_account/hr_payroll_account.py index 184f9a06867..94df47f6e44 100644 --- a/addons/hr_payroll_account/hr_payroll_account.py +++ b/addons/hr_payroll_account/hr_payroll_account.py @@ -36,7 +36,7 @@ class hr_payslip(osv.osv): _columns = { 'period_id': fields.many2one('account.period', 'Force Period',states={'draft': [('readonly', False)]}, readonly=True, domain=[('state','<>','done')], help="Keep empty to use the period of the validation(Payslip) date."), - 'journal_id': fields.many2one('account.journal', 'Expense Journal',states={'draft': [('readonly', False)]}, readonly=True, required=True), + 'journal_id': fields.many2one('account.journal', 'Salary Journal',states={'draft': [('readonly', False)]}, readonly=True, required=True), 'move_id': fields.many2one('account.move', 'Accounting Entry', readonly=True), } @@ -215,7 +215,7 @@ class hr_payslip_run(osv.osv): _inherit = 'hr.payslip.run' _description = 'Payslip Run' _columns = { - 'journal_id': fields.many2one('account.journal', 'Expense Journal', states={'draft': [('readonly', False)]}, readonly=True, required=True), + 'journal_id': fields.many2one('account.journal', 'Salary Journal', states={'draft': [('readonly', False)]}, readonly=True, required=True), } def _get_default_journal(self, cr, uid, context=None): From 2b1384dcfcdef5a336eee4864a7185c4e41072eb Mon Sep 17 00:00:00 2001 From: Vishmita Date: Wed, 3 Oct 2012 18:22:38 +0530 Subject: [PATCH 133/342] [Fix]remove delete icon from attachment for google doc bzr revid: vja@tinyerp.com-20121003125238-dnhytzvxj13gnsrf --- addons/web/static/src/xml/base.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 3d979186aa3..f4c5de237ac 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -554,7 +554,7 @@ - x + x
  • From 98d979529d021383867be7086467018ceba23d50 Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 18:44:14 +0530 Subject: [PATCH 134/342] [IMP]event_sale : improve view bzr revid: mma@tinyerp.com-20121003131414-v6e0srgum29xdyjx --- addons/event_sale/event_sale_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/event_sale/event_sale_view.xml b/addons/event_sale/event_sale_view.xml index 174f82f9b6b..ab7568d704e 100644 --- a/addons/event_sale/event_sale_view.xml +++ b/addons/event_sale/event_sale_view.xml @@ -4,7 +4,7 @@ event.product product.product - +
    From 4858fff4ca67af7e59956750d85b1f7e042d8da1 Mon Sep 17 00:00:00 2001 From: "Khushboo Bhatt (Open ERP)" Date: Wed, 3 Oct 2012 18:45:43 +0530 Subject: [PATCH 135/342] [FIX]stock:err of attrs when select any field from group by in search view bzr revid: kbh@tinyerp.com-20121003131543-01aszl4xgujw8sac --- addons/stock/stock_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index f5320a963c7..f66f24ae551 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -1119,7 +1119,7 @@ stock.move - + From 6c908a4b221e7c809c0e6d852cf8bf108064e8be Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 3 Oct 2012 18:52:00 +0530 Subject: [PATCH 136/342] [IMP]event_sale : improve templates view bzr revid: mma@tinyerp.com-20121003132200-bxk7i0thj3prqymp --- addons/product/product_view.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index 32d6711d386..14571fbcc55 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -709,11 +709,19 @@ + + + + + + + + From fa2bc110258b48842fa95d370440b9399dcf8571 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Oct 2012 16:50:48 +0200 Subject: [PATCH 137/342] [IMP] formalize state transitions via an actual fsm bzr revid: xmo@openerp.com-20121003145048-wdoo712j551jow3t --- addons/base_import/__openerp__.py | 1 + .../lib/javascript-state-machine/LICENSE | 20 ++ .../lib/javascript-state-machine/README.md | 327 ++++++++++++++++++ .../javascript-state-machine/RELEASE_NOTES.md | 32 ++ .../lib/javascript-state-machine/Rakefile | 8 + .../lib/javascript-state-machine/index.html | 39 +++ .../javascript-state-machine/state-machine.js | 155 +++++++++ addons/base_import/static/src/js/import.js | 90 +++-- 8 files changed, 638 insertions(+), 34 deletions(-) create mode 100644 addons/base_import/static/lib/javascript-state-machine/LICENSE create mode 100644 addons/base_import/static/lib/javascript-state-machine/README.md create mode 100644 addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md create mode 100644 addons/base_import/static/lib/javascript-state-machine/Rakefile create mode 100644 addons/base_import/static/lib/javascript-state-machine/index.html create mode 100644 addons/base_import/static/lib/javascript-state-machine/state-machine.js diff --git a/addons/base_import/__openerp__.py b/addons/base_import/__openerp__.py index eff20bc90ab..bb527e7b49d 100644 --- a/addons/base_import/__openerp__.py +++ b/addons/base_import/__openerp__.py @@ -33,6 +33,7 @@ Re-implement openerp's file import system: ], 'js': [ 'static/lib/select2/select2.js', + 'static/lib/javascript-state-machine/state-machine.js', 'static/src/js/import.js', ], 'qweb': ['static/src/xml/import.xml'], diff --git a/addons/base_import/static/lib/javascript-state-machine/LICENSE b/addons/base_import/static/lib/javascript-state-machine/LICENSE new file mode 100644 index 00000000000..8ad703ca4ef --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Jake Gordon and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/addons/base_import/static/lib/javascript-state-machine/README.md b/addons/base_import/static/lib/javascript-state-machine/README.md new file mode 100644 index 00000000000..64c045e69d7 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/README.md @@ -0,0 +1,327 @@ +Javascript Finite State Machine (v2.1.0) +======================================== + +This standalone javascript micro-framework provides a finite state machine for your pleasure. + + * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine) + * You can find a [description here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/) + +Download +======== + +You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js), +or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js) + +Alternatively: + + git clone git@github.com:jakesgordon/javascript-state-machine + + + * All code is in state-machine.js + * Minified version provided in state-machine.min.js + * No 3rd party library is required + * Demo can be found in /index.html + * QUnit tests can be found in /test/index.html + +Usage +===== + +Include `state-machine.min.js` in your application. + +In its simplest form, create a standalone state machine using: + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ]}); + +... will create an object with a method for each event: + + * fsm.warn() - transition from 'green' to 'yellow' + * fsm.panic() - transition from 'yellow' to 'red' + * fsm.calm() - transition from 'red' to 'yellow' + * fsm.clear() - transition from 'yellow' to 'green' + +along with the following members: + + * fsm.current - contains the current state + * fsm.is(s) - return true if state `s` is the current state + * fsm.can(e) - return true if event `e` can be fired in the current state + * fsm.cannot(e) - return true if event `e` cannot be fired in the current state + +Multiple 'from' and 'to' states for a single event +================================================== + +If an event is allowed **from** multiple states, and always transitions to the same +state, then simply provide an array of states in the `from` attribute of an event. However, +if an event is allowed from multiple states, but should transition **to** a different +state depending on the current state, then provide multiple event entries with +the same name: + + var fsm = StateMachine.create({ + initial: 'hungry', + events: [ + { name: 'eat', from: 'hungry', to: 'satisfied' }, + { name: 'eat', from: 'satisfied', to: 'full' }, + { name: 'eat', from: 'full', to: 'sick' }, + { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, + ]}); + +This example will create an object with 2 event methods: + + * fsm.eat() + * fsm.rest() + +The `rest` event will always transition to the `hungry` state, while the `eat` event +will transition to a state that is dependent on the current state. + +>> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be +allowed from any current state. + +>> NOTE: The `rest` event in the above example can also be specified as multiple events with +the same name if you prefer the verbose approach. + +Callbacks +========= + +4 callbacks are available if your state machine has methods using the following naming conventions: + + * onbefore**event** - fired before the event + * onleave**state** - fired when leaving the old state + * onenter**state** - fired when entering the new state + * onafter**event** - fired after the event + +You can affect the event in 3 ways: + + * return `false` from an `onbeforeevent` handler to cancel the event. + * return `false` from an `onleavestate` handler to cancel the event. + * return `ASYNC` from an `onleavestate` handler to perform an asynchronous state transition (see next section) + +For convenience, the 2 most useful callbacks can be shortened: + + * on**event** - convenience shorthand for onafter**event** + * on**state** - convenience shorthand for onenter**state** + +In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes: + +All callbacks will be passed the same arguments: + + * **event** name + * **from** state + * **to** state + * _(followed by any arguments you passed into the original event method)_ + +Callbacks can be specified when the state machine is first created: + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ], + callbacks: { + onpanic: function(event, from, to, msg) { alert('panic! ' + msg); }, + onclear: function(event, from, to, msg) { alert('thanks to ' + msg); }, + ongreen: function(event, from, to) { document.body.className = 'green'; }, + onyellow: function(event, from, to) { document.body.className = 'yellow'; }, + onred: function(event, from, to) { document.body.className = 'red'; }, + } + }); + + fsm.panic('killer bees'); + fsm.clear('sedatives in the honey pots'); + ... + +Additionally, they can be added and removed from the state machine at any time: + + fsm.ongreen = null; + fsm.onyellow = null; + fsm.onred = null; + fsm.onchangestate = function(event, from, to) { document.body.className = to; }; + +Asynchronous State Transitions +============================== + +Sometimes, you need to execute some asynchronous code during a state transition and ensure the +new state is not entered until your code has completed. + +A good example of this is when you transition out of a `menu` state, perhaps you want to gradually +fade the menu away, or slide it off the screen and don't want to transition to your `game` state +until after that animation has been performed. + +You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine +will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()` +method. + +For example, using jQuery effects: + + var fsm = StateMachine.create({ + + initial: 'menu', + + events: [ + { name: 'play', from: 'menu', to: 'game' }, + { name: 'quit', from: 'game', to: 'menu' } + ], + + callbacks: { + + onentermenu: function() { $('#menu').show(); }, + onentergame: function() { $('#game').show(); }, + + onleavemenu: function() { + $('#menu').fadeOut('fast', function() { + fsm.transition(); + }); + return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above) + }, + + onleavegame: function() { + $('#game').slideDown('slow', function() { + fsm.transition(); + }; + return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above) + } + + } + }); + + +State Machine Classes +===================== + +You can also turn all instances of a _class_ into an FSM by applying +the state machine functionality to the prototype, including your callbacks +in your prototype, and providing a `startup` event for use when constructing +instances: + + MyFSM = function() { // my constructor function + this.startup(); + }; + + MyFSM.prototype = { + + onpanic: function(event, from, to) { alert('panic'); }, + onclear: function(event, from, to) { alert('all is clear'); }, + + // my other prototype methods + + }; + + StateMachine.create({ + target: MyFSM.prototype, + events: [ + { name: 'startup', from: 'none', to: 'green' }, + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ]}); + + +This should be easy to adjust to fit your appropriate mechanism for object construction. + +Initialization Options +====================== + +How the state machine should initialize can depend on your application requirements, so +the library provides a number of simple options. + +By default, if you dont specify any initial state, the state machine will be in the `'none'` +state and you would need to provide an event to take it out of this state: + + var fsm = StateMachine.create({ + events: [ + { name: 'startup', from: 'none', to: 'green' }, + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "none" + fsm.startup(); + alert(fsm.current); // "green" + +If you specify the name of your initial event (as in all the earlier examples), then an +implicit `startup` event will be created for you and fired when the state machine is constructed. + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "green" + +If your object already has a `startup` method you can use a different name for the initial event + + var fsm = StateMachine.create({ + initial: { state: 'green', event: 'init' }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "green" + +Finally, if you want to wait to call the initial state transition event until a later date you +can `defer` it: + + var fsm = StateMachine.create({ + initial: { state: 'green', event: 'init', defer: true }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "none" + fsm.init(); + alert(fsm.current); // "green" + +Of course, we have now come full circle, this last example is pretty much functionally the +same as the first example in this section where you simply define your own startup event. + +So you have a number of choices available to you when initializing your state machine. + +Handling Failures +====================== + +By default, if you try to call an event method that is not allowed in the current state, the +state machine will throw an exception. If you prefer to handle the problem yourself, you can +define a custom `error` handler: + + var fsm = StateMachine.create({ + initial: 'green', + error: function(eventName, from, to, args, errorCode, errorMessage) { + return 'event ' + eventName + ' was naughty :- ' + errorMessage; + }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green" + +Release Notes +============= + +See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file. + +License +======= + +See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file. + +Contact +======= + +If you have any ideas, feedback, requests or bug reports, you can reach me at +[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via +my website: [Code inComplete](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + + + + + diff --git a/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md new file mode 100644 index 00000000000..06abf402d38 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md @@ -0,0 +1,32 @@ +Version 2.1.0 (January 7th 2012) +-------------------------------- + + * Wrapped in self executing function to be more easily used with loaders like `require.js` or `curl.js` (issue #15) + * Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`) + * Added explicit return values for event methods (issue #12) + * Added support for wildcard events that can be fired 'from' any state (issue #11) + * Added support for no-op events that transition 'to' the same state (issue #5) + * extended custom error callback to handle any exceptions caused by caller provided callbacks + * added custom error callback to override exception when an illegal state transition is attempted (thanks to cboone) + * fixed typos (thanks to cboone) + * fixed issue #4 - ensure before/after event hooks are called even if the event doesn't result in a state change + +Version 2.0.0 (August 19th 2011) +-------------------------------- + + * adding support for asynchronous state transitions (see README) - with lots of qunit tests (see test/async.js). + * consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method. + * added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function. + * allow callbacks to be declared at creation time (instead of having to attach them afterwards) + * renamed 'hooks' => 'callbacks' + * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + +Version 1.2.0 (June 21st 2011) +------------------------------ + * allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md) + * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/) + +Version 1.0.0 (June 1st 2011) +----------------------------- + * initial version + * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/) diff --git a/addons/base_import/static/lib/javascript-state-machine/Rakefile b/addons/base_import/static/lib/javascript-state-machine/Rakefile new file mode 100644 index 00000000000..beb8702a7f0 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/Rakefile @@ -0,0 +1,8 @@ + +desc "create minified version of state-machine.js" +task :minify do + require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier')) + Minifier.enabled = true + Minifier.minify('state-machine.js') +end + diff --git a/addons/base_import/static/lib/javascript-state-machine/index.html b/addons/base_import/static/lib/javascript-state-machine/index.html new file mode 100644 index 00000000000..2d6cb62617b --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/index.html @@ -0,0 +1,39 @@ + + + + Javascript Finite State Machine + + + + + + +
    + +

    Finite State Machine

    + +
    + + + + +
    + +
    +
    + +
    + dashed lines are asynchronous state transitions (3 seconds) +
    + + + +
    + + + + + + + diff --git a/addons/base_import/static/lib/javascript-state-machine/state-machine.js b/addons/base_import/static/lib/javascript-state-machine/state-machine.js new file mode 100644 index 00000000000..0d503ee7bdb --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/state-machine.js @@ -0,0 +1,155 @@ +(function (window) { + + StateMachine = { + + //--------------------------------------------------------------------------- + + VERSION: "2.1.0", + + //--------------------------------------------------------------------------- + + Result: { + SUCCEEDED: 1, // the event transitioned successfully from one state to another + NOTRANSITION: 2, // the event was successfull but no state transition was necessary + CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback + ASYNC: 4, // the event is asynchronous and the caller is in control of when the transition occurs + }, + + Error: { + INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state + PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending + INVALID_CALLBACK: 300, // caller provided callback function threw an exception + }, + + WILDCARD: '*', + ASYNC: 'async', + + //--------------------------------------------------------------------------- + + create: function(cfg, target) { + + var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false } + var fsm = target || cfg.target || {}; + var events = cfg.events || []; + var callbacks = cfg.callbacks || {}; + var map = {}; + + var add = function(e) { + var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified + map[e.name] = map[e.name] || {}; + for (var n = 0 ; n < from.length ; n++) + map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified + }; + + if (initial) { + initial.event = initial.event || 'startup'; + add({ name: initial.event, from: 'none', to: initial.state }); + } + + for(var n = 0 ; n < events.length ; n++) + add(events[n]); + + for(var name in map) { + if (map.hasOwnProperty(name)) + fsm[name] = StateMachine.buildEvent(name, map[name]); + } + + for(var name in callbacks) { + if (callbacks.hasOwnProperty(name)) + fsm[name] = callbacks[name] + } + + fsm.current = 'none'; + fsm.is = function(state) { return this.current == state; }; + fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } + fsm.cannot = function(event) { return !this.can(event); }; + fsm.error = cfg.error || function(name, from, to, args, error, msg) { throw msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3) + + if (initial && !initial.defer) + fsm[initial.event](); + + return fsm; + + }, + + //=========================================================================== + + doCallback: function(fsm, func, name, from, to, args) { + if (func) { + try { + return func.apply(fsm, [name, from, to].concat(args)); + } + catch(e) { + return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function"); + } + } + }, + + beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); }, + afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); }, + leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); }, + enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); }, + changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); }, + + + buildEvent: function(name, map) { + return function() { + + var from = this.current; + var to = map[from] || map[StateMachine.WILDCARD] || from; + var args = Array.prototype.slice.call(arguments); // turn arguments into pure array + + if (this.transition) + return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); + + if (this.cannot(name)) + return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); + + if (false === StateMachine.beforeEvent(this, name, from, to, args)) + return StateMachine.CANCELLED; + + if (from === to) { + StateMachine.afterEvent(this, name, from, to, args); + return StateMachine.NOTRANSITION; + } + + // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) + var fsm = this; + this.transition = function() { + fsm.transition = null; // this method should only ever be called once + fsm.current = to; + StateMachine.enterState( fsm, name, from, to, args); + StateMachine.changeState(fsm, name, from, to, args); + StateMachine.afterEvent( fsm, name, from, to, args); + }; + + var leave = StateMachine.leaveState(this, name, from, to, args); + if (false === leave) { + this.transition = null; + return StateMachine.CANCELLED; + } + else if ("async" === leave) { + return StateMachine.ASYNC; + } + else { + if (this.transition) + this.transition(); // in case user manually called transition() but forgot to return ASYNC + return StateMachine.SUCCEEDED; + } + + }; + } + + }; // StateMachine + + //=========================================================================== + + if ("function" === typeof define) { + define("statemachine", [], function() { return StateMachine; }); + } + else { + window.StateMachine = StateMachine; + } + +}(this)); + diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index 4135f303513..19d4e466cc7 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -71,14 +71,8 @@ openerp.base_import = function (instance) { ], events: { // 'change .oe_import_grid input': 'import_dryrun', - 'change input.oe_import_file': 'file_update', - 'change input.oe_import_has_header, .oe_import_options input': 'settings_updated', - 'click a.oe_import_csv': function (e) { - e.preventDefault(); - }, - 'click a.oe_import_export': function (e) { - e.preventDefault(); - }, + 'change input.oe_import_file': 'loaded_file', + 'change input.oe_import_has_header, .oe_import_options input': 'settings_changed', 'click a.oe_import_toggle': function (e) { e.preventDefault(); var $el = $(e.target); @@ -109,14 +103,15 @@ openerp.base_import = function (instance) { this.do_action(_.extend(action, {target: 'new'})); }, // buttons - 'click .oe_import_validate': 'import_dryrun', - 'click .oe_import_import': 'do_import', + 'click .oe_import_validate': 'validate', + 'click .oe_import_import': 'import', 'click .oe_import_cancel': function (e) { e.preventDefault(); this.exit(); } }, init: function (parent, params) { + var self = this; this._super.apply(this, arguments); this.res_model = params.model; // import object id @@ -150,42 +145,48 @@ openerp.base_import = function (instance) { }, //- File & settings change section - file_update: function (e) { + onfile_loaded: function () { this.$('.oe_import_button').prop('disabled', true); if (!this.$('input.oe_import_file').val()) { return; } this.$el.removeClass('oe_import_preview oe_import_error'); jsonp(this.$el, { url: '/base_import/set_file' - }, this.proxy('settings_updated')); + }, this.proxy('settings_changed')); }, - settings_updated: function () { + onpreviewing: function () { + var self = this; + this.$('.oe_import_button').prop('disabled', true); this.$el.addClass('oe_import_with_file'); // TODO: test that write // succeeded? - this.Import.call( - 'parse_preview', [this.id, this.import_options()]) - .then(this.proxy('preview')); - }, - preview: function (result) { this.$el.removeClass('oe_import_preview_error oe_import_error'); this.$el.toggleClass( 'oe_import_noheaders', !this.$('input.oe_import_has_header').prop('checked')); - if (result.error) { - this.$('.oe_import_options').show(); - this.$el.addClass('oe_import_preview_error oe_import_error'); - this.$('.oe_import_error_report').html( - QWeb.render('ImportView.preview.error', result)) - .get(0).scrollIntoView(); - return; - } + this.Import.call( + 'parse_preview', [this.id, this.import_options()]) + .then(function (result) { + var signal = result.error ? 'preview_failed' : 'preview_succeeded'; + self[signal](result); + }); + }, + onpreview_error: function (event, from, to, result) { + this.$('.oe_import_options').show(); + this.$el.addClass('oe_import_preview_error oe_import_error'); + this.$('.oe_import_error_report').html( + QWeb.render('ImportView.preview.error', result)) + .get(0).scrollIntoView(); + }, + onpreview_success: function (event, from, to, result) { + this.$('.oe_import_import').removeClass('oe_highlight'); + this.$('.oe_import_validate').addClass('oe_highlight'); this.$('.oe_import_button').prop('disabled', false); this.$el.addClass('oe_import_preview'); this.$('table').html(QWeb.render('ImportView.preview', result)); if (result.headers.length === 1) { this.$('.oe_import_options').show(); - this.render_import_result([{ + this.onresults(null, null, null, [{ type: 'warning', message: _t("A single column was found in the file, this often means the file separator is incorrect") }]); @@ -225,7 +226,6 @@ openerp.base_import = function (instance) { width: 'resolve', dropdownCssClass: 'oe_import_selector' }); - //this.import_dryrun(); }, generate_fields_completion: function (root) { var basic = []; @@ -303,28 +303,31 @@ openerp.base_import = function (instance) { return this.Import.call( 'do', [this.id, fields, this.import_options()], options); }, - import_dryrun: function () { + onvalidate: function () { return this.call_import({ dryrun: true }) - .then(this.proxy('render_import_result')); + .then(this.proxy('validated')); }, - do_import: function () { + onimport: function () { var self = this; return this.call_import({ dryrun: false }).then(function (message) { if (!_.any(message, function (message) { return message.type === 'error' })) { - self.exit(); + self['import_succeeded'](); return; } - self.render_import_result(message); + self['import_failed'](message); }); }, + onimported: function () { + this.exit(); + }, exit: function () { this.do_action({ type: 'ir.actions.client', tag: 'history_back' }); }, - render_import_result: function (message) { + onresults: function (event, from, to, message) { var no_messages = _.isEmpty(message); this.$('.oe_import_import').toggleClass('oe_highlight', no_messages); this.$('.oe_import_validate').toggleClass('oe_highlight', !no_messages); @@ -386,4 +389,23 @@ openerp.base_import = function (instance) { })).get(0).scrollIntoView(); }, }); + // FSM-ize DataImport + StateMachine.create({ + target: instance.web.DataImport.prototype, + events: [ + { name: 'loaded_file', + from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'file_loaded' }, + { name: 'settings_changed', + from: ['file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'previewing' }, + { name: 'preview_failed', from: 'previewing', to: 'preview_error' }, + { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' }, + { name: 'validate', from: 'preview_success', to: 'validating' }, + { name: 'validated', from: 'validating', to: 'results' }, + { name: 'import', from: ['preview_success', 'results'], to: 'importing' }, + { name: 'import_succeeded', from: 'importing', to: 'imported'}, + { name: 'import_failed', from: 'importing', to: 'results' } + ] + }) }; From 9c9d9781093cf35bb5abec0d605a02bbd9b3f108 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 3 Oct 2012 17:24:44 +0200 Subject: [PATCH 138/342] [IMP] relational field 'more values' dialog list features force display of pager and search view, make list non-selectable bzr revid: xmo@openerp.com-20121003152444-phnn9sc9nqo1ecpn --- addons/base_import/static/src/js/import.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index 19d4e466cc7..2867242749c 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -100,7 +100,15 @@ openerp.base_import = function (instance) { : 'tree' ]; }); - this.do_action(_.extend(action, {target: 'new'})); + this.do_action(_.extend(action, { + target: 'new', + flags: { + search_view: true, + display_title: true, + pager: true, + list: {selectable: false} + } + })); }, // buttons 'click .oe_import_validate': 'validate', From 082f696ef47eceb19e3ce8d82d10440aee80004f Mon Sep 17 00:00:00 2001 From: Tejas Tank Date: Thu, 4 Oct 2012 11:41:32 +0530 Subject: [PATCH 139/342] UOM, List view: remove the ratio column. bzr revid: tta@openerp.com-20121004061132-0p5o9qcfpo0xyvzh --- addons/product/product_view.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index 8c19aee026f..2509abfec4e 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -449,7 +449,6 @@ -
    From 601568f5f9431338886aa1aeda240a7137f16173 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 4 Oct 2012 08:42:15 +0200 Subject: [PATCH 140/342] [FIX] don't import empty cells at all rather than set them to False, to allow defaults handling to do its job before actually creating the record bzr revid: xmo@openerp.com-20121004064215-fqgir3ovmte2v438 --- openerp/osv/orm.py | 1 - openerp/tests/addons/test_impex/models.py | 10 ++++++++++ .../tests/addons/test_impex/tests/test_load.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4f21a159cdd..b0b0fad88fa 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1501,7 +1501,6 @@ 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 diff --git a/openerp/tests/addons/test_impex/models.py b/openerp/tests/addons/test_impex/models.py index 3c85b56fff7..50340082d08 100644 --- a/openerp/tests/addons/test_impex/models.py +++ b/openerp/tests/addons/test_impex/models.py @@ -123,3 +123,13 @@ class Many2ManyChild(orm.Model): , context=context) if isinstance(name, basestring) and name.split(':')[0] == self._name else []) + +class SelectionWithDefault(orm.Model): + _name = 'export.selection.withdefault' + + _columns = { + 'value': fields.selection([(1, "Foo"), (2, "Bar")], required=True), + } + _defaults = { + 'value': 2, + } diff --git a/openerp/tests/addons/test_impex/tests/test_load.py b/openerp/tests/addons/test_impex/tests/test_load.py index 18b26a16354..65c4b6e9e57 100644 --- a/openerp/tests/addons/test_impex/tests/test_load.py +++ b/openerp/tests/addons/test_impex/tests/test_load.py @@ -474,6 +474,21 @@ class test_selection(ImporterCase): u"Value '42' not found in selection field 'unknown'", moreinfo="Foo Bar Qux 4".split())]) +class test_selection_with_default(ImporterCase): + model_name = 'export.selection.withdefault' + + def test_skip_empty(self): + """ Empty cells should be entirely skipped so that default values can + be inserted by the ORM + """ + result = self.import_(['value'], [['']]) + self.assertFalse(result['messages']) + self.assertEqual(len(result['ids']), 1) + + self.assertEqual( + values(self.read()), + [2]) + class test_selection_function(ImporterCase): model_name = 'export.selection.function' translations_fr = [ From 6aba62a847daf134ea56e5421eae9496485b06ec Mon Sep 17 00:00:00 2001 From: "Jalpesh Patel (OpenERP)" Date: Thu, 4 Oct 2012 12:32:27 +0530 Subject: [PATCH 141/342] [fix]fix problem of send RFQ bzr revid: pja@tinyerp.com-20121004070227-rqo5ec0mckj5vqx8 --- addons/email_template/email_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 1dc79dc634c..37dca4ceddb 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -319,7 +319,7 @@ class email_template(osv.osv): ext = "." + format if not report_name.endswith(ext): report_name += ext - attachments.append(report_name, result) + attachments.append((report_name, result)) # Add template attachments for attach in template.attachment_ids: From 82e849e180c89698fd86fb1e6aa9a6d59119216e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 4 Oct 2012 10:32:31 +0200 Subject: [PATCH 142/342] [FIX] base_import: should be possible to re-validate a file after a failed import or validation bzr revid: xmo@openerp.com-20121004083231-qyl8l58wyhb7g2c3 --- addons/base_import/static/src/js/import.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index 2867242749c..937565a24e8 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -410,6 +410,7 @@ openerp.base_import = function (instance) { { name: 'preview_failed', from: 'previewing', to: 'preview_error' }, { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' }, { name: 'validate', from: 'preview_success', to: 'validating' }, + { name: 'validate', from: 'results', to: 'validating' }, { name: 'validated', from: 'validating', to: 'results' }, { name: 'import', from: ['preview_success', 'results'], to: 'importing' }, { name: 'import_succeeded', from: 'importing', to: 'imported'}, From dbb18562a5eb549c9fbe2aaee0a456e382acd727 Mon Sep 17 00:00:00 2001 From: "Khushboo Bhatt (Open ERP)" Date: Thu, 4 Oct 2012 14:12:53 +0530 Subject: [PATCH 143/342] [FIX]from product's view if we click on Bill of Materials view it opens bill of materials view which is showing BoM of this product as a component too bzr revid: kbh@tinyerp.com-20121004084253-hc1nibjcdkamocni --- addons/mrp/mrp_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml index abf0f823e5c..c00582dbcc4 100644 --- a/addons/mrp/mrp_view.xml +++ b/addons/mrp/mrp_view.xml @@ -1037,6 +1037,7 @@ {'default_product_id': active_id, 'search_default_product_id': active_id} Bill of Materials + [('bom_id','=',False)] mrp.bom From c5ac7217a867146f6456978713cfe511c7a41966 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 4 Oct 2012 11:09:02 +0200 Subject: [PATCH 144/342] [IMP]mail: follower/partners list on wall posting message form bzr revid: chm@openerp.com-20121004090902-p4b3t4zvs9frc3ky --- addons/mail/mail_message_view.xml | 8 +-- addons/mail/static/src/css/mail.css | 36 +++++++++--- addons/mail/static/src/js/mail.js | 89 +++++++++++++++-------------- addons/mail/static/src/xml/mail.xml | 16 ++++++ 4 files changed, 93 insertions(+), 56 deletions(-) diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 6d854d4d701..2992868bfcf 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -88,28 +88,28 @@ Inbox mail.wall + 'context': {'default_model': 'res.partner', 'default_res_id': uid, 'default_is_private':True} }""/> Wall mail.wall + 'context': {'default_model': 'res.partner', 'default_res_id': uid, 'default_is_private':False} }""/> Archives mail.wall + 'context': {'default_model': 'res.partner', 'default_res_id': uid} }""/> Sent mail.wall + 'context': {'default_model': 'res.partner', 'default_res_id': uid} }""/> diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 4ffb9013496..e5a9ad0c825 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -173,16 +173,41 @@ clear: both; } +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_header .oe_all_follower { + color: blue; +} +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_header .oe_partner_follower a { + color: red; +} +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_header .oe_hidden, +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_header .oe_more_hidden { + display: none; +} + +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_bottom button.post { + float: left; +} + +.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_bottom span { + float: right; +} + /* default textarea (oe_mail_compose_textarea), and body textarea for compose form view */ -.openerp .oe_mail_msg_content textarea.oe_mail_compose_textarea, -.openerp .oe_mail_msg_content div.oe_mail_compose_message_body textarea { +.openerp .oe_mail.oe_semantic_html_override .oe_mail_compose_textarea textarea, +.openerp .oe_mail div.oe_mail_compose_message_body textarea { width: 474px; - height: 60px; + min-height: 120px; + height: auto; padding: 4px; font-size: 12px; border: 1px solid #cccccc; } +/* not top textarea */ +.openerp .oe_mail.oe_semantic_html_override .oe_semantic_html_override .oe_mail_compose_textarea textarea { + height: 60px; +} + /* default textarea (oe_mail_compose_textarea), and body textarea for compose form view */ .openerp .oe_mail_msg_content textarea.oe_mail_compose_textarea:focus, .openerp .oe_mail_msg_content div.oe_mail_compose_message_body textarea:focus { @@ -413,11 +438,6 @@ } /* Dropdown menu */ -/*.openerp .oe_mail_msg_content .oe_dropdown_toggle { - position: absolute; - top: 0px; - right: 3px; -}*/ .openerp .oe_mail .oe_semantic_html_override { position: relative; diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 8915f1387e1..b2fa071b191 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -336,13 +336,40 @@ openerp.mail = function(session) { */ init: function(parent, options) { this._super(parent); + + // record parameters + var param = options.parameters; + for(var i in param){ + this[i] = param[i]; + } + this.id = param.id || -1; + this.model = param.model || false; + this.parent_id= param.parent_id || false; + this.res_id = param.res_id || false; + this.type = param.type || false; + this.is_author = param.is_author || false; + this.subject = param.subject || false; + this.name = param.name || false; + this.record_name = param.record_name || false; + this.body = param.body || false; + this.vote_user_ids =param.vote_user_ids || []; + this.has_voted = param.has_voted || false; + + this.vote_user_ids = param.vote_user_ids || []; + + this.unread = param.unread || false; + this._date = param.date; + this.author_id = param.author_id || []; + this.attachment_ids = param.attachment_ids || []; + + // record domain and context this.domain = options.domain || []; this.context = _.extend({ default_model: 'mail.thread', default_res_id: 0, default_parent_id: false }, options.context || {}); - // options + // record options this.options={ 'thread' : options.options.thread, 'message' : { @@ -356,31 +383,9 @@ openerp.mail = function(session) { 'truncate_limit': options.options.message.truncate_limit || 250, } }; + // record options and data - this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0] ; - - var param = options.parameters; - // record parameters - this.id = param.id || -1; - this.model = param.model || false; - this.parent_id= param.parent_id || false; - this.res_id = param.res_id || false; - this.type = param.type || false; - this.is_author = param.is_author || false; - this.subject = param.subject || false; - this.name = param.name || false; - this.record_name = param.record_name || false; - this.body = param.body || false; - this.vote_user_ids =param.vote_user_ids || []; - this.has_voted = param.has_voted || false; - - this.vote_user_ids = param.vote_user_ids || []; - - this.unread = param.unread || false; - this._date = param.date; - this.author_id = param.author_id || []; - this.attachment_ids = param.attachment_ids || []; - + this.parent_thread= parent.messages!= undefined ? parent : options.options.thread._parents[0]; this.thread = false; if( param.id > 0 ) { @@ -468,6 +473,7 @@ openerp.mail = function(session) { if(this.thread){ return false; } + var param = _.extend(self, {'parent_id': self.id}); /*create thread*/ self.thread = new mail.Thread(self, { 'domain': self.domain, @@ -480,11 +486,7 @@ openerp.mail = function(session) { 'thread' : self.options.thread, 'message' : self.options.message }, - 'parameters':{ - 'model': self.model, - 'id': self.id, - 'parent_id': self.id - } + 'parameters': param } ); /*insert thread in parent message*/ @@ -520,7 +522,7 @@ openerp.mail = function(session) { */ on_message_read_unread: function (event) { event.stopPropagation(); - this.animated_destroy({fadeTime:250}); + if($(event.srcElement).hasClass("oe_read")) this.animated_destroy({fadeTime:250}); // if this message is read, all childs message display is read var ids = [this.id].concat( this.get_child_ids() ); this.ds_notification.call('set_message_read', [ids,$(event.srcElement).hasClass("oe_read")]); @@ -667,6 +669,13 @@ openerp.mail = function(session) { this.id= param.id || false; this.model= param.model || false; this.parent_id= param.parent_id || false; + this.is_private = param.is_private || false; + this.partner_ids = []; + for(var i in param.partner_ids){ + if(param.partner_ids[i][0]!=(param.author_id ? param.author_id[0] : -1)){ + this.partner_ids.push(param.partner_ids[i]); + } + } this.messages = []; @@ -720,17 +729,9 @@ openerp.mail = function(session) { * in the function. */ bind_events: function() { var self = this; - // event: writing in basic textarea of composition form (quick reply) - // event: onblur for hide 'Reply' - this.$('.oe_mail_compose_textarea:first textarea') - .keyup(function (event) { - var charCode = (event.which) ? event.which : window.event.keyCode; - if (event.shiftKey && charCode == 13) { this.value = this.value+"\n"; } - else if (charCode == 13) { return self.message_post(); } - }) - .blur(function (event) { - $(this).parents('.oe_mail_thread_action:first').hide(); - }); + self.$('.oe_mail_compose_textarea:first button.post').click(function () {return self.message_post();}); + self.$('.oe_mail_compose_textarea .oe_more').click(function () { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').show(); p.find('.oe_more').hide(); }); + self.$('.oe_mail_compose_textarea .oe_more_hidden').click(function () { var p=$(this).parent(); p.find('.oe_more_hidden, .oe_hidden').hide(); p.find('.oe_more').show(); }); }, /* get all child message/thread id linked @@ -1037,7 +1038,7 @@ openerp.mail = function(session) { 'options':{ 'thread':{ 'thread_level': this.options.thread_level, - 'show_header_compose': show_header_compose, + 'show_header_compose': 0, //show_header_compose, 'use_composer': show_header_compose, 'display_on_flat':true }, @@ -1142,7 +1143,7 @@ openerp.mail = function(session) { 'thread' :{ 'thread_level': this.options.thread_level, 'use_composer': true, - 'show_header_compose': 1, + 'show_header_compose': 0, }, 'message': { 'show_reply': this.options.thread_level > 0, diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 1e2e6a970b6..b7eebd0b4d1 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -67,7 +67,23 @@
    +
    + Post a message to: + All Followers + and + + + + + , others... + <<< + +