Initial commit: OCA Mrp packages (117 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 277e84fd7a
4403 changed files with 395154 additions and 0 deletions

View file

@ -0,0 +1,9 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import res_company
from . import res_config
from . import account_analytic_account
from . import account_analytic_line
from . import hr_timesheet_sheet
from . import hr_department
from . import hr_employee

View file

@ -0,0 +1,28 @@
# Copyright 2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, models
from odoo.exceptions import ValidationError
class AccountAnalyticAccount(models.Model):
_inherit = "account.analytic.account"
@api.constrains("company_id")
def _check_timesheet_sheet_company_id(self):
for rec in self.sudo():
sheets = rec.line_ids.mapped("sheet_id").filtered(
lambda s: s.company_id and s.company_id != rec.company_id
)
if sheets:
raise ValidationError(
_(
"You cannot change the company, "
"as this %(rec_name)s (%(rec_display_name)s) "
"is assigned to %(current_name)s (%(current_display_name)s).",
rec_name=rec._name,
rec_display_name=rec.display_name,
current_name=sheets[0]._name,
current_display_name=sheets[0].display_name,
)
)

View file

@ -0,0 +1,141 @@
# Copyright 2018 ForgeFlow, S.L.
# Copyright 2018-2019 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
sheet_id = fields.Many2one(
comodel_name="hr_timesheet.sheet", string="Sheet", copy=False
)
sheet_state = fields.Selection(string="Sheet State", related="sheet_id.state")
def _get_sheet_domain(self):
"""Hook for extensions"""
self.ensure_one()
return [
("date_end", ">=", self.date),
("date_start", "<=", self.date),
("employee_id", "=", self.employee_id.id),
("company_id", "in", [self.company_id.id, False]),
("state", "in", ["new", "draft"]),
]
def _determine_sheet(self):
"""Hook for extensions"""
self.ensure_one()
return self.env["hr_timesheet.sheet"].search(self._get_sheet_domain(), limit=1)
def _compute_sheet(self):
"""Links the timesheet line to the corresponding sheet"""
for timesheet in self.filtered("project_id"):
sheet = timesheet._determine_sheet()
if timesheet.sheet_id != sheet:
timesheet.sheet_id = sheet
@api.constrains("company_id", "sheet_id")
def _check_company_id_sheet_id(self):
for aal in self.sudo():
if (
aal.company_id
and aal.sheet_id.company_id
and aal.company_id != aal.sheet_id.company_id
):
raise ValidationError(
_(
"You cannot create a timesheet of a different company "
"than the one of the timesheet sheet:"
"\n - %(sheet_name)s of %(sheet_company)s"
"\n - %(name)s of %(company)s",
sheet_name=aal.sheet_id.complete_name,
sheet_company=aal.sheet_id.company_id.name,
name=aal.name,
company=aal.company_id.name,
)
)
@api.model_create_multi
def create(self, values):
if not self.env.context.get("sheet_create") and "sheet_id" in values:
del values["sheet_id"]
res = super().create(values)
res._compute_sheet()
return res
@api.model
def _sheet_create(self, values):
return self.with_context(sheet_create=True).create(values)
def write(self, values):
self._check_state_on_write(values)
res = super().write(values)
if self._timesheet_should_compute_sheet(values):
self._compute_sheet()
return res
def unlink(self):
self._check_state()
return super().unlink()
def _check_state_on_write(self, values):
"""Hook for extensions"""
if self._timesheet_should_check_write(values):
self._check_state()
@api.model
def _timesheet_should_check_write(self, values):
"""Hook for extensions"""
return bool(set(self._get_timesheet_protected_fields()) & set(values.keys()))
@api.model
def _timesheet_should_compute_sheet(self, values):
"""Hook for extensions"""
return any(f in self._get_sheet_affecting_fields() for f in values)
@api.model
def _get_timesheet_protected_fields(self):
"""Hook for extensions"""
return [
"name",
"date",
"unit_amount",
"user_id",
"employee_id",
"department_id",
"company_id",
"task_id",
"project_id",
"sheet_id",
]
@api.model
def _get_sheet_affecting_fields(self):
"""Hook for extensions"""
return ["date", "employee_id", "project_id", "company_id"]
def _check_state(self):
if self.env.context.get("skip_check_state"):
return
for line in self.exists().filtered("sheet_id"):
if line.sheet_id.state not in ["new", "draft"]:
raise UserError(
_(
"You cannot modify an entry in a confirmed timesheet sheet"
": %(names)s",
names=line.sheet_id.complete_name,
)
)
def merge_timesheets(self):
unit_amount = sum(t.unit_amount for t in self)
amount = sum(t.amount for t in self)
self[0].write({"unit_amount": unit_amount, "amount": amount})
self[1:].unlink()
return self[0]
def _check_can_update_timesheet(self):
return super()._check_can_update_timesheet() or not self.filtered("sheet_id")

View file

@ -0,0 +1,56 @@
# Copyright 2018 ForgeFlow, S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class HrDepartment(models.Model):
_inherit = "hr.department"
timesheet_sheet_to_approve_count = fields.Integer(
compute="_compute_timesheet_to_approve", string="Timesheet Sheets to Approve"
)
def _compute_timesheet_to_approve(self):
timesheet_data = self.env["hr_timesheet.sheet"].read_group(
[("department_id", "in", self.ids), ("state", "=", "confirm")],
["department_id"],
["department_id"],
)
result = {
data["department_id"][0]: data["department_id_count"]
for data in timesheet_data
}
for department in self:
department.timesheet_sheet_to_approve_count = result.get(department.id, 0)
@api.constrains("company_id")
def _check_company_id(self):
for rec in self.sudo().filtered("company_id"):
for field in [
rec.env["hr_timesheet.sheet"].search(
[
("department_id", "=", rec.id),
("company_id", "!=", rec.company_id.id),
("company_id", "!=", False),
],
limit=1,
)
]:
if (
rec.company_id
and field.company_id
and rec.company_id != field.company_id
):
raise ValidationError(
_(
"You cannot change the company, "
"as this %(rec_name)s (%(rec_display_name)s) "
"is assigned to %(current_name)s (%(current_display_name)s).",
rec_name=rec._name,
rec_display_name=rec.display_name,
current_name=field._name,
current_display_name=field.display_name,
)
)

View file

@ -0,0 +1,56 @@
# Copyright 2018 ForgeFlow, S.L.
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class HrEmployee(models.Model):
_inherit = "hr.employee"
timesheet_sheet_ids = fields.One2many(
comodel_name="hr_timesheet.sheet",
inverse_name="employee_id",
string="Timesheet Sheets",
)
timesheet_sheet_count = fields.Integer(
compute="_compute_timesheet_sheet_count", string="Timesheet Sheets Count"
)
def _compute_timesheet_sheet_count(self):
Sheet = self.env["hr_timesheet.sheet"]
for employee in self:
employee.timesheet_sheet_count = Sheet.search_count(
[("employee_id", "=", employee.id)]
)
@api.constrains("company_id")
def _check_company_id(self):
for rec in self.sudo().filtered("company_id"):
for field in [
rec.env["hr_timesheet.sheet"].search(
[
("employee_id", "=", rec.id),
("company_id", "!=", rec.company_id.id),
("company_id", "!=", False),
],
limit=1,
)
]:
if (
rec.company_id
and field.company_id
and rec.company_id != field.company_id
):
raise ValidationError(
_(
"You cannot change the company, "
"as this %(rec_name)s (%(rec_display_name)s) "
"is assigned to %(current_name)s (%(current_display_name)s).",
rec_name=rec._name,
rec_display_name=rec.display_name,
current_name=field._name,
current_display_name=field.display_name,
)
)

View file

@ -0,0 +1,922 @@
# Copyright 2018-2020 ForgeFlow, S.L.
# Copyright 2018-2020 Brainbean Apps (https://brainbeanapps.com)
# Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
import re
from collections import namedtuple
from datetime import datetime, time
import babel.dates
from dateutil.relativedelta import SU, relativedelta
from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
empty_name = "/"
class Sheet(models.Model):
_name = "hr_timesheet.sheet"
_description = "Timesheet Sheet"
_inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
_table = "hr_timesheet_sheet"
_order = "id desc"
_rec_name = "complete_name"
def _default_date_start(self):
return self._get_period_start(
self.env.user.company_id, fields.Date.context_today(self)
)
def _default_date_end(self):
return self._get_period_end(
self.env.user.company_id, fields.Date.context_today(self)
)
def _selection_review_policy(self):
ResCompany = self.env["res.company"]
return ResCompany._fields["timesheet_sheet_review_policy"].selection
def _default_review_policy(self):
company = self.env.company
return company.timesheet_sheet_review_policy
def _default_employee(self):
company = self.env.company
return self.env["hr.employee"].search(
[("user_id", "=", self.env.uid), ("company_id", "in", [company.id, False])],
limit=1,
order="company_id ASC",
)
def _default_department_id(self):
return self._default_employee().department_id
name = fields.Char(compute="_compute_name")
employee_id = fields.Many2one(
comodel_name="hr.employee",
string="Employee",
default=lambda self: self._default_employee(),
required=True,
readonly=True,
states={"new": [("readonly", False)]},
)
user_id = fields.Many2one(
comodel_name="res.users",
related="employee_id.user_id",
string="User",
store=True,
readonly=True,
)
date_start = fields.Date(
string="Date From",
default=lambda self: self._default_date_start(),
required=True,
index=True,
readonly=True,
states={"new": [("readonly", False)]},
)
date_end = fields.Date(
string="Date To",
default=lambda self: self._default_date_end(),
required=True,
index=True,
readonly=True,
states={"new": [("readonly", False)]},
)
timesheet_ids = fields.One2many(
comodel_name="account.analytic.line",
inverse_name="sheet_id",
string="Timesheets",
readonly=True,
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
)
line_ids = fields.One2many(
comodel_name="hr_timesheet.sheet.line",
compute="_compute_line_ids",
string="Timesheet Sheet Lines",
readonly=True,
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
)
new_line_ids = fields.One2many(
comodel_name="hr_timesheet.sheet.new.analytic.line",
inverse_name="sheet_id",
string="Temporary Timesheets",
readonly=True,
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
)
state = fields.Selection(
[
("new", "New"),
("draft", "Open"),
("confirm", "Waiting Review"),
("done", "Approved"),
],
default="new",
tracking=True,
string="Status",
required=True,
readonly=True,
index=True,
)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
default=lambda self: self.env.company,
required=True,
readonly=True,
)
review_policy = fields.Selection(
selection=lambda self: self._selection_review_policy(),
default=lambda self: self._default_review_policy(),
required=True,
readonly=True,
)
department_id = fields.Many2one(
comodel_name="hr.department",
string="Department",
default=lambda self: self._default_department_id(),
readonly=True,
states={"new": [("readonly", False)]},
)
reviewer_id = fields.Many2one(
comodel_name="hr.employee", string="Reviewer", readonly=True, tracking=True
)
add_line_project_id = fields.Many2one(
comodel_name="project.project",
string="Select Project",
domain="[('company_id', '=', company_id), ('allow_timesheets', '=', True)]",
help="If selected, the associated project is added "
"to the timesheet sheet when clicked the button.",
)
add_line_task_id = fields.Many2one(
comodel_name="project.task",
string="Select Task",
domain="[('id', 'in', available_task_ids)]",
help="If selected, the associated task is added "
"to the timesheet sheet when clicked the button.",
)
available_task_ids = fields.Many2many(
comodel_name="project.task",
string="Available Tasks",
compute="_compute_available_task_ids",
)
total_time = fields.Float(compute="_compute_total_time", store=True)
can_review = fields.Boolean(
compute="_compute_can_review", search="_search_can_review"
)
complete_name = fields.Char(compute="_compute_complete_name")
@api.depends("date_start", "date_end")
def _compute_name(self):
locale = self.env.context.get("lang") or self.env.user.lang or "en_US"
for sheet in self:
if sheet.date_start == sheet.date_end:
sheet.name = babel.dates.format_skeleton(
skeleton="MMMEd",
datetime=datetime.combine(sheet.date_start, time.min),
locale=locale,
)
continue
period_start = sheet.date_start.strftime("%V, %Y")
period_end = sheet.date_end.strftime("%V, %Y")
if sheet.date_end <= sheet.date_start + relativedelta(weekday=SU):
sheet.name = _("Week %(end)s", end=period_end)
else:
sheet.name = _(
"Weeks %(start)s - %(end)s", start=period_start, end=period_end
)
@api.depends("timesheet_ids.unit_amount")
def _compute_total_time(self):
for sheet in self:
sheet.total_time = sum(sheet.mapped("timesheet_ids.unit_amount"))
@api.depends("review_policy")
def _compute_can_review(self):
for sheet in self:
sheet.can_review = self.env.user in sheet._get_possible_reviewers()
@api.model
def _search_can_review(self, operator, value):
def check_in(users):
return self.env.user in users
def check_not_in(users):
return self.env.user not in users
if (operator == "=" and value) or (operator in ["<>", "!="] and not value):
check = check_in
else:
check = check_not_in
sheets = self.search([]).filtered(
lambda sheet: check(sheet._get_possible_reviewers())
)
return [("id", "in", sheets.ids)]
@api.depends("name", "employee_id")
def _compute_complete_name(self):
for sheet in self:
complete_name = sheet.name
complete_name_components = sheet._get_complete_name_components()
if complete_name_components:
complete_name = "{} ({})".format(
complete_name,
", ".join(complete_name_components),
)
sheet.complete_name = complete_name
@api.constrains("date_start", "date_end")
def _check_start_end_dates(self):
for sheet in self:
if sheet.date_start > sheet.date_end:
raise ValidationError(
_("The start date cannot be later than the end date.")
)
def _get_complete_name_components(self):
"""Hook for extensions"""
self.ensure_one()
return [self.employee_id.name_get()[0][1]]
def _get_overlapping_sheet_domain(self):
"""Hook for extensions"""
self.ensure_one()
return [
("id", "!=", self.id),
("date_start", "<=", self.date_end),
("date_end", ">=", self.date_start),
("employee_id", "=", self.employee_id.id),
("company_id", "=", self._get_timesheet_sheet_company().id),
]
@api.constrains(
"date_start", "date_end", "company_id", "employee_id", "review_policy"
)
def _check_overlapping_sheets(self):
for sheet in self:
overlapping_sheets = self.search(sheet._get_overlapping_sheet_domain())
if overlapping_sheets:
raise ValidationError(
_(
"You cannot have 2 or more sheets that overlap!\n"
'Please use the menu "Timesheet Sheet" '
"to avoid this problem.\nConflicting sheets:\n - %(names)s",
names=(
"\n - ".join(overlapping_sheets.mapped("complete_name")),
),
)
)
@api.constrains("company_id", "employee_id")
def _check_company_id_employee_id(self):
for rec in self.sudo():
if (
rec.company_id
and rec.employee_id.company_id
and rec.company_id != rec.employee_id.company_id
):
raise ValidationError(
_(
"The Company in the Timesheet Sheet and in "
"the Employee must be the same."
)
)
@api.constrains("company_id", "department_id")
def _check_company_id_department_id(self):
for rec in self.sudo():
if (
rec.company_id
and rec.department_id.company_id
and rec.company_id != rec.department_id.company_id
):
raise ValidationError(
_(
"The Company in the Timesheet Sheet and in "
"the Department must be the same."
)
)
@api.constrains("company_id", "add_line_project_id")
def _check_company_id_add_line_project_id(self):
for rec in self.sudo():
if (
rec.company_id
and rec.add_line_project_id.company_id
and rec.company_id != rec.add_line_project_id.company_id
):
raise ValidationError(
_(
"The Company in the Timesheet Sheet and in "
"the Project must be the same."
)
)
@api.constrains("company_id", "add_line_task_id")
def _check_company_id_add_line_task_id(self):
for rec in self.sudo():
if (
rec.company_id
and rec.add_line_task_id.company_id
and rec.company_id != rec.add_line_task_id.company_id
):
raise ValidationError(
_(
"The Company in the Timesheet Sheet and in "
"the Task must be the same."
)
)
def _get_possible_reviewers(self):
self.ensure_one()
res = self.env["res.users"].browse(SUPERUSER_ID)
if self.review_policy == "hr":
res |= self.env.ref("hr.group_hr_user").users
elif self.review_policy == "hr_manager":
res |= self.env.ref("hr.group_hr_manager").users
elif self.review_policy == "timesheet_manager":
res |= self.env.ref("hr_timesheet.group_hr_timesheet_approver").users
return res
def _get_timesheet_sheet_company(self):
self.ensure_one()
employee = self.employee_id
company = employee.company_id or employee.department_id.company_id
if not company:
company = employee.user_id.company_id
return company
@api.onchange("employee_id")
def _onchange_employee_id(self):
if self.employee_id:
company = self._get_timesheet_sheet_company()
self.company_id = company
self.review_policy = company.timesheet_sheet_review_policy
self.department_id = self.employee_id.department_id
def _get_timesheet_sheet_lines_domain(self):
self.ensure_one()
return [
("date", "<=", self.date_end),
("date", ">=", self.date_start),
("employee_id", "=", self.employee_id.id),
("company_id", "=", self._get_timesheet_sheet_company().id),
("project_id", "!=", False),
]
@api.depends("date_start", "date_end")
def _compute_line_ids(self):
SheetLine = self.env["hr_timesheet.sheet.line"]
for sheet in self:
if not all([sheet.date_start, sheet.date_end]):
continue
matrix = sheet._get_data_matrix()
vals_list = []
for key in sorted(matrix, key=lambda key: sheet._get_matrix_sortby(key)):
vals_list.append(sheet._get_default_sheet_line(matrix, key))
if sheet.state in ["new", "draft"] and self.env.context.get(
"hr_timesheet_sheet_clean_timesheets", True
):
sheet.clean_timesheets(matrix[key])
sheet.line_ids = [(6, 0, SheetLine.create(vals_list).ids)]
@api.model
def _matrix_key_attributes(self):
"""Hook for extensions"""
return ["date", "project_id", "task_id"]
@api.model
def _matrix_key(self):
return namedtuple("MatrixKey", self._matrix_key_attributes())
@api.model
def _get_matrix_key_values_for_line(self, aal):
"""Hook for extensions"""
return {"date": aal.date, "project_id": aal.project_id, "task_id": aal.task_id}
@api.model
def _get_matrix_sortby(self, key):
res = []
for attribute in key:
if hasattr(attribute, "name_get"):
name = attribute.name_get()
value = name[0][1] if name else ""
else:
value = attribute
res.append(value)
return res
def _get_data_matrix(self):
self.ensure_one()
MatrixKey = self._matrix_key()
matrix = {}
empty_line = self.env["account.analytic.line"]
for line in self.timesheet_ids:
key = MatrixKey(**self._get_matrix_key_values_for_line(line))
if key not in matrix:
matrix[key] = empty_line
matrix[key] += line
for date in self._get_dates():
for key in matrix.copy():
key = MatrixKey(**{**key._asdict(), "date": date})
if key not in matrix:
matrix[key] = empty_line
return matrix
def _compute_timesheet_ids(self):
AccountAnalyticLines = self.env["account.analytic.line"]
for sheet in self:
domain = sheet._get_timesheet_sheet_lines_domain()
timesheets = AccountAnalyticLines.search(domain)
sheet.link_timesheets_to_sheet(timesheets)
sheet.timesheet_ids = [(6, 0, timesheets.ids)]
@api.onchange("date_start", "date_end", "employee_id")
def _onchange_scope(self):
self._compute_timesheet_ids()
@api.onchange("date_start", "date_end")
def _onchange_dates(self):
if self.date_start > self.date_end:
self.date_end = self.date_start
@api.onchange("timesheet_ids")
def _onchange_timesheets(self):
self._compute_line_ids()
@api.depends(
"add_line_project_id", "company_id", "timesheet_ids", "timesheet_ids.task_id"
)
def _compute_available_task_ids(self):
project_task_obj = self.env["project.task"]
for rec in self:
if rec.add_line_project_id:
rec.available_task_ids = project_task_obj.search(
[
("project_id", "=", rec.add_line_project_id.id),
("company_id", "=", rec.company_id.id),
("id", "not in", rec.timesheet_ids.mapped("task_id").ids),
]
).ids
else:
rec.available_task_ids = []
@api.model
def _check_employee_user_link(self, vals):
if vals.get("employee_id"):
employee = self.env["hr.employee"].sudo().browse(vals["employee_id"])
if not employee.user_id:
raise UserError(
_(
"In order to create a sheet for this employee, you must"
" link him/her to an user: %s"
)
% (employee.name,)
)
return employee.user_id.id
return False
def copy(self, default=None):
if not self.env.context.get("allow_copy_timesheet"):
raise UserError(_("You cannot duplicate a sheet."))
return super().copy(default=default)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._check_employee_user_link(vals)
res = super().create(vals_list)
res.write({"state": "draft"})
return res
def _sheet_write(self, field, recs):
self.with_context(sheet_write=True).write({field: [(6, 0, recs.ids)]})
def write(self, vals):
self._check_employee_user_link(vals)
res = super().write(vals)
for rec in self:
if rec.state == "draft" and not self.env.context.get("sheet_write"):
rec._update_analytic_lines_from_new_lines(vals)
if "add_line_project_id" not in vals:
rec.delete_empty_lines(True)
return res
def unlink(self):
for sheet in self:
if sheet.state in ("confirm", "done"):
raise UserError(
_(
"You cannot delete a timesheet sheet which is already"
" submitted or confirmed: %s"
)
% (sheet.complete_name,)
)
return super().unlink()
def onchange(self, values, field_name, field_onchange):
"""
Pass a flag for _compute_line_ids not to clean timesheet lines to be (kind of)
idempotent during onchange
"""
return super(
Sheet, self.with_context(hr_timesheet_sheet_clean_timesheets=False)
).onchange(values, field_name, field_onchange)
def _get_informables(self):
"""Hook for extensions"""
self.ensure_one()
return self.employee_id.parent_id.user_id.partner_id
def _get_subscribers(self):
"""Hook for extensions"""
self.ensure_one()
subscribers = self._get_possible_reviewers().mapped("partner_id")
subscribers |= self._get_informables()
return subscribers
def _timesheet_subscribe_users(self):
for sheet in self.sudo():
subscribers = sheet._get_subscribers()
if subscribers:
sheet.message_subscribe(partner_ids=subscribers.ids)
def action_timesheet_draft(self):
if self.filtered(lambda sheet: sheet.state != "done"):
raise UserError(_("Cannot revert to draft a non-approved sheet."))
self._check_can_review()
self.write({"state": "draft", "reviewer_id": False})
def action_timesheet_confirm(self):
self._timesheet_subscribe_users()
self.reset_add_line()
self.write({"state": "confirm"})
def action_timesheet_done(self):
if self.filtered(lambda sheet: sheet.state != "confirm"):
raise UserError(_("Cannot approve a non-submitted sheet."))
self._check_can_review()
self.write({"state": "done", "reviewer_id": self._get_current_reviewer().id})
def action_timesheet_refuse(self):
if self.filtered(lambda sheet: sheet.state != "confirm"):
raise UserError(_("Cannot reject a non-submitted sheet."))
self._check_can_review()
self.write({"state": "draft", "reviewer_id": False})
@api.model
def _get_current_reviewer(self):
reviewer = self.env["hr.employee"].search(
[("user_id", "=", self.env.uid)], limit=1
)
if not reviewer:
raise UserError(
_(
"In order to review a timesheet sheet, your user needs to be"
" linked to an employee."
)
)
return reviewer
def _check_can_review(self):
if self.filtered(lambda x: not x.can_review and x.review_policy == "hr"):
raise UserError(_("Only a HR Officer or Manager can review the sheet."))
def button_add_line(self):
for rec in self:
if rec.state in ["new", "draft"]:
rec.add_line()
rec.reset_add_line()
def reset_add_line(self):
self.write({"add_line_project_id": False, "add_line_task_id": False})
def _get_date_name(self, date):
name = babel.dates.format_skeleton(
skeleton="MMMEd",
datetime=datetime.combine(date, time.min),
locale=(self.env.context.get("lang") or self.env.user.lang or "en_US"),
)
name = re.sub(r"(\s*[^\w\d\s])\s+", r"\1\n", name)
name = re.sub(r"([\w\d])\s([\w\d])", "\\1\u00A0\\2", name)
return name
def _get_dates(self):
start = self.date_start
end = self.date_end
if end < start:
return []
dates = [start]
while start != end:
start += relativedelta(days=1)
dates.append(start)
return dates
def _get_line_name(self, project_id, task_id=None, **kwargs):
self.ensure_one()
if task_id:
return "{} - {}".format(
project_id.name_get()[0][1], task_id.name_get()[0][1]
)
return project_id.name_get()[0][1]
def _get_new_line_unique_id(self):
"""Hook for extensions"""
self.ensure_one()
return {
"project_id": self.add_line_project_id,
"task_id": self.add_line_task_id,
}
def _get_default_sheet_line(self, matrix, key):
self.ensure_one()
values = {
"value_x": self._get_date_name(key.date),
"value_y": self._get_line_name(**key._asdict()),
"date": key.date,
"project_id": key.project_id.id,
"task_id": key.task_id.id,
"unit_amount": sum(t.unit_amount for t in matrix[key]),
"employee_id": self.employee_id.id,
"company_id": self.company_id.id,
}
if self.id:
values.update({"sheet_id": self.id})
return values
@api.model
def _prepare_empty_analytic_line(self):
return {
"name": empty_name,
"employee_id": self.employee_id.id,
"date": self.date_start,
"project_id": self.add_line_project_id.id,
"task_id": self.add_line_task_id.id,
"sheet_id": self.id,
"unit_amount": 0.0,
"company_id": self.company_id.id,
}
def add_line(self):
if not self.add_line_project_id:
return
values = self._prepare_empty_analytic_line()
new_line_unique_id = self._get_new_line_unique_id()
existing_unique_ids = list(
{frozenset(line.get_unique_id().items()) for line in self.line_ids}
)
if existing_unique_ids:
self.delete_empty_lines(False)
if frozenset(new_line_unique_id.items()) not in existing_unique_ids:
new_line = self.env["account.analytic.line"]._sheet_create(values)
self.write({"timesheet_ids": [(4, new_line.id)]})
def link_timesheets_to_sheet(self, timesheets):
self.ensure_one()
if self.id and self.state in ["new", "draft"]:
for aal in timesheets.filtered(lambda a: not a.sheet_id):
aal.write({"sheet_id": self.id})
def clean_timesheets(self, timesheets):
repeated = timesheets.filtered(
lambda t: t.name == empty_name and not t.timesheet_invoice_id
)
if len(repeated) > 1 and self.id:
return repeated.merge_timesheets()
return timesheets
def _is_add_line(self, row):
"""Hook for extensions"""
self.ensure_one()
return (
self.add_line_project_id == row.project_id
and self.add_line_task_id == row.task_id
)
@api.model
def _is_line_of_row(self, aal, row):
"""Hook for extensions"""
return (
aal.project_id.id == row.project_id.id and aal.task_id.id == row.task_id.id
)
def delete_empty_lines(self, delete_empty_rows=False):
self.ensure_one()
for name in list(set(self.line_ids.mapped("value_y"))):
rows = self.line_ids.filtered(lambda l: l.value_y == name)
if not rows:
continue
row = fields.first(rows)
if delete_empty_rows and self._is_add_line(row):
check = any([line.unit_amount for line in rows])
else:
check = not all([line.unit_amount for line in rows])
if not check:
continue
row_lines = self.timesheet_ids.filtered(
lambda aal: self._is_line_of_row(aal, row)
)
row_lines.filtered(
lambda t: t.name == empty_name
and not t.unit_amount
and not t.timesheet_invoice_id
).unlink()
if self.timesheet_ids != self.timesheet_ids.exists():
self._sheet_write("timesheet_ids", self.timesheet_ids.exists())
def _update_analytic_lines_from_new_lines(self, vals):
self.ensure_one()
new_line_ids_list = []
for line in vals.get("line_ids", []):
# Every time we change a value in the grid a new line in line_ids
# is created with the proposed changes, even though the line_ids
# is a computed field. We capture the value of 'new_line_ids'
# in the proposed dict before it disappears.
# This field holds the ids of the transient records
# of model 'hr_timesheet.sheet.new.analytic.line'.
if line[0] == 1 and line[2] and line[2].get("new_line_id"):
new_line_ids_list += [line[2].get("new_line_id")]
for new_line in self.new_line_ids.exists():
if new_line.id in new_line_ids_list:
new_line._update_analytic_lines()
self.new_line_ids.exists().unlink()
self._sheet_write("new_line_ids", self.new_line_ids.exists())
@api.model
def _prepare_new_line(self, line):
"""Hook for extensions"""
return {
"sheet_id": line.sheet_id.id,
"date": line.date,
"project_id": line.project_id.id,
"task_id": line.task_id.id,
"unit_amount": line.unit_amount,
"company_id": line.company_id.id,
"employee_id": line.employee_id.id,
}
def _is_compatible_new_line(self, line_a, line_b):
"""Hook for extensions"""
self.ensure_one()
return (
line_a.project_id.id == line_b.project_id.id
and line_a.task_id.id == line_b.task_id.id
and line_a.date == line_b.date
)
def add_new_line(self, line):
self.ensure_one()
new_line_model = self.env["hr_timesheet.sheet.new.analytic.line"]
new_line = self.new_line_ids.filtered(
lambda l: self._is_compatible_new_line(l, line)
)
if new_line:
new_line.write({"unit_amount": line.unit_amount})
else:
vals = self._prepare_new_line(line)
new_line = new_line_model.create(vals)
self._sheet_write("new_line_ids", self.new_line_ids | new_line)
line.new_line_id = new_line.id
@api.model
def _get_period_start(self, company, date):
r = company and company.sheet_range or "WEEKLY"
if r == "WEEKLY":
if company.timesheet_week_start:
delta = relativedelta(weekday=int(company.timesheet_week_start), days=6)
else:
delta = relativedelta(days=date.weekday())
return date - delta
elif r == "MONTHLY":
return date + relativedelta(day=1)
return date
@api.model
def _get_period_end(self, company, date):
r = company and company.sheet_range or "WEEKLY"
if r == "WEEKLY":
if company.timesheet_week_start:
delta = relativedelta(
weekday=(int(company.timesheet_week_start) + 6) % 7
)
else:
delta = relativedelta(days=6 - date.weekday())
return date + delta
elif r == "MONTHLY":
return date + relativedelta(months=1, day=1, days=-1)
return date
# ------------------------------------------------
# OpenChatter methods and notifications
# ------------------------------------------------
def _track_subtype(self, init_values):
if self:
record = self[0]
if "state" in init_values and record.state == "confirm":
return self.env.ref("hr_timesheet_sheet.mt_timesheet_confirmed")
elif "state" in init_values and record.state == "done":
return self.env.ref("hr_timesheet_sheet.mt_timesheet_approved")
return super()._track_subtype(init_values)
class AbstractSheetLine(models.AbstractModel):
_name = "hr_timesheet.sheet.line.abstract"
_description = "Abstract Timesheet Sheet Line"
sheet_id = fields.Many2one(comodel_name="hr_timesheet.sheet", ondelete="cascade")
date = fields.Date()
project_id = fields.Many2one(comodel_name="project.project", string="Project")
task_id = fields.Many2one(comodel_name="project.task", string="Task")
unit_amount = fields.Float(string="Quantity", default=0.0)
company_id = fields.Many2one(comodel_name="res.company", string="Company")
employee_id = fields.Many2one(comodel_name="hr.employee", string="Employee")
def get_unique_id(self):
"""Hook for extensions"""
self.ensure_one()
return {"project_id": self.project_id, "task_id": self.task_id}
class SheetLine(models.TransientModel):
_name = "hr_timesheet.sheet.line"
_inherit = "hr_timesheet.sheet.line.abstract"
_description = "Timesheet Sheet Line"
value_x = fields.Char(string="Date Name")
value_y = fields.Char(string="Project Name")
new_line_id = fields.Integer(default=0)
@api.onchange("unit_amount")
def onchange_unit_amount(self):
"""This method is called when filling a cell of the matrix."""
self.ensure_one()
sheet = self._get_sheet()
if not sheet:
return {
"warning": {
"title": _("Warning"),
"message": _("Save the Timesheet Sheet first."),
}
}
sheet.add_new_line(self)
@api.model
def _get_sheet(self):
sheet = (self._origin or self).sheet_id
if not sheet:
model = self.env.context.get("params", {}).get("model", "")
obj_id = self.env.context.get("params", {}).get("id")
if model == "hr_timesheet.sheet" and isinstance(obj_id, int):
sheet = self.env["hr_timesheet.sheet"].browse(obj_id)
return sheet
class SheetNewAnalyticLine(models.TransientModel):
_name = "hr_timesheet.sheet.new.analytic.line"
_inherit = "hr_timesheet.sheet.line.abstract"
_description = "Timesheet Sheet New Analytic Line"
@api.model
def _is_similar_analytic_line(self, aal):
"""Hook for extensions"""
return (
aal.date == self.date
and aal.project_id.id == self.project_id.id
and aal.task_id.id == self.task_id.id
)
@api.model
def _update_analytic_lines(self):
sheet = self.sheet_id
timesheets = sheet.timesheet_ids.filtered(
lambda aal: self._is_similar_analytic_line(aal)
)
new_ts = timesheets.filtered(lambda t: t.name == empty_name)
amount = sum(t.unit_amount for t in timesheets)
diff_amount = self.unit_amount - amount
if len(new_ts) > 1:
new_ts = new_ts.merge_timesheets()
sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
if not diff_amount:
return
if new_ts:
unit_amount = new_ts.unit_amount + diff_amount
if unit_amount:
new_ts.write({"unit_amount": unit_amount})
else:
new_ts.unlink()
sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
else:
new_ts_values = sheet._prepare_new_line(self)
new_ts_values.update({"name": empty_name, "unit_amount": diff_amount})
self.env["account.analytic.line"]._sheet_create(new_ts_values)

