mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'mig/15.0/sale_timesheet_work_entry_rate' into '15.0'
mig/15.0/sale_timesheet_work_entry_rate into 15.0 See merge request hibou-io/hibou-odoo/suite!1146
This commit is contained in:
3
sale_timesheet_work_entry_rate/__init__.py
Normal file
3
sale_timesheet_work_entry_rate/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
25
sale_timesheet_work_entry_rate/__manifest__.py
Normal file
25
sale_timesheet_work_entry_rate/__manifest__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Timesheet Billing Rate',
|
||||
'version': '15.0.1.0.0',
|
||||
'category': 'Sale',
|
||||
'author': 'Hibou Corp.',
|
||||
'license': 'OPL-1',
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'hibou_professional',
|
||||
'hr_timesheet_work_entry',
|
||||
'sale_timesheet',
|
||||
],
|
||||
'data': [
|
||||
'views/timesheet_views.xml',
|
||||
'views/work_entry_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/hr_timesheet_work_entry_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'application': False,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_timesheet_work_entry.work_input_timesheet_internal" model="hr.work.entry.type">
|
||||
<field name="timesheet_billing_rate" eval="0.0"/>
|
||||
</record>
|
||||
|
||||
<record id="work_input_timesheet_double" model="hr.work.entry.type">
|
||||
<field name="name">Double Time</field>
|
||||
<field name="code">TS_DOUBLE</field>
|
||||
<field name="allow_timesheet" eval="True"/>
|
||||
<field name="timesheet_billing_rate" eval="2.0"/>
|
||||
</record>
|
||||
|
||||
<record id="work_input_timesheet_free" model="hr.work.entry.type">
|
||||
<field name="name">Non-Productive Time</field>
|
||||
<field name="code">TS_NON_PRODUCTIVE</field>
|
||||
<field name="allow_timesheet" eval="True"/>
|
||||
<field name="timesheet_billing_rate" eval="0.0"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
5
sale_timesheet_work_entry_rate/models/__init__.py
Normal file
5
sale_timesheet_work_entry_rate/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import sale
|
||||
from . import timesheet
|
||||
from . import work_entry
|
||||
63
sale_timesheet_work_entry_rate/models/sale.py
Normal file
63
sale_timesheet_work_entry_rate/models/sale.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
@api.depends('analytic_line_ids.work_type_id')
|
||||
def _compute_qty_delivered(self):
|
||||
super(SaleOrderLine, self)._compute_qty_delivered()
|
||||
|
||||
# Overridden to select work_type_id and do multiplication at the end
|
||||
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', 'product_uom_id', 'work_type_id'], ['product_uom_id', 'so_line', 'work_type_id'], lazy=False
|
||||
)
|
||||
|
||||
# convert uom and sum all unit_amount 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)}
|
||||
work_type_ids = [item['work_type_id'][0] for item in data if item['work_type_id']]
|
||||
work_type_map = {work.id: work for work in self.env['hr.work.entry.type'].browse(work_type_ids)}
|
||||
for item in data:
|
||||
if not item['product_uom_id']:
|
||||
continue
|
||||
work_type_rate = False
|
||||
if item['work_type_id']:
|
||||
work_type_rate = work_type_map.get(item['work_type_id'][0]).timesheet_billing_rate
|
||||
if work_type_rate is False:
|
||||
# unset field should be 1.0 by default, you CAN set it to 0.0 if you'd like.
|
||||
work_type_rate = 1.0
|
||||
|
||||
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 so_line.product_uom.category_id == uom.category_id:
|
||||
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP')
|
||||
else:
|
||||
qty = item['unit_amount']
|
||||
|
||||
qty *= work_type_rate
|
||||
result[so_line_id] += qty
|
||||
|
||||
return result
|
||||
9
sale_timesheet_work_entry_rate/models/timesheet.py
Normal file
9
sale_timesheet_work_entry_rate/models/timesheet.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
work_billing_rate = fields.Float(related='work_type_id.timesheet_billing_rate', string='Billing Multiplier')
|
||||
9
sale_timesheet_work_entry_rate/models/work_entry.py
Normal file
9
sale_timesheet_work_entry_rate/models/work_entry.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrWorkEntryType(models.Model):
|
||||
_inherit = 'hr.work.entry.type'
|
||||
|
||||
timesheet_billing_rate = fields.Float(string='Timesheet Billing Multiplier', default=1.0)
|
||||
3
sale_timesheet_work_entry_rate/tests/__init__.py
Normal file
3
sale_timesheet_work_entry_rate/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import test_sale_flow
|
||||
100
sale_timesheet_work_entry_rate/tests/test_sale_flow.py
Normal file
100
sale_timesheet_work_entry_rate/tests/test_sale_flow.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale_timesheet.tests.test_project_billing import TestProjectBilling
|
||||
|
||||
|
||||
class TestSaleFlow(TestProjectBilling):
|
||||
|
||||
# Mainly from test_billing_task_rate
|
||||
# Additional tests at the bottom.
|
||||
def test_billing_work_entry_rate(self):
|
||||
Task = self.env['project.task'].with_context(tracking_disable=True)
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
|
||||
# create a task
|
||||
task = Task.with_context(default_project_id=self.project_task_rate.id).create({
|
||||
'name': 'first task',
|
||||
})
|
||||
|
||||
self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task, "Task created in a project billed on 'task rate' should be linked to a SOL containing a prepaid service product and the remaining hours of this SOL should be greater than 0.")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'task rate' should have the same customer as the one from the project")
|
||||
|
||||
# log timesheet on task
|
||||
timesheet1 = Timesheet.create({
|
||||
'name': 'Test Line',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 50,
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
|
||||
self.assertEqual(task.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the task since the pricing type of the project is task rate.")
|
||||
|
||||
# create a subtask
|
||||
subtask = Task.with_context(default_project_id=self.project_task_rate.id).create({
|
||||
'name': 'first subtask task',
|
||||
'parent_id': task.id,
|
||||
'display_project_id': self.project_subtask.id,
|
||||
})
|
||||
|
||||
self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask should have the same customer as the one from their mother")
|
||||
|
||||
# log timesheet on subtask
|
||||
timesheet2 = Timesheet.create({
|
||||
'name': 'Test Line on subtask',
|
||||
'project_id': subtask.display_project_id.id,
|
||||
'task_id': subtask.id,
|
||||
'unit_amount': 50,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(subtask.display_project_id, timesheet2.project_id, "The timesheet is in the subtask project")
|
||||
self.assertFalse(timesheet2.so_line, "The timesheet should not be linked to SOL as it's a non billable project")
|
||||
|
||||
# move task and subtask into task rate project
|
||||
task.write({
|
||||
'project_id': self.project_employee_rate.id,
|
||||
})
|
||||
subtask.write({
|
||||
'display_project_id': self.project_employee_rate.id,
|
||||
})
|
||||
|
||||
self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task moved in a employee rate billable project should keep its SOL because the partner_id has not changed too.")
|
||||
self.assertEqual(task.partner_id, self.project_task_rate.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from its initial project.")
|
||||
|
||||
self.assertEqual(subtask.sale_line_id, subtask.parent_id.sale_line_id, "Subtask moved in a employee rate billable project should have the SOL of its parent since it keep its partner_id and this partner is different than the one in the destination project.")
|
||||
self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask moved in a project billed on 'employee rate' should keep its initial customer, that is the one of its parent.")
|
||||
|
||||
default_work_entry_type = self.env.ref('hr_timesheet_work_entry.work_input_timesheet')
|
||||
# Timesheets were for regular default 'Timesheet' type
|
||||
self.assertEqual((timesheet1 + timesheet2).mapped('work_type_id'), default_work_entry_type)
|
||||
# Line is set and total adds up to all of the timesheets.
|
||||
self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task)
|
||||
self.assertEqual(task.sale_line_id.qty_delivered, 50.0)
|
||||
|
||||
double_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_double')
|
||||
self.assertEqual(double_rate_work_entry_type.timesheet_billing_rate, 2.0)
|
||||
|
||||
# Convert to double rate.
|
||||
timesheet1.write({
|
||||
'work_type_id': double_rate_work_entry_type.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id.qty_delivered, 100.0)
|
||||
|
||||
# Ensure that a created timesheet WITHOUT a work entry type behaves
|
||||
# the same as it would have before this module (e.g. for historic reasons)
|
||||
timesheet1.write({
|
||||
'work_type_id': False,
|
||||
})
|
||||
timesheet2.write({
|
||||
'work_type_id': False,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id.qty_delivered, 50.0)
|
||||
|
||||
# Ensure we can bill zero even with above default.
|
||||
zero_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_free')
|
||||
self.assertEqual(zero_rate_work_entry_type.timesheet_billing_rate, 0.0)
|
||||
|
||||
timesheet1.write({
|
||||
'work_type_id': zero_rate_work_entry_type.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id.qty_delivered, 0.0)
|
||||
22
sale_timesheet_work_entry_rate/views/timesheet_views.xml
Normal file
22
sale_timesheet_work_entry_rate/views/timesheet_views.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_task_form2_inherited_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherited.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='timesheet_ids']/tree/field[@name='name']" position="before">
|
||||
<field name="work_type_id"
|
||||
domain="[('allow_timesheet', '=', True)]"
|
||||
context="{'default_allow_timesheet': True}" />
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']/form//field[@name='name']" position="before">
|
||||
<field name="work_type_id"
|
||||
domain="[('allow_timesheet', '=', True)]"
|
||||
context="{'default_allow_timesheet': True}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
15
sale_timesheet_work_entry_rate/views/work_entry_views.xml
Normal file
15
sale_timesheet_work_entry_rate/views/work_entry_views.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_work_entry_type_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.work.type.view.form.inherit</field>
|
||||
<field name="model">hr.work.entry.type</field>
|
||||
<field name="inherit_id" ref="hr_timesheet_work_entry.hr_work_entry_type_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='allow_timesheet']" position="after">
|
||||
<field name="timesheet_billing_rate"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user