def setRecordGroup(self, group): if not group: self.group = None self._currentRecord = None self._currentRecordPosition = None # Call setCurrentRecord() after setting self.group # because it will emit a signal with the count of elements # which must be 0. self.setCurrentRecord(None) return self.name = group.resource self.resource = group.resource self.context = group.context() self.rpc = RpcProxy(self.resource) self.group = group self._currentRecord = None self._currentRecordPosition = None group.addFields(self.fields) self.fields.update(group.fields) if self.isVisible(): if self.group and self.group.count() and not self.currentRecord(): self.setCurrentRecord(self.group.recordByIndex(0)) else: # Note that we need to setCurrentRecord so it is initialized and # emits the recordMessage() signal. self.setCurrentRecord(None) self._firstTimeShown = False else: self._firstTimeShown = True
def setConditionalDefaults(self, field, value): ir = RpcProxy('ir.values') values = ir.get('default', '%s=%s' % (field, value), [(self.group.resource, False)], False, {}) data = {} for index, fname, value in values: data[fname] = value self.setDefaults(data)
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)
def set(self, record, value, test_state=False, modified=False): if not value: record.values[self.name] = False return ref_model, ident = value.split(',') Rpc2 = RpcProxy(ref_model) result = Rpc2.name_get([int(ident)], Rpc.session.context) if result: record.values[self.name] = ref_model, result[0] else: record.values[self.name] = False if modified: record.modified = True record.modified_fields.setdefault(self.name)
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 update(self): """ Reload the record group with current selected sort field, order, domain and filter :return: """ # 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)
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 = ''
def set(self, record, value, test_state=False, modified=False): if value and isinstance(value, (int, str)): Rpc2 = RpcProxy(self.attrs['relation']) result = Rpc2.name_get([value], Rpc.session.context) # In some very rare cases we may get an empty # list from the server so we just check it before # trying to store result[0] if result: record.values[self.name] = result[0] else: record.values[self.name] = value if modified: record.modified = True record.modified_fields.setdefault(self.name)
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
def set(self, record, value, test_state=False, modified=False): if value and isinstance(value, (int, str, unicode, long)): Rpc2 = RpcProxy(self.attrs['relation']) result = Rpc2.name_get([value], Rpc.session.context) # In some very rare cases we may get an empty # list from the server so we just check it before # trying to store result[0] if result: assert isinstance(result[0], (tuple, list)), result[0] record.values[self.name] = tuple(result[0]) elif isinstance(value, dict): record.values[self.name] = (value['id'], value.get('name', False)) else: record.values[self.name] = value if modified: record.modified = True record.modified_fields.setdefault(self.name)
def setConditionalDefaults(self, field, value): """ This functions is called whenever a field with 'change_default' attribute set to True is modified. The function sets all conditional defaults to each field. Conditional defaults is a mechanism by which the user can establish default values on fields, depending on the value of another field ( the 'change_default' field). An example of this case is the zip field in the partner model. :param field: :param value: :return: """ ir = RpcProxy('ir.values') values = ir.get('default', '%s=%s' % (field, value), [(self.group.resource, False)], False, {}) data = {} for index, fname, value in values: data[fname] = value self.setDefaults(data)
def showValue(self): value = self.record.value(self.name) if value: model, (id, name) = value self.uiModel.setCurrentIndex( self.uiModel.findText(self.invertedModels[model])) if not name: id, name = RpcProxy(model).name_get( [int(id)], Rpc.session.context)[0] self.setText(name) self.pushOpen.setIcon(QIcon(":/images/folder.png")) self.pushOpen.setToolTip(_("Open")) else: self.uiText.clear() self.uiText.setToolTip('') self.uiModel.setCurrentIndex(-1) self.pushOpen.setIcon(QIcon(":/images/find.png")) self.pushOpen.setToolTip(_("Search"))
class Screen(QScrollArea): """ The Screen class is a widget that provides an easy way of handling multiple views. This class is capable of managing various views of the same model and provides functions for moving to the next and previous record. If neither setViewTypes() nor setViewIds() are called, form and tree views (in this order) will be used. If you use only a single 'id' and say it's a 'tree', one you try to switchView the default 'form' view will be shown. If you only want to show the 'tree' view, use setViewTypes( [] ) or setViewTypes( ['tree'] ) When you add a new view by it's ID the type of the given view is removed from the list of view types. (See: addViewById() ) A Screen can emit four different signals: activated() -> Emited each time a record is activated (such as a double click on a list). closed() -> Emited when a view asks for the screen to be closed (such as a 'close' button on a form). currentChanged() -> Emited when the current record has been modified. recordMessage(int,int,int) -> Emited each time the current record changes (such as moving to previous or next). """ activated = pyqtSignal() closed = pyqtSignal() currentChangedSignal = pyqtSignal() recordMessage = pyqtSignal(int, int, int) statusMessage = pyqtSignal('QString') def __init__(self, parent=None): QScrollArea.__init__(self, parent) self.setFocusPolicy(Qt.NoFocus) # GUI Stuff self.setFrameShape(QFrame.NoFrame) self.setWidgetResizable(True) self.container = QWidget(self) self.setWidget(self.container) self.container.show() self.searchForm = SearchFormWidget(self.container) # @xtorello toreview self.searchForm.performSearch.connect(self.search) self.searchForm.keyDownPressed.connect(self.setFocusToView) self.searchForm.hide() self.containerView = None self.toolBar = ToolBar(self) self.setToolbarVisible(False) self.viewLayout = QVBoxLayout() self.layout = QHBoxLayout() self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addLayout(self.viewLayout) self.layout.addWidget(self.toolBar) vLay = QVBoxLayout(self.container) vLay.setContentsMargins(0, 0, 0, 0) vLay.addWidget(self.searchForm) vLay.addLayout(self.layout) # Non GUI Stuff self.actions = [] self._embedded = True self.views_preload = {} self.rpc = None self.name = None self.views = {} self.fields = {} self.group = None self._currentRecordPosition = None self._currentRecord = None self._currentView = -1 self._previousView = -1 self._viewQueue = ViewQueue() self._readOnly = False # The first time Screen is shown it will try to setCurrentRecord # if none is selected. self._firstTimeShown = True # @xtorello toreview signal to method integration self.currentChangedSignal.connect(self.currentChanged) def save(self): """ Dummy save :return: None :rtype: None """ pass def cancel(self): """ Dummy cancel :return: None :rtype: No """ pass def showEvent(self, event): if self._firstTimeShown: self._firstTimeShown = False # The first time Screen is shown/rendered we'll set current record # if none is yet selected. Note that this means that it's not # possible to explicitly make Screen NOT select any items. # # The reason for doing this is that it allows delayed loading of # embedded one2many and many2many fields because those not in the # main tab won't receive the 'showEvent' and won't try to load data # from the server, which greatly improves load time of some forms. # # If we don't do this here, and let 2many widgets to try to # implement it, they have to set current record on switchView, but # the problem is that label is kept as 0/0 (instead of 1/3, for # example), until user clicks switch view. if self.group and self.group.count() and not self.currentRecord(): self.setCurrentRecord(self.group.recordByIndex(0)) return QScrollArea.showEvent(self, event) def setFocusToView(self): """ Sets the focus to current view. :return: """ self.currentView().setFocus() def sizeHint(self): return self.container.sizeHint() def setPreloadedViews(self, views): self.views_preload = views def preloadedViews(self, views): return self.views_preload def setupViews(self, types, ids): """ Initializes the list of views using a types list and an ids list. Example: screen.setupViews( ['tree','form'], [False, False] ) :param types: :param ids: :return: None :rtype: None """ self._viewQueue.setup(types, ids) # Try to load only if model group has been set if self.name: self.switchView() def setViewIds(self, ids): self._viewQueue.setViewIds(ids) # Try to load only if model group has been set if self.name: self.switchView() def viewIds(self): return self._viewIds def setViewTypes(self, types): self._viewQueue.setViewTypes(types) # Try to load only if model group has been set if self.name: self.switchView() def setEmbedded(self, value): """ Sets whether the screen is embedded. Embedded screens don't show the search or toolbar widgets. By default embedded is True so it doesn't load unnecessary forms. :param value: :return: """ self._embedded = value self.setToolbarVisible(not value) self.setSearchFormVisible(not value) def embedded(self): """ Returns True if the Screen acts in embedded mode. :return: """ return self._embedded # @brief def setToolbarVisible(self, value): """ Allows making the toolbar visible or hidden. :param value: :return: """ self._toolbarVisible = value self.toolBar.setVisible(value) # @brief Allows making the search form visible or hidden. def setSearchFormVisible(self, value): self._searchFormVisible = value self.searchForm.setVisible(value) if value: if self.currentView() and self.currentView().showsMultipleRecords(): self.loadSearchForm() def loadSearchForm(self): if not Settings.value('koo.show_search_form', True): self.searchForm.hide() return if self.currentView().showsMultipleRecords() and not self._embedded: if not self.searchForm.isLoaded(): form = Rpc.session.execute( '/object', 'execute', self.resource, 'fields_view_get', False, 'form', self.context) tree = Rpc.session.execute( '/object', 'execute', self.resource, 'fields_view_get', False, 'tree', self.context) fields = form['fields'] fields.update(tree['fields']) arch = form['arch'] # Due to the fact that searchForm.setup() requires an XML and we want it to # be able to process select=True from both form and tree, we need to fake # an XML. dom = xml.dom.minidom.parseString(tree['arch'].encode('utf-8')) children = dom.childNodes[0].childNodes tempArch = '' for i in range(1, len(children)): tempArch += children[i].toxml() # Generic case when we need to remove the last occurance of </form> from form view arch = arch[0:form['arch'].rfind('</form>')] # Special case when form is replaced,we need to remove </form> index = arch.rfind('</form>') if index > 0: arch = arch[0:index] arch = arch + tempArch + '\n</form>' self.searchForm.setup( arch, fields, self.resource, self.group.domain()) self.searchForm.show() else: self.searchForm.hide() def setReadOnly(self, value): self._readOnly = value self.display() def isReadOnly(self): return self._readOnly def triggerAction(self): """ This function is expected to be used as a slot for an Action trigger signal. (as it will check the sender). It will call the Action.execute(id,ids) function. :return: """ # We expect a Screen.Action here action = self.sender() # If record has been modified save before executing the action. # Otherwise: # - With new records nothing is done without notifying the user # which isn't intuitive. # - With existing records it prints (for example) old values, which # isn't intuitive either. if self.isModified(): if not self.save(): return # Do not trigger action if there is no record selected. This is # only permitted for plugins. if not self.currentId() and action.type() != 'plugin': return ident = self.currentId() ids = self.selectedIds() if action.type() != 'relate': self.save() self.display() context = self.context.copy() if self.currentRecord(): # Plugins may be executed even if there's no current record. context.update(self.currentRecord().get()) action.execute(ident, ids, context) if action.type() != 'relate': self.reload() # @brief Sets the current widget of the Screen def setView(self, widget): if self.containerView: self.containerView.activated.disconnect(self.activate) self.containerView.currentChanged['PyQt_PyObject'].disconnect(self.currentChanged) self.containerView.statusMessage['QString'].disconnect(self.statusMessage['QString']) self.containerView.hide() self.containerView = widget # Calling first "loadSearchForm()" because when the search form is # hidden # it looks better to the user. If we show the widget and then hide the # search form it produces an ugly flickering. self.loadSearchForm() self.containerView.show() # @xtorello toreview zzz widget.activated.connect(self.activate) widget.currentChanged['PyQt_PyObject'].connect(self.currentChanged) widget.statusMessage['QString'].connect(self.statusMessage['QString']) # Set focus proxy so other widgets can try to setFocus to us # and the focus is set to the expected widget. self.setFocusProxy(self.containerView) self.ensureWidgetVisible(widget) self.updateGeometry() def activate(self): self.activated.emit() def close(self): # @xtorello @xbarnada TODO revisar si hi ha canvis pendents d'aplicar abans de tancar ## veure FormWidget.canClose / modifiedSave self.closed.emit() def search(self): """ Searches with the current parameters of the search form and loads the models that fit the criteria. :return: """ QApplication.setOverrideCursor(Qt.WaitCursor) try: value = self.searchForm.value() self.group.setFilter(value) # We setCurrentRecord( None ) first because self.group.update() # can emit some events that can be used to query the currentRecord(). # Previous record may not exist and cause an exception. self.setCurrentRecord(None) self.group.update() if self.group.count() > 0: self.setCurrentRecord(self.group.recordByIndex(0)) else: self.setCurrentRecord(None) self.display() except Rpc.RpcException as e: pass QApplication.restoreOverrideCursor() def updateSearchFormStatus(self): # Do not allow searches if group is modified. This avoids 'maximum recursion' errors if user # tries to search after modifying a record in an editable list. if self.group.isModified(): self.searchForm.setEnabled(False) else: self.searchForm.setEnabled(True) # Slot to recieve the signal from a view when the current item changes @pyqtSlot() def currentChanged(self, model): self.setCurrentRecord(model) self.currentChanged.emit() self.updateSearchFormStatus() def setRecordGroup(self, group): """ @brief Sets the RecordGroup this Screen should show. :param group: group RecordGroup object :return: None :rtype: None """ if not group: self.group = None self._currentRecord = None self._currentRecordPosition = None # Call setCurrentRecord() after setting self.group # because it will emit a signal with the count of elements # which must be 0. self.setCurrentRecord(None) return self.name = group.resource self.resource = group.resource self.context = group.context() self.rpc = RpcProxy(self.resource) self.group = group self._currentRecord = None self._currentRecordPosition = None group.addFields(self.fields) self.fields.update(group.fields) if self.isVisible(): if self.group and self.group.count() and not self.currentRecord(): self.setCurrentRecord(self.group.recordByIndex(0)) else: # Note that we need to setCurrentRecord so it is initialized and # emits the recordMessage() signal. self.setCurrentRecord(None) self._firstTimeShown = False else: self._firstTimeShown = True def currentRecord(self): """ Returns a reference the current record (Record). :return: """ # Checking _currentRecordPosition before count() can save a search() # call to the server because count() will execute a search() in the # server if no items have been loaded yet. What happens is that the # first time a screen with a TreeView is shown currentRecord() will be # called but there will be no currentRecord. TreeView then will set the # appropiate order by loading settings from the server through # restoreViewSettings, and KooModel will load data on demand. if self._currentRecordPosition and self._currentRecordPosition >= 0 and self.group.count(): # In some cases self._currentRecordPosition might point to a # position beyond group size. In this case ensure current record # is set to None. if self._currentRecordPosition >= self.group.count(): self.setCurrentRecord(None) return None # Use modelByIndex because this ensures all missing fields of the # model are loaded. For example, the model could have been loaded # in tree view but now might need more fields for form view. return self.group.modelByIndex(self._currentRecordPosition) else: # New records won't have that problem (althouth the problem might # be that fields haven't been created yet??) return self._currentRecord def setCurrentRecord(self, value): """ Sets the current record. Note that value will be a reference to the Record. :param value: :return: """ if self.group and self.group.recordExists(value): pos = self.group.indexOfRecord(value) else: pos = -1 # In order to "discover" if we need to update current record we # use "self._currentRecordPosition", because setRecordGroup sets # self._currentRecordPosition = None and then calls # setCurrentRecord( None ) # Which will make pos = -1 and will emit the recordMessage() signal. # # Trying to "discover" this with self._currentRecord will not work. if self._currentRecordPosition == pos: return self._currentRecord = value self._currentRecordPosition = pos if value and value.id: ident = value.id else: ident = -1 if self.group: count = self.group.count() else: count = 0 self.recordMessage.emit(pos, count, ident) if self._currentRecord: if self.currentView(): self.currentView().setSelected(self._currentRecord) def switchView(self, viewType=None): """ Switches the current view to the previous one. If viewType (such as 'calendar') is given it will switch to that view type. :param viewType: :return: """ if self.currentView(): self.currentView().store() if self.currentRecord() and (not self.group.recordExists(self.currentRecord())): self.setCurrentRecord(None) if viewType: self._previousView = self._currentView self._currentView = self._viewQueue.indexFromType(viewType) else: if self._currentView >= 0: # Swap currentView and previousView values self._currentView, self._previousView = self._previousView, self._currentView else: self._currentView = 0 # The second time switchView is executed, currentView might get -1 # and we want it in fact to be 1 ... self._currentView = abs(self._currentView) # ... unless there's only one view self._currentView = min( self._currentView, self._viewQueue.count() - 1) # If the view can show multiple records we set the default loading # method in the record group, otherwise we set the load one-by-one # mode, so only the current record is loaded. This improves performance # on switching views from list to form with forms that contain a lot # of fields. if self.currentView().showsMultipleRecords(): self.group.setLoadOneByOne(False) else: self.group.setLoadOneByOne(True) self.setView(self.currentView()) if self.currentRecord(): self.currentRecord().setValidate() self.display() def addViewByIdAndType(self, id, type, display=False): """ Adds a view given it's id and type. This function is needed to resemble server's fields_view_get function. This function wasn't necessary but accounting module needs it because it tries to open a view with it's ID but reimplements fields_view_get and checks the view type. see AddViewById see AddViewByType :param id: :param type: :param display: :return: """ if type in self.views_preload: return self.addView( self.views_preload[type]['arch'], self.views_preload[type]['fields'], display, toolbar=self.views_preload[type].get('toolbar', False), ident=self.views_preload[type].get('view_id', False) ) else: # By now we set toolbar to True always. Even when the Screen is # embedded. # This way we don't force setting the embedded option in the # class constructor and can be set later. view = self.rpc.fields_view_get(id, type, self.context, True) return self.addView( view['arch'], view['fields'], display, toolbar=view.get('toolbar', False), ident=view.get('view_id', False) ) def addViewById(self, id, display=False): """ Adds a view given its id. see AddViewByType seee AddViewByIdAndType :param id: View id to load or False if you want to load given view_type. :param display: Whether you want the added view to be shown (True) or only loaded (False). :return: The view widget """ # By now we set toolbar to True always. Even when the Screen is embedded. # This way we don't force setting the embedded option in the class constructor # and can be set later. view = self.rpc.fields_view_get(id, False, self.context, True) return self.addView(view['arch'], view['fields'], display, toolbar=view.get('toolbar', False), id=id) def addViewByType(self, type, display=False): """ see AddViewById see AddViewByIdAndType :param type: View type ('form', 'tree', 'calendar', 'graph'...). :param display: Whether you want the added view to be shown (True) or only loaded (False). :return: The view widget """ if type in self.views_preload: return self.addView(self.views_preload[type]['arch'], self.views_preload[type]['fields'], display, toolbar=self.views_preload[type].get('toolbar', False), id=self.views_preload[type].get('view_id', False)) else: # By now we set toolbar to True always. Even when the Screen is embedded. # This way we don't force setting the embedded option in the class constructor # and can be set later. view = self.rpc.fields_view_get(False, type, self.context, True) return self.addView(view['arch'], view['fields'], display, toolbar=view.get('toolbar', False), ident=view.get('view_id', False)) def _parse_fields(self, node, fields): if node.nodeType == node.ELEMENT_NODE: if node.localName == 'field': attrs = Common.nodeAttributes(node) if attrs.get('widget', False): if attrs['widget'] == 'one2many_list': attrs['widget'] = 'one2many' attrs['type'] = attrs['widget'] try: fields[attrs['name']].update(attrs) except: print("-" * 30, "\n malformed tag for :", attrs) print("-" * 30) raise for node2 in node.childNodes: self._parse_fields(node2, fields) def addView(self, arch, fields, display=False, toolbar=None, ident=False): """ Adds a view given it's XML description and fields :param arch: typically 'arch' field returned by model fields_view_get() function. :type arch: str :param fields: Fields dictionary containing each field (widget) properties. :param display: Whether you want the added view to be shown (True) or only loaded (False) :param toolbar: Toolbar information as returned from fields_view_get server function. :param ident: View id. This parameter is used for storing and loading settings for the view. If id=False, no :return: The view widget """ if toolbar is None: toolbar = {} dom = xml.dom.minidom.parseString(arch.encode('utf-8')) self._parse_fields(dom, fields) self.group.addFields(fields) self.fields = self.group.fields view = ViewFactory.create(ident, self, self.resource, dom, self.fields) self.viewLayout.addWidget(view) self.setOnWriteFunction(view.onWriteFunction()) # Load view settings if not self.group.updated: domain = self.group.domain() self.group.setDomainForEmptyGroup() view.setViewSettings(ViewSettings.load(view.id)) self.group.setDomain(domain) else: view.setViewSettings(ViewSettings.load(view.id)) self.views[view.viewType()] = view self.loadActions(toolbar) if display: if not self._viewQueue.typeExists(view.viewType()): self._viewQueue.addViewType(view.viewType()) self._currentView = self._viewQueue.indexFromType(view.viewType()) self.currentView().display(self.currentRecord(), self.group) self.setView(view) return view def loadActions(self, actions): """ Loads all actions associated with the current model including plugins. :param actions: :return: """ self.actions = ActionFactory.create(self, actions, self.resource) if self.actions: for action in self.actions: action.triggered.connect(self.triggerAction) # If there's only one action it will be the 'Print Screen' action # that is added "manually" by ActionFactory. In those cases in which # Print Screen is the only action we won't show it in the toolbar. # We don't consider Plugins a good reason to show the toolbar # either. # This way dashboards won't show the toolbar, though the option will # remain available in the menu for those screens that don't have any # actions configured in the server, but Print Screen can be useful. if len(self.actions) > 0 and Settings.value('koo.show_toolbar') and self._toolbarVisible: self.toolBar.setup(self.actions) self.toolBar.show() else: self.toolBar.hide() def new(self, default=True, context=None): """ Creates a new record in the current model. If the current view is not editable it will automatically switch to a view that allows editing. :param default: :param context: :return: """ if context is None: context = {} if self.currentView() and self.currentView().showsMultipleRecords() \ and self.currentView().isReadOnly(): self.switchView('form') ctx = self.context.copy() ctx.update(context) record = self.group.create( default, self.newRecordPosition(), self.group.domain(), ctx) if self.currentView(): self.currentView().reset() self.setCurrentRecord(record) self.display() if self.currentView(): self.currentView().startEditing() return self.currentRecord() def newRecordPosition(self): """ Returns 0 or -1 depending on new records policy for the current view. If the view adds on top it will return 0, otherwise it will return -1 :return: :rtype: int """ if self.currentView() and self.currentView().addOnTop(): return 0 else: return -1 def addOnTop(self): """ Returns whether new records will be added on top or on the bottom. Note that this is a property defined by the current view. If there's no current view it will return False. :return: """ if self.currentView(): return self.currentView().addOnTop() else: return False def setOnWriteFunction(self, functionName): """ Sets the on_write function. That is the function (in the server) that must be called after storing a record. :param functionName: :return: """ self.group.setOnWriteFunction(functionName) def save(self): """ Stores all modified models. :return: """ if not self.currentRecord(): return False self.currentView().store() ident = False if self.currentRecord().validate(): ident = self.currentRecord().save(reload=True) else: self.currentView().display(self.currentRecord(), self.group) return False if self.currentView().showsMultipleRecords(): for record in self.group.modifiedRecords(): if record.validate(): ident = record.save(reload=True) else: self.setCurrentRecord(record) self.display() return False self.display() self.display() return ident # @brief Reload current model and refreshes the view. # # If the current view only shows a single record, only the current one will # be reloaded. If the view shows multiple records it will reload the whole model. def reload(self): if not self.currentView().showsMultipleRecords(): if self.currentRecord(): self.currentRecord().reload() self.display() return id = 0 idx = -1 if self.currentRecord(): id = self.currentId() idx = self.group.indexOfRecord(self.currentRecord()) # If we didn't cancel before updating, update may not work if there were modified # records. So we first ensure the group is not set as modified and then update/reload # it. Note that not cancelling here has other side effects such as that group.sortAll # may send a "could not sort" signal and cause an infinite recursion exception. self.cancel() self.group.update() if id: record = self.group.modelById(id) if record: self.setCurrentRecord(record) else: # If what it was the current record no longer exists # at least keep index position idx = min(idx, self.group.count() - 1) if idx >= 0: self.setCurrentRecord(self.group.recordByIndex(idx)) else: self.setCurrentRecord(None) else: self.setCurrentRecord(None) self.display() # @brief Removes all new records and marks all modified ones as not loaded. def cancel(self): idx = -1 if self.currentRecord(): idx = self.group.indexOfRecord(self.currentRecord()) self.group.cancel() if idx != -1: idx = min(idx, self.group.count() - 1) if idx >= 0: self.setCurrentRecord(self.group.recordByIndex(idx)) else: self.setCurrentRecord(None) self.display() def currentView(self): """ Returns a reference to the current view. :return: """ if self._currentView < 0: return None type = self._viewQueue.typeFromIndex(self._currentView) if not type in self.views: (ident, type) = self._viewQueue.viewFromType(type) self.addViewByIdAndType(ident, type) return self.views[type] # @brief Returns a dictionary with all field values for the current record. def get(self): if not self.currentRecord(): return None self.currentView().store() return self.currentRecord().get() def isModified(self): """ Returns True if any record has been modified. Returns False otherwise. :return: """ if not self.currentRecord(): return False self.currentView().store() res = False if self.currentView().showsMultipleRecords(): return self.group.isModified() else: return self.currentRecord().isModified() def remove(self, unlink=False): """ Removes all selected ids. :param unlink: If unlink is False (the default) records are only removed from the list. :type unlink: bool :return:If unlink is True records will be removed from the server too. :rtype: bool """ records = self.selectedRecords() if unlink and records: # Remove records with id None as they would cause an exception # trying to remove from the server idsToUnlink = [x.id for x in records if x.id != None] # It could be that after removing records with id == None # there are no records to remove from the database. That is, # all records that should be removed are new and not stored yet. if idsToUnlink: unlinked = self.rpc.unlink(idsToUnlink) # Try to be consistent with database # If records could not be removed from the database # don't remove them on the client. Don't report it directly # though as probably an exception (Warning) has already # been shown to the user. if not unlinked: return False if records: # Set no current record, so refreshes in the middle of the removal # process (caused by signals) do not crash. # Note that we want to ensure there are ids to remove so we don't # setCurrentRecord(None) if it's not strictly necessary. idx = self._currentRecordPosition self.setCurrentRecord(None) self.group.remove(records) if self.group.count(): idx = min(idx, self.group.count() - 1) self.setCurrentRecord(self.group.recordByIndex(idx)) else: self.setCurrentRecord(None) self.display() if records: return True else: return False def load(self, ids, addOnTop=False): """ Loads the given ids to the RecordGroup and refreshes the view. :param ids: :param addOnTop: :return: """ self.currentView().reset() self.group.load(ids, addOnTop) if ids: self.display(ids[0]) else: self.setCurrentRecord(None) self.display() def display(self, id=None): """ Displays the record with id 'id' or refreshes the current record if no id is given. :param id: :return: """ if id: self.setCurrentRecord(self.group[id]) if self.views: self.currentView().setReadOnly(self.isReadOnly()) self.currentView().display(self.currentRecord(), self.group) def displayNext(self): """ Moves current record to the next one in the list and displays it in the current view. :return: None """ self.currentView().store() if self.group.recordExists(self.currentRecord()): idx = self.group.indexOfRecord(self.currentRecord()) idx = (idx + 1) % self.group.count() self.setCurrentRecord(self.group.modelByIndex(idx)) else: self.setCurrentRecord( self.group.count() and self.group.modelByIndex(0) or None) if self.currentRecord(): self.currentRecord().setValidate() self.display() def displayPrevious(self): """ Moves current record to the previous one in the list and displays it in the current view. :return: None """ self.currentView().store() if self.group.recordExists(self.currentRecord()): #idx = self.group.records.index(self.currentRecord())-1 idx = self.group.indexOfRecord(self.currentRecord()) - 1 if idx < 0: idx = self.group.count() - 1 self.setCurrentRecord(self.group.modelByIndex(idx)) else: self.setCurrentRecord( self.group.count() and self.group.modelByIndex(-1) or None) if self.currentRecord(): self.currentRecord().setValidate() self.display() def selectedIds(self): """ Returns all selected record ids. Note that if there are new unsaved records, they might all have ID=None. You're probably looking for selectedRecords() function. see selectedRecords :return: """ records = self.currentView().selectedRecords() ids = [record.id for record in records] return ids def selectedRecords(self): """ Returns all selected records :return: """ return self.currentView().selectedRecords() def currentId(self): """ Returns the current record id. :return: """ if self.currentRecord(): return self.currentRecord().id else: return None def clear(self): """ Clears the list of records and refreshes the view. Note that this won't remove the records from the database. But clears the records from the model. It means that sometimes you might want to use setRecordGroup( None ) instead of calling clear(). This is what OneToMany and ManyToMany widgets do, for example. see remove() :return: """ self.group.clear() self.display() def storeViewSettings(self): """ Stores settings of all opened views :return: """ for view in list(self.views.values()): ViewSettings.store(view.id, view.viewSettings())
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))
def __init__(self, resource, fields=None, ids=None, parent=None, context=None): """ 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 Context for RPC calls. """ 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 is None: self.fields = {} else: self.fields = fields self.fieldObjects = {} self.loadFieldObjects(list(self.fields.keys())) self.records = [] # @xtorello toreview signal to method integration # self.recordChangedSignal.connect(self.recordChanged) 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 = ''