# -*- coding: utf-8 -*- import base64 from odoo.exceptions import UserError from odoo.tests.common import users from odoo.addons.documents.tests.test_documents_common import TransactionCaseDocuments from odoo.addons.project.tests.test_project_base import TestProjectCommon GIF = b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" TEXT = base64.b64encode(bytes("workflow bridge project", 'utf-8')) class TestDocumentsBridgeProject(TestProjectCommon, TransactionCaseDocuments): @classmethod def setUpClass(cls): super().setUpClass() cls.document_txt_2 = cls.env['documents.document'].create({ 'datas': TEXT, 'name': 'file2.txt', 'mimetype': 'text/plain', 'folder_id': cls.folder_a_a.id, }) cls.pro_admin = cls.env['res.users'].create({ 'name': 'Project Admin', 'login': 'proj_admin', 'email': 'proj_admin@example.com', 'group_ids': [(4, cls.env.ref('project.group_project_manager').id)], }) def test_archive_folder_on_projects_unlinked(self): """ Projects folders should be archived if every related projects have been unlinked. """ folder_1, folder_2, folder_3, folder_4 = self.env['documents.document'].create( [{'name': f'F{i}', 'type': 'folder'} for i in range(4)] ) ( project_0, project_1, project_2, project_3, project_4, project_5, project_6, _, ) = self.env['project.project'].create([ {'name': f'p{i}', 'documents_folder_id': folder.id} for i, folder in enumerate([folder_1] * 4 + [folder_2] * 3 + [folder_3]) ]) cases = [ ( project_1 | project_4, folder_1 | folder_2 | folder_3 | folder_4, self.env['documents.document'] ), ( project_2, folder_1 | folder_2 | folder_3 | folder_4, self.env['documents.document'] ), ( project_0 | project_3 | project_5 | project_6, folder_3 | folder_4, folder_1 | folder_2 ), ] count_start = self.env['documents.document'].search_count([('active', '=', True)]) for projects_to_unlink, active_folders, inactive_folders in cases: with self.subTest(projects_to_unlink=projects_to_unlink, active_folders=active_folders, inactive_folders=inactive_folders): projects_to_unlink.unlink() self.assertTrue(all(active_folders.mapped('active'))) self.assertFalse(any(inactive_folders.mapped('active'))) count_end = self.env['documents.document'].search_count([('active', '=', True)]) self.assertEqual(count_end, count_start - 2) def test_bridge_parent_folder(self): """ Tests the "Parent Folder" setting """ parent_folder = self.env.company.documents_project_folder_id self.assertEqual(self.project_pigs.documents_folder_id.folder_id, parent_folder, "The folder of the project should be a child of the 'Projects' folder.") def test_project_folder_creation(self): project = self.env['project.project'].create({ 'name': 'Project', }) self.assertTrue(project.documents_folder_id, "A folder should be created for the project") def test_project_task_access_document(self): """ Tests that 'MissingRecord' error should not be raised when trying to switch folder for a non-existing document. - The 'active_id' here is the 'id' of a non-existing document. - We then try to access 'All' folder by calling the 'search_panel_select_range' method. We should be able to access the folder. """ missing_id = self.env['documents.document'].search([], order='id DESC', limit=1).id + 1 result = self.env['documents.document'].with_context( active_id=missing_id, active_model='project.task').search_panel_select_range('user_folder_id') self.assertTrue(result) def test_copy_project(self): """ When duplicating a project, there should be exactly one copy of the folder linked to the project. If there is the `no_create_folder` context key, then the folder should not be copied (note that in normal flows, when this context key is used, it is expected that a folder will be copied/created manually, so that we don't end up with a project having the documents feature enabled but no folder). """ last_folder_id = self.env['documents.document'].search([('type', '=', 'folder')], order='id desc', limit=1).id self.project_pigs.copy() new_folder = self.env['documents.document'].search([('type', '=', 'folder'), ('id', '>', last_folder_id)]) self.assertEqual(len(new_folder), 1, "There should only be one new folder created.") self.project_goats.with_context(no_create_folder=True).copy() self.assertEqual(self.env['documents.document'].search_count( [('type', '=', 'folder'), ('id', '>', new_folder.id)], limit=1), 0, "There should be no new folder created." ) @users('proj_admin') def test_rename_project(self): """ When renaming a project, the corresponding folder should be renamed as well. Even when the user does not have write access on the folder, the project should be able to rename it. """ new_name = 'New Name' self.project_pigs.with_user(self.env.user).name = new_name self.assertEqual(self.project_pigs.documents_folder_id.name, new_name, "The folder should have been renamed along with the project.") def test_delete_project_folder(self): """ It should not be possible to delete the "Projects" folder. """ project_folder = self.env.ref('documents_project.document_project_folder') with self.assertRaises(UserError, msg="It should not be possible to delete the 'Projects' folder"): project_folder.unlink() current = project_folder for i in range(3): current.folder_id = self.env['documents.document'].create({ "name": f"Ancestor Test {i}", "type": "folder", }) current = current.folder_id with self.assertRaises(UserError, msg="It should not be possible to delete an ancestor of the 'Projects' folder"): current.unlink() # But it shouldn't interfere with legit deletion/archiving project_folder.action_update_access_rights( access_internal='none', access_via_link='none', partners={self.doc_user.partner_id.id: (False, False)}) project_folder.invalidate_recordset(fnames=['parent_path']) self.document_txt.with_user(self.doc_user).action_archive() self.assertFalse(self.document_txt.active) self.document_txt.with_user(self.doc_user).unlink() self.assertFalse(self.document_txt.exists()) def test_changing_project_folder_moves_documents(self): """ When a project folder changes, move documents. """ project = self.env['project.project'].create({'name': 'Project'}) project_document, other_folder = self.env['documents.document'].create([{ 'name': 'Test project request', 'folder_id': project.documents_folder_id.id, }, { 'name': 'Other Folder', 'type': 'folder', 'folder_id': self.env.ref('documents_project.document_project_folder').id, }]) project.invalidate_recordset() self.assertEqual(project_document, project.document_ids) project.documents_folder_id = other_folder self.assertEqual(project_document.folder_id, other_folder) self.assertEqual(project_document, project.document_ids) def test_delete_folder_used_by_project(self): """ It shouldn't be possible to delete a folder that is used by one or multiple projects. """ project = self.env['project.project'].create({ 'name': "Test", }) with self.assertRaises(UserError): project.documents_folder_id.unlink() project_1, project_2, project_3 = self.env['project.project'].create([{ 'name': f"Test Project {i}", } for i in range(3)]) project_2.documents_folder_id = project_3.documents_folder_id (project_1 | project_2).documents_folder_id.folder_id = parent_folder = self.env['documents.document'].create({ 'name': "Test Folder", 'type': 'folder', }) with self.assertRaises(UserError, msg="It shouldn't be possible to delete a folder that is used by one or multiple projects."): parent_folder.unlink() def test_add_attachment_to_project_folder_id(self): project = self.env['project.project'].create({'name': "Test Project"}) self.assertTrue(project.documents_folder_id) task = self.env['project.task'].create({'name': "Test Task", 'project_id': project.id}) project_attachment, task_attachment = self.env['ir.attachment'].create([ {"name": "project_image.png", "datas": GIF, "res_model": "project.project", "res_id": project.id}, {"name": "task_image.png", "datas": GIF, "res_model": "project.task", "res_id": task.id}, ]) for attachment in (project_attachment, task_attachment): self.assertEqual(attachment.get_documents_operation_add_destination(), { 'destination': str(project.documents_folder_id.id), 'display_name': project.documents_folder_id.display_name, }) def test_project_copy_includes_linked_documents(self): """ Test that copying a project also copies its linked document. Steps: 1. Link a document to the project (set folder and res_id). 2. Copy the project. 3. Check that the copied project has one linked document. """ self.document_txt_2.write({ 'folder_id': self.project_pigs.documents_folder_id.id, 'res_id': self.project_pigs.id, }) copied_project_documents = self.project_pigs.copy().document_ids self.assertEqual( len(copied_project_documents), 1, "The copied project should have one document copied." ) def test_project_document_unarchive_on_revert(self): """ Ensure project documents are unarchived when a project is reverted Steps: 1. Create a project 2. Convert the project into a project template 3. Undo the project 4. Check project documents """ client_action = self.project_pigs.action_create_template_from_project() self.env['project.project'].browse(client_action['params']['project_id']).unlink() self.project_pigs.create_template_from_project_undo_callback( client_action['params']['callback_data']['args'][1] ) self.project_pigs.action_create_template_from_project()