From 5fea47d1f80c0bda2a34f40d3871ad4c7d0c1ece Mon Sep 17 00:00:00 2001
From: Arnaud Fontaine <arnaud.fontaine@nexedi.com>
Date: Thu, 21 May 2020 14:06:31 +0900
Subject: [PATCH] ZODB Components: Products import compatibility (MR !1271).

This implements Products import compatibility based on Component.source_reference
using existing import hooks so that Products.ERP5.Document.Person for example
is importable (actually returning erp5.component.document.Person module).

This only works with non-FS modules (IOW when the ZODB is accessible and
portal.portal_components is available).
---
 .../test.erp5.testBusinessTemplate.py         |  39 +++++
 .../interactions/Component_reset.xml          |   1 +
 .../person_module/test_person.xml             | 138 +++++++++++++++++
 .../test_person/default_address.xml           |  38 +++++
 .../test_person/default_career.xml            |  61 ++++++++
 .../test_person/default_email.xml             |  30 ++++
 .../person_module/test_person/default_fax.xml |  30 ++++
 .../test_person/default_link.xml              |  40 +++++
 .../test_person/default_telephone.xml         |  30 ++++
 .../bt/description                            |   1 +
 .../bt/template_format_version                |   1 +
 .../bt/template_path_list                     |   2 +
 .../bt/title                                  |   1 +
 product/ERP5Type/Tool/ComponentTool.py        |   1 +
 product/ERP5Type/dynamic/component_package.py |  81 +++++++---
 product/ERP5Type/dynamic/dynamic_module.py    |   3 +
 product/ERP5Type/mixin/component.py           |   6 +-
 .../tests/testDynamicClassGeneration.py       | 140 +++++++++++++++---
 product/ERP5Type/tests/testERP5Type.py        |  36 +++++
 19 files changed, 640 insertions(+), 39 deletions(-)
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list
 create mode 100644 product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title

diff --git a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
index 379bc3c0d9..dd2e5f7fc8 100644
--- a/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
+++ b/bt5/erp5_core_test/TestTemplateItem/portal_components/test.erp5.testBusinessTemplate.py
@@ -6380,6 +6380,45 @@ class TestBusinessTemplate(BusinessTemplateMixin):
       self.assertEqual("FooBar", getattr(foo_in_portal, "title"))
     self.uninstallBusinessTemplate('test_168_CheckPortalTypeAndPathInSameBusinessTemplate')
 
