Skip to content

Commit

Permalink
[16.0][ADD] hr_timesheet_hours_billed:hours_billed
Browse files Browse the repository at this point in the history
This module allows to specify billed amount of time in timesheet while
still keeping original time tracked. Additional field "Hours Billed" and
"Approved" "Approved by" and "Approved on" are added to timesheet.
These fields are visible only for "Timesheets: Administrator" group
  • Loading branch information
OCA-git-bot authored and Andrey committed Jul 3, 2023
1 parent 5f8d1e8 commit 27d023b
Show file tree
Hide file tree
Showing 18 changed files with 352 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .copier-answers.yml
@@ -1,10 +1,11 @@
# Do NOT update manually; changes here will be overwritten by Copier
_commit: v1.11.0
_commit: v1.14.1
_src_path: gh:oca/oca-addons-repo-template
ci: GitHub
dependency_installation_mode: PIP
generate_requirements_txt: true
github_check_license: true
github_ci_extra_env: {}
github_enable_codecov: true
github_enable_makepot: true
github_enable_stale_action: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yml
Expand Up @@ -11,7 +11,7 @@ on:

jobs:
pre-commit:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Expand Up @@ -28,7 +28,7 @@ jobs:
fi
done
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
container: ${{ matrix.container }}
name: ${{ matrix.name }}
strategy:
Expand Down
11 changes: 8 additions & 3 deletions .pre-commit-config.yaml
Expand Up @@ -27,6 +27,11 @@ repos:
entry: found forbidden files; remove them
language: fail
files: "\\.rej$"
- id: en-po-files
name: en.po files cannot exist
entry: found a en.po file
language: fail
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
- repo: https://github.com/oca/maintainer-tools
rev: 4cd2b852214dead80822e93e6749b16f2785b2fe
hooks:
Expand Down Expand Up @@ -96,15 +101,15 @@ repos:
- id: pyupgrade
args: ["--keep-percent-format"]
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.12.0
hooks:
- id: isort
name: isort except __init__.py
args:
- --settings=.
exclude: /__init__\.py$
- repo: https://github.com/acsone/setuptools-odoo
rev: 3.1.5
rev: 3.1.8
hooks:
- id: setuptools-odoo-make-default
- id: setuptools-odoo-get-requirements
Expand All @@ -113,7 +118,7 @@ repos:
- requirements.txt
- --header
- "# generated from manifests external_dependencies"
- repo: https://gitlab.com/PyCQA/flake8
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8
Expand Down
Empty file.
1 change: 1 addition & 0 deletions hr_timesheet_hours_billed/__init__.py
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions hr_timesheet_hours_billed/__manifest__.py
@@ -0,0 +1,17 @@
{
"name": "Timesheet Hours Billed",
"summary": """Add ‘Hours Billed’, 'Approved', 'Approved by',
'Approved on' field for timesheets""",
"version": "16.0.1.0.0",
"category": "Timesheet",
"website": "https://github.com/OCA/timesheet",
"maintainers": ["solo4games", "CetmixGitDrone"],
"author": "Odoo Community Association (OCA), Cetmix",
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": ["hr_timesheet", "sale_management"],
"data": [
"views/account_analytic_line_view.xml",
],
}
2 changes: 2 additions & 0 deletions hr_timesheet_hours_billed/models/__init__.py
@@ -0,0 +1,2 @@
from . import account_analytic_line
from . import sale_order_line
39 changes: 39 additions & 0 deletions hr_timesheet_hours_billed/models/account_analytic_line.py
@@ -0,0 +1,39 @@
# Copyright 2023-nowdays Cetmix OU (https://cetmix.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, fields, models


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

approved = fields.Boolean(default=False, inverse="_inverse_approved")
approved_user_id = fields.Many2one(
"res.users",
string="Approved by",
index=True,
tracking=True,
readonly=True,
)
approved_date = fields.Datetime(copy=False, readonly=True)
unit_amount_billed = fields.Float(string="Hours Billed")

