# Part of Odoo. See LICENSE file for full copyright and licensing details. import json from odoo import api, fields, models from odoo.exceptions import UserError from odoo.tools import _, frozendict class ProjectProject(models.Model): _name = 'project.project' _inherit = ['project.project'] documents_folder_id = fields.Many2one( 'documents.document', string="Documents Folder", copy=False, context=lambda env: { 'default_folder_id': env.company.documents_project_folder_id.id, }, domain="[('type', '=', 'folder'), ('shortcut_document_id', '=', False), " "'|', ('company_id', '=', False), ('company_id', '=', company_id)]", index='btree_not_null', help="Folder in which all of the documents of this project will be categorized.") document_count = fields.Integer( compute='_compute_documents', export_string_translation=False) document_ids = fields.One2many('documents.document', compute='_compute_documents', export_string_translation=False) @api.ondelete(at_uninstall=False) def _archive_folder_on_projects_unlinked(self): """ Archives the project folder if all its related projects are unlinked. """ self.env['documents.document'].sudo().search([ ('project_ids', '!=', False), ('project_ids', 'not any', [('id', 'not in', self.ids)] )]).sudo(False)._filtered_access('unlink').action_archive() @api.constrains('documents_folder_id') def _check_company_is_folders_company(self): for project in self.filtered('documents_folder_id'): if folder := project['documents_folder_id']: if folder.company_id and project.company_id != folder.company_id: raise UserError(_( 'The "%(folder)s" folder should either be in the "%(company)s" company like this' ' project or be open to all companies.', folder=folder.name, company=project.company_id.name) ) def _compute_documents(self): documents = self.env['documents.document'].search([ ('type', 'in', ('binary', 'url')), ('id', 'child_of', self.documents_folder_id.ids) ]) for project in self: if not project.documents_folder_id: project.document_ids = self.env['documents.document'] project.document_count = 0 continue document_ids = documents.filtered(lambda doc: doc.parent_path.startswith(project.documents_folder_id.parent_path)) project.document_ids = document_ids project.document_count = len(document_ids) def _create_missing_folders(self): folders_to_create_vals = [] projects_with_folder_to_create = [] documents_project_folder_id = self.env.company.documents_project_folder_id.id for project in self: if not project.documents_folder_id: folder_vals = { 'access_internal': 'edit' if project.privacy_visibility in ['employees', 'portal'] else 'none', 'company_id': project.company_id.id, 'folder_id': documents_project_folder_id, 'name': project.name, 'type': 'folder', } folders_to_create_vals.append(folder_vals) projects_with_folder_to_create.append(project) if folders_to_create_vals: created_folders = self.env['documents.document'].sudo().create(folders_to_create_vals) for project, folder in zip(projects_with_folder_to_create, created_folders): project.sudo().documents_folder_id = folder @api.model_create_multi def create(self, vals_list): projects = super().create(vals_list) if not self.env.context.get('no_create_folder'): projects._create_missing_folders() return projects def write(self, vals): if 'company_id' in vals: for project in self: if project.documents_folder_id and project.documents_folder_id.company_id and len(project.documents_folder_id.project_ids) > 1: other_projects = project.documents_folder_id.project_ids - self if other_projects and other_projects.company_id.id != vals['company_id']: lines = [f"- {project.name}" for project in other_projects] raise UserError(_( 'You cannot change the company of this project, because its folder is linked to the other following projects that are still in the "%(other_company)s" company:\n%(other_folders)s\n\n' 'Please update the company of all projects so that they remain in the same company as their folder, or leave the company of the "%(folder)s" folder blank.', other_company=other_projects.company_id.name, other_folders='\n'.join(lines), folder=project.documents_folder_id.name)) if 'name' in vals: projects = self.filtered( lambda p: ( (folder := p.documents_folder_id.sudo()) and len(folder.project_ids) == 1 and p.name == folder.name ) ) projects.documents_folder_id.sudo().name = vals['name'] if new_visibility := vals.get('privacy_visibility'): (self.documents_folder_id | self.document_ids).action_update_access_rights( access_internal='none' if new_visibility == 'followers' else 'edit') project_root_documents = self.env['documents.document'] if 'documents_folder_id' in vals: project_root_documents = self.documents_folder_id.children_ids res = super().write(vals) if 'company_id' in vals: for project in self: if project.documents_folder_id and project.documents_folder_id.company_id: project.documents_folder_id.company_id = project.company_id if not self.env.context.get('no_create_folder'): self._create_missing_folders() if project_root_documents: project_root_documents.folder_id = self.documents_folder_id return res def copy(self, default=None): # We have to add no_create_folder=True to the context, otherwise a folder # will be automatically created during the call to create. copied_projects = super(ProjectProject, self.with_context(no_create_folder=True)).copy(default).with_env(self.env) for old_project, new_project in zip(self, copied_projects): if not self.env.context.get('no_create_folder') and old_project.documents_folder_id: new_project.documents_folder_id = old_project.documents_folder_id.sudo().copy( {'name': new_project.name, 'owner_id': False} ) return copied_projects def _get_stat_buttons(self): buttons = super()._get_stat_buttons() if self.documents_folder_id.user_permission != 'none': buttons.append({ 'icon': 'file-text-o', 'text': self.env._('Documents'), 'number': self.document_count, 'action_type': 'object', 'action': 'action_view_documents_project', 'additional_context': json.dumps({ 'active_id': self.id, }), 'sequence': 20, }) return buttons def action_view_documents_project(self): self.ensure_one() action = self.documents_folder_id.get_formview_action() if action['tag'] != 'display_notification': action['context'] |= { 'active_id': self.id, 'active_model': 'project.project', } return action def create_template_from_project_undo_callback(self, callbacks): super().create_template_from_project_undo_callback(callbacks) if self.documents_folder_id and callbacks.get("unarchive_project"): self.documents_folder_id.action_unarchive() def _get_template_from_project_undo_callbacks(self): callbacks = super()._get_template_from_project_undo_callbacks() if self.documents_folder_id and callbacks.get("unarchive_project"): self.documents_folder_id.action_archive() return callbacks