+  def test_legacy_products_erp5_document_compatibility(self):
+    """Check we can import a business template referencing classes from
+    Products.ERP5Type.Document namespace and that the classes are migrated
+    """
+    import Products.ERP5.tests
+    bt_path = os.path.join(
+        os.path.dirname(Products.ERP5.tests.__file__),
+        'test_data',
+        'BusinessTemplate_test_legacy_products_erp5_document_compatibility')
+    bt = self.portal.portal_templates.download(bt_path)
+    bt.install()
+    self.tic()
+
+    # when loaded, the legacy classes have been updated to use
+    # erp5.portal_type namespace
+    self.assertEqual(
+        str(self.portal.person_module.test_person.__class__),
+        "<class 'erp5.portal_type.Person'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_address.__class__),
+        "<class 'erp5.portal_type.Address'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_career.__class__),
+        "<class 'erp5.portal_type.Career'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_email.__class__),
+        "<class 'erp5.portal_type.Email'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_fax.__class__),
+        "<class 'erp5.portal_type.Fax'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_telephone.__class__),
+        "<class 'erp5.portal_type.Telephone'>")
+    self.assertEqual(
+        str(self.portal.person_module.test_person.default_link.__class__),
+        "<class 'erp5.portal_type.Link'>")
+
+    self.uninstallBusinessTemplate('BusinessTemplate_test_legacy_products_erp5_document_compatibility')
+
   def test_169_CheckPortalTypeAndPathInSameBusinessTemplateAndBrokenObjectModification(self):
     """
     Make sure we have possibility to change broken
diff --git a/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml b/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml
index 0e0b1feef0..2091b19c1e 100644
--- a/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml
+++ b/product/ERP5/bootstrap/erp5_core/WorkflowTemplateItem/portal_workflow/dynamic_class_generation_interaction_workflow/interactions/Component_reset.xml
@@ -56,6 +56,7 @@
             <key> <string>method_id</string> </key>
             <value>
               <list>
+                <string>_setSourceReference</string>
                 <string>validate</string>
                 <string>invalidate</string>
               </list>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml
new file mode 100644
index 0000000000..71b59b1de0
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person.xml
@@ -0,0 +1,138 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Person" module="Products.ERP5Type.Document.Person"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_Access_contents_information_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Associate</string>
+                <string>Auditor</string>
+                <string>Author</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Add_portal_content_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Associate</string>
+                <string>Author</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Modify_portal_content_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Associate</string>
+                <string>Author</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_View_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Associate</string>
+                <string>Auditor</string>
+                <string>Author</string>
+                <string>Manager</string>
+                <string>Owner</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>_tree</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>first_name</string> </key>
+            <value> <string>Test</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>test_person</string> </value>
+        </item>
+        <item>
+            <key> <string>language</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>last_name</string> </key>
+            <value> <string>Person</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Person</string> </value>
+        </item>
+        <item>
+            <key> <string>user_id</string> </key>
+            <value> <string>P6</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="OOBTree" module="BTrees.OOBTree"/>
+    </pickle>
+    <pickle>
+      <none/>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml
new file mode 100644
index 0000000000..f17f727d40
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_address.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="GeographicAddress" module="Products.ERP5Type.Document.GeographicAddress"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>city</string> </key>
+            <value> <string>city</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_address</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Address</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>street_address</string> </key>
+            <value> <string>the address</string> </value>
+        </item>
+        <item>
+            <key> <string>zip_code</string> </key>
+            <value> <string>zip code</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml
new file mode 100644
index 0000000000..85cb1e2089
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_career.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Career" module="Products.ERP5Type.Document.Career"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_Access_contents_information_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Auditor</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_Modify_portal_content_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>_View_Permission</string> </key>
+            <value>
+              <tuple>
+                <string>Assignee</string>
+                <string>Assignor</string>
+                <string>Auditor</string>
+                <string>Manager</string>
+                <string>Owner</string>
+              </tuple>
+            </value>
+        </item>
+        <item>
+            <key> <string>categories</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_career</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Career</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml
new file mode 100644
index 0000000000..8eeb093759
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_email.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Url" module="Products.ERP5Type.Document.Url"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>coordinate_text</string> </key>
+            <value> <string>mail@example.com</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_email</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Email</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml
new file mode 100644
index 0000000000..3ec49a2305
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_fax.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Telephone" module="Products.ERP5Type.Document.Telephone"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>coordinate_text</string> </key>
+            <value> <string>123456</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_fax</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Fax</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml
new file mode 100644
index 0000000000..f89399b6db
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_link.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Url" module="Products.ERP5Type.Document.Url"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_link</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Link</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <string>1</string> </value>
+        </item>
+        <item>
+            <key> <string>url_string</string> </key>
+            <value> <string>https://example.com</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml
new file mode 100644
index 0000000000..b4008c4332
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/PathTemplateItem/person_module/test_person/default_telephone.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Telephone" module="Products.ERP5Type.Document.Telephone"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>coordinate_text</string> </key>
+            <value> <string>123456</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>default_telephone</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Telephone</string> </value>
+        </item>
+        <item>
+            <key> <string>sid</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description
new file mode 100644
index 0000000000..f79a6b7318
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/description
@@ -0,0 +1 @@
+A business templates with documents exported with their __class__ from Products.ERP5Type.Document
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version
new file mode 100644
index 0000000000..56a6051ca2
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_format_version
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list
new file mode 100644
index 0000000000..eca988f49a
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/template_path_list
@@ -0,0 +1,2 @@
+person_module/test_person
+person_module/test_person/**
\ No newline at end of file
diff --git a/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title
new file mode 100644
index 0000000000..3a1a31ca46
--- /dev/null
+++ b/product/ERP5/tests/test_data/BusinessTemplate_test_legacy_products_erp5_document_compatibility/bt/title
@@ -0,0 +1 @@
+BusinessTemplate_test_legacy_products_erp5_document_compatibility
\ No newline at end of file
diff --git a/product/ERP5Type/Tool/ComponentTool.py b/product/ERP5Type/Tool/ComponentTool.py
index 2a4c6070a2..4e531344cb 100644
--- a/product/ERP5Type/Tool/ComponentTool.py
+++ b/product/ERP5Type/Tool/ComponentTool.py
@@ -153,6 +153,7 @@ class ComponentTool(BaseTool):
           package.reset()
           component_package_list.append(package.__name__)
 
+      erp5.component.filesystem_import_dict = None
       erp5.component.ref_manager.gc()
 
       # Clear pylint cache
diff --git a/product/ERP5Type/dynamic/component_package.py b/product/ERP5Type/dynamic/component_package.py
index 9352c71181..154edccd6d 100644
--- a/product/ERP5Type/dynamic/component_package.py
+++ b/product/ERP5Type/dynamic/component_package.py
@@ -35,6 +35,7 @@ import imp
 import collections
 
 from Products.ERP5.ERP5Site import getSite
+from Products.ERP5Type import product_path as ERP5Type_product_path
 from . import aq_method_lock
 from types import ModuleType
 from zLOG import LOG, BLATHER, WARNING
@@ -109,11 +110,21 @@ class ComponentDynamicPackage(ModuleType):
     perhaps because the Finder of another Component Package could do it or
     because this is a filesystem module...
     """
