-
-
Notifications
You must be signed in to change notification settings - Fork 334
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[16.0][ADD] hr_timesheet_hours_billed:hours_billed
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
1 parent
a9d4161
commit d7e7969
Showing
14 changed files
with
340 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import account_analytic_line | ||
from . import sale_order_line |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_hours_billed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
) |
67 changes: 67 additions & 0 deletions
67
hr_timesheet_hours_billed/views/account_analytic_line_view.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<odoo> | ||
<record id="hr_timesheet_line_tree" model="ir.ui.view"> | ||
<field name="name">Hours Billed</field> | ||
<field name="model">account.analytic.line</field> | ||
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_tree" /> | ||
<field name="arch" type="xml"> | ||
<field name="unit_amount" position="after"> | ||
<field | ||
name="unit_amount_billed" | ||
optional="show" | ||
groups="hr_timesheet.group_timesheet_manager" | ||
widget="timesheet_uom" | ||
/> | ||
<field | ||
name="approved" | ||
optional="show" | ||
groups="hr_timesheet.group_timesheet_manager" | ||
/> | ||
<field | ||
name="approved_user_id" | ||
optional="hide" | ||
groups="hr_timesheet.group_timesheet_manager" | ||
/> | ||
<field | ||
name="approved_date" | ||
optional="hide" | ||
groups="hr_timesheet.group_timesheet_manager" | ||
/> | ||
</field> | ||
</field> | ||
</record> | ||
|
||
<record model="ir.actions.server" id="action_approve_records"> | ||
<field name="name">Approve</field> | ||
<field | ||
name="model_id" | ||
ref="hr_timesheet_hours_billed.model_account_analytic_line" | ||
/> | ||
<field | ||
name="binding_model_id" | ||
ref="hr_timesheet_hours_billed.model_account_analytic_line" | ||
/> | ||
<field name="binding_view_types">list</field> | ||
<field name="state">code</field> | ||
<field name="code"> | ||
action = records.action_approve_records() | ||
</field> | ||
</record> | ||
|
||
<record model="ir.actions.server" id="action_decline_records"> | ||
<field name="name">Decline</field> | ||
<field | ||
name="model_id" | ||
ref="hr_timesheet_hours_billed.model_account_analytic_line" | ||
/> | ||
<field | ||
name="binding_model_id" | ||
ref="hr_timesheet_hours_billed.model_account_analytic_line" | ||
/> | ||
<field name="binding_view_types">list</field> | ||
<field name="state">code</field> | ||
<field name="code"> | ||
action = records.action_decline_records() | ||
</field> | ||
</record> | ||
</odoo> |
1 change: 1 addition & 0 deletions
1
setup/hr_timesheet_hours_billed/odoo/addons/hr_timesheet_hours_billed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../hr_timesheet_hours_billed |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import setuptools | ||
|
||
setuptools.setup( | ||
setup_requires=['setuptools-odoo'], | ||
odoo_addon=True, | ||
) |