diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.py
new file mode 100644
index 0000000000000000000000000000000000000000..da060e3fe2cbbbdeb69923f0891e87b505d4dd03
--- /dev/null
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.py
@@ -0,0 +1,135 @@
+##############################################################################
+#
+# Copyright (c) 2021 Nexedi SARL and Contributors. All Rights Reserved.
+#                     Nicolas Wavrant <nicolas.wavrant@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.
+#
+##############################################################################
+
+import unittest
+from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
+from Products.ERP5Type.tests.Sequence import SequenceList, StoredSequence
+
+class TestStoredSequence(ERP5TypeTestCase):
+
+  def afterSetUp(self):
+    self.portal = self.getPortalObject()
+    self.portal.person_module.manage_delObjects(
+      ids=list(self.portal.person_module.objectIds())
+    )
+    for trashbin_value in self.portal.portal_trash.objectValues():
+      if trashbin_value.getId().startswith(self.__class__.__name__):
+        self.portal.portal_trash.manage_delObjects(ids=[trashbin_value.getId()])
+    self.tic()
+
+  registerSequenceString = ERP5TypeTestCase.registerSequenceString
+
+  def _getCleanupDict(self):
+    return {
+      "person_module": list(self.portal.person_module.objectIds()),
+    }
+
+  def stepLogin(self, sequence):
+    self.login()
+
+  def stepCreatePerson(self, sequence):
+    sequence['person'] =  self.portal.person_module.newContent(
+        id="person",
+        title="Person",
+    )
+
+  def stepUpdatePerson1(self, sequence):
+    sequence['person'].setTitle(sequence['person'].getTitle() + " 1")
+
+  def stepUpdatePerson2(self, sequence):
+    sequence['person'].setTitle(sequence['person'].getTitle() + " 2")
+
+  def stepFillSequenceDict(self, sequence):
+    sequence["string"] = "a string"
+    sequence["int"] = 10
+    sequence["float"] = 3.14
+    sequence["erp5_document"] = self.portal.person_module.newContent(
+      portal_type="Person",
+      id="erp5_document_0",
+    )
+    sequence["list_of_int"] = [1, 2]
+    sequence["list_of_erp5_document"] = [
+      self.portal.person_module.newContent(
+        portal_type="Person",
+        id="erp5_document_%d" % i,
+      ) for i in range(1, 3)
+    ]
+
+  def test_storedSequenceCanRestoreAState(self):
+    sequence_id = "sequence_can_restore"
+    self.registerSequenceString(sequence_id, """
+      stepCreatePerson
+    """)
+    sequence = StoredSequence(self, sequence_id)
+    sequence.setSequenceString("stepUpdatePerson1")
+    sequence_list = SequenceList()
+    sequence_list.addSequence(sequence)
+    sequence_list.play(self)
+    self.assertEqual(self.portal.person_module.person.getTitle(), "Person 1")
+    trashbin_value = self.portal.portal_trash[sequence._getTrashBinId(self)]
+    self.assertEqual(trashbin_value.person_module.person.getTitle(), "Person")
+    self.assertEqual(
+      trashbin_value.getProperty("serialised_sequence"),
+      ({"key": "person", "type": "erp5_object", "value": "person_module/person"},)
+    )
+    self.portal.person_module.manage_delObjects(ids=["person"])
+    # Run new sequence, with same base sequence.
+    # Update the title of the person document in the trashbin to be
+    # sure it has been restored from trash and not created
+    trashbin_value.person_module.person.setTitle("Trash Person")
+    self.tic()
+    sequence = StoredSequence(self, sequence_id)
+    sequence.setSequenceString("stepUpdatePerson2")
+    sequence_list = SequenceList()
+    sequence_list.addSequence(sequence)
+    sequence_list.play(self)
+    self.assertEqual(trashbin_value.person_module.person.getTitle(), "Trash Person")
+    self.assertEqual(self.portal.person_module.person.getTitle(), "Trash Person 2")
+
+  def test_serialisationOfSequenceDict(self):
+    sequence_id = "serialisation"
+    self.registerSequenceString(sequence_id, "stepFillSequenceDict")
+    sequence = StoredSequence(self, sequence_id)
+    sequence.setSequenceString("stepLogin")
+    sequence_list = SequenceList()
+    sequence_list.addSequence(sequence)
+    sequence_list.play(self)
+    sequence_dict = sequence._dict
+    # sequence._dict will be recalculated
+    sequence.deserialiseSequenceDict(
+      self.portal.portal_trash[sequence._getTrashBinId(self)].serialised_sequence
+    )
+    self.assertEqual(
+      sequence_dict,
+      sequence._dict,
+    )
+
+def test_suite():
+  suite = unittest.TestSuite()
+  suite.addTest(unittest.makeSuite(TestStoredSequence))
+  return suite
\ No newline at end of file
diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.xml b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5beab2dac4feb612c2087aff15a1a8b592fb4521
--- /dev/null
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testSequence.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Test Component" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_recorded_property_dict</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>default_reference</string> </key>
+            <value> <string>testSequence</string> </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>test.erp5.testSequence</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Test 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">AAAAAAAAAAM=</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/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <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">AAAAAAAAAAQ=</string> </persistent>
+                    </value>
+                </item>
+              </dictionary>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="4" aka="AAAAAAAAAAQ=">
+    <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/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testTrashTool.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testTrashTool.py
index 0e9da30c73694a7e8a5f030c4e11b024eb991666..45ad601d462ae6081cb397ce68917a67dff1be6c 100644
--- a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testTrashTool.py
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testTrashTool.py
@@ -355,6 +355,32 @@ class TestTrashTool(ERP5TypeTestCase):
     self.assertTrue(subcat is not None)
     sequence.edit(subcat_path=subcat.getPath())
 