-    # Ignore imports with a path which are filesystem-only and any
-    # absolute imports which does not start with this package prefix,
-    # None there means that "normal" sys.path will be used
-    if path or not fullname.startswith(self._namespace_prefix):
-      return None
+    import erp5.component
+
+    # ZODB Components
+    if not path:
+      if not fullname.startswith(self._namespace_prefix):
+        return None
+    # FS import backward compatibility
+    else:
+      try:
+        fullname = erp5.component.filesystem_import_dict[fullname]
+      except (TypeError, KeyError):
+        return None
+      else:
+        if not fullname.startswith(self._namespace_prefix):
+          return None
 
     import_lock_held = True
     try:
@@ -124,6 +135,26 @@ class ComponentDynamicPackage(ModuleType):
     try:
       site = getSite()
 
+      if erp5.component.filesystem_import_dict is None:
+        filesystem_import_dict = {}
+        try:
+          component_tool = aq_base(site.portal_components)
+        except AttributeError:
+          # For old sites, just use FS Documents...
+          return None
+        else:
+          for component in component_tool.objectValues():
+            if component.getValidationState() == 'validated':
+              component_module_name = '%s.%s' % (component._getDynamicModuleNamespace(),
+                                                 component.getReference())
+              if component.getSourceReference() is not None:
+                filesystem_import_dict[component.getSourceReference()] = component_module_name
+
+              if component.getPortalType() == 'Document Component':
+                filesystem_import_dict[('Products.ERP5Type.Document.' +
+                                        component.getReference())] = component_module_name
+          erp5.component.filesystem_import_dict = filesystem_import_dict
+
       # __import__ will first try a relative import, for example
       # erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current
       # Component where an import is done
@@ -139,13 +170,7 @@ class ComponentDynamicPackage(ModuleType):
         id_ = "%s.%s.%s" % (self._id_prefix, version, name)
         # aq_base() because this should not go up to ERP5Site and trigger
         # side-effects, after all this only check for existence...
-        try:
-          component_tool = aq_base(site.portal_components)
-        except AttributeError:
-          # For old sites, just use FS Documents...
-          return None
-
-        component = getattr(component_tool, id_, None)
+        component = getattr(aq_base(site.portal_components), id_, None)
         if component is None or component.getValidationState() not in ('modified',
                                                                        'validated'):
           return None
@@ -161,12 +186,7 @@ class ComponentDynamicPackage(ModuleType):
 
       # name=REFERENCE
       else:
-        try:
-          component_tool = aq_base(site.portal_components)
-        except AttributeError:
-          # For old sites, just use FS Documents...
-          return None
-
+        component_tool = aq_base(site.portal_components)
         for version in site.getVersionPriorityNameList():
           id_ = "%s.%s.%s" % (self._id_prefix, version, name)
           component = getattr(component_tool, id_, None)
