From 0b9a3f94fb866826da1d02e27abc185edbc59456 Mon Sep 17 00:00:00 2001 From: Gaurav Panchal Date: Tue, 19 Apr 2016 17:02:20 +0530 Subject: [PATCH 01/20] [REM] project: removed dead task delegation code Task delegation was removed at e485b6c5ee25d270ae4d4467b7dcaab18b8fadae. --- addons/project/project.py | 64 --------------------------------- addons/project/project_view.xml | 1 - 2 files changed, 65 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index 5c46c6a4957..14afdee9baf 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -276,7 +276,6 @@ class project(osv.osv): 'name': task.name} map_task_id[task.id] = task_obj.copy(cr, uid, task.id, defaults, context=context) self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]}) - task_obj.duplicate_task(cr, uid, map_task_id, context=context) return True def copy(self, cr, uid, id, default=None, context=None): @@ -438,15 +437,6 @@ class task(osv.osv): vals['date_start'] = fields.datetime.now() return {'value': vals} - def duplicate_task(self, cr, uid, map_ids, context=None): - mapper = lambda t: map_ids.get(t.id, t.id) - for task in self.browse(cr, uid, map_ids.values(), context): - new_child_ids = set(map(mapper, task.child_ids)) - new_parent_ids = set(map(mapper, task.parent_ids)) - if new_child_ids or new_parent_ids: - task.write({'parent_ids': [(6,0,list(new_parent_ids))], - 'child_ids': [(6,0,list(new_child_ids))]}) - def copy_data(self, cr, uid, id, default=None, context=None): if default is None: default = {} @@ -482,8 +472,6 @@ class task(osv.osv): 'date_deadline': fields.date('Deadline', select=True, copy=False), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True, copy=False, readonly=True), 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True), - 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'), - 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'), 'notes': fields.text('Notes'), 'planned_hours': fields.float('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('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."), @@ -516,36 +504,6 @@ class task(osv.osv): } _order = "priority desc, sequence, date_start, name, id" - def _check_recursion(self, cr, uid, ids, context=None): - for id in ids: - visited_branch = set() - visited_node = set() - res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context) - if not res: - return False - - return True - - def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None): - if id in visited_branch: #Cycle - return False - - if id in visited_node: #Already tested don't work one more time for nothing - return True - - visited_branch.add(id) - visited_node.add(id) - - #visit child using DFS - task = self.browse(cr, uid, id, context=context) - for child in task.child_ids: - res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context) - if not res: - return False - - visited_branch.remove(id) - return True - def _check_dates(self, cr, uid, ids, context=None): if context == None: context = {} @@ -558,7 +516,6 @@ class task(osv.osv): return True _constraints = [ - (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']), (_check_dates, 'Error ! Task starting date must be lower than its ending date.', ['date_start','date_end']) ] @@ -646,18 +603,6 @@ class task(osv.osv): return stage_ids[0] return False - def _check_child_task(self, cr, uid, ids, context=None): - if context == None: - context = {} - tasks = self.browse(cr, uid, ids, context=context) - for task in tasks: - if task.child_ids: - for child in task.child_ids: - if child.stage_id and not child.stage_id.fold: - raise UserError(_("Child task still open.\nPlease cancel or complete child task first.")) - return True - - def _store_history(self, cr, uid, ids, context=None): for task in self.browse(cr, uid, ids, context=context): self.pool.get('project.task.history').create(cr, uid, { @@ -711,13 +656,6 @@ class task(osv.osv): self._store_history(cr, uid, ids, context=context) return result - def unlink(self, cr, uid, ids, context=None): - if context is None: - context = {} - self._check_child_task(cr, uid, ids, context=context) - res = super(task, self).unlink(cr, uid, ids, context) - return res - def _get_total_hours(self): return self.remaining_hours @@ -913,8 +851,6 @@ class account_analytic_account(osv.osv): def create(self, cr, uid, vals, context=None): if context is None: context = {} - if vals.get('child_ids', False) and context.get('analytic_project_copy', False): - vals['child_ids'] = [] analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context) self.project_create(cr, uid, analytic_account_id, vals, context=context) return analytic_account_id diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index ba767839ed8..d00d7ef8719 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -171,7 +171,6 @@ project.project.tree project.project - child_ids From b4280b3bb57c871e2c193ca70761c346d91d41c8 Mon Sep 17 00:00:00 2001 From: Gaurav Panchal Date: Tue, 19 Apr 2016 16:58:04 +0530 Subject: [PATCH 02/20] [REM] project: removed dead code since Archive/Unarchive --- addons/project/project.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index 14afdee9baf..0761659058c 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -260,9 +260,6 @@ class project(osv.osv): def set_template(self, cr, uid, ids, context=None): return self.setActive(cr, uid, ids, value=False, context=context) - def reset_project(self, cr, uid, ids, context=None): - return self.setActive(cr, uid, ids, value=True, context=context) - def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None): """ copy and map tasks from old to new project """ if context is None: @@ -331,11 +328,6 @@ class project(osv.osv): 'search_view_id': search_view['res_id'], } - @api.multi - def setActive(self, value=True): - """ Set a project as active/inactive, and its tasks as well. """ - self.write({'active': value}) - def create(self, cr, uid, vals, context=None): ir_values = self.pool.get('ir.values').get_default(cr, uid, 'project.config.settings', 'generate_project_alias') if ir_values: From e7e277480cbc2947d0c5f5f6e93683f446cc771f Mon Sep 17 00:00:00 2001 From: Gaurav Panchal Date: Tue, 19 Apr 2016 17:07:48 +0530 Subject: [PATCH 03/20] [REM] project: removed dead code from removed template mechanism --- addons/project/project.py | 44 --------------------------------------- 1 file changed, 44 deletions(-) diff --git a/addons/project/project.py b/addons/project/project.py index 0761659058c..5d47bccbada 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, date from lxml import etree -import time from openerp import api from openerp import SUPERUSER_ID @@ -257,9 +255,6 @@ class project(osv.osv): (_check_dates, 'Error! project start-date must be lower than project end-date.', ['date_start', 'date']) ] - def set_template(self, cr, uid, ids, context=None): - return self.setActive(cr, uid, ids, value=False, context=context) - def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None): """ copy and map tasks from old to new project """ if context is None: @@ -289,45 +284,6 @@ class project(osv.osv): self.map_tasks(cr, uid, id, res, context=context) return res - def duplicate_template(self, cr, uid, ids, context=None): - context = dict(context or {}) - data_obj = self.pool.get('ir.model.data') - result = [] - for proj in self.browse(cr, uid, ids, context=context): - context.update({'analytic_project_copy': True}) - new_date_start = time.strftime('%Y-%m-%d') - new_date_end = False - if proj.date_start and proj.date: - start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3]) - end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3]) - new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d') - context.update({'copy':True}) - new_id = self.copy(cr, uid, proj.id, default = { - 'name':_("%s (copy)") % (proj.name), - 'date_start':new_date_start, - 'date':new_date_end}, context=context) - result.append(new_id) - - if result and len(result): - res_id = result[0] - form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project') - form_view = data_obj.read(cr, uid, form_view_id, ['res_id']) - tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project') - tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id']) - search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter') - search_view = data_obj.read(cr, uid, search_view_id, ['res_id']) - return { - 'name': _('Projects'), - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_model': 'project.project', - 'view_id': False, - 'res_id': res_id, - 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')], - 'type': 'ir.actions.act_window', - 'search_view_id': search_view['res_id'], - } - def create(self, cr, uid, vals, context=None): ir_values = self.pool.get('ir.values').get_default(cr, uid, 'project.config.settings', 'generate_project_alias') if ir_values: From b41bbb2b8723ae23478201ab187a6651601a9105 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Tue, 28 Jun 2016 11:23:13 +0200 Subject: [PATCH 04/20] [REM] project,hr_timesheet: removed dead _get_total_hours function It should have been removed by 9b69184ffacd8ef10a8d2b0e4db76f3676e5f908. --- addons/hr_timesheet/project_timesheet.py | 3 --- addons/project/project.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/addons/hr_timesheet/project_timesheet.py b/addons/hr_timesheet/project_timesheet.py index 8d358f10240..5b6ebd60132 100644 --- a/addons/hr_timesheet/project_timesheet.py +++ b/addons/hr_timesheet/project_timesheet.py @@ -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: diff --git a/addons/project/project.py b/addons/project/project.py index 5d47bccbada..b87915a3ae5 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -604,9 +604,6 @@ class task(osv.osv): self._store_history(cr, uid, ids, context=context) return result - def _get_total_hours(self): - return self.remaining_hours - # --------------------------------------------------- # Mail gateway # --------------------------------------------------- From 1449a1eaf1b43379cdabefd00e940c2485dff044 Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Thu, 9 Jun 2016 16:27:09 +0530 Subject: [PATCH 05/20] [MOV] project: renamed and moved files according to new API guidelines --- addons/project/__init__.py | 6 +---- addons/project/__openerp__.py | 24 +++++++++---------- addons/project/{ => data}/project_data.xml | 0 addons/project/{ => data}/project_demo.xml | 0 .../{ => data}/project_mail_template_data.xml | 0 .../project/{ => data}/web_planner_data.xml | 0 addons/project/models/__init__.py | 8 +++++++ addons/project/{ => models}/project.py | 0 .../{company.py => models/res_company.py} | 0 addons/project/{ => models}/res_config.py | 0 addons/project/{ => models}/res_partner.py | 0 addons/project/{ => models}/web_planner.py | 0 ...ative.xml => project_cumulative_views.xml} | 0 ...port_view.xml => project_report_views.xml} | 0 .../project/{ => views}/project_dashboard.xml | 0 .../{project.xml => project_templates.xml} | 0 .../project_views.xml} | 0 .../res_config_views.xml} | 0 .../res_partner_views.xml} | 0 .../{tour_view.xml => views/tour_views.xml} | 0 20 files changed, 21 insertions(+), 17 deletions(-) rename addons/project/{ => data}/project_data.xml (100%) rename addons/project/{ => data}/project_demo.xml (100%) rename addons/project/{ => data}/project_mail_template_data.xml (100%) rename addons/project/{ => data}/web_planner_data.xml (100%) create mode 100644 addons/project/models/__init__.py rename addons/project/{ => models}/project.py (100%) rename addons/project/{company.py => models/res_company.py} (100%) rename addons/project/{ => models}/res_config.py (100%) rename addons/project/{ => models}/res_partner.py (100%) rename addons/project/{ => models}/web_planner.py (100%) rename addons/project/report/{project_cumulative.xml => project_cumulative_views.xml} (100%) rename addons/project/report/{project_report_view.xml => project_report_views.xml} (100%) rename addons/project/{ => views}/project_dashboard.xml (100%) rename addons/project/views/{project.xml => project_templates.xml} (100%) rename addons/project/{project_view.xml => views/project_views.xml} (100%) rename addons/project/{res_config_view.xml => views/res_config_views.xml} (100%) rename addons/project/{res_partner_view.xml => views/res_partner_views.xml} (100%) rename addons/project/{tour_view.xml => views/tour_views.xml} (100%) diff --git a/addons/project/__init__.py b/addons/project/__init__.py index 433a751190b..a5f67aebd79 100644 --- a/addons/project/__init__.py +++ b/addons/project/__init__.py @@ -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 diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index 7550b6027ec..a1aa33ae70d 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -37,20 +37,20 @@ 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', + 'views/project_views.xml', + 'views/res_partner_views.xml', + 'report/project_report_views.xml', + 'report/project_cumulative_views.xml', + 'views/res_config_views.xml', + 'views/project_templates.xml', + 'views/project_dashboard.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, diff --git a/addons/project/project_data.xml b/addons/project/data/project_data.xml similarity index 100% rename from addons/project/project_data.xml rename to addons/project/data/project_data.xml diff --git a/addons/project/project_demo.xml b/addons/project/data/project_demo.xml similarity index 100% rename from addons/project/project_demo.xml rename to addons/project/data/project_demo.xml diff --git a/addons/project/project_mail_template_data.xml b/addons/project/data/project_mail_template_data.xml similarity index 100% rename from addons/project/project_mail_template_data.xml rename to addons/project/data/project_mail_template_data.xml diff --git a/addons/project/web_planner_data.xml b/addons/project/data/web_planner_data.xml similarity index 100% rename from addons/project/web_planner_data.xml rename to addons/project/data/web_planner_data.xml diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py new file mode 100644 index 00000000000..04eb14303cb --- /dev/null +++ b/addons/project/models/__init__.py @@ -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 diff --git a/addons/project/project.py b/addons/project/models/project.py similarity index 100% rename from addons/project/project.py rename to addons/project/models/project.py diff --git a/addons/project/company.py b/addons/project/models/res_company.py similarity index 100% rename from addons/project/company.py rename to addons/project/models/res_company.py diff --git a/addons/project/res_config.py b/addons/project/models/res_config.py similarity index 100% rename from addons/project/res_config.py rename to addons/project/models/res_config.py diff --git a/addons/project/res_partner.py b/addons/project/models/res_partner.py similarity index 100% rename from addons/project/res_partner.py rename to addons/project/models/res_partner.py diff --git a/addons/project/web_planner.py b/addons/project/models/web_planner.py similarity index 100% rename from addons/project/web_planner.py rename to addons/project/models/web_planner.py diff --git a/addons/project/report/project_cumulative.xml b/addons/project/report/project_cumulative_views.xml similarity index 100% rename from addons/project/report/project_cumulative.xml rename to addons/project/report/project_cumulative_views.xml diff --git a/addons/project/report/project_report_view.xml b/addons/project/report/project_report_views.xml similarity index 100% rename from addons/project/report/project_report_view.xml rename to addons/project/report/project_report_views.xml diff --git a/addons/project/project_dashboard.xml b/addons/project/views/project_dashboard.xml similarity index 100% rename from addons/project/project_dashboard.xml rename to addons/project/views/project_dashboard.xml diff --git a/addons/project/views/project.xml b/addons/project/views/project_templates.xml similarity index 100% rename from addons/project/views/project.xml rename to addons/project/views/project_templates.xml diff --git a/addons/project/project_view.xml b/addons/project/views/project_views.xml similarity index 100% rename from addons/project/project_view.xml rename to addons/project/views/project_views.xml diff --git a/addons/project/res_config_view.xml b/addons/project/views/res_config_views.xml similarity index 100% rename from addons/project/res_config_view.xml rename to addons/project/views/res_config_views.xml diff --git a/addons/project/res_partner_view.xml b/addons/project/views/res_partner_views.xml similarity index 100% rename from addons/project/res_partner_view.xml rename to addons/project/views/res_partner_views.xml diff --git a/addons/project/tour_view.xml b/addons/project/views/tour_views.xml similarity index 100% rename from addons/project/tour_view.xml rename to addons/project/views/tour_views.xml From 2c29168307c433a114af7c3a947615bd507a8631 Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Thu, 9 Jun 2016 17:01:36 +0530 Subject: [PATCH 06/20] [MOV] project: split and restructured files according to new API guidelines - created report/project_cumulative.py for model project.task.history.cumulative that was in project.py - moved project.project kanban view from project_dashboard.xml to project_views.xml, which required moving some menuitems as well so they are available while refering them in other files --- addons/project/__openerp__.py | 5 +- addons/project/models/project.py | 39 ------ addons/project/report/__init__.py | 1 + addons/project/report/project_cumulative.py | 46 +++++++ .../report/project_cumulative_views.xml | 3 - .../project/report/project_report_views.xml | 6 - addons/project/views/project_dashboard.xml | 111 --------------- addons/project/views/project_views.xml | 127 +++++++++++++++++- 8 files changed, 173 insertions(+), 165 deletions(-) create mode 100644 addons/project/report/project_cumulative.py delete mode 100644 addons/project/views/project_dashboard.xml diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index a1aa33ae70d..d9a26448063 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -38,13 +38,12 @@ Dashboard / Reports for Project Management will include: 'security/project_security.xml', 'security/ir.model.access.csv', 'data/project_data.xml', - 'views/project_views.xml', - 'views/res_partner_views.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/project_dashboard.xml', 'views/tour_views.xml', 'data/web_planner_data.xml', 'data/project_mail_template_data.xml', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index b87915a3ae5..d852d383241 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -913,45 +913,6 @@ class project_task_history(osv.osv): 'date': fields.date.context_today, } -class project_task_history_cumulative(osv.osv): - _name = 'project.task.history.cumulative' - _table = 'project_task_history_cumulative' - _inherit = 'project.task.history' - _auto = False - - _columns = { - 'end_date': fields.date('End Date'), - 'nbr_tasks': fields.integer('# of Tasks', readonly=True), - 'project_id': fields.many2one('project.project', 'Project'), - } - - def init(self, cr): - tools.drop_view_if_exists(cr, 'project_task_history_cumulative') - - cr.execute(""" CREATE VIEW project_task_history_cumulative 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 - ) - """) class project_tags(osv.Model): """ Tags of project's tasks (or issues) """ diff --git a/addons/project/report/__init__.py b/addons/project/report/__init__.py index 8d6f3204295..da053e69717 100644 --- a/addons/project/report/__init__.py +++ b/addons/project/report/__init__.py @@ -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 diff --git a/addons/project/report/project_cumulative.py b/addons/project/report/project_cumulative.py new file mode 100644 index 00000000000..14003a235c7 --- /dev/null +++ b/addons/project/report/project_cumulative.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import tools +from openerp.osv import fields, osv + + +class project_task_history_cumulative(osv.osv): + _name = 'project.task.history.cumulative' + _table = 'project_task_history_cumulative' + _inherit = 'project.task.history' + _auto = False + + _columns = { + 'end_date': fields.date('End Date'), + 'nbr_tasks': fields.integer('# of Tasks', readonly=True), + 'project_id': fields.many2one('project.project', 'Project'), + } + + def init(self, cr): + tools.drop_view_if_exists(cr, 'project_task_history_cumulative') + + cr.execute(""" CREATE VIEW project_task_history_cumulative 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 + ) + """) diff --git a/addons/project/report/project_cumulative_views.xml b/addons/project/report/project_cumulative_views.xml index 5cabe21ec73..cd119881d92 100644 --- a/addons/project/report/project_cumulative_views.xml +++ b/addons/project/report/project_cumulative_views.xml @@ -82,9 +82,6 @@ - diff --git a/addons/project/report/project_report_views.xml b/addons/project/report/project_report_views.xml index 6e0eff34a2d..33425bf1e00 100644 --- a/addons/project/report/project_report_views.xml +++ b/addons/project/report/project_report_views.xml @@ -2,10 +2,6 @@ - - report.project.task.user.pivot report.project.task.user @@ -107,7 +103,5 @@ 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. - - diff --git a/addons/project/views/project_dashboard.xml b/addons/project/views/project_dashboard.xml deleted file mode 100644 index d6f7355d144..00000000000 --- a/addons/project/views/project_dashboard.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - project.project.form.popup - project.project - -
- - - - -
-
- -
-
-
-
- - - Create a Project - project.project - form - form - - new - - - - project.project.kanban - project.project - - - - - - - - - - - - - - - - - -
-
-
-
-
- -
-
- -
-
-
-
-
- Settings -
-
- Tasks -
- -
-
-
    -