+  def stepDeleteBaseCategory(self, sequence=None, sequence_list=None, **kw):
+    pc = self.getCategoryTool()
+    pc.manage_delObjects(ids=[sequence.get('bc_id')])
+
+  def stepRestore(self, sequence=None, sequence_list=None, **kw):
+    trash_id = sequence.get('trash_id')
+    trash = self.getTrashTool()
+    trashbin = trash._getOb(trash_id, None)
+    bc_id = sequence.get('bc_id')
+    trash.restoreObject(trashbin, ['portal_categories_items'], bc_id)
+
+  def stepCheckRestore(self, sequence=None, sequence_list=None, **kw):
+    bc_id = sequence.get('bc_id')
+    bc = self.portal.portal_categories[bc_id]
+    self.assertTrue(
+      sorted(bc.objectIds()) == sorted(sequence.get('category_id_list'))
+    )
+    self.assertEqual(
+      len(
+        self.portal.portal_catalog(
+          portal_type='Category',
+          parent_uid=bc.getUid()
+        )
+      ), 10
+    )
+
   # tests
   def test_01_checkTrashBinCreation(self, quiet=quiet, run=run_all_test):
     if not run: return
@@ -477,6 +503,30 @@ class TestTrashTool(ERP5TypeTestCase):
     sequence_list.addSequenceString(sequence_string)
     sequence_list.play(self, quiet=quiet)
 
+  def test_07_checkRestore(self, quiet=quiet, run=run_all_test):
+    if not run: return
+    if not quiet:
+      message = 'Test Check Backup Without Subobjects'
+      ZopeTestCase._print('\n%s ' % message)
+      LOG('Testing... ', 0, message)
+    sequence_list = SequenceList()
+    sequence_string = '\
+                       CheckTrashToolExists  \
+                       CreateTrashBin \
+                       AddBaseCategory \
+                       AddCategories \
+                       Tic \
+                       BackupObjectsWithKeepingSubobjects \
+                       Tic \
+                       CheckObjectBackupWithSubObjects \
+                       DeleteBaseCategory \
+                       Tic \
+                       Restore \
+                       Tic \
+                       CheckRestore \
+                       '
+    sequence_list.addSequenceString(sequence_string)
+    sequence_list.play(self, quiet=quiet)
 
 
 def test_suite():