@@ -219,6 +239,14 @@ class ComponentDynamicPackage(ModuleType):
     module for any reason...
     """
     site = getSite()
+
+    if fullname.startswith('Products.'):
+      module_fullname_filesystem = fullname
+      import erp5.component
+      fullname = erp5.component.filesystem_import_dict[module_fullname_filesystem]
+    else:
+      module_fullname_filesystem = None
+
     name = fullname[len(self._namespace_prefix):]
 
     # if only Version package (erp5.component.XXX.VERSION_version) is
@@ -268,6 +296,9 @@ class ComponentDynamicPackage(ModuleType):
         setattr(self, name, module)
         sys.modules[module_fullname_alias] = module
         MNAME_MAP[module_fullname_alias] = module.__name__
+        if module_fullname_filesystem:
+          sys.modules[module_fullname_filesystem] = module
+          MNAME_MAP[module_fullname_filesystem] = module.__name__
         return module
 
     component = getattr(site.portal_components, component_id)
@@ -288,6 +319,8 @@ class ComponentDynamicPackage(ModuleType):
       sys.modules[module_fullname] = module
       if module_fullname_alias:
         sys.modules[module_fullname_alias] = module
+      if module_fullname_filesystem:
+        sys.modules[module_fullname_filesystem] = module
 
       # This must be set for imports at least (see PEP 302)
       module.__file__ = '<' + relative_url + '>'
@@ -308,6 +341,8 @@ class ComponentDynamicPackage(ModuleType):
         del sys.modules[module_fullname]
         if module_fullname_alias:
           del sys.modules[module_fullname_alias]
+        if module_fullname_filesystem:
+          del sys.modules[module_fullname_filesystem]
 
         raise ImportError(
           "%s: cannot load Component %s (%s)" % (fullname, name, error)), \
@@ -319,6 +354,8 @@ class ComponentDynamicPackage(ModuleType):
       if module_fullname_alias:
         setattr(self, name, module)
         MNAME_MAP[module_fullname_alias] = module_fullname
+        if module_fullname_filesystem:
+          MNAME_MAP[module_fullname_filesystem] = module.__name__
 
       import erp5.component
       erp5.component.ref_manager.add_module(module)
@@ -408,10 +445,14 @@ class ComponentDynamicPackage(ModuleType):
         for k in modsec_dict.keys():
           if k.startswith(self._namespace):
             del modsec_dict[k]
-      for k in MNAME_MAP.keys():
-        if k.startswith(self._namespace):
+      for k, v in MNAME_MAP.items():
+        if v.startswith(self._namespace):
           del MNAME_MAP[k]
 
+          # Products import compatibility (module_fullname_filesystem)
+          if k.startswith('Products.'):
+            del sys.modules[k]
+
     for name, module in package.__dict__.items():
       if name[0] == '_' or not isinstance(module, ModuleType):
         continue
diff --git a/product/ERP5Type/dynamic/dynamic_module.py b/product/ERP5Type/dynamic/dynamic_module.py
index f6f32f082c..e145dbf68b 100644
--- a/product/ERP5Type/dynamic/dynamic_module.py
+++ b/product/ERP5Type/dynamic/dynamic_module.py
@@ -120,6 +120,9 @@ class ComponentPackageType(PackageType):
   the top level, otherwise a module being relied upon may have a
   different API after reset, thus it may fail...
   """
+  # 'Products.ERP5.Document.Person' => 'erp5.component.document.Person'
+  filesystem_import_dict = None
+
   def __init__(self, *args, **kwargs):
     super(ComponentPackageType, self).__init__(*args, **kwargs)
     self.ref_manager = RefManager()
diff --git a/product/ERP5Type/mixin/component.py b/product/ERP5Type/mixin/component.py
index fe67f7e215..084543be32 100644
--- a/product/ERP5Type/mixin/component.py
+++ b/product/ERP5Type/mixin/component.py
@@ -178,7 +178,11 @@ class ComponentMixin(PropertyRecordableMixin, Base):
     'description': BaseAccessor.Getter('getDescription',
                                        'description',
                                        'string',
-                                       default='')
+                                       default=''),
+    'source_reference': BaseAccessor.Getter('getSourceReference',
+	                                    'source_reference',
+	                                    'string',
+		                            storage_id='default_source_reference'),
     }
 
   _message_invalid_id = "ID is invalid, should be '${id_prefix}.VERSION.REFERENCE'"
diff --git a/product/ERP5Type/tests/testDynamicClassGeneration.py b/product/ERP5Type/tests/testDynamicClassGeneration.py
index 9387b37c75..26e0b93d68 100644
--- a/product/ERP5Type/tests/testDynamicClassGeneration.py
+++ b/product/ERP5Type/tests/testDynamicClassGeneration.py
@@ -2655,25 +2655,11 @@ def foobar(self, a, b="portal_type"):
 
 from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
 
-class TestZodbDocumentComponent(_TestZodbComponent):
+class _TestZodbDocumentComponentMixin(_TestZodbComponent):
   """
-  Tests specific to ZODB Document Component. This is only for Document
-  previously defined in bt5 and installed on the filesystem in
-  $INSTANCE_HOME/Document. Later on, Product Documents will also be migrated
+  Common to all Component class inheriting from Document Component (so
+  Interface and Mixin)
   """