# set approved_user_id and date if approved state change
def _inverse_approved(self):
user = self.env.user
now = fields.Datetime.now()
for record in self.filtered(lambda rec: rec.approved):
record.write({"approved_date": now, "approved_user_id": user})

# override create method to initialize unit_amount_billed
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get("unit_amount_billed"):
vals["unit_amount_billed"] = vals["unit_amount"]
return super(AccountAnalyticLine, self).create(vals_list)

def action_approve_records(self):
self.filtered(lambda rec: not rec.approved).write({"approved": True})

def action_decline_records(self):
self.filtered(lambda rec: rec.approved).write({"approved": False})
75 changes: 75 additions & 0 deletions hr_timesheet_hours_billed/models/sale_order_line.py
@@ -0,0 +1,75 @@
# Copyright 2023-nowdays Cetmix OU (https://cetmix.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, models
from odoo.osv import expression


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

@api.depends(
"qty_delivered_method",
"analytic_line_ids.so_line",
"analytic_line_ids.unit_amount_billed",
"analytic_line_ids.product_uom_id",
"analytic_line_ids.approved",
)
def _compute_qty_delivered(self):
"""This method compute the delivered quantity of the SO lines:
it covers the case provide by sale module, aka
expense/vendor bills (sum of unit_amount_billed of AAL), and manual case.
"""

return super(SaleOrderLine, self)._compute_qty_delivered()

def _get_delivered_quantity_by_analytic(self, additional_domain):
"""Compute and write the delivered quantity of current SO lines,
based on their related analytic lines.
:param additional_domain: domain to restrict AAL to include in computation
(required since timesheet is an AAL with a project ...)
"""
result = {}

# avoid recomputation if no SO lines concerned
if not self:
return result

# group analytic lines by product uom and so line
domain = expression.AND([[("so_line", "in", self.ids)], additional_domain])
data = self.env["account.analytic.line"].read_group(
domain,
["so_line", "unit_amount_billed", "approved", "product_uom_id"],
["product_uom_id", "so_line", "approved"],
lazy=False,
)

# convert uom and sum all unit_amount_billed of analytic lines
# to get the delivered qty of SO lines
# browse so lines and product uoms here to make them share the same prefetch
lines = self.browse([item["so_line"][0] for item in data])
lines_map = {line.id: line for line in lines}
product_uom_ids = [
item["product_uom_id"][0] for item in data if item["product_uom_id"]
]
product_uom_map = {
uom.id: uom for uom in self.env["uom.uom"].browse(product_uom_ids)
}
for item in data:
if not item["product_uom_id"]:
continue
so_line_id = item["so_line"][0]
so_line = lines_map[so_line_id]
result.setdefault(so_line_id, 0.0)
uom = product_uom_map.get(item["product_uom_id"][0])
if item.get("approved"):
if so_line.product_uom.category_id == uom.category_id:
qty = uom._compute_quantity(
item["unit_amount_billed"],
so_line.product_uom,
rounding_method="HALF-UP",
)
else:
qty = item["unit_amount_billed"]
result[so_line_id] += qty

return result
9 changes: 9 additions & 0 deletions hr_timesheet_hours_billed/readme/DESCRIPTION.rst
@@ -0,0 +1,9 @@
This module allows to specify billed amount of time in timesheet while still keeping original time tracked.
It might be useful if you want to invoice only part of the time logged in the timesheet.
It also implements „approval“ mechanism so only approved timesheets will be invoiced.

Additional field "Hours Billed" and "Approved" "Approved by" and "Approved on" are added to timesheet.
These fields are visible only for "Timesheets: Administrator" group
"Hours Billed" field is used to compute Delivered Quantity in related Sale Order line.Only timesheets with "Approved" enabled are counted.

By default "Hours Bulled" field values are populated from the "Hours Spent" field of the timesheet.
Empty file.
5 changes: 5 additions & 0 deletions hr_timesheet_hours_billed/readme/USAGE.rst
@@ -0,0 +1,5 @@
To use this module you need to:

#. Go to Timesheets, there you can see new rows called 'Hours Billed', 'Approved', 'Approved by', 'Approved date'.
#. Modify 'Approved' field. Choose yes, then field 'Approved by' will be filled with the current user, field 'Approved date' will be filled with the current date.
#. Field 'Hours Billed' is used to calcualte 'Delivered' field in sale order line, only approved timesheets will be invoiced.
1 change: 1 addition & 0 deletions hr_timesheet_hours_billed/tests/__init__.py
@@ -0,0 +1 @@
from . import test_hours_billed
117 changes: 117 additions & 0 deletions hr_timesheet_hours_billed/tests/test_hours_billed.py
@@ -0,0 +1,117 @@
# Copyright 2023-nowdays Cetmix OU (https://cetmix.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo.tests import tagged

from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet


@tagged("-at_install", "post_install")
class TestCommonHourBilled(TestCommonSaleTimesheet):
# @classmethod
def setUp(self):

super().setUp()
self.task_rate_task = self.env["project.task"].create(
{
"name": "Task",
"project_id": self.project_task_rate.id,
"sale_line_id": self.so.order_line[0].id,
}
)

def test_compute_hours_billed(self):
# create some timesheet on this task
timesheet1 = self.env["account.analytic.line"].create(
{
"name": "Test Line",
"project_id": self.project_task_rate.id,
"task_id": self.task_rate_task.id,
"unit_amount": 5,
"employee_id": self.employee_manager.id,
"approved": True,
}
)

# Check on creation if unit_amount_billed not set it must be equal unit_amount
self.assertEqual(
timesheet1.unit_amount,
timesheet1.unit_amount_billed,
"Hours Billed and Hours Spent should be the same",
)
# Create some timesheet on this task
timesheet2 = self.env["account.analytic.line"].create(
{
"name": "Test Line",
"project_id": self.project_task_rate.id,
"task_id": self.task_rate_task.id,
"unit_amount": 5,
"unit_amount_billed": 10,
"employee_id": self.employee_manager.id,
}
)
# Check on creation if unit_amount_billed set it shouldn't be equal unit_amount
self.assertNotEqual(
timesheet2.unit_amount,
timesheet2.unit_amount_billed,
"Hours Billed and Hours Spent should be different",
)
# Check about recompute qty_delivered after changing unit_amount_billed
before_change_amount_billed = self.so.order_line[0].qty_delivered
timesheet1.write({"unit_amount_billed": 20.0})

self.assertNotEqual(
before_change_amount_billed,
self.so.order_line[0].qty_delivered,
"qty_deliverede must be recompute, after changing unit_amount_billed",
)

# Check about recompute qty_delivered after changing approved
before_change_approved = self.so.order_line[0].qty_delivered
timesheet1.write({"approved": False})

self.assertNotEqual(
before_change_approved,
self.so.order_line[0].qty_delivered,
"qty_deliverede must be recompute, after changing approved",
)

def test_compute_approved_user_id(self):
# Create some timesheet on this task
timesheet3 = self.env["account.analytic.line"].create(
{
"name": "Test Line",
"project_id": self.project_task_rate.id,
"task_id": self.task_rate_task.id,
"unit_amount": 5,
"employee_id": self.employee_manager.id,
}
)
uid_before_change_approved = timesheet3.approved_user_id

timesheet3.write({"approved": True})
# Check about changing approved_user_id after changing state of approved to "yes"
self.assertNotEqual(
uid_before_change_approved,
timesheet3.approved_user_id,
"approved_user_id must be different",
)

def test_compute_approved_date(self):
# Create some timesheet on this task
timesheet4 = self.env["account.analytic.line"].create(
{
"name": "Test Line",
"project_id": self.project_task_rate.id,
"task_id": self.task_rate_task.id,
"unit_amount": 5,
"employee_id": self.employee_manager.id,
}
)
# Check about changing approved_date after changing state of approved to "yes"
date_before_change_approved = timesheet4.approved_date
timesheet4.write({"approved": True})
self.assertNotEqual(
date_before_change_approved,
timesheet4.approved_date,
"approved_date must be different",
)

0 comments on commit 27d023b

Please sign in to comment.