- -
-
- More -
-
- - -
-
-
-
-
-
-
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index d00d7ef8719..f7f4485523b 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -182,6 +182,116 @@ + + project.project.form.popup + project.project + +
+ + + + +
+
+ +
+
+
+
+ + + Create a Project + project.project + form + form + + new + + + + project.project.kanban + project.project + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ Settings +
+
+ Tasks +
+ +
+
+
    +
+ +
+
+ More +
+
+ + +
+
+
+
+
+
+ Activate Sample Project @@ -251,7 +361,7 @@ - + - @@ -637,8 +747,19 @@ - + + + + + From 40fa74e3f561327979d8b56e23e7630291d26683 Mon Sep 17 00:00:00 2001 From: Gaurav Panchal Date: Thu, 9 Jun 2016 19:09:52 +0530 Subject: [PATCH 07/20] [MIG] project: migration to new api except project.py --- addons/project/data/project_data.xml | 6 +- addons/project/data/project_demo.xml | 4 +- .../data/project_mail_template_data.xml | 1 + addons/project/models/res_company.py | 18 +++-- addons/project/models/res_config.py | 55 ++++++++------- addons/project/models/res_partner.py | 23 +++---- addons/project/models/web_planner.py | 4 +- addons/project/report/project_cumulative.py | 22 +++--- .../report/project_cumulative_views.xml | 6 +- addons/project/report/project_report.py | 69 ++++++++++--------- .../project/report/project_report_views.xml | 6 +- addons/project/security/project_security.xml | 6 +- addons/project/views/project_templates.xml | 6 +- addons/project/views/project_views.xml | 6 +- addons/project/views/res_config_views.xml | 6 +- addons/project/views/res_partner_views.xml | 8 +-- 16 files changed, 115 insertions(+), 131 deletions(-) diff --git a/addons/project/data/project_data.xml b/addons/project/data/project_data.xml index 16b88df0a52..f00a06e49ae 100644 --- a/addons/project/data/project_data.xml +++ b/addons/project/data/project_data.xml @@ -1,5 +1,5 @@ - + @@ -33,7 +33,6 @@ - @@ -107,7 +106,6 @@

You can manage todo lists on tasks by installing the Todo Lists application, supporting the Getting Things Done (GTD) methodology.