-  _portal_type = 'Document Component'
-  _document_class = DocumentComponent
-
-  def _getValidSourceCode(self, class_name):
-    return '''from erp5.component.document.Person import Person
-
-class %sAnything:
-  pass
-
-class %s(Person):
-  pass
-''' % (class_name, class_name)
-
   def testAtLeastOneClassNamedAfterReference(self):
     component = self._newComponent(
       self._generateReference('TestClassNamedAfterReference'))
@@ -2921,6 +2907,124 @@ class TestGC(XMLObject):
        'gc: collectable <Implements 0x%x>\n' % Implements_id],
       sorted(found_line_list))
 
+class TestZodbDocumentComponent(_TestZodbDocumentComponentMixin):
+  """
+  Tests specific to ZODB Document Component. This is only for Document
+  previously defined in bt5 and installed on the filesystem in
+  $INSTANCE_HOME/Document. Later on, Product Documents will also be migrated
+  """
+  _portal_type = 'Document Component'
+  _document_class = DocumentComponent
+
+  def _getValidSourceCode(self, class_name):
+    return '''from erp5.component.document.Person import Person
+
+class %sAnything:
+  pass
+
+class %s(Person):
+  pass
+''' % (class_name, class_name)
+
+  def testProductsERP5DocumentCompatibility(self):
+    """Check that document class also exist in its original namespace (source_reference)
+
+    Document Component that were moved from file system Products/*/Document needs
+    to be still importable from their initial location, as there might be classes
+    in the database of these instances.
+
+    There is no such test for Mixin/Interface/Tool because the code is the
+    same for all of them (component_package.py).
+    """
+    self.failIfModuleImportable('TestProductsERP5DocumentCompatibility')
+
+    test_component = self._newComponent(
+        'TestProductsERP5DocumentCompatibility',
+        """\
+from Products.ERP5Type.Base import Base
+class TestProductsERP5DocumentCompatibility(Base):
+  portal_type = 'Test ProductsERP5Document Compatibility'
+  test_attribute = 'TestProductsERP5DocumentCompatibility'
+"""
+    )
+    test_component.setSourceReference('Products.ERP5.Document.TestProductsERP5DocumentCompatibility')
+    test_component.validate()
+    self.tic()
+
+    self.assertModuleImportable('TestProductsERP5DocumentCompatibility')
+
+    from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+    self.assertEqual(TestProductsERP5DocumentCompatibility.test_attribute, 'TestProductsERP5DocumentCompatibility')
+
+    # this also exist in Products.ERP5Type.Document
+    from Products.ERP5Type.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility as TestProductsERP5DocumentCompatibility_from_ProductsERP5Type  # pylint:disable=import-error,no-name-in-module
+    self.assertIs(TestProductsERP5DocumentCompatibility_from_ProductsERP5Type, TestProductsERP5DocumentCompatibility)
+
+    # another component can also import the migrated component from its original name
+    test_component_importing = self._newComponent(
+        'TestComponentImporting',
+        """\
+from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility
+class TestComponentImporting(TestProductsERP5DocumentCompatibility):
+  pass
+"""
+    )
+    test_component_importing.validate()
+    self.tic()
+
+    self.assertModuleImportable('TestComponentImporting')
+    from erp5.component.document.TestComponentImporting import TestComponentImporting  # pylint:disable=import-error,no-name-in-module
+
+    from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+    self.assertTrue(issubclass(TestComponentImporting, TestProductsERP5DocumentCompatibility))
+
+    test_component.invalidate()
+    self.tic()
+
+    # after invalidating the component, the legacy modules are no longer importable
+    with self.assertRaises(ImportError):
+      from Products.ERP5.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+    with self.assertRaises(ImportError):
+      from Products.ERP5Type.Document.TestProductsERP5DocumentCompatibility import TestProductsERP5DocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+
+  def testProductsERP5TypeDocumentCompatibility(self):
+    """Check that document class also exist in Products.ERP5Type.Document namespace
+    for compatibility.
+
+    We also check that this module is properly reloaded when a document component
+    is modified.
+    """
+    self.failIfModuleImportable('TestProductsERP5TypeDocumentCompatibility')
+
+    test_component = self._newComponent(
+        'TestProductsERP5TypeDocumentCompatibility',
+        """\
+from Products.ERP5Type.Base import Base
+class TestProductsERP5TypeDocumentCompatibility(Base):
+  portal_type = 'Test ProductsERP5TypeDocument Compatibility'
+  generation = 1
+"""
+    )
+    test_component.validate()
+    self.tic()
+
+    self.assertModuleImportable('TestProductsERP5TypeDocumentCompatibility')
+
+    from Products.ERP5Type.Document.TestProductsERP5TypeDocumentCompatibility import TestProductsERP5TypeDocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+    self.assertEqual(TestProductsERP5TypeDocumentCompatibility.generation, 1)
+
+    test_component.setTextContent(
+        """\
+from Products.ERP5Type.Base import Base
+class TestProductsERP5TypeDocumentCompatibility(Base):
+  portal_type = 'Test ProductsERP5TypeDocument Compatibility'
+  generation = 2
+""")
+    self.tic()
+    self.assertModuleImportable('TestProductsERP5TypeDocumentCompatibility')
+    from Products.ERP5Type.Document.TestProductsERP5TypeDocumentCompatibility import TestProductsERP5TypeDocumentCompatibility  # pylint:disable=import-error,no-name-in-module
+    self.assertEqual(TestProductsERP5TypeDocumentCompatibility.generation, 2)
+
 from Products.ERP5Type.Core.TestComponent import TestComponent
 
 class TestZodbTestComponent(_TestZodbComponent):
