From 405464b3ac202f0998e0f606c3bd51fa5a51ff18 Mon Sep 17 00:00:00 2001
From: Arnaud Fontaine <arnaud.fontaine@nexedi.com>
Date: Tue, 25 Aug 2020 07:20:05 +0900
Subject: [PATCH] WIP: ZODB Components: erp5_core: Migrate Preference and
 PreferenceTool.

XXX: Creating ERP5Site works fine and testPreferences succeeds but Upgrader
     requires access to UI and portal_preferences.getXXX() is used everywhere.
---
 product/ERP5/ERP5Site.py                      |   1 -
 .../document.erp5.Preference.py}              |   4 +-
 .../document.erp5.Preference.xml              | 110 +++++++
 .../tool.erp5.PreferenceTool.py               | 294 +++++++++++++++++
 .../tool.erp5.PreferenceTool.xml              | 110 +++++++
 .../ToolTemplateItem/portal_preferences.xml   | 139 ++++++++
 .../erp5_core/bt/template_document_id_list    |   1 +
 .../bt/template_tool_component_id_list        |   1 +
 .../erp5_core/bt/template_tool_id_list        |   1 +
 .../ERP5Form/Document/PreferenceToolType.py   |  44 ++-
 product/ERP5Form/PreferenceTool.py            | 309 +-----------------
 product/ERP5Form/__init__.py                  |   3 +-
 12 files changed, 704 insertions(+), 313 deletions(-)
 rename product/{ERP5Form/Document/Preference.py => ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.py} (92%)
 create mode 100644 product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.xml
 create mode 100644 product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.py
 create mode 100644 product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.xml
 create mode 100644 product/ERP5/bootstrap/erp5_core/ToolTemplateItem/portal_preferences.xml

diff --git a/product/ERP5/ERP5Site.py b/product/ERP5/ERP5Site.py
index ff6bfa0c17..37e5be88fb 100644
--- a/product/ERP5/ERP5Site.py
+++ b/product/ERP5/ERP5Site.py
@@ -2133,7 +2133,6 @@ class ERP5Generator(PortalGenerator):
 
     # Add ERP5Form Tools
     addERP5Tool(p, 'portal_selections', 'Selection Tool')
-    addERP5Tool(p, 'portal_preferences', 'Preference Tool')
 
     # Add Message Catalog
     if not 'Localizer' in p.objectIds():
diff --git a/product/ERP5Form/Document/Preference.py b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.py
similarity index 92%
rename from product/ERP5Form/Document/Preference.py
rename to product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.py
index 10431ac040..6316a91f1b 100644
--- a/product/ERP5Form/Document/Preference.py
+++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.py
@@ -28,10 +28,8 @@
 
 from AccessControl import ClassSecurityInfo
 
-from Products.ERP5Type import Permissions, PropertySheet, Constraint
+from Products.ERP5Type import Permissions, PropertySheet
 from Products.ERP5Type.Core.Folder import Folder
