[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:
committed by
Xavier Bol (xbo)
parent
57168aa648
commit
76a6a0d2ec
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user