You can also manage issues/bugs in projects by installing the "Issue Tracker" application.

]]> -
@@ -333,4 +331,4 @@ - + diff --git a/addons/project/data/project_demo.xml b/addons/project/data/project_demo.xml index a953d89bd94..95ba9da9078 100644 --- a/addons/project/data/project_demo.xml +++ b/addons/project/data/project_demo.xml @@ -1,5 +1,5 @@ - + @@ -435,4 +435,4 @@ Send it ASAP, its urgent. - + diff --git a/addons/project/data/project_mail_template_data.xml b/addons/project/data/project_mail_template_data.xml index 489643afa6a..7c045d2b635 100644 --- a/addons/project/data/project_mail_template_data.xml +++ b/addons/project/data/project_mail_template_data.xml @@ -1,3 +1,4 @@ + diff --git a/addons/project/models/res_company.py b/addons/project/models/res_company.py index 7a08da8071f..282b611ca75 100644 --- a/addons/project/models/res_company.py +++ b/addons/project/models/res_company.py @@ -1,15 +1,13 @@ # -*- 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 _ +from odoo import fields, models -class res_company(osv.osv): + +class ResCompany(models.Model): _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.", - ), - } + + 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.") diff --git a/addons/project/models/res_config.py b/addons/project/models/res_config.py index bf084efb584..930ce8773d4 100644 --- a/addons/project/models/res_config.py +++ b/addons/project/models/res_config.py @@ -1,36 +1,35 @@ # -*- 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 _ +from odoo import api, fields, models -class project_configuration(osv.osv_memory): + +class ProjectConfiguration(models.TransientModel): _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"), - } + 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") - 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) + @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) diff --git a/addons/project/models/res_partner.py b/addons/project/models/res_partner.py index a7f3939cd8f..2fd7f0f7e3a 100644 --- a/addons/project/models/res_partner.py +++ b/addons/project/models/res_partner.py @@ -1,19 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields,osv +from odoo import fields, models -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 - } - + +class ResPartner(models.Model): """ 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'), - } + + 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) diff --git a/addons/project/models/web_planner.py b/addons/project/models/web_planner.py index 23710215b51..ae8b43efc56 100644 --- a/addons/project/models/web_planner.py +++ b/addons/project/models/web_planner.py @@ -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): diff --git a/addons/project/report/project_cumulative.py b/addons/project/report/project_cumulative.py index 14003a235c7..eed808dbc9a 100644 --- a/addons/project/report/project_cumulative.py +++ b/addons/project/report/project_cumulative.py @@ -1,26 +1,22 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import tools -from openerp.osv import fields, osv +from odoo import fields, models, tools -class project_task_history_cumulative(osv.osv): +class ProjectTaskHistoryCumulative(models.Model): _name = 'project.task.history.cumulative' - _table = 'project_task_history_cumulative' _inherit = 'project.task.history' _auto = False - _columns = { - 'end_date': fields.date('End Date'), - 'nbr_tasks': fields.integer('# of Tasks', readonly=True), - 'project_id': fields.many2one('project.project', 'Project'), - } + 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, cr): - tools.drop_view_if_exists(cr, 'project_task_history_cumulative') + def init(self): + tools.drop_view_if_exists(self._cr, self._table) - cr.execute(""" CREATE VIEW project_task_history_cumulative AS ( + self._cr.execute(""" CREATE VIEW %s AS ( SELECT history.date::varchar||'-'||history.history_id::varchar AS id, history.date AS end_date, @@ -43,4 +39,4 @@ class project_task_history_cumulative(osv.osv): ) AS history ) - """) + """ % (self._table,)) diff --git a/addons/project/report/project_cumulative_views.xml b/addons/project/report/project_cumulative_views.xml index cd119881d92..1c63610f0da 100644 --- a/addons/project/report/project_cumulative_views.xml +++ b/addons/project/report/project_cumulative_views.xml @@ -1,6 +1,5 @@ - - + project.task.history.cumulative.pivot @@ -83,5 +82,4 @@ - - + diff --git a/addons/project/report/project_report.py b/addons/project/report/project_report.py index ecbfada5849..9bbe1518304 100644 --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@ -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())) diff --git a/addons/project/report/project_report_views.xml b/addons/project/report/project_report_views.xml index 33425bf1e00..9717b54d777 100644 --- a/addons/project/report/project_report_views.xml +++ b/addons/project/report/project_report_views.xml @@ -1,6 +1,5 @@ - - + report.project.task.user.pivot @@ -103,5 +102,4 @@ 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. - - + diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml index bda3e201b7a..56c51d7eea2 100644 --- a/addons/project/security/project_security.xml +++ b/addons/project/security/project_security.xml @@ -1,6 +1,5 @@ - - + User @@ -24,7 +23,6 @@ - @@ -133,4 +131,4 @@ - + diff --git a/addons/project/views/project_templates.xml b/addons/project/views/project_templates.xml index 099eabed05d..3806674c4b9 100644 --- a/addons/project/views/project_templates.xml +++ b/addons/project/views/project_templates.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index f7f4485523b..838209d48bc 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -1,6 +1,5 @@ - - + - - + diff --git a/addons/project/views/res_config_views.xml b/addons/project/views/res_config_views.xml index 5474a47f8bc..4baf40323d0 100644 --- a/addons/project/views/res_config_views.xml +++ b/addons/project/views/res_config_views.xml @@ -1,6 +1,5 @@ - - + project settings project.config.settings @@ -40,5 +39,4 @@ - - + diff --git a/addons/project/views/res_partner_views.xml b/addons/project/views/res_partner_views.xml index 82448073bf1..a838a87d732 100644 --- a/addons/project/views/res_partner_views.xml +++ b/addons/project/views/res_partner_views.xml @@ -1,6 +1,5 @@ - - - + + @@ -20,5 +19,4 @@ - - \ No newline at end of file + From 58b5d09890107fa9396d2243be513675b4854427 Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Fri, 10 Jun 2016 18:33:48 +0530 Subject: [PATCH 08/20] [MIG] project: migration to new api of project.py Also: - removed onchange_partner_id() from 'project.project' model as 'pricelist_id' field does not exist in project nor in any inherit(s) since its removal in 2008 by 07b1f0b9daac1d53aa25dcf26e7a3f7cce469520 - removed 'type' field from _defaults dictionary of 'project.project' model as it does not exists anymore, it used to be defined on account.analytic.account - removed on_change_template() from 'account.analytic.account' model as it doesn't seem to be useful and/or called from any view - overridden _default_get() for 'project.project' model to set default value for 'use_tasks' field of inherits account.analytic.account --- addons/project/models/project.py | 1012 +++++++++++------------- addons/project/views/project_views.xml | 5 +- 2 files changed, 460 insertions(+), 557 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index d852d383241..3bf2d9d7f82 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -3,16 +3,12 @@ from lxml import etree -from openerp import api -from openerp import SUPERUSER_ID -from openerp import tools -from openerp.osv import fields, osv +from odoo import api, fields, models, tools, SUPERUSER_ID, _ from openerp.tools.safe_eval import safe_eval as eval -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from openerp.exceptions import UserError, ValidationError -class project_task_type(osv.osv): +class ProjectTaskType(models.Model): _name = 'project.task.type' _description = 'Task Stage' _order = 'sequence' @@ -20,112 +16,95 @@ class project_task_type(osv.osv): def _get_mail_template_id_domain(self): return [('model', '=', 'project.task')] - _columns = { - 'name': fields.char('Stage Name', required=True, translate=True), - 'description': fields.text('Description', translate=True), - 'sequence': fields.integer('Sequence'), - 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'), - 'legend_priority': fields.char( - '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( - '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( - '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( - '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('Folded in Tasks Pipeline', - help='This stage is folded in the kanban view when ' - 'there are no records in that stage to display.'), - } - - def _get_default_project_ids(self, cr, uid, ctx=None): - if ctx is None: - ctx = {} - default_project_id = ctx.get('default_project_id') + 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 - _defaults = { - 'sequence': 1, - 'project_ids': _get_default_project_ids, - } + 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(osv.osv): +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 = super(Project, self).get_alias_values() values['alias_defaults'] = {'project_id': self.id} return values - def onchange_partner_id(self, cr, uid, ids, part=False, context=None): - partner_obj = self.pool.get('res.partner') - val = {} - if not part: - return {'value': val} - if 'pricelist_id' in self.fields_get(cr, uid, context=context): - pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context) - pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False - val['pricelist_id'] = pricelist_id - return {'value': val} - - def unlink(self, cr, uid, ids, context=None): - analytic_account_to_delete = set() - for proj in self.browse(cr, uid, ids, context=context): - if proj.tasks: + @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 proj.analytic_account_id and not proj.analytic_account_id.line_ids: - analytic_account_to_delete.add(proj.analytic_account_id.id) - res = super(project, self).unlink(cr, uid, ids, context=context) - self.pool['account.analytic.account'].unlink(cr, uid, list(analytic_account_to_delete), context=context) + 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 _get_attached_docs(self, cr, uid, ids, field_name, arg, context): - res = {} - attachment = self.pool.get('ir.attachment') - task = self.pool.get('project.task') - for id in ids: - project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True) - task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context) - task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True) - res[id] = (project_attachments or 0) + (task_attachments or 0) - return res - def _task_count(self, cr, uid, ids, field_name, arg, context=None): - if context is None: - context = {} - res={} - for project in self.browse(cr, uid, ids, context=context): - res[project.id] = len(project.task_ids) - 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 _task_needaction_count(self, cr, uid, ids, field_name, arg, context=None): - Task = self.pool['project.task'] - res = dict.fromkeys(ids, 0) - projects = Task.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 + def _compute_task_count(self): + for project in self: + project.task_count = len(project.task_ids) - def _get_alias_models(self, cr, uid, context=None): + 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, cr, uid, context=None): + def _get_visibility_selection(self): """ Overriden in portal_project to offer more options """ return [ ('employees', _('Visible by all employees')), @@ -133,13 +112,13 @@ class project(osv.osv): ('portal', _('Shared with a customer')) ] - def attachment_tree_view(self, cr, uid, ids, context): - task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)]) + @api.multi + def attachment_tree_view(self): + self.ensure_one() domain = [ - '|', - '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids), - '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)] - res_id = ids and ids[0] or False + '|', + '&', ('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, @@ -154,338 +133,306 @@ class project(osv.osv): documents to your project.

'''), 'limit': 80, - 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id) + 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id) } - def activate_sample_project(self, cr, uid, context=None): + @api.model + def activate_sample_project(self): """ Unarchives the sample project 'project.project_project_data' and reloads the project dashboard """ # Unarchive sample project - data_obj = self.pool.get('ir.model.data') - proj_id = data_obj.xmlid_to_res_id(cr, uid, 'project.project_project_data') - if proj_id: - self.browse(cr, uid, proj_id, context=context).write({'active': True}) + 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) - act_id = data_obj.xmlid_to_res_id(cr, uid, 'project.open_view_project_all') - action = None - if act_id: - act_window_obj = self.pool.get('ir.actions.act_window') - act_window_obj.write(cr, SUPERUSER_ID, [act_id], { + action = self.env.ref('project.open_view_project_all', False) + action_data = None + if action: + action.sudo().write({ "help": _('''

Click to create a new project.

