forked from Mapan/odoo17e
301 lines
14 KiB
Python
301 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import timedelta, datetime
|
|
from typing import Dict, List
|
|
import pytz
|
|
|
|
from odoo import Command, fields, models, api, _
|
|
from odoo.osv import expression
|
|
from odoo.tools import get_lang
|
|
from odoo.addons.project.models.project_task import CLOSED_STATES
|
|
|
|
class Task(models.Model):
|
|
_inherit = "project.task"
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
result = super(Task, self).default_get(fields_list)
|
|
is_fsm_mode = self._context.get('fsm_mode')
|
|
if 'project_id' in fields_list and not result.get('project_id') and is_fsm_mode and not (result.get('parent_id') or self._context.get('default_parent_id')):
|
|
company_id = self.env.context.get('default_company_id') or self.env.company.id
|
|
fsm_project = self.env['project.project'].search([('is_fsm', '=', True), ('company_id', '=', company_id)], order='sequence', limit=1)
|
|
if fsm_project:
|
|
result['stage_id'] = self.stage_find(fsm_project.id, [('fold', '=', False)])
|
|
result['project_id'] = fsm_project.id
|
|
result['company_id'] = company_id
|
|
return result
|
|
|
|
is_fsm = fields.Boolean(compute='_compute_is_fsm', search='_search_is_fsm', compute_sudo=True)
|
|
fsm_done = fields.Boolean("Task Done", compute='_compute_fsm_done', readonly=False, store=True, copy=False)
|
|
# Use to count conditions between : time, worksheet and materials
|
|
# If 2 over 3 are enabled for the project, the required count = 2
|
|
# If 1 over 3 is met (enabled + encoded), the satisfied count = 2
|
|
display_enabled_conditions_count = fields.Integer(compute='_compute_display_conditions_count')
|
|
display_satisfied_conditions_count = fields.Integer(compute='_compute_display_conditions_count')
|
|
display_mark_as_done_primary = fields.Boolean(compute='_compute_mark_as_done_buttons')
|
|
display_mark_as_done_secondary = fields.Boolean(compute='_compute_mark_as_done_buttons')
|
|
partner_phone = fields.Char(
|
|
compute='_compute_partner_phone', inverse='_inverse_partner_phone',
|
|
string="Phone", readonly=False, store=True, copy=False)
|
|
partner_city = fields.Char(related='partner_id.city', readonly=False)
|
|
is_task_phone_update = fields.Boolean(compute='_compute_is_task_phone_update')
|
|
|
|
@property
|
|
def SELF_READABLE_FIELDS(self):
|
|
return super().SELF_READABLE_FIELDS | {'is_fsm',
|
|
'planned_date_begin',
|
|
'fsm_done',
|
|
'partner_phone',
|
|
'partner_city',}
|
|
|
|
@api.depends(
|
|
'fsm_done', 'is_fsm', 'timer_start',
|
|
'display_enabled_conditions_count', 'display_satisfied_conditions_count')
|
|
def _compute_mark_as_done_buttons(self):
|
|
for task in self:
|
|
primary, secondary = True, True
|
|
if task.fsm_done or not task.is_fsm or task.timer_start:
|
|
primary, secondary = False, False
|
|
else:
|
|
if task.display_enabled_conditions_count == task.display_satisfied_conditions_count:
|
|
secondary = False
|
|
else:
|
|
primary = False
|
|
task.update({
|
|
'display_mark_as_done_primary': primary,
|
|
'display_mark_as_done_secondary': secondary,
|
|
})
|
|
|
|
@api.depends('partner_id.phone')
|
|
def _compute_partner_phone(self):
|
|
for task in self:
|
|
if task.partner_phone != task.partner_id.phone:
|
|
task.partner_phone = task.partner_id.phone
|
|
|
|
def _inverse_partner_phone(self):
|
|
for task in self:
|
|
if task.partner_id and task.partner_phone != task.partner_id.phone:
|
|
task.partner_id.phone = task.partner_phone
|
|
|
|
@api.depends('partner_phone', 'partner_id.phone')
|
|
def _compute_is_task_phone_update(self):
|
|
for task in self:
|
|
task.is_task_phone_update = task.partner_phone != task.partner_id.phone
|
|
|
|
@api.depends('project_id.allow_timesheets', 'total_hours_spent')
|
|
def _compute_display_conditions_count(self):
|
|
for task in self:
|
|
enabled = 1 if task.project_id.allow_timesheets else 0
|
|
satisfied = 1 if enabled and task.total_hours_spent else 0
|
|
task.update({
|
|
'display_enabled_conditions_count': enabled,
|
|
'display_satisfied_conditions_count': satisfied
|
|
})
|
|
|
|
@api.depends('fsm_done', 'display_timesheet_timer', 'timer_start', 'total_hours_spent')
|
|
def _compute_display_timer_buttons(self):
|
|
fsm_done_tasks = self.filtered(lambda task: task.fsm_done)
|
|
fsm_done_tasks.update({
|
|
'display_timer_start_primary': False,
|
|
'display_timer_start_secondary': False,
|
|
'display_timer_stop': False,
|
|
'display_timer_pause': False,
|
|
'display_timer_resume': False,
|
|
})
|
|
super(Task, self - fsm_done_tasks)._compute_display_timer_buttons()
|
|
|
|
@api.depends('project_id')
|
|
def _compute_is_fsm(self):
|
|
for task in self:
|
|
task.is_fsm = task.project_id.is_fsm
|
|
|
|
@api.model
|
|
def _search_is_fsm(self, operator, value):
|
|
project_query = self.env['project.project'].sudo()._search([('is_fsm', operator, value)])
|
|
return [('project_id', 'in', project_query)]
|
|
|
|
# TODO: remove in master
|
|
def _onchange_planned_date(self):
|
|
return
|
|
|
|
@api.onchange('date_deadline', 'planned_date_begin')
|
|
def _onchange_planned_dates(self):
|
|
if not self.is_fsm:
|
|
return super()._onchange_planned_dates()
|
|
|
|
def write(self, vals):
|
|
self_fsm = self.filtered('is_fsm')
|
|
super(Task, self - self_fsm).write(vals.copy())
|
|
|
|
is_start_date_set = bool(vals.get('planned_date_begin', False))
|
|
is_end_date_set = bool(vals.get("date_deadline", False))
|
|
both_dates_changed = 'planned_date_begin' in vals and 'date_deadline' in vals
|
|
self_fsm = self_fsm.with_context(fsm_mode=True)
|
|
|
|
if self_fsm and (
|
|
(both_dates_changed and is_start_date_set != is_end_date_set) or (not both_dates_changed and (
|
|
('planned_date_begin' in vals and not all(bool(t.date_deadline) == is_start_date_set for t in self)) or \
|
|
('date_deadline' in vals and not all(bool(t.planned_date_begin) == is_end_date_set for t in self))
|
|
))
|
|
):
|
|
vals.update({"date_deadline": False, "planned_date_begin": False})
|
|
|
|
return super(Task, self_fsm).write(vals)
|
|
|
|
@api.model
|
|
def _group_expand_project_ids(self, projects, domain, order):
|
|
res = super()._group_expand_project_ids(projects, domain, order)
|
|
if self._context.get('fsm_mode'):
|
|
search_on_comodel = self._search_on_comodel(domain, "project_id", "project.project", order, [('is_fsm', '=', True)])
|
|
res &= search_on_comodel
|
|
return res
|
|
|
|
@api.model
|
|
def _group_expand_user_ids(self, users, domain, order):
|
|
res = super()._group_expand_user_ids(users, domain, order)
|
|
if self.env.context.get('fsm_mode'):
|
|
recently_created_tasks_user_ids = self.env['project.task']._read_group([
|
|
('create_date', '>', datetime.now() - timedelta(days=30)),
|
|
('is_fsm', '=', True),
|
|
('user_ids', '!=', False)
|
|
], [], ['user_ids:array_agg'])[0][0]
|
|
search_domain = ['&', ('company_id', 'in', self.env.companies.ids), '|', '|', ('id', 'in', users.ids), ('groups_id', 'in', self.env.ref('industry_fsm.group_fsm_user').id), ('id', 'in', recently_created_tasks_user_ids)]
|
|
res |= users.search(search_domain, order=order)
|
|
return res
|
|
|
|
def _compute_fsm_done(self):
|
|
closed_tasks = self.filtered(lambda t: t.state in CLOSED_STATES)
|
|
closed_tasks.fsm_done = True
|
|
|
|
def action_timer_start(self):
|
|
if not self.user_timer_id.timer_start and self.display_timesheet_timer:
|
|
super(Task, self).action_timer_start()
|
|
if self.is_fsm:
|
|
time = fields.Datetime.context_timestamp(self, self.timer_start)
|
|
self.message_post(
|
|
body=_(
|
|
'Timer started at: %(date)s %(time)s',
|
|
date=time.strftime(get_lang(self.env).date_format),
|
|
time=time.strftime(get_lang(self.env).time_format),
|
|
),
|
|
)
|
|
|
|
def action_view_timesheets(self):
|
|
kanban_view = self.env.ref('hr_timesheet.view_kanban_account_analytic_line')
|
|
form_view = self.env.ref('industry_fsm.timesheet_view_form')
|
|
tree_view = self.env.ref('industry_fsm.timesheet_view_tree_user_inherit')
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Time'),
|
|
'res_model': 'account.analytic.line',
|
|
'view_mode': 'list,form,kanban',
|
|
'views': [(tree_view.id, 'list'), (kanban_view.id, 'kanban'), (form_view.id, 'form')],
|
|
'domain': [('task_id', '=', self.id), ('project_id', '!=', False)],
|
|
'context': {
|
|
'fsm_mode': True,
|
|
'default_project_id': self.project_id.id,
|
|
'default_task_id': self.id,
|
|
}
|
|
}
|
|
|
|
def action_fsm_validate(self, stop_running_timers=False):
|
|
""" Moves Task to done state.
|
|
If allow billable on task, timesheet product set on project and user has privileges :
|
|
Create SO confirmed with time and material.
|
|
"""
|
|
Timer = self.env['timer.timer']
|
|
tasks_running_timer_ids = Timer.search([('res_model', '=', 'project.task'), ('res_id', 'in', self.ids)])
|
|
timesheets = self.env['account.analytic.line'].sudo().search([('task_id', 'in', self.ids)])
|
|
timesheets_running_timer_ids = None
|
|
if timesheets:
|
|
timesheets_running_timer_ids = Timer.search([
|
|
('res_model', '=', 'account.analytic.line'),
|
|
('res_id', 'in', timesheets.ids)])
|
|
if tasks_running_timer_ids or timesheets_running_timer_ids:
|
|
if stop_running_timers:
|
|
self._stop_all_timers_and_create_timesheets(tasks_running_timer_ids, timesheets_running_timer_ids, timesheets)
|
|
else:
|
|
wizard = self.env['project.task.stop.timers.wizard'].create({
|
|
'line_ids': [Command.create({'task_id': task.id}) for task in self],
|
|
})
|
|
return {
|
|
'name': _('Do you want to stop the running timers?'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('industry_fsm.view_task_stop_timer_wizard_form').id,
|
|
'target': 'new',
|
|
'res_model': 'project.task.stop.timers.wizard',
|
|
'res_id': wizard.id,
|
|
}
|
|
|
|
self.write({'fsm_done': True, 'state': '1_done'})
|
|
|
|
return True
|
|
|
|
@api.model
|
|
def _stop_all_timers_and_create_timesheets(self, tasks_running_timer_ids, timesheets_running_timer_ids, timesheets):
|
|
ConfigParameter = self.env['ir.config_parameter'].sudo()
|
|
Timesheet = self.env['account.analytic.line']
|
|
|
|
if not tasks_running_timer_ids and not timesheets_running_timer_ids:
|
|
return Timesheet
|
|
|
|
result = Timesheet
|
|
minimum_duration = int(ConfigParameter.get_param('timesheet_grid.timesheet_min_duration', 0))
|
|
rounding = int(ConfigParameter.get_param('timesheet_grid.timesheet_rounding', 0))
|
|
if tasks_running_timer_ids:
|
|
task_dict = {task.id: task for task in self}
|
|
timesheets_vals = []
|
|
for timer in tasks_running_timer_ids:
|
|
minutes_spent = timer._get_minutes_spent()
|
|
time_spent = self._timer_rounding(minutes_spent, minimum_duration, rounding) / 60
|
|
task = task_dict[timer.res_id]
|
|
timesheets_vals.append({
|
|
'task_id': task.id,
|
|
'project_id': task.project_id.id,
|
|
'user_id': timer.user_id.id,
|
|
'unit_amount': time_spent,
|
|
})
|
|
tasks_running_timer_ids.sudo().unlink()
|
|
result += Timesheet.sudo().create(timesheets_vals)
|
|
|
|
if timesheets_running_timer_ids:
|
|
timesheets_dict = {timesheet.id: timesheet for timesheet in timesheets}
|
|
for timer in timesheets_running_timer_ids:
|
|
timesheet = timesheets_dict[timer.res_id]
|
|
minutes_spent = timer._get_minutes_spent()
|
|
timesheet._add_timesheet_time(minutes_spent)
|
|
result += timesheet
|
|
timesheets_running_timer_ids.sudo().unlink()
|
|
|
|
return result
|
|
|
|
def action_fsm_navigate(self):
|
|
if not self.partner_id.city or not self.partner_id.country_id:
|
|
return {
|
|
'name': _('Customer'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'res.partner',
|
|
'res_id': self.partner_id.id,
|
|
'view_mode': 'form',
|
|
'view_id': self.env.ref('industry_fsm.view_partner_address_form_industry_fsm').id,
|
|
'target': 'new',
|
|
}
|
|
return self.partner_id.action_partner_navigate()
|
|
|
|
def web_read(self, specification: Dict[str, Dict]) -> List[Dict]:
|
|
if len(self) == 1 and 'partner_id' in specification and 'show_address_if_fsm' in specification['partner_id'].get('context', {}):
|
|
specification['partner_id']['context']['show_address'] = self.is_fsm
|
|
return super().web_read(specification)
|
|
|
|
# ---------------------------------------------------------
|
|
# Business Methods
|
|
# ---------------------------------------------------------
|
|
|
|
def _get_projects_to_make_billable_domain(self, additional_domain=None):
|
|
return expression.AND([
|
|
super()._get_projects_to_make_billable_domain(additional_domain),
|
|
[('is_fsm', '=', False)],
|
|
])
|