Skip to content

Commit 1650324

Browse files
committed
[ADD] dms_import
1 parent 9223063 commit 1650324

File tree

11 files changed

+810
-0
lines changed

11 files changed

+810
-0
lines changed

dms_import/README.rst

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
=================================
2+
Document Management System Import
3+
=================================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:d26c6b062fc8f9eab8449f88f869f2da1d45e05f6332b2ca1db17948961c7ed0
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github
20+
:target: https://github.com/OCA/dms/tree/16.0/dms_import
21+
:alt: OCA/dms
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/dms-16-0/dms-16-0-dms_import
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
Use this module to migrate from the EE `documents*` modules to the OCA `dms*` modules.
32+
33+
**Table of contents**
34+
35+
.. contents::
36+
:local:
37+
38+
Bug Tracker
39+
===========
40+
41+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/dms/issues>`_.
42+
In case of trouble, please check there if your issue has already been reported.
43+
If you spotted it first, help us to smash it by providing a detailed and welcomed
44+
`feedback <https://github.com/OCA/dms/issues/new?body=module:%20dms_import%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
45+
46+
Do not contact contributors directly about support or help with technical issues.
47+
48+
Credits
49+
=======
50+
51+
Authors
52+
~~~~~~~
53+
54+
* Kencove
55+
56+
Contributors
57+
~~~~~~~~~~~~
58+
59+
- [Trobz](https://www.trobz.com):
60+
- Do Anh Duy <<duyda@trobz.com>>
61+
62+
Maintainers
63+
~~~~~~~~~~~
64+
65+
This module is maintained by the OCA.
66+
67+
.. image:: https://odoo-community.org/logo.png
68+
:alt: Odoo Community Association
69+
:target: https://odoo-community.org
70+
71+
OCA, or the Odoo Community Association, is a nonprofit organization whose
72+
mission is to support the collaborative development of Odoo features and
73+
promote its widespread use.
74+
75+
This module is part of the `OCA/dms <https://github.com/OCA/dms/tree/16.0/dms_import>`_ project on GitHub.
76+
77+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

dms_import/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .hooks import pre_init_hook

dms_import/__manifest__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2025 Kencove (https://www.kencove.com).
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
{
5+
"name": "Document Management System Import",
6+
"summary": """
7+
Import data from document EE to dms CE
8+
""",
9+
"version": "16.0.1.0.0",
10+
"license": "AGPL-3",
11+
"category": "Document Management",
12+
"author": "Kencove, Odoo Community Association (OCA)",
13+
"website": "https://github.com/OCA/dms",
14+
"depends": ["dms"],
15+
"pre_init_hook": "pre_init_hook",
16+
}

dms_import/hooks.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Copyright 2025 Kencove (https://www.kencove.com).
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
import logging
5+
from random import randint
6+
7+
from odoo import SUPERUSER_ID, api
8+
from odoo.tools import table_exists
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def _default_color():
14+
return randint(1, 11)
15+
16+
17+
def _default_storage(cr, env):
18+
cr.execute("SELECT id FROM dms_storage WHERE save_type = 'database' LIMIT 1")
19+
db_storage = cr.fetchone()
20+
if db_storage:
21+
return db_storage[0]
22+
storage = env["dms.storage"].create(
23+
{
24+
"name": "Database Storage EE",
25+
"save_type": "database",
26+
}
27+
)
28+
logger.info("Created default database storage (ID: %s)", storage.id)
29+
return storage.id
30+
31+
32+
def _assign_dms_group(env, doc_folder):
33+
dms_access_group = env["dms.access.group"].browse()
34+
if doc_folder.group_ids:
35+
write_group = env["dms.access.group"].create(
36+
{
37+
"name": f"{doc_folder.name} Write Group",
38+
"perm_create": True,
39+
"perm_write": True,
40+
"perm_unlink": True,
41+
"group_ids": [(6, 0, doc_folder.group_ids.ids)],
42+
}
43+
)
44+
dms_access_group |= write_group
45+
if doc_folder.read_group_ids:
46+
read_group = env["dms.access.group"].create(
47+
{
48+
"name": f"{doc_folder.name} Read Group",
49+
"group_ids": [(6, 0, doc_folder.read_group_ids.ids)],
50+
}
51+
)
52+
dms_access_group |= read_group
53+
if not dms_access_group:
54+
dms_access_group = env["dms.access.group"].create(
55+
{
56+
"name": f"{doc_folder.name} Default Group",
57+
"group_ids": [(6, 0, env.ref("dms.group_dms_manager").ids)],
58+
}
59+
)
60+
return dms_access_group
61+
62+
63+
def migrate_documents_tag_to_dms_tag(cr, env, lang):
64+
if not table_exists(cr, "documents_tag"):
65+
logger.info("Skipping tag migration: documents_tag table does not exist.")
66+
return {}
67+
68+
logger.info("Migrating tags from documents_tag to dms_tag...")
69+
70+
# Fetch existing dms categories
71+
cr.execute("SELECT id, name->>%s AS name FROM dms_category", (lang,))
72+
existing_tag_categories = {row["name"]: row["id"] for row in cr.dictfetchall()}
73+
74+
# Migrate facets -> categories
75+
cr.execute("SELECT id, name->>%s AS name FROM documents_facet", (lang,))
76+
documents_facets = cr.dictfetchall()
77+
78+
category_mapping = {}
79+
dms_category = env["dms.category"]
80+
for facet in documents_facets:
81+
name = facet["name"]
82+
if name in existing_tag_categories:
83+
category_mapping[facet["id"]] = existing_tag_categories[name]
84+
else:
85+
try:
86+
cat = dms_category.create({"name": name})
87+
category_mapping[facet["id"]] = cat.id
88+
existing_tag_categories[name] = cat.id
89+
logger.info("Created dms.category: '%s' (ID: %s)", name, cat.id)
90+
except Exception as e:
91+
logger.info("Failed to create dms.category for '%s': %s", name, str(e))
92+
93+
# Migrate tags
94+
cr.execute("""SELECT id, name->>%s AS name, facet_id FROM documents_tag""", (lang,))
95+
document_tags = cr.dictfetchall()
96+
97+
cr.execute("SELECT id, name->>%s AS name, category_id FROM dms_tag", (lang,))
98+
existing_tags = {
99+
(row["name"], row.get("category_id")): row["id"] for row in cr.dictfetchall()
100+
}
101+
102+
tag_mapping = {}
103+
dms_tag = env["dms.tag"]
104+
for tag in document_tags:
105+
name = tag["name"]
106+
category_id = category_mapping.get(tag["facet_id"])
107+
key = (name, category_id)
108+
if key in existing_tags:
109+
tag_mapping[tag["id"]] = existing_tags[key]
110+
else:
111+
try:
112+
vals = {"name": name, "color": _default_color()}
113+
if category_id:
114+
vals["category_id"] = category_id
115+
new_tag = dms_tag.create(vals)
116+
tag_mapping[tag["id"]] = new_tag.id
117+
existing_tags[key] = new_tag.id
118+
logger.info("Created dms.tag: '%s' (ID: %s)", name, new_tag.id)
119+
except Exception as e:
120+
logger.info("Failed to create dms.tag for '%s': %s", name, str(e))
121+
logger.info("Successfully migrated %d tags.", len(tag_mapping))
122+
return tag_mapping
123+
124+
125+
def migrate_documents_folders_to_dms_directories(cr, env, lang, tag_mapping):
126+
if not table_exists(cr, "documents_folder"):
127+
logger.info("Skipping folder migration: documents_folder table does not exist.")
128+
return {}
129+
130+
logger.info("Migrating folders from documents_folder to dms_directory...")
131+
132+
cr.execute(
133+
"""
134+
SELECT id, name->>%s AS name, parent_folder_id, sequence
135+
FROM documents_folder
136+
ORDER BY parent_path
137+
""",
138+
(lang,),
139+
)
140+
folders = cr.dictfetchall()
141+
142+
cr.execute("SELECT name FROM dms_directory WHERE is_root_directory = TRUE")
143+
existing_root_dirs = {row[0] for row in cr.fetchall()}
144+
145+
folder_mapping = {}
146+
dms_directory = env["dms.directory"]
147+
created_root_count = 0
148+
db_storage_id = _default_storage(cr, env)
149+
150+
for folder in folders:
151+
try:
152+
is_root = not folder["parent_folder_id"]
153+
parent_id = folder_mapping.get(folder["parent_folder_id"])
154+
155+
vals = {
156+
"name": folder["name"],
157+
"parent_id": parent_id,
158+
"is_root_directory": is_root,
159+
"color": folder["sequence"]
160+
if folder["sequence"] < 11
161+
else _default_color(),
162+
}
163+
164+
if is_root and folder["name"] not in existing_root_dirs:
165+
vals["storage_id"] = db_storage_id
166+
existing_root_dirs.add(folder["name"])
167+
created_root_count += 1
168+
169+
new_dir = dms_directory.create(vals)
170+
folder_mapping[folder["id"]] = new_dir.id
171+
logger.info(
172+
"Created dms.directory: '%s' (ID: %s)", folder["name"], new_dir.id
173+
)
174+
175+
doc_folder = env["documents.folder"].browse(folder["id"])
176+
dms_groups = _assign_dms_group(env, doc_folder)
177+
if dms_groups:
178+
new_dir.group_ids = [(6, 0, dms_groups.ids)]
179+
180+
dir_tag = []
181+
for facet in doc_folder.facet_ids:
182+
for tag in facet.tag_ids:
183+
tag_id = tag.id
184+
if tag_id in tag_mapping:
185+
dir_tag.append(tag_mapping[tag_id])
186+
if dir_tag:
187+
new_dir.tag_ids = [(6, 0, dir_tag)]
188+
189+
except Exception as e:
190+
logger.info(
191+
"Error migrating folder ID %s (%s)", folder["id"], folder["name"]
192+
)
193+
logger.info(str(e))
194+
195+
logger.info(
196+
"Successfully migrated %d folders (%d new root directories).",
197+
len(folder_mapping),
198+
created_root_count,
199+
)
200+
return folder_mapping
201+
202+
203+
def migrate_documents_to_dms_files(cr, env, folder_mapping, tag_mapping):
204+
if not table_exists(cr, "documents_document") or not folder_mapping:
205+
logger.info(
206+
"Skipping file migration: documents_document table or folder mapping missing."
207+
)
208+
return
209+
210+
logger.info("Migrating files from documents_document to dms_file...")
211+
212+
cr.execute(
213+
"""
214+
SELECT id, name, folder_id, attachment_id
215+
FROM documents_document
216+
WHERE type = 'binary' AND active = TRUE
217+
"""
218+
)
219+
documents = cr.dictfetchall()
220+
221+
for doc in documents:
222+
try:
223+
directory_id = folder_mapping.get(doc["folder_id"])
224+
if doc["folder_id"] and not directory_id:
225+
logger.info("Skipping document %s: no mapped folder found.", doc["id"])
226+
continue
227+
228+
file_vals = {
229+
"name": doc["name"],
230+
"directory_id": directory_id,
231+
}
232+
233+
if doc["attachment_id"]:
234+
file_vals["attachment_id"] = doc["attachment_id"]
235+
236+
dms_file = env["dms.file"].create(file_vals)
237+
logger.info("Created dms.file: '%s' (ID: %s)", doc["name"], dms_file.id)
238+
239+
cr.execute(
240+
"""
241+
SELECT documents_tag_id
242+
FROM document_tag_rel
243+
WHERE documents_document_id = %s
244+
""",
245+
(doc["id"],),
246+
)
247+
tag_ids = [
248+
tag_mapping[rec[0]] for rec in cr.fetchall() if rec[0] in tag_mapping
249+
]
250+
if tag_ids:
251+
dms_file.tag_ids = [(6, 0, tag_ids)]
252+
253+
if doc["attachment_id"]:
254+
env["ir.attachment"].browse(doc["attachment_id"]).write(
255+
{
256+
"res_model": "dms.file",
257+
"res_id": dms_file.id,
258+
}
259+
)
260+
except Exception as e:
261+
logger.info("Error migrating document ID %s (%s)", doc["id"], doc["name"])
262+
logger.info(str(e))
263+
264+
265+
def pre_init_hook(cr):
266+
env = api.Environment(cr, SUPERUSER_ID, {})
267+
try:
268+
lang = env.lang or "en_US"
269+
logger.info("Using language '%s' to migration...", lang)
270+
tag_mapping = migrate_documents_tag_to_dms_tag(cr, env, lang)
271+
folder_mapping = migrate_documents_folders_to_dms_directories(
272+
cr, env, lang, tag_mapping
273+
)
274+
migrate_documents_to_dms_files(cr, env, folder_mapping, tag_mapping)
275+
env.cr.commit()
276+
logger.info("Migration completed successfully.")
277+
except Exception as e:
278+
env.cr.rollback()
279+
logger.info("Migration failed: %s", str(e))

dms_import/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"

dms_import/readme/CONTRIBUTORS.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- [Trobz](https://www.trobz.com):
2+
- Do Anh Duy <<duyda@trobz.com>>

dms_import/readme/DESCRIPTION.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use this module to migrate from the EE `documents*` modules to the OCA `dms*` modules.
9.23 KB
Loading

0 commit comments

Comments
 (0)