diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index 1ee084d94f3..1b35f5f5c35 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -20,7 +20,7 @@ It is completely integrated with the cost accounting module. It allows you to se up a management by affair. """, 'website': 'https://www.odoo.com/page/employees', - 'depends': ['hr', 'project'], + 'depends': ['hr', 'analytic', 'project'], 'data': [ 'security/hr_timesheet_security.xml', 'security/ir.model.access.csv', diff --git a/addons/hr_timesheet/data/hr_timesheet_demo.xml b/addons/hr_timesheet/data/hr_timesheet_demo.xml index d3fbc2e3367..b3b26e3abb9 100644 --- a/addons/hr_timesheet/data/hr_timesheet_demo.xml +++ b/addons/hr_timesheet/data/hr_timesheet_demo.xml @@ -1,6 +1,20 @@ + + Research & Development Project + RD + + + + + + + + + + + Requirements analysis and specification @@ -35,7 +49,7 @@ 1.00 -30.00 - + Delivery and maintenance @@ -44,6 +58,6 @@ 1.00 -30.00 - + diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index 4db761e1e09..11998476ce2 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import analytic_account from . import hr from . import hr_timesheet from . import res_config_settings diff --git a/addons/project/models/analytic.py b/addons/hr_timesheet/models/analytic_account.py similarity index 72% rename from addons/project/models/analytic.py rename to addons/hr_timesheet/models/analytic_account.py index 9cb748e1bac..347634ec185 100644 --- a/addons/project/models/analytic.py +++ b/addons/hr_timesheet/models/analytic_account.py @@ -13,9 +13,12 @@ class AccountAnalyticAccount(models.Model): project_ids = fields.One2many('project.project', 'analytic_account_id', string='Projects') project_count = fields.Integer("Project Count", compute='_compute_project_count') + @api.multi def _compute_project_count(self): + project_data = self.env['project.project'].read_group([('analytic_account_id', 'in', self.ids)], ['analytic_account_id'], ['analytic_account_id']) + mapping = {m['analytic_account_id'][0]: m['analytic_account_id_count'] for m in project_data} for account in self: - account.project_count = len(account.with_context(active_test=False).project_ids) + account.project_count = mapping.get(account.id, 0) @api.multi def unlink(self): @@ -25,18 +28,9 @@ class AccountAnalyticAccount(models.Model): raise UserError(_('Please remove existing tasks in the project linked to the accounts you want to delete.')) return super(AccountAnalyticAccount, self).unlink() - @api.model - def name_search(self, name, args=None, operator='ilike', limit=100): - if args is None: - args = [] - if self.env.context.get('current_model') == 'project.project': - return self.search(args + [('name', operator, name)], limit=limit).name_get() - - return super(AccountAnalyticAccount, self).name_search(name, args=args, operator=operator, limit=limit) - @api.multi - def projects_action(self): - projects = self.with_context(active_test=False).mapped('project_ids') + def action_view_projects(self): + projects = self.mapped('project_ids') result = { "type": "ir.actions.act_window", "res_model": "project.project", diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index dd4b83dad8b..d1d41829e7b 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError class AccountAnalyticLine(models.Model): @@ -64,6 +65,8 @@ class AccountAnalyticLine(models.Model): if vals.get('project_id') and not vals.get('account_id'): project = self.env['project.project'].browse(vals.get('project_id')) vals['account_id'] = project.analytic_account_id.id + if not project.analytic_account_id.active: + raise UserError(_('The project you are timesheeting on is not linked to a active analytic account. Please the project configuration.')) # employee implies user if vals.get('employee_id') and not vals.get('user_id'): employee = self.env['hr.employee'].browse(vals['employee_id']) diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 72a88277886..cb41bf7c5bb 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -1,20 +1,89 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields, api -from odoo.exceptions import UserError -from odoo.tools.translate import _ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError class Project(models.Model): _inherit = "project.project" allow_timesheets = fields.Boolean("Allow timesheets", default=True) + analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", ondelete='set null', + 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.") + + @api.onchange('partner_id') + def _onchange_partner_id(self): + domain = [] + if self.partner_id: + domain = [('partner_id', '=', self.partner_id.id)] + return {'domain': {'analytic_account_id': domain}} + + @api.constrains('allow_timesheets', 'analytic_account_id') + def _check_allow_timesheet(self): + for project in self: + if project.allow_timesheets and not project.analytic_account_id: + raise ValidationError(_('To allow timesheet, your project %s should have an analytic account set.' % (project.name,))) + + @api.model + def name_create(self, name): + """ Create a project with name_create should generate analytic account creation """ + values = { + 'name': name, + 'allow_timesheets': True, + } + return self.create(values).name_get()[0] + + @api.model + def create(self, values): + """ Create an analytic account if project allow timesheet and don't provide one """ + allow_timesheets = values['allow_timesheets'] if 'allow_timesheets' in values else self.default_get(['allow_timesheets'])['allow_timesheets'] + if allow_timesheets and not values.get('analytic_account_id'): + analytic_account = self._create_analytic_account(values) + values['analytic_account_id'] = analytic_account.id + return super(Project, self).create(values) + + @api.multi + def write(self, values): + result = super(Project, self).write(values) + if values.get('allow_timesheets'): + for project in self: + if not project.analytic_account_id: + analytic_account = project._create_analytic_account({ + 'name': project.name, + 'company_id': project.company_id.id, + 'partner_id': project.partner_id.id, + }) + project.write({'analytic_account_id': analytic_account.id}) + return result + + @api.multi + def unlink(self): + """ Delete the empty related analytic account """ + analytic_accounts_to_delete = self.env['account.analytic.account'] + for project in self: + if project.analytic_account_id and not project.analytic_account_id.line_ids: + analytic_accounts_to_delete |= project.analytic_account_id + result = super(Project, self).unlink() + analytic_accounts_to_delete.unlink() + return result + + def _create_analytic_account(self, values): + analytic_account = self.env['account.analytic.account'].create({ + 'name': values.get('name', _('Unkwon Analytic Account')), + 'company_id': values.get('company_id', self.env.user.company_id.id), + 'partner_id': values.get('partner_id'), + 'active': True, + }) + return analytic_account class Task(models.Model): _inherit = "project.task" + analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", related='project_id.analytic_account_id', readonly=True) + allow_timesheets = fields.Boolean("Allow timesheets", compute='_compute_allow_timesheets', help="Timesheets can be logged on this task.") remaining_hours = fields.Float("Remaining Hours", compute='_compute_progress_hours', inverse='_inverse_remaining_hours', store=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.") effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.") total_hours_spent = fields.Float("Total Hours", compute='_compute_progress_hours', store=True, help="Computed as: Time Spent + Sub-tasks Hours.") @@ -22,10 +91,10 @@ class Task(models.Model): subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)", oldname='children_hours') timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets') - @api.onchange('remaining_hours') - def _inverse_remaining_hours(self): + @api.depends('project_id.allow_timesheets', 'project_id.analytic_account_id') + def _compute_allow_timesheets(self): for task in self: - task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours + task.allow_timesheets = task.project_id.allow_timesheets and task.project_id.analytic_account_id.active @api.depends('timesheet_ids.unit_amount') def _compute_effective_hours(self): @@ -47,6 +116,11 @@ class Task(models.Model): task.remaining_hours = task.planned_hours - task.effective_hours - task.subtask_effective_hours task.total_hours_spent = task.effective_hours + task.subtask_effective_hours + @api.onchange('remaining_hours') + def _inverse_remaining_hours(self): + for task in self: + task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours + @api.depends('child_ids.effective_hours', 'child_ids.subtask_effective_hours') def _compute_subtask_effective_hours(self): for task in self: diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index 87656c6ef0c..63bc0cace28 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -2,6 +2,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1 access_account_analytic_line_portal,access_timesheets_portal,analytic.model_account_analytic_line,base.group_portal,1,0,0,0 access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.model_account_analytic_account,hr_timesheet.group_hr_timesheet_user,1,1,0,0 +access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0 +access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1 +access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0 +access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 access_product_product_user,product.product.timesheet.user,product.model_product_product,hr_timesheet.group_hr_timesheet_user,1,1,1,0 access_product_template_timesheet_user,product.template.timesheet.user,product.model_product_template,hr_timesheet.group_hr_timesheet_user,1,1,1,0 access_uom_uom_hr_timesheet,uom.uom.timesheet.user,uom.model_uom_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py index f5ad8f40076..616af044ff7 100644 --- a/addons/hr_timesheet/tests/test_timesheet.py +++ b/addons/hr_timesheet/tests/test_timesheet.py @@ -17,11 +17,18 @@ class TestCommonTimesheet(TransactionCase): 'customer': True, }) + self.analytic_account = self.env['account.analytic.account'].create({ + 'name': 'Analytic Account for Test Customer', + 'partner_id': self.partner.id, + 'code': 'TEST' + }) + # project and tasks self.project_customer = self.env['project.project'].create({ 'name': 'Project X', 'allow_timesheets': True, 'partner_id': self.partner.id, + 'analytic_account_id': self.analytic_account.id, }) self.task1 = self.env['project.task'].create({ 'name': 'Task One', diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index 9cda6102048..ccc13c433c1 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -1,6 +1,26 @@ + + + account.analytic.account.form.inherit + account.analytic.account + + + + + + + + + + + + + + @@ -58,7 +61,9 @@ - + + + @@ -71,7 +76,12 @@ - + +
+ Your project is linked to an inactive analytic account. Change it, or reactivate the current one to timesheet on the project. +
+
+ @@ -81,13 +91,13 @@ - - - - - - - + + + + + + +
diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 1bec9a995fe..5da8fb35d0c 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -11,7 +11,6 @@ 'depends': [ 'base_setup', 'product', - 'analytic', 'mail', 'portal', 'rating', diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index c020486360b..e202189a304 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import analytic from . import project from . import res_config_settings from . import res_company diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 4b32b7bc4d3..cad7a8b89dc 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -69,7 +69,6 @@ class Project(models.Model): _name = "project.project" _description = "Project" _inherit = ['portal.mixin', 'mail.alias.mixin', 'mail.thread'] - _inherits = {'account.analytic.account': "analytic_account_id"} _order = "sequence, name, id" _period_number = 5 @@ -83,15 +82,10 @@ class Project(models.Model): @api.multi def unlink(self): - analytic_accounts_to_delete = self.env['account.analytic.account'] for project in self: if project.tasks: raise UserError(_('You cannot delete a project containing tasks. You can either delete all the project\'s tasks and then delete the project or simply deactivate the project.')) - if project.analytic_account_id and not project.analytic_account_id.line_ids: - analytic_accounts_to_delete |= project.analytic_account_id - res = super(Project, self).unlink() - analytic_accounts_to_delete.unlink() - return res + return super(Project, self).unlink() def _compute_attached_docs_count(self): Attachment = self.env['ir.attachment'] @@ -179,14 +173,14 @@ class Project(models.Model): def _get_default_favorite_user_ids(self): return [(6, 0, [self.env.uid])] + name = fields.Char("Name", index=True, required=True, track_visibility='onchange') 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) + partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True, track_visibility='onchange') + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + currency_id = fields.Many2one('res.currency', related="company_id.currency_id", string="Currency", readonly=True) + favorite_user_ids = fields.Many2many( 'res.users', 'project_favorite_user_rel', 'project_id', 'user_id', default=_get_default_favorite_user_ids, @@ -315,8 +309,6 @@ class Project(models.Model): if 'active' in vals: # archiving/unarchiving a project does it on its tasks, too self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']}) - # archiving/unarchiving a project implies that we don't want to use the analytic account anymore - self.with_context(active_test=False).mapped('analytic_account_id').write({'active': vals['active']}) if vals.get('partner_id') or vals.get('privacy_visibility'): for project in self.filtered(lambda project: project.privacy_visibility == 'portal'): project.message_subscribe(project.partner_id.ids) diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 746cc34d51c..ed22a1b4da7 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -1,9 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_project_project,project.project,model_project_project,project.group_project_user,1,0,0,0 access_project_project_manager,project.project,model_project_project,project.group_project_manager,1,1,1,1 -access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0 -access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1 -access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0 access_project_task_type_user,project.task.type.user,model_project_task_type,base.group_user,1,0,0,0 access_project_task_type_project_user,project.task.type.project.user,model_project_task_type,project.group_project_user,1,0,0,0 access_project_task_type_manager,project.task.type manager,model_project_task_type,project.group_project_manager,1,1,1,1 @@ -15,7 +12,6 @@ access_task_on_partner,project.task on partners,model_project_task,base.group_us access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0 access_project_user,project.project on partners,model_project_project,base.group_user,1,0,0,0 access_project_portal,project_portal,project.model_project_project,base.group_portal,1,0,0,0 -access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 access_resource_calendar,project.resource_calendar user,resource.model_resource_calendar,project.group_project_user,1,0,0,0 access_resource_calendar_attendance,project.resource_calendar_attendance user,resource.model_resource_calendar_attendance,project.group_project_user,1,0,0,0 access_resource_calendar_leaves_user,resource.calendar.leaves user,resource.model_resource_calendar_leaves,project.group_project_user,1,1,1,1 diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index eebd86b050b..23282752dc2 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -102,7 +102,6 @@
-
- - - - Tasks project.task diff --git a/addons/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml index cbfeb761016..446bb90a847 100644 --- a/addons/sale_timesheet/data/sale_service_demo.xml +++ b/addons/sale_timesheet/data/sale_service_demo.xml @@ -11,7 +11,7 @@ 5.0 - + 30 Fixed @@ -19,17 +19,31 @@ Done + + Internal - GAP Analysis + INT + + + Internal - GAP Analysis + 2 employees + + Support + INT + + + Support +