''') - }, context=context) - action = act_window_obj.read(cr, uid, [act_id])[0] + }) + action_data = action.read()[0] # Reload the dashboard - return action + return action_data - def _get_favorite(self, cr, uid, ids, name, args, context=None): - return dict((project.id, uid in project.favorite_user_ids.ids) for project in self.browse(cr, uid, ids, context=context)) + 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, cr, uid, context=None): - return [(6, 0, [uid])] + 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, *args, **kwargs: self._get_alias_models(*args, **kwargs) - _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs) + _alias_models = lambda self: self._get_alias_models() + _visibility_selection = lambda self: self._get_visibility_selection() - _columns = { - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."), - 'analytic_account_id': fields.many2one( - 'account.analytic.account', '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', - string='Members'), - 'is_favorite': fields.function(_get_favorite, type="boolean", string='Show Project on dashboard', - help="Whether this project should be displayed on the dashboard or not"), - 'label_tasks': fields.char('Use Tasks as', help="Gives label to tasks on project's kanban view."), - 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"), - 'resource_calendar_id': fields.many2one('resource.calendar', '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', 'Tasks Stages'), - 'task_count': fields.function(_task_count, type='integer', string="Tasks",), - 'task_needaction_count': fields.function(_task_needaction_count, type='integer', string="Tasks",), - 'task_ids': fields.one2many('project.task', 'project_id', string='Tasks', - domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)]), - 'color': fields.integer('Color Index'), - 'user_id': fields.many2one('res.users', 'Project Manager'), - 'alias_id': fields.many2one('mail.alias', '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, "Alias Model", select=True, required=True, - help="The kind of document created when an email is received on this project's email alias"), - 'privacy_visibility': fields.selection(_visibility_selection, 'Privacy', required=True, - 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.function( - _get_attached_docs, string="Number of documents attached", type='integer' - ), - 'date_start': fields.date('Start Date'), - 'date': fields.date('Expiration Date', select=True, track_visibility='onchange'), - } - - _order = "sequence, name, id" - _defaults = { - 'active': True, - 'favorite_user_ids': _get_default_favorite_user_ids, - 'type': 'contract', - 'label_tasks': 'Tasks', - 'sequence': 10, - 'user_id': lambda self,cr,uid,ctx: uid, - 'alias_model': 'project.task', - 'privacy_visibility': 'employees', - } + 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') # TODO: Why not using a SQL contraints ? - def _check_dates(self, cr, uid, ids, context=None): - for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context): - if leave['date_start'] and leave['date']: - if leave['date_start'] > leave['date']: - return False - return True + @api.constrains('date_start', 'date') + def _check_dates(self): + if any(self.filtered(lambda project: project.date_start and project.date and project.date_start > project.date)): + raise ValidationError(_('Error! project start-date must be lower than project end-date.')) - _constraints = [ - (_check_dates, 'Error! project start-date must be lower than project end-date.', ['date_start', 'date']) - ] - - def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None): + @api.multi + def map_tasks(self, new_project_id): """ copy and map tasks from old to new project """ - if context is None: - context = {} - map_task_id = {} - task_obj = self.pool.get('project.task') - proj = self.browse(cr, uid, old_project_id, context=context) - for task in proj.tasks: + 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} - map_task_id[task.id] = task_obj.copy(cr, uid, task.id, defaults, context=context) - self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]}) - return True + tasks += task.copy(defaults) + return self.browse(new_project_id).write({'tasks': [(6, 0, tasks.ids)]}) - def copy(self, cr, uid, id, default=None, context=None): + @api.multi + def copy(self, default=None): if default is None: default = {} - context = dict(context or {}) - context['active_test'] = False - proj = self.browse(cr, uid, id, context=context) + self = self.with_context(active_test=False) if not default.get('name'): - default.update(name=_("%s (copy)") % (proj.name)) - res = super(project, self).copy(cr, uid, id, default, context) - for follower in proj.message_follower_ids: - self.message_subscribe(cr, uid, res, partner_ids=[follower.partner_id.id], subtype_ids=[subtype.id for subtype in follower.subtype_ids]) - self.map_tasks(cr, uid, id, res, context=context) - return res + 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 - def create(self, cr, uid, vals, context=None): - ir_values = self.pool.get('ir.values').get_default(cr, uid, 'project.config.settings', 'generate_project_alias') + @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 - create_context = dict(context or {}, - project_creation_in_progress=True, - mail_create_nosubscribe=True) - return super(project, self).create(cr, uid, vals, context=create_context) + self = self.with_context(project_creation_in_progress=True, mail_create_nosubscribe=True) + return super(Project, self).create(vals) - def write(self, cr, uid, ids, vals, context=None): + @api.multi + def write(self, vals): # if alias_model has been changed, update alias_model_id accordingly if vals.get('alias_model'): - model_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', vals.get('alias_model', 'project.task'))]) - vals.update(alias_model_id=model_ids[0]) - res = super(project, self).write(cr, uid, ids, vals, context=context) + 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 - projects = self.browse(cr, uid, ids, context) - tasks = projects.with_context(active_test=False).mapped('tasks') - tasks.write({'active': vals['active']}) + self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']}) return res - def toggle_favorite(self, cr, uid, ids, context=None): - favorite_project_ids = [] - not_fav_project_ids = [] - for project in self.browse(cr, uid, ids, context=context): - if uid in project.favorite_user_ids.ids: - favorite_project_ids.append(project.id) + @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_project_ids.append(project.id) + not_fav_projects |= project # Project User has no write access for project. - self.write(cr, SUPERUSER_ID, not_fav_project_ids, {'favorite_user_ids': [(4, uid)]}, context=context) - self.write(cr, SUPERUSER_ID, favorite_project_ids, {'favorite_user_ids': [(3, uid)]}, context=context) + 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(osv.osv): + +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, 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_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, cr, uid, context=None): + def _get_default_stage_id(self): """ Gives default stage_id """ - if context is None: - context = {} - return self.stage_find(cr, uid, [], context.get('default_project_id'), [('fold', '=', False)], context=context) + return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', False)]) - 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 = {} - stage_obj = self.pool.get('project.task.type') - order = stage_obj._order - access_rights_uid = access_rights_uid or uid + @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 context: - search_domain = ['|', ('project_ids', '=', context['default_project_id']), ('id', 'in', 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', ids)] - 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) + 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]))) - 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 + return result, {stage.id: stage.fold for stage in stages} _group_by_full = { 'stage_id': _read_group_stage_ids, } - def onchange_project(self, cr, uid, id, 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}} - return {} + 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') - def onchange_user_id(self, cr, uid, ids, user_id, context=None): - vals = {} - if user_id: - vals['date_start'] = fields.datetime.now() - return {'value': vals} + @api.onchange('project_id') + def _onchange_project(self): + if self.project_id: + self.partner_id = self.project_id.partner_id - def copy_data(self, cr, uid, id, default=None, context=None): + @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 = {} - current = self.browse(cr, uid, id, context=context) if not default.get('name'): - default['name'] = _("%s (copy)") % current.name + default['name'] = _("%s (copy)") % self.name if 'remaining_hours' not in default: - default['remaining_hours'] = current.planned_hours + default['remaining_hours'] = self.planned_hours + return super(Task, self).copy(default) - return super(task, self).copy_data(cr, uid, id, default, context) - - _columns = { - 'active': fields.boolean('Active'), - 'name': fields.char('Task Title', track_visibility='onchange', size=128, required=True, select=True), - 'description': fields.html('Description'), - 'priority': fields.selection([('0','Normal'), ('1','High')], 'Priority', select=True), - 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."), - 'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange', select=True, - 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')], 'Kanban State', - track_visibility='onchange', - 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", - required=True, copy=False), - 'create_date': fields.datetime('Create Date', readonly=True, select=True), - 'write_date': fields.datetime('Last Modification Date', readonly=True, select=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('Starting Date', select=True, copy=False), - 'date_end': fields.datetime('Ending Date', select=True, copy=False), - 'date_assign': fields.datetime('Assigning Date', select=True, copy=False, readonly=True), - 'date_deadline': fields.date('Deadline', select=True, copy=False), - 'date_last_stage_update': fields.datetime('Last Stage Update', select=True, copy=False, readonly=True), - 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True), - 'notes': fields.text('Notes'), - 'planned_hours': fields.float('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('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', 'Assigned to', select=True, track_visibility='onchange'), - 'partner_id': fields.many2one('res.partner', 'Customer'), - 'manager_id': fields.related('project_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'), - 'company_id': fields.many2one('res.company', 'Company'), - 'id': fields.integer('ID', readonly=True), - 'color': fields.integer('Color Index'), - 'user_email': fields.related('user_id', 'email', type='char', 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.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 = { - 'stage_id': _get_default_stage_id, - 'project_id': lambda self, cr, uid, ctx=None: ctx.get('default_project_id') if ctx is not None else False, - 'date_last_stage_update': fields.datetime.now, - 'kanban_state': 'normal', - 'priority': '0', - 'sequence': 10, - 'active': True, - 'user_id': lambda obj, cr, uid, ctx=None: uid, - 'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx), - 'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx), - 'date_start': fields.datetime.now, - } - _order = "priority desc, sequence, date_start, name, id" - - def _check_dates(self, cr, uid, ids, context=None): - if context == None: - context = {} - obj_task = self.browse(cr, uid, ids[0], context=context) - start = obj_task.date_start or False - end = obj_task.date_end or False - if start and end : - if start > end: - return False - return True - - _constraints = [ - (_check_dates, 'Error ! Task starting date must be lower than its ending date.', ['date_start','date_end']) - ] + @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 - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - users_obj = self.pool.get('res.users') - if context is None: context = {} + @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 = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id + 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(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu) + 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 = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id - try: - # using get_object to get translation value - uom_hour = self.pool['ir.model.data'].get_object(cr, uid, 'product', 'product_uom_hour', context=context) - except ValueError: - uom_hour = False + 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 @@ -493,8 +440,8 @@ class task(osv.osv): # 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') + if eview.attrib.get('widget', '') == 'float_time': + eview.set('widget', 'float') for child in eview: _check_rec(child) return True @@ -512,33 +459,31 @@ class task(osv.osv): res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) return res - def get_empty_list_help(self, cr, uid, help, context=None): - context = dict(context or {}) - context['empty_list_help_id'] = context.get('default_project_id') - context['empty_list_help_model'] = 'project.project' - context['empty_list_help_document_name'] = _("tasks") - return super(task, self).get_empty_list_help(cr, uid, help, context=context) + @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, cr, uid, cases, section_id, domain=[], order='sequence', context=None): + 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 """ - if isinstance(cases, (int, long)): - cases = self.browse(cr, uid, cases, context=context) # collect all section_ids section_ids = [] if section_id: section_ids.append(section_id) - for task in cases: - if task.project_id: - section_ids.append(task.project_id.id) + section_ids.extend(self.mapped('project_id').ids) search_domain = [] if section_ids: search_domain = [('|')] * (len(section_ids) - 1) @@ -546,14 +491,11 @@ class task(osv.osv): search_domain.append(('project_ids', '=', section_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 + return self.env['project.task.type'].search(search_domain, order=order, limit=1).id - def _store_history(self, cr, uid, ids, context=None): - for task in self.browse(cr, uid, ids, context=context): - self.pool.get('project.task.history').create(cr, uid, { + 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, @@ -561,47 +503,45 @@ class task(osv.osv): 'type_id': task.stage_id.id, 'user_id': task.user_id.id - }, context=context) + }) return True # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ - def create(self, cr, uid, vals, context=None): - context = dict(context or {}) + @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() - # context: no_log, because subtype already handle this - create_context = dict(context, mail_create_nolog=True) - task_id = super(task, self).create(cr, uid, vals, context=create_context) - self._store_history(cr, uid, [task_id], context=context) - return task_id - - def write(self, cr, uid, ids, vals, context=None): - if isinstance(ids, (int, long)): - ids = [ids] + 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'] = fields.datetime.now() - # user_id change: update date_assign - if vals.get('user_id'): - vals['date_assign'] = fields.datetime.now() - # reset kanban state when changing stage - 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(cr, uid, ids, vals, context=context) + 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(cr, uid, ids, context=context) + self._store_history() return result # --------------------------------------------------- @@ -610,50 +550,51 @@ class task(osv.osv): @api.multi def _track_template(self, tracking): - res = super(task, self)._track_template(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 - 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': + @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 record.kanban_state == 'done': + elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'project.mt_task_ready' - elif 'user_id' in init_values and record.user_id: # assigned -> new + elif 'user_id' in init_values and self.user_id: # assigned -> new return 'project.mt_task_new' - elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1: # start stage -> 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(cr, uid, ids, init_values, context=context) + return super(Task, self)._track_subtype(init_values) - def _notification_group_recipients(self, cr, uid, ids, message, recipients, done_ids, group_data, context=None): + @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.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'project.group_project_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_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(cr, uid, ids, message, recipients, done_ids, group_data, context=context) + return super(Task, self)._notification_group_recipients(message, recipients, done_ids, group_data) - def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None): - res = super(task, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context) + @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(cr, uid, ids, 'assign', context=context) - new_action_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'project.action_view_task') - new_action = self._notification_link_helper(cr, uid, ids, 'new', context=context, action_id=new_action_id) + 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) - task_record = self.browse(cr, uid, ids[0], context=context) actions = [] - if not task_record.user_id: + if not self.user_id: actions.append({'url': take_action, 'title': _('I take it')}) else: actions.append({'url': new_action, 'title': _('New Task')}) @@ -663,39 +604,42 @@ class task(osv.osv): } return res - @api.cr_uid_context - def message_get_reply_to(self, cr, uid, ids, default=None, context=None): + @api.model + def message_get_reply_to(self, res_ids, default=None): """ Override to get the reply_to of the parent project. """ - tasks = self.browse(cr, SUPERUSER_ID, ids, context=context) - project_ids = set([task.project_id.id for task in tasks if task.project_id]) - aliases = self.pool['project.project'].message_get_reply_to(cr, uid, list(project_ids), default=default, context=context) - return dict((task.id, aliases.get(task.project_id and task.project_id.id or 0, False)) for task in tasks) + 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} - def email_split(self, cr, uid, ids, msg, context=None): + @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 - task_ids = self.browse(cr, uid, ids, context=context) - aliases = [task.project_id.alias_name for task in task_ids if task.project_id] + aliases = self.mapped('project_id.alias_name') return filter(lambda x: x.split('@')[0] not in aliases, email_list) - def message_new(self, cr, uid, msg, custom_values=None, context=None): + @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', False) + 'partner_id': msg.get('author_id') } defaults.update(custom_values) - res = super(task, self).message_new(cr, uid, msg, custom_values=defaults, context=context) - email_list = self.email_split(cr, uid, [res], msg, context=context) - partner_ids = filter(None, self._find_partner_from_emails(cr, uid, [res], email_list, force_create=False, context=context)) - self.message_subscribe(cr, uid, [res], partner_ids, context=context) + 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 - def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): + @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 = {} @@ -714,147 +658,127 @@ class task(osv.osv): except (ValueError, TypeError): pass - 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(task, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context) + 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) - def message_get_suggested_recipients(self, cr, uid, ids, context=None): - recipients = super(task, self).message_get_suggested_recipients(cr, uid, ids, context=context) - for data in self.browse(cr, uid, ids, context=context): - if data.partner_id: - reason = _('Customer Email') if data.partner_id.email else _('Customer') - data._message_add_suggested_recipient(recipients, partner=data.partner_id, reason=reason) + @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 - def message_get_email_values(self, cr, uid, ids, notif_mail=None, context=None): - res = super(task, self).message_get_email_values(cr, uid, ids, notif_mail=notif_mail, context=context) - current_task = self.browse(cr, uid, ids[0], context=context) + @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 current_task.project_id: + if self.project_id: current_objects = filter(None, headers.get('X-Odoo-Objects', '').split(',')) - current_objects.insert(0, 'project.project-%s, ' % current_task.project_id.id) + current_objects.insert(0, 'project.project-%s, ' % self.project_id.id) headers['X-Odoo-Objects'] = ','.join(current_objects) - if current_task.tag_ids: - headers['X-Odoo-Tags'] = ','.join([tag.name for tag in current_task.tag_ids]) + if self.tag_ids: + headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name')) res['headers'] = repr(headers) return res -class account_analytic_account(osv.osv): +class AccountAnalyticAccount(models.Model): _inherit = 'account.analytic.account' _description = 'Analytic Account' - def _compute_project_count(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, 0) - for account in self.browse(cr, uid, ids, context=context): - result[account.id] = len(account.project_ids) - return result + 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') - _columns = { - 'use_tasks': fields.boolean(string='Use Tasks', help="Check this box to manage internal activities through this project"), - 'company_uom_id': fields.related('company_id', 'project_time_mode_id', string="Company UOM", type='many2one', relation='product.uom'), - 'project_ids': fields.one2many('project.project', 'analytic_account_id', 'Projects'), - 'project_count': fields.function(_compute_project_count, string='Project Count', type='integer') - } + def _compute_project_count(self): + for account in self: + account.project_count = len(account.project_ids) - 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_tasks'] = template.use_tasks - return res - - def _trigger_project_creation(self, cr, uid, vals, context=None): + @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. ''' - if context is None: context = {} - return vals.get('use_tasks') and not 'project_creation_in_progress' in context + return vals.get('use_tasks') and 'project_creation_in_progress' not in self.env.context - @api.cr_uid_id_context - def project_create(self, cr, uid, analytic_account_id, vals, context=None): + @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. ''' - project_pool = self.pool.get('project.project') - project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)]) - if not project_id and self._trigger_project_creation(cr, uid, vals, context=context): + self.ensure_one() + Project = self.env['project.project'] + project = Project.search([('analytic_account_id', '=', self.id)]) + if not project and self._trigger_project_creation(vals): project_values = { 'name': vals.get('name'), - 'analytic_account_id': analytic_account_id, + 'analytic_account_id': self.id, 'use_tasks': True, } - return project_pool.create(cr, uid, project_values, context=context) + return Project.create(project_values).id return False - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context) - self.project_create(cr, uid, analytic_account_id, vals, context=context) - return analytic_account_id + @api.model + def create(self, vals): + analytic_account = super(AccountAnalyticAccount, self).create(vals) + analytic_account.project_create(vals) + return analytic_account - def write(self, cr, uid, ids, vals, context=None): - if isinstance(ids, (int, long)): - ids = [ids] + @api.multi + def write(self, vals): vals_for_project = vals.copy() - for account in self.browse(cr, uid, ids, context=context): + for account in self: if not vals.get('name'): vals_for_project['name'] = account.name - self.project_create(cr, uid, account.id, vals_for_project, context=context) - return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context) + account.project_create(vals_for_project) + return super(AccountAnalyticAccount, self).write(vals) - def unlink(self, cr, uid, ids, context=None): - proj_ids = self.pool['project.project'].search(cr, uid, [('analytic_account_id', 'in', ids)]) - has_tasks = self.pool['project.task'].search(cr, uid, [('project_id', 'in', proj_ids)], count=True, context=context) + @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(account_analytic_account, self).unlink(cr, uid, ids, context=context) + return super(AccountAnalyticAccount, self).unlink() - def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): if args is None: args = [] - if context is None: - context={} - if context.get('current_model') == 'project.project': - project_ids = self.search(cr, uid, args + [('name', operator, name)], limit=limit, context=context) - return self.name_get(cr, uid, project_ids, context=context) + if self.env.context.get('current_model') == 'project.project': + return self.search(args + [('name', operator, name)], limit=limit).name_get() - return super(account_analytic_account, self).name_search(cr, uid, name, args=args, operator=operator, context=context, limit=limit) + return super(AccountAnalyticAccount, self).name_search(name, args=args, operator=operator, limit=limit) - def projects_action(self, cr, uid, ids, context=None): - accounts = self.browse(cr, uid, ids, context=context) - project_ids = sum([account.project_ids.ids for account in accounts], []) + @api.multi + def projects_action(self): + projects = self.mapped('project_ids') result = { "type": "ir.actions.act_window", "res_model": "project.project", "views": [[False, "tree"], [False, "form"]], - "domain": [["id", "in", project_ids]], + "domain": [["id", "in", projects.ids]], "context": {"create": False}, "name": "Projects", } - if len(project_ids) == 1: + if len(projects) == 1: result['views'] = [(False, "form")] - result['res_id'] = project_ids[0] + result['res_id'] = projects.id else: result = {'type': 'ir.actions.act_window_close'} return result - -class project_project(osv.osv): - _inherit = 'project.project' - _defaults = { - 'use_tasks': True - } - -class project_task_history(osv.osv): +class ProjectTaskHistory(models.Model): """ Tasks History, used for cumulative flow charts (Lean/Agile) """ @@ -863,13 +787,13 @@ class project_task_history(osv.osv): _rec_name = 'task_id' _log_access = False - def _get_date(self, cr, uid, ids, name, arg, context=None): - result = {} - for history in self.browse(cr, uid, ids, context=context): + @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: - result[history.id] = history.date + history.end_date = history.date continue - cr.execute('''select + self.env.cr.execute('''select date from project_task_history @@ -877,51 +801,31 @@ class project_task_history(osv.osv): task_id=%s and id>%s order by id limit 1''', (history.task_id.id, history.id)) - res = cr.fetchone() - result[history.id] = res and res[0] or False - return result + res = self.env.cr.fetchone() + history.end_date = res and res[0] or False - def _get_related_date(self, cr, uid, ids, context=None): - result = [] - for history in self.browse(cr, uid, ids, context=context): - cr.execute('''select - id - from - project_task_history - where - task_id=%s and - id<%s - order by id desc limit 1''', (history.task_id.id, history.id)) - res = cr.fetchone() - if res: - result.append(res[0]) - return result - - _columns = { - 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True), - 'type_id': fields.many2one('project.task.type', 'Stage'), - 'kanban_state': fields.selection([('normal', 'Normal'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], 'Kanban State', required=False), - 'date': fields.date('Date', select=True), - 'end_date': fields.function(_get_date, string='End Date', type="date", store={ - 'project.task.history': (_get_related_date, None, 20) - }), - 'remaining_hours': fields.float('Remaining Time', digits=(16, 2)), - 'planned_hours': fields.float('Planned Time', digits=(16, 2)), - 'user_id': fields.many2one('res.users', 'Responsible'), - } - _defaults = { - 'date': fields.date.context_today, - } + 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 project_tags(osv.Model): +class ProjectTags(models.Model): """ Tags of project's tasks (or issues) """ _name = "project.tags" _description = "Tags of project's tasks, issues..." - _columns = { - 'name': fields.char('Name', required=True), - 'color': fields.integer('Color Index'), - } + + name = fields.Char(required=True) + color = fields.Integer(string='Color Index') + _sql_constraints = [ - ('name_uniq', 'unique (name)', "Tag name already exists !"), + ('name_uniq', 'unique (name)', "Tag name already exists !"), ] diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 838209d48bc..4937c41d29e 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -107,7 +107,7 @@ - - + From 25dcc5e03cb3a54232431cfc403eb1950eba131d Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Wed, 15 Jun 2016 15:49:27 +0530 Subject: [PATCH 09/20] [IMP] project: use sql constraint for start/end date --- addons/project/models/project.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 3bf2d9d7f82..be666a636b4 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -216,11 +216,9 @@ class Project(models.Model): date_start = fields.Date(string='Start Date') date = fields.Date(string='Expiration Date', index=True, track_visibility='onchange') - # TODO: Why not using a SQL contraints ? - @api.constrains('date_start', 'date') - def _check_dates(self): - if any(self.filtered(lambda project: project.date_start and project.date and project.date_start > project.date)): - raise ValidationError(_('Error! project start-date must be lower than project end-date.')) + _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): From f077a6c7bdc4cec514eecd309cad02f0f14da5bb Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Thu, 16 Jun 2016 10:38:27 +0530 Subject: [PATCH 10/20] [IMP] project: replaced except_orm by AccessError in tests Comments were referring to Warning rather than AccessError but: - Warning is deprecated - The functions that are tested actually raise AccessError --- addons/project/tests/test_access_rights.py | 30 ++++++++-------------- addons/project/tests/test_project_base.py | 2 +- addons/project/tests/test_project_flow.py | 5 ++-- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py index b0979927812..4add7a186c3 100644 --- a/addons/project/tests/test_access_rights.py +++ b/addons/project/tests/test_access_rights.py @@ -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}) diff --git a/addons/project/tests/test_project_base.py b/addons/project/tests/test_project_base.py index 187dbf368bf..189fc0cab21 100644 --- a/addons/project/tests/test_project_base.py +++ b/addons/project/tests/test_project_base.py @@ -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): diff --git a/addons/project/tests/test_project_flow.py b/addons/project/tests/test_project_flow.py index 36b5c41c9b7..b928331d388 100644 --- a/addons/project/tests/test_project_flow.py +++ b/addons/project/tests/test_project_flow.py @@ -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: @@ -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 From b9e8bc56878561592571f4af2d381fe4d2bae613 Mon Sep 17 00:00:00 2001 From: Ravi Gohil Date: Mon, 27 Jun 2016 16:28:11 +0200 Subject: [PATCH 11/20] [FIX] project: consider inactive projects for analytic accounts --- addons/project/models/project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index be666a636b4..a979fd74317 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -699,7 +699,7 @@ class AccountAnalyticAccount(models.Model): def _compute_project_count(self): for account in self: - account.project_count = len(account.project_ids) + account.project_count = len(account.with_context(active_test=False).project_ids) @api.model def _trigger_project_creation(self, vals): @@ -715,7 +715,7 @@ class AccountAnalyticAccount(models.Model): ''' self.ensure_one() Project = self.env['project.project'] - project = Project.search([('analytic_account_id', '=', self.id)]) + 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'), @@ -759,7 +759,7 @@ class AccountAnalyticAccount(models.Model): @api.multi def projects_action(self): - projects = self.mapped('project_ids') + projects = self.with_context(active_test=False).mapped('project_ids') result = { "type": "ir.actions.act_window", "res_model": "project.project", From 0239d9d4da5e5383a5c185b4af28585809e66b5f Mon Sep 17 00:00:00 2001 From: vbh-odoo Date: Mon, 27 Jul 2015 11:43:19 +0530 Subject: [PATCH 12/20] [MOV] project_issue: re-organized file structure --- addons/project_issue/__init__.py | 2 -- addons/project_issue/__openerp__.py | 12 ++++++------ .../project_issue/{ => data}/project_issue_data.xml | 0 .../project_issue/{ => data}/project_issue_demo.xml | 0 addons/project_issue/models/__init__.py | 5 +++++ addons/project_issue/{ => models}/project_issue.py | 0 .../{ => models}/project_issue_config_settings.py | 0 .../project_issue/{ => views}/project_dashboard.xml | 0 .../project_issue_config_settings_views.xml | 0 .../project_issue/{ => views}/project_issue_menu.xml | 0 .../project_issue/{ => views}/project_issue_view.xml | 0 11 files changed, 11 insertions(+), 8 deletions(-) rename addons/project_issue/{ => data}/project_issue_data.xml (100%) rename addons/project_issue/{ => data}/project_issue_demo.xml (100%) rename addons/project_issue/{ => models}/project_issue.py (100%) rename addons/project_issue/{ => models}/project_issue_config_settings.py (100%) rename addons/project_issue/{ => views}/project_dashboard.xml (100%) rename addons/project_issue/{ => views}/project_issue_config_settings_views.xml (100%) rename addons/project_issue/{ => views}/project_issue_menu.xml (100%) rename addons/project_issue/{ => views}/project_issue_view.xml (100%) diff --git a/addons/project_issue/__init__.py b/addons/project_issue/__init__.py index 87f09d513e1..54bcaa9cd53 100644 --- a/addons/project_issue/__init__.py +++ b/addons/project_issue/__init__.py @@ -2,6 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import models -import project_issue import report -import project_issue_config_settings diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index 6fdca5fd298..5cdd6aec9bb 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -19,16 +19,16 @@ 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', + 'views/project_issue_view.xml', + 'views/project_issue_config_settings_views.xml', + 'views/project_issue_menu.xml', 'report/project_issue_report_view.xml', 'security/project_issue_security.xml', 'security/ir.model.access.csv', - 'project_issue_data.xml', - 'project_dashboard.xml', + 'data/project_issue_data.xml', + 'views/project_dashboard.xml', ], - 'demo': ['project_issue_demo.xml'], + 'demo': ['data/project_issue_demo.xml'], 'test': [ 'test/issue_users.yml', 'test/subscribe_issue.yml', diff --git a/addons/project_issue/project_issue_data.xml b/addons/project_issue/data/project_issue_data.xml similarity index 100% rename from addons/project_issue/project_issue_data.xml rename to addons/project_issue/data/project_issue_data.xml diff --git a/addons/project_issue/project_issue_demo.xml b/addons/project_issue/data/project_issue_demo.xml similarity index 100% rename from addons/project_issue/project_issue_demo.xml rename to addons/project_issue/data/project_issue_demo.xml diff --git a/addons/project_issue/models/__init__.py b/addons/project_issue/models/__init__.py index 50bd1232d2e..7bb2eddb06b 100644 --- a/addons/project_issue/models/__init__.py +++ b/addons/project_issue/models/__init__.py @@ -1 +1,6 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import project_issue import project_task_type +import project_issue_config_settings diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/models/project_issue.py similarity index 100% rename from addons/project_issue/project_issue.py rename to addons/project_issue/models/project_issue.py diff --git a/addons/project_issue/project_issue_config_settings.py b/addons/project_issue/models/project_issue_config_settings.py similarity index 100% rename from addons/project_issue/project_issue_config_settings.py rename to addons/project_issue/models/project_issue_config_settings.py diff --git a/addons/project_issue/project_dashboard.xml b/addons/project_issue/views/project_dashboard.xml similarity index 100% rename from addons/project_issue/project_dashboard.xml rename to addons/project_issue/views/project_dashboard.xml diff --git a/addons/project_issue/project_issue_config_settings_views.xml b/addons/project_issue/views/project_issue_config_settings_views.xml similarity index 100% rename from addons/project_issue/project_issue_config_settings_views.xml rename to addons/project_issue/views/project_issue_config_settings_views.xml diff --git a/addons/project_issue/project_issue_menu.xml b/addons/project_issue/views/project_issue_menu.xml similarity index 100% rename from addons/project_issue/project_issue_menu.xml rename to addons/project_issue/views/project_issue_menu.xml diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/views/project_issue_view.xml similarity index 100% rename from addons/project_issue/project_issue_view.xml rename to addons/project_issue/views/project_issue_view.xml From 3eb717baecb590580bb3a098308822a6ed70cd42 Mon Sep 17 00:00:00 2001 From: vbh-odoo Date: Mon, 27 Jul 2015 13:00:08 +0530 Subject: [PATCH 13/20] [MOV] project_issue: split project_issue.py and project_issue_view files --- addons/project_issue/__openerp__.py | 7 +- ...data.xml => mail_message_subtype_data.xml} | 0 addons/project_issue/models/__init__.py | 3 + .../models/account_analytic_account.py | 35 +++++ addons/project_issue/models/project_issue.py | 120 ------------------ .../project_issue/models/project_project.py | 72 +++++++++++ addons/project_issue/models/res_partner.py | 21 +++ .../views/account_analytic_account_view.xml | 17 +++ .../views/project_issue_view.xml | 103 +++------------ .../views/project_project_view.xml | 36 ++++++ .../project_issue/views/res_partner_view.xml | 24 ++++ 11 files changed, 232 insertions(+), 206 deletions(-) rename addons/project_issue/data/{project_issue_data.xml => mail_message_subtype_data.xml} (100%) create mode 100644 addons/project_issue/models/account_analytic_account.py create mode 100644 addons/project_issue/models/project_project.py create mode 100644 addons/project_issue/models/res_partner.py create mode 100644 addons/project_issue/views/account_analytic_account_view.xml create mode 100644 addons/project_issue/views/project_project_view.xml create mode 100644 addons/project_issue/views/res_partner_view.xml diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index 5cdd6aec9bb..7637a61e7ce 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -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. """, @@ -21,11 +21,14 @@ It allows the manager to quickly check the issues, assign them and decide on the 'data': [ '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', 'report/project_issue_report_view.xml', 'security/project_issue_security.xml', 'security/ir.model.access.csv', - 'data/project_issue_data.xml', + 'data/mail_message_subtype_data.xml', 'views/project_dashboard.xml', ], 'demo': ['data/project_issue_demo.xml'], diff --git a/addons/project_issue/data/project_issue_data.xml b/addons/project_issue/data/mail_message_subtype_data.xml similarity index 100% rename from addons/project_issue/data/project_issue_data.xml rename to addons/project_issue/data/mail_message_subtype_data.xml diff --git a/addons/project_issue/models/__init__.py b/addons/project_issue/models/__init__.py index 7bb2eddb06b..859bb01a7b3 100644 --- a/addons/project_issue/models/__init__.py +++ b/addons/project_issue/models/__init__.py @@ -2,5 +2,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 diff --git a/addons/project_issue/models/account_analytic_account.py b/addons/project_issue/models/account_analytic_account.py new file mode 100644 index 00000000000..3dc67c26f6f --- /dev/null +++ b/addons/project_issue/models/account_analytic_account.py @@ -0,0 +1,35 @@ +# -*- 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 _ +from openerp.exceptions import UserError + + +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) diff --git a/addons/project_issue/models/project_issue.py b/addons/project_issue/models/project_issue.py index 9302bf45d57..bc929a67352 100644 --- a/addons/project_issue/models/project_issue.py +++ b/addons/project_issue/models/project_issue.py @@ -457,123 +457,3 @@ class project_issue(osv.Model): 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'), - } diff --git a/addons/project_issue/models/project_project.py b/addons/project_issue/models/project_project.py new file mode 100644 index 00000000000..490c837f700 --- /dev/null +++ b/addons/project_issue/models/project_project.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields, osv +from openerp import api + + +class project_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",), + '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) + + @api.multi + def write(self, vals): + self._check_create_write_values(vals) + res = super(project_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 diff --git a/addons/project_issue/models/res_partner.py b/addons/project_issue/models/res_partner.py new file mode 100644 index 00000000000..7e9655a41c9 --- /dev/null +++ b/addons/project_issue/models/res_partner.py @@ -0,0 +1,21 @@ +# -*- 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): + """ Inherits partner and adds Issue information in the partner form """ + _inherit = 'res.partner' + + 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() + } + + _columns = { + 'issue_count': fields.function(_issue_count, string='# Issues', type='integer'), + } diff --git a/addons/project_issue/views/account_analytic_account_view.xml b/addons/project_issue/views/account_analytic_account_view.xml new file mode 100644 index 00000000000..c1f787abecb --- /dev/null +++ b/addons/project_issue/views/account_analytic_account_view.xml @@ -0,0 +1,17 @@ + + + + + account.analytic.account.issue.form.inherit + account.analytic.account + + + + + + + + + + diff --git a/addons/project_issue/views/project_issue_view.xml b/addons/project_issue/views/project_issue_view.xml index 7d20e4b9055..6bd3295c085 100644 --- a/addons/project_issue/views/project_issue_view.xml +++ b/addons/project_issue/views/project_issue_view.xml @@ -257,52 +257,6 @@ - - - project.project.form.inherited - project.project - - - - on_change_use_tasks_or_issues(use_tasks, use_issues) - - - 0 - - -
- -
-
-
- -
- - {'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'} - -
-
- - - account.analytic.account.issue.form.inherit - account.analytic.account - - - - - - - - - project.issue form @@ -317,47 +271,28 @@
- - - res.partner.issues.button.view - res.partner - - - - -
- -
+ + + Issues + project.issue + form + tree,form + + + {"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'} + + current + +

+ Click to create an issue. +

+ You can track your issues from this menu and the action we + will take. +

- - - Issues - project.issue - form - tree,form - - - {"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'} - - current - -

- Click to create an issue. -

- You can track your issues from this menu and the action we - will take. -

-
-
- - diff --git a/addons/project_issue/views/project_project_view.xml b/addons/project_issue/views/project_project_view.xml new file mode 100644 index 00000000000..35e0f140f1b --- /dev/null +++ b/addons/project_issue/views/project_project_view.xml @@ -0,0 +1,36 @@ + + + + + project.project.form.inherited + project.project + + + + on_change_use_tasks_or_issues(use_tasks, use_issues) + + + 0 + + +
+ +
+
+
+ +
+ + {'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'} + +
+
+
+
diff --git a/addons/project_issue/views/res_partner_view.xml b/addons/project_issue/views/res_partner_view.xml new file mode 100644 index 00000000000..d018464c01b --- /dev/null +++ b/addons/project_issue/views/res_partner_view.xml @@ -0,0 +1,24 @@ + + + + + + res.partner.issues.button.view + res.partner + + + + +
+ +
+
+
+ +
+
From e70605f2d19c70e108207ca702b616afe7e92e8c Mon Sep 17 00:00:00 2001 From: vbh-odoo Date: Tue, 28 Jul 2015 18:18:08 +0530 Subject: [PATCH 14/20] [MIG] project_issue: migration to new api --- addons/project_issue/__init__.py | 2 +- addons/project_issue/__openerp__.py | 14 +- .../data/mail_message_subtype_data.xml | 9 +- .../project_issue/data/project_issue_demo.xml | 94 ++- addons/project_issue/models/__init__.py | 2 +- .../models/account_analytic_account.py | 41 +- addons/project_issue/models/project_issue.py | 555 ++++++++---------- .../project_issue/models/project_project.py | 77 +-- .../project_issue/models/project_task_type.py | 4 +- addons/project_issue/models/res_partner.py | 21 +- .../report/project_issue_report.py | 49 +- .../report/project_issue_report_view.xml | 10 +- .../security/project_issue_security.xml | 8 +- .../views/account_analytic_account_view.xml | 32 +- .../project_issue/views/project_dashboard.xml | 1 + .../views/project_issue_menu.xml | 25 +- .../views/project_issue_view.xml | 61 +- .../views/project_project_view.xml | 62 +- .../project_issue/views/res_partner_view.xml | 45 +- 19 files changed, 469 insertions(+), 643 deletions(-) diff --git a/addons/project_issue/__init__.py b/addons/project_issue/__init__.py index 54bcaa9cd53..a5f67aebd79 100644 --- a/addons/project_issue/__init__.py +++ b/addons/project_issue/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import models diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index 7637a61e7ce..d003b39a9dd 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. { @@ -19,18 +19,18 @@ It allows the manager to quickly check the issues, assign them and decide on the 'project', ], 'data': [ + 'data/mail_message_subtype_data.xml', + 'report/project_issue_report_view.xml', + 'security/project_issue_security.xml', + 'security/ir.model.access.csv', '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', - 'report/project_issue_report_view.xml', - 'security/project_issue_security.xml', - 'security/ir.model.access.csv', - 'data/mail_message_subtype_data.xml', 'views/project_dashboard.xml', - ], + ], 'demo': ['data/project_issue_demo.xml'], 'test': [ 'test/issue_users.yml', @@ -38,7 +38,5 @@ It allows the manager to quickly check the issues, assign them and decide on the 'test/issue_process.yml', 'test/issue_demo.yml' ], - 'installable': True, - 'auto_install': False, 'application': True, } diff --git a/addons/project_issue/data/mail_message_subtype_data.xml b/addons/project_issue/data/mail_message_subtype_data.xml index 4c425ff2104..2386d408295 100644 --- a/addons/project_issue/data/mail_message_subtype_data.xml +++ b/addons/project_issue/data/mail_message_subtype_data.xml @@ -1,9 +1,9 @@ - - + + - + mail.channel notification @@ -73,6 +73,5 @@ Access all issues from the top Project menu, and access the issues of a specific project_id - - + diff --git a/addons/project_issue/data/project_issue_demo.xml b/addons/project_issue/data/project_issue_demo.xml index 09f80d2d493..b2431710833 100644 --- a/addons/project_issue/data/project_issue_demo.xml +++ b/addons/project_issue/data/project_issue_demo.xml @@ -1,7 +1,5 @@ - - - - + + @@ -21,7 +19,6 @@
- @@ -37,13 +34,11 @@ - - 0 + - - + Bug in Accounts module agr@agrolait.com @@ -52,23 +47,20 @@ - + 1 - - + Program not giving proper output - - 0 + - - + Output incorrect @@ -78,36 +70,33 @@ - + 1 - - + Problem loading page - + 1 - - + Page not Found - + 1 - - + Programming Error @@ -117,72 +106,65 @@ - + 2 - - + Logical Error in Program - + 2 - - + Constraint Error - + 2 - - + Error in Program - + 2 - - + Patches Error in Program - - 0 + - - + New Features To Be Added - + 1 - - + Add menus to the module info@opensides.be @@ -190,12 +172,11 @@ - + 1 - - + Include Attendance sheet in Project contact@tecsas.fr @@ -203,44 +184,41 @@ - + 1 - - + Create new object - - 0 + - - + Improve Reports in HRMS - + 2 - - + Improve Reports in PMS + - + diff --git a/addons/project_issue/models/__init__.py b/addons/project_issue/models/__init__.py index 859bb01a7b3..96cee86e356 100644 --- a/addons/project_issue/models/__init__.py +++ b/addons/project_issue/models/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import project_issue diff --git a/addons/project_issue/models/account_analytic_account.py b/addons/project_issue/models/account_analytic_account.py index 3dc67c26f6f..e6de19fe2b7 100644 --- a/addons/project_issue/models/account_analytic_account.py +++ b/addons/project_issue/models/account_analytic_account.py @@ -1,35 +1,30 @@ # -*- 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 _ -from openerp.exceptions import UserError +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class account_analytic_account(osv.Model): +class AccountAnalyticAccount(models.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"), - } + use_issues = fields.Boolean(string='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) + @api.multi + def on_change_template(self, template_id, date_start=False): + res = super(AccountAnalyticAccount, self).on_change_template(template_id, date_start=date_start) if template_id and 'value' in res: - template = self.browse(cr, uid, template_id, context=context) - res['value']['use_issues'] = template.use_issues + account_analytic_account = self.browse(template_id) + res['value']['use_issues'] = account_analytic_account.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: + @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(account_analytic_account, self).unlink(cr, uid, ids, context=context) + 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) diff --git a/addons/project_issue/models/project_issue.py b/addons/project_issue/models/project_issue.py index bc929a67352..8d4dacd0caf 100644 --- a/addons/project_issue/models/project_issue.py +++ b/addons/project_issue/models/project_issue.py @@ -1,304 +1,223 @@ # -*- 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 _ +from odoo import api, fields, models, tools, _ +from odoo.exceptions import AccessError +from odoo.tools.safe_eval import safe_eval as eval -class project_issue(osv.Model): +class ProjectIssue(models.Model): _name = "project.issue" _description = "Project Issue" - _order = "priority desc, create_date desc" _inherit = ['mail.thread', 'ir.needaction_mixin'] + _order = "priority desc, create_date desc" _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 + @api.model + def _get_default_stage_id(self): + return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', 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) + 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) - 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 + 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 context: - search_domain = ['|', ('project_ids', '=', context['default_project_id']), ('id', 'in', 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', ids)] + search_domain = [('id', 'in', self.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) + 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 - 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 + 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 - 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) + @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 - def create(self, cr, uid, vals, context=None): - context = dict(context or {}) - if vals.get('project_id') and not context.get('default_project_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() + 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']) + vals.update(self.update_date_closed(vals['stage_id'])) # 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) + context['mail_create_nolog'] = True + return super(ProjectIssue, self.with_context(context)).create(vals) - def write(self, cr, uid, ids, vals, context=None): + @api.multi + def write(self, vals): # 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() + 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() + vals['date_open'] = fields.Datetime.now() + return super(ProjectIssue, self).write(vals) - 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) + @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 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 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, cr, uid, cases, project_id, domain=[], order='sequence', context=None): + 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: - - 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 = [] + search_domain = list(domain) if domain else [] 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 + 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 @@ -306,57 +225,57 @@ class project_issue(osv.Model): @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'}) + 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, 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': + 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 record.kanban_state == 'done': + elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'project_issue.mt_issue_ready' - elif 'user_id' in init_values and record.user_id: # assigned -> new + 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 record.stage_id and record.stage_id.sequence <= 1: # start stage -> 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(project_issue, self)._track_subtype(cr, uid, ids, init_values, context=context) + return super(ProjectIssue, self)._track_subtype(init_values) - def _notification_group_recipients(self, cr, uid, ids, message, recipients, done_ids, group_data, context=None): + @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.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') + 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 in recipient.user_ids[0].groups_id.ids: + 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(project_issue, self)._notification_group_recipients(cr, uid, ids, message, recipients, done_ids, group_data, context=context) + return super(ProjectIssue, self)._notification_group_recipients(message, recipients, done_ids, group_data) - 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) + @api.multi + def _notification_get_recipient_groups(self, message, recipients): + self.ensure_one() + res = super(ProjectIssue, self)._notification_get_recipient_groups(message, recipients) - 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: + 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'] = { @@ -364,18 +283,19 @@ class project_issue(osv.Model): } return res - @api.cr_uid_context - def message_get_reply_to(self, cr, uid, ids, default=None, context=None): + @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(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) + 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) - 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) + @api.multi + def message_get_suggested_recipients(self): + recipients = super(ProjectIssue, self).message_get_suggested_recipients() try: - for issue in self.browse(cr, uid, ids, context=context): + for issue in self: if issue.partner_id: issue._message_add_suggested_recipient(recipients, partner=issue.partner_id, reason=_('Customer')) elif issue.email_from: @@ -384,14 +304,14 @@ class project_issue(osv.Model): pass return recipients - def email_split(self, cr, uid, ids, msg, context=None): + @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 - 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) + return filter(lambda x: x.split('@')[0] not in self.mapped('project_id.alias_name'), email_list) - def message_new(self, cr, uid, msg, custom_values=None, context=None): + @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. @@ -400,60 +320,59 @@ class project_issue(osv.Model): # 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 = dict(self.env.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) + if custom_values: + 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) + 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 - def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): + @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) - 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.multi @api.returns('mail.message', lambda value: value.id) - def message_post(self, cr, uid, thread_id, subtype=None, context=None, **kwargs): + 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. """ - 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 + 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 - 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) + @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 current_issue.project_id: + if self.project_id: current_objects = filter(None, headers.get('X-Odoo-Objects', '').split(',')) - current_objects.insert(0, 'project.project-%s, ' % current_issue.project_id.id) + current_objects.insert(0, 'project.project-%s, ' % self.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]) + if self.tag_ids: + headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name')) res['headers'] = repr(headers) return res diff --git a/addons/project_issue/models/project_project.py b/addons/project_issue/models/project_project.py index 490c837f700..acc0b37b1a4 100644 --- a/addons/project_issue/models/project_project.py +++ b/addons/project_issue/models/project_project.py @@ -1,70 +1,43 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp import api +from odoo import api, fields, models -class project_project(osv.Model): +class Project(models.Model): _inherit = 'project.project' - def _get_alias_models(self, cr, uid, context=None): - res = super(project, self)._get_alias_models(cr, uid, context=context) + 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_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): + 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)) - 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",), - '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) + @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): - self._check_create_write_values(vals) - res = super(project_project, self).write(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') diff --git a/addons/project_issue/models/project_task_type.py b/addons/project_issue/models/project_task_type.py index e214a2c7ef3..07515890ba0 100644 --- a/addons/project_issue/models/project_task_type.py +++ b/addons/project_issue/models/project_task_type.py @@ -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): diff --git a/addons/project_issue/models/res_partner.py b/addons/project_issue/models/res_partner.py index 7e9655a41c9..2ec0f77a5f7 100644 --- a/addons/project_issue/models/res_partner.py +++ b/addons/project_issue/models/res_partner.py @@ -1,21 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from odoo import fields, models -class res_partner(osv.osv): - """ Inherits partner and adds Issue information in the partner form """ +class ResPartner(models.Model): _inherit = 'res.partner' - 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() - } + issue_count = fields.Integer(compute='_compute_issue_count', string='# Issues') - _columns = { - 'issue_count': fields.function(_issue_count, string='# Issues', type='integer'), - } + 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])]) diff --git a/addons/project_issue/report/project_issue_report.py b/addons/project_issue/report/project_issue_report.py index 6a6b2b07e99..63d5281d885 100644 --- a/addons/project_issue/report/project_issue_report.py +++ b/addons/project_issue/report/project_issue_report.py @@ -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 = 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") + 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') diff --git a/addons/project_issue/report/project_issue_report_view.xml b/addons/project_issue/report/project_issue_report_view.xml index 5c8b58f8619..69dde2a4e32 100644 --- a/addons/project_issue/report/project_issue_report_view.xml +++ b/addons/project_issue/report/project_issue_report_view.xml @@ -1,6 +1,5 @@ - - + project.issue.report.pivot project.issue.report @@ -93,9 +92,6 @@ 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. - - - - - + diff --git a/addons/project_issue/security/project_issue_security.xml b/addons/project_issue/security/project_issue_security.xml index bb1e519f604..f6608700ab9 100644 --- a/addons/project_issue/security/project_issue_security.xml +++ b/addons/project_issue/security/project_issue_security.xml @@ -1,15 +1,15 @@ - + - + Project/Issue: project manager: see all [(1, '=', 1)] - + Project/Issue: employees: employee or (followers and following) ['|', @@ -46,4 +46,4 @@ - + diff --git a/addons/project_issue/views/account_analytic_account_view.xml b/addons/project_issue/views/account_analytic_account_view.xml index c1f787abecb..8512cdc677c 100644 --- a/addons/project_issue/views/account_analytic_account_view.xml +++ b/addons/project_issue/views/account_analytic_account_view.xml @@ -1,17 +1,15 @@ - - - - - account.analytic.account.issue.form.inherit - account.analytic.account - - - - - - - - - - + + + + account.analytic.account.issue.form.inherit + account.analytic.account + + + + + + + + + diff --git a/addons/project_issue/views/project_dashboard.xml b/addons/project_issue/views/project_dashboard.xml index b7197fa086d..0138a09f382 100644 --- a/addons/project_issue/views/project_dashboard.xml +++ b/addons/project_issue/views/project_dashboard.xml @@ -1,3 +1,4 @@ + project.project.kanban.inherited diff --git a/addons/project_issue/views/project_issue_menu.xml b/addons/project_issue/views/project_issue_menu.xml index 020ad61a787..675b5061af4 100644 --- a/addons/project_issue/views/project_issue_menu.xml +++ b/addons/project_issue/views/project_issue_menu.xml @@ -1,11 +1,9 @@ - - - - + + - + Issues project.issue form @@ -16,49 +14,48 @@