-from Products.CMFCore.utils import getToolByName
-from Products.ERP5Form.PreferenceTool import PreferenceTool
 
 class Priority:
   """ names for priorities
diff --git a/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.xml b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.xml
new file mode 100644
index 0000000000..0ff2abdfe0
--- /dev/null
+++ b/product/ERP5/bootstrap/erp5_core/DocumentTemplateItem/portal_components/document.erp5.Preference.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Document Component" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>default_reference</string> </key>
+            <value> <string>Preference</string> </value>
+        </item>
+        <item>
+            <key> <string>default_source_reference</string> </key>
+            <value> <string>Products.ERP5Form.Document.Preference</string> </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>document.erp5.Preference</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Document Component</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>text_content_error_message</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>text_content_warning_message</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>version</string> </key>
+            <value> <string>erp5</string> </value>
+        </item>
+        <item>
+            <key> <string>workflow_history</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>component_validation_workflow</string> </key>
+                    <value>
+                      <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+                    </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_log</string> </key>
+            <value>
+              <list>
+                <dictionary>
+                  <item>
+                      <key> <string>action</string> </key>
+                      <value> <string>validate</string> </value>
+                  </item>
+                  <item>
+                      <key> <string>validation_state</string> </key>
+                      <value> <string>validated</string> </value>
+                  </item>
+                </dictionary>
+              </list>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.py b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.py
new file mode 100644
index 0000000000..1e3f4e03d5
--- /dev/null
+++ b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.py
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
+#                    Jerome Perrin <jerome@nexedi.com>
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+##############################################################################
+
+from AccessControl import ClassSecurityInfo
+from AccessControl.SecurityManagement import getSecurityManager,\
+                          setSecurityManager, newSecurityManager
+
+from Products.ERP5Type.Globals import InitializeClass, DTMLFile
+
+from Products.CMFCore.utils import getToolByName
+from Products.ERP5Type.Tool.BaseTool import BaseTool
+from Products.ERP5Type import Permissions
+from Products.ERP5Type.Cache import CachingMethod
+from Products.ERP5Type.Utils import convertToUpperCase
+from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
+from Products.ERP5Form import _dtmldir
+from BTrees.OIBTree import OIBTree
+from Products.ERP5Form.Document.PreferenceToolType import _marker
+from Products.ERP5Form.PreferenceTool import Priority
+
+class PreferenceTool(BaseTool):
+  """
+    PreferenceTool manages User Preferences / User profiles.
+
+    TODO:
+      - make the preference tool an action provider (templates)
+  """
+  id            = 'portal_preferences'
+  meta_type     = 'ERP5 Preference Tool'
+  portal_type   = 'Preference Tool'
+  title         = 'Preferences'
+  allowed_types = ( 'ERP5 Preference',)
+  security      = ClassSecurityInfo()
+
+  aq_preference_generated = False
+
+  security.declareProtected(
+       Permissions.ManagePortal, 'manage_overview' )
+  manage_overview = DTMLFile( 'explainPreferenceTool', _dtmldir )
+
+  security.declarePrivate('manage_afterAdd')
+  def manage_afterAdd(self, item, container) :
+    """ init the permissions right after creation """
+    item.manage_permission(Permissions.AddPortalContent,
+          ['Member', 'Author', 'Manager'])
+    item.manage_permission(Permissions.AddPortalFolders,
+          ['Member', 'Author', 'Manager'])
+    item.manage_permission(Permissions.View,
+          ['Member', 'Auditor', 'Manager'])
+    item.manage_permission(Permissions.CopyOrMove,
+          ['Member', 'Auditor', 'Manager'])
+    item.manage_permission(Permissions.ManageProperties,
+          ['Manager'], acquire=0)
+    item.manage_permission(Permissions.SetOwnPassword,
+          ['Member', 'Author', 'Manager'])
+    BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)
+
+  security.declarePublic('getPreference')
+  def getPreference(self, pref_name, default=_marker) :
+    """ get the preference on the most appopriate Preference object. """
+    method = getattr(self, 'get%s' % convertToUpperCase(pref_name), None)
+    if method is not None:
+      return method(default)
+    if default is _marker:
+      return None
+    return default
+
+  security.declareProtected(Permissions.ModifyPortalContent, "setPreference")
+  def setPreference(self, pref_name, value) :
+    """ set the preference on the active Preference object"""
+    self.getActivePreference()._edit(**{pref_name:value})
+
+  def _getSortedPreferenceList(self, sql_catalog_id=None):
+    """ return the most appropriate preferences objects,
+        sorted so that the first in the list should be applied first
+    """
+    tv = getTransactionalVariable()
+    security_manager = getSecurityManager()
+    user = security_manager.getUser()
+    acl_users = self.getPortalObject().acl_users
+    try:
+      # reset a security manager without any proxy role or unrestricted method,
+      # wich affects the catalog search that we do to find applicable
+      # preferences.
+      actual_user = acl_users.getUserById(user.getId())
+      if actual_user is not None:
+        newSecurityManager(None, actual_user.__of__(acl_users))
+      tv_key = 'PreferenceTool._getSortedPreferenceList/%s/%s' % (user.getId(),
+                                                                  sql_catalog_id)
+      if tv.get(tv_key, None) is None:
+        prefs = []
+        # XXX will also cause problems with Manager (too long)
+        # XXX For manager, create a manager specific preference
+        #                  or better solution
+        user_is_manager = 'Manager' in user.getRolesInContext(self)
+        for pref in self.searchFolder(portal_type='Preference', sql_catalog_id=sql_catalog_id):
+          pref = pref.getObject()
+            # XXX quick workaround so that managers only see user preference
+            #     they actually own.
+          if pref is not None and (not user_is_manager or
+                                   pref.getPriority() != Priority.USER or
+                                   pref.getOwnerTuple()[1] == user.getId()):
+            if pref.getProperty('preference_state',
+                                'broken') in ('enabled', 'global'):
+              prefs.append(pref)
+        prefs.sort(key=lambda x: x.getPriority(), reverse=True)
+        # add system preferences before user preferences
+        sys_prefs = [x.getObject() for x in self.searchFolder(portal_type='System Preference', sql_catalog_id=sql_catalog_id) \
+                     if x.getObject().getProperty('preference_state', 'broken') in ('enabled', 'global')]
+        sys_prefs.sort(key=lambda x: x.getPriority(), reverse=True)
+        preference_list = sys_prefs + prefs
+        tv[tv_key] = preference_list
+      return tv[tv_key]
+    finally:
+      setSecurityManager(security_manager)
+
+  def _getActivePreferenceByPortalType(self, portal_type):
+    enabled_prefs = self._getSortedPreferenceList()
+    if len(enabled_prefs) > 0 :
+      try:
+        return [x for x in enabled_prefs
+            if x.getPortalType() == portal_type][0]
+      except IndexError:
+        pass
+    return None
+
+  security.declareProtected(Permissions.View, 'getActivePreference')
+  def getActivePreference(self) :
+    """ returns the current preference for the user.
+       Note that this preference may be read only. """
+    return self._getActivePreferenceByPortalType('Preference')
+
+  security.declareProtected(Permissions.View, 'clearCache')
+  def clearCache(self, preference):
+    """ clear cache when a preference is modified.
+    This is called by an interaction workflow on preferences.
+    """
+    self._getCacheId() # initialize _preference_cache if needed.
+    if preference.getPriority() == Priority.USER:
+      user_id = getSecurityManager().getUser().getId()
+      self._preference_cache[user_id] = \
+          self._preference_cache.get(user_id, 0) + 1
+    self._preference_cache[None] = self._preference_cache.get(None, 0) + 1
+
+  def _getCacheId(self):
+    """Return a cache id for preferences.
+
+    We use:
+     - user_id: because preferences are always different by user
+     - self._preference_cache[user_id] which is increased everytime a user
+       preference is modified
+     - self._preference_cache[None] which is increased everytime a global
+       preference is modified
+    """
+    user_id = getSecurityManager().getUser().getId()
+    try:
+      self._preference_cache
+    except AttributeError:
+      self._preference_cache = OIBTree()
+    return self._preference_cache.get(None), self._preference_cache.get(user_id), user_id
+
+  security.declareProtected(Permissions.View, 'getActiveUserPreference')
+  def getActiveUserPreference(self) :
+    """ returns the current user preference for the user.
+    If no preference exists, then try to create one with `createUserPreference`
+    type based method.
+
+    This method returns a preference that the user will be able to edit or
+    None, if `createUserPreference` refused to create a preference.
+
+    It is intendended for "click here to edit your preferences" actions.
+    """
+    active_preference = self.getActivePreference()
+    if active_preference is None or active_preference.getPriority() != Priority.USER:
+      # If user does not have a preference, let's try to create one
+      user = self.getPortalObject().portal_membership.getAuthenticatedMember().getUserValue()
+      if user is not None:
+        createUserPreference = user.getTypeBasedMethod('createUserPreference')
+        if createUserPreference is not None:
+          active_preference = createUserPreference()
+    return active_preference
+
+  security.declareProtected(Permissions.View, 'getActiveSystemPreference')
+  def getActiveSystemPreference(self) :
+    """ returns the current system preference for the user.
+       Note that this preference may be read only. """
+    return self._getActivePreferenceByPortalType('System Preference')
+
+  security.declareProtected(Permissions.View, 'getDocumentTemplateList')
+  def getDocumentTemplateList(self, folder=None): # pylint: disable=arguments-differ
+    """ returns all document templates that are in acceptable Preferences
+        based on different criteria such as folder, portal_type, etc.
+    """
+    if folder is None:
+      # as the preference tool is also a Folder, this method is called by
+      # page templates to get the list of document templates for self.
+      folder = self
+
+    # We must set the user_id as a parameter to make sure each
+    # user can get a different cache
+    def _getDocumentTemplateList(user_id, portal_type=None):
+      acceptable_template_list = []
+      for pref in self._getSortedPreferenceList() :
+        for doc in pref.contentValues(portal_type=portal_type) :
+          acceptable_template_list.append(doc.getRelativeUrl())
+      return acceptable_template_list
+    _getDocumentTemplateList = CachingMethod(
+        _getDocumentTemplateList,
+        'portal_preferences.getDocumentTemplateList.{}'.format(self._getCacheId()),
+        cache_factory='erp5_ui_long')
+
+    allowed_content_types = [pti.id for pti in folder.allowedContentTypes()]
+    user_id = getToolByName(self, 'portal_membership').getAuthenticatedMember().getId()
+    template_list = []
+    for portal_type in allowed_content_types:
+      for template_url in _getDocumentTemplateList(user_id, portal_type=portal_type):
+        template = self.restrictedTraverse(template_url, None)
+        if template is not None:
+          template_list.append(template)
+    return template_list
+
+  security.declareProtected(Permissions.ManagePortal,
+                            'createActiveSystemPreference')
+  def createActiveSystemPreference(self):
+    """ Create a System Preference and enable it if there is no other
+        enabled System Preference in present.
+    """
+    if self.getActiveSystemPreference() is not None:
+      raise ValueError("Another Active Preference already exists.")
+    system_preference = self.newContent(portal_type='System Preference')
+    system_preference.enable()
+
+  security.declareProtected(Permissions.ManagePortal,
+                            'createPreferenceForUser')
+  def createPreferenceForUser(self, user_id, enable=True):
+    """Creates a preference for a given user, and optionnally enable the
+    preference.
+    """
+    user_folder = self.acl_users
+    user = user_folder.getUserById(user_id)
+    if user is None:
+      raise ValueError("User %r not found" % (user_id, ))
+    security_manager = getSecurityManager()
+    try:
+      newSecurityManager(None, user.__of__(user_folder))
+      preference = self.newContent(portal_type='Preference')
+      if enable:
+        preference.enable()
+      return preference
+    finally:
+      setSecurityManager(security_manager)
+
+  security.declarePublic('isAuthenticationPolicyEnabled')
+  def isAuthenticationPolicyEnabled(self) :
+    """
+    Return True if authentication policy is enabled.
+    This method exists here due to bootstrap issues.
+    It should work even if erp5_authentication_policy bt5 is not installed.
+    """
+    # isPreferredAuthenticationPolicyEnabled exisss if property sheets from
+    # erp5_authentication_policy are installed.
+    method = getattr(self, 'isPreferredAuthenticationPolicyEnabled', None)
+    if method is not None and method():
+      return True
+    # if it does not exist, for sure authentication policy is not enabled.
+    return False
+
+InitializeClass(PreferenceTool)
\ No newline at end of file
diff --git a/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.xml b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.xml
new file mode 100644
index 0000000000..70cbf6b59a
--- /dev/null
+++ b/product/ERP5/bootstrap/erp5_core/ToolComponentTemplateItem/portal_components/tool.erp5.PreferenceTool.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Tool Component" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>default_reference</string> </key>
+            <value> <string>PreferenceTool</string> </value>
+        </item>
+        <item>
+            <key> <string>default_source_reference</string> </key>
+            <value> <string>Products.ERP5Form.PreferenceTool</string> </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>tool.erp5.PreferenceTool</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Tool Component</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>text_content_error_message</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>text_content_warning_message</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>version</string> </key>
+            <value> <string>erp5</string> </value>
+        </item>
+        <item>
+            <key> <string>workflow_history</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary>
+                <item>
+                    <key> <string>component_validation_workflow</string> </key>
+                    <value>
+                      <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+                    </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_log</string> </key>
+            <value>
+              <list>
+                <dictionary>
+                  <item>
+                      <key> <string>action</string> </key>
+                      <value> <string>validate</string> </value>
+                  </item>
+                  <item>
+                      <key> <string>validation_state</string> </key>
+                      <value> <string>validated</string> </value>
+                  </item>
+                </dictionary>
+              </list>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/bootstrap/erp5_core/ToolTemplateItem/portal_preferences.xml b/product/ERP5/bootstrap/erp5_core/ToolTemplateItem/portal_preferences.xml
new file mode 100644
index 0000000000..d263ed75ce
--- /dev/null
+++ b/product/ERP5/bootstrap/erp5_core/ToolTemplateItem/portal_preferences.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Preference Tool" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_Add_portal_content_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Member</string>
+                <string>Author</string>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Add_portal_folders_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Member</string>
+                <string>Author</string>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Copy_or_Move_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Member</string>
+                <string>Auditor</string>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Manage_properties_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Set_own_password_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Member</string>
+                <string>Author</string>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_View_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Member</string>
+                <string>Auditor</string>
+                <string>Manager</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_count</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_mt_index</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_preference_cache</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_tree</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAU=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>portal_preferences</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="Length" module="BTrees.Length"/>
+    </pickle>
+    <pickle> <int>0</int> </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="OOBTree" module="BTrees.OOBTree"/>
+    </pickle>
+    <pickle>
+      <none/>
+    </pickle>
+  </record>
+  <record id="4" aka="AAAAAAAAAAQ=">
+    <pickle>
+      <global name="OIBTree" module="BTrees.OIBTree"/>
+    </pickle>
+    <pickle>
+      <tuple>
+        <tuple>
+          <tuple>
+            <tuple>
+              <none/>
+              <int>30</int>
+              <string>zope</string>
+              <int>29</int>
+            </tuple>
+          </tuple>
+        </tuple>
+      </tuple>
+    </pickle>
+  </record>
+  <record id="5" aka="AAAAAAAAAAU=">
+    <pickle>
+      <global name="OOBTree" module="BTrees.OOBTree"/>
+    </pickle>
+    <pickle>
+      <none/>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list
index e442714d8c..2822977baa 100644
--- a/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list
+++ b/product/ERP5/bootstrap/erp5_core/bt/template_document_id_list
@@ -30,6 +30,7 @@ document.erp5.PackingList
 document.erp5.Path
 document.erp5.PredicateGroup
 document.erp5.PredicateMatrix
+document.erp5.Preference
 document.erp5.Project
 document.erp5.ScriptConstraint
 document.erp5.SimulationMovement
diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list
index a0b0e927ef..71649518d5 100644
--- a/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list
+++ b/product/ERP5/bootstrap/erp5_core/bt/template_tool_component_id_list
@@ -10,6 +10,7 @@ tool.erp5.IntrospectionTool
 tool.erp5.NotificationTool
 tool.erp5.OrderTool
 tool.erp5.PasswordTool
+tool.erp5.PreferenceTool
 tool.erp5.RuleTool
 tool.erp5.SessionTool
 tool.erp5.SimulationTool
diff --git a/product/ERP5/bootstrap/erp5_core/bt/template_tool_id_list b/product/ERP5/bootstrap/erp5_core/bt/template_tool_id_list
index ec3fdb76f5..4034af906c 100644
--- a/product/ERP5/bootstrap/erp5_core/bt/template_tool_id_list
+++ b/product/ERP5/bootstrap/erp5_core/bt/template_tool_id_list
@@ -10,6 +10,7 @@ portal_introspections
 portal_notifications
 portal_orders
 portal_password
+portal_preferences
 portal_rules
 portal_sessions
 portal_simulation
diff --git a/product/ERP5Form/Document/PreferenceToolType.py b/product/ERP5Form/Document/PreferenceToolType.py
index a0b3653781..04da63c6eb 100644
--- a/product/ERP5Form/Document/PreferenceToolType.py
+++ b/product/ERP5Form/Document/PreferenceToolType.py
@@ -33,7 +33,49 @@ from Products.ERP5Type.dynamic.accessor_holder import AccessorHolderType
 
 from Products.ERP5Type.Accessor.TypeDefinition import list_types
 from Products.ERP5Type.Utils import convertToUpperCase
-from Products.ERP5Form.PreferenceTool import PreferenceMethod
+
+class func_code: pass
+
+from MethodObject import Method
+from Products.ERP5Type.Cache import CachingMethod
+_marker = object()
+class PreferenceMethod(Method):
+  """ A method object that lookup the attribute on preferences. """
+  # This is required to call the method form the Web
+  func_code = func_code()
+  func_code.co_varnames = ('self', )
+  func_code.co_argcount = 1
+  func_defaults = ()
+
+  def __init__(self, attribute, default):
+    self.__name__ = self._preference_getter = attribute
+    self._preference_default = default
+    self._preference_cache_id = 'PreferenceTool.CachingMethod.%s' % attribute
+
+  def __call__(self, instance, default=_marker, *args, **kw):
+    def _getPreference(default, *args, **kw):
+      # XXX: sql_catalog_id is passed when calling getPreferredArchive
+      # This is inconsistent with regular accessor API, and indicates that
+      # there is a design problem in current archive API.
+      sql_catalog_id = kw.pop('sql_catalog_id', None)
+      for pref in instance._getSortedPreferenceList(sql_catalog_id=sql_catalog_id):
+        value = getattr(pref, self._preference_getter)(_marker, *args, **kw)
+        # XXX Due to UI limitation, null value is treated as if the property
+        #     was not defined. The drawback is that it is not possible for a
+        #     user to mask a non-null global value with a null value.
+        if value not in (_marker, None, '', (), []):
+          return value
+      if default is _marker:
+        return self._preference_default
+      return default
+    # XXX-arnau: This should probably not be a CachingMethod(): if Property
+    #            definition changes, a reset will be performed but this will
+    #            take effect until cache is reset...
+    _getPreference = CachingMethod(_getPreference,
+            id='%s.%s' % (self._preference_cache_id,
+                          instance.getPortalObject().portal_preferences._getCacheId()),
+            cache_factory='erp5_ui_long')
+    return _getPreference(default, *args, **kw)
 
 def _generatePreferenceToolAccessorHolder(portal_type_name,
                                           accessor_holder_list):
diff --git a/product/ERP5Form/PreferenceTool.py b/product/ERP5Form/PreferenceTool.py
index db93eda388..b09ed6a612 100644
--- a/product/ERP5Form/PreferenceTool.py
+++ b/product/ERP5Form/PreferenceTool.py
@@ -27,314 +27,11 @@
 #
 ##############################################################################
 
-from AccessControl import ClassSecurityInfo
-from AccessControl.SecurityManagement import getSecurityManager,\
-                          setSecurityManager, newSecurityManager
-from MethodObject import Method
-from Products.ERP5Type.Globals import InitializeClass, DTMLFile
-from zLOG import LOG, PROBLEM
-
-from Products.CMFCore.utils import getToolByName
-from Products.ERP5Type.Tool.BaseTool import BaseTool
-from Products.ERP5Type import Permissions
-from Products.ERP5Type.Cache import CachingMethod
-from Products.ERP5Type.Utils import convertToUpperCase
-from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
-from Products.ERP5Form import _dtmldir
-from BTrees.OIBTree import OIBTree
-
-_marker = object()
-
+# Code migrated to tool.erp5.PreferenceTool but kept here because:
+# * Bootstrap: System Preference created on ERP5Site creation.
+# * Backward compatibility.
 class Priority:
   """ names for priorities """
   SITE  = 1
   GROUP = 2
   USER  = 3
-
-class func_code: pass
-
-class PreferenceMethod(Method):
-  """ A method object that lookup the attribute on preferences. """
-  # This is required to call the method form the Web
-  func_code = func_code()
-  func_code.co_varnames = ('self', )
-  func_code.co_argcount = 1
-  func_defaults = ()
-
-  def __init__(self, attribute, default):
-    self.__name__ = self._preference_getter = attribute
-    self._preference_default = default
-    self._preference_cache_id = 'PreferenceTool.CachingMethod.%s' % attribute
-
-  def __call__(self, instance, default=_marker, *args, **kw):
-    def _getPreference(default, *args, **kw):
-      # XXX: sql_catalog_id is passed when calling getPreferredArchive
-      # This is inconsistent with regular accessor API, and indicates that
-      # there is a design problem in current archive API.
-      sql_catalog_id = kw.pop('sql_catalog_id', None)
-      for pref in instance._getSortedPreferenceList(sql_catalog_id=sql_catalog_id):
-        value = getattr(pref, self._preference_getter)(_marker, *args, **kw)
-        # XXX Due to UI limitation, null value is treated as if the property
-        #     was not defined. The drawback is that it is not possible for a
-        #     user to mask a non-null global value with a null value.
-        if value not in (_marker, None, '', (), []):
-          return value
-      if default is _marker:
-        return self._preference_default
-      return default
-    _getPreference = CachingMethod(_getPreference,
-            id='%s.%s' % (self._preference_cache_id,
-                          instance.getPortalObject().portal_preferences._getCacheId()),
-            cache_factory='erp5_ui_long')
-    return _getPreference(default, *args, **kw)
-
-
-class PreferenceTool(BaseTool):
-  """
-    PreferenceTool manages User Preferences / User profiles.
-
-    TODO:
-      - make the preference tool an action provider (templates)
-  """
-  id            = 'portal_preferences'
-  meta_type     = 'ERP5 Preference Tool'
-  portal_type   = 'Preference Tool'
-  title         = 'Preferences'
-  allowed_types = ( 'ERP5 Preference',)
-  security      = ClassSecurityInfo()
-
-  aq_preference_generated = False
-
-  security.declareProtected(
-       Permissions.ManagePortal, 'manage_overview' )
-  manage_overview = DTMLFile( 'explainPreferenceTool', _dtmldir )
-
-  security.declarePrivate('manage_afterAdd')
-  def manage_afterAdd(self, item, container) :
-    """ init the permissions right after creation """
-    item.manage_permission(Permissions.AddPortalContent,
-          ['Member', 'Author', 'Manager'])
-    item.manage_permission(Permissions.AddPortalFolders,
-          ['Member', 'Author', 'Manager'])
-    item.manage_permission(Permissions.View,
-          ['Member', 'Auditor', 'Manager'])
-    item.manage_permission(Permissions.CopyOrMove,
-          ['Member', 'Auditor', 'Manager'])
-    item.manage_permission(Permissions.ManageProperties,
-          ['Manager'], acquire=0)
-    item.manage_permission(Permissions.SetOwnPassword,
-          ['Member', 'Author', 'Manager'])
-    BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)
-
-  security.declarePublic('getPreference')
-  def getPreference(self, pref_name, default=_marker) :
-    """ get the preference on the most appopriate Preference object. """
-    method = getattr(self, 'get%s' % convertToUpperCase(pref_name), None)
-    if method is not None:
-      return method(default)
-    if default is _marker:
-      return None
-    return default
-
-  security.declareProtected(Permissions.ModifyPortalContent, "setPreference")
-  def setPreference(self, pref_name, value) :
-    """ set the preference on the active Preference object"""
-    self.getActivePreference()._edit(**{pref_name:value})
-
-  def _getSortedPreferenceList(self, sql_catalog_id=None):
-    """ return the most appropriate preferences objects,
-        sorted so that the first in the list should be applied first
-    """
-    tv = getTransactionalVariable()
-    security_manager = getSecurityManager()
-    user = security_manager.getUser()
-    acl_users = self.getPortalObject().acl_users
-    try:
-      # reset a security manager without any proxy role or unrestricted method,
-      # wich affects the catalog search that we do to find applicable
-      # preferences.
-      actual_user = acl_users.getUserById(user.getId())
-      if actual_user is not None:
-        newSecurityManager(None, actual_user.__of__(acl_users))
-      tv_key = 'PreferenceTool._getSortedPreferenceList/%s/%s' % (user.getId(),
-                                                                  sql_catalog_id)
-      if tv.get(tv_key, None) is None:
-        prefs = []
-        # XXX will also cause problems with Manager (too long)
-        # XXX For manager, create a manager specific preference
-        #                  or better solution
-        user_is_manager = 'Manager' in user.getRolesInContext(self)
-        for pref in self.searchFolder(portal_type='Preference', sql_catalog_id=sql_catalog_id):
-          pref = pref.getObject()
-            # XXX quick workaround so that managers only see user preference
-            #     they actually own.
-          if pref is not None and (not user_is_manager or
-                                   pref.getPriority() != Priority.USER or
-                                   pref.getOwnerTuple()[1] == user.getId()):
-            if pref.getProperty('preference_state',
-                                'broken') in ('enabled', 'global'):
-                prefs.append(pref)
-        prefs.sort(key=lambda x: x.getPriority(), reverse=True)
-        # add system preferences before user preferences
-        sys_prefs = [x.getObject() for x in self.searchFolder(portal_type='System Preference', sql_catalog_id=sql_catalog_id) \
-                     if x.getObject().getProperty('preference_state', 'broken') in ('enabled', 'global')]
-        sys_prefs.sort(key=lambda x: x.getPriority(), reverse=True)
-        preference_list = sys_prefs + prefs
-        tv[tv_key] = preference_list
-      return tv[tv_key]
-    finally:
-      setSecurityManager(security_manager)
-
-  def _getActivePreferenceByPortalType(self, portal_type):
-    enabled_prefs = self._getSortedPreferenceList()
-    if len(enabled_prefs) > 0 :
-      try:
-        return [x for x in enabled_prefs
-            if x.getPortalType() == portal_type][0]
-      except IndexError:
-        pass
-    return None
-
-  security.declareProtected(Permissions.View, 'getActivePreference')
-  def getActivePreference(self) :
-    """ returns the current preference for the user.
-       Note that this preference may be read only. """
-    return self._getActivePreferenceByPortalType('Preference')
-
-  security.declareProtected(Permissions.View, 'clearCache')
-  def clearCache(self, preference):
-    """ clear cache when a preference is modified.
-    This is called by an interaction workflow on preferences.
-    """
-    self._getCacheId() # initialize _preference_cache if needed.
-    if preference.getPriority() == Priority.USER:
-      user_id = getSecurityManager().getUser().getId()
-      self._preference_cache[user_id] = \
-          self._preference_cache.get(user_id, 0) + 1
-    self._preference_cache[None] = self._preference_cache.get(None, 0) + 1
-
-  def _getCacheId(self):
-    """Return a cache id for preferences.
-
-    We use:
-     - user_id: because preferences are always different by user
-     - self._preference_cache[user_id] which is increased everytime a user
-       preference is modified
-     - self._preference_cache[None] which is increased everytime a global
-       preference is modified
-    """
-    user_id = getSecurityManager().getUser().getId()
-    try:
-      self._preference_cache
-    except AttributeError:
-      self._preference_cache = OIBTree()
-    return self._preference_cache.get(None), self._preference_cache.get(user_id), user_id
-
-  security.declareProtected(Permissions.View, 'getActiveUserPreference')
-  def getActiveUserPreference(self) :
-    """ returns the current user preference for the user.
-    If no preference exists, then try to create one with `createUserPreference`
-    type based method.
-
-    This method returns a preference that the user will be able to edit or
-    None, if `createUserPreference` refused to create a preference.
-
-    It is intendended for "click here to edit your preferences" actions.
-    """
-    active_preference = self.getActivePreference()
-    if active_preference is None or active_preference.getPriority() != Priority.USER:
-      # If user does not have a preference, let's try to create one
-      user = self.getPortalObject().portal_membership.getAuthenticatedMember().getUserValue()
-      if user is not None:
-        createUserPreference = user.getTypeBasedMethod('createUserPreference')
-        if createUserPreference is not None:
-          active_preference = createUserPreference()
-    return active_preference
-
-  security.declareProtected(Permissions.View, 'getActiveSystemPreference')
-  def getActiveSystemPreference(self) :
-    """ returns the current system preference for the user.
-       Note that this preference may be read only. """
-    return self._getActivePreferenceByPortalType('System Preference')
-
-  security.declareProtected(Permissions.View, 'getDocumentTemplateList')
-  def getDocumentTemplateList(self, folder=None) :
-    """ returns all document templates that are in acceptable Preferences
-        based on different criteria such as folder, portal_type, etc.
-    """
-    if folder is None:
-      # as the preference tool is also a Folder, this method is called by
-      # page templates to get the list of document templates for self.
-      folder = self
-
-    # We must set the user_id as a parameter to make sure each
-    # user can get a different cache
-    def _getDocumentTemplateList(user_id, portal_type=None):
-      acceptable_template_list = []
-      for pref in self._getSortedPreferenceList() :
-        for doc in pref.contentValues(portal_type=portal_type) :
-          acceptable_template_list.append(doc.getRelativeUrl())
-      return acceptable_template_list
-    _getDocumentTemplateList = CachingMethod(
-        _getDocumentTemplateList,
-        'portal_preferences.getDocumentTemplateList.{}'.format(self._getCacheId()),
-        cache_factory='erp5_ui_long')
-
-    allowed_content_types = map(lambda pti: pti.id,
-                                folder.allowedContentTypes())
-    user_id = getToolByName(self, 'portal_membership').getAuthenticatedMember().getId()
-    template_list = []
-    for portal_type in allowed_content_types:
-      for template_url in _getDocumentTemplateList(user_id, portal_type=portal_type):
-        template = self.restrictedTraverse(template_url, None)
-        if template is not None:
-          template_list.append(template)
-    return template_list
-
-  security.declareProtected(Permissions.ManagePortal,
-                            'createActiveSystemPreference')
-  def createActiveSystemPreference(self):
-    """ Create a System Preference and enable it if there is no other
-        enabled System Preference in present.
-    """
-    if self.getActiveSystemPreference() is not None:
-      raise ValueError("Another Active Preference already exists.")
-    system_preference = self.newContent(portal_type='System Preference')
-    system_preference.enable()
-
-  security.declareProtected(Permissions.ManagePortal,
-                            'createPreferenceForUser')
-  def createPreferenceForUser(self, user_id, enable=True):
-    """Creates a preference for a given user, and optionnally enable the
-    preference.
-    """
-    user_folder = self.acl_users
-    user = user_folder.getUserById(user_id)
-    if user is None:
-      raise ValueError("User %r not found" % (user_id, ))
-    security_manager = getSecurityManager()
-    try:
-      newSecurityManager(None, user.__of__(user_folder))
-      preference = self.newContent(portal_type='Preference')
-      if enable:
-        preference.enable()
-      return preference
-    finally:
-      setSecurityManager(security_manager)
-
-  security.declarePublic('isAuthenticationPolicyEnabled')
-  def isAuthenticationPolicyEnabled(self) :
-    """
-    Return True if authentication policy is enabled.
-    This method exists here due to bootstrap issues.
-    It should work even if erp5_authentication_policy bt5 is not installed.
-    """
-    # isPreferredAuthenticationPolicyEnabled exisss if property sheets from
-    # erp5_authentication_policy are installed.
-    method = getattr(self, 'isPreferredAuthenticationPolicyEnabled', None)
-    if method is not None and method():
-      return True
-    # if it does not exist, for sure authentication policy is not enabled.
-    return False
-
-InitializeClass(PreferenceTool)
diff --git a/product/ERP5Form/__init__.py b/product/ERP5Form/__init__.py
index ae4d98686e..fbc70b265d 100644
--- a/product/ERP5Form/__init__.py
+++ b/product/ERP5Form/__init__.py
@@ -47,7 +47,6 @@ import OOoChart, PDFTemplate, Report, ParallelListField
 import PlanningBox, POSBox, FormBox, EditorField, ProxyField, DurationField
 import RelationField, ImageField, MultiRelationField, MultiLinkField, InputButtonField
 import CaptchaField
-import PreferenceTool
 
 from Products.Formulator.FieldRegistry import FieldRegistry
 from Products.Formulator import StandardFields, HelperFields
@@ -56,7 +55,7 @@ from Products.CMFCore.utils import registerIcon
 
 object_classes = ( Form.ERP5Form, FSForm.ERP5FSForm, PDFTemplate.PDFTemplate,
                    Report.ERP5Report)
-portal_tools = ( SelectionTool.SelectionTool, PreferenceTool.PreferenceTool )
+portal_tools = ( SelectionTool.SelectionTool,)
 content_classes = ( )
 content_constructors = ()
 
-- 
2.30.9