[IMP] sale_project: allow the creation of new records from the SO

view

The goal of these changes is to make it easier for the user to manage
their SO. They can create new tickets/timesheets/etc without being
forced to leave the application to open the one they need, nor do they
need to manually link the new record to the SO

task-2954409

closes odoo/odoo#100300

Related: odoo/enterprise#31391
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
Damien Raquet
2023-05-30 19:09:53 +02:00
committed by Xavier Bol (xbo)
parent 57168aa648
commit 76a6a0d2ec
11 changed files with 393 additions and 58 deletions
@@ -859,6 +859,30 @@
</field>
</record>
<record id="view_task_kanban_inherit_view_default_project" model="ir.ui.view" >
<field name="name">project.task.kanban.sale.order</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="view_task_kanban"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<kanban position="attributes">
<attribute name="quick_create_view">project.quick_create_task_form_inherit_view_default_project</attribute>
</kanban>
</field>
</record>
<record id="quick_create_task_form_inherit_view_default_project" model="ir.ui.view">
<field name="name">project.task.form.quick.create.sale.order.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="quick_create_task_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<field name="project_id" position="attributes">
<attribute name="invisible">0</attribute>
</field>
</field>
</record>
<record id="action_view_my_task" model="ir.actions.act_window">
<field name="name">My Tasks</field>
<field name="res_model">project.task</field>
+12
View File
@@ -579,6 +579,18 @@ class Project(models.Model):
return not self.allow_billable
def action_view_tasks(self):
if self.env.context.get('generate_milestone'):
line_id = self.env.context.get('default_sale_line_id')
default_line = self.env['sale.order.line'].browse(line_id)
milestone = self.env['project.milestone'].create({
'name': default_line.name,
'project_id': self.id,
'sale_line_id': line_id,
'quantity_percentage': 1,
})
if default_line.product_id.service_tracking == 'task_in_project':
default_line.task_id.milestone_id = milestone.id
action = super().action_view_tasks()
action['context']['hide_partner'] = self._get_hide_partner()
return action
+77 -33
View File
@@ -21,6 +21,9 @@ class SaleOrder(models.Model):
project_count = fields.Integer(string='Number of Projects', compute='_compute_project_ids', groups='project.group_project_user')
milestone_count = fields.Integer(compute='_compute_milestone_count')
is_product_milestone = fields.Boolean(compute='_compute_is_product_milestone')
show_create_project_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user')
show_project_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user')
show_task_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user')
def _compute_milestone_count(self):
read_group = self.env['project.milestone']._read_group(
@@ -36,6 +39,18 @@ class SaleOrder(models.Model):
for order in self:
order.is_product_milestone = order.order_line.product_id.filtered(lambda p: p.service_policy == 'delivered_milestones')
def _compute_show_project_and_task_button(self):
is_project_manager = self.env.user.has_group('project.group_project_manager')
show_button_ids = self.env['sale.order.line']._read_group([
('order_id', 'in', self.ids),
('order_id.state', 'not in', ['draft', 'sent']),
('product_id.detailed_type', '=', 'service'),
], aggregates=['order_id:array_agg'])[0][0]
for order in self:
order.show_project_button = order.id in show_button_ids and order.project_count
order.show_task_button = order.show_project_button or order.tasks_count
order.show_create_project_button = is_project_manager and order.id in show_button_ids and not order.project_count
@api.depends('order_line.product_id.project_id')
def _compute_tasks_ids(self):
for order in self:
@@ -62,6 +77,7 @@ class SaleOrder(models.Model):
projects = order.order_line.mapped('product_id.project_id')
projects |= order.order_line.mapped('project_id')
projects |= order.project_id
projects = projects.filtered('active')
projects |= projects_per_so[order.id or order._origin.id]
if not is_project_manager:
projects = projects._filter_access_rules('read')
@@ -88,56 +104,84 @@ class SaleOrder(models.Model):
def action_view_task(self):
self.ensure_one()
if not self.order_line:
return {'type': 'ir.actions.act_window_close'}
list_view_id = self.env.ref('project.view_task_tree2').id
form_view_id = self.env.ref('project.view_task_form2').id
kanban_view_id = self.env.ref('project.view_task_kanban_inherit_view_default_project').id
action = {'type': 'ir.actions.act_window_close'}
task_projects = self.tasks_ids.mapped('project_id')
if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...)
action = self.with_context(active_id=task_projects.id).env['ir.actions.actions']._for_xml_id(
'project.act_project_project_2_project_task_all')
action['domain'] = [('id', 'in', self.tasks_ids.ids)]
if action.get('context'):
eval_context = self.env['ir.actions.actions']._get_eval_context()
eval_context.update({'active_id': task_projects.id})
action_context = safe_eval(action['context'], eval_context)
action_context.update(eval_context)
action['context'] = action_context
else:
action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task")
action['context'] = {} # erase default context to avoid default filter
if len(self.tasks_ids) > 1: # cross project kanban task
action['views'] = [[False, 'kanban'], [list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'calendar'], [False, 'pivot']]
elif len(self.tasks_ids) == 1: # single task -> form view
action['views'] = [(form_view_id, 'form')]
action['res_id'] = self.tasks_ids.id
# filter on the task of the current SO
action.setdefault('context', {})
action['context'].update({'search_default_sale_order_id': self.id})
action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task")
if self.tasks_count > 1: # cross project kanban task
action['views'] = [[kanban_view_id, 'kanban'], [list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'calendar'], [False, 'pivot']]
else: # 1 or 0 tasks -> form view
action['views'] = [(form_view_id, 'form')]
action['res_id'] = self.tasks_ids.id
# set default project
default_line = next(sol for sol in self.order_line if sol.product_id.detailed_type == 'service')
default_project_id = default_line.project_id.id or self.project_id.id or self.project_ids[:1].id
action['context'] = {
'search_default_sale_order_id': self.id,
'default_sale_order_id': self.id,
'default_sale_line_id': default_line.id,
'default_partner_id': self.partner_id.id,
'default_project_id': default_project_id,
'default_user_ids': [self.env.uid],
}
return action
def action_create_project(self):
self.ensure_one()
if not self.order_line:
return {'type': 'ir.actions.act_window_close'}
sorted_line = self.order_line.sorted('sequence')
default_sale_line = next(sol for sol in sorted_line if sol.product_id.detailed_type == 'service')
return {
**self.env["ir.actions.actions"]._for_xml_id("project.open_create_project"),
'context': {
'default_sale_order_id': self.id,
'default_sale_line_id': default_sale_line.id,
'default_partner_id': self.partner_id.id,
'default_user_ids': [self.env.uid],
'default_allow_billable': 1,
'hide_allow_billable': True,
'default_company_id': self.company_id.id,
'generate_milestone': default_sale_line.product_id.service_policy == 'delivered_milestones',
},
}
def action_view_project_ids(self):
self.ensure_one()
view_form_id = self.env.ref('project.edit_project').id
view_kanban_id = self.env.ref('project.view_project_kanban').id
if not self.order_line:
return {'type': 'ir.actions.act_window_close'}
sorted_line = self.order_line.sorted('sequence')
default_sale_line = next(sol for sol in sorted_line if sol.product_id.detailed_type == 'service')
action = {
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.project_ids.ids)],
'view_mode': 'kanban,form',
'name': _('Projects'),
'domain': ['|', ('sale_order_id', '=', self.id), ('id', 'in', self.project_ids.ids)],
'res_model': 'project.project',
'views': [(False, 'kanban'), (False, 'tree'), (False, 'form')],
'view_mode': 'kanban,tree,form',
'context': {
**self._context,
'default_partner_id': self.partner_id.id,
'default_sale_line_id': default_sale_line.id,
'default_allow_billable': 1,
}
}
if len(self.project_ids) == 1:
action.update({'views': [(view_form_id, 'form')], 'res_id': self.project_ids.id})
else:
action['views'] = [(view_kanban_id, 'kanban'), (view_form_id, 'form')]
action.update({'views': [(False, 'form')], 'res_id': self.project_ids.id})
return action
def action_view_milestone(self):
self.ensure_one()
default_project = self.project_ids and self.project_ids[0]
default_sale_line = default_project.sale_line_id or self.order_line and self.order_line[0]
sorted_line = self.order_line.sorted('sequence')
default_sale_line = next(sol for sol in sorted_line if sol.is_service and sol.product_id.service_policy == 'delivered_milestones')
return {
'type': 'ir.actions.act_window',
'name': _('Milestones'),
@@ -154,8 +198,8 @@ class SaleOrder(models.Model):
"""),
'context': {
**self.env.context,
'default_project_id' : default_project.id,
'default_sale_line_id' : default_sale_line.id,
'default_project_id': default_project.id,
'default_sale_line_id': default_sale_line.id,
}
}
+38 -1
View File
@@ -118,7 +118,44 @@ class TestSaleProjectCommon(TestSaleCommon):
'taxes_id': False,
'property_account_income_id': cls.account_sale.id,
})
price_vals = {
'standard_price': 11,
'list_price': 13,
}
service_vals = {
'type': 'service',
'service_tracking': 'no',
'project_id': False,
}
(
cls.product_service_ordered_prepaid,
cls.product_service_delivered_milestone,
cls.product_service_delivered_manual,
cls.product_consumable,
) = cls.env['product.product'].create([{
'name': "Service prepaid",
**price_vals,
**service_vals,
'invoice_policy': 'order',
'service_type': 'manual',
}, {
'name': "Service milestone",
**price_vals,
**service_vals,
'invoice_policy': 'delivery',
'service_type': 'milestones',
}, {
'name': "Service manual",
**price_vals,
**service_vals,
'invoice_policy': 'delivery',
'service_type': 'manual',
}, {
'name': "Consumable",
**price_vals,
'type': 'consu',
'invoice_policy': 'order',
}])
# -- devliered_milestones (delivered, milestones)
product_milestone_vals = {
'type': 'service',
+83 -7
View File
@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, users
from odoo.tests.common import new_test_user
from .common import TestSaleProjectCommon
from odoo.tests.common import tagged
class TestSaleProject(TransactionCase):
@tagged('post_install', '-at_install')
class TestSaleProject(TestSaleProjectCommon):
@classmethod
def setUpClass(cls):
@@ -207,14 +209,12 @@ class TestSaleProject(TransactionCase):
self.product_order_service3.type = 'service'
self.assertTrue(sale_order_line.is_service, "As the product is a service, the SOL should be a service")
@users('demo')
def test_cancel_so_linked_to_project(self):
""" Test that cancelling a SO linked to a project will not raise an error """
# Ensure user don't have edit right access to the project
group_sale_manager = self.env.ref('sales_team.group_sale_manager')
group_project_user = self.env.ref('project.group_project_user')
self.env.user.write({'groups_id': [(6, 0, [group_sale_manager.id, group_project_user.id])]})
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner.id,
'partner_invoice_id': self.partner.id,
@@ -230,7 +230,83 @@ class TestSaleProject(TransactionCase):
sale_order.action_confirm()
self.assertEqual(self.project_global.tasks.sale_line_id.id, sale_order_line.id, "The project tasks should be linked to the SOL from the SO")
self.project_global.sale_line_id = sale_order_line
#use of sudo() since the env.user does not have the access right to edit projects.
self.project_global.sudo().sale_line_id = sale_order_line
sale_order.with_context({'disable_cancel_warning': True}).action_cancel()
self.assertFalse(self.project_global.sale_line_id, "The project should not be linked to the SOL anymore")
def test_compute_project_and_task_button(self):
"""
Ensures that when a sale order has at least one sol with a service product whose service-policy is either delivered on milestone or ordered prepaid, then the
show_create_project_button is set to True. If the sale order also has one project linked to it, then the show_project_and_task_button should be True, and the show_create_button
should be updated to False
"""
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner.id,
'partner_invoice_id': self.partner.id,
'partner_shipping_id': self.partner.id,
})
sale_order_2 = sale_order_1.copy()
sale_order_3 = sale_order_1.copy()
(sale_order_1 | sale_order_2 | sale_order_3).action_confirm()
# consumable product, manual service product
self.env['sale.order.line'].create([{
'product_id': self.product_consumable.id,
'order_id': sale_order_1.id,
}])
self.assertFalse(sale_order_1.show_create_project_button, "There is no service product with one of the correct service_policy on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden")
# add a milestone product
line_delivered_milestone = self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_milestone.id,
'order_id': sale_order_1.id,
})
# the user does not have project creation right
user_wrong_group = new_test_user(self.env, groups='sales_team.group_sale_manager,project.group_project_user', login='wendy', name='wendy')
sale_order_1.with_user(user_wrong_group)._compute_show_project_and_task_button()
self.assertFalse(sale_order_1.show_create_project_button, "The user does not have the right to create a new project, the button should be hidden")
self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden")
# the user has project creation right
sale_order_1._compute_show_project_and_task_button()
self.assertTrue(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on milestone' on the sale order, the button should be displayed")
self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden")
# add a project to the SO
line_delivered_milestone.project_id = self.project_global
sale_order_1._compute_show_project_and_task_button()
self.assertFalse(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be hidden")
self.assertTrue(sale_order_1.show_project_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be displayed")
self.assertTrue(sale_order_1.show_task_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be displayed")
# add an ordered_prepaid service product
line_prepaid = self.env['sale.order.line'].create({
'product_id': self.product_service_ordered_prepaid.id,
'order_id': sale_order_2.id,
})
sale_order_2._compute_show_project_and_task_button()
self.assertTrue(sale_order_2.show_create_project_button, "There is a product service with the service_policy set on 'ordered_prepaid' on the sale order, the button should be displayed")
self.assertFalse(sale_order_2.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_2.show_task_button, "There is no project on the sale order, the button should be hidden")
# create a new task, whose sale order item is a sol of the SO
self.env['project.task'].create({
'name': 'Test Task',
'project_id': self.project_global.id,
'sale_line_id': line_prepaid.id,
})
sale_order_2._compute_tasks_ids()
sale_order_2._compute_show_project_and_task_button()
self.assertTrue(sale_order_2.show_create_project_button, "There is a product service with the service_policy set on 'ordered_prepaid' on the sale order, the button should be displayed")
self.assertFalse(sale_order_2.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertTrue(sale_order_2.show_task_button, "There is no project on the sale order and there is a task whose sale item is one of the sale_line of the SO, the button should be displayed")
# add a manual service product
self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_manual.id,
'order_id': sale_order_3.id,
})
sale_order_3._compute_show_project_and_task_button()
self.assertTrue(sale_order_3.show_create_project_button, "There is a product service with the service_policy set on 'manual' on the sale order, the button should be displayed")
self.assertFalse(sale_order_3.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_3.show_task_button, "There is no project on the sale order, the button should be hidden")
+11 -2
View File
@@ -7,16 +7,25 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="priority">10</field>
<field name="arch" type="xml">
<button id="create_invoice_percentage" position="after">
<button id="create_project" name="action_create_project" string="Create Project"
type="object" data-hotkey="q"
attrs="{'invisible': [('show_create_project_button', '=', False)]}"
groups="project.group_project_manager"/>
</button>
<xpath expr="//button[@name='action_view_invoice']" position="before">
<field name="show_task_button" invisible="1"/>
<field name="show_project_button" invisible="1"/>
<field name="show_create_project_button" invisible="1"/>
<field name="project_ids" invisible="1"/>
<field name="is_product_milestone" invisible="1"/>
<button type="object" name="action_view_project_ids" class="oe_stat_button" icon="fa-puzzle-piece" attrs="{'invisible': ['|', ('state', 'in', ['draft', 'sent']), ('project_ids', '=', [])]}" groups="project.group_project_user">
<button type="object" name="action_view_project_ids" class="oe_stat_button" icon="fa-puzzle-piece" attrs="{'invisible': [('show_project_button', '=', False)]}" groups="project.group_project_user">
<field name="project_count" widget="statinfo" string="Projects"/>
</button>
<button class="oe_stat_button" name="action_view_milestone" type="object" icon="fa-check-square-o" attrs="{'invisible': ['|', ('is_product_milestone', '=', False), ('project_ids', '=', [])]}" groups="project.group_project_milestone">
<field name="milestone_count" widget="statinfo" string="Milestones"/>
</button>
<button type="object" name="action_view_task" class="oe_stat_button" icon="fa-tasks" attrs="{'invisible': [('tasks_count', '=', 0)]}" groups="project.group_project_user">
<button type="object" name="action_view_task" class="oe_stat_button" icon="fa-tasks" attrs="{'invisible': [('show_task_button', '=', False)]}" groups="project.group_project_user">
<field name="tasks_count" widget="statinfo" string="Tasks"/>
</button>
</xpath>
+46 -13
View File
@@ -20,6 +20,8 @@ class SaleOrder(models.Model):
timesheet_total_duration = fields.Integer("Timesheet Total Duration", compute='_compute_timesheet_total_duration',
help="Total recorded duration, expressed in the encoding UoM, and rounded to the unit", compute_sudo=True,
groups="hr_timesheet.group_hr_timesheet_user")
show_hours_recorded_button = fields.Boolean(compute="_compute_show_hours_recorded_button", groups="hr_timesheet.group_hr_timesheet_user")
def _compute_timesheet_count(self):
timesheets_per_so = {
@@ -63,6 +65,21 @@ class SaleOrder(models.Model):
# We want to display only one time the warning for each SOL
upsellable_lines.write({'has_displayed_warning_upsell': True})
def _compute_show_hours_recorded_button(self):
show_button_ids = self._get_order_with_valid_service_product()
for order in self:
order.show_hours_recorded_button = order.timesheet_count or order.project_count and order.id in show_button_ids
def _get_order_with_valid_service_product(self):
return self.env['sale.order.line']._read_group([
('order_id', 'in', self.ids),
('state', 'in', ['sale', 'done']),
('is_service', '=', True),
'|',
('product_id.service_type', 'not in', ['milestones', 'manual']),
('product_id.invoice_policy', '!=', 'delivery'),
], aggregates=['order_id:array_agg'])[0][0]
def _get_prepaid_service_lines_to_upsell(self):
""" Retrieve all sols which need to display an upsell activity warning in the SO
@@ -85,22 +102,38 @@ class SaleOrder(models.Model):
def action_view_timesheet(self):
self.ensure_one()
if not self.order_line:
return {'type': 'ir.actions.act_window_close'}
action = self.env["ir.actions.actions"]._for_xml_id("sale_timesheet.timesheet_action_from_sales_order")
action['context'] = {
'search_default_billable_timesheet': True
default_sale_line = next(sale_line for sale_line in self.order_line if sale_line.is_service and sale_line.product_id.service_policy in ['ordered_prepaid', 'delivered_timesheet'])
context = {
'search_default_billable_timesheet': True,
'default_is_so_line_edited': True,
'default_so_line': default_sale_line.id,
} # erase default filters
if self.order_line:
tasks = self.order_line.task_id._filter_access_rules_python('write')
if tasks:
action['context']['default_task_id'] = tasks[0].id
else:
projects = self.order_line.project_id._filter_access_rules_python('write')
if projects:
action['context']['default_project_id'] = projects[0].id
if self.timesheet_count > 0:
action['domain'] = [('so_line', 'in', self.order_line.ids)]
tasks = self.order_line.task_id._filter_access_rules_python('write')
if tasks:
context['default_task_id'] = tasks[0].id
else:
action = {'type': 'ir.actions.act_window_close'}
projects = self.order_line.project_id._filter_access_rules_python('write')
if projects:
context['default_project_id'] = projects[0].id
elif self.project_ids:
context['default_project_id'] = self.project_ids[0].id
action.update({
'context': context,
'domain': [('so_line', 'in', self.order_line.ids)],
'help': _("""
<p class="o_view_nocontent_smiling_face">
No activities found. Let's start a new one!
</p><p>
Track your working hours by projects every day and invoice this time to your customers.
</p>
""")
})
return action
def _create_invoices(self, grouped=False, final=False, date=None):
+10
View File
@@ -254,6 +254,16 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
'taxes_id': False,
'property_account_income_id': cls.account_sale.id,
})
cls.product_service_delivered_timesheet = cls.env['product.product'].create({
'name': "Service timesheet",
'standard_price': 11,
'list_price': 13,
'type': 'service',
'service_tracking': 'no',
'project_id': False,
'invoice_policy': 'delivery',
'service_type': 'timesheet',
})
def setUp(self):
super().setUp()
@@ -784,3 +784,92 @@ class TestSaleService(TestCommonSaleTimesheet):
self.sale_order.action_confirm()
project_allocated_hours = self.sale_order.order_line[0].project_id.allocated_hours
self.assertEqual(30, project_allocated_hours, 'current set the project allocated hours.')
def test_compute_project_and_task_button_with_ts(self):
""" This test ensures that the button are correctly computed when there is a timesheet service product on a SO. The behavior was not modified in sale_timesheet, but since
the timesheet product case can not be tested in sale_project, we have to add the test here."""
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create([{
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}])
# delivered timesheet
line_1 = self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_timesheet.id,
'order_id': sale_order_1.id,
})
sale_order_1.action_confirm()
self.assertTrue(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' on the sale order, the button should be displayed")
self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden")
line_1.project_id = self.project_global.id
sale_order_1._compute_show_project_and_task_button()
self.assertFalse(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be hidden")
self.assertTrue(sale_order_1.show_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
self.assertTrue(sale_order_1.show_task_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
def test_compute_show_timesheet_button(self):
""" This test ensures that the hours recorded button is correctly computed. If there is a service product with an invoice policy of prepaid or timesheet, and there is
at least on project linked to the SO, then the button should be displayed """
sale_order_1, sale_order_2 = self.env['sale.order'].with_context(tracking_disable=True).create([{
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}, {
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}])
# consumable product, delivered milestone, and delivered manual
self.env['sale.order.line'].create([{
'product_id': self.product_consumable.id,
'order_id': sale_order_1.id,
}, {
'product_id': self.product_service_delivered_milestone.id,
'order_id': sale_order_1.id,
}, {
'product_id': self.product_service_delivered_manual.id,
'order_id': sale_order_1.id,
}])
sale_order_1.action_confirm()
self.assertFalse(sale_order_1.show_hours_recorded_button, "There is no service product service with the correct service_policy set on 'delivered on timesheet' on the sale order, the button should be hidden")
# adds a delivered timesheet product to the SO
line_4 = self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_timesheet.id,
'order_id': sale_order_1.id,
})
self.assertFalse(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
line_4.project_id = self.project_global
sale_order_1._compute_show_hours_recorded_button()
self.assertTrue(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
line_1 = self.env['sale.order.line'].create({
'product_id': self.product_service_ordered_prepaid.id,
'order_id': sale_order_2.id,
})
sale_order_2.action_confirm()
self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
line_1.project_id = self.project_global
sale_order_2._compute_show_hours_recorded_button()
self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
# remove the project from the so and ensure the SO is back to its previous state
line_1.project_id = False
sale_order_2._compute_show_hours_recorded_button()
self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
# adds a task whose sale item is a sale order line from the SO, and adds a timesheet in that task. This should enable the display of the button
task = self.env['project.task'].create({
'name': 'Test Task',
'project_id': self.project_global.id,
'sale_line_id': line_1.id,
})
self.env['account.analytic.line'].create({
'name': "timesheet",
'unit_amount': 5,
'project_id': task.project_id.id,
'task_id': task.id,
'employee_id': self.employee_user.id,
})
sale_order_2._compute_timesheet_count()
sale_order_2._compute_show_hours_recorded_button()
self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
@@ -38,7 +38,7 @@
<field name="arch" type="xml">
<xpath expr="//setting" position="before">
<field name="company_id" invisible="1"/>
<setting help="Invoice your time and material to customers">
<setting help="Invoice your time and material to customers" invisible="context.get('hide_allow_billable', False)">
<field name="allow_billable"/>
<div attrs="{'invisible': [('allow_billable', '=', False)]}">
<label for="partner_id"/>
@@ -8,11 +8,12 @@
<data>
<xpath expr="//button[@name='action_view_task']" position="after">
<field name="timesheet_count" invisible="1"/>
<field name="show_hours_recorded_button" invisible="1"/>
<button type="object"
name="action_view_timesheet"
class="oe_stat_button"
icon="fa-clock-o"
attrs="{'invisible': [('timesheet_count', '=', 0)]}"
attrs="{'invisible': [('show_hours_recorded_button', '=', False)]}"
groups="hr_timesheet.group_hr_timesheet_user">
<div class="o_stat_info">
<span class="o_stat_value">