#This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.model import Model from trytond.model import fields from trytond.model.browse import BrowseRecordList, BrowseRecord, BrowseRecordNull from trytond.model.browse import EvalEnvironment import datetime import time from decimal import Decimal OPERATORS = ( 'child_of', 'not child_of', '=', '!=', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', '<=', '>=', '<', '>', ) class ModelStorage(Model): """ Define a model with storage capability in Tryton. """ id = fields.Integer('ID', readonly=True) create_uid = fields.Many2One('res.user', 'Create User', required=True, readonly=True) create_date = fields.DateTime('Create Date', required=True, readonly=True) write_uid = fields.Many2One('res.user', 'Write User', readonly=True) write_date = fields.DateTime('Write Date', readonly=True) rec_name = fields.Function('get_rec_name', type='char', string='Name', fnct_search='search_rec_name') def __init__(self): super(ModelStorage, self).__init__() self._rpc_allowed += [ 'create', 'read', 'write', 'delete', 'copy', 'search', 'search_count', 'search_read', 'export_data', 'import_data', ] self._constraints = [] def default_create_uid(self, cursor, user, context=None): "Default value for uid field" return int(user) def default_create_date(self, cursor, user, context=None): "Default value for create_date field" return datetime.datetime.today() def default_sequence(self, cursor, user, context=None): ''' Return the default value for sequence field. ''' cursor.execute('SELECT MAX(sequence) ' \ 'FROM "' + self._table + '"') res = cursor.fetchone() if res: return res[0] return 0 def create(self, cursor, user, values, context=None): ''' Create records. :param cursor: the database cursor :param user: the user id :param values: a dictionary with fields names as key and created values as value :param context: the context :return: the id of the created record ''' model_access_obj = self.pool.get('ir.model.access') model_access_obj.check(cursor, user, self._name, 'create', context=context) return False def read(self, cursor, user, ids, fields_names=None, context=None): ''' Read records. :param cursor: the database cursor :param user: the user id :param ids: a list of ids or an id :param fields_names: fields names to read if None read all fields :param context: the context :return: a list of dictionnary or a dictionnary if ids is an id the dictionnaries will have fields names as key and fields value as value ''' model_access_obj = self.pool.get('ir.model.access') model_access_obj.check(cursor, user, self._name, 'read', context=context) if isinstance(ids, (int, long)): return {} return [] def write(self, cursor, user, ids, values, context=None): ''' Write values on records :param cursor: the database cursor :param user: the user id :param ids: a list of ids or an id :param values: a dictionary with fields names as key and written values as value :param context: the context :return: True if succeed ''' model_access_obj = self.pool.get('ir.model.access') rule_group_obj = self.pool.get('ir.rule.group') rule_obj = self.pool.get('ir.rule') model_access_obj.check(cursor, user, self._name, 'write', context=context) if not self.check_xml_record(cursor, user, ids, values, context=context): self.raise_user_error(cursor, 'write_xml_record', error_description='xml_record_desc', context=context) # Restart rule cache if rule_group_obj.search(cursor, 0, [ ('model.model', '=', self._name), ], context=context): rule_obj.domain_get(cursor.dbname) return False def delete(self, cursor, user, ids, context=None): ''' Delete records. :param cursor: the database cursor :param user: the user id :param ids: a list of ids or an id :param context: the context :return: True if succeed ''' model_access_obj = self.pool.get('ir.model.access') model_access_obj.check(cursor, user, self._name, 'delete', context=context) if not self.check_xml_record(cursor, user, ids, None, context=context): self.raise_user_error(cursor, 'delete_xml_record', error_description='xml_record_desc', context=context) return False def copy(self, cursor, user, ids, default=None, context=None): ''' Duplicate the record in ids. :param cursor: the database cursor :param user: the user id :param ids: a list of ids or an id :param default: a dictionnary with field name as keys and new value for the field as value :param context: the context :return: a list of new ids or the new id ''' lang_obj = self.pool.get('ir.lang') if default is None: default = {} if context is None: context = {} int_id = False if isinstance(ids, (int, long)): int_id = True ids = [ids] if 'state' not in default: if 'state' in self._defaults: default['state'] = self._defaults['state'](cursor, user, context) def convert_data(fields, data): data = data.copy() data_o2m = {} for field_name in fields: ftype = fields[field_name]['type'] if field_name in ( 'create_date', 'create_uid', 'write_date', 'write_uid', ): del data[field_name] if field_name in default: data[field_name] = default[field_name] elif ftype == 'function': del data[field_name] elif ftype == 'many2one': try: data[field_name] = data[field_name] and \ data[field_name][0] except: pass elif ftype in ('one2many',): if data[field_name]: data_o2m[field_name] = data[field_name] data[field_name] = False elif ftype == 'many2many': if data[field_name]: data[field_name] = [('set', data[field_name])] if 'id' in data: del data['id'] for i in self._inherits: if self._inherits[i] in data: del data[self._inherits[i]] return data, data_o2m new_ids = {} datas = self.read(cursor, user, ids, context=context) fields = self.fields_get(cursor, user, context=context) for data in datas: data_id = data['id'] data, data_o2m = convert_data(fields, data) new_ids[data_id] = self.create(cursor, user, data, context=context) for field_name in data_o2m: relation_model = self.pool.get(fields[field_name]['relation']) relation_field = fields[field_name]['relation_field'] relation_model.copy(cursor, user, data_o2m[field_name], default={relation_field: new_ids[data_id]}, context=context) fields_translate = {} for field_name, field in fields.iteritems(): if field_name in self._columns and \ self._columns[field_name].translate: fields_translate[field_name] = field elif field_name in self._inherit_fields and \ self._inherit_fields[field_name][2].translate: fields_translate[field_name] = field if fields_translate: lang_ids = lang_obj.search(cursor, user, [ ('translatable', '=', True), ], context=context) if lang_ids: lang_ids += lang_obj.search(cursor, user, [ ('code', '=', 'en_US'), ], context=context) langs = lang_obj.browse(cursor, user, lang_ids, context=context) for lang in langs: ctx = context.copy() ctx['language'] = lang.code datas = self.read(cursor, user, ids, fields_names=fields_translate.keys() + ['id'], context=ctx) for data in datas: data_id = data['id'] data, _ = convert_data(fields_translate, data) self.write(cursor, user, new_ids[data_id], data, context=ctx) if int_id: return new_ids.values()[0] return new_ids.values() def search(self, cursor, user, domain, offset=0, limit=None, order=None, context=None, count=False): ''' Return a list of id that match the clauses defined in args. :param cursor: the database cursor :param user: the user id :param domain: a list of tuples or lists lists are construct like this: ['operator', args, args, ...] operator can be 'AND' or 'OR', if it is missing the default value will be 'AND' tuples are construct like this: ('field name', 'operator', value) field name: is a field name from the model or a relational field by using '.' as separator. operator must be in OPERATORS :param offset: an integer to specify the offset for the result :param limit: an integer to specify the number of result :param order: a list of tuple that are constructed like this: ('field name', 'DESC|ASC') it allow to specify the order of result :param context: the context :param count: a boolean to return only the len of the result :return: a list of ids or an interger ''' if count: return 0 return [] def search_count(self, cursor, user, domain, context=None): ''' Return the number of record that match the domain. (See search) :param cursor: the database cursor :param user: the user id :param domain: a domain like in search :param context: the context :return: an integer ''' res = self.search(cursor, user, domain, context=context, count=True) if isinstance(res, list): return len(res) return res def search_read(self, cursor, user, domain, offset=0, limit=None, order=None, context=None, fields_names=None): ''' Call search function and read in once. Usefull for the client to reduce the number of calls. :param cursor: the database cursor :param user: the user id :param domain: a domain like in search :param offset: an integer to specify the offset for the result :param limit: an integer to specify the number of result :param order: a list of tuple that are constructed like this: ('field name', 'DESC|ASC') it allow to specify the order of result :param context: the context :param fields_names: fields names to read if None read all fields :return: a list of dictionnary or a dictionnary if limit is 1 the dictionnaries will have fields names as key and fields value as value ''' ids = self.search(cursor, user, domain, offset=offset, limit=limit, order=order, context=context) if limit == 1: if not ids: return [] ids = ids[0] return self.read(cursor, user, ids, fields_names=fields_names, context=context) def _search_domain_active(self, domain, active_test=True, context=None): if context is None: context = {} domain = domain[:] # if the object has a field named 'active', filter out all inactive # records unless they were explicitely asked for if not (('active' in self._columns or \ 'active' in self._inherit_fields.keys()) \ and (active_test and context.get('active_test', True))): return domain def process(domain): i = 0 active_found = False while i < len(domain): if isinstance(domain[i], list): domain[i] = process(domain[i]) if isinstance(domain[i], tuple): if domain[i][0] == 'active': active_found = True i += 1 if not active_found: if domain and ((isinstance(domain[0], basestring) \ and domain[0] == 'AND') \ or (not isinstance(domain[0], basestring))): domain.append(('active', '=', 1)) else: domain = ['AND', domain, ('active', '=', 1)] return domain return process(domain) def get_rec_name(self, cursor, user, ids, name, arg, context=None): ''' Return a dictionary with id as key and rec_name as value. It is used by the Function field rec_name :param cursor: the database cursor :param user: the user id :param ids: a list of ids :param name: the name of the Function field :param arg: the argument of the Function field :param context: the context :return: a dictionary ''' if not ids: return {} res = {} for record in self.browse(cursor, user, ids, context=context): res[record.id] = unicode(record[self._rec_name]) return res def search_rec_name(self, cursor, user, name, args, context=None): ''' Return a list of arguments for search on rec_name :param cursor: the database cursor :param user: the user id :param name: the name of the Function field :param args: the list of arguments :param context: the context :return: a list of arguments ''' args2 = [] i = 0 while i < len(args): args2.append((self._rec_name, args[i][1], args[i][2])) i += 1 return args2 def browse(self, cursor, user, ids, context=None): ''' Return a browse a BrowseRecordList for the ids or BrowseRecord if ids is a integer. :param cursor: the database cursor :param user: the user id :param ids: a list of ids or an id :param context: the context :return: a BrowseRecordList or a BrowseRecord ''' cache = {} if isinstance(ids, (int, long)): return BrowseRecord(cursor, user, ids, self, cache, context=context) return BrowseRecordList([BrowseRecord(cursor, user, x, self, cache, context=context) for x in ids], context) def __export_row(self, cursor, user, row, fields_names, context=None): lines = [] data = ['' for x in range(len(fields_names))] done = [] for fpos in range(len(fields_names)): field = fields_names[fpos] if field: row2 = row i = 0 while i < len(field): row2 = row2[field[i]] if not row2: break if isinstance(row2, (BrowseRecordList, list)): first = True fields2 = [(x[:i+1]==field[:i+1] and x[i+1:]) \ or [] for x in fields_names] if fields2 in done: break done.append(fields2) for row2 in row2: lines2 = self.__export_row(cursor, user, row2, fields2, context) if first: for fpos2 in range(len(fields_names)): if lines2 and lines2[0][fpos2]: data[fpos2] = lines2[0][fpos2] lines += lines2[1:] first = False else: lines += lines2 break i += 1 if i == len(field): data[fpos] = row2 or '' return [data] + lines def export_data(self, cursor, user, ids, fields_names, context=None): ''' Return list of list of values for each ids. The list of values follow the fields_names. Relational fields are defined with '/' at any deep. :param cursor: the database cursor :param ids: a list of ids :param fields_names: a list of fields names :param context: the context :return: a list of list of values for each ids ''' fields_names = [x.split('/') for x in fields_names] datas = [] for row in self.browse(cursor, user, ids, context): datas += self.__export_row(cursor, user, row, fields_names, context) return datas def import_data(self, cursor, user, fields_names, datas, context=None): ''' Create record for each values in datas. The fields name of values must be defined in fields_names. :param cursor: the database cursor :param user: the user id :param fields_names: a list of fields names :param datas: the datas to import :param context: the context :return: a tuple with - the number of records imported - the last values if failed - the exception if failed - the warning if failed ''' if context is None: context = {} fields_names = [x.split('/') for x in fields_names] logger = logging.getLogger('import') def process_liness(self, datas, prefix, fields_def, position=0): line = datas[position] row = {} translate = {} todo = [] warning = '' # Import normal fields_names for i in range(len(fields_names)): if i >= len(line): raise Exception('ImportError', 'Please check that all your lines have %d cols.' % \ (len(fields_names),)) field = fields_names[i] if (len(field) == len(prefix) + 1) \ and field[len(prefix)].endswith(':id'): res_id = False if line[i]: if fields_def[field[len(prefix)][:-3]]['type'] \ == 'many2many': res_id = [] for word in line[i].split(','): module, xml_id = word.rsplit('.', 1) ir_model_data_obj = \ self.pool.get('ir.model.data') new_id = ir_model_data_obj._get_id(cursor, user, module, xml_id) res_id2 = ir_model_data_obj.read(cursor, user, [new_id], ['res_id'])[0]['res_id'] if res_id2: res_id.append(res_id2) if len(res_id): res_id = [('set', res_id)] else: module, xml_id = line[i].rsplit('.', 1) ir_model_data_obj = self.pool.get('ir.model.data') new_id = ir_model_data_obj._get_id(cursor, user, module, xml_id) res_id = ir_model_data_obj.read(cursor, user, [new_id], ['res_id'])[0]['res_id'] row[field[0][:-3]] = res_id or False continue if (len(field) == len(prefix)+1) and \ len(field[len(prefix)].split(':lang=')) == 2: field, lang = field[len(prefix)].split(':lang=') translate.setdefault(lang, {})[field]=line[i] or False continue if (len(field) == len(prefix)+1) and \ (prefix == field[0:len(prefix)]): if fields_def[field[len(prefix)]]['type'] == 'integer': res = line[i] and int(line[i]) elif fields_def[field[len(prefix)]]['type'] == 'float': res = line[i] and float(line[i]) elif fields_def[field[len(prefix)]]['type'] == 'selection': res = False if isinstance( fields_def[field[len(prefix)]]['selection'], (tuple, list)): sel = fields_def[field[len(prefix)]]['selection'] else: sel = getattr(self, fields_def[field[len(prefix)]]\ ['selection'])(cursor, user, context) for key, val in sel: if str(key) == line[i]: res = key if line[i] and not res: logger.warning("key '%s' not found " \ "in selection field '%s'" % \ (line[i], field[len(prefix)])) elif fields_def[field[len(prefix)]]['type'] == 'many2one': res = False if line[i]: relation = \ fields_def[field[len(prefix)]]['relation'] relation_obj = self.pool.get(relation) res = relation_obj.search(cursor, user, [ ('rec_name', '=', line[i]), ], limit=1, context=context) if not res: warning += ('Relation not found: ' + line[i] + \ ' on ' + relation + ' !\n') logger.warning( 'Relation not found: ' + line[i] + \ ' on ' + relation + ' !\n') elif fields_def[field[len(prefix)]]['type'] == 'many2many': res = [] if line[i]: relation = \ fields_def[field[len(prefix)]]['relation'] for word in line[i].split(','): relation_obj = self.pool.get(relation) res2 = relation_obj.search(cursor, user, [ ('rec_name', '=', word), ], limit=1, context=context) if not res2: warning += ('Relation not found: ' + \ line[i] + ' on '+relation + ' !\n') logger.warning( 'Relation not found: ' + line[i] + \ ' on '+relation + ' !\n') else: res.append(res2) if len(res): res = [('set', res)] else: res = line[i] or False row[field[len(prefix)]] = res elif (prefix==field[0:len(prefix)]): if field[0] not in todo: todo.append(field[len(prefix)]) # Import one2many fields nbrmax = 1 for field in todo: newfd = self.pool.get(fields_def[field]['relation']).fields_get( cursor, user, context=context) res = process_liness(self, datas, prefix + [field], newfd, position) (newrow, max2, warning2, translate2) = res nbrmax = max(nbrmax, max2) warning = warning + warning2 reduce(lambda x, y: x and y, newrow) row[field] = (reduce(lambda x, y: x or y, newrow.values()) and \ [('create', newrow)]) or [] i = max2 while (position+i)