diff --git a/CHANGELOG b/CHANGELOG index 51b01ecccd..427a21de28 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Allow to update record when importing data * Set context on record instantiated by Field.__set__ * Set context when reading related fields * Check read access on field in search order (issue8189) diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index 0c3f81c34b..b456d49490 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -305,7 +305,7 @@ Class methods: .. classmethod:: ModelStorage.import_data(fields_names, data) - Create records for all values in ``datas``. + Create or update records for all values in ``data``. The field names of values must be defined in ``fields_names``. It returns the number of imported records. diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 5b4dbc8f05..72e9e0f931 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -7,7 +7,7 @@ import csv from decimal import Decimal from itertools import islice, chain -from functools import reduce, wraps +from functools import wraps from operator import itemgetter from collections import defaultdict @@ -807,7 +807,16 @@ class ModelStorage(Model): return '%s,%s' % (relation, str(res_ids[0])) return res_ids and res_ids[0] or False - def process_lines(data, prefix, fields_def, position=0): + def dispatch(create, write, row, Relation=cls): + id_ = row.pop('id', None) + if id_: + write.append([Relation(id_)]) + write.append(row) + else: + create.append(row) + return id_ + + def process_lines(data, prefix, fields_def, position=0, klass=cls): line = data[position] row = {} translate = {} @@ -831,7 +840,12 @@ class ModelStorage(Model): this_field_def = fields_def[field[-1]] field_type = this_field_def['type'] res = None - if field_type == 'boolean': + if field[-1] == 'id': + try: + res = int(value) + except ValueError: + res = get_many2one(klass.__name__, value) + elif field_type == 'boolean': if value.lower() == 'true': res = True elif value.lower() == 'false': @@ -878,15 +892,13 @@ class ModelStorage(Model): # Import one2many fields nbrmax = 1 for field in todo: - newfd = pool.get(fields_def[field]['relation'] - ).fields_get() - res = process_lines(data, prefix + [field], newfd, - position) - (newrow, max2, _) = res + Relation = pool.get(fields_def[field]['relation']) + newfd = Relation.fields_get() + newrow, max2, _ = process_lines( + data, prefix + [field], newfd, position, klass=Relation) nbrmax = max(nbrmax, max2) - reduce(lambda x, y: x and y, newrow) - row[field] = (reduce(lambda x, y: x or y, list(newrow.values())) and - [('create', [newrow])]) or [] + create, write = [], [] + dispatch(create, write, newrow, Relation) i = max2 while (position + i) < len(data): test = True @@ -897,13 +909,17 @@ class ModelStorage(Model): break if not test: break - (newrow, max2, _) = \ - process_lines(data, prefix + [field], newfd, - position + i) - if reduce(lambda x, y: x or y, list(newrow.values())): - row[field].append(('create', [newrow])) + newrow, max2, _ = process_lines( + data, prefix + [field], newfd, position + i, + klass=Relation) + dispatch(create, write, newrow, Relation) i += max2 nbrmax = max(nbrmax, i) + row[field] = [] + if create: + row[field].append(('create', create)) + if write: + row[field].append(('write',) + tuple(write)) if prefix_len == 0: for i in range(max(nbrmax, 1)): data.pop(0) @@ -916,20 +932,35 @@ class ModelStorage(Model): fields_names = [x.split('/') for x in fields_names] fields_def = cls.fields_get() - to_create, translations, languages = [], [], set() + to_create, to_create_translations = [], [] + to_write, to_write_translations = [], [] + languages = set() while len(data): - (res, _, translate) = \ + (row, _, translate) = \ process_lines(data, [], fields_def) - to_create.append(res) - translations.append(translate) + if dispatch(to_create, to_write, row): + to_write_translations.append(translate) + else: + to_create_translations.append(translate) languages.update(translate) - new_records = cls.create(to_create) - for language in languages: - translated = [t.get(language, {}) for t in translations] - with Transaction().set_context(language=language): - cls.write(*chain(*filter(itemgetter(1), - zip(([r] for r in new_records), translated)))) - return len(new_records) + + def translate(records, translations): + for language in languages: + translated = [t.get(language, {}) for t in translations] + with Transaction().set_context(language=language): + cls.write(*chain(*filter(itemgetter(1), + zip(([r] for r in records), translated)))) + count = 0 + if to_create: + records = cls.create(to_create) + translate(records, to_create_translations) + count += len(records) + if to_write: + cls.write(*to_write) + records = sum(to_write[0:None:2], []) + translate(records, to_write_translations) + count += len(records) + return count @classmethod def check_xml_record(cls, records, values): diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py index 255a29d8dd..b8d54f94e2 100644 --- a/trytond/tests/__init__.py +++ b/trytond/tests/__init__.py @@ -10,7 +10,7 @@ except ImportError: from ..pool import Pool from .modelview import * from .mptt import * -from .import_data import * +from . import import_data from .export_data import * from .trigger import * from . import access @@ -56,27 +56,6 @@ def register(): ModelViewEmptyPage, ModelViewCircularDepends, MPTT, - ImportDataBoolean, - ImportDataInteger, - ImportDataIntegerRequired, - ImportDataFloat, - ImportDataFloatRequired, - ImportDataNumeric, - ImportDataNumericRequired, - ImportDataChar, - ImportDataText, - ImportDataDate, - ImportDataDateTime, - ImportDataSelection, - ImportDataMany2OneTarget, - ImportDataMany2One, - ImportDataMany2ManyTarget, - ImportDataMany2Many, - ImportDataMany2ManyRelation, - ImportDataOne2Many, - ImportDataOne2ManyTarget, - ImportDataReferenceSelection, - ImportDataReference, ExportDataTarget, ExportData, ExportDataTarget2, @@ -94,6 +73,7 @@ def register(): TestWizard, module='tests', type_='wizard') + import_data.register('tests') access.register('tests') model.register('tests') modelstorage.register('tests') diff --git a/trytond/tests/import_data.py b/trytond/tests/import_data.py index bebd892a55..22481ebf0d 100644 --- a/trytond/tests/import_data.py +++ b/trytond/tests/import_data.py @@ -2,19 +2,7 @@ # this repository contains the full copyright notices and license terms. "Test for import_data" from trytond.model import ModelSQL, fields - -__all__ = [ - 'ImportDataBoolean', 'ImportDataInteger', 'ImportDataIntegerRequired', - 'ImportDataFloat', 'ImportDataFloatRequired', - 'ImportDataNumeric', 'ImportDataNumericRequired', - 'ImportDataChar', 'ImportDataText', - 'ImportDataDate', 'ImportDataDateTime', 'ImportDataSelection', - 'ImportDataMany2OneTarget', 'ImportDataMany2One', - 'ImportDataMany2ManyTarget', 'ImportDataMany2Many', - 'ImportDataMany2ManyRelation', 'ImportDataOne2Many', - 'ImportDataOne2ManyTarget', 'ImportDataReferenceSelection', - 'ImportDataReference', - ] +from trytond.pool import Pool class ImportDataBoolean(ModelSQL): @@ -154,3 +142,36 @@ class ImportDataReference(ModelSQL): (None, ''), ('test.import_data.reference.selection', 'Test'), ]) + + +class ImportDataUpdate(ModelSQL): + "Import Data for Update" + __name__ = 'test.import_data.update' + name = fields.Char("Name") + + +def register(module): + Pool.register( + ImportDataBoolean, + ImportDataInteger, + ImportDataIntegerRequired, + ImportDataFloat, + ImportDataFloatRequired, + ImportDataNumeric, + ImportDataNumericRequired, + ImportDataChar, + ImportDataText, + ImportDataDate, + ImportDataDateTime, + ImportDataSelection, + ImportDataMany2OneTarget, + ImportDataMany2One, + ImportDataMany2ManyTarget, + ImportDataMany2Many, + ImportDataMany2ManyRelation, + ImportDataOne2Many, + ImportDataOne2ManyTarget, + ImportDataReferenceSelection, + ImportDataReference, + ImportDataUpdate, + module=module, type_='model') diff --git a/trytond/tests/test_importdata.py b/trytond/tests/test_importdata.py index b68dfc11ac..bff8307f85 100644 --- a/trytond/tests/test_importdata.py +++ b/trytond/tests/test_importdata.py @@ -493,6 +493,53 @@ class ImportDataTestCase(unittest.TestCase): [['test.import_data.reference.selection,test.foo']]) transaction.rollback() + @with_transaction() + def test_update_id(self): + "Test update with ID" + pool = Pool() + Char = pool.get('test.import_data.update') + record = Char(name="foo") + record.save() + + count = Char.import_data(['id', 'name'], [[str(record.id), "bar"]]) + + record, = Char.search([]) + self.assertEqual(count, 1) + self.assertEqual(record.name, "bar") + + @with_transaction() + def test_update_rec_name(self): + "Test update with rec_name" + pool = Pool() + Char = pool.get('test.import_data.update') + record = Char(name="foo") + record.save() + + count = Char.import_data(['id', 'name'], [[record.rec_name, "bar"]]) + + record, = Char.search([]) + self.assertEqual(count, 1) + self.assertEqual(record.name, "bar") + + @with_transaction() + def test_update_one2many(self): + "Test update one2many" + pool = Pool() + One2many = pool.get('test.import_data.one2many') + Target = pool.get('test.import_data.one2many.target') + record = One2many(name="test", one2many=[Target(name="foo")]) + record.save() + target, = record.one2many + + count = One2many.import_data( + ['id', 'one2many/id', 'one2many/name'], + [[record.id, target.id, "bar"], + ['', '', "baz"]]) + + self.assertEqual(count, 1) + self.assertEqual(len(record.one2many), 2) + self.assertEqual([t.name for t in record.one2many], ["bar", "baz"]) + def suite(): return unittest.TestLoader().loadTestsFromTestCase(ImportDataTestCase)