[MERGE] project,project_issue,project_issue_sheet: migration to new api
This commit is contained in:
@@ -15,9 +15,6 @@ class Project(models.Model):
|
||||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
def _get_total_hours(self):
|
||||
return super(Task, self)._get_total_hours() + self.effective_hours
|
||||
|
||||
@api.multi
|
||||
def _get_subtask_count(self):
|
||||
for task in self:
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project
|
||||
import company
|
||||
import models
|
||||
import report
|
||||
import res_partner
|
||||
import res_config
|
||||
import web_planner
|
||||
|
||||
@@ -37,20 +37,19 @@ Dashboard / Reports for Project Management will include:
|
||||
'data': [
|
||||
'security/project_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'project_data.xml',
|
||||
'project_view.xml',
|
||||
'res_partner_view.xml',
|
||||
'report/project_report_view.xml',
|
||||
'report/project_cumulative.xml',
|
||||
'res_config_view.xml',
|
||||
'views/project.xml',
|
||||
'project_dashboard.xml',
|
||||
'tour_view.xml',
|
||||
'web_planner_data.xml',
|
||||
'project_mail_template_data.xml',
|
||||
'data/project_data.xml',
|
||||
'report/project_report_views.xml',
|
||||
'report/project_cumulative_views.xml',
|
||||
'views/project_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/res_config_views.xml',
|
||||
'views/project_templates.xml',
|
||||
'views/tour_views.xml',
|
||||
'data/web_planner_data.xml',
|
||||
'data/project_mail_template_data.xml',
|
||||
],
|
||||
'qweb': ['static/src/xml/project.xml'],
|
||||
'demo': ['project_demo.xml'],
|
||||
'demo': ['data/project_demo.xml'],
|
||||
'test': [
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class res_company(osv.osv):
|
||||
_inherit = 'res.company'
|
||||
_columns = {
|
||||
'project_time_mode_id': fields.many2one('product.uom', 'Project Time Unit',
|
||||
help='This will set the unit of measure used in projects and tasks.\n' \
|
||||
"If you use the timesheet linked to projects, don't " \
|
||||
"forget to setup the right unit of measure in your employees.",
|
||||
),
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- This will set the unit of measure used in projects and tasks.-->
|
||||
@@ -33,7 +33,6 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data>
|
||||
|
||||
|
||||
<!-- Task-related subtypes for messaging / Chatter -->
|
||||
@@ -107,7 +106,6 @@
|
||||
<p>You can manage todo lists on tasks by installing the <i>Todo Lists</i> application, supporting the Getting Things Done (GTD) methodology.</p>
|
||||
<p>You can also manage issues/bugs in projects by installing the "Issue Tracker" application.</p>]]></field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
|
||||
@@ -333,4 +331,4 @@
|
||||
|
||||
</data>
|
||||
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Users -->
|
||||
@@ -435,4 +435,4 @@ Send it ASAP, its urgent.</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- notify to admin of module installation -->
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project
|
||||
import res_company
|
||||
import res_partner
|
||||
import res_config
|
||||
import web_planner
|
||||
@@ -0,0 +1,829 @@
|
||||
# -*- 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")]
|
||||
|
||||
def _get_visibility_selection(self):
|
||||
""" Overriden in portal_project to offer more options """
|
||||
return [
|
||||
('employees', _('Visible by all employees')),
|
||||
('followers', _('On invitation only')),
|
||||
('portal', _('Shared with a customer'))
|
||||
]
|
||||
|
||||
@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()
|
||||
_visibility_selection = lambda self: self._get_visibility_selection()
|
||||
|
||||
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(_visibility_selection, string='Privacy', required=True,
|
||||
default='employees',
|
||||
help="Holds visibility of the tasks or issues that belong to the current project:\n"
|
||||
"- Portal : employees see everything;\n"
|
||||
" if portal is activated, portal users see the tasks or issues followed by\n"
|
||||
" them or by someone of their company\n"
|
||||
"- Employees Only: employees see all tasks or issues\n"
|
||||
"- Followers Only: employees see only the followed tasks or issues; if portal\n"
|
||||
" is activated, portal users see the followed tasks or issues.")
|
||||
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 !"),
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
project_time_mode_id = fields.Many2one('product.uom', string='Project Time Unit',
|
||||
help="This will set the unit of measure used in projects and tasks.\n"
|
||||
"If you use the timesheet linked to projects, don't "
|
||||
"forget to setup the right unit of measure in your employees.")
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProjectConfiguration(models.TransientModel):
|
||||
_name = 'project.config.settings'
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
module_pad = fields.Selection([
|
||||
(0, "Task description is a plain text"),
|
||||
(1, "Collaborative rich text on task description")
|
||||
], string="Pads",
|
||||
help='Lets the company customize which Pad installation should be used to link to new pads '
|
||||
'(for example: http://ietherpad.com/).\n'
|
||||
'-This installs the module pad.')
|
||||
module_rating_project = fields.Selection([
|
||||
(0, "No customer rating"),
|
||||
(1, 'Allow activating customer rating on projects, at issue completion')
|
||||
], string="Rating",
|
||||
help="This allows customers to give rating on provided services")
|
||||
generate_project_alias = fields.Selection([
|
||||
(0, "Do not create an email alias automatically"),
|
||||
(1, "Automatically generate an email alias at the project creation")
|
||||
], string="Project Alias",
|
||||
help="Odoo will generate an email alias at the project creation from project name.")
|
||||
module_project_forecast = fields.Boolean(string="Forecasts, planning and Gantt charts")
|
||||
|
||||
@api.multi
|
||||
def set_default_generate_project_alias(self):
|
||||
check = self.env.user.has_group('base.group_system')
|
||||
Values = check and self.env['ir.values'].sudo() or self.env['ir.values']
|
||||
for config in self:
|
||||
Values.set_default('project.config.settings', 'generate_project_alias', config.generate_project_alias)
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
""" Inherits partner and adds Tasks information in the partner form """
|
||||
_inherit = 'res.partner'
|
||||
|
||||
task_ids = fields.One2many('project.task', 'partner_id', string='Tasks')
|
||||
task_count = fields.Integer(compute='_compute_task_count', string='# Tasks')
|
||||
|
||||
def _compute_task_count(self):
|
||||
for partner in self:
|
||||
partner.task_count = len(partner.task_ids)
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp import models
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class PlannerProject(models.Model):
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,111 +0,0 @@
|
||||
<odoo>
|
||||
<record id="edit_project_simplified" model="ir.ui.view">
|
||||
<field name="name">project.project.form.popup</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" class="o_project_name oe_inline"
|
||||
string="Project Name" placeholder="e.g. Office Party"/>
|
||||
<label for="alias_name" string="Choose a Project Email" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<div name="alias_def" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Create" name="close_dialog" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-default" special="cancel"/>
|
||||
</footer>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_create_project" model="ir.actions.act_window">
|
||||
<field name="name">Create a Project</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="edit_project_simplified"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_project_kanban">
|
||||
<field name="name">project.project.kanban</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="oe_background_grey o_kanban_dashboard o_project_kanban" on_create="project.open_create_project">
|
||||
<field name="name"/>
|
||||
<field name="use_tasks"/>
|
||||
<field name="user_id"/>
|
||||
<field name="color"/>
|
||||
<field name="task_count"/>
|
||||
<field name="task_needaction_count"/>
|
||||
<field name="label_tasks"/>
|
||||
<field name="task_ids"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="alias_name"/>
|
||||
<field name="alias_domain"/>
|
||||
<field name="doc_count"/>
|
||||
<field name="is_favorite"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
|
||||
<div class="o_project_kanban_main">
|
||||
<div class="o_kanban_card_content o_visible">
|
||||
<div class="o_kanban_primary_left">
|
||||
<div class="o_primary">
|
||||
<span><t t-esc="record.name.value"/></span>
|
||||
</div>
|
||||
<div t-if="record.alias_name.value and record.alias_domain.value">
|
||||
<span><i class="fa fa-envelope"></i> <t t-esc="record.alias_id.value"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div><div class="o_kanban_card_manage_pane o_invisible">
|
||||
<div class="o_kanban_card_manage_section o_kanban_manage_reports">
|
||||
<div groups="project.group_project_manager">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(action_project_task_user_tree_filtered)d" type="action">Tasks</a>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(action_view_task_history_cumulative_filter)d" type="action">Cumulative Flow</a>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="widget.editable" class="o_project_kanban_colorpicker">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color"/>
|
||||
</div>
|
||||
<div>
|
||||
<a type="object" name="toggle_favorite"><i t-attf-class="fa o_dashboard_star #{record.is_favorite.raw_value ? 'fa-star' : 'fa-star-o'}" title="Click to add/remove from favorite"/> Favorite</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_project_kanban_manage">
|
||||
<a class="o_kanban_manage_toggle_button" href="#">More <i class="fa fa-caret-down"/></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_project_kanban_boxes">
|
||||
<div t-if="record.use_tasks.raw_value" class="o_project_kanban_box">
|
||||
<a name="%(act_project_project_2_project_task_all)d" type="action">
|
||||
<span class="o_value"><t t-esc="record.task_count.value"/></span>
|
||||
<span class="o_label"><t t-esc="record.label_tasks.value"/></span>
|
||||
</a>
|
||||
<a t-if="record.task_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}">
|
||||
<t t-esc="record.task_needaction_count.raw_value"/>
|
||||
</a>
|
||||
</div>
|
||||
<a t-if="record.doc_count.raw_value" class="o_project_kanban_box" name="attachment_tree_view" type="object">
|
||||
<span class="o_value"><t t-esc="record.doc_count.value"/></span>
|
||||
<span class="o_label">Documents</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project_cumulative
|
||||
import project_report
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class ProjectTaskHistoryCumulative(models.Model):
|
||||
_name = 'project.task.history.cumulative'
|
||||
_inherit = 'project.task.history'
|
||||
_auto = False
|
||||
|
||||
end_date = fields.Date(string='End Date')
|
||||
nbr_tasks = fields.Integer(string='# of Tasks', readonly=True)
|
||||
project_id = fields.Many2one('project.project', string='Project')
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self._cr, self._table)
|
||||
|
||||
self._cr.execute(""" CREATE VIEW %s AS (
|
||||
SELECT
|
||||
history.date::varchar||'-'||history.history_id::varchar AS id,
|
||||
history.date AS end_date,
|
||||
*
|
||||
FROM (
|
||||
SELECT
|
||||
h.id AS history_id,
|
||||
h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date,
|
||||
h.task_id, h.type_id, h.user_id, h.kanban_state,
|
||||
count(h.task_id) as nbr_tasks,
|
||||
greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours,
|
||||
t.project_id
|
||||
FROM
|
||||
project_task_history AS h
|
||||
JOIN project_task AS t ON (h.task_id = t.id)
|
||||
GROUP BY
|
||||
h.id,
|
||||
h.task_id,
|
||||
t.project_id
|
||||
|
||||
) AS history
|
||||
)
|
||||
""" % (self._table,))
|
||||
+2
-7
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<record id="view_task_history_pivot" model="ir.ui.view">
|
||||
<field name="name">project.task.history.cumulative.pivot</field>
|
||||
@@ -82,9 +81,5 @@
|
||||
</field>
|
||||
<field name="view_id"></field> <!-- force empty -->
|
||||
</record>
|
||||
<menuitem action="action_view_task_history_cumulative"
|
||||
id="menu_action_view_task_history_cumulative"
|
||||
parent="base.menu_project_report"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,37 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp import tools
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class report_project_task_user(osv.osv):
|
||||
class ReportProjectTaskUser(models.Model):
|
||||
_name = "report.project.task.user"
|
||||
_description = "Tasks by user and project"
|
||||
_auto = False
|
||||
_columns = {
|
||||
'name': fields.char('Task Title', readonly=True),
|
||||
'user_id': fields.many2one('res.users', 'Assigned To', readonly=True),
|
||||
'date_start': fields.datetime('Assignation Date', readonly=True),
|
||||
'no_of_days': fields.integer('# of Days', size=128, readonly=True),
|
||||
'date_end': fields.datetime('Ending Date', readonly=True),
|
||||
'date_deadline': fields.date('Deadline', readonly=True),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True),
|
||||
'project_id': fields.many2one('project.project', 'Project', readonly=True),
|
||||
'closing_days': fields.float('Days to Close', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the task"),
|
||||
'opening_days': fields.float('Days to Assign', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to Open the task"),
|
||||
'delay_endings_days': fields.float('Overpassed Deadline', digits=(16,2), readonly=True),
|
||||
'nbr': fields.integer('# of Tasks', readonly=True), # TDE FIXME master: rename into nbr_tasks
|
||||
'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')],
|
||||
string='Priority', size=1, readonly=True),
|
||||
'state': fields.selection([('normal', 'In Progress'),('blocked', 'Blocked'),('done', 'Ready for next stage')],'Status', readonly=True),
|
||||
'company_id': fields.many2one('res.company', 'Company', readonly=True),
|
||||
'partner_id': fields.many2one('res.partner', 'Contact', readonly=True),
|
||||
'stage_id': fields.many2one('project.task.type', 'Stage'),
|
||||
}
|
||||
_order = 'name desc, project_id'
|
||||
_auto = False
|
||||
|
||||
name = fields.Char(string='Task Title', readonly=True)
|
||||
user_id = fields.Many2one('res.users', string='Assigned To', readonly=True)
|
||||
date_start = fields.Datetime(string='Assignation Date', readonly=True)
|
||||
no_of_days = fields.Integer(string='# of Days', readonly=True)
|
||||
date_end = fields.Datetime(string='Ending Date', readonly=True)
|
||||
date_deadline = fields.Date(string='Deadline', readonly=True)
|
||||
date_last_stage_update = fields.Datetime(string='Last Stage Update', readonly=True)
|
||||
project_id = fields.Many2one('project.project', string='Project', readonly=True)
|
||||
closing_days = fields.Float(string='Days to Close',
|
||||
digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the task")
|
||||
opening_days = fields.Float(string='Days to Assign',
|
||||
digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to Open the task")
|
||||
delay_endings_days = fields.Float(string='Overpassed Deadline', digits=(16,2), readonly=True)
|
||||
nbr = fields.Integer('# of Tasks', readonly=True) # TDE FIXME master: rename into nbr_tasks
|
||||
priority = fields.Selection([
|
||||
('0','Low'),
|
||||
('1','Normal'),
|
||||
('2','High')
|
||||
], size=1, readonly=True)
|
||||
state = fields.Selection([
|
||||
('normal', 'In Progress'),
|
||||
('blocked', 'Blocked'),
|
||||
('done', 'Ready for next stage')
|
||||
], string='Status', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Contact', readonly=True)
|
||||
stage_id = fields.Many2one('project.task.type', string='Stage')
|
||||
|
||||
def _select(self):
|
||||
select_str = """
|
||||
@@ -77,12 +84,12 @@ class report_project_task_user(osv.osv):
|
||||
"""
|
||||
return group_by_str
|
||||
|
||||
def init(self, cr):
|
||||
tools.sql.drop_view_if_exists(cr, 'report_project_task_user')
|
||||
cr.execute("""
|
||||
CREATE view report_project_task_user as
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self._cr, self._table)
|
||||
self._cr.execute("""
|
||||
CREATE view %s as
|
||||
%s
|
||||
FROM project_task t
|
||||
WHERE t.active = 'true'
|
||||
%s
|
||||
"""% (self._select(), self._group_by()))
|
||||
""" % (self._table, self._select(), self._group_by()))
|
||||
|
||||
+2
-10
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<menuitem id="base.menu_project_report" name="Reports"
|
||||
groups="project.group_project_manager"
|
||||
parent="base.menu_main_pm" sequence="99"/>
|
||||
<odoo>
|
||||
|
||||
<record id="view_task_project_user_pivot" model="ir.ui.view">
|
||||
<field name="name">report.project.task.user.pivot</field>
|
||||
@@ -107,7 +102,4 @@
|
||||
<field name="help">This report allows you to analyse the performance of your projects and users. You can analyse the quantities of tasks, the hours spent compared to the planned hours, the average number of days to open or close a task, etc.</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Tasks" action="action_project_task_user_tree" id="menu_project_task_user_tree" parent="base.menu_project_report"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,36 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class project_configuration(osv.osv_memory):
|
||||
_name = 'project.config.settings'
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
_columns = {
|
||||
'module_pad': fields.selection([
|
||||
(0, "Task description is a plain text"),
|
||||
(1, "Collaborative rich text on task description")
|
||||
], "Pads",
|
||||
help='Lets the company customize which Pad installation should be used to link to new pads '
|
||||
'(for example: http://ietherpad.com/).\n'
|
||||
'-This installs the module pad.'),
|
||||
'module_rating_project': fields.selection([
|
||||
(0, "No customer rating"),
|
||||
(1, 'Allow activating customer rating on projects, at issue completion')
|
||||
], "Rating",
|
||||
help="This allows customers to give rating on provided services"),
|
||||
'generate_project_alias': fields.selection([
|
||||
(0, "Do not create an email alias automatically"),
|
||||
(1, "Automatically generate an email alias at the project creation")
|
||||
], "Project Alias",
|
||||
help="Odoo will generate an email alias at the project creation from project name."),
|
||||
'module_project_forecast': fields.boolean("Forecasts, planning and Gantt charts"),
|
||||
}
|
||||
|
||||
def set_default_generate_project_alias(self, cr, uid, ids, context=None):
|
||||
config_value = self.browse(cr, uid, ids, context=context).generate_project_alias
|
||||
check = self.pool['res.users'].has_group(cr, uid, 'base.group_system')
|
||||
self.pool.get('ir.values').set_default(cr, check and SUPERUSER_ID or uid, 'project.config.settings', 'generate_project_alias', config_value)
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.osv import fields,osv
|
||||
|
||||
class res_partner(osv.osv):
|
||||
def _task_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
Task = self.pool['project.task']
|
||||
return {
|
||||
partner_id: Task.search_count(cr,uid, [('partner_id', '=', partner_id)], context=context)
|
||||
for partner_id in ids
|
||||
}
|
||||
|
||||
""" Inherits partner and adds Tasks information in the partner form """
|
||||
_inherit = 'res.partner'
|
||||
_columns = {
|
||||
'task_ids': fields.one2many('project.task', 'partner_id', 'Tasks'),
|
||||
'task_count': fields.function(_task_count, string='# Tasks', type='integer'),
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
<odoo>
|
||||
|
||||
<record id="group_project_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
@@ -24,7 +23,6 @@
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="project_comp_rule">
|
||||
@@ -133,4 +131,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.addons.project.tests.test_project_base import TestProjectBase
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.exceptions import except_orm
|
||||
from openerp.tools import mute_logger
|
||||
from odoo.addons.project.tests.test_project_base import TestProjectBase
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestPortalProjectBase(TestProjectBase):
|
||||
@@ -56,9 +55,7 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
# Do: Chell reads project -> ok (portal ok public)
|
||||
pigs.sudo(self.user_portal).read(['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko portal)
|
||||
# TODO: Change the except_orm to Warning ( Because here it's call check_access_rule
|
||||
# which still generate exception in except_orm.)
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Test: no access right to project.task
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, [])
|
||||
# Data: task follower cleaning
|
||||
@@ -80,14 +77,12 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
# Do: Chell reads project -> ko (portal ko employee)
|
||||
# TODO Change the except_orm to Warning
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
# Test: no project task visible + assigned
|
||||
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
|
||||
self.assertFalse(tasks.ids, 'access rights: portal user should not see tasks of an employees project, even if assigned')
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
# TODO Change the except_orm to Warning
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Do: project user is employee and can create a task
|
||||
tmp_task = self.env['project.task'].sudo(self.user_projectuser).with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs task',
|
||||
@@ -100,7 +95,6 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
pigs.write({'privacy_visibility': 'followers'})
|
||||
|
||||
# Do: Alfred reads project -> ko (employee ko followers)
|
||||
# TODO Change the except_orm to Warning
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_projectuser).read, ['user_id'])
|
||||
# Test: no project task visible
|
||||
tasks = self.env['project.task'].sudo(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
@@ -111,15 +105,14 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
|
||||
# Do: Chell reads project -> ko (portal ko employee)
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
# Test: no project task visible
|
||||
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_3,
|
||||
'access rights: portal user should not see tasks of a not-followed followers project, only assigned')
|
||||
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
# TODO Change the except_orm to Warning
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
|
||||
# Data: subscribe Alfred, Chell and Donovan as follower
|
||||
pigs.message_subscribe_users(user_ids=[self.user_projectuser.id, self.user_portal.id, self.user_public.id])
|
||||
@@ -133,17 +126,16 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
# Do: Chell reads project -> ok (follower ok follower)
|
||||
pigs.sudo(self.user_portal).read(['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko follower even if follower)
|
||||
# TODO Change the except_orm to Warning
|
||||
self.assertRaises(except_orm, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Do: project user is follower of the project and can create a task
|
||||
self.env['project.task'].sudo(self.user_projectuser.id).with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs task', 'project_id': pigs.id
|
||||
})
|
||||
# not follower user should not be able to create a task
|
||||
pigs.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_projectuser.id])
|
||||
self.assertRaises(except_orm, self.env['project.task'].sudo(self.user_projectuser).with_context({
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_projectuser).with_context({
|
||||
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
|
||||
|
||||
# Do: project user can create a task without project
|
||||
self.assertRaises(except_orm, self.env['project.task'].sudo(self.user_projectuser).with_context({
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_projectuser).with_context({
|
||||
'mail_create_nolog': True}).create, {'name': 'Pigs task', 'project_id': pigs.id})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openerp.addons.mail.tests.common import TestMail
|
||||
from odoo.addons.mail.tests.common import TestMail
|
||||
|
||||
|
||||
class TestProjectBase(TestMail):
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from .test_project_base import TestProjectBase
|
||||
from openerp.exceptions import AccessError
|
||||
from openerp.tools import mute_logger
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
EMAIL_TPL = """Return-Path: <whatever-2a840@postmaster.twitter.com>
|
||||
@@ -11,7 +10,7 @@ X-Original-To: {to}
|
||||
Delivered-To: {to}
|
||||
To: {to}
|
||||
cc: {cc}
|
||||
Received: by mail1.openerp.com (Postfix, from userid 10002)
|
||||
Received: by mail1.odoo.com (Postfix, from userid 10002)
|
||||
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
|
||||
Message-ID: {msg_id}
|
||||
Date: Tue, 29 Nov 2011 12:43:21 +0530
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="project assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/project/static/src/css/project.css"/>
|
||||
@@ -11,5 +10,4 @@
|
||||
<script type="text/javascript" src="/project/static/src/js/web_planner_project.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
|
||||
<!-- Top menu item -->
|
||||
<menuitem name="Project"
|
||||
@@ -108,7 +107,7 @@
|
||||
<page string="Settings">
|
||||
<group>
|
||||
<field name="privacy_visibility" widget="radio"/>
|
||||
<field name="partner_id" on_change="onchange_partner_id(partner_id)" string="Customer"
|
||||
<field name="partner_id" string="Customer"
|
||||
attrs="{'invisible':[('privacy_visibility','in',['employees', 'followers'])]}"/>
|
||||
<field name="user_id" string="Project Manager"
|
||||
attrs="{'readonly':[('active','=',False)]}"
|
||||
@@ -171,7 +170,6 @@
|
||||
<record id="view_project" model="ir.ui.view">
|
||||
<field name="name">project.project.tree</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="field_parent">child_ids</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-bf="message_needaction==True" decoration-muted="active == False" string="Projects">
|
||||
<field name="sequence" widget="handle"/>
|
||||
@@ -183,6 +181,116 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="edit_project_simplified" model="ir.ui.view">
|
||||
<field name="name">project.project.form.popup</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" class="o_project_name oe_inline"
|
||||
string="Project Name" placeholder="e.g. Office Party"/>
|
||||
<label for="alias_name" string="Choose a Project Email" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<div name="alias_def" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Create" name="close_dialog" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-default" special="cancel"/>
|
||||
</footer>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_create_project" model="ir.actions.act_window">
|
||||
<field name="name">Create a Project</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="edit_project_simplified"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_project_kanban">
|
||||
<field name="name">project.project.kanban</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="oe_background_grey o_kanban_dashboard o_project_kanban" on_create="project.open_create_project">
|
||||
<field name="name"/>
|
||||
<field name="use_tasks"/>
|
||||
<field name="user_id"/>
|
||||
<field name="color"/>
|
||||
<field name="task_count"/>
|
||||
<field name="task_needaction_count"/>
|
||||
<field name="label_tasks"/>
|
||||
<field name="task_ids"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="alias_name"/>
|
||||
<field name="alias_domain"/>
|
||||
<field name="doc_count"/>
|
||||
<field name="is_favorite"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
|
||||
<div class="o_project_kanban_main">
|
||||
<div class="o_kanban_card_content o_visible">
|
||||
<div class="o_kanban_primary_left">
|
||||
<div class="o_primary">
|
||||
<span><t t-esc="record.name.value"/></span>
|
||||
</div>
|
||||
<div t-if="record.alias_name.value and record.alias_domain.value">
|
||||
<span><i class="fa fa-envelope"></i> <t t-esc="record.alias_id.value"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div><div class="o_kanban_card_manage_pane o_invisible">
|
||||
<div class="o_kanban_card_manage_section o_kanban_manage_reports">
|
||||
<div groups="project.group_project_manager">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(project.action_project_task_user_tree_filtered)d" type="action">Tasks</a>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(project.action_view_task_history_cumulative_filter)d" type="action">Cumulative Flow</a>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="widget.editable" class="o_project_kanban_colorpicker">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color"/>
|
||||
</div>
|
||||
<div>
|
||||
<a type="object" name="toggle_favorite"><i t-attf-class="fa o_dashboard_star #{record.is_favorite.raw_value ? 'fa-star' : 'fa-star-o'}" title="Click to add/remove from favorite"/> Favorite</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_project_kanban_manage">
|
||||
<a class="o_kanban_manage_toggle_button" href="#">More <i class="fa fa-caret-down"/></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_project_kanban_boxes">
|
||||
<div t-if="record.use_tasks.raw_value" class="o_project_kanban_box">
|
||||
<a name="%(act_project_project_2_project_task_all)d" type="action">
|
||||
<span class="o_value"><t t-esc="record.task_count.value"/></span>
|
||||
<span class="o_label"><t t-esc="record.label_tasks.value"/></span>
|
||||
</a>
|
||||
<a t-if="record.task_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}">
|
||||
<t t-esc="record.task_needaction_count.raw_value"/>
|
||||
</a>
|
||||
</div>
|
||||
<a t-if="record.doc_count.raw_value" class="o_project_kanban_box" name="attachment_tree_view" type="object">
|
||||
<span class="o_value"><t t-esc="record.doc_count.value"/></span>
|
||||
<span class="o_label">Documents</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="activate_sample_project" model="ir.actions.server">
|
||||
<field name="name">Activate Sample Project</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
@@ -252,9 +360,8 @@
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project_id" domain="[('active','=',False)]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
|
||||
<field name="project_id" domain="[('active', '=', False)]" context="{'default_use_tasks':1}"/>
|
||||
<field name="user_id"
|
||||
on_change="onchange_user_id(user_id, context)"
|
||||
class="o_task_user_field"
|
||||
options='{"no_open": True}'/>
|
||||
<field name="legend_blocked" invisible="1"/>
|
||||
@@ -431,7 +538,7 @@
|
||||
<label for="use_tasks" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button" type="object" name="projects_action"
|
||||
<button class="oe_stat_button" type="object" name="projects_action"
|
||||
icon="fa-puzzle-piece" attrs="{'invisible': [('project_count', '=', 0)]}">
|
||||
<field string="Projects" name="project_count" widget="statinfo"/>
|
||||
</button>
|
||||
@@ -638,8 +745,18 @@
|
||||
<menuitem action="project_tags_action" id="menu_project_tags_act" parent="menu_project_config" groups="base.group_no_one"/>
|
||||
|
||||
<!-- Portal menuitems -->
|
||||
<menuitem name="Projects" id="portal_services_projects" parent="portal.portal_projects"
|
||||
<menuitem name="Projects" id="portal_services_projects" parent="portal.portal_projects"
|
||||
action="open_view_project_all" sequence="10"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
<!-- Reporting menus -->
|
||||
<menuitem id="base.menu_project_report" name="Reports"
|
||||
groups="project.group_project_manager"
|
||||
parent="base.menu_main_pm" sequence="99"/>
|
||||
<menuitem id="menu_project_task_user_tree" name="Tasks"
|
||||
action="project.action_project_task_user_tree"
|
||||
parent="base.menu_project_report"/>
|
||||
<menuitem id="menu_action_view_task_history_cumulative"
|
||||
action="project.action_view_task_history_cumulative"
|
||||
parent="base.menu_project_report"/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<record id="view_config_settings" model="ir.ui.view">
|
||||
<field name="name">project settings</field>
|
||||
<field name="model">project.config.settings</field>
|
||||
@@ -40,5 +39,4 @@
|
||||
|
||||
<menuitem id="base.menu_project_general_settings" name="Settings" parent="menu_project_config"
|
||||
sequence="0" action="action_config_settings" groups="base.group_system"/>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Partners inherited form -->
|
||||
<record id="view_task_partner_info_form" model="ir.ui.view">
|
||||
@@ -20,5 +19,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1,7 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import models
|
||||
import project_issue
|
||||
import report
|
||||
import project_issue_config_settings
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
@@ -10,7 +10,7 @@
|
||||
'description': """
|
||||
Track Issues/Bugs Management for Projects
|
||||
=========================================
|
||||
This application allows you to manage the issues you might face in a project like bugs in a system, client complaints or material breakdowns.
|
||||
This application allows you to manage the issues you might face in a project like bugs in a system, client complaints or material breakdowns.
|
||||
|
||||
It allows the manager to quickly check the issues, assign them and decide on their status quickly as they evolve.
|
||||
""",
|
||||
@@ -19,23 +19,18 @@ It allows the manager to quickly check the issues, assign them and decide on the
|
||||
'project',
|
||||
],
|
||||
'data': [
|
||||
'project_issue_view.xml',
|
||||
'project_issue_config_settings_views.xml',
|
||||
'project_issue_menu.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'report/project_issue_report_view.xml',
|
||||
'security/project_issue_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'project_issue_data.xml',
|
||||
'project_dashboard.xml',
|
||||
],
|
||||
'demo': ['project_issue_demo.xml'],
|
||||
'test': [
|
||||
'test/issue_users.yml',
|
||||
'test/subscribe_issue.yml',
|
||||
'test/issue_process.yml',
|
||||
'test/issue_demo.yml'
|
||||
'views/project_issue_view.xml',
|
||||
'views/project_issue_config_settings_views.xml',
|
||||
'views/account_analytic_account_view.xml',
|
||||
'views/project_project_view.xml',
|
||||
'views/res_partner_view.xml',
|
||||
'views/project_issue_menu.xml',
|
||||
'views/project_dashboard.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'demo': ['data/project_issue_demo.xml'],
|
||||
'application': True,
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- notify all employees of module installation -->
|
||||
<record model="mail.message" id="module_install_notification">
|
||||
<record id="module_install_notification" model="mail.message">
|
||||
<field name="model">mail.channel</field>
|
||||
<field name="res_id" ref="mail.channel_all_employees"/>
|
||||
<field name="message_type">notification</field>
|
||||
@@ -73,6 +73,5 @@ Access all issues from the top Project menu, and access the issues of a specific
|
||||
<field name="parent_id" eval="ref('mt_issue_stage')"/>
|
||||
<field name="relation_field">project_id</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+36
-58
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="project.project_project_1" model="project.project">
|
||||
<field name="use_issues" eval="True"/>
|
||||
</record>
|
||||
@@ -21,7 +19,6 @@
|
||||
<field name="use_issues" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Categories -->
|
||||
@@ -37,13 +34,11 @@
|
||||
|
||||
<!-- Issues -->
|
||||
<record id="crm_case_buginaccountsmodule0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-08 10:15:00')" name="date"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="date" eval="time.strftime('%Y-%m-08 10:15:00')"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="portal.partner_demo_portal"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
<field eval="15.0" name="duration"/>
|
||||
<field name="duration" eval="15.0"/>
|
||||
<field name="name">Bug in Accounts module</field>
|
||||
<field name="email_from">agr@agrolait.com</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
@@ -52,23 +47,20 @@
|
||||
</record>
|
||||
|
||||
<record id="crm_case_programnotgivingproperoutput0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-15 12:50:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-15 12:50:00')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="3.5" name="duration"/>
|
||||
<field name="duration" eval="3.5"/>
|
||||
<field name="name">Program not giving proper output</field>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_outputincorrect0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-18 14:30:00')" name="date"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="date" eval="time.strftime('%Y-%m-18 14:30:00')"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="2.3" name="duration"/>
|
||||
<field name="duration" eval="2.3"/>
|
||||
<field name="project_id" ref="project.project_project_5"/>
|
||||
<field name="name">Output incorrect</field>
|
||||
<field name="stage_id" ref="project.project_stage_2"/>
|
||||
@@ -78,36 +70,33 @@
|
||||
</record>
|
||||
|
||||
<record id="crm_case_problemloadingpage0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-20 15:25:05')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-20 15:25:05')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="4.0" name="duration"/>
|
||||
<field name="duration" eval="4.0"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Problem loading page</field>
|
||||
<field name="stage_id" ref="project.project_stage_0"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_pagenotfound0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-22 18:15:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-22 18:15:00')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="1.0" name="duration"/>
|
||||
<field name="duration" eval="1.0"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Page not Found</field>
|
||||
<field name="stage_id" ref="project.project_stage_0"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_programmingerror0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-24 09:45:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-24 09:45:00')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="4.0" name="duration"/>
|
||||
<field name="duration" eval="4.0"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Programming Error</field>
|
||||
<field name="stage_id" ref="project.project_stage_0"/>
|
||||
@@ -117,72 +106,65 @@
|
||||
</record>
|
||||
|
||||
<record id="crm_case_logicalerrorinprogram0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-26 11:10:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-26 11:10:00')"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="2.0" name="duration"/>
|
||||
<field name="duration" eval="2.0"/>
|
||||
<field name="project_id" ref="project.project_project_2"/>
|
||||
<field name="name">Logical Error in Program</field>
|
||||
<field name="stage_id" ref="project.project_stage_3"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_constrainterror0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-25 13:35:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-25 13:35:00')"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="7.3" name="duration"/>
|
||||
<field name="duration" eval="7.3"/>
|
||||
<field name="project_id" ref="project.project_project_2"/>
|
||||
<field name="name">Constraint Error</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_errorinprogram0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-28 15:40:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-28 15:40:00')"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="1.3" name="duration"/>
|
||||
<field name="duration" eval="1.3"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Error in Program</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_patcheserrorinprogram0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-28 16:30:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-28 16:30:00')"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="13.0" name="duration"/>
|
||||
<field name="duration" eval="13.0"/>
|
||||
<field name="project_id" ref="project.project_project_2"/>
|
||||
<field name="name">Patches Error in Program</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_newfeaturestobeadded0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-01 12:15:10')" name="date"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="date" eval="time.strftime('%Y-%m-01 12:15:10')"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="3.2" name="duration"/>
|
||||
<field name="duration" eval="3.2"/>
|
||||
<field name="project_id" ref="project.project_project_3"/>
|
||||
<field name="name">New Features To Be Added</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_addmenustothemodule0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-05 18:00:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-05 18:00:00')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="3.0" name="duration"/>
|
||||
<field name="duration" eval="3.0"/>
|
||||
<field name="project_id" ref="project.project_project_3"/>
|
||||
<field name="name">Add menus to the module</field>
|
||||
<field name="email_from">info@opensides.be</field>
|
||||
@@ -190,12 +172,11 @@
|
||||
</record>
|
||||
|
||||
<record id="crm_case_includeattendancesheetinproject0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-10 17:05:30')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-10 17:05:30')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="2.0" name="duration"/>
|
||||
<field name="duration" eval="2.0"/>
|
||||
<field name="project_id" ref="project.project_project_2"/>
|
||||
<field name="name">Include Attendance sheet in Project</field>
|
||||
<field name="email_from">contact@tecsas.fr</field>
|
||||
@@ -203,44 +184,41 @@
|
||||
</record>
|
||||
|
||||
<record id="crm_case_createnewobject0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-15 10:35:15')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-15 10:35:15')"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="2.45" name="duration"/>
|
||||
<field name="duration" eval="2.45"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Create new object</field>
|
||||
<field name="stage_id" ref="project.project_stage_0"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_improvereportsinhrms0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-19 12:15:00')" name="date"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="date" eval="time.strftime('%Y-%m-19 12:15:00')"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="15.0" name="duration"/>
|
||||
<field name="duration" eval="15.0"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Improve Reports in HRMS</field>
|
||||
<field name="stage_id" ref="project.project_stage_2"/>
|
||||
</record>
|
||||
|
||||
<record id="crm_case_improvereportsinpms0" model="project.issue">
|
||||
<field eval="time.strftime('%Y-%m-21 14:30:00')" name="date"/>
|
||||
<field name="date" eval="time.strftime('%Y-%m-21 14:30:00')"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field eval="1" name="active"/>
|
||||
<field eval="06.15" name="duration"/>
|
||||
<field name="duration" eval="06.15"/>
|
||||
<field name="project_id" ref="project.project_project_4"/>
|
||||
<field name="name">Improve Reports in PMS</field>
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
</record>
|
||||
|
||||
<!-- Call function to set some issues as 'unread' -->
|
||||
|
||||
<!-- <function model="project.issue" name="message_mark_as_unread"
|
||||
eval="[ ref('crm_case_improvereportsinhrms0'), ref('crm_case_createnewobject0')], {}"/> -->
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
@@ -1 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project_issue
|
||||
import account_analytic_account
|
||||
import project_project
|
||||
import project_task_type
|
||||
import res_partner
|
||||
import project_issue_config_settings
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_inherit = 'account.analytic.account'
|
||||
|
||||
use_issues = fields.Boolean(string='Use Issues', help="Check this box to manage customer activities through this project")
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
if self.env['project.issue'].search_count([('project_id.analytic_account_id', 'in', self.ids)]):
|
||||
raise UserError(_('Please remove existing issues in the project linked to the accounts you want to delete.'))
|
||||
return super(AccountAnalyticAccount, self).unlink()
|
||||
|
||||
@api.model
|
||||
def _trigger_project_creation(self, vals):
|
||||
res = super(AccountAnalyticAccount, self)._trigger_project_creation(vals)
|
||||
return res or (vals.get('use_issues') and not 'project_creation_in_progress' in self.env.context)
|
||||
@@ -0,0 +1,378 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools.safe_eval import safe_eval as eval
|
||||
|
||||
|
||||
class ProjectIssue(models.Model):
|
||||
_name = "project.issue"
|
||||
_description = "Project Issue"
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
_order = "priority desc, create_date desc"
|
||||
_mail_post_access = 'read'
|
||||
|
||||
@api.model
|
||||
def _get_default_stage_id(self):
|
||||
return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', False)])
|
||||
|
||||
name = fields.Char(string='Issue', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
days_since_creation = fields.Integer(compute='_compute_inactivity_days', string='Days since creation date',
|
||||
help="Difference in days between creation date and current date")
|
||||
date_deadline = fields.Date(string='Deadline')
|
||||
partner_id = fields.Many2one('res.partner', string='Contact', index=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id)
|
||||
description = fields.Text('Private Note')
|
||||
kanban_state = fields.Selection([('normal', 'Normal'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], string='Kanban State',
|
||||
track_visibility='onchange', required=True, default='normal',
|
||||
help="""An Issue's kanban state indicates special situations affecting it:\n
|
||||
* Normal is the default situation\n
|
||||
* Blocked indicates something is preventing the progress of this issue\n
|
||||
* Ready for next stage indicates the issue is ready to be pulled to the next stage""")
|
||||
email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
|
||||
email_cc = fields.Char(string='Watchers Emails', help="""These email addresses will be added to the CC field of all inbound
|
||||
and outbound emails for this record before being sent. Separate multiple email addresses with a comma""")
|
||||
date_open = fields.Datetime(string='Assigned', readonly=True, index=True)
|
||||
date_closed = fields.Datetime(string='Closed', readonly=True, index=True)
|
||||
date = fields.Datetime('Date')
|
||||
date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, default=fields.Datetime.now)
|
||||
channel = fields.Char(string='Channel', help="Communication channel.")
|
||||
tag_ids = fields.Many2many('project.tags', string='Tags')
|
||||
priority = fields.Selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], 'Priority', index=True, default='0')
|
||||
stage_id = fields.Many2one('project.task.type', string='Stage', track_visibility='onchange', index=True,
|
||||
domain="[('project_ids', '=', project_id)]", copy=False,
|
||||
default=_get_default_stage_id)
|
||||
project_id = fields.Many2one('project.project', string='Project', track_visibility='onchange', index=True)
|
||||
duration = fields.Float('Duration')
|
||||
task_id = fields.Many2one('project.task', string='Task', domain="[('project_id','=',project_id)]",
|
||||
help="You can link this issue to an existing task or directly create a new one from here")
|
||||
day_open = fields.Float(compute='_compute_day', string='Days to Assign', store=True)
|
||||
day_close = fields.Float(compute='_compute_day', string='Days to Close', store=True)
|
||||
|
||||
user_id = fields.Many2one('res.users', string='Assigned to', index=True, track_visibility='onchange', default=lambda self: self.env.uid)
|
||||
working_hours_open = fields.Float(compute='_compute_day', string='Working Hours to assign the Issue', store=True)
|
||||
working_hours_close = fields.Float(compute='_compute_day', string='Working Hours to close the Issue', store=True)
|
||||
inactivity_days = fields.Integer(compute='_compute_inactivity_days', string='Days since last action',
|
||||
help="Difference in days between last action and current date")
|
||||
color = fields.Integer('Color Index')
|
||||
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True)
|
||||
date_action_last = fields.Datetime(string='Last Action', readonly=True)
|
||||
date_action_next = fields.Datetime(string='Next Action', readonly=True)
|
||||
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.multi
|
||||
def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
|
||||
access_rights_uid = access_rights_uid or self.env.uid
|
||||
ProjectTaskType = self.env['project.task.type']
|
||||
order = ProjectTaskType._order
|
||||
# lame hack to allow reverting search, should just work in the trivial case
|
||||
if read_group_order == 'stage_id desc':
|
||||
order = "%s desc" % order
|
||||
# retrieve project_id from the context, add them to already fetched columns (ids)
|
||||
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)]
|
||||
# perform search
|
||||
project_task_types = ProjectTaskType.sudo(access_rights_uid).search(search_domain, order=order)
|
||||
result = project_task_types.sudo(access_rights_uid).name_get()
|
||||
# restore order of the search
|
||||
project_task_type_ids = project_task_types.mapped('id')
|
||||
result.sort(lambda x, y: cmp(project_task_type_ids.index(x[0]), project_task_type_ids.index(y[0])))
|
||||
fold = {project_task_type.id: project_task_type.fold for project_task_type in project_task_types}
|
||||
return result, fold
|
||||
|
||||
_group_by_full = {
|
||||
'stage_id': _read_group_stage_ids
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@api.depends('create_date', 'date_closed', 'date_open')
|
||||
def _compute_day(self):
|
||||
for issue in self:
|
||||
# if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None
|
||||
calendar_id = None
|
||||
if issue.project_id and issue.project_id.resource_calendar_id:
|
||||
calendar_id = issue.project_id.resource_calendar_id.id
|
||||
|
||||
dt_create_date = fields.Datetime.from_string(issue.create_date)
|
||||
if issue.date_open:
|
||||
dt_date_open = fields.Datetime.from_string(issue.date_open)
|
||||
issue.day_open = (dt_date_open - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||
# FIXME when resource.calendar is migrated to new api
|
||||
# Old api call because get_working_hours uses get_weekdays
|
||||
# which has a different behavior if the given id is None.
|
||||
issue.working_hours_open = self.pool['resource.calendar'].get_working_hours(
|
||||
self.env.cr, self.env.uid, calendar_id, dt_create_date, dt_date_open,
|
||||
compute_leaves=True, resource_id=False, default_interval=(8, 16),
|
||||
context=self.env.context
|
||||
)
|
||||
|
||||
if issue.date_closed:
|
||||
dt_date_closed = fields.Datetime.from_string(issue.date_closed)
|
||||
issue.day_close = (dt_date_closed - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||
# FIXME when resource.calendar is migrated to new api
|
||||
# Old api call because get_working_hours uses get_weekdays
|
||||
# which has a different behavior if the given id is None.
|
||||
issue.working_hours_close = self.pool['resource.calendar'].get_working_hours(
|
||||
self.env.cr, self.env.uid, calendar_id, dt_create_date, dt_date_closed,
|
||||
compute_leaves=True, resource_id=False, default_interval=(8, 16),
|
||||
context=self.env.context
|
||||
)
|
||||
|
||||
@api.multi
|
||||
@api.depends('create_date', 'date_action_last', 'date_last_stage_update')
|
||||
def _compute_inactivity_days(self):
|
||||
current_datetime = fields.Datetime.from_string(fields.Datetime.now())
|
||||
for issue in self:
|
||||
dt_create_date = fields.Datetime.from_string(issue.create_date)
|
||||
issue.days_since_creation = (current_datetime - dt_create_date).days
|
||||
|
||||
if issue.date_action_last:
|
||||
issue.inactivity_days = (current_datetime - fields.Datetime.from_string(issue.date_action_last)).days
|
||||
elif issue.date_last_stage_update:
|
||||
issue.inactivity_days = (current_datetime - fields.Datetime.from_string(issue.date_last_stage_update)).days
|
||||
else:
|
||||
issue.inactivity_days = (current_datetime - dt_create_date).days
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
""" This function sets partner email address based on partner
|
||||
"""
|
||||
self.email_from = self.partner_id.email
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project_id(self):
|
||||
if self.project_id:
|
||||
self.partner_id = self.project_id.partner_id.id
|
||||
self.email_from = self.project_id.partner_id.email
|
||||
|
||||
@api.onchange('task_id')
|
||||
def _onchange_task_id(self):
|
||||
self.user_id = self.task_id.user_id
|
||||
|
||||
@api.one
|
||||
def copy(self, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
default.update(name=_('%s (copy)') % (self.name))
|
||||
return super(ProjectIssue, self).copy(default=default)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
context = dict(self.env.context)
|
||||
if vals.get('project_id') and not self.env.context.get('default_project_id'):
|
||||
context['default_project_id'] = vals.get('project_id')
|
||||
if vals.get('user_id') and not vals.get('date_open'):
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
if 'stage_id' in vals:
|
||||
vals.update(self.update_date_closed(vals['stage_id']))
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
context['mail_create_nolog'] = True
|
||||
return super(ProjectIssue, self.with_context(context)).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# stage change: update date_last_stage_update
|
||||
if 'stage_id' in vals:
|
||||
vals.update(self.update_date_closed(vals['stage_id']))
|
||||
vals['date_last_stage_update'] = fields.Datetime.now()
|
||||
if 'kanban_state' not in vals:
|
||||
vals['kanban_state'] = 'normal'
|
||||
# user_id change: update date_open
|
||||
if vals.get('user_id') and 'date_open' not in vals:
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
return super(ProjectIssue, self).write(vals)
|
||||
|
||||
@api.model
|
||||
def get_empty_list_help(self, help):
|
||||
return super(ProjectIssue, self.with_context(
|
||||
empty_list_help_model='project.project',
|
||||
empty_list_help_id=self.env.context.get('default_project_id'),
|
||||
empty_list_help_document_name=_("issues")
|
||||
)).get_empty_list_help(help)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Stage management
|
||||
# -------------------------------------------------------
|
||||
|
||||
def update_date_closed(self, stage_id):
|
||||
project_task_type = self.env['project.task.type'].browse(stage_id)
|
||||
if project_task_type.fold:
|
||||
return {'date_closed': fields.Datetime.now()}
|
||||
return {'date_closed': False}
|
||||
|
||||
def stage_find(self, project_id, domain=None, order='sequence'):
|
||||
""" Override of the base.stage method
|
||||
Parameter of the stage search taken from the issue:
|
||||
- project_id: if set, stages must belong to this project or
|
||||
be a default case
|
||||
"""
|
||||
search_domain = list(domain) if domain else []
|
||||
if project_id:
|
||||
search_domain += [('project_ids', '=', project_id)]
|
||||
project_task_type = self.env['project.task.type'].search(search_domain, order=order, limit=1)
|
||||
return project_task_type.id
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Mail gateway
|
||||
# -------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _track_template(self, tracking):
|
||||
self.ensure_one()
|
||||
res = super(ProjectIssue, self)._track_template(tracking)
|
||||
changes, dummy = tracking[self.id]
|
||||
if 'stage_id' in changes and self.stage_id.mail_template_id:
|
||||
res['stage_id'] = (self.stage_id.mail_template_id, {'composition_mode': 'mass_mail'})
|
||||
return res
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'kanban_state' in init_values and self.kanban_state == 'blocked':
|
||||
return 'project_issue.mt_issue_blocked'
|
||||
elif 'kanban_state' in init_values and self.kanban_state == 'done':
|
||||
return 'project_issue.mt_issue_ready'
|
||||
elif 'user_id' in init_values and self.user_id: # assigned -> new
|
||||
return 'project_issue.mt_issue_new'
|
||||
elif 'stage_id' in init_values and self.stage_id and self.stage_id.sequence <= 1: # start stage -> new
|
||||
return 'project_issue.mt_issue_new'
|
||||
elif 'stage_id' in init_values:
|
||||
return 'project_issue.mt_issue_stage'
|
||||
return super(ProjectIssue, 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_id = self.env.ref('project.group_project_user').id
|
||||
for recipient in recipients:
|
||||
if recipient.id in done_ids:
|
||||
continue
|
||||
if recipient.user_ids and group_project_user_id in recipient.user_ids[0].groups_id.ids:
|
||||
group_data['group_project_user'] |= recipient
|
||||
elif not recipient.user_ids:
|
||||
group_data['partner'] |= recipient
|
||||
else:
|
||||
group_data['user'] |= recipient
|
||||
done_ids.add(recipient.id)
|
||||
return super(ProjectIssue, 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(ProjectIssue, self)._notification_get_recipient_groups(message, recipients)
|
||||
|
||||
actions = []
|
||||
if not self.user_id:
|
||||
take_action = self._notification_link_helper('assign')
|
||||
actions.append({'url': take_action, 'title': _('I take it')})
|
||||
else:
|
||||
new_action_id = self.env.ref('project_issue.project_issue_categ_act0').id
|
||||
new_action = self._notification_link_helper('new', action_id=new_action_id)
|
||||
actions.append({'url': new_action, 'title': _('New Issue')})
|
||||
|
||||
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. """
|
||||
issues = self.browse(res_ids)
|
||||
project_ids = set(issues.mapped('project_id').ids)
|
||||
aliases = self.env['project.project'].message_get_reply_to(list(project_ids), default=default)
|
||||
return dict((issue.id, aliases.get(issue.project_id and issue.project_id.id or 0, False)) for issue in issues)
|
||||
|
||||
@api.multi
|
||||
def message_get_suggested_recipients(self):
|
||||
recipients = super(ProjectIssue, self).message_get_suggested_recipients()
|
||||
try:
|
||||
for issue in self:
|
||||
if issue.partner_id:
|
||||
issue._message_add_suggested_recipient(recipients, partner=issue.partner_id, reason=_('Customer'))
|
||||
elif issue.email_from:
|
||||
issue._message_add_suggested_recipient(recipients, email=issue.email_from, reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
|
||||
pass
|
||||
return recipients
|
||||
|
||||
@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
|
||||
return filter(lambda x: x.split('@')[0] not in self.mapped('project_id.alias_name'), email_list)
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg, custom_values=None):
|
||||
""" Overrides mail_thread message_new that is called by the mailgateway
|
||||
through message_process.
|
||||
This override updates the document according to the email.
|
||||
"""
|
||||
# remove default author when going through the mail gateway. Indeed we
|
||||
# do not want to explicitly set user_id to False; however we do not
|
||||
# want the gateway user to be responsible if no other responsible is
|
||||
# found.
|
||||
create_context = dict(self.env.context or {})
|
||||
create_context['default_user_id'] = False
|
||||
defaults = {
|
||||
'name': msg.get('subject') or _("No Subject"),
|
||||
'email_from': msg.get('from'),
|
||||
'email_cc': msg.get('cc'),
|
||||
'partner_id': msg.get('author_id', False),
|
||||
}
|
||||
if custom_values:
|
||||
defaults.update(custom_values)
|
||||
|
||||
res_id = super(ProjectIssue, self.with_context(create_context)).message_new(msg, custom_values=defaults)
|
||||
issue = self.browse(res_id)
|
||||
email_list = issue.email_split(msg)
|
||||
partner_ids = filter(None, issue._find_partner_from_emails(email_list))
|
||||
issue.message_subscribe(partner_ids)
|
||||
return res_id
|
||||
|
||||
@api.multi
|
||||
def message_update(self, msg, update_vals=None):
|
||||
""" Override to update the issue according to the email. """
|
||||
email_list = self.email_split(msg)
|
||||
partner_ids = filter(None, self._find_partner_from_emails(email_list))
|
||||
self.message_subscribe(partner_ids)
|
||||
return super(ProjectIssue, self).message_update(msg, update_vals=update_vals)
|
||||
|
||||
@api.multi
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, subtype=None, **kwargs):
|
||||
""" Overrides mail_thread message_post so that we can set the date of last action field when
|
||||
a new message is posted on the issue.
|
||||
"""
|
||||
self.ensure_one()
|
||||
mail_message = super(ProjectIssue, self).message_post(subtype=subtype, **kwargs)
|
||||
if subtype:
|
||||
self.sudo().write({'date_action_last': fields.Datetime.now()})
|
||||
return mail_message
|
||||
|
||||
@api.multi
|
||||
def message_get_email_values(self, notif_mail=None):
|
||||
self.ensure_one()
|
||||
res = super(ProjectIssue, 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
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
issue_count = fields.Integer(compute='_compute_issue_count', string="Issues")
|
||||
issue_ids = fields.One2many('project.issue', 'project_id', string="Issues", domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
|
||||
label_issues = fields.Char(string='Use Issues as', help="Customize the issues label, for example to call them cases.", default='Issues')
|
||||
use_issues = fields.Boolean(related="analytic_account_id.use_issues", default=True)
|
||||
issue_needaction_count = fields.Integer(compute="_issue_needaction_count", string="Issues")
|
||||
|
||||
@api.model
|
||||
def _get_alias_models(self):
|
||||
res = super(Project, self)._get_alias_models()
|
||||
res.append(("project.issue", "Issues"))
|
||||
return res
|
||||
|
||||
def _compute_issue_count(self):
|
||||
self.issue_count = self.env['project.issue'].search_count([('project_id', '=', self.id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
|
||||
|
||||
def _issue_needaction_count(self):
|
||||
issue_data = self.env['project.issue'].read_group([('project_id', 'in', self.ids), ('message_needaction', '=', True)], ['project_id'], ['project_id'])
|
||||
result = dict((data['project_id'][0], data['project_id_count']) for data in issue_data)
|
||||
for project in self:
|
||||
project.issue_needaction_count = int(result.get(project.id, 0))
|
||||
|
||||
@api.onchange('use_issues', 'use_tasks')
|
||||
def _on_change_use_tasks_or_issues(self):
|
||||
if self.use_tasks and not self.use_issues:
|
||||
self.alias_model = 'project.task'
|
||||
elif not self.use_tasks and self.use_issues:
|
||||
self.alias_model = 'project.issue'
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(Project, self).write(vals)
|
||||
if 'active' in vals:
|
||||
# archiving/unarchiving a project does it on its issues, too
|
||||
issues = self.with_context(active_test=False).mapped('issue_ids')
|
||||
issues.write({'active': vals['active']})
|
||||
return res
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp import models
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProjectStage(models.Model):
|
||||
_name = "project.task.type"
|
||||
_inherit = ['project.task.type']
|
||||
|
||||
def _get_mail_template_id_domain(self):
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
issue_count = fields.Integer(compute='_compute_issue_count', string='# Issues')
|
||||
|
||||
def _compute_issue_count(self):
|
||||
Issue = self.pool['project.issue']
|
||||
partners = {id: self.search([('id', 'child_of', self.ids)]) for id in self.ids}
|
||||
for partner_id in partners.keys():
|
||||
partner.issue_count = Issue.search_count([('partner_id', 'in', partners[partner_id])])
|
||||
@@ -1,579 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from openerp import api
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import tools
|
||||
from openerp.exceptions import UserError, AccessError
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from openerp.tools.safe_eval import safe_eval as eval
|
||||
from openerp.tools.translate import _
|
||||
|
||||
|
||||
class project_issue(osv.Model):
|
||||
_name = "project.issue"
|
||||
_description = "Project Issue"
|
||||
_order = "priority desc, create_date desc"
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
_mail_post_access = 'read'
|
||||
|
||||
def _get_default_partner(self, cr, uid, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if 'default_project_id' in context:
|
||||
project = self.pool.get('project.project').browse(cr, uid, context['default_project_id'], context=context)
|
||||
if project and project.partner_id:
|
||||
return project.partner_id.id
|
||||
return False
|
||||
|
||||
def _get_default_stage_id(self, cr, uid, context=None):
|
||||
""" Gives default stage_id """
|
||||
if context is None:
|
||||
context = {}
|
||||
return self.stage_find(cr, uid, [], context.get('default_project_id'), [('fold', '=', False)], context=context)
|
||||
|
||||
def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
access_rights_uid = access_rights_uid or uid
|
||||
stage_obj = self.pool.get('project.task.type')
|
||||
order = stage_obj._order
|
||||
# lame hack to allow reverting search, should just work in the trivial case
|
||||
if read_group_order == 'stage_id desc':
|
||||
order = "%s desc" % order
|
||||
# retrieve project_id from the context, add them to already fetched columns (ids)
|
||||
if 'default_project_id' in context:
|
||||
search_domain = ['|', ('project_ids', '=', context['default_project_id']), ('id', 'in', ids)]
|
||||
else:
|
||||
search_domain = [('id', 'in', ids)]
|
||||
# perform search
|
||||
stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
|
||||
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
|
||||
# restore order of the search
|
||||
result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
|
||||
|
||||
fold = {}
|
||||
for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
|
||||
fold[stage.id] = stage.fold or False
|
||||
return result, fold
|
||||
|
||||
def _compute_day(self, cr, uid, ids, fields, args, context=None):
|
||||
"""
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of Openday’s IDs
|
||||
@return: difference between current date and log date
|
||||
@param context: A standard dictionary for contextual values
|
||||
"""
|
||||
Calendar = self.pool['resource.calendar']
|
||||
|
||||
res = dict((res_id, {}) for res_id in ids)
|
||||
for issue in self.browse(cr, uid, ids, context=context):
|
||||
values = {
|
||||
'day_open': 0.0, 'day_close': 0.0,
|
||||
'working_hours_open': 0.0, 'working_hours_close': 0.0,
|
||||
'days_since_creation': 0.0, 'inactivity_days': 0.0,
|
||||
}
|
||||
# if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None
|
||||
calendar_id = None
|
||||
if issue.project_id and issue.project_id.resource_calendar_id:
|
||||
calendar_id = issue.project_id.resource_calendar_id.id
|
||||
|
||||
dt_create_date = datetime.strptime(issue.create_date, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
if issue.date_open:
|
||||
dt_date_open = datetime.strptime(issue.date_open, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
values['day_open'] = (dt_date_open - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||
values['working_hours_open'] = Calendar._interval_hours_get(
|
||||
cr, uid, calendar_id, dt_create_date, dt_date_open,
|
||||
timezone_from_uid=issue.user_id.id or uid,
|
||||
exclude_leaves=False, context=context)
|
||||
|
||||
if issue.date_closed:
|
||||
dt_date_closed = datetime.strptime(issue.date_closed, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
values['day_close'] = (dt_date_closed - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||
values['working_hours_close'] = Calendar._interval_hours_get(
|
||||
cr, uid, calendar_id, dt_create_date, dt_date_closed,
|
||||
timezone_from_uid=issue.user_id.id or uid,
|
||||
exclude_leaves=False, context=context)
|
||||
|
||||
days_since_creation = datetime.today() - dt_create_date
|
||||
values['days_since_creation'] = days_since_creation.days
|
||||
if issue.date_action_last:
|
||||
inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
elif issue.date_last_stage_update:
|
||||
inactive_days = datetime.today() - datetime.strptime(issue.date_last_stage_update, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
else:
|
||||
inactive_days = datetime.today() - datetime.strptime(issue.create_date, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
values['inactivity_days'] = inactive_days.days
|
||||
|
||||
# filter only required values
|
||||
for field in fields:
|
||||
res[issue.id][field] = values[field]
|
||||
|
||||
return res
|
||||
|
||||
def on_change_project(self, cr, uid, ids, project_id, context=None):
|
||||
if project_id:
|
||||
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
|
||||
if project and project.partner_id:
|
||||
return {'value': {'partner_id': project.partner_id.id, 'email_from': project.partner_id.email}}
|
||||
return {}
|
||||
|
||||
_columns = {
|
||||
'id': fields.integer('ID', readonly=True),
|
||||
'name': fields.char('Issue', required=True),
|
||||
'active': fields.boolean('Active', required=False),
|
||||
'create_date': fields.datetime('Creation Date', readonly=True, select=True),
|
||||
'write_date': fields.datetime('Update Date', readonly=True),
|
||||
'days_since_creation': fields.function(_compute_day, string='Days since creation date', \
|
||||
multi='compute_day', type="integer", help="Difference in days between creation date and current date",
|
||||
groups='base.group_user'),
|
||||
'date_deadline': fields.date('Deadline'),
|
||||
'partner_id': fields.many2one('res.partner', 'Contact', select=1),
|
||||
'company_id': fields.many2one('res.company', 'Company'),
|
||||
'description': fields.text('Private Note'),
|
||||
'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
|
||||
track_visibility='onchange',
|
||||
help="A Issue's kanban state indicates special situations affecting it:\n"
|
||||
" * Normal is the default situation\n"
|
||||
" * Blocked indicates something is preventing the progress of this issue\n"
|
||||
" * Ready for next stage indicates the issue is ready to be pulled to the next stage",
|
||||
required=True),
|
||||
'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1),
|
||||
'email_cc': fields.char('Watchers Emails', size=256, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
|
||||
'date_open': fields.datetime('Assigned', readonly=True, select=True),
|
||||
# Project Issue fields
|
||||
'date_closed': fields.datetime('Closed', readonly=True, select=True),
|
||||
'date': fields.datetime('Date'),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
|
||||
'channel': fields.char('Channel', help="Communication channel."),
|
||||
'tag_ids': fields.many2many('project.tags', string='Tags'),
|
||||
'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority', select=True),
|
||||
'stage_id': fields.many2one ('project.task.type', 'Stage',
|
||||
track_visibility='onchange', select=True,
|
||||
domain="[('project_ids', '=', project_id)]", copy=False),
|
||||
'project_id': fields.many2one('project.project', 'Project', track_visibility='onchange', select=True),
|
||||
'duration': fields.float('Duration'),
|
||||
'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]",
|
||||
help="You can link this issue to an existing task or directly create a new one from here"),
|
||||
'day_open': fields.function(_compute_day, string='Days to Assign',
|
||||
multi='compute_day', type="float",
|
||||
store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)},
|
||||
groups='base.group_user'),
|
||||
'day_close': fields.function(_compute_day, string='Days to Close',
|
||||
multi='compute_day', type="float",
|
||||
store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)},
|
||||
groups='base.group_user'),
|
||||
'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1, track_visibility='onchange'),
|
||||
'working_hours_open': fields.function(_compute_day, string='Working Hours to assign the Issue',
|
||||
multi='compute_day', type="float",
|
||||
store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)},
|
||||
groups='base.group_user'),
|
||||
'working_hours_close': fields.function(_compute_day, string='Working Hours to close the Issue',
|
||||
multi='compute_day', type="float",
|
||||
store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)},
|
||||
groups='base.group_user'),
|
||||
'inactivity_days': fields.function(_compute_day, string='Days since last action',
|
||||
multi='compute_day', type="integer", help="Difference in days between last action and current date",
|
||||
groups='base.group_user'),
|
||||
'color': fields.integer('Color Index'),
|
||||
'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
|
||||
'date_action_last': fields.datetime('Last Action', readonly=1),
|
||||
'date_action_next': fields.datetime('Next Action', readonly=1),
|
||||
'legend_blocked': fields.related("stage_id", "legend_blocked", type="char", string='Kanban Blocked Explanation'),
|
||||
'legend_done': fields.related("stage_id", "legend_done", type="char", string='Kanban Valid Explanation'),
|
||||
'legend_normal': fields.related("stage_id", "legend_normal", type="char", string='Kanban Ongoing Explanation'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'active': 1,
|
||||
'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
|
||||
'company_id': lambda s, cr, uid, c: s.pool['res.users']._get_company(cr, uid, context=c),
|
||||
'priority': '0',
|
||||
'kanban_state': 'normal',
|
||||
'date_last_stage_update': fields.datetime.now,
|
||||
'user_id': lambda obj, cr, uid, context: uid,
|
||||
}
|
||||
|
||||
_group_by_full = {
|
||||
'stage_id': _read_group_stage_ids
|
||||
}
|
||||
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
issue = self.read(cr, uid, [id], ['name'], context=context)[0]
|
||||
if not default:
|
||||
default = {}
|
||||
default = default.copy()
|
||||
default.update(name=_('%s (copy)') % (issue['name']))
|
||||
return super(project_issue, self).copy(cr, uid, id, default=default, context=context)
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
context = dict(context or {})
|
||||
if vals.get('project_id') and not context.get('default_project_id'):
|
||||
context['default_project_id'] = vals.get('project_id')
|
||||
if vals.get('user_id') and not vals.get('date_open'):
|
||||
vals['date_open'] = fields.datetime.now()
|
||||
if 'stage_id' in vals:
|
||||
vals.update(self.onchange_stage_id(cr, uid, None, vals.get('stage_id'), context=context)['value'])
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
create_context = dict(context, mail_create_nolog=True)
|
||||
return super(project_issue, self).create(cr, uid, vals, context=create_context)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
# stage change: update date_last_stage_update
|
||||
if 'stage_id' in vals:
|
||||
vals.update(self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value'])
|
||||
vals['date_last_stage_update'] = fields.datetime.now()
|
||||
if 'kanban_state' not in vals:
|
||||
vals['kanban_state'] = 'normal'
|
||||
# user_id change: update date_open
|
||||
if vals.get('user_id') and 'date_open' not in vals:
|
||||
vals['date_open'] = fields.datetime.now()
|
||||
|
||||
return super(project_issue, self).write(cr, uid, ids, vals, context)
|
||||
|
||||
def onchange_task_id(self, cr, uid, ids, task_id, context=None):
|
||||
if not task_id:
|
||||
return {'value': {}}
|
||||
task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
|
||||
return {'value': {'user_id': task.user_id.id, }}
|
||||
|
||||
def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
|
||||
""" This function returns value of partner email address based on partner
|
||||
:param part: Partner's id
|
||||
"""
|
||||
if partner_id:
|
||||
partner = self.pool['res.partner'].browse(cr, uid, partner_id, context)
|
||||
return {'value': {'email_from': partner.email}}
|
||||
return {'value': {'email_from': False}}
|
||||
|
||||
def get_empty_list_help(self, cr, uid, help, context=None):
|
||||
context = dict(context or {})
|
||||
context['empty_list_help_model'] = 'project.project'
|
||||
context['empty_list_help_id'] = context.get('default_project_id')
|
||||
context['empty_list_help_document_name'] = _("issues")
|
||||
return super(project_issue, self).get_empty_list_help(cr, uid, help, context=context)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Stage management
|
||||
# -------------------------------------------------------
|
||||
|
||||
def onchange_stage_id(self, cr, uid, ids, stage_id, context=None):
|
||||
if not stage_id:
|
||||
return {'value': {}}
|
||||
stage = self.pool['project.task.type'].browse(cr, uid, stage_id, context=context)
|
||||
if stage.fold:
|
||||
return {'value': {'date_closed': fields.datetime.now()}}
|
||||
return {'value': {'date_closed': False}}
|
||||
|
||||
def stage_find(self, cr, uid, cases, project_id, domain=[], order='sequence', context=None):
|
||||
""" Override of the base.stage method
|
||||
Parameter of the stage search taken from the issue:
|
||||
- type: stage type must be the same or 'both'
|
||||
- project_id: if set, stages must belong to this project or
|
||||
be a default case
|
||||
"""
|
||||
if isinstance(cases, (int, long)):
|
||||
cases = self.browse(cr, uid, cases, context=context)
|
||||
# collect all project_ids
|
||||
project_ids = []
|
||||
if project_id:
|
||||
project_ids.append(project_id)
|
||||
for task in cases:
|
||||
if task.project_id:
|
||||
project_ids.append(task.project_id.id)
|
||||
# OR all project_ids and OR with case_default
|
||||
search_domain = []
|
||||
if project_ids:
|
||||
search_domain += [('|')] * (len(project_ids) - 1)
|
||||
for project_id in project_ids:
|
||||
search_domain.append(('project_ids', '=', project_id))
|
||||
search_domain += list(domain)
|
||||
# perform search, return the first found
|
||||
stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
|
||||
if stage_ids:
|
||||
return stage_ids[0]
|
||||
return False
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Mail gateway
|
||||
# -------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _track_template(self, tracking):
|
||||
res = super(project_issue, self)._track_template(tracking)
|
||||
test_issue = self[0]
|
||||
changes, dummy = tracking[test_issue.id]
|
||||
if 'stage_id' in changes and test_issue.stage_id.mail_template_id:
|
||||
res['stage_id'] = (test_issue.stage_id.mail_template_id, {'composition_mode': 'mass_mail'})
|
||||
return res
|
||||
|
||||
def _track_subtype(self, cr, uid, ids, init_values, context=None):
|
||||
record = self.browse(cr, uid, ids[0], context=context)
|
||||
if 'kanban_state' in init_values and record.kanban_state == 'blocked':
|
||||
return 'project_issue.mt_issue_blocked'
|
||||
elif 'kanban_state' in init_values and record.kanban_state == 'done':
|
||||
return 'project_issue.mt_issue_ready'
|
||||
elif 'user_id' in init_values and record.user_id: # assigned -> new
|
||||
return 'project_issue.mt_issue_new'
|
||||
elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1: # start stage -> new
|
||||
return 'project_issue.mt_issue_new'
|
||||
elif 'stage_id' in init_values:
|
||||
return 'project_issue.mt_issue_stage'
|
||||
return super(project_issue, self)._track_subtype(cr, uid, ids, init_values, context=context)
|
||||
|
||||
def _notification_group_recipients(self, cr, uid, ids, message, recipients, done_ids, group_data, context=None):
|
||||
""" 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.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'project.group_project_user')
|
||||
group_user = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'base.group_user')
|
||||
for recipient in recipients:
|
||||
if recipient.id in done_ids:
|
||||
continue
|
||||
if recipient.user_ids and group_project_user in recipient.user_ids[0].groups_id.ids:
|
||||
group_data['group_project_user'] |= recipient
|
||||
elif not recipient.user_ids:
|
||||
group_data['partner'] |= recipient
|
||||
else:
|
||||
group_data['user'] |= recipient
|
||||
done_ids.add(recipient.id)
|
||||
return super(project_issue, self)._notification_group_recipients(cr, uid, ids, message, recipients, done_ids, group_data, context=context)
|
||||
|
||||
def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None):
|
||||
res = super(project_issue, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context)
|
||||
|
||||
new_action_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'project_issue.project_issue_categ_act0')
|
||||
take_action = self._notification_link_helper(cr, uid, ids, 'assign', context=context)
|
||||
new_action = self._notification_link_helper(cr, uid, ids, 'new', context=context, action_id=new_action_id)
|
||||
|
||||
task_record = self.browse(cr, uid, ids[0], context=context)
|
||||
actions = []
|
||||
if not task_record.user_id:
|
||||
actions.append({'url': take_action, 'title': _('I take it')})
|
||||
else:
|
||||
actions.append({'url': new_action, 'title': _('New Issue')})
|
||||
|
||||
res['group_project_user'] = {
|
||||
'actions': actions
|
||||
}
|
||||
return res
|
||||
|
||||
@api.cr_uid_context
|
||||
def message_get_reply_to(self, cr, uid, ids, default=None, context=None):
|
||||
""" Override to get the reply_to of the parent project. """
|
||||
issues = self.browse(cr, SUPERUSER_ID, ids, context=context)
|
||||
project_ids = set([issue.project_id.id for issue in issues if issue.project_id])
|
||||
aliases = self.pool['project.project'].message_get_reply_to(cr, uid, list(project_ids), default=default, context=context)
|
||||
return dict((issue.id, aliases.get(issue.project_id and issue.project_id.id or 0, False)) for issue in issues)
|
||||
|
||||
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
|
||||
recipients = super(project_issue, self).message_get_suggested_recipients(cr, uid, ids, context=context)
|
||||
try:
|
||||
for issue in self.browse(cr, uid, ids, context=context):
|
||||
if issue.partner_id:
|
||||
issue._message_add_suggested_recipient(recipients, partner=issue.partner_id, reason=_('Customer'))
|
||||
elif issue.email_from:
|
||||
issue._message_add_suggested_recipient(recipients, email=issue.email_from, reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def email_split(self, cr, uid, ids, msg, context=None):
|
||||
email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or ''))
|
||||
# check left-part is not already an alias
|
||||
issue_ids = self.browse(cr, uid, ids, context=context)
|
||||
aliases = [issue.project_id.alias_name for issue in issue_ids if issue.project_id]
|
||||
return filter(lambda x: x.split('@')[0] not in aliases, email_list)
|
||||
|
||||
def message_new(self, cr, uid, msg, custom_values=None, context=None):
|
||||
""" Overrides mail_thread message_new that is called by the mailgateway
|
||||
through message_process.
|
||||
This override updates the document according to the email.
|
||||
"""
|
||||
# remove default author when going through the mail gateway. Indeed we
|
||||
# do not want to explicitly set user_id to False; however we do not
|
||||
# want the gateway user to be responsible if no other responsible is
|
||||
# found.
|
||||
create_context = dict(context or {})
|
||||
create_context['default_user_id'] = False
|
||||
if custom_values is None:
|
||||
custom_values = {}
|
||||
context = dict(context or {}, state_to='draft')
|
||||
defaults = {
|
||||
'name': msg.get('subject') or _("No Subject"),
|
||||
'email_from': msg.get('from'),
|
||||
'email_cc': msg.get('cc'),
|
||||
'partner_id': msg.get('author_id', False),
|
||||
}
|
||||
defaults.update(custom_values)
|
||||
|
||||
res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=create_context)
|
||||
email_list = self.email_split(cr, uid, [res_id], msg, context=context)
|
||||
partner_ids = filter(None, self._find_partner_from_emails(cr, uid, [res_id], email_list, force_create=False, context=context))
|
||||
self.message_subscribe(cr, uid, [res_id], partner_ids, context=context)
|
||||
return res_id
|
||||
|
||||
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
|
||||
""" Override to update the issue according to the email. """
|
||||
|
||||
email_list = self.email_split(cr, uid, ids, msg, context=context)
|
||||
partner_ids = filter(None, self._find_partner_from_emails(cr, uid, ids, email_list, force_create=False, context=context))
|
||||
self.message_subscribe(cr, uid, ids, partner_ids, context=context)
|
||||
return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
|
||||
|
||||
@api.cr_uid_ids_context
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, cr, uid, thread_id, subtype=None, context=None, **kwargs):
|
||||
""" Overrides mail_thread message_post so that we can set the date of last action field when
|
||||
a new message is posted on the issue.
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(project_issue, self).message_post(cr, uid, thread_id, subtype=subtype, context=context, **kwargs)
|
||||
if thread_id and subtype:
|
||||
self.write(cr, SUPERUSER_ID, thread_id, {'date_action_last': fields.datetime.now()}, context=context)
|
||||
return res
|
||||
|
||||
def message_get_email_values(self, cr, uid, ids, notif_mail=None, context=None):
|
||||
res = super(project_issue, self).message_get_email_values(cr, uid, ids, notif_mail=notif_mail, context=context)
|
||||
current_issue = self.browse(cr, uid, ids[0], context=context)
|
||||
headers = {}
|
||||
if res.get('headers'):
|
||||
try:
|
||||
headers.update(eval(res['headers']))
|
||||
except Exception:
|
||||
pass
|
||||
if current_issue.project_id:
|
||||
current_objects = filter(None, headers.get('X-Odoo-Objects', '').split(','))
|
||||
current_objects.insert(0, 'project.project-%s, ' % current_issue.project_id.id)
|
||||
headers['X-Odoo-Objects'] = ','.join(current_objects)
|
||||
if current_issue.tag_ids:
|
||||
headers['X-Odoo-Tags'] = ','.join([tag.name for tag in current_issue.tag_ids])
|
||||
res['headers'] = repr(headers)
|
||||
return res
|
||||
|
||||
|
||||
class project(osv.Model):
|
||||
_inherit = "project.project"
|
||||
|
||||
def _get_alias_models(self, cr, uid, context=None):
|
||||
res = super(project, self)._get_alias_models(cr, uid, context=context)
|
||||
res.append(("project.issue", "Issues"))
|
||||
return res
|
||||
|
||||
def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
Issue = self.pool['project.issue']
|
||||
return {
|
||||
project_id: Issue.search_count(cr,uid, [('project_id', '=', project_id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)], context=context)
|
||||
for project_id in ids
|
||||
}
|
||||
|
||||
def _issue_needaction_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
Issue = self.pool['project.issue']
|
||||
res = dict.fromkeys(ids, 0)
|
||||
projects = Issue.read_group(cr, uid, [('project_id', 'in', ids), ('message_needaction', '=', True)], ['project_id'], ['project_id'], context=context)
|
||||
res.update({project['project_id'][0]: int(project['project_id_count']) for project in projects})
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'issue_count': fields.function(_issue_count, type='integer', string="Issues",),
|
||||
'issue_ids': fields.one2many('project.issue', 'project_id', string="Issues",
|
||||
domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)]),
|
||||
'issue_needaction_count': fields.function(_issue_needaction_count, type='integer', string="Issues",),
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(project, self).write(vals)
|
||||
if 'active' in vals:
|
||||
# archiving/unarchiving a project does it on its issues, too
|
||||
issues = self.with_context(active_test=False).mapped('issue_ids')
|
||||
issues.write({'active': vals['active']})
|
||||
return res
|
||||
|
||||
|
||||
class account_analytic_account(osv.Model):
|
||||
_inherit = 'account.analytic.account'
|
||||
_description = 'Analytic Account'
|
||||
|
||||
_columns = {
|
||||
'use_issues': fields.boolean('Use Issues', help="Check this box to manage customer activities through this project"),
|
||||
}
|
||||
|
||||
def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
|
||||
res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
|
||||
if template_id and 'value' in res:
|
||||
template = self.browse(cr, uid, template_id, context=context)
|
||||
res['value']['use_issues'] = template.use_issues
|
||||
return res
|
||||
|
||||
def _trigger_project_creation(self, cr, uid, vals, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
|
||||
return res or (vals.get('use_issues') and not 'project_creation_in_progress' in context)
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
proj_ids = self.pool['project.project'].search(cr, uid, [('analytic_account_id', 'in', ids)])
|
||||
has_issues = self.pool['project.issue'].search(cr, uid, [('project_id', 'in', proj_ids)], count=True, context=context)
|
||||
if has_issues:
|
||||
raise UserError(_('Please remove existing issues in the project linked to the accounts you want to delete.'))
|
||||
return super(account_analytic_account, self).unlink(cr, uid, ids, context=context)
|
||||
|
||||
|
||||
class project_project(osv.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
_columns = {
|
||||
'label_issues': fields.char('Use Issues as', help="Customize the issues label, for example to call them cases."),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'use_issues': True,
|
||||
'label_issues': 'Issues',
|
||||
}
|
||||
|
||||
def _check_create_write_values(self, cr, uid, vals, context=None):
|
||||
""" Perform some check on values given to create or write. """
|
||||
# Handle use_tasks / use_issues: if only one is checked, alias should take the same model
|
||||
if vals.get('use_tasks') and not vals.get('use_issues'):
|
||||
vals['alias_model'] = 'project.task'
|
||||
elif vals.get('use_issues') and not vals.get('use_tasks'):
|
||||
vals['alias_model'] = 'project.issue'
|
||||
|
||||
def on_change_use_tasks_or_issues(self, cr, uid, ids, use_tasks, use_issues, context=None):
|
||||
values = {}
|
||||
if use_tasks and not use_issues:
|
||||
values['alias_model'] = 'project.task'
|
||||
elif not use_tasks and use_issues:
|
||||
values['alias_model'] = 'project.issue'
|
||||
return {'value': values}
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
self._check_create_write_values(cr, uid, vals, context=context)
|
||||
return super(project_project, self).create(cr, uid, vals, context=context)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
self._check_create_write_values(cr, uid, vals, context=context)
|
||||
return super(project_project, self).write(cr, uid, ids, vals, context=context)
|
||||
|
||||
class res_partner(osv.osv):
|
||||
def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
|
||||
Issue = self.pool['project.issue']
|
||||
partners = {id: self.search(cr, uid, [('id', 'child_of', ids)]) for id in ids}
|
||||
return {
|
||||
partner_id: Issue.search_count(cr, uid, [('partner_id', 'in', partners[partner_id])])
|
||||
for partner_id in partners.keys()
|
||||
}
|
||||
|
||||
""" Inherits partner and adds Issue information in the partner form """
|
||||
_inherit = 'res.partner'
|
||||
_columns = {
|
||||
'issue_count': fields.function(_issue_count, string='# Issues', type='integer'),
|
||||
}
|
||||
@@ -1,38 +1,33 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp import tools
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class project_issue_report(osv.osv):
|
||||
class ProjectIssueReport(models.Model):
|
||||
_name = "project.issue.report"
|
||||
_auto = False
|
||||
|
||||
_columns = {
|
||||
'company_id': fields.many2one('res.company', 'Company', readonly=True),
|
||||
'opening_date': fields.datetime('Date of Opening', readonly=True),
|
||||
'create_date': fields.datetime('Create Date', readonly=True),
|
||||
'date_closed': fields.datetime('Date of Closing', readonly=True),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True),
|
||||
'stage_id': fields.many2one('project.task.type', 'Stage'),
|
||||
'nbr': fields.integer('# of Issues', readonly=True), # TDE FIXME master: rename into nbr_issues
|
||||
'working_hours_open': fields.float('Avg. Working Hours to Open', readonly=True, group_operator="avg"),
|
||||
'working_hours_close': fields.float('Avg. Working Hours to Close', readonly=True, group_operator="avg"),
|
||||
'delay_open': fields.float('Avg. Delay to Open', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to open the project issue."),
|
||||
'delay_close': fields.float('Avg. Delay to Close', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the project issue"),
|
||||
'company_id' : fields.many2one('res.company', 'Company'),
|
||||
'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority'),
|
||||
'project_id':fields.many2one('project.project', 'Project',readonly=True),
|
||||
'user_id' : fields.many2one('res.users', 'Assigned to',readonly=True),
|
||||
'partner_id': fields.many2one('res.partner','Contact'),
|
||||
'channel': fields.char('Channel', readonly=True, help="Communication Channel."),
|
||||
'task_id': fields.many2one('project.task', 'Task'),
|
||||
'email': fields.integer('# Emails', size=128, readonly=True),
|
||||
}
|
||||
company_id = fields.Many2one('res.company', 'Company', readonly=True)
|
||||
opening_date = fields.Datetime('Date of Opening', readonly=True)
|
||||
create_date = fields.Datetime('Create Date', readonly=True)
|
||||
date_closed = fields.Datetime('Date of Closing', readonly=True)
|
||||
date_last_stage_update = fields.Datetime('Last Stage Update', readonly=True)
|
||||
stage_id = fields.Many2one('project.task.type', 'Stage')
|
||||
nbr_issues = fields.Integer('# of Issues', readonly=True)
|
||||
working_hours_open = fields.Float('Avg. Working Hours to Open', readonly=True, group_operator="avg")
|
||||
working_hours_close = fields.Float('Avg. Working Hours to Close', readonly=True, group_operator="avg")
|
||||
delay_open = fields.Float('Avg. Delay to Open', digits=(16, 2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to open the project issue.")
|
||||
delay_close = fields.Float('Avg. Delay to Close', digits=(16, 2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the project issue")
|
||||
priority = fields.Selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')])
|
||||
project_id = fields.Many2one('project.project', 'Project', readonly=True)
|
||||
user_id = fields.Many2one('res.users', 'Assigned to', readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Contact')
|
||||
channel = fields.Char('Channel', readonly=True, help="Communication Channel.")
|
||||
task_id = fields.Many2one('project.task', 'Task')
|
||||
email = fields.Integer('# Emails', readonly=True)
|
||||
|
||||
def init(self, cr):
|
||||
tools.drop_view_if_exists(cr, 'project_issue_report')
|
||||
@@ -51,7 +46,7 @@ class project_issue_report(osv.osv):
|
||||
c.company_id as company_id,
|
||||
c.priority as priority,
|
||||
c.project_id as project_id,
|
||||
1 as nbr,
|
||||
1 as nbr_issues,
|
||||
c.partner_id,
|
||||
c.channel,
|
||||
c.task_id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<odoo>
|
||||
<record id="view_project_issue_report_pivot" model="ir.ui.view">
|
||||
<field name="name">project.issue.report.pivot</field>
|
||||
<field name="model">project.issue.report</field>
|
||||
@@ -8,7 +7,7 @@
|
||||
<pivot string="Project Issue" disable_linking="True">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="create_date" interval="month" type="col"/>
|
||||
<field name="nbr" type="measure"/>
|
||||
<field name="nbr_issues" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
@@ -20,7 +19,7 @@
|
||||
<graph string="Project Issue">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="create_date" interval="month" type="col"/>
|
||||
<field name="nbr" type="measure"/>
|
||||
<field name="nbr_issues" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
@@ -93,9 +92,6 @@
|
||||
<field name="help">This report on the project issues allows you to analyse the quality of your support or after-sales services. You can track the issues per age. You can analyse the time required to open or close an issue, the number of email to exchange and the time spent on average by issues.</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_project_issue_report" id="menu_project_issue_report_tree"
|
||||
<menuitem id="menu_project_issue_report_tree" action="action_project_issue_report"
|
||||
parent="base.menu_project_report"/>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record model="ir.rule" id="issue_project_manager_rule">
|
||||
<record id="issue_project_manager_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: project manager: see all</field>
|
||||
<field name="model_id" ref="model_project_issue"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="issue_user_rule">
|
||||
<record id="issue_user_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: employees: employee or (followers and following)</field>
|
||||
<field name="model_id" ref="model_project_issue"/>
|
||||
<field name="domain_force">['|',
|
||||
@@ -46,4 +46,4 @@
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
-
|
||||
Test the whole create project issue with project manager.
|
||||
-
|
||||
!context
|
||||
uid: 'res_users_project_issue_manager'
|
||||
-
|
||||
!record {model: project.issue, id: project_task_1, view: False}:
|
||||
task_id: 'project.project_task_17'
|
||||
name: 'Error in account module'
|
||||
-
|
||||
!record {model: project.issue, id: project01, view: False}:
|
||||
project_id: 'project.project_project_2'
|
||||
name: 'OpenERP Integration'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
-
|
||||
I send mail to get more details. TODO revert mail.mail to mail.compose.message (conversion to customer should be automatic).
|
||||
-
|
||||
!python {model: mail.mail }: |
|
||||
ctx = context.copy()
|
||||
ctx.update({'active_model': 'project.issue', 'active_id': ref("crm_case_buginaccountsmodule0"), 'active_ids': [ref("crm_case_buginaccountsmodule0")]})
|
||||
vals = self.default_get(cr, uid , [], context=ctx)
|
||||
try:
|
||||
new_id = self.create(cr, uid, {'email_from': 'support@mycompany.com','email_to': 'Robert_Adersen@yahoo.com', 'subject': 'Regarding error in account module we nees more details'})
|
||||
self.send_mail(cr, uid, [new_id], context=ctx)
|
||||
except Exception, e:
|
||||
pass
|
||||
@@ -1,28 +0,0 @@
|
||||
-
|
||||
Create a user as 'Project manager'
|
||||
-
|
||||
!record {model: res.users, id: res_users_project_issue_manager, view: False}:
|
||||
company_id: base.main_company
|
||||
name: Project Manager
|
||||
login: prim
|
||||
email: issuemanager@yourcompany.com
|
||||
-
|
||||
I added groups for Project manager.
|
||||
-
|
||||
!record {model: res.users, id: res_users_project_issue_manager}:
|
||||
groups_id:
|
||||
- project.group_project_manager
|
||||
-
|
||||
Create a user as 'Project user'
|
||||
-
|
||||
!record {model: res.users, id: res_users_project_issue_user, view: False}:
|
||||
company_id: base.main_company
|
||||
name: Project User
|
||||
login: priu
|
||||
email: issueuser@yourcompany.com
|
||||
-
|
||||
I added groups for Project user.
|
||||
-
|
||||
!record {model: res.users, id: res_users_project_issue_user}:
|
||||
groups_id:
|
||||
- project.group_project_user
|
||||
@@ -1,16 +0,0 @@
|
||||
-
|
||||
In Order to test process of Issue in OpenERP, Customer send the issue by email.
|
||||
-
|
||||
!python {model: mail.thread}: |
|
||||
request_file = open(openerp.modules.module.get_module_resource('project_issue','test', 'issue.eml'),'rb')
|
||||
request_message = request_file.read()
|
||||
self.message_process(cr, uid, 'project.issue', request_message)
|
||||
-
|
||||
After getting the mail, I check details of new issue of that customer.
|
||||
-
|
||||
!python {model: project.issue}: |
|
||||
issue_ids = self.search(cr, uid, [('email_from', '=', 'Robert Adersen <Robert_Adersen@yahoo.com>')])
|
||||
assert issue_ids and len(issue_ids), "issue is not created after getting request"
|
||||
issue = self.browse(cr, uid, issue_ids[0], context=context)
|
||||
assert not issue.partner_id, "Customer should be a new"
|
||||
assert issue.name == "Error in the account module", "Subject does not match"
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import test_subscribe_issue
|
||||
import test_issue_process
|
||||
import test_issue_demo
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestIssueUsers(TransactionCase):
|
||||
"""Tests for unit of res users"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestIssueUsers, self).setUp()
|
||||
self.ProjectIssue = self.env["project.issue"]
|
||||
|
||||
# Created a user as 'Project Manager'
|
||||
# Added group for Project Manager.
|
||||
self.project_manager = self.env["res.users"].create({
|
||||
'name': 'Project Manager',
|
||||
'login': 'prim',
|
||||
'password': 'prim',
|
||||
'email': 'issuemanager@yourcompany.com',
|
||||
'groups_id': [(6, 0, [self.ref('project.group_project_manager')])]
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.project_issue.tests.common import TestIssueUsers
|
||||
|
||||
|
||||
class TestIssueDemo(TestIssueUsers):
|
||||
|
||||
def test_issue_demo(self):
|
||||
# Test the whole create project issue with project manager.
|
||||
self.ProjectIssue.sudo(self.project_manager.id).create({
|
||||
'name': 'Error in account module',
|
||||
'task_id': self.ref('project.project_task_17')
|
||||
})
|
||||
|
||||
self.ProjectIssue.sudo(self.project_manager.id).create({
|
||||
'name': 'Odoo Integration',
|
||||
'project_id': self.ref('project.project_project_2')
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.project_issue.tests.common import TestIssueUsers
|
||||
|
||||
|
||||
class TestIssueProcess(TestIssueUsers):
|
||||
|
||||
def test_issue_process(self):
|
||||
# Sending mail to get more details.
|
||||
|
||||
vals = {'email_from': 'support@mycompany.com',
|
||||
'email_to': 'Robert_Adersen@yahoo.com',
|
||||
'subject': 'We need more details regarding your issue in HR module',
|
||||
'body_html': """
|
||||
<p>
|
||||
Hello Mr. Adersen,
|
||||
</p>
|
||||
<p>
|
||||
We need more details about your issue in the HR module. Could you please
|
||||
send us complete details about the error eg. error message, traceback
|
||||
or what operations you were doing when you the error occured ?
|
||||
</p>
|
||||
<p>
|
||||
Thank You.
|
||||
</p>
|
||||
<pre>
|
||||
--
|
||||
YourCompany
|
||||
info@yourcompany.example.com
|
||||
+1 555 123 8069
|
||||
</pre>
|
||||
"""}
|
||||
crm_bug_id = self.ref('project_issue.crm_case_buginaccountsmodule0')
|
||||
mail = self.env["mail.mail"].with_context(active_model='project.issue',
|
||||
active_id=crm_bug_id,
|
||||
active_ids=[crm_bug_id]).create(vals)
|
||||
mail.send()
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.project_issue.tests.common import TestIssueUsers
|
||||
from odoo.modules import module
|
||||
|
||||
|
||||
class TestSubscribeIssue(TestIssueUsers):
|
||||
|
||||
def test_subscribe_issues(self):
|
||||
# In Order to test process of Issue in Odoo, Customer send the issue by email.
|
||||
request_file = open(module.get_module_resource('project_issue', 'tests', 'issue.eml'), 'rb')
|
||||
request_message = request_file.read()
|
||||
self.env["mail.thread"].message_process('project.issue', request_message)
|
||||
|
||||
# After getting the mail, Check details of new issue of that customer.
|
||||
issue = self.ProjectIssue.search([('email_from', '=', 'Robert Adersen <Robert_Adersen@yahoo.com>')], limit=1)
|
||||
self.assertEquals(len(issue), 1, "Issue is not created after getting request")
|
||||
self.assertFalse(issue.partner_id, "Customer should be a new")
|
||||
self.assertEquals(issue.name, "Error in the account module", "Subject does not match")
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="analytic_account_inherited_issue_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.issue.form.inherit</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="project.analytic_account_inherited_form"/>
|
||||
<field name="priority" eval="18"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="project"]' position='inside'>
|
||||
<field name="use_issues" groups="base.group_no_one"/>
|
||||
<label for="use_issues" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_project_kanban_inherited" model="ir.ui.view">
|
||||
<field name="name">project.project.kanban.inherited</field>
|
||||
+11
-14
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
ALL BUGS
|
||||
-->
|
||||
<record model="ir.actions.act_window" id="project_issue_categ_act0">
|
||||
<record id="project_issue_categ_act0" model="ir.actions.act_window">
|
||||
<field name="name">Issues</field>
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="view_type">form</field>
|
||||
@@ -16,49 +14,48 @@
|
||||
<field name="search_view_id" ref="view_project_issue_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
The Odoo issues tacker allows you to efficiently manage things
|
||||
The Odoo issues tracker allows you to efficiently manage things
|
||||
like internal requests, software development bugs, customer
|
||||
complaints, project troubles, material breakdowns, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_crm_tag_kanban_view0">
|
||||
<record id="action_crm_tag_kanban_view0" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="project_issue_kanban_view"/>
|
||||
<field name="act_window_id" ref="project_issue_categ_act0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_crm_tag_tree_view0">
|
||||
<record id="action_crm_tag_tree_view0" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="project_issue_tree_view"/>
|
||||
<field name="act_window_id" ref="project_issue_categ_act0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_crm_tag_calendar_view0">
|
||||
<record id="action_crm_tag_calendar_view0" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="view_mode">calendar</field>
|
||||
<field name="view_id" ref="project_issue_calendar_view"/>
|
||||
<field name="act_window_id" ref="project_issue_categ_act0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_crm_tag_form_view0">
|
||||
<record id="action_crm_tag_form_view0" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="project_issue_form_view"/>
|
||||
<field name="act_window_id" ref="project_issue_categ_act0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window.view" id="action_crm_tag_graph_view0">
|
||||
<record id="action_crm_tag_graph_view0" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="4"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="project_issue_graph_view"/>
|
||||
<field name="act_window_id" ref="project_issue_categ_act0"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Issues" id="menu_project_issue_track" parent="project.menu_project_management"
|
||||
<menuitem id="menu_project_issue_track" name="Issues" parent="project.menu_project_management"
|
||||
action="project_issue_categ_act0" sequence="15"/>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
+43
-115
@@ -1,9 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_form_view">
|
||||
<field name="name">Project Issue Tracker Form</field>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="project_issue_form_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.form.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
@@ -30,11 +28,10 @@
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="partner_id" on_change="onchange_partner_id(partner_id)"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="email_from"/>
|
||||
<field name="project_id" on_change="on_change_project(project_id)" context="{'default_use_issues':1}"
|
||||
groups="base.group_user"/>
|
||||
<field name="task_id" groups="base.group_no_one" on_change="onchange_task_id(task_id)" context="{'default_project_id':project_id}"/>
|
||||
<field name="project_id" context="{'default_use_issues':1}" groups="base.group_user"/>
|
||||
<field name="task_id" groups="base.group_no_one" context="{'default_project_id':project_id}"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
@@ -67,8 +64,8 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_tree_view">
|
||||
<field name="name">Project Issue Tracker Tree</field>
|
||||
<record id="project_issue_tree_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.tree.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Issue Tracker Tree" decoration-bf="message_needaction==True">
|
||||
@@ -78,7 +75,7 @@
|
||||
<field name="partner_id"/>
|
||||
<field name="project_id" groups="base.group_user"/>
|
||||
<field name="priority" string="Priority" groups="base.group_user"/>
|
||||
<field name="create_date"/>
|
||||
<field name="create_date" string="Creation Date"/>
|
||||
<field name="user_id"/>
|
||||
<field name="stage_id" widget="selection" readonly="1"/>
|
||||
<field name="tag_ids" invisible="1"/>
|
||||
@@ -88,7 +85,7 @@
|
||||
</record>
|
||||
|
||||
<record id="view_project_issue_filter" model="ir.ui.view">
|
||||
<field name="name">Project Issue Tracker Search</field>
|
||||
<field name="name">project.issue.search.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Issue Tracker Search">
|
||||
@@ -121,8 +118,8 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_calendar_view">
|
||||
<field name="name">Project Issue Tracker Calendar</field>
|
||||
<record id="project_issue_calendar_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.calendar.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="priority" eval="2"/>
|
||||
<field name="arch" type="xml">
|
||||
@@ -136,8 +133,8 @@
|
||||
|
||||
<!-- Project Issue Karban View-->
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_kanban_view">
|
||||
<field name="name">Project Issue Kanban</field>
|
||||
<record id="project_issue_kanban_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.kanban.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="stage_id" class="o_kanban_small_column">
|
||||
@@ -198,8 +195,8 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_pivot_view">
|
||||
<field name="name">Project Issue Tracker Pivot</field>
|
||||
<record id="project_issue_pivot_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.pivot.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Project Issues">
|
||||
@@ -209,8 +206,8 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="project_issue_graph_view">
|
||||
<field name="name">Project Issue Tracker Graph</field>
|
||||
<record id="project_issue_graph_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.graph.view</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Project Issues" type="bar">
|
||||
@@ -222,8 +219,8 @@
|
||||
|
||||
|
||||
<!-- Feature Requests -->
|
||||
<record model="ir.ui.view" id="project_feature_tree_view">
|
||||
<field name="name">Project Issue- Feature Tracker Tree</field>
|
||||
<record id="project_feature_tree_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.tree.view.featured</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Feature Tracker Tree" decoration-bf="message_needaction==True">
|
||||
@@ -250,59 +247,13 @@
|
||||
</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
The Odoo issues tacker allows you to efficiently manage things
|
||||
The Odoo issues tracker allows you to efficiently manage things
|
||||
like internal requests, software development bugs, customer
|
||||
complaints, project troubles, material breakdowns, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Project -->
|
||||
<record id="view_project_form_inherited" model="ir.ui.view">
|
||||
<field name="name">project.project.form.inherited</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//field[@name="use_tasks"]' position='attributes'>
|
||||
<attribute name="on_change">on_change_use_tasks_or_issues(use_tasks, use_issues)</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//div[@name="options_active"]' position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//div[@name="options_active"]' position='inside'>
|
||||
<div>
|
||||
<field name="use_issues" class="oe_inline"
|
||||
on_change="on_change_use_tasks_or_issues(use_tasks, use_issues)"/>
|
||||
<label for="use_issues" class="oe_inline" string="Issues"/>
|
||||
<span attrs="{'invisible':[('use_issues', '=', False)]}">as </span>
|
||||
<field name="label_issues" class="oe_inline oe_input_align" attrs="{'invisible': [('use_issues', '=', False)]}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="action" attrs="{'invisible':[('use_issues','=', False)]}"
|
||||
name="%(act_project_project_2_project_issue_all)d" icon="fa-bug">
|
||||
<field string="Issues" name="issue_count" widget="statinfo" options="{'label_field': 'label_issues'}"/>
|
||||
</button>
|
||||
</div>
|
||||
<xpath expr='//field[@name="use_tasks"]' position="attributes">
|
||||
<attribute name="attrs">{'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="analytic_account_inherited_issue_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.issue.form.inherit</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="project.analytic_account_inherited_form"/>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="project"]' position='inside'>
|
||||
<field name="use_issues" groups="base.group_no_one"/>
|
||||
<label for="use_issues" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_issues" model="ir.actions.act_window">
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="view_type">form</field>
|
||||
@@ -310,55 +261,32 @@
|
||||
<field name="view_mode">kanban,tree,form,calendar,graph</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
The Odoo issues tacker allows you to efficiently manage things
|
||||
The Odoo issues tracker allows you to efficiently manage things
|
||||
like internal requests, software development bugs, customer
|
||||
complaints, project troubles, material breakdowns, etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- res.partner -->
|
||||
<record model="ir.ui.view" id="res_partner_issues_button_view">
|
||||
<field name="name">res.partner.issues.button.view</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="groups_id" eval="[(4, ref('project.group_project_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="action" name="%(action_view_issues)d"
|
||||
context="{'search_default_partner_id': active_id, 'default_partner_id': active_id}"
|
||||
attrs="{'invisible': [('customer', '=', False)]}"
|
||||
icon="fa-bug">
|
||||
<field string="Issues" name="issue_count" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Portal -->
|
||||
<record id="open_view_issue" model="ir.actions.act_window">
|
||||
<field name="name">Issues</field>
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="project_issue.project_issue_tree_view"/>
|
||||
<field name="domain" eval=""/>
|
||||
<field name="context">{"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'}</field>
|
||||
<field name="search_view_id" ref="project_issue.view_project_issue_filter"/>
|
||||
<field name="target">current</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create an issue.
|
||||
</p><p>
|
||||
You can track your issues from this menu and the action we will take.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Portal -->
|
||||
<record id="open_view_issue" model="ir.actions.act_window">
|
||||
<field name="name">Issues</field>
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="project_issue.project_issue_tree_view"/>
|
||||
<field name="domain" eval=""/>
|
||||
<field name="context">{"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'}</field>
|
||||
<field name="search_view_id" ref="project_issue.view_project_issue_filter"/>
|
||||
<field name="target">current</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create an issue.
|
||||
</p><p>
|
||||
You can track your issues from this menu and the action we
|
||||
will take.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Issues" id="portal_services_issues" parent="portal.portal_projects"
|
||||
action="open_view_issue" sequence="10"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
<menuitem id="portal_services_issues" name="Issues" parent="portal.portal_projects"
|
||||
action="open_view_issue" sequence="10"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Project -->
|
||||
<record id="view_project_form_inherited" model="ir.ui.view">
|
||||
<field name="name">project.project.form.inherited</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="options_active"]' position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//div[@name="options_active"]' position='inside'>
|
||||
<div>
|
||||
<field name="use_issues" class="oe_inline"/>
|
||||
<label for="use_issues" class="oe_inline" string="Issues"/>
|
||||
<span attrs="{'invisible':[('use_issues', '=', False)]}">as </span>
|
||||
<field name="label_issues" class="oe_inline oe_input_align" attrs="{'invisible': [('use_issues', '=', False)]}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="action" attrs="{'invisible':[('use_issues','=', False)]}"
|
||||
name="%(act_project_project_2_project_issue_all)d" icon="fa-bug">
|
||||
<field string="Issues" name="issue_count" widget="statinfo" options="{'label_field': 'label_issues'}"/>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- res.partner -->
|
||||
<record id="res_partner_issues_button_view" model="ir.ui.view">
|
||||
<field name="name">res.partner.issues.button.view.inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="groups_id" eval="[(4, ref('project.group_project_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="action" name="%(action_view_issues)d"
|
||||
context="{'search_default_partner_id': active_id, 'default_partner_id': active_id}"
|
||||
attrs="{'invisible': [('customer', '=', False)]}"
|
||||
icon="fa-bug">
|
||||
<field name="issue_count" string="Issues" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project_issue_sheet
|
||||
import models
|
||||
|
||||
@@ -17,11 +17,8 @@ Worklogs can be maintained to signify number of hours spent by users to handle a
|
||||
'hr_timesheet_sheet',
|
||||
],
|
||||
'data': [
|
||||
'project_issue_sheet_view.xml',
|
||||
'views/project_issue_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/portal_security.xml',
|
||||
],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import project_issue
|
||||
import account_analytic_line
|
||||
@@ -0,0 +1,9 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
issue_id = fields.Many2one('project.issue', 'Issue')
|
||||
@@ -0,0 +1,21 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class ProjectIssue(models.Model):
|
||||
_inherit = 'project.issue'
|
||||
|
||||
progress = fields.Float(related='task_id.progress', store=True, string='Progress (%)', multi='line_id', group_operator="avg", help="Computed as: Time Spent / Total Time.")
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'issue_id', 'Timesheets')
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account')
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project_id(self):
|
||||
super(ProjectIssue, self)._onchange_project_id()
|
||||
if not self.project_id:
|
||||
self.analytic_account_id = False
|
||||
else:
|
||||
account = self.project_id.analytic_account_id
|
||||
if account:
|
||||
self.analytic_account_id = account.id
|
||||
@@ -1,53 +0,0 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.addons.analytic.models import analytic
|
||||
from openerp.osv import fields,osv,orm
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class project_issue(osv.osv):
|
||||
_inherit = 'project.issue'
|
||||
_description = 'project issue'
|
||||
|
||||
def _hours_get(self, cr, uid, ids, field_names, args, context=None):
|
||||
res = {}
|
||||
for issue in self.browse(cr, uid, ids, context=context):
|
||||
res[issue.id] = { 'progress' : issue.task_id.progress or 0.0 }
|
||||
return res
|
||||
|
||||
def _get_issue_task(self, cr, uid, task_ids, context=None):
|
||||
return self.pool['project.issue'].search(cr, uid, [('task_id', 'in', task_ids)], context=context)
|
||||
|
||||
_columns = {
|
||||
'progress': fields.function(_hours_get, string='Progress (%)', multi='line_id', group_operator="avg", help="Computed as: Time Spent / Total Time.",
|
||||
store = {
|
||||
'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['task_id'], 10),
|
||||
'project.task': (_get_issue_task, ['progress'], 10),
|
||||
}),
|
||||
'timesheet_ids': fields.one2many('account.analytic.line', 'issue_id', 'Timesheets'),
|
||||
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
|
||||
}
|
||||
|
||||
def on_change_project(self, cr, uid, ids, project_id, context=None):
|
||||
if not project_id:
|
||||
return {'value': {'analytic_account_id': False}}
|
||||
|
||||
result = super(project_issue, self).on_change_project(cr, uid, ids, project_id, context=context)
|
||||
|
||||
project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
|
||||
if 'value' not in result:
|
||||
result['value'] = {}
|
||||
|
||||
account = project.analytic_account_id
|
||||
if account:
|
||||
result['value']['analytic_account_id'] = account.id
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class account_analytic_line(osv.osv):
|
||||
_inherit = 'account.analytic.line'
|
||||
_description = 'account analytic line'
|
||||
_columns = {
|
||||
'issue_id' : fields.many2one('project.issue', 'Issue'),
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="portal_issue_sheet_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue Timesheets: frontend access</field>
|
||||
<field name="model_id" ref="analytic.model_account_analytic_line"/>
|
||||
<field name="domain_force">['|',
|
||||
'&',
|
||||
('issue_id.project_id.privacy_visibility', '=', 'portal'),
|
||||
('issue_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('issue_id.project_id.privacy_visibility', '=', 'followers'),
|
||||
('issue_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="domain_force">
|
||||
['|',
|
||||
'&',
|
||||
('issue_id.project_id.privacy_visibility', '=', 'portal'),
|
||||
('issue_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('issue_id.project_id.privacy_visibility', '=', 'followers'),
|
||||
('issue_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
|
||||
+5
-10
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="project_issue_form_view">
|
||||
<field name="name">Project Issue Tracker Form</field>
|
||||
<odoo>
|
||||
<record id="project_issue_form_view" model="ir.ui.view">
|
||||
<field name="name">project.issue.form.inherited</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="inherit_id" ref="project_issue.project_issue_form_view" />
|
||||
@@ -10,9 +9,6 @@
|
||||
<field name="task_id" position="after">
|
||||
<field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/>
|
||||
</field>
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="on_change">on_change_project(project_id)</attribute>
|
||||
</field>
|
||||
<xpath expr="//notebook/page[@name='extra_info']" position="before">
|
||||
<page string="Timesheets">
|
||||
<field name="timesheet_ids" colspan="4" nolabel="1" context="{'default_user_id' : uid, 'default_project_id': project_id}"
|
||||
@@ -31,7 +27,7 @@
|
||||
</record>
|
||||
|
||||
<record id="project_issue_tree_view_inherited" model="ir.ui.view">
|
||||
<field name="name">Project Issue Tracker Tree Inherited</field>
|
||||
<field name="name">projet.issue.tree.inherited</field>
|
||||
<field name="model">project.issue</field>
|
||||
<field name="inherit_id" ref="project_issue.project_issue_tree_view" />
|
||||
<field name="arch" type="xml">
|
||||
@@ -41,5 +37,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user