Esempio n. 1
0
    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
Esempio n. 2
0
	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)
Esempio n. 3
0
 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)
Esempio n. 4
0
 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)
Esempio n. 5
0
    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
Esempio n. 6
0
    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)
Esempio n. 7
0
    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 = ''
Esempio n. 8
0
    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)
Esempio n. 9
0
    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
Esempio n. 10
0
    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)
Esempio n. 11
0
 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)
Esempio n. 12
0
 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"))
Esempio n. 13
0
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())
Esempio n. 14
0
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))
Esempio n. 15
0
    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 = ''