Skip to content

Commit

Permalink
[IMP] project_workload_timesheet: Link timesheet lines with workload …
Browse files Browse the repository at this point in the history
…units

Compute remaining time for unit
Add quick timesheet buttons
Handle unit done status
Report unfinished unit at sheet validation
  • Loading branch information
paradoxxxzero committed Feb 15, 2024
1 parent f70d849 commit 8420905
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 40 deletions.
6 changes: 4 additions & 2 deletions project_workload_timesheet/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"license": "AGPL-3",
"depends": [
"project_workload",
"hr_timesheet",
"hr_timesheet_sheet",
],
"data": [
"views/hr_timesheet_sheet_views.xml",
],
"data": [],
}
1 change: 1 addition & 0 deletions project_workload_timesheet/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import account_analytic_line
from . import hr_timesheet_sheet
from . import project_workload_unit
65 changes: 65 additions & 0 deletions project_workload_timesheet/models/account_analytic_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2024 Akretion (https://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


from odoo.addons.project_workload.models.project_task_workload import week_name


class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"

workload_unit_id = fields.Many2one(
comodel_name="project.workload.unit",
string="Workload Unit",
readonly=False,
compute="_compute_workload_unit_id",
store=True,
domain="["
"('week', '=', week), "
"('user_id', '=', user_id), "
"('project_id', '=', project_id), "
"('task_id', '=', task_id)"
"]",
)

week = fields.Char(
string="Week",
compute="_compute_week",
help="Week number of the year",
)

@api.depends("date")
def _compute_week(self):
for record in self:
record.week = week_name(record.date)

@api.depends("task_id", "date", "user_id")
def _compute_workload_unit_id(self):
for record in self:
if not record.project_id or not record.task_id:
record.workload_unit_id = False
continue
available_workload_units = self.env["project.workload.unit"].search(
record._get_available_workload_units_domain()
)
if (
not record.workload_unit_id
or record.workload_unit_id not in available_workload_units
):
record.workload_unit_id = (
available_workload_units[0]
if len(available_workload_units) > 0
else False
)

def _get_available_workload_units_domain(self):
return [
("project_id", "=", self.project_id.id),
("task_id", "=", self.task_id.id),
("week", "=", self.week),
("user_id", "=", self.user_id.id),
]
115 changes: 92 additions & 23 deletions project_workload_timesheet/models/hr_timesheet_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,27 @@ class Sheet(models.Model):

workload_unit_ids = fields.One2many(
"project.workload.unit",
"sheet_id",
string="Workload Units",
compute="_compute_workload_unit_ids",
readonly=False,
)

next_week_load = fields.Float(
"Next Week Load",
compute="_compute_next_week_load",
help="The workload of the next week",
)
next_week_units_count = fields.Integer(
"Next Week Units Count",
compute="_compute_next_week_load",
help="The number of workload units of the next week",
)

current = fields.Boolean(
"Current",
compute="_compute_current",
help="Is this the current timesheet",
)

@api.depends("date_start", "date_end", "user_id")
def _compute_workload_unit_ids(self):
Expand All @@ -37,7 +48,14 @@ def _compute_workload_unit_ids(self):
("user_id", "=", record.user_id.id),
],
)
.sorted(lambda p: -int(p.priority or 0)) # Hum
.sorted(
lambda p: (
p.done, # Put done tasks at the end
-int(p.priority or 0), # Sort by priority
p.remaining_hours, # Sort by remaining hours
-p.id, # Stabilize sort
)
)
)

@api.depends("date_start", "date_end", "user_id")
Expand All @@ -50,9 +68,14 @@ def _compute_next_week_load(self):
("user_id", "=", record.user_id.id),
],
)

record.next_week_units_count = len(next_week_units)
record.next_week_load = sum(next_week_units.mapped("hours"))

@api.depends("date_start", "date_end")
def _compute_current(self):
for record in self:
record.current = record.date_start <= fields.Date.today() <= record.date_end

def button_open_next_week(self):
self.ensure_one()
next_week = self.date_start + timedelta(days=7)
Expand Down Expand Up @@ -97,28 +120,74 @@ def _add_line_from_unit(self, unit):
if existing_unique_ids:
self.delete_empty_lines(False)
if frozenset(new_line_unique_id.items()) not in existing_unique_ids:
# TODO MAKE this configurable
DAILY_HOURS = 8
task = unit._get_timesheeting_task()
if self.current:
values["date"] = fields.Date.today()

if task.date_start and task.date_end:
task_start = task.date_start.date()
task_end = task.date_end.date()

today = fields.Date.today()
# If this is the current week
if self.date_start <= today <= self.date_end:
# Take the closest day to today
values["date"] = max(min(today, task_end), task_start)
else:
# If start date is in week, take it
if self.date_start <= task_start <= self.date_end:
values["date"] = task_start