diff --git a/bt5/erp5_core_test/bt/template_test_id_list b/bt5/erp5_core_test/bt/template_test_id_list
index 58f030516b107e5cef864bf273cc759a2955338f..2b43db6b537102b6d94c382cfe9985a962388491 100644
--- a/bt5/erp5_core_test/bt/template_test_id_list
+++ b/bt5/erp5_core_test/bt/template_test_id_list
@@ -1,4 +1,5 @@
 test.erp5.testAccessTab
+test.erp5.testSequence
 test.erp5.testActivityTool
 test.erp5.testAlarm
 test.erp5.testArrow
diff --git a/product/ERP5/Tool/TrashTool.py b/product/ERP5/Tool/TrashTool.py
index bcbb8764489a107f4e653e97a5539c521a2c0a84..a9bf0bc9b4c6642b01008f6535c925a83c0396dc 100644
--- a/product/ERP5/Tool/TrashTool.py
+++ b/product/ERP5/Tool/TrashTool.py
@@ -34,6 +34,7 @@ from Products.ERP5Type.Globals import InitializeClass, DTMLFile
 from Products.ERP5Type.Tool.BaseTool import BaseTool
 from Products.ERP5Type import Permissions
 from Products.ERP5 import _dtmldir
+from zExceptions import BadRequest
 from zLOG import LOG, WARNING
 from DateTime import DateTime
 from Acquisition import aq_base
@@ -189,6 +190,79 @@ class TrashTool(BaseTool):
                               )
     return trashbin
 