- 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.

- + kanban - + tree - + calendar - + form - + graph - -
-
+
diff --git a/addons/project_issue/views/project_issue_view.xml b/addons/project_issue/views/project_issue_view.xml index 6bd3295c085..f897fd1f5db 100644 --- a/addons/project_issue/views/project_issue_view.xml +++ b/addons/project_issue/views/project_issue_view.xml @@ -1,9 +1,7 @@ - - - - - - Project Issue Tracker Form + + + + project.issue.form.view project.issue
@@ -30,11 +28,10 @@ - + - - + + @@ -67,8 +64,8 @@ - - Project Issue Tracker Tree + + project.issue.tree.view project.issue @@ -78,7 +75,7 @@ - + @@ -88,7 +85,7 @@ - Project Issue Tracker Search + project.issue.search.view project.issue @@ -121,8 +118,8 @@ - - Project Issue Tracker Calendar + + project.issue.calendar.view project.issue @@ -136,8 +133,8 @@ - - Project Issue Kanban + + project.issue.kanban.view project.issue @@ -198,8 +195,8 @@ - - Project Issue Tracker Pivot + + project.issue.pivot.view project.issue @@ -209,8 +206,8 @@ - - Project Issue Tracker Graph + + project.issue.graph.view project.issue @@ -222,8 +219,8 @@ - - Project Issue- Feature Tracker Tree + + project.issue.tree.view.featured project.issue @@ -250,7 +247,7 @@