values["unit_amount"] = min(DAILY_HOURS, unit.hours)
values["unit_amount"] = 0
values["project_id"] = task.project_id.id
values["task_id"] = task.id
self.timesheet_ids |= self.env["account.analytic.line"]._sheet_create(
values
values["workload_unit_id"] = unit.id
return self.env["account.analytic.line"]._sheet_create(values)

@api.model
def _prepare_new_line(self, line):
# We need to check if the new line is similar to a worload unit
# If it is, we need to link it to the workload unit
vals = super()._prepare_new_line(line)
# Yeah, using the same function for 2 different things leads to this :/
if line._name != "hr_timesheet.sheet.new.analytic.line":
return vals
timesheets = line.sheet_id.timesheet_ids
similar_timesheets = timesheets.filtered(
lambda t: t.project_id == line.project_id and t.task_id == line.task_id
)
if not similar_timesheets:
return vals

similar_workload_units = similar_timesheets.mapped("workload_unit_id")
vals["workload_unit_id"] = (
similar_workload_units.ids[0] if similar_workload_units else False
)
return vals

def action_timesheet_done(self):
self.ensure_one()
super().action_timesheet_done()

next_week = week_name(self.date_start + timedelta(days=7))
next_week_units = self.env["project.workload.unit"].search(
[
("week", "=", next_week),
("user_id", "=", self.user_id.id),
],
)

unfinished_units = self.workload_unit_ids.filtered(lambda u: not u.done)
for unit in unfinished_units:
next_week_unit = next_week_units.filtered(
lambda u: u.project_id == unit.project_id
and u.task_id == unit.task_id
and u.workload_id == unit.workload_id
)
return True

# But we report the remaining hours to the next week
# We also report if the remaining hours are negative
# It will decrease the next week workload
# But we don't create a unit in this case
if not next_week_unit and unit.remaining_hours > 0:
# And we create a new unit if it does not exist
# Even if the unit could be after end_date for now
next_week_unit = self.env["project.workload.unit"].create(
{
"task_id": unit.task_id.id,
"workload_id": unit.workload_id.id,
"week": next_week,
"hours": unit.remaining_hours,
}
)
else:
next_week_unit.hours += unit.remaining_hours

# The unit are now done so the unit hours are the timesheeted hours
unit.hours = unit.timesheeted_hours
73 changes: 70 additions & 3 deletions project_workload_timesheet/models/project_workload_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,89 @@
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models
from odoo import api, fields, models


class ProjectWorkloadUnit(models.Model):
_inherit = "project.workload.unit"

sheet_id = fields.Many2one("hr_timesheet.sheet")
timesheet_ids = fields.One2many(
"account.analytic.line",
"workload_unit_id",
"Timesheets",
help="The timesheets (normally one) in which the workload is timesheeted",
)
priority = fields.Selection(related="task_id.priority")

def action_add(self):
timesheeted_hours = fields.Float(
"Timesheeted Hours",
compute="_compute_timesheeted_hours",
help="The hours timesheeted on this workload",
)
remaining_hours = fields.Float(
"Remaining Hours",
compute="_compute_remaining_hours",
help="The remaining hours to timesheet on this workload (can be negative)",
)
progress = fields.Float(
"Progress",
compute="_compute_progress",
help="The progress of the task",
)
done = fields.Boolean(
"Done",
)
task_stage_id = fields.Many2one(
related="task_id.stage_id", string="Task Stage", readonly=False
)

@api.depends("timesheet_ids.unit_amount")
def _compute_timesheeted_hours(self):
for record in self:
record.timesheeted_hours = sum(record.timesheet_ids.mapped("unit_amount"))

@api.depends("hours", "timesheeted_hours")
def _compute_progress(self):
for record in self:
if record.hours:
record.progress = 100 * record.timesheeted_hours / record.hours
else:
record.progress = 0

@api.depends("hours", "timesheeted_hours", "done")
def _compute_remaining_hours(self):
for record in self:
if record.done:
record.remaining_hours = 0
else:
record.remaining_hours = record.hours - record.timesheeted_hours

def action_add_to_timesheet(self):
sheet_id = self.env.context.get("current_sheet_id")
if not sheet_id:
return
sheet = self.env["hr_timesheet.sheet"].browse(sheet_id)
return sheet._add_line_from_unit(self)

def action_timesheet_time(self):
sheet_id = self.env.context.get("current_sheet_id")
if not sheet_id:
return
sheet = self.env["hr_timesheet.sheet"].browse(sheet_id)
if not sheet or not sheet.current:
return
time = self.env.context.get("time", 0) / 60

timesheet = self.timesheet_ids.filtered(
lambda t: t.date == fields.Date.today()
) or sheet._add_line_from_unit(self)
timesheet.unit_amount += time
return True

def action_timesheet_done(self):
self.done = True
pass

def _get_timesheeting_task(self):
# For overrides
return self.task_id
Loading

0 comments on commit 8420905

Please sign in to comment.