[IMP] sale{_timesheet}: order upsell activity for prepaid services
When offering services, it is critical to make sure that the time being
delivered is billable. Otherwise, the company is loosing money.
This commit adds two fields in product.template:
1. service_upsell_warning
2. service_upsell_threshold
The first one is a boolean, if it is checked than the second one is
displayed to add a threshold. This threshold is used to create a upsell
activity want the (qty_delivered / qty_ordered) in a SOL of this product
is greater than the threshold defined in the product.
Moreover, when the following condition for a SOL:
(delivered quantity / ordered quantity) >= threshold set in the product
is True, then we display an upsell warning for the corresponding
SO. The problem is we don't check if the warning has already displayed
by a certain SOL. Thus, each time we timesheet for this SOL, we display
one more time.
This commit avoids to spam the salesman, to do this, we add a new
boolean field in sale.order.line, this field will be True if the warning
upsell activity is shown thanks to the SOL.
A method is added in sale module to create the upsell activity for each SO.
This method is used in sale_timesheet module to avoid duplicated code.
Finally, an unit test has been added to check the feature.
task-2411291
closes odoo/odoo#64252
Related: odoo/upgrade#2066
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
+14
-10
@@ -505,18 +505,11 @@ class SaleOrder(models.Model):
|
||||
if field.name != 'invoice_status' or self.env.context.get('mail_activity_automation_skip'):
|
||||
return
|
||||
|
||||
filtered_self = self.filtered(lambda so: so.user_id and so.invoice_status == 'upselling')
|
||||
if not filtered_self:
|
||||
upselling_orders = self.filtered(lambda so: (so.user_id or so.partner_id.user_id) and so.invoice_status == 'upselling')
|
||||
if not upselling_orders:
|
||||
return
|
||||
|
||||
filtered_self.activity_unlink(['sale.mail_act_sale_upsell'])
|
||||
for order in filtered_self:
|
||||
order.activity_schedule(
|
||||
'sale.mail_act_sale_upsell',
|
||||
user_id=order.user_id.id,
|
||||
note=_("Upsell <a href='#' data-oe-model='%s' data-oe-id='%d'>%s</a> for customer <a href='#' data-oe-model='%s' data-oe-id='%s'>%s</a>") % (
|
||||
order._name, order.id, order.name,
|
||||
order.partner_id._name, order.partner_id.id, order.partner_id.display_name))
|
||||
upselling_orders._create_upsell_activity()
|
||||
|
||||
def copy_data(self, default=None):
|
||||
if default is None:
|
||||
@@ -549,6 +542,17 @@ class SaleOrder(models.Model):
|
||||
return self._search(domain, limit=limit, access_rights_uid=name_get_uid)
|
||||
return super(SaleOrder, self)._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
||||
|
||||
def _create_upsell_activity(self):
|
||||
self and self.activity_unlink(['sale.mail_act_sale_upsell'])
|
||||
for order in self:
|
||||
ref = "<a href='#' data-oe-model='%s' data-oe-id='%d'>%s</a>"
|
||||
order_ref = ref % (order._name, order.id, order.name)
|
||||
customer_ref = ref % (order.partner_id._name, order.partner_id.id, order.partner_id.display_name)
|
||||
order.activity_schedule(
|
||||
'sale.mail_act_sale_upsell',
|
||||
user_id=order.user_id.id or order.partner_id.user_id.id,
|
||||
note=_("Upsell %(order)s for customer %(customer)s", order=order_ref, customer=customer_ref))
|
||||
|
||||
def _prepare_invoice(self):
|
||||
"""
|
||||
Prepare the dict of values to create the new invoice for a sales order. This method may be
|
||||
|
||||
@@ -21,6 +21,8 @@ class ProductTemplate(models.Model):
|
||||
# override domain
|
||||
project_id = fields.Many2one(domain="[('allow_billable', '=', True), ('pricing_type', '=', 'task_rate'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]")
|
||||
project_template_id = fields.Many2one(domain="[('allow_billable', '=', True), ('pricing_type', 'in', ('fixed_rate', 'employee_rate')), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]")
|
||||
service_upsell_warning = fields.Boolean('Upsell Warning', help="The salesperson in charge will be assigned an activity informing him of an upselling opportunity once the selected threshold is reached.")
|
||||
service_upsell_threshold = fields.Float('Threshold', help="Percentage of time delivered compared to the prepaid amount that must be reached for the upselling opportunity activity to be triggered.")
|
||||
|
||||
def _default_visible_expense_policy(self):
|
||||
visibility = self.user_has_groups('project.group_project_user')
|
||||
|
||||
@@ -5,6 +5,7 @@ import math
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
@@ -42,6 +43,46 @@ class SaleOrder(models.Model):
|
||||
total_time *= sale_order.timesheet_encode_uom_id.factor
|
||||
sale_order.timesheet_total_duration = round(total_time)
|
||||
|
||||
def _compute_field_value(self, field):
|
||||
super()._compute_field_value(field)
|
||||
if field.name != 'invoice_status' or self.env.context.get('mail_activity_automation_skip'):
|
||||
return
|
||||
|
||||
# Get SOs which their state is not equal to upselling or invoied and if at least a SOL has warning prepaid service upsell set to True and the warning has not already been displayed
|
||||
upsellable_orders = self.filtered(lambda so:
|
||||
so.state == 'sale'
|
||||
and so.invoice_status not in ('upselling', 'invoiced')
|
||||
and (so.user_id or so.partner_id.user_id) # salesperson needed to assign upsell activity
|
||||
)
|
||||
for order in upsellable_orders:
|
||||
upsellable_lines = order._get_prepaid_service_lines_to_upsell()
|
||||
if upsellable_lines:
|
||||
order._create_upsell_activity()
|
||||
# We want to display only one time the warning for each SOL
|
||||
upsellable_lines.write({'has_displayed_warning_upsell': True})
|
||||
|
||||
def _get_prepaid_service_lines_to_upsell(self):
|
||||
""" Retrieve all sols which need to display an upsell activity warning in the SO
|
||||
|
||||
These SOLs should contain a product which has:
|
||||
- type="service",
|
||||
- service_policy="ordered_timesheet",
|
||||
- service_upsell_warning=True.
|
||||
"""
|
||||
self.ensure_one()
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
return self.order_line.filtered(lambda sol:
|
||||
sol.is_service
|
||||
and not sol.has_displayed_warning_upsell # we don't want to display many times the warning each time we timesheet on the SOL
|
||||
and sol.product_id.service_policy == 'ordered_timesheet'
|
||||
and sol.product_id.service_upsell_warning
|
||||
and float_compare(
|
||||
sol.qty_delivered,
|
||||
sol.product_uom_qty * (sol.product_id.service_upsell_threshold or 1.0),
|
||||
precision_digits=precision
|
||||
) >= 0
|
||||
)
|
||||
|
||||
def action_view_project_ids(self):
|
||||
self.ensure_one()
|
||||
# redirect to form or kanban view
|
||||
@@ -81,6 +122,7 @@ class SaleOrderLine(models.Model):
|
||||
analytic_line_ids = fields.One2many(domain=[('project_id', '=', False)]) # only analytic lines, not timesheets (since this field determine if SO line came from expense)
|
||||
remaining_hours_available = fields.Boolean(compute='_compute_remaining_hours_available', compute_sudo=True)
|
||||
remaining_hours = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours', compute_sudo=True, store=True)
|
||||
has_displayed_warning_upsell = fields.Boolean('Has Displayed Warning Upsell')
|
||||
|
||||
def name_get(self):
|
||||
res = super(SaleOrderLine, self).name_get()
|
||||
|
||||
@@ -9,3 +9,4 @@ from . import test_reinvoice
|
||||
from . import test_reporting
|
||||
from . import test_project_overview
|
||||
from . import test_project_billing_multicompany
|
||||
from . import test_upsell_warning
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestUpsellWarning(TestCommonSaleTimesheet):
|
||||
def test_display_upsell_warning(self):
|
||||
""" Test to display an upsell warning
|
||||
|
||||
|
||||
We display an upsell warning in SO when this following condition is satisfy in its SOL:
|
||||
(qty_delivered / product_uom_qty) >= product_id.service_upsell_threshold
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Configure the upsell warning in prepaid service product
|
||||
2) Create SO with a SOL containing this updated product,
|
||||
3) Create Project and Task,
|
||||
4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning,
|
||||
5) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
"""
|
||||
# 1) Configure the upsell warning in prepaid service product
|
||||
self.product_order_timesheet1.write({
|
||||
'service_upsell_warning': True,
|
||||
'service_upsell_threshold': 0.6,
|
||||
})
|
||||
|
||||
# 2) Create SO with a SOL containing this updated product
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
})
|
||||
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'name': self.product_order_timesheet1.name,
|
||||
'product_id': self.product_order_timesheet1.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': self.product_order_timesheet1.list_price,
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
# 3) Create Project and Task
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_a.id,
|
||||
'analytic_account_id': self.analytic_account_sale.id,
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task Test',
|
||||
'project_id': project.id,
|
||||
})
|
||||
task._compute_sale_line()
|
||||
|
||||
# 4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': project.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
timesheet._compute_so_line()
|
||||
so.order_line._compute_qty_delivered()
|
||||
so.order_line._compute_invoice_status()
|
||||
so._get_invoice_status()
|
||||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
self.assertEqual(len(so.activity_search(['sale.mail_act_sale_upsell'])), 0, 'No upsell warning should appear in the SO.')
|
||||
timesheet.write({
|
||||
'unit_amount': 6,
|
||||
})
|
||||
timesheet._compute_so_line()
|
||||
so.order_line._compute_qty_delivered()
|
||||
so.order_line._compute_invoice_status()
|
||||
so._get_invoice_status()
|
||||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
# 5) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
self.assertEqual(len(so.activity_search(['sale.mail_act_sale_upsell'])), 1, 'A upsell warning should appear in the SO.')
|
||||
@@ -11,6 +11,8 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='service_type']" position="after">
|
||||
<field name="service_policy" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/>
|
||||
<field name="service_upsell_warning" attrs="{'invisible': ['|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet')]}" />
|
||||
<field name="service_upsell_threshold" attrs="{'invisible': ['|', '|', ('type', '!=', 'service'), ('service_policy', '!=', 'ordered_timesheet'), ('service_upsell_warning', '=', False)]}" widget="percentage"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user