- 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.

@@ -264,7 +261,7 @@ kanban,tree,form,calendar,graph

- 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.

@@ -286,14 +283,10 @@

Click to create an issue.

- You can track your issues from this menu and the action we - will take. + You can track your issues from this menu and the action we will take.

- - - - - + + diff --git a/addons/project_issue/views/project_project_view.xml b/addons/project_issue/views/project_project_view.xml index 35e0f140f1b..76bce706a8f 100644 --- a/addons/project_issue/views/project_project_view.xml +++ b/addons/project_issue/views/project_project_view.xml @@ -1,36 +1,28 @@ - - - - - project.project.form.inherited - project.project - - - - on_change_use_tasks_or_issues(use_tasks, use_issues) - - - 0 - - -
- -
-
-
- + + + + + project.project.form.inherited + project.project + + + + 0 + + +
+ +
- - {'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'} - -
-
- - + +
+ +
+ + +
diff --git a/addons/project_issue/views/res_partner_view.xml b/addons/project_issue/views/res_partner_view.xml index d018464c01b..aa2ac8a80b5 100644 --- a/addons/project_issue/views/res_partner_view.xml +++ b/addons/project_issue/views/res_partner_view.xml @@ -1,24 +1,21 @@ - - - - - - res.partner.issues.button.view - res.partner - - - - -
- -
-
-
- -
-
+ + + + + res.partner.issues.button.view.inherit + res.partner + + + + +
+ +
+
+
+
From ccfea6b2f614e55c14d8dd3c2ade78d5b91906ec Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Thu, 21 Apr 2016 12:40:10 +0200 Subject: [PATCH 15/20] [REM] project_issue: removed dead on_change_template override This function was removed in 9.0 at 1dbccc8215d6efe70f3e455a52d04ddb06eba1e3. --- addons/project_issue/models/account_analytic_account.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/addons/project_issue/models/account_analytic_account.py b/addons/project_issue/models/account_analytic_account.py index e6de19fe2b7..76d9439abce 100644 --- a/addons/project_issue/models/account_analytic_account.py +++ b/addons/project_issue/models/account_analytic_account.py @@ -10,14 +10,6 @@ class AccountAnalyticAccount(models.Model): use_issues = fields.Boolean(string='Use Issues', help="Check this box to manage customer activities through this project") - @api.multi - def on_change_template(self, template_id, date_start=False): - res = super(AccountAnalyticAccount, self).on_change_template(template_id, date_start=date_start) - if template_id and 'value' in res: - account_analytic_account = self.browse(template_id) - res['value']['use_issues'] = account_analytic_account.use_issues - return res - @api.multi def unlink(self): if self.env['project.issue'].search_count([('project_id.analytic_account_id', 'in', self.ids)]): From 42b498dc6f6bfd1d375c854d8b415295fbc84e65 Mon Sep 17 00:00:00 2001 From: vbh-odoo Date: Thu, 6 Aug 2015 16:29:42 +0530 Subject: [PATCH 16/20] [MIG] project_issue: converted test cases from yml to python --- addons/project_issue/__openerp__.py | 6 --- addons/project_issue/test/issue_demo.yml | 14 ------- addons/project_issue/test/issue_process.yml | 12 ------ addons/project_issue/test/issue_users.yml | 28 -------------- addons/project_issue/test/subscribe_issue.yml | 16 -------- addons/project_issue/tests/__init__.py | 6 +++ addons/project_issue/tests/common.py | 21 +++++++++++ .../project_issue/{test => tests}/issue.eml | 0 addons/project_issue/tests/test_issue_demo.py | 18 +++++++++ .../project_issue/tests/test_issue_process.py | 37 +++++++++++++++++++ .../tests/test_subscribe_issue.py | 19 ++++++++++ 11 files changed, 101 insertions(+), 76 deletions(-) delete mode 100644 addons/project_issue/test/issue_demo.yml delete mode 100644 addons/project_issue/test/issue_process.yml delete mode 100644 addons/project_issue/test/issue_users.yml delete mode 100644 addons/project_issue/test/subscribe_issue.yml create mode 100644 addons/project_issue/tests/__init__.py create mode 100644 addons/project_issue/tests/common.py rename addons/project_issue/{test => tests}/issue.eml (100%) create mode 100644 addons/project_issue/tests/test_issue_demo.py create mode 100644 addons/project_issue/tests/test_issue_process.py create mode 100644 addons/project_issue/tests/test_subscribe_issue.py diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index d003b39a9dd..7c62d6b7d4c 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -32,11 +32,5 @@ It allows the manager to quickly check the issues, assign them and decide on the 'views/project_dashboard.xml', ], 'demo': ['data/project_issue_demo.xml'], - 'test': [ - 'test/issue_users.yml', - 'test/subscribe_issue.yml', - 'test/issue_process.yml', - 'test/issue_demo.yml' - ], 'application': True, } diff --git a/addons/project_issue/test/issue_demo.yml b/addons/project_issue/test/issue_demo.yml deleted file mode 100644 index f84c716baeb..00000000000 --- a/addons/project_issue/test/issue_demo.yml +++ /dev/null @@ -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' - diff --git a/addons/project_issue/test/issue_process.yml b/addons/project_issue/test/issue_process.yml deleted file mode 100644 index 1bc1633688c..00000000000 --- a/addons/project_issue/test/issue_process.yml +++ /dev/null @@ -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 diff --git a/addons/project_issue/test/issue_users.yml b/addons/project_issue/test/issue_users.yml deleted file mode 100644 index a0ad327b86b..00000000000 --- a/addons/project_issue/test/issue_users.yml +++ /dev/null @@ -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 diff --git a/addons/project_issue/test/subscribe_issue.yml b/addons/project_issue/test/subscribe_issue.yml deleted file mode 100644 index a3645feb156..00000000000 --- a/addons/project_issue/test/subscribe_issue.yml +++ /dev/null @@ -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 ')]) - 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" diff --git a/addons/project_issue/tests/__init__.py b/addons/project_issue/tests/__init__.py new file mode 100644 index 00000000000..423b64bf971 --- /dev/null +++ b/addons/project_issue/tests/__init__.py @@ -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 diff --git a/addons/project_issue/tests/common.py b/addons/project_issue/tests/common.py new file mode 100644 index 00000000000..c5a1654188f --- /dev/null +++ b/addons/project_issue/tests/common.py @@ -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')])] + }) diff --git a/addons/project_issue/test/issue.eml b/addons/project_issue/tests/issue.eml similarity index 100% rename from addons/project_issue/test/issue.eml rename to addons/project_issue/tests/issue.eml diff --git a/addons/project_issue/tests/test_issue_demo.py b/addons/project_issue/tests/test_issue_demo.py new file mode 100644 index 00000000000..21846a17351 --- /dev/null +++ b/addons/project_issue/tests/test_issue_demo.py @@ -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') + }) diff --git a/addons/project_issue/tests/test_issue_process.py b/addons/project_issue/tests/test_issue_process.py new file mode 100644 index 00000000000..eeedfba39a4 --- /dev/null +++ b/addons/project_issue/tests/test_issue_process.py @@ -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': """ +