+  security.declarePrivate('restoreObject')
+  def restoreObject(self, trashbin, container_path, object_id, pass_if_exist=True):
+    """
+      Restore an object from the trash bin (copy it under portal)
+    """
+    portal = self.getPortalObject()
+    # recreate path of the backup object if necessary
+    backup_object_container = portal
+    for path in container_path:
+      if path.endswith('_items'):
+        path = path[0:-len('_items')]
+      if path not in backup_object_container.objectIds():
+        if not hasattr(aq_base(backup_object_container), "newContent"):
+          backup_object_container.manage_addFolder(id=path,)
+          backup_object_container = backup_object_container._getOb(path)
+        else:
+          backup_object_container = backup_object_container.newContent(
+            portal_type='Folder',
+            id=path,
+          )
+      else:
+        backup_object_container = backup_object_container._getOb(path)
+    # backup the object
+    # here we choose export/import to copy because cut/paste
+    # do too many things and check for what we want to do
+    object_path = container_path + [object_id]
+    obj = trashbin.restrictedTraverse(object_path, None)
+    if obj is not None:
+      connection = obj._p_jar
+      o = obj
+      while connection is None:
+        o = o.aq_parent
+        connection=o._p_jar
+      if obj._p_oid is None:
+        LOG("Trash Tool backupObject", WARNING,
+            "Trying to backup uncommitted object %s" % object_path)
+        return {}
+      if isinstance(obj, Broken):
+        LOG("Trash Tool backupObject", WARNING,
+            "Can't backup broken object %s" % object_path)
+        klass = obj.__class__
+        if klass.__module__[:27] in ('Products.ERP5Type.Document.',
+                                      'erp5.portal_type'):
+          # meta_type is required so that a broken object
+          # can be removed properly from a BTreeFolder2
+          # (unfortunately, we can only guess it)
+          klass.meta_type = 'ERP5' + re.subn('(?=[A-Z])', ' ',
+                                              klass.__name__)[0]
+        return
+      copy = connection.exportFile(obj._p_oid)
+      # import object in trash
+      connection = backup_object_container._p_jar
+      o = backup_object_container
+      while connection is None:
+        o = o.aq_parent
+        connection=o._p_jar
+      copy.seek(0)
+      try:
+        backup = connection.importFile(copy)
+        if hasattr(aq_base(backup), 'isIndexable'):
+          del backup.isIndexable
+        backup_object_container._setObject(object_id, backup)
+      except (AttributeError, ImportError):
+        # XXX we can go here due to formulator because attribute
+        # field_added doesn't not exists on parent if it is a Trash
+        # Folder and not a Form, or a module for the old object is
+        # already removed, and we cannot backup the object
+        LOG("Trash Tool backupObject", WARNING,
+            "Can't backup object %s" % object_path)
+      except BadRequest:
+        if pass_if_exist:
+          pass
+
   security.declareProtected(Permissions.ManagePortal, 'getTrashBinObjectsList')
   def getTrashBinObjectsList(self, trashbin):
     """
diff --git a/product/ERP5Type/tests/ERP5TypeTestCase.py b/product/ERP5Type/tests/ERP5TypeTestCase.py
index 92f9ee387cd0517ea724e50968f26a2437a45590..0b35d1d9df10973ddcb4c28aa10837ee61a788eb 100644
--- a/product/ERP5Type/tests/ERP5TypeTestCase.py
+++ b/product/ERP5Type/tests/ERP5TypeTestCase.py
@@ -217,6 +217,10 @@ DateTime._parse_args = _parse_args
 class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
     """Mixin class for ERP5 based tests.
     """
+    def __init__(self, *args, **kw):
+      super(ERP5TypeTestCaseMixin, self).__init__(*args, **kw)
+      self.sequence_string_registry = {}
+
     def dummy_test(self):
       ZopeTestCase._print('All tests are skipped when --save option is passed '
                           'with --update_business_templates or without --load')
@@ -864,6 +868,53 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
         setup_once()
         ZopeTestCase._print('done (%.3fs)\n' % (time.time() - start))
 
+    def _getCleanupDict(self):
+      """
+        You must override this. Return the documents that should be
+        stored while saving/restoring a StoredSequence as a dict,
+        the keys being the module containing them, and the values
+        the list of ids of documents
+      """
+      return {}
+
+    def registerSequenceString(self, sequence_title, sequence_string):
+      self.sequence_string_registry[sequence_title] = sequence_string
+
+    def getSequenceString(self, sequence_title):
+      return self.sequence_string_registry[sequence_title]
+
+    def stepStoreSequence(self, sequence):
+      sequence_title = "sequence_title"
+      document_dict = self._getCleanupDict()
+      if sequence_title in self.portal.portal_trash:
+        self.portal.portal_trash.manage_delObjects(ids=[sequence_title])
+      trashbin_value = self.portal.portal_trash.newContent(
+        portal_type="Trash Bin",
+        id=sequence_title,
+        title=sequence_title,
+        serialized_sequence=sequence.serializeSequenceDict(),
+        document_dict=document_dict,
+      )
+      for module_id, object_id_list in document_dict.iteritems():
+        for object_id in object_id_list:
+          self.portal.portal_trash.backupObject(
+            trashbin_value, [module_id], object_id, save=True, keep_subobjects=True
+          )
+
+    def stepRestoreSequence(self, sequence):
+      sequence_title = "sequence_title"
+      trashbin_value = self.portal.portal_trash[sequence_title]
+      document_dict = trashbin_value.getProperty('document_dict')
+      for module_id, object_id_list in document_dict.iteritems():
+        for object_id in object_id_list:
+          self.portal.portal_trash.restoreObject(
+            trashbin_value, [module_id], object_id, pass_if_exist=True
+          )
+      sequence.deserializeSequenceDict(
+        trashbin_value.getProperty("serialized_sequence"),
+      )
+      self.tic()
+
 class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
     __original_ZMySQLDA_connect = None
 
diff --git a/product/ERP5Type/tests/Sequence.py b/product/ERP5Type/tests/Sequence.py
index f481da37383a09496e37a85032f44e4914062e64..9c29894c91bf9f6536e43c9f0ee0cac817147d96 100644
--- a/product/ERP5Type/tests/Sequence.py
+++ b/product/ERP5Type/tests/Sequence.py
@@ -175,6 +175,115 @@ class Sequence:
           step = step[4:]
         self.addStep(step)
 
+
+class StoredSequence(Sequence):
+  """A StoredSequence is a Sequence that can store an ERP5's state into
+  a Trash Bin and restore it before before being played. If the state is
+  not stored yet, then it will create it then store it.
+  This capability is interesting when multiple tests share a same initial
+  state, as the state needs to be generated only once and can be reused
+  for all of them.
+  """
+
+  def __init__(self, context, id):
+    Sequence.__init__(self, context)
+    self._id = id
+
+  def serialiseSequenceDict(self):
+    def _serialise(key, value):
+      result_dict = {'key': key}
+      if (
+          isinstance(value, str) or
+          isinstance(value, int) or
+          isinstance(value, float) or
+          isinstance(value, dict) or
+          value is None
+        ):
+        result_dict['type'] = "raw"
+        result_dict['value'] = value
+      elif isinstance(value, list):
+        result_dict['type'] = "list"
+        result_dict['value'] = [_serialise(key, x) for x in value]
+      else:
+        result_dict['type'] = "erp5_object"
+        result_dict['value'] = value.getRelativeUrl()
+      return result_dict
+
+    result_list = []
+    for key, value in self._dict.iteritems():
+      result_list.append(_serialise(key, value))
+    return result_list
+
+  def deserialiseSequenceDict(self, data):
+    portal = self._context.getPortalObject()
+    def _deserialise(serialised_dict):
+      if serialised_dict["type"] == "raw":
+        return serialised_dict["value"]
+      elif serialised_dict["type"] == "list":
+        return [_deserialise(x) for x in serialised_dict["value"]]
+      elif serialised_dict["type"] == "erp5_object":
+        return portal.restrictedTraverse(serialised_dict['value'])
+      else:
+        raise TypeError("Unknown serialised type %s", serialised_dict["type"])
+
+    for serialised_dict in data:
+      self._dict[serialised_dict['key']] = _deserialise(serialised_dict)
+
+  def _getTrashBinId(self, context):
+    if not context:
+      context = self.context
+    return "%s_%s" % (context.__class__.__name__, self._id)
+
+  def store(self, context):
+    context.login()
+    document_dict = context._getCleanupDict()
+    trashbin_id = self._getTrashBinId(context)
+    if trashbin_id in context.portal.portal_trash:
+      context.portal.portal_trash.manage_delObjects(ids=[self._id])
+    trashbin_value = context.portal.portal_trash.newContent(
+      portal_type="Trash Bin",
+      id=trashbin_id,
+      title=trashbin_id,
+      serialised_sequence=self.serialiseSequenceDict(),
+      document_dict=document_dict,
+    )
+    for module_id, object_id_list in document_dict.iteritems():
+      for object_id in object_id_list:
+        context.portal.portal_trash.backupObject(
+          trashbin_value, [module_id], object_id, save=True, keep_subobjects=True
+        )
+    context.tic()
+    context.logout()
+
+  def restore(self, context):
+    context.login()
+    trashbin_value = context.portal.portal_trash[self._getTrashBinId(context)]
+    document_dict = trashbin_value.getProperty('document_dict')
+    for module_id, object_id_list in document_dict.iteritems():
+      for object_id in object_id_list:
+        context.portal.portal_trash.restoreObject(
+          trashbin_value, [module_id], object_id, pass_if_exist=True
+        )
+    self.deserialiseSequenceDict(
+      trashbin_value.getProperty("serialised_sequence"),
+    )
+    context.tic()
+    context.logout()
+
+  def play(self, context, **kw):
+    portal = self._context.getPortal()
+    if getattr(portal.portal_trash, self._getTrashBinId(context), None) is None:
+      ZopeTestCase._print('\nRunning and saving stored sequence \"%s\" ...' % self._id)
+      sequence = Sequence()
+      sequence.setSequenceString(context.getSequenceString(self._id))
+      sequence.play(context)
+      self._dict = sequence._dict.copy()
+      self.store(context)
+    else:
+      ZopeTestCase._print('\nRestoring stored sequence \"%s\" ...' % self._id)
+      self.restore(context)
+    Sequence.play(self, context, **kw)
+
 class SequenceList:
 
   def __init__(self):