@@ -3091,7 +3195,7 @@ class Test(ERP5TypeTestCase):
       self.commit()
 
 from Products.ERP5Type.Core.InterfaceComponent import InterfaceComponent
-class TestZodbInterfaceComponent(TestZodbDocumentComponent):
+class TestZodbInterfaceComponent(_TestZodbDocumentComponentMixin):
   """
   Tests specific to ZODB Interface Component.
   """
diff --git a/product/ERP5Type/tests/testERP5Type.py b/product/ERP5Type/tests/testERP5Type.py
index 4396ad66b1..081c2ef0f6 100644
--- a/product/ERP5Type/tests/testERP5Type.py
+++ b/product/ERP5Type/tests/testERP5Type.py
@@ -33,6 +33,7 @@ of Portal Type as Classes and ZODB Components
 
 import unittest
 from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
+from AccessControl.ZopeGuards import guarded_import
 from Products.ERP5Type.tests.utils import LogInterceptor
 
 class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
@@ -205,6 +206,41 @@ class TestERP5Type(ERP5TypeTestCase, LogInterceptor):
       subdocument_record = sql_catalog.getRecordForUid(subdocument.uid)
       self.assertEqual(subdocument.getPath(), subdocument_record.path)
 
+    def test_products_document_legacy(self):
+      """check document classes defined in Products/*/Document/*.py
+      """
+      # note: this assertion below checks Alarm is really a legacy document class.
+      # if one day Alarm is moved to component, then this test needs to be updated
+      # with another module that lives on the file system.
+      import Products.ERP5.Document.Alarm
+      self.assertIn('product/ERP5/Document/Alarm.py', Products.ERP5.Document.Alarm.__file__)
+
+      # document classes are also dynamically loaded in Products.ERP5Type.Document module
+      from Products.ERP5Type.Document.Alarm import Alarm as Alarm_from_ERP5Type  # pylint:disable=import-error,no-name-in-module
+      self.assertIs(Alarm_from_ERP5Type, Products.ERP5.Document.Alarm.Alarm)
+
+      # a new temp constructor is created
+      from Products.ERP5Type.Document import newTempAlarm  # pylint:disable=import-error,no-name-in-module
+      self.assertIn(Alarm_from_ERP5Type, newTempAlarm(self.portal, '').__class__.mro())
+
+      # temp constructors are deprecated, they issue a warning when called
+      import mock
+      with mock.patch('Products.ERP5Type.Utils.warnings.warn') as warn:
+          newTempAlarm(self.portal, '')
+      warn.assert_called_with(
+          'newTemp*(self, ID) will be removed, use self.newContent(temp_object=True, id=ID, portal_type=...)',
+          DeprecationWarning, 2)
+
+    def test_03_NewTempObject(self):
+      # Products.ERP5Type.Document.newTempBase is another (not recommended) way
+      # of creating temp objects
+      import Products.ERP5Type.Document
+      o = Products.ERP5Type.Document.newTempBase(self.portal, 'id')
+      self.assertEqual(o.getId(), 'id')
+      self.assertEqual(o.getPortalType(), 'Base Object')
+      self.assertTrue(o.isTempObject())
+      self.assertTrue(guarded_import("Products.ERP5Type.Document", fromlist=["newTempBase"]))
+
 def test_suite():
   suite = unittest.TestSuite()
   suite.addTest(unittest.makeSuite(TestERP5Type))
-- 
2.30.9