+ Hello Mr. Adersen, +

+

+ 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 ? +

+

+ Thank You. +

+
+--
+YourCompany
+info@yourcompany.example.com
++1 555 123 8069
+                    
+ """} + 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() diff --git a/addons/project_issue/tests/test_subscribe_issue.py b/addons/project_issue/tests/test_subscribe_issue.py new file mode 100644 index 00000000000..2577033bbf0 --- /dev/null +++ b/addons/project_issue/tests/test_subscribe_issue.py @@ -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 ')], 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") From b86284ecc2435181efb22a4c2f61f3d292b680de Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Mon, 18 Apr 2016 15:49:40 +0200 Subject: [PATCH 17/20] [REF] project_issue: renamed project.issue.report field nbr into nbr_issue --- addons/project_issue/report/project_issue_report.py | 4 ++-- addons/project_issue/report/project_issue_report_view.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/project_issue/report/project_issue_report.py b/addons/project_issue/report/project_issue_report.py index 63d5281d885..18939b43397 100644 --- a/addons/project_issue/report/project_issue_report.py +++ b/addons/project_issue/report/project_issue_report.py @@ -14,7 +14,7 @@ class ProjectIssueReport(models.Model): 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 + 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", @@ -46,7 +46,7 @@ class ProjectIssueReport(models.Model): 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, diff --git a/addons/project_issue/report/project_issue_report_view.xml b/addons/project_issue/report/project_issue_report_view.xml index 69dde2a4e32..68217744ca8 100644 --- a/addons/project_issue/report/project_issue_report_view.xml +++ b/addons/project_issue/report/project_issue_report_view.xml @@ -7,7 +7,7 @@ - + @@ -19,7 +19,7 @@ - + From 095abc3f4c7256a78fb75bfb6da2a47ace2a9b70 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Thu, 21 Apr 2016 16:34:09 +0200 Subject: [PATCH 18/20] [MOV] project_issue_sheet: re-organized file structure --- addons/project_issue_sheet/__init__.py | 2 +- addons/project_issue_sheet/__openerp__.py | 2 +- addons/project_issue_sheet/models/__init__.py | 4 ++++ .../{project_issue_sheet.py => models/project_issue.py} | 0 .../project_issue_view.xml} | 0 5 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 addons/project_issue_sheet/models/__init__.py rename addons/project_issue_sheet/{project_issue_sheet.py => models/project_issue.py} (100%) rename addons/project_issue_sheet/{project_issue_sheet_view.xml => views/project_issue_view.xml} (100%) diff --git a/addons/project_issue_sheet/__init__.py b/addons/project_issue_sheet/__init__.py index bb7a7d5d6ea..2b6fd3008e4 100644 --- a/addons/project_issue_sheet/__init__.py +++ b/addons/project_issue_sheet/__init__.py @@ -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 diff --git a/addons/project_issue_sheet/__openerp__.py b/addons/project_issue_sheet/__openerp__.py index 2e2f0ba30c5..23b81ca8187 100644 --- a/addons/project_issue_sheet/__openerp__.py +++ b/addons/project_issue_sheet/__openerp__.py @@ -17,7 +17,7 @@ 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', ], diff --git a/addons/project_issue_sheet/models/__init__.py b/addons/project_issue_sheet/models/__init__.py new file mode 100644 index 00000000000..0aad703f2b6 --- /dev/null +++ b/addons/project_issue_sheet/models/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import project_issue diff --git a/addons/project_issue_sheet/project_issue_sheet.py b/addons/project_issue_sheet/models/project_issue.py similarity index 100% rename from addons/project_issue_sheet/project_issue_sheet.py rename to addons/project_issue_sheet/models/project_issue.py diff --git a/addons/project_issue_sheet/project_issue_sheet_view.xml b/addons/project_issue_sheet/views/project_issue_view.xml similarity index 100% rename from addons/project_issue_sheet/project_issue_sheet_view.xml rename to addons/project_issue_sheet/views/project_issue_view.xml From 73df45d8c5112e20b287e82d8b2ca3294f1f51e9 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Thu, 21 Apr 2016 17:14:32 +0200 Subject: [PATCH 19/20] [MOV] project_issue_sheet: split project_issue_sheet.py file --- addons/project_issue_sheet/models/__init__.py | 1 + .../models/account_analytic_line.py | 11 +++++++++++ addons/project_issue_sheet/models/project_issue.py | 8 -------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 addons/project_issue_sheet/models/account_analytic_line.py diff --git a/addons/project_issue_sheet/models/__init__.py b/addons/project_issue_sheet/models/__init__.py index 0aad703f2b6..254e6f68d27 100644 --- a/addons/project_issue_sheet/models/__init__.py +++ b/addons/project_issue_sheet/models/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import project_issue +import account_analytic_line diff --git a/addons/project_issue_sheet/models/account_analytic_line.py b/addons/project_issue_sheet/models/account_analytic_line.py new file mode 100644 index 00000000000..b7cd479acdd --- /dev/null +++ b/addons/project_issue_sheet/models/account_analytic_line.py @@ -0,0 +1,11 @@ +#-*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp.osv import fields,osv + +class account_analytic_line(osv.osv): + _inherit = 'account.analytic.line' + _description = 'account analytic line' + _columns = { + 'issue_id' : fields.many2one('project.issue', 'Issue'), + } diff --git a/addons/project_issue_sheet/models/project_issue.py b/addons/project_issue_sheet/models/project_issue.py index 267a4d56d1a..8f26ff24878 100644 --- a/addons/project_issue_sheet/models/project_issue.py +++ b/addons/project_issue_sheet/models/project_issue.py @@ -43,11 +43,3 @@ class project_issue(osv.osv): 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'), - } From 5a6a701815ea826688a57e24ebc96812ecf225c9 Mon Sep 17 00:00:00 2001 From: David Monjoie Date: Thu, 21 Apr 2016 16:40:55 +0200 Subject: [PATCH 20/20] [MIG] project_issue_sheet: migration to new api --- addons/project_issue_sheet/__openerp__.py | 3 -- .../models/account_analytic_line.py | 10 ++-- .../models/project_issue.py | 52 +++++-------------- .../security/portal_security.xml | 22 ++++---- .../views/project_issue_view.xml | 15 ++---- 5 files changed, 35 insertions(+), 67 deletions(-) diff --git a/addons/project_issue_sheet/__openerp__.py b/addons/project_issue_sheet/__openerp__.py index 23b81ca8187..f367064d1e5 100644 --- a/addons/project_issue_sheet/__openerp__.py +++ b/addons/project_issue_sheet/__openerp__.py @@ -21,7 +21,4 @@ Worklogs can be maintained to signify number of hours spent by users to handle a 'security/ir.model.access.csv', 'security/portal_security.xml', ], - 'demo': [], - 'installable': True, - 'auto_install': False, } diff --git a/addons/project_issue_sheet/models/account_analytic_line.py b/addons/project_issue_sheet/models/account_analytic_line.py index b7cd479acdd..5ab61ec7321 100644 --- a/addons/project_issue_sheet/models/account_analytic_line.py +++ b/addons/project_issue_sheet/models/account_analytic_line.py @@ -1,11 +1,9 @@ #-*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields,osv +from odoo import fields, models -class account_analytic_line(osv.osv): +class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' - _description = 'account analytic line' - _columns = { - 'issue_id' : fields.many2one('project.issue', 'Issue'), - } + + issue_id = fields.Many2one('project.issue', 'Issue') diff --git a/addons/project_issue_sheet/models/project_issue.py b/addons/project_issue_sheet/models/project_issue.py index 8f26ff24878..99aa28b2f61 100644 --- a/addons/project_issue_sheet/models/project_issue.py +++ b/addons/project_issue_sheet/models/project_issue.py @@ -1,45 +1,21 @@ #-*- 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 _ +from odoo import api, fields, models -class project_issue(osv.osv): +class ProjectIssue(models.Model): _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 + 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') - 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 + @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 diff --git a/addons/project_issue_sheet/security/portal_security.xml b/addons/project_issue_sheet/security/portal_security.xml index 60d70e0e5fc..aedc93e1014 100644 --- a/addons/project_issue_sheet/security/portal_security.xml +++ b/addons/project_issue_sheet/security/portal_security.xml @@ -1,19 +1,21 @@ - + Project/Issue Timesheets: frontend access - ['|', - '&', - ('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]), - ] + + ['|', + '&', + ('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]), + ] + - + diff --git a/addons/project_issue_sheet/views/project_issue_view.xml b/addons/project_issue_sheet/views/project_issue_view.xml index a06bfe0cce2..940b281fea0 100644 --- a/addons/project_issue_sheet/views/project_issue_view.xml +++ b/addons/project_issue_sheet/views/project_issue_view.xml @@ -1,8 +1,7 @@ - - - - Project Issue Tracker Form + + + project.issue.form.inherited project.issue @@ -10,9 +9,6 @@ - - on_change_project(project_id) - - Project Issue Tracker Tree Inherited + projet.issue.tree.inherited project.issue @@ -41,5 +37,4 @@ - - +