View file

@ -0,0 +1,39 @@
# Copyright 2018 ForgeFlow, S.L.
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
_WEEKDAYS = [
("0", "Monday"),
("1", "Tuesday"),
("2", "Wednesday"),
("3", "Thursday"),
("4", "Friday"),
("5", "Saturday"),
("6", "Sunday"),
]
class ResCompany(models.Model):
_inherit = "res.company"
sheet_range = fields.Selection(
[("MONTHLY", "Month"), ("WEEKLY", "Week"), ("DAILY", "Day")],
string="Timesheet Sheet Range",
default="WEEKLY",
help="The range of your Timesheet Sheet.",
)
timesheet_week_start = fields.Selection(
selection=_WEEKDAYS, string="Week start day", default="0"
)
timesheet_sheet_review_policy = fields.Selection(
selection=[
("hr", "By HR Officers"),
("hr_manager", "By HR Managers"),
("timesheet_manager", "By Timesheets Managers"),
],
default="hr",
help="How Timesheet Sheets review is performed.",
)

View file

@ -0,0 +1,27 @@
# Copyright 2018 ForgeFlow, S.L.
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ResConfig(models.TransientModel):
_inherit = "res.config.settings"
sheet_range = fields.Selection(
related="company_id.sheet_range",
string="Timesheet Sheet Range",
help="The range of your Timesheet Sheet.",
readonly=False,
)
timesheet_week_start = fields.Selection(
related="company_id.timesheet_week_start",
string="Week Start Day",
help="Starting day for Timesheet Sheets.",
readonly=False,
)
timesheet_sheet_review_policy = fields.Selection(
related="company_id.timesheet_sheet_review_policy", readonly=False
)