mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-20 22:22:09 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import project_project_stage_delete
|
||||
from . import project_task_share_wizard
|
||||
from . import project_task_type_delete
|
||||
from . import project_share_wizard
|
||||
from . import project_share_collaborator_wizard
|
||||
from . import project_template_create_wizard
|
||||
from . import portal_share
|
||||
|
|
|
|||
15
odoo-bringout-oca-ocb-project/project/wizard/portal_share.py
Normal file
15
odoo-bringout-oca-ocb-project/project/wizard/portal_share.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class PortalShare(models.TransientModel):
|
||||
_inherit = 'portal.share'
|
||||
|
||||
def action_send_mail(self):
|
||||
# Extend portal share to subscribe partners when sharing project tasks.
|
||||
result = super().action_send_mail()
|
||||
|
||||
# Only subscribe partners if shared from project.task
|
||||
if self.res_model == 'project.task':
|
||||
self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids)
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from ast import literal_eval
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProjectProjectStageDeleteWizard(models.TransientModel):
|
||||
_name = 'project.project.stage.delete.wizard'
|
||||
_description = 'Project Stage Delete Wizard'
|
||||
|
||||
stage_ids = fields.Many2many('project.project.stage', string='Stages To Delete', ondelete='cascade', context={'active_test': False}, export_string_translation=False)
|
||||
projects_count = fields.Integer('Number of Projects', compute='_compute_projects_count', export_string_translation=False)
|
||||
stages_active = fields.Boolean(compute='_compute_stages_active', export_string_translation=False)
|
||||
|
||||
def _compute_projects_count(self):
|
||||
for wizard in self:
|
||||
wizard.projects_count = self.with_context(active_test=False).env['project.project'].search_count([('stage_id', 'in', wizard.stage_ids.ids)])
|
||||
|
||||
@api.depends('stage_ids')
|
||||
def _compute_stages_active(self):
|
||||
for wizard in self:
|
||||
wizard.stages_active = all(wizard.stage_ids.mapped('active'))
|
||||
|
||||
def action_archive(self):
|
||||
projects = self.with_context(active_test=False).env['project.project'].search([('stage_id', 'in', self.stage_ids.ids)])
|
||||
projects.write({'active': False})
|
||||
self.stage_ids.write({'active': False})
|
||||
return self._get_action()
|
||||
|
||||
def action_unarchive_project(self):
|
||||
inactive_projects = self.env['project.project'].with_context(active_test=False).search(
|
||||
[('active', '=', False), ('stage_id', 'in', self.stage_ids.ids)])
|
||||
inactive_projects.action_unarchive()
|
||||
|
||||
def action_unlink(self):
|
||||
self.stage_ids.unlink()
|
||||
return self._get_action()
|
||||
|
||||
def _get_action(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("project.project_project_stage_configure")\
|
||||
if self.env.context.get('stage_view')\
|
||||
else self.env["ir.actions.actions"]._for_xml_id("project.open_view_project_all_group_stage")
|
||||
|
||||
context = action.get('context', '{}')
|
||||
context = context.replace('uid', str(self.env.uid))
|
||||
context = dict(literal_eval(context), active_test=True)
|
||||
action['context'] = context
|
||||
action['target'] = 'main'
|
||||
return action
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_project_project_stage_delete_wizard" model="ir.ui.view">
|
||||
<field name="name">project.project.stage.delete.wizard.form</field>
|
||||
<field name="model">project.project.stage.delete.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Delete Project Stage">
|
||||
<field name="projects_count" invisible="1" />
|
||||
<field name="stages_active" invisible="1" />
|
||||
<div invisible="projects_count > 0">
|
||||
<p>Are you sure you want to delete these stages?</p>
|
||||
</div>
|
||||
<div invisible="not stages_active or projects_count == 0">
|
||||
<p>You cannot delete stages containing projects. You can either archive them or first delete all of their projects.</p>
|
||||
</div>
|
||||
<div invisible="stages_active or projects_count == 0">
|
||||
<p>You cannot delete stages containing projects. You should first delete all of their projects.</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Archive Stages" type="object" name="action_archive" class="btn btn-primary" invisible="not stages_active or projects_count == 0" data-hotkey="q"/>
|
||||
<button string="Delete" type="object" name="action_unlink" class="btn btn-primary" invisible="projects_count > 0" data-hotkey="w"/>
|
||||
<button string="Discard" special="cancel" data-hotkey="x" class="btn btn-primary" invisible="not (stages_active or projects_count)" />
|
||||
<button string="Discard" special="cancel" data-hotkey="x" invisible="stages_active or projects_count" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_project_stage_unarchive_wizard" model="ir.ui.view">
|
||||
<field name="name">project.project.stage.delete.wizard.form</field>
|
||||
<field name="model">project.project.stage.delete.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Delete Stage">
|
||||
<div>
|
||||
<p>Would you like to unarchive all of the projects contained in these stages as well?</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Confirm" type="object" name="action_unarchive_project" class="btn btn-primary"/>
|
||||
<button string="Discard" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProjectShareCollaboratorWizard(models.TransientModel):
|
||||
_name = 'project.share.collaborator.wizard'
|
||||
_description = 'Project Sharing Collaborator Wizard'
|
||||
|
||||
parent_wizard_id = fields.Many2one(
|
||||
'project.share.wizard',
|
||||
export_string_translation=False,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Collaborator',
|
||||
required=True,
|
||||
)
|
||||
access_mode = fields.Selection(
|
||||
[('read', 'Read'), ('edit_limited', 'Edit with limited access'), ('edit', 'Edit')],
|
||||
default='read',
|
||||
required=True,
|
||||
help="Read: collaborators can view tasks but cannot edit them.\n"
|
||||
"Edit with limited access: collaborators can view and edit tasks they follow in the Kanban view.\n"
|
||||
"Edit: collaborators can view and edit all tasks in the Kanban view. Additionally, they can choose which tasks they want to follow."
|
||||
)
|
||||
send_invitation = fields.Boolean(
|
||||
string='Send Invitation',
|
||||
compute='_compute_send_invitation',
|
||||
store=True,
|
||||
readonly=False,
|
||||
default=True,
|
||||
)
|
||||
|
||||
@api.depends('partner_id', 'access_mode')
|
||||
def _compute_send_invitation(self):
|
||||
project = self.parent_wizard_id.resource_ref
|
||||
for collaborator in self:
|
||||
if (
|
||||
collaborator.partner_id not in project.message_partner_ids
|
||||
or (collaborator.access_mode != 'read' and collaborator.partner_id not in project.collaborator_ids.partner_id)
|
||||
):
|
||||
collaborator.send_invitation = True
|
||||
|
|
@ -1,22 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
import operator
|
||||
|
||||
from odoo import Command, api, fields, models, _
|
||||
|
||||
|
||||
class ProjectShareWizard(models.TransientModel):
|
||||
_name = 'project.share.wizard'
|
||||
_inherit = 'portal.share'
|
||||
_inherit = ['portal.share']
|
||||
_description = 'Project Sharing'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super().default_get(fields)
|
||||
if not result.get('access_mode'):
|
||||
result.update(
|
||||
access_mode='read',
|
||||
display_access_mode=True,
|
||||
)
|
||||
# The project share action could be called in `project.collaborator`
|
||||
# and so we have to check the active_model and active_id to use
|
||||
# the right project.
|
||||
active_model = self.env.context.get('active_model', '')
|
||||
active_id = self.env.context.get('active_id', False)
|
||||
if active_model == 'project.collaborator':
|
||||
active_model = 'project.project'
|
||||
active_id = self.env.context.get('default_project_id', False)
|
||||
result = super(ProjectShareWizard, self.with_context(active_model=active_model, active_id=active_id)).default_get(fields)
|
||||
if result['res_model'] and result['res_id']:
|
||||
project = self.env[result['res_model']].browse(result['res_id'])
|
||||
collaborator_vals_list = []
|
||||
collaborator_ids = []
|
||||
for collaborator in project.collaborator_ids:
|
||||
collaborator_ids.append(collaborator.partner_id.id)
|
||||
collaborator_vals_list.append({
|
||||
'partner_id': collaborator.partner_id.id,
|
||||
'partner_name': collaborator.partner_id.display_name,
|
||||
'access_mode': 'edit_limited' if collaborator.limited_access else 'edit',
|
||||
})
|
||||
for follower in project.message_partner_ids:
|
||||
if follower.partner_share and follower.id not in collaborator_ids:
|
||||
collaborator_vals_list.append({
|
||||
'partner_id': follower.id,
|
||||
'partner_name': follower.display_name,
|
||||
'access_mode': 'read',
|
||||
})
|
||||
if collaborator_vals_list:
|
||||
collaborator_vals_list.sort(key=operator.itemgetter('partner_name'))
|
||||
result['collaborator_ids'] = [
|
||||
Command.create({'partner_id': collaborator['partner_id'], 'access_mode': collaborator['access_mode'], 'send_invitation': False})
|
||||
for collaborator in collaborator_vals_list
|
||||
]
|
||||
return result
|
||||
|
||||
@api.model
|
||||
|
|
@ -24,8 +52,9 @@ class ProjectShareWizard(models.TransientModel):
|
|||
project_model = self.env['ir.model']._get('project.project')
|
||||
return [(project_model.model, project_model.name)]
|
||||
|
||||
access_mode = fields.Selection([('read', 'Readonly'), ('edit', 'Edit')])
|
||||
display_access_mode = fields.Boolean()
|
||||
share_link = fields.Char("Public Link", help="Anyone with this link can access the project in read mode.")
|
||||
collaborator_ids = fields.One2many('project.share.collaborator.wizard', 'parent_wizard_id', string='Collaborators')
|
||||
existing_partner_ids = fields.Many2many('res.partner', compute='_compute_existing_partner_ids', export_string_translation=False)
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_resource_ref(self):
|
||||
|
|
@ -35,14 +64,121 @@ class ProjectShareWizard(models.TransientModel):
|
|||
else:
|
||||
wizard.resource_ref = None
|
||||
|
||||
def action_send_mail(self):
|
||||
@api.depends('collaborator_ids')
|
||||
def _compute_existing_partner_ids(self):
|
||||
for wizard in self:
|
||||
wizard.existing_partner_ids = wizard.collaborator_ids.partner_id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
wizards = super().create(vals_list)
|
||||
for wizard in wizards:
|
||||
collaborator_ids_to_add = []
|
||||
collaborator_ids_to_add_with_limited_access = []
|
||||
collaborator_ids_vals_list = []
|
||||
project = wizard.resource_ref
|
||||
project_collaborator_ids_to_remove = [
|
||||
c.id
|
||||
for c in project.collaborator_ids
|
||||
if c.partner_id not in wizard.collaborator_ids.partner_id
|
||||
]
|
||||
project_followers = project.message_partner_ids
|
||||
project_followers_to_add = []
|
||||
project_followers_to_remove = [
|
||||
partner.id
|
||||
for partner in project_followers
|
||||
if partner not in wizard.collaborator_ids.partner_id and partner.partner_share
|
||||
]
|
||||
project_collaborator_per_partner_id = {c.partner_id.id: c for c in project.collaborator_ids}
|
||||
for collaborator in wizard.collaborator_ids:
|
||||
partner_id = collaborator.partner_id.id
|
||||
project_collaborator = project_collaborator_per_partner_id.get(partner_id, self.env['project.collaborator'])
|
||||
if collaborator.access_mode in ("edit", "edit_limited"):
|
||||
limited_access = collaborator.access_mode == "edit_limited"
|
||||
if not project_collaborator:
|
||||
if limited_access:
|
||||
collaborator_ids_to_add_with_limited_access.append(partner_id)
|
||||
else:
|
||||
collaborator_ids_to_add.append(partner_id)
|
||||
elif project_collaborator.limited_access != limited_access:
|
||||
collaborator_ids_vals_list.append(
|
||||
Command.update(
|
||||
project_collaborator.id,
|
||||
{'limited_access': limited_access},
|
||||
)
|
||||
)
|
||||
elif project_collaborator:
|
||||
project_collaborator_ids_to_remove.append(project_collaborator.id)
|
||||
if partner_id not in project_followers.ids:
|
||||
project_followers_to_add.append(partner_id)
|
||||
if collaborator_ids_to_add:
|
||||
partners = project._get_new_collaborators(self.env['res.partner'].browse(collaborator_ids_to_add))
|
||||
collaborator_ids_vals_list.extend(Command.create({'partner_id': partner_id}) for partner_id in partners.ids)
|
||||
project.tasks.message_subscribe(partner_ids=partners.ids)
|
||||
if collaborator_ids_to_add_with_limited_access:
|
||||
partners = project._get_new_collaborators(self.env['res.partner'].browse(collaborator_ids_to_add_with_limited_access))
|
||||
collaborator_ids_vals_list.extend(
|
||||
Command.create({'partner_id': partner_id, 'limited_access': True}) for partner_id in partners.ids
|
||||
)
|
||||
if project_collaborator_ids_to_remove:
|
||||
collaborator_ids_vals_list.extend(Command.delete(collaborator_id) for collaborator_id in project_collaborator_ids_to_remove)
|
||||
project_vals = {}
|
||||
if collaborator_ids_vals_list:
|
||||
project_vals['collaborator_ids'] = collaborator_ids_vals_list
|
||||
if project_vals:
|
||||
project.write(project_vals)
|
||||
if project_followers_to_add:
|
||||
project._add_followers(self.env['res.partner'].browse(project_followers_to_add))
|
||||
if project_followers_to_remove:
|
||||
project.message_unsubscribe(project_followers_to_remove)
|
||||
return wizards
|
||||
|
||||
def action_share_record(self):
|
||||
# Confirmation dialog is only opened if new portal user(s) need to be created in a 'on invitation' website
|
||||
self.ensure_one()
|
||||
if self.access_mode == 'edit':
|
||||
portal_partners = self.partner_ids.filtered('user_ids')
|
||||
note = self._get_note()
|
||||
self.resource_ref._add_collaborators(self.partner_ids)
|
||||
self._send_public_link(note, portal_partners)
|
||||
self._send_signup_link(note, partners=self.partner_ids - portal_partners)
|
||||
self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
return super().action_send_mail()
|
||||
if not self.collaborator_ids:
|
||||
return
|
||||
on_invite = self.env['res.users']._get_signup_invitation_scope() == 'b2b'
|
||||
new_portal_user = self.collaborator_ids.filtered(lambda c: c.send_invitation and not c.partner_id.user_ids) and on_invite
|
||||
if not new_portal_user:
|
||||
return self.action_send_mail()
|
||||
return {
|
||||
'name': _('Confirmation'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'views': [(self.env.ref('project.project_share_wizard_confirm_form').id, 'form')],
|
||||
'res_model': 'project.share.wizard',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
'context': self.env.context,
|
||||
}
|
||||
|
||||
def action_send_mail(self):
|
||||
self.env['project.project'].browse(self.res_id).privacy_visibility = 'portal'
|
||||
result = {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _("Project shared with your collaborators."),
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
}}
|
||||
partner_ids_in_readonly_mode = []
|
||||
partner_ids_in_edit_mode = []
|
||||
for collaborator in self.collaborator_ids:
|
||||
if not collaborator.send_invitation:
|
||||
continue
|
||||
if collaborator.access_mode == 'read':
|
||||
partner_ids_in_readonly_mode.append(collaborator.partner_id.id)
|
||||
else:
|
||||
partner_ids_in_edit_mode.append(collaborator.partner_id.id)
|
||||
if partner_ids_in_edit_mode:
|
||||
new_collaborators = self.env['res.partner'].browse(partner_ids_in_edit_mode)
|
||||
portal_partners = new_collaborators.filtered('user_ids')
|
||||
# send mail to users
|
||||
self._send_public_link(portal_partners)
|
||||
self._send_signup_link(partners=new_collaborators.with_context({'signup_valid': True}) - portal_partners)
|
||||
if partner_ids_in_readonly_mode:
|
||||
self.partner_ids = self.env['res.partner'].browse(partner_ids_in_readonly_mode)
|
||||
super().action_send_mail()
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -8,27 +8,46 @@
|
|||
<form string="Share Project">
|
||||
<field name="res_model" invisible="1"/>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="display_access_mode" invisible="1" />
|
||||
<p class="alert alert-warning" attrs="{'invisible': [('access_warning', '=', '')]}" role="alert"><field name="access_warning"/></p>
|
||||
<group attrs="{'invisible': [('display_access_mode', '=', False)]}">
|
||||
<field class="flex-row" name="access_mode" widget="radio"/>
|
||||
</group>
|
||||
<group name="share_link" attrs="{'invisible': [('access_mode', '=', 'edit')]}">
|
||||
<field name="share_link" widget="CopyClipboardChar" options="{'string': 'Copy Link'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<div class="o_td_label">
|
||||
<label for="partner_ids" string="Invite People" attrs="{'invisible': [('access_mode', '=', 'read')]}"/>
|
||||
<label for="partner_ids" attrs="{'invisible': [('access_mode', '=', 'edit')]}"/>
|
||||
</div>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to share the project..." nolabel="1" context="{'show_email': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="note" placeholder="Add a note" nolabel="1" colspan="2"/>
|
||||
<field name="share_link" widget="CopyClipboardChar"/>
|
||||
</group>
|
||||
<field name="collaborator_ids" nolabel="1">
|
||||
<list string="Collaborators" editable="bottom">
|
||||
<field name="partner_id"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
domain="[('id', 'not in', parent.existing_partner_ids), ('partner_share', '=', True)]"
|
||||
context="{'show_email': True}"
|
||||
/>
|
||||
<field name="access_mode"/>
|
||||
<field name="send_invitation"/>
|
||||
</list>
|
||||
</field>
|
||||
<p class="text-muted">Choose one of the following access modes for your collaborators:</p>
|
||||
<ul class="text-muted">
|
||||
<li>Read: collaborators can view tasks but cannot edit them.</li>
|
||||
<li>Edit with limited access: collaborators can view and edit tasks they follow in the Kanban view.</li>
|
||||
<li>Edit: collaborators can view and edit all tasks in the Kanban view. Additionally, they can choose which tasks they want to follow.</li>
|
||||
</ul>
|
||||
<footer>
|
||||
<button string="Send" name="action_send_mail" attrs="{'invisible': [('access_warning', '!=', '')]}" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" />
|
||||
<button string="Share Project" name="action_share_record" type="object" class="btn-primary" data-hotkey="q" invisible="not collaborator_ids" />
|
||||
<button string="Save" class="btn-primary" special="save" data-hotkey="q" invisible="collaborator_ids" />
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_share_wizard_confirm_form" model="ir.ui.view">
|
||||
<field name="name">project.share.wizard.view.form</field>
|
||||
<field name="model">project.share.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Confirmation">
|
||||
<p>People invited to collaborate on the project will have portal access rights.</p>
|
||||
<p>They can edit shared project tasks and view specific documents in read mode on your website. This includes leads/opportunities, quotations/sales orders, purchase orders, invoices and bills, timesheets, and tickets.</p>
|
||||
<p>You have full control and can revoke portal access anytime. Are you ready to proceed?</p>
|
||||
<footer>
|
||||
<button string="Grant Portal Access" name="action_send_mail" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
|
@ -37,7 +56,6 @@
|
|||
<record id="project_share_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Share Project</field>
|
||||
<field name="res_model">project.share.wizard</field>
|
||||
<field name="binding_model_id" ref="model_project_project"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class TaskShareWizard(models.TransientModel):
|
||||
_name = 'task.share.wizard'
|
||||
_inherit = ['portal.share']
|
||||
_description = 'Task Sharing'
|
||||
|
||||
task_id = fields.Many2one('project.task', default=lambda self: self.res_id)
|
||||
project_privacy_visibility = fields.Selection(related='task_id.project_privacy_visibility')
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="portal_task_share_wizard" model="ir.ui.view">
|
||||
<field name="name">task.share.wizard</field>
|
||||
<field name="model">task.share.wizard</field>
|
||||
<field name="inherit_id" ref="portal.portal_share_wizard"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[hasclass('btn-default')]" position="replace">
|
||||
<button string="Discard" class="btn-default" special="cancel" data-hotkey="x" />
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('btn-primary')]" position="attributes">
|
||||
<attribute name="invisible">project_privacy_visibility in ['invited_users', 'followers']</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='note']" position="attributes">
|
||||
<attribute name="invisible">project_privacy_visibility in ['invited_users', 'followers']</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_ids']" position="attributes">
|
||||
<attribute name="invisible">project_privacy_visibility in ['invited_users', 'followers']</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -5,14 +5,14 @@ from odoo import api, fields, models, _
|
|||
from ast import literal_eval
|
||||
|
||||
|
||||
class ProjectTaskTypeDelete(models.TransientModel):
|
||||
class ProjectTaskTypeDeleteWizard(models.TransientModel):
|
||||
_name = 'project.task.type.delete.wizard'
|
||||
_description = 'Project Stage Delete Wizard'
|
||||
_description = 'Project Task Stage Delete Wizard'
|
||||
|
||||
project_ids = fields.Many2many('project.project', domain="['|', ('active', '=', False), ('active', '=', True)]", string='Projects', ondelete='cascade')
|
||||
stage_ids = fields.Many2many('project.task.type', string='Stages To Delete', ondelete='cascade')
|
||||
tasks_count = fields.Integer('Number of Tasks', compute='_compute_tasks_count')
|
||||
stages_active = fields.Boolean(compute='_compute_stages_active')
|
||||
project_ids = fields.Many2many('project.project', domain="['|', ('active', '=', False), ('active', '=', True)]", string='Projects', ondelete='cascade', export_string_translation=False)
|
||||
stage_ids = fields.Many2many('project.task.type', string='Stages To Delete', ondelete='cascade', export_string_translation=False)
|
||||
tasks_count = fields.Integer('Number of Tasks', compute='_compute_tasks_count', export_string_translation=False)
|
||||
stages_active = fields.Boolean(compute='_compute_stages_active', export_string_translation=False)
|
||||
|
||||
@api.depends('project_ids')
|
||||
def _compute_tasks_count(self):
|
||||
|
|
@ -59,7 +59,7 @@ class ProjectTaskTypeDelete(models.TransientModel):
|
|||
|
||||
if project_id:
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task")
|
||||
action['domain'] = [('display_project_id', '=', project_id)]
|
||||
action['domain'] = [('project_id', '=', project_id)]
|
||||
action['context'] = str({
|
||||
'pivot_row_groupby': ['user_ids'],
|
||||
'default_project_id': project_id,
|
||||
|
|
@ -67,7 +67,7 @@ class ProjectTaskTypeDelete(models.TransientModel):
|
|||
elif self.env.context.get('stage_view'):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("project.open_task_type_form")
|
||||
else:
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_all_task")
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_my_task")
|
||||
|
||||
context = action.get('context', '{}')
|
||||
context = context.replace('uid', str(self.env.uid))
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@
|
|||
<form string="Delete Stage">
|
||||
<field name="tasks_count" invisible="1" />
|
||||
<field name="stages_active" invisible="1" />
|
||||
<div attrs="{'invisible': [('tasks_count', '>', 0)]}">
|
||||
<p>Are you sure you want to delete those stages ?</p>
|
||||
<div invisible="tasks_count > 0">
|
||||
<p>Are you sure you want to delete these stages?</p>
|
||||
</div>
|
||||
<div attrs="{'invisible': ['|', ('stages_active', '=', False), ('tasks_count', '=', 0)]}">
|
||||
<div invisible="not stages_active or tasks_count == 0">
|
||||
<p>You cannot delete stages containing tasks. You can either archive them or first delete all of their tasks.</p>
|
||||
</div>
|
||||
<div attrs="{'invisible': ['|', ('stages_active', '=', True), ('tasks_count', '=', 0)]}">
|
||||
<div invisible="stages_active or tasks_count == 0">
|
||||
<p>You cannot delete stages containing tasks. You should first delete all of their tasks.</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Archive Stages" type="object" name="action_archive" class="btn btn-primary" attrs="{'invisible': ['|', ('stages_active', '=', False), ('tasks_count', '=', 0)]}" data-hotkey="q"/>
|
||||
<button string="Delete" type="object" name="action_unlink" class="btn btn-primary" attrs="{'invisible': [('tasks_count', '>', 0)]}" data-hotkey="w"/>
|
||||
<button string="Discard" special="cancel" data-hotkey="z" />
|
||||
<button string="Archive Stages" type="object" name="action_archive" class="btn btn-primary" invisible="not stages_active or tasks_count == 0" data-hotkey="q"/>
|
||||
<button string="Delete" type="object" name="action_unlink" class="btn btn-primary" invisible="tasks_count > 0" data-hotkey="w"/>
|
||||
<button string="Discard" special="cancel" data-hotkey="x" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
|
@ -33,15 +33,15 @@
|
|||
<div>
|
||||
<p>This will archive the stages and all the tasks they contain from the following projects:</p>
|
||||
<field name="project_ids" readonly="1">
|
||||
<tree>
|
||||
<list>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
<p>Are you sure you want to continue?</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Confirm" type="object" name="action_confirm" class="btn btn-primary" data-hotkey="q"/>
|
||||
<button string="Discard" special="cancel" data-hotkey="z" />
|
||||
<button string="Discard" special="cancel" data-hotkey="x" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
from odoo import Command, api, fields, models
|
||||
|
||||
|
||||
class ProjectTemplateCreateWizard(models.TransientModel):
|
||||
_name = 'project.template.create.wizard'
|
||||
_description = 'Project Template create Wizard'
|
||||
|
||||
def _default_role_to_users_ids(self):
|
||||
res = []
|
||||
template = self.env['project.project'].browse(self.env.context.get('template_id'))
|
||||
if template:
|
||||
res = [Command.create({'role_id': role.id}) for role in template.task_ids.role_ids]
|
||||
return res
|
||||
|
||||
name = fields.Char(string="Name", required=True)
|
||||
date_start = fields.Date(string="Start Date")
|
||||
date = fields.Date(string='Expiration Date')
|
||||
alias_name = fields.Char(string="Alias Name")
|
||||
alias_domain_id = fields.Many2one("mail.alias.domain", string="Alias Domain")
|
||||
template_id = fields.Many2one("project.project", default=lambda self: self.env.context.get('template_id'))
|
||||
template_has_dates = fields.Boolean(compute="_compute_template_has_dates")
|
||||
role_to_users_ids = fields.One2many('project.template.role.to.users.map', 'wizard_id', default=_default_role_to_users_ids)
|
||||
|
||||
@api.depends("template_id")
|
||||
def _compute_template_has_dates(self):
|
||||
for wizard in self:
|
||||
wizard.template_has_dates = wizard.template_id.date_start and wizard.template_id.date
|
||||
|
||||
def _get_template_whitelist_fields(self):
|
||||
"""
|
||||
Whitelist of fields of this wizard that will be used when creating a project from a template.
|
||||
"""
|
||||
return ["name", "date_start", "date", "alias_name", "alias_domain_id"]
|
||||
|
||||
def _create_project_from_template(self):
|
||||
# Dictionary with all whitelist fields and their values
|
||||
field_values = self._convert_to_write(
|
||||
{
|
||||
fname: self[fname]
|
||||
for fname in self._fields.keys() & self._get_template_whitelist_fields()
|
||||
}
|
||||
)
|
||||
return self.template_id.action_create_from_template(values=field_values, role_to_users_mapping=self.role_to_users_ids)
|
||||
|
||||
def create_project_from_template(self):
|
||||
# Opening project task views after creation of project from template
|
||||
return self._create_project_from_template().action_view_tasks()
|
||||
|
||||
@api.model
|
||||
def action_open_template_view(self):
|
||||
view = self.env.ref('project.project_project_view_form_simplified_template', raise_if_not_found=False)
|
||||
if not view:
|
||||
return {}
|
||||
return {
|
||||
'name': self.env._('Create a Project from Template %s', self.env.context.get('template_name')),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'views': [(view.id, 'form')],
|
||||
'res_model': 'project.template.create.wizard',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
key: value
|
||||
for key, value in self.env.context.items()
|
||||
if not key.startswith('default_')
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ProjectTemplateRoleToUsersMap(models.TransientModel):
|
||||
_name = 'project.template.role.to.users.map'
|
||||
_description = 'Project role to users mapping'
|
||||
|
||||
wizard_id = fields.Many2one('project.template.create.wizard', export_string_translation=False)
|
||||
role_id = fields.Many2one('project.role', string='Project Role', required=True)
|
||||
user_ids = fields.Many2many('res.users', string='Assignees', domain=[('share', '=', False), ('active', '=', True)])
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_project_view_form_simplified_template" model="ir.ui.view">
|
||||
<field name="name">project.project.create.wizard.form</field>
|
||||
<field name="model">project.template.create.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project">
|
||||
<div class="oe_title mb-2">
|
||||
<label for="name" string="Name"/>
|
||||
<h1>
|
||||
<field name="name" class="o_project_name" placeholder="e.g. Office Party"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date" required="not template_has_dates and date_start" invisible="1"/>
|
||||
<field name="date_start" required="not template_has_dates and date" string="Planned Date" widget="daterange" options='{"end_date_field": "date", "always_range": "1"}'/>
|
||||
<label for="alias_name" string="Create tasks by sending an email to" class="pe-2"/>
|
||||
<div class="d-inline-flex">
|
||||
<field name="alias_name" placeholder="e.g. office-party"/>@ <field name="alias_domain_id" placeholder="e.g. mycompany.com" options="{'no_create': True, 'no_open': True}"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<field name="role_to_users_ids" invisible="not role_to_users_ids">
|
||||
<list create="0" delete="0" no_open="1" editable="bottom">
|
||||
<field name="role_id" force_save="1" readonly="1" options="{'no_open': True}"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
</list>
|
||||
</field>
|
||||
<footer>
|
||||
<button string="Create project" name="create_project_from_template" type="object" class="btn-primary o_open_tasks" data-hotkey="q"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue