Files
odoo_source/addons/project/models/project.py
T
Antony Lesuisse ccf606e026 [ADD] website_project: Portal access to Projects
Split project to move portal features to a new module named website_project.

Portal users may now access their projects and tasks throught the
website_portal My account page. The Backend portal menuitem is removed.

Simplify the privacy settings of project, to be visible to portal users the
project must be in 'portal' mode.

Original authors: Vipul Bhatt, Florian Wintjens
2016-07-06 01:17:43 +02:00

825 lines
37 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from openerp.tools.safe_eval import safe_eval as eval
from openerp.exceptions import UserError, ValidationError
class ProjectTaskType(models.Model):
_name = 'project.task.type'
_description = 'Task Stage'
_order = 'sequence'
def _get_mail_template_id_domain(self):
return [('model', '=', 'project.task')]
def _get_default_project_ids(self):
default_project_id = self.env.context.get('default_project_id')
return [default_project_id] if default_project_id else None
name = fields.Char(string='Stage Name', required=True, translate=True)
description = fields.Text(translate=True)
sequence = fields.Integer(default=1)
project_ids = fields.Many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', string='Projects',
default=_get_default_project_ids)
legend_priority = fields.Char(
string='Priority Management Explanation', translate=True,
help='Explanation text to help users using the star and priority mechanism on stages or issues that are in this stage.')
legend_blocked = fields.Char(
string='Kanban Blocked Explanation', translate=True,
help='Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage.')
legend_done = fields.Char(
string='Kanban Valid Explanation', translate=True,
help='Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage.')
legend_normal = fields.Char(
string='Kanban Ongoing Explanation', translate=True,
help='Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage.')
mail_template_id = fields.Many2one(
'mail.template',
string='Email Template',
domain=lambda self: self._get_mail_template_id_domain(),
help="If set an email will be sent to the customer when the task or issue reaches this step.")
fold = fields.Boolean(string='Folded in Tasks Pipeline',
help='This stage is folded in the kanban view when there are no records in that stage to display.')
class Project(models.Model):
_name = "project.project"
_description = "Project"
_inherit = ['mail.alias.mixin', 'mail.thread', 'ir.needaction_mixin']
_inherits = {'account.analytic.account': "analytic_account_id"}
_order = "sequence, name, id"
_period_number = 5
def get_alias_model_name(self, vals):
return vals.get('alias_model', 'project.task')
def get_alias_values(self):
values = super(Project, self).get_alias_values()
values['alias_defaults'] = {'project_id': self.id}
return values
@api.multi
def unlink(self):
analytic_accounts_to_delete = self.env['account.analytic.account']
for project in self:
if project.tasks:
raise UserError(_('You cannot delete a project containing tasks. You can either delete all the project\'s tasks and then delete the project or simply deactivate the project.'))
if project.analytic_account_id and not project.analytic_account_id.line_ids:
analytic_accounts_to_delete |= project.analytic_account_id
res = super(Project, self).unlink()
analytic_accounts_to_delete.unlink()
return res
def _compute_attached_docs_count(self):
Attachment = self.env['ir.attachment']
for project in self:
project.doc_count = Attachment.search_count([
'|',
'&',
('res_model', '=', 'project.project'), ('res_id', '=', project.id),
'&',
('res_model', '=', 'project.task'), ('res_id', 'in', project.task_ids.ids)
])
def _compute_task_count(self):
for project in self:
project.task_count = len(project.task_ids)
def _compute_task_needaction_count(self):
projects_data = self.env['project.task'].read_group([
('project_id', 'in', self.ids),
('message_needaction', '=', True)
], ['project_id'], ['project_id'])
mapped_data = {project_data['project_id'][0]: int(project_data['project_id_count'])
for project_data in projects_data}
for project in self:
project.task_needaction_count = mapped_data.get(project.id, 0)
@api.model
def _get_alias_models(self):
""" Overriden in project_issue to offer more options """
return [('project.task', "Tasks")]
@api.multi
def attachment_tree_view(self):
self.ensure_one()
domain = [
'|',
'&', ('res_model', '=', 'project.project'), ('res_id', 'in', self.ids),
'&', ('res_model', '=', 'project.task'), ('res_id', 'in', self.task_ids.ids)]
return {
'name': _('Attachments'),
'domain': domain,
'res_model': 'ir.attachment',
'type': 'ir.actions.act_window',
'view_id': False,
'view_mode': 'kanban,tree,form',
'view_type': 'form',
'help': _('''<p class="oe_view_nocontent_create">
Documents are attached to the tasks and issues of your project.</p><p>
Send messages or log internal notes with attachments to link
documents to your project.
</p>'''),
'limit': 80,
'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id)
}
@api.model
def activate_sample_project(self):
""" Unarchives the sample project 'project.project_project_data' and
reloads the project dashboard """
# Unarchive sample project
project = self.env.ref('project.project_project_data', False)
if project:
project.write({'active': True})
# Change the help message on the action (no more activate project)
action = self.env.ref('project.open_view_project_all', False)
action_data = None
if action:
action.sudo().write({
"help": _('''<p class="oe_view_nocontent_create">Click to create a new project.</p>''')
})
action_data = action.read()[0]
# Reload the dashboard
return action_data
def _compute_is_favorite(self):
for project in self:
project.is_favorite = self.env.user in project.favorite_user_ids
def _get_default_favorite_user_ids(self):
return [(6, 0, [self.env.uid])]
@api.model
def default_get(self, flds):
result = super(Project, self).default_get(flds)
result['use_tasks'] = True
return result
# Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
_alias_models = lambda self: self._get_alias_models()
active = fields.Boolean(default=True,
help="If the active field is set to False, it will allow you to hide the project without removing it.")
sequence = fields.Integer(default=10, help="Gives the sequence order when displaying a list of Projects.")
analytic_account_id = fields.Many2one(
'account.analytic.account', string='Contract/Analytic',
help="Link this project to an analytic account if you need financial management on projects. "
"It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.",
ondelete="cascade", required=True, auto_join=True)
favorite_user_ids = fields.Many2many(
'res.users', 'project_favorite_user_rel', 'project_id', 'user_id',
default=_get_default_favorite_user_ids,
string='Members')
is_favorite = fields.Boolean(compute='_compute_is_favorite', string='Show Project on dashboard',
help="Whether this project should be displayed on the dashboard or not")
label_tasks = fields.Char(string='Use Tasks as', default='Tasks', help="Gives label to tasks on project's kanban view.")
tasks = fields.One2many('project.task', 'project_id', string="Task Activities")
resource_calendar_id = fields.Many2one('resource.calendar', string='Working Time',
help="Timetable working hours to adjust the gantt diagram report")
type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages')
task_count = fields.Integer(compute='_compute_task_count', string="Tasks")
task_needaction_count = fields.Integer(compute='_compute_task_needaction_count', string="Tasks")
task_ids = fields.One2many('project.task', 'project_id', string='Tasks',
domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
color = fields.Integer(string='Color Index')
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user)
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True,
help="Internal email associated with this project. Incoming emails are automatically synchronized "
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
alias_model = fields.Selection(_alias_models, string="Alias Model", index=True, required=True, default='project.task',
help="The kind of document created when an email is received on this project's email alias")
privacy_visibility = fields.Selection([
('followers', _('On invitation only')),
('employees', _('Visible by all employees')),
('portal', _('Visible by following customers')),
],
string='Privacy', required=True,
default='employees',
help="Holds visibility of the tasks or issues that belong to the current project:\n"
"- On invitation only: Employees may only see the followed project, tasks or issues\n"
"- Visible by all employees: Employees may see all project, tasks or issues\n"
"- Visible by following customers: employees see everything;\n"
" if website is activated, portal users may see project, tasks or issues followed by\n"
" them or by someone of their company\n")
doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
date_start = fields.Date(string='Start Date')
date = fields.Date(string='Expiration Date', index=True, track_visibility='onchange')
_sql_constraints = [
('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.')
]
@api.multi
def map_tasks(self, new_project_id):
""" copy and map tasks from old to new project """
tasks = self.env['project.task']
for task in self.tasks:
# preserve task name and stage, normally altered during copy
defaults = {'stage_id': task.stage_id.id,
'name': task.name}
tasks += task.copy(defaults)
return self.browse(new_project_id).write({'tasks': [(6, 0, tasks.ids)]})
@api.multi
def copy(self, default=None):
if default is None:
default = {}
self = self.with_context(active_test=False)
if not default.get('name'):
default['name'] = _("%s (copy)") % (self.name)
project = super(Project, self).copy(default)
for follower in self.message_follower_ids:
project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids)
self.map_tasks(project.id)
return project
@api.model
def create(self, vals):
ir_values = self.env['ir.values'].get_default('project.config.settings', 'generate_project_alias')
if ir_values:
vals['alias_name'] = vals.get('alias_name') or vals.get('name')
# Prevent double project creation when 'use_tasks' is checked
self = self.with_context(project_creation_in_progress=True, mail_create_nosubscribe=True)
return super(Project, self).create(vals)
@api.multi
def write(self, vals):
# if alias_model has been changed, update alias_model_id accordingly
if vals.get('alias_model'):
vals['alias_model_id'] = self.env['ir.model'].search([
('model', '=', vals.get('alias_model', 'project.task'))
], limit=1).id
res = super(Project, self).write(vals)
if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too
self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
return res
@api.multi
def toggle_favorite(self):
favorite_projects = not_fav_projects = self.env['project.project'].sudo()
for project in self:
if self.env.user in project.favorite_user_ids:
favorite_projects |= project
else:
not_fav_projects |= project
# Project User has no write access for project.
not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]})
favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]})
@api.multi
def close_dialog(self):
return {'type': 'ir.actions.act_window_close'}
class Task(models.Model):
_name = "project.task"
_description = "Task"
_date_name = "date_start"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_mail_post_access = 'read'
_order = "priority desc, sequence, date_start, name, id"
def _get_default_partner(self):
if 'default_project_id' in self.env.context:
default_project_id = self.env['project.project'].browse(self.env.context['default_project_id'])
return default_project_id.exists().partner_id
def _get_default_stage_id(self):
""" Gives default stage_id """
return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', False)])
@api.multi
def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
TaskType = self.env['project.task.type']
order = TaskType._order
access_rights_uid = access_rights_uid or self.env.uid
if read_group_order == 'stage_id desc':
order = '%s desc' % order
if 'default_project_id' in self.env.context:
search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id']), ('id', 'in', self.ids)]
else:
search_domain = [('id', 'in', self.ids)]
stage_ids = TaskType._search(search_domain, order=order, access_rights_uid=access_rights_uid)
stages = TaskType.sudo(access_rights_uid).browse(stage_ids)
result = stages.name_get()
# restore order of the search
result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
return result, {stage.id: stage.fold for stage in stages}
_group_by_full = {
'stage_id': _read_group_stage_ids,
}
active = fields.Boolean(default=True)
name = fields.Char(string='Task Title', track_visibility='onchange', required=True, index=True)
description = fields.Html(string='Description')
priority = fields.Selection([
('0','Normal'),
('1','High')
], default='0', index=True)
sequence = fields.Integer(string='Sequence', index=True, default=10,
help="Gives the sequence order when displaying a list of tasks.")
stage_id = fields.Many2one('project.task.type', string='Stage', track_visibility='onchange', index=True,
default=_get_default_stage_id,
domain="[('project_ids', '=', project_id)]", copy=False)
tag_ids = fields.Many2many('project.tags', string='Tags', oldname='categ_ids')
kanban_state = fields.Selection([
('normal', 'In Progress'),
('done', 'Ready for next stage'),
('blocked', 'Blocked')
], string='Kanban State',
default='normal',
track_visibility='onchange',
required=True, copy=False,
help="A task's kanban state indicates special situations affecting it:\n"
" * Normal is the default situation\n"
" * Blocked indicates something is preventing the progress of this task\n"
" * Ready for next stage indicates the task is ready to be pulled to the next stage")
create_date = fields.Datetime(index=True)
write_date = fields.Datetime(index=True) #not displayed in the view but it might be useful with base_action_rule module (and it needs to be defined first for that)
date_start = fields.Datetime(string='Starting Date',
default=fields.Datetime.now,
index=True, copy=False)
date_end = fields.Datetime(string='Ending Date', index=True, copy=False)
date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True)
date_deadline = fields.Date(string='Deadline', index=True, copy=False)
date_last_stage_update = fields.Datetime(string='Last Stage Update',
default=fields.Datetime.now,
index=True,
copy=False,
readonly=True)
project_id = fields.Many2one('project.project',
string='Project',
default=lambda self: self.env.context.get('default_project_id'),
index=True,
track_visibility='onchange',
change_default=True)
notes = fields.Text(string='Notes')
planned_hours = fields.Float(string='Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.')
remaining_hours = fields.Float(string='Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
user_id = fields.Many2one('res.users',
string='Assigned to',
default=lambda self: self.env.uid,
index=True, track_visibility='onchange')
partner_id = fields.Many2one('res.partner',
string='Customer',
default=_get_default_partner)
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id')
company_id = fields.Many2one('res.company',
string='Company',
default=lambda self: self.env['res.company']._company_default_get())
color = fields.Integer(string='Color Index')
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True)
attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=lambda self: [('res_model', '=', self._name)], auto_join=True, string='Attachments')
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Displayed Image')
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation')
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation')
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation')
@api.onchange('project_id')
def _onchange_project(self):
if self.project_id:
self.partner_id = self.project_id.partner_id
@api.onchange('user_id')
def _onchange_user(self):
if self.user_id:
self.date_start = fields.Datetime.now()
@api.multi
def copy(self, default=None):
if default is None:
default = {}
if not default.get('name'):
default['name'] = _("%s (copy)") % self.name
if 'remaining_hours' not in default:
default['remaining_hours'] = self.planned_hours
return super(Task, self).copy(default)
@api.constrains('date_start', 'date_end')
def _check_dates(self):
if any(self.filtered(lambda task: task.date_start and task.date_end and task.date_start > task.date_end)):
return ValidationError(_('Error ! Task starting date must be lower than its ending date.'))
# Override view according to the company definition
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
# read uom as admin to avoid access rights issues, e.g. for portal/share users,
# this should be safe (no context passed to avoid side-effects)
obj_tm = self.env.user.company_id.project_time_mode_id
tm = obj_tm and obj_tm.name or 'Hours'
res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
# read uom as admin to avoid access rights issues, e.g. for portal/share users,
# this should be safe (no context passed to avoid side-effects)
obj_tm = self.env.user.company_id.project_time_mode_id
# using get_object to get translation value
uom_hour = self.env.ref('product.product_uom_hour', False)
if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id:
return res
eview = etree.fromstring(res['arch'])
# if the project_time_mode_id is not in hours (so in days), display it as a float field
def _check_rec(eview):
if eview.attrib.get('widget', '') == 'float_time':
eview.set('widget', 'float')
for child in eview:
_check_rec(child)
return True
_check_rec(eview)
res['arch'] = etree.tostring(eview)
# replace reference of 'Hours' to 'Day(s)'
for f in res['fields']:
# TODO this NOT work in different language than english
# the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days'
# but string 'Initially Planned Days' is not available in translation
if 'Hours' in res['fields'][f]['string']:
res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name)
return res
@api.model
def get_empty_list_help(self, help):
self = self.with_context(
empty_list_help_id=self.env.context.get('default_project_id'),
empty_list_help_model='project.project',
empty_list_help_document_name=_("tasks")
)
return super(Task, self).get_empty_list_help(help)
# ----------------------------------------
# Case management
# ----------------------------------------
def stage_find(self, section_id, domain=[], order='sequence'):
""" Override of the base.stage method
Parameter of the stage search taken from the lead:
- section_id: if set, stages must belong to this section or
be a default stage; if not set, stages must be default
stages
"""
# collect all section_ids
section_ids = []
if section_id:
section_ids.append(section_id)
section_ids.extend(self.mapped('project_id').ids)
search_domain = []
if section_ids:
search_domain = [('|')] * (len(section_ids) - 1)
for section_id in section_ids:
search_domain.append(('project_ids', '=', section_id))
search_domain += list(domain)
# perform search, return the first found
return self.env['project.task.type'].search(search_domain, order=order, limit=1).id
def _store_history(self):
for task in self:
self.env['project.task.history'].create({
'task_id': task.id,
'remaining_hours': task.remaining_hours,
'planned_hours': task.planned_hours,
'kanban_state': task.kanban_state,
'type_id': task.stage_id.id,
'user_id': task.user_id.id
})
return True
# ------------------------------------------------
# CRUD overrides
# ------------------------------------------------
@api.model
def create(self, vals):
# context: no_log, because subtype already handle this
context = dict(self.env.context, mail_create_nolog=True)
# for default stage
if vals.get('project_id') and not context.get('default_project_id'):
context['default_project_id'] = vals.get('project_id')
# user_id change: update date_assign
if vals.get('user_id'):
vals['date_assign'] = fields.Datetime.now()
task = super(Task, self.with_context(context)).create(vals)
task._store_history()
return task
@api.multi
def write(self, vals):
now = fields.Datetime.now()
# stage change: update date_last_stage_update
if 'stage_id' in vals:
vals['date_last_stage_update'] = now
# reset kanban state when changing stage
if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
# user_id change: update date_assign
if vals.get('user_id'):
vals['date_assign'] = now
result = super(Task, self).write(vals)
if any(item in vals for item in ['stage_id', 'remaining_hours', 'user_id', 'kanban_state']):
self._store_history()
return result
# ---------------------------------------------------
# Mail gateway
# ---------------------------------------------------
@api.multi
def _track_template(self, tracking):
res = super(Task, self)._track_template(tracking)
test_task = self[0]
changes, tracking_value_ids = tracking[test_task.id]
if 'stage_id' in changes and test_task.stage_id.mail_template_id:
res['stage_id'] = (test_task.stage_id.mail_template_id, {'composition_mode': 'mass_mail'})
return res
@api.multi
def _track_subtype(self, init_values):
self.ensure_one()
if 'kanban_state' in init_values and self.kanban_state == 'blocked':
return 'project.mt_task_blocked'
elif 'kanban_state' in init_values and self.kanban_state == 'done':
return 'project.mt_task_ready'
elif 'user_id' in init_values and self.user_id: # assigned -> new
return 'project.mt_task_new'
elif 'stage_id' in init_values and self.stage_id and self.stage_id.sequence <= 1: # start stage -> new
return 'project.mt_task_new'
elif 'stage_id' in init_values:
return 'project.mt_task_stage'
return super(Task, self)._track_subtype(init_values)
@api.multi
def _notification_group_recipients(self, message, recipients, done_ids, group_data):
""" Override the mail.thread method to handle project users and officers
recipients. Indeed those will have specific action in their notification
emails: creating tasks, assigning it. """
group_project_user = self.env.ref('project.group_project_user')
for recipient in recipients.filtered(lambda recipient: recipient.id not in done_ids):
if recipient.user_ids and group_project_user in recipient.user_ids[0].groups_id:
group_data['group_project_user'] |= recipient
done_ids.add(recipient.id)
return super(Task, self)._notification_group_recipients(message, recipients, done_ids, group_data)
@api.multi
def _notification_get_recipient_groups(self, message, recipients):
self.ensure_one()
res = super(Task, self)._notification_get_recipient_groups(message, recipients)
take_action = self._notification_link_helper('assign')
new_action_id = self.env.ref('project.action_view_task').id
new_action = self._notification_link_helper('new', action_id=new_action_id)
actions = []
if not self.user_id:
actions.append({'url': take_action, 'title': _('I take it')})
else:
actions.append({'url': new_action, 'title': _('New Task')})
res['group_project_user'] = {
'actions': actions
}
return res
@api.model
def message_get_reply_to(self, res_ids, default=None):
""" Override to get the reply_to of the parent project. """
tasks = self.sudo().browse(res_ids)
project_ids = tasks.mapped('project_id').ids
aliases = self.env['project.project'].message_get_reply_to(project_ids, default=default)
return {task.id: aliases.get(task.project_id.id, False) for task in tasks}
@api.multi
def email_split(self, msg):
email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or ''))
# check left-part is not already an alias
aliases = self.mapped('project_id.alias_name')
return filter(lambda x: x.split('@')[0] not in aliases, email_list)
@api.model
def message_new(self, msg, custom_values=None):
""" Override to updates the document according to the email. """
if custom_values is None:
custom_values = {}
defaults = {
'name': msg.get('subject'),
'planned_hours': 0.0,
'partner_id': msg.get('author_id')
}
defaults.update(custom_values)
res = super(Task, self).message_new(msg, custom_values=defaults)
task = self.browse(res)
email_list = task.email_split(msg)
partner_ids = filter(None, task._find_partner_from_emails(email_list, force_create=False))
task.message_subscribe(partner_ids)
return res
@api.multi
def message_update(self, msg, update_vals=None):
""" Override to update the task according to the email. """
if update_vals is None:
update_vals = {}
maps = {
'cost': 'planned_hours',
}
for line in msg['body'].split('\n'):
line = line.strip()
res = tools.command_re.match(line)
if res:
match = res.group(1).lower()
field = maps.get(match)
if field:
try:
update_vals[field] = float(res.group(2).lower())
except (ValueError, TypeError):
pass
email_list = self.email_split(msg)
partner_ids = filter(None, self._find_partner_from_emails(email_list, force_create=False))
self.message_subscribe(partner_ids)
return super(Task, self).message_update(msg, update_vals=update_vals)
@api.multi
def message_get_suggested_recipients(self):
recipients = super(Task, self).message_get_suggested_recipients()
for task in self.filtered('partner_id'):
reason = _('Customer Email') if task.partner_id.email else _('Customer')
task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason)
return recipients
@api.multi
def message_get_email_values(self, notif_mail=None):
res = super(Task, self).message_get_email_values(notif_mail=notif_mail)
headers = {}
if res.get('headers'):
try:
headers.update(eval(res['headers']))
except Exception:
pass
if self.project_id:
current_objects = filter(None, headers.get('X-Odoo-Objects', '').split(','))
current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
headers['X-Odoo-Objects'] = ','.join(current_objects)
if self.tag_ids:
headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name'))
res['headers'] = repr(headers)
return res
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
_description = 'Analytic Account'
use_tasks = fields.Boolean(string='Use Tasks', help="Check this box to manage internal activities through this project")
company_uom_id = fields.Many2one('product.uom', related='company_id.project_time_mode_id', string="Company UOM")
project_ids = fields.One2many('project.project', 'analytic_account_id', string='Projects')
project_count = fields.Integer(compute='_compute_project_count', string='Project Count')
def _compute_project_count(self):
for account in self:
account.project_count = len(account.with_context(active_test=False).project_ids)
@api.model
def _trigger_project_creation(self, vals):
'''
This function is used to decide if a project needs to be automatically created or not when an analytic account is created. It returns True if it needs to be so, False otherwise.
'''
return vals.get('use_tasks') and 'project_creation_in_progress' not in self.env.context
@api.multi
def project_create(self, vals):
'''
This function is called at the time of analytic account creation and is used to create a project automatically linked to it if the conditions are meet.
'''
self.ensure_one()
Project = self.env['project.project']
project = Project.with_context(active_test=False).search([('analytic_account_id', '=', self.id)])
if not project and self._trigger_project_creation(vals):
project_values = {
'name': vals.get('name'),
'analytic_account_id': self.id,
'use_tasks': True,
}
return Project.create(project_values).id
return False
@api.model
def create(self, vals):
analytic_account = super(AccountAnalyticAccount, self).create(vals)
analytic_account.project_create(vals)
return analytic_account
@api.multi
def write(self, vals):
vals_for_project = vals.copy()
for account in self:
if not vals.get('name'):
vals_for_project['name'] = account.name
account.project_create(vals_for_project)
return super(AccountAnalyticAccount, self).write(vals)
@api.multi
def unlink(self):
projects = self.env['project.project'].search([('analytic_account_id', 'in', self.ids)])
has_tasks = self.env['project.task'].search_count([('project_id', 'in', projects.ids)])
if has_tasks:
raise UserError(_('Please remove existing tasks in the project linked to the accounts you want to delete.'))
return super(AccountAnalyticAccount, self).unlink()
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
if args is None:
args = []
if self.env.context.get('current_model') == 'project.project':
return self.search(args + [('name', operator, name)], limit=limit).name_get()
return super(AccountAnalyticAccount, self).name_search(name, args=args, operator=operator, limit=limit)
@api.multi
def projects_action(self):
projects = self.with_context(active_test=False).mapped('project_ids')
result = {
"type": "ir.actions.act_window",
"res_model": "project.project",
"views": [[False, "tree"], [False, "form"]],
"domain": [["id", "in", projects.ids]],
"context": {"create": False},
"name": "Projects",
}
if len(projects) == 1:
result['views'] = [(False, "form")]
result['res_id'] = projects.id
else:
result = {'type': 'ir.actions.act_window_close'}
return result
class ProjectTaskHistory(models.Model):
"""
Tasks History, used for cumulative flow charts (Lean/Agile)
"""
_name = 'project.task.history'
_description = 'History of Tasks'
_rec_name = 'task_id'
_log_access = False
@api.depends('date', 'task_id', 'type_id', 'type_id.fold')
def _compute_end_date(self):
for history in self:
if history.type_id and history.type_id.fold:
history.end_date = history.date
continue
self.env.cr.execute('''select
date
from
project_task_history
where
task_id=%s and
id>%s
order by id limit 1''', (history.task_id.id, history.id))
res = self.env.cr.fetchone()
history.end_date = res and res[0] or False
task_id = fields.Many2one('project.task', string='Task', ondelete='cascade', required=True, index=True)
type_id = fields.Many2one('project.task.type', string='Stage')
kanban_state = fields.Selection([
('normal', 'Normal'),
('blocked', 'Blocked'),
('done', 'Ready for next stage')
], string='Kanban State')
date = fields.Date(string='Date', index=True, default=fields.Date.context_today)
end_date = fields.Date(string='End Date', compute='_compute_end_date', store=True)
remaining_hours = fields.Float(string='Remaining Time', digits=(16, 2))
planned_hours = fields.Float(string='Planned Time', digits=(16, 2))
user_id = fields.Many2one('res.users', string='Responsible')
class ProjectTags(models.Model):
""" Tags of project's tasks (or issues) """
_name = "project.tags"
_description = "Tags of project's tasks, issues..."
name = fields.Char(required=True)
color = fields.Integer(string='Color Index')
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]