def setDefault(self, record, value): group = record.values[self.name] if value and len(value): context = self.context(record) Rpc2 = RpcProxy(self.attrs['relation']) fields = Rpc2.fields_get(list(value[0].keys()), context) group.addFields(fields) for recordData in (value or []): newRecord = group.create(default=False) newRecord.setDefaults(recordData) newRecord.modified = True return True
def setDefault(self, record, value): from Koo.Model.Group import RecordGroup group = record.values[self.name] if value and len(value): assert isinstance(value[0], dict), "%s: %r" % (self.name, value) context = self.context(record) Rpc2 = RpcProxy(self.attrs['relation']) fields = Rpc2.fields_get(value[0].keys(), context) group.addFields(fields) for rec in (value or []): newRecord = group.create(default=False) newRecord.setDefaults(rec) newRecord.modified = True return True
class RecordGroup(QObject): SortVisibleItems = 1 SortAllItems = 2 SortingPossible = 0 SortingNotPossible = 1 SortingOnlyGroups = 2 SortingNotPossibleModified = 3 # @brief Creates a new RecordGroup object. # @param resource Name of the model to load. Such as 'res.partner'. # @param fields Dictionary with the fields to load. This value typically comes from the server. # @param ids Record identifiers to load in the group. # @param parent Only used if this RecordGroup serves as a relation to another model. Otherwise it's None. # @param context Context for RPC calls. def __init__(self, resource, fields=None, ids=None, parent=None, context=None): QObject.__init__(self) if ids is None: ids = [] if context is None: context = {} self.parent = parent self._context = context self._context.update(Rpc.session.context) self.resource = resource self.limit = Settings.value('koo.limit', 80, int) self.maximumLimit = self.limit self.rpc = RpcProxy(resource) if fields == None: self.fields = {} else: self.fields = fields self.fieldObjects = {} self.loadFieldObjects(list(self.fields.keys())) self.records = [] self.enableSignals() # toBeSorted properties store information each time sort() function # is called. If loading of records is not enabled, records won't be # loaded but we keep by which field we want information to be sorted # so when record loading is enabled again we know how should the sorting # be. self.toBeSortedField = None self.toBeSortedOrder = None self.sortedField = None self.sortedOrder = None self.updated = False self._domain = [] self._filter = [] if Settings.value('koo.sort_mode') == 'visible_items': self._sortMode = self.SortVisibleItems else: self._sortMode = self.SortAllItems self._sortMode = self.SortAllItems self._allFieldsLoaded = False self.load(ids) self.removedRecords = [] self._onWriteFunction = '' # @brief Sets wether data loading should be done on record chunks or one by one. # # Setting value to True, will make the RecordGroup ignore the current 'limit' property, # and load records by one by, instead. If set to False (the default) it will load records # in groups of 'limit' (80, by default). # # In some cases (widgets that show multiple records) it's better to load in chunks, in other # cases, it's better to load one by one. def setLoadOneByOne(self, value): if value: self.limit = 1 else: self.limit = self.maximumLimit def setSortMode(self, mode): self._sortMode = mode def sortMode(self): return self._sortMode def setOnWriteFunction(self, value): self._onWriteFunction = value def onWriteFunction(self): return self._onWriteFunction def __del__(self): if self.parent: self.disconnect(self, SIGNAL('modified'), self.tomanyfield.groupModified) self.tomanyfield = None self.rpc = None self.parent = None self.resource = None self._context = None self.fields = None for r in self.records: if not isinstance(r, Record): continue self.disconnect( r, SIGNAL('recordChanged( PyQt_PyObject )'), self.recordChanged) self.disconnect( r, SIGNAL('recordModified( PyQt_PyObject )'), self.recordModified) r.__del__() self.records = [] for f in self.fieldObjects: self.fieldObjects[f].parent = None # @xtorello toreview ##self.fieldObjects[f].setParent(None) # self.fieldObjects[f].__del__() #self.disconnect( self.fieldObjects[f], None, 0, 0 ) #self.fieldObjects[f] = None #del self.fieldObjects[f] self.fieldObjects = {} # @brief Returns a string with the name of the type of a given field. Such as 'char'. def fieldType(self, fieldName): if not fieldName in self.fields: return None return self.fields[fieldName]['type'] # Creates the entries in 'fieldObjects' for each key of the 'fkeys' list. def loadFieldObjects(self, fkeys): for fname in fkeys: fvalue = self.fields[fname] fvalue['name'] = fname self.fieldObjects[fname] = Field.FieldFactory.create( fvalue['type'], self, fvalue) if fvalue['type'] in ('binary', 'image'): self.fieldObjects['%s.size' % fname] = Field.FieldFactory.create( 'binary-size', self, fvalue) # @brief Saves all the records. # # Note that there will be one request to the server per modified or # created record. def save(self): for record in self.records: if isinstance(record, Record): saved = record.save() # @brief Returns a list with all modified records def modifiedRecords(self): modified = [] for record in self.records: if isinstance(record, Record) and record.isModified(): modified.append(record) return modified # @brief This function executes the 'onWriteFunction' function in the server. # # If there is a 'onWriteFunction' function associated with the model type handled by # this record group it will be executed. 'editedId' should provide the # id of the just saved record. # # This functionality is provided here instead of on the record because # the remote function might update some other records, and they need to # be (re)loaded. def written(self, editedId): if not self._onWriteFunction or not editedId: return # Execute the onWriteFunction function on the server. # It's expected it'll return a list of ids to be loaded or reloaded. new_ids = getattr(self.rpc, self._onWriteFunction)( editedId, self.context()) record_idx = self.records.index(self.recordById(editedId)) result = False indexes = [] for id in new_ids: cont = False for m in self.records: if isinstance(m, Record): if m.id == id: cont = True # TODO: Shouldn't we just call cancel() so the record # is reloaded on demand? m.reload() if cont: continue # TODO: Should we reconsider this? Do we need/want to reload. Probably we # only want to add the id to the list. record = Record(id, self, parent=self.parent) self.connect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.connect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) record.reload() if not result: result = record newIndex = min(record_idx, len(self.records) - 1) self.add(record, newIndex) indexes.append(newIndex) if indexes: self.emit(SIGNAL('recordsInserted(int,int)'), min(indexes), max(indexes)) return result # @brief Adds a list of records as specified by 'values'. # # 'values' has to be a list of dictionaries, each of which containing fields # names -> values. At least key 'id' needs to be in all dictionaries. def loadFromValues(self, values): start = len(self.records) for value in values: record = Record(value['id'], self, parent=self.parent) record.set(value) self.records.append(record) self.connect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.connect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) end = len(self.records) - 1 self.emit(SIGNAL('recordsInserted(int,int)'), start, end) # @brief Creates as many records as len(ids) with the ids[x] as id. # # 'ids' needs to be a list of identifiers. The addFields() function # can be used later to load the necessary fields for each record. def load(self, ids, addOnTop=False): if not ids: return if addOnTop: start = 0 # Discard from 'ids' those that are already loaded. # If we didn't do that, some records could be repeated if the programmer # doesn't verify that, and we'd end up in errors because when records are # actually loaded they're only checked against a single appearance of the # id in the list of records. # # Note we don't use sets to discard ids, because we want to keep the order # their order and because it can cause infinite recursion. currentIds = self.ids() for id in ids: if id not in currentIds: self.records.insert(0, id) end = len(ids) - 1 else: start = len(self.records) # Discard from 'ids' those that are already loaded. Same as above. currentIds = self.ids() for id in ids: if id not in currentIds: self.records.append(id) end = len(self.records) - 1 # We consider the group is updated because otherwise calling count() would # force an update() which would cause one2many relations to load elements # when we only want to know how many are there. self.updated = True self.emit(SIGNAL('recordsInserted(int,int)'), start, end) # @brief Clears the list of records. It doesn't remove them. def clear(self): for record in self.records: if isinstance(record, Record): self.disconnect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.disconnect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) last = len(self.records) - 1 self.records = [] self.removedRecords = [] self.emit(SIGNAL('recordsRemoved(int,int)'), 0, last) # @brief Returns a copy of the current context def context(self): ctx = {} ctx.update(self._context) return ctx # @brief Sets the context that will be used for RPC calls. def setContext(self, context): self._context = context.copy() # @brief Adds a record to the list def add(self, record, position=-1): if not record.group is self: fields = {} for mf in record.group.fields: fields[record.group.fields[mf]['name'] ] = record.group.fields[mf] self.addFields(fields) record.group.addFields(self.fields) record.group = self if position == -1: self.records.append(record) else: self.records.insert(position, record) record.parent = self.parent self.connect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.connect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) return record # @brief Creates a new record of the same type of the records in the group. # # If 'default' is true, the record is filled in with default values. # 'domain' and 'context' are only used if default is true. def create(self, default=True, position=-1, domain=None, context=None): if domain is None: domain = [] if context is None: context = {} self.ensureUpdated() record = Record(None, self, parent=self.parent, new=True) if default: ctx = context.copy() ctx.update(self.context()) record.fillWithDefaults(domain, ctx) self.add(record, position) if position == -1: start = len(self.records) - 1 else: start = position self.emit(SIGNAL('recordsInserted(int,int)'), start, start) return record def disableSignals(self): self._signalsEnabled = False def enableSignals(self): self._signalsEnabled = True def recordChanged(self, record): if self._signalsEnabled: self.emit(SIGNAL('recordChanged(PyQt_PyObject)'), record) def recordModified(self, record): if self._signalsEnabled: self.emit(SIGNAL('modified')) # @brief Removes a record from the record group but not from the server. # # If the record doesn't exist it will ignore it silently. def removeRecord(self, record): idx = self.records.index(record) if isinstance(record, Record): id = record.id else: id = record if id: # Only store removedRecords if they have a valid Id. # Otherwise we don't need them because they don't have # to be removed in the server. self.removedRecords.append(id) if isinstance(record, Record): if record.parent: record.parent.modified = True self.freeRecord(record) self.emit(SIGNAL('modified')) self.emit(SIGNAL('recordsRemoved(int,int)'), idx, idx) # @brief Remove a list of records from the record group but not from the server. # # If a record doesn't exist it will ignore it silently. def removeRecords(self, records): firstIdx = -1 lastIdx = -1 toRemove = [] for record in records: if not record in records: continue idx = self.records.index(record) if firstIdx < 0 or idx < firstIdx: firstIdx = idx if lastIdx < 0 or idx > lastIdx: lastIdx = idx if isinstance(record, Record): id = record.id else: id = record if id: # Only store removedRecords if they have a valid Id. # Otherwise we don't need them because they don't have # to be removed in the server. self.removedRecords.append(id) if isinstance(record, Record): if record.parent: record.parent.modified = True self.freeRecord(record) self.emit(SIGNAL('modified')) self.emit(SIGNAL('recordsRemoved(int,int)'), firstIdx, lastIdx) # @brief Removes a record from the record group but not from the server. # # If the record doesn't exist it will ignore it silently. def remove(self, record): if isinstance(record, list): self.removeRecords(record) else: self.removeRecord(record) def binaryFieldNames(self): return [x[:-5] for x in list(self.fieldObjects.keys()) if x.endswith('.size')] def allFieldNames(self): return [x for x in list(self.fieldObjects.keys()) if not x.endswith('.size')] def createAllFields(self): if self._allFieldsLoaded: return fields = self.rpc.fields_get() self.addFields(fields) self._allFieldsLoaded = True # @brief Adds the specified fields to the record group # # Note that it updates 'fields' and 'fieldObjects' in the group. # 'fields' is a dict of dicts as typically returned by 'fields_get' # server function. def addFields(self, fields): to_add = [] for f in list(fields.keys()): if not f in self.fields: self.fields[f] = fields[f] self.fields[f]['name'] = f to_add.append(f) else: self.fields[f].update(fields[f]) self.loadFieldObjects(to_add) return to_add # @brief Ensures all records in the group are loaded. def ensureAllLoaded(self): ids = self.unloadedIds() if not ids: return c = Rpc.session.context.copy() c.update(self.context()) c['bin_size'] = True values = self.rpc.read(ids, list(self.fields.keys()), c) if values: for v in values: #self.recordById( v['id'] ).set(v, signal=False) r = self.recordById(v['id']) r.set(v, signal=False) # @brief Returns the list of ids that have not been loaded yet. The list # won't include new records as those have id 0 or None. def unloadedIds(self): self.ensureUpdated() ids = [] for x in self.records: if isinstance(x, Record): if x.id and not x._loaded: ids.append(x.id) elif x: ids.append(x) return ids # @brief Returns the list of loaded records. The list won't include new records. def loadedRecords(self): records = [] for x in self.records: if isinstance(x, Record): if x.id and x._loaded: records.append(x) return records # @brief Returns a list with all ids. def ids(self): ids = [] for x in self.records: if isinstance(x, Record): ids.append(x.id) else: ids.append(x) return ids # @brief Returns a list with all new records. def newRecords(self): records = [] for x in self.records: if not isinstance(x, Record): continue if x.id: continue records.append(x) return records # @brief Returns the number of records in this group. def count(self): self.ensureUpdated() return len(self.records) def __iter__(self): self.ensureUpdated() self.ensureAllLoaded() return iter(self.records) # @brief Returns the record with id 'id'. You can use [] instead. # Note that it will check if the record is loaded and load it if not. def modelById(self, id): record = self.recordById(id) if not record: return None return record __getitem__ = modelById # @brief Returns the record at the specified row number. def modelByIndex(self, row): record = self.recordByIndex(row) return record # @brief Returns the row number of the given record. Note that # the record must be in the group. Otherwise an exception is risen. def indexOfRecord(self, record): if record in self.records: return self.records.index(record) else: return -1 # @brief Returns the row number of the given id. # If the id doesn't exist it returns -1. def indexOfId(self, id): i = 0 for record in self.records: if isinstance(record, Record): if record.id == id: return i elif record == id: return i i += 1 return -1 # @brief Returns True if the given record exists in the group. def recordExists(self, record): return record in self.records # @brief Returns True if the given field name exists in the group. def fieldExists(self, fieldName): return fieldName in self.fieldObjects # @brief Returns the record with id 'id'. You can use [] instead. # Note that it will return the record but won't try to load it. def recordById(self, id): for record in self.records: if isinstance(record, Record): if record.id == id: return record elif record == id: idx = self.records.index(id) record = Record(id, self, parent=self.parent) self.connect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.connect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) self.records[idx] = record return record def duplicate(self, record): if record.id: # If record exists in the database, ensure we copy all fields. self.createAllFields() self.ensureRecordLoaded(record) newRecord = self.create() newRecord.values = record.values.copy() for field in list(newRecord.values.keys()): if self.fieldType(field) in ('one2many'): del newRecord.values[field] newRecord.modified = True newRecord.changed() return newRecord # @brief Returns a Record object for the given row. def recordByIndex(self, row): record = self.records[row] if isinstance(record, Record): return record else: record = Record(record, self, parent=self.parent) self.connect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.connect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) self.records[row] = record return record # @brief Returns True if the RecordGroup handles information of a wizard. def isWizard(self): return self.resource.startswith('wizard.') # @brief Checks whether the specified record is fully loaded and loads # it if necessary. def ensureRecordLoaded(self, record): self.ensureUpdated() # Do not try to load if record is new. if not record.id: record.createMissingFields() return if record.isFullyLoaded(): return c = Rpc.session.context.copy() c.update(self.context()) ids = self.ids() pos = ids.index(record.id) / self.limit queryIds = ids[int(pos * self.limit): int(pos * self.limit) + self.limit] if None in queryIds: queryIds.remove(None) missingFields = record.missingFields() self.disableSignals() c['bin_size'] = True values = self.rpc.read(queryIds, missingFields, c) if values: for v in values: id = v['id'] if 'id' not in missingFields: del v['id'] self.recordById(id).set(v, signal=False) self.enableSignals() # TODO: Take a look if we need to set default values for new records! # Set defaults # if len(new) and len(to_add): #values = self.rpc.default_get( to_add, self.context() ) # for t in to_add: # if t not in values: #values[t] = False # for mod in new: # mod.setDefaults(values) # @brief Allows setting the domain for this group of records. def setDomain(self, value): # In some (rare) cases we receive {} as domain. So let's just test # 'not value', and that should work in all cases, not only when value # is None. if not value: self._domain = [] else: self._domain = value if Settings.value('koo.load_on_open', True): self.updated = False # @brief Returns the current domain. def domain(self): return self._domain # @brief Allows setting a filter for this group of records. # # The filter is conatenated to the domain to further restrict the records of # the group. def setFilter(self, value): if value == None: self._filter = [] else: self._filter = value self.updated = False # @brief Returns the current filter. def filter(self): return self._filter # @brief Disables record loading by setting domain to [('id','in',[])] # # RecordGroup will optimize the case when domain + filter = [('id','in',[])] # by not even querying the server and searching ids. It will simply consider # the result is [] and thus the group will be kept empty. # # Domain may be changed using setDomain() function. def setDomainForEmptyGroup(self): if self.isModified(): return self.setDomain([('id', 'in', [])]) self.clear() # @brief Returns True if domain is [('id','in',[])] def isDomainForEmptyGroup(self): return self.domain() == [('id', 'in', [])] # @brief Reload the record group with current selected sort field, order, domain and filter def update(self): # Update context from Rpc.session.context as language # (or other settings) might have changed. self._context.update(Rpc.session.context) self.rpc = RpcProxy(self.resource) # Make it reload again self.updated = False self.sort(self.toBeSortedField, self.toBeSortedOrder) # @brief Ensures the group is updated. def ensureUpdated(self): if self.updated: return self.update() # @brief Sorts the group by the given field name. def sort(self, field, order): self.toBeSortedField = field self.toBeSortedOrder = order if self._sortMode == self.SortAllItems: self.sortAll(field, order) else: self.sortVisible(field, order) # Sorts the records in the group using ALL records in the database def sortAll(self, field, order): if self.updated and field == self.sortedField and order == self.sortedOrder: return # Check there're no new or modified records. If there are # we won't sort as it means reloading data from the server # and we'd loose current changes. if self.isModified(): self.emit(SIGNAL("sorting"), self.SortingNotPossibleModified) return oldSortedField = self.sortedField # We set this fields in the very beggining in case some signals are cought # and retry to sort again which would cause an infinite recursion. self.sortedField = field self.sortedOrder = order self.updated = True sorted = False sortingResult = self.SortingPossible if self._domain + self._filter == [('id', 'in', [])]: # If setDomainForEmptyGroup() was called, or simply the domain # included no tuples, we don't even need to query the server. # Note that this may be quite important in some wizards because # the model will actually not exist in the server and would raise # an exception. ids = [] elif not field in list(self.fields.keys()): # If the field doesn't exist use default sorting. Usually this will # happen when we update and haven't selected a field to sort by. ids = self.rpc.search( self._domain + self._filter, 0, False, False, self._context) else: type = self.fields[field]['type'] if type == 'one2many' or type == 'many2many': # We're not able to sort 2many fields sortingResult = self.SortingNotPossible elif type == 'many2one': # This works only if '#407667' is fixed, but it was fixed in 2010-02-03 orderby = '"%s"' % field if order == Qt.AscendingOrder: orderby += " ASC" else: orderby += " DESC" try: ids = Rpc.session.call( '/koo', 'search', self.resource, self._domain + self._filter, 0, 0, orderby, self._context) sortingResult = self.SortingPossible sorted = True except: sortingResult = self.SortingOnlyGroups # We check whether the field is stored or not. In case the server # is not _ready_ we consider it's stored and we'll catch the exception # later. stored = self.fields[field].get('stored', True) if not stored: sortingResult = self.SortingNotPossible if not sorted and sortingResult != self.SortingNotPossible: # A lot of the work done here should be done on the server by core OpenERP # functions. This means this runs slower than it should due to network and # serialization latency. Even more, we lack some information to make it # work well. # Ensure the field is quoted, otherwise fields such as 'to' can't be sorted # and return an exception. orderby = '"%s"' % field if order == Qt.AscendingOrder: orderby += " ASC" else: orderby += " DESC" try: # Use call to catch exceptions ids = Rpc.session.call('/object', 'execute', self.resource, 'search', self._domain + self._filter, 0, 0, orderby, self._context) except: # In functional fields not stored in the database this will # cause an exception :( sortingResult = self.SortingNotPossible if sortingResult != self.SortingNotPossible: self.clear() # The load function will be in charge of loading and sorting elements self.load(ids) elif oldSortedField == self.sortedField or not self.ids(): # If last sorted field was the same as the current one, possibly only filter crierias have changed # so we might need to reload in this case. # If sorting is not possible, but no data was loaded yet, we load by model default field and order. # Otherwise, a view might not load any data. ids = self.rpc.search( self._domain + self._filter, 0, 0, False, self._context) self.clear() # The load function will be in charge of loading and sorting elements self.load(ids) self.emit(SIGNAL("sorting"), sortingResult) # Sorts the records of the group taking into account only loaded fields. def sortVisible(self, field, order): if self.updated and field == self.sortedField and order == self.sortedOrder: return if not self.updated: ids = self.rpc.search( self._domain + self._filter, 0, self.limit, False, self._context) self.clear() self.load(ids) if not field in self.fields: return self.ensureAllLoaded() if field != self.sortedField: # Sort only if last sorted field was different than current # We need this function here as we use the 'field' variable def ignoreCase(record): v = record.value(field) if isinstance(v, str) or isinstance(v, str): return v.lower() else: return v type = self.fields[field]['type'] if type == 'one2many' or type == 'many2many': self.records.sort(key=lambda x: len(x.value(field).group)) else: self.records.sort(key=ignoreCase) if order == Qt.DescendingOrder: self.records.reverse() else: # If we're only reversing the order, then reverse simply reverse if order != self.sortedOrder: self.records.reverse() self.sortedField = field self.sortedOrder = order self.updated = True # Emit recordsInserted() to ensure KooModel is updated. self.emit(SIGNAL('recordsInserted(int,int)'), 0, len(self.records) - 1) self.emit(SIGNAL("sorting"), self.SortingPossible) # @brief Removes all new records and marks all modified ones as not loaded. def cancel(self): for record in self.records[:]: if isinstance(record, Record): if not record.id: self.freeRecord(record) elif record.isModified(): record.cancel() else: if not record: self.freeRecord(record) # @brief Removes a record from the list (but not the record from the database). # # This function is used to take care signals are disconnected. def freeRecord(self, record): self.records.remove(record) if isinstance(record, Record): self.disconnect(record, SIGNAL( 'recordChanged( PyQt_PyObject )'), self.recordChanged) self.disconnect(record, SIGNAL( 'recordModified( PyQt_PyObject )'), self.recordModified) # @brief Returns True if any of the records in the group has been modified. def isModified(self): for record in self.records: if isinstance(record, Record): if record.isModified(): return True return False # @brief Returns True if the given record has been modified. def isRecordModified(self, id): for record in self.records: if isinstance(record, Record): if record.id == id: return record.isModified() elif record == id: return False return False # @brief Returns True if the given field is required in the RecordGroup, otherwise returns False. # Note that this is a flag for the whole group, but each record could have different values depending # on its state. def isFieldRequired(self, fieldName): required = self.fields[fieldName].get('required', False) if isinstance(required, bool): return required if isinstance(required, str) or isinstance(required, str): if required.lower() == 'true': return True if required.lower() == 'false': return False return bool(int(required))