[IMP] sale_{project,timesheet}: merge bill_type and pricing_type
Before this commit, the user need to select in these both fields to
configure the pricing type of a project. Three choices are possible :
1. Task Rate
2. Project Rate
3. Employee Rate
This commit merges the both fields to have single field called
pricing_type and the user selects the one of three options cited above.
The goal of this changes is to facilitate the configuration of a
project.
task-2424382
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='service_type']" position="after">
|
||||
<field name="service_tracking" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/>
|
||||
<field name="project_id" context="{'default_allow_billable': True, 'default_bill_type': 'customer_task'}" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/>
|
||||
<field name="project_template_id" context="{'active_test': False, 'default_allow_billable': True, 'default_bill_type': 'customer_project', 'default_pricing_type': 'fixed_rate'}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/>
|
||||
<field name="project_id" context="{'default_allow_billable': True, 'default_pricing_type': 'task_rate'}" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/>
|
||||
<field name="project_template_id" context="{'active_test': False, 'default_allow_billable': True, 'default_pricing_type': 'fixed_rate'}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -23,8 +23,8 @@ class AccountAnalyticLine(models.Model):
|
||||
so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False)
|
||||
is_so_line_edited = fields.Boolean("Is Sales Order Item Manually Edited")
|
||||
|
||||
# TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to bill_type and pricing_type of project (See in master)
|
||||
@api.depends('so_line.product_id', 'project_id', 'task_id', 'task_id.bill_type', 'task_id.pricing_type')
|
||||
# TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to pricing_type of project (See in master)
|
||||
@api.depends('so_line.product_id', 'project_id', 'task_id', 'task_id.pricing_type')
|
||||
def _compute_timesheet_invoice_type(self):
|
||||
for timesheet in self:
|
||||
if timesheet.project_id: # AAL will be set to False
|
||||
@@ -81,18 +81,16 @@ class AccountAnalyticLine(models.Model):
|
||||
on the one on the project
|
||||
"""
|
||||
if not task:
|
||||
if project.bill_type == 'customer_project' and project.pricing_type == 'employee_rate':
|
||||
if project.pricing_type == 'employee_rate':
|
||||
map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', project.id), ('employee_id', '=', employee.id)])
|
||||
if map_entry:
|
||||
return map_entry.sale_line_id
|
||||
if project.sale_line_id:
|
||||
return project.sale_line_id
|
||||
if task.allow_billable:
|
||||
if task.bill_type == 'customer_task':
|
||||
if task.pricing_type in ('task_rate', 'fixed_rate'):
|
||||
return task.sale_line_id
|
||||
if task.pricing_type == 'fixed_rate':
|
||||
return task.sale_line_id
|
||||
elif task.pricing_type == 'employee_rate':
|
||||
else: # then pricing_type = 'employee_rate'
|
||||
map_entry = project.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == employee)
|
||||
if map_entry:
|
||||
return map_entry.sale_line_id
|
||||
|
||||
@@ -19,8 +19,8 @@ class ProductTemplate(models.Model):
|
||||
('timesheet', 'Timesheets on project (one fare per SO/Project)'),
|
||||
], ondelete={'timesheet': 'set default'})
|
||||
# override domain
|
||||
project_id = fields.Many2one(domain="[('allow_billable', '=', True), ('bill_type', '=', 'customer_task'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]")
|
||||
project_template_id = fields.Many2one(domain="[('allow_billable', '=', True), ('bill_type', '=', 'customer_project'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]")
|
||||
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])]")
|
||||
|
||||
def _default_visible_expense_policy(self):
|
||||
visibility = self.user_has_groups('project.group_project_user')
|
||||
|
||||
@@ -24,16 +24,12 @@ class Project(models.Model):
|
||||
def _default_timesheet_product_id(self):
|
||||
return self.env.ref('sale_timesheet.time_product', False)
|
||||
|
||||
bill_type = fields.Selection([
|
||||
('customer_task', 'Different customers'),
|
||||
('customer_project', 'A unique customer')
|
||||
], string="Invoice Tasks to", default="customer_task",
|
||||
help='When billing tasks individually, a Sales Order will be created from each task. It is perfect if you would like to bill different services to different customers at different rates. \n When billing the whole project, a Sales Order will be created from the project instead. This option is better if you would like to bill all the tasks of a given project to a specific customer either at a fixed rate, or at an employee rate.')
|
||||
pricing_type = fields.Selection([
|
||||
('task_rate', 'Task rate'),
|
||||
('fixed_rate', 'Project rate'),
|
||||
('employee_rate', 'Employee rate')
|
||||
], string="Pricing", default="fixed_rate",
|
||||
help='The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the employee who performed it. The employee rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.')
|
||||
], string="Pricing", default="task_rate",
|
||||
help='The task rate is perfect if you would like to bill different services to different customers at different rates. The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the employee who performed it. The employee rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.')
|
||||
sale_line_employee_ids = fields.One2many('project.sale.line.employee.map', 'project_id', "Sale line/Employee map", copy=False,
|
||||
help="Employee/Sale Order Item Mapping:\n Defines to which sales order item an employee's timesheet entry will be linked."
|
||||
"By extension, it defines the rate at which an employee's time on the project is billed.")
|
||||
@@ -63,11 +59,11 @@ class Project(models.Model):
|
||||
|
||||
]
|
||||
|
||||
@api.depends('allow_billable', 'sale_order_id', 'partner_id', 'bill_type')
|
||||
@api.depends('allow_billable', 'sale_order_id', 'partner_id', 'pricing_type')
|
||||
def _compute_display_create_order(self):
|
||||
for project in self:
|
||||
show = True
|
||||
if not project.partner_id or project.bill_type != 'customer_project' or not project.allow_billable or project.sale_order_id:
|
||||
if not project.partner_id or project.pricing_type == 'task_rate' or not project.allow_billable or project.sale_order_id:
|
||||
show = False
|
||||
project.display_create_order = show
|
||||
|
||||
@@ -80,9 +76,9 @@ class Project(models.Model):
|
||||
elif not project.timesheet_product_id:
|
||||
project.timesheet_product_id = default_product
|
||||
|
||||
@api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id', 'bill_type')
|
||||
@api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id')
|
||||
def _compute_warning_employee_rate(self):
|
||||
projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.bill_type == 'customer_project' and p.pricing_type == 'employee_rate')
|
||||
projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.pricing_type == 'employee_rate')
|
||||
employees = self.env['account.analytic.line'].read_group([('task_id', 'in', projects.task_ids.ids)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
|
||||
dict_project_employee = defaultdict(list)
|
||||
for line in employees:
|
||||
@@ -208,7 +204,7 @@ class ProjectTask(models.Model):
|
||||
|
||||
if not result.get('timesheet_product_id', False) and 'project_id' in result:
|
||||
project = self.env['project.project'].browse(result['project_id'])
|
||||
if project.bill_type != 'customer_project' or project.pricing_type != 'employee_rate':
|
||||
if project.pricing_type != 'employee_rate':
|
||||
result['timesheet_product_id'] = project.timesheet_product_id.id
|
||||
return result
|
||||
|
||||
@@ -216,7 +212,6 @@ class ProjectTask(models.Model):
|
||||
sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False,
|
||||
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id')
|
||||
bill_type = fields.Selection(related="project_id.bill_type")
|
||||
pricing_type = fields.Selection(related="project_id.pricing_type")
|
||||
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
|
||||
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
|
||||
@@ -290,7 +285,7 @@ class ProjectTask(models.Model):
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project(self):
|
||||
if self.project_id and self.project_id.bill_type == 'customer_project':
|
||||
if self.project_id and self.project_id.pricing_type != 'task_rate':
|
||||
if not self.partner_id:
|
||||
self.partner_id = self.project_id.partner_id
|
||||
if not self.sale_line_id:
|
||||
@@ -301,7 +296,7 @@ class ProjectTask(models.Model):
|
||||
# Done after super to avoid constraints on field recomputation
|
||||
if values.get('project_id'):
|
||||
project_dest = self.env['project.project'].browse(values['project_id'])
|
||||
if project_dest.bill_type == 'customer_project' and project_dest.pricing_type == 'employee_rate':
|
||||
if project_dest.pricing_type == 'employee_rate':
|
||||
self.write({'sale_line_id': False})
|
||||
return res
|
||||
|
||||
@@ -311,7 +306,7 @@ class ProjectTask(models.Model):
|
||||
if not self.commercial_partner_id or not self.allow_billable:
|
||||
return False
|
||||
domain = [('is_service', '=', True), ('order_partner_id', 'child_of', self.commercial_partner_id.id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('remaining_hours', '>', 0)]
|
||||
if self.project_id.bill_type == 'customer_project' and self.project_sale_order_id:
|
||||
if self.project_id.pricing_type != 'task_rate' and self.project_sale_order_id:
|
||||
domain.append(('order_id', '=?', self.project_sale_order_id.id))
|
||||
return self.env['sale.order.line'].search(domain, limit=1)
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ class Project(models.Model):
|
||||
task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids]
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman'):
|
||||
if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_id:
|
||||
if self.pricing_type != 'task_rate' and self.allow_billable and not self.sale_order_id:
|
||||
actions.append({
|
||||
'label': _("Create a Sales Order"),
|
||||
'type': 'action',
|
||||
|
||||
@@ -13,7 +13,7 @@ class SaleOrder(models.Model):
|
||||
timesheet_count = fields.Float(string='Timesheet activities', compute='_compute_timesheet_ids', groups="hr_timesheet.group_hr_timesheet_user")
|
||||
|
||||
# override domain
|
||||
project_id = fields.Many2one(domain="['|', ('bill_type', '=', 'customer_task'), ('pricing_type', '=', 'fixed_rate'), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]")
|
||||
project_id = fields.Many2one(domain="[('pricing_type', 'in', ('fixed_rate', 'task_rate')), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]")
|
||||
timesheet_encode_uom_id = fields.Many2one('uom.uom', related='company_id.timesheet_encode_uom_id')
|
||||
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")
|
||||
|
||||
@@ -147,7 +147,7 @@ class SaleOrderLine(models.Model):
|
||||
if not line.is_expense and line.product_id.type == 'service' and line.product_id.service_type == 'timesheet':
|
||||
line.qty_delivered_method = 'timesheet'
|
||||
|
||||
@api.depends('analytic_line_ids.project_id', 'project_id.pricing_type', 'project_id.bill_type')
|
||||
@api.depends('analytic_line_ids.project_id', 'project_id.pricing_type')
|
||||
def _compute_qty_delivered(self):
|
||||
super(SaleOrderLine, self)._compute_qty_delivered()
|
||||
|
||||
@@ -182,7 +182,6 @@ class SaleOrderLine(models.Model):
|
||||
"""Generate project values"""
|
||||
values = super()._timesheet_create_project_prepare_values()
|
||||
values['allow_billable'] = True
|
||||
values['bill_type'] = 'customer_project'
|
||||
values['pricing_type'] = 'fixed_rate'
|
||||
return values
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
|
||||
# set up
|
||||
cls.employee_tde = cls.env['hr.employee'].create({
|
||||
'name': 'Employee TDE',
|
||||
@@ -97,7 +97,6 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
'name': "Project billed at Employee Rate",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'bill_type': 'customer_project',
|
||||
'pricing_type': 'employee_rate',
|
||||
'sale_order_id': cls.sale_order_1.id,
|
||||
'partner_id': cls.sale_order_1.partner_id.id,
|
||||
@@ -146,7 +145,6 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
# Change project to billable at task rate
|
||||
self.project_non_billable.write({
|
||||
'allow_billable': True,
|
||||
'bill_type': 'customer_project',
|
||||
'pricing_type': 'fixed_rate',
|
||||
})
|
||||
task.timesheet_product_id = self.product_delivery_timesheet3
|
||||
@@ -202,7 +200,6 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
# Change project to billable at employee rate
|
||||
self.project_non_billable.write({
|
||||
'allow_billable': True,
|
||||
'bill_type': 'customer_project',
|
||||
'pricing_type': 'employee_rate',
|
||||
})
|
||||
|
||||
@@ -260,7 +257,6 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
})
|
||||
|
||||
self.assertTrue(task.allow_billable, "Task in project 'employee rate' should be billable")
|
||||
self.assertEqual(task.bill_type, 'customer_project', "Task in project 'employee rate' should be billed at employee rate")
|
||||
self.assertEqual(task.pricing_type, 'employee_rate', "Task in project 'employee rate' should be billed at employee rate")
|
||||
self.assertFalse(task.sale_line_id, "Task created in a project billed on 'employee rate' should not be linked to a SOL")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project")
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestProjectBillingMulticompany(TestCommonSaleTimesheet):
|
||||
'name': "Non Billable Project",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'bill_type': 'customer_project',
|
||||
'pricing_type': 'fixed_rate',
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button string="Project Overview" class="oe_stat_button" type="object" name="action_view_timesheet" icon="fa-puzzle-piece" attrs="{'invisible': [('allow_billable', '=', False)]}"/>
|
||||
<button class="d-none d-md-inline oe_stat_button"
|
||||
type="object" name="action_view_so" icon="fa-dollar"
|
||||
attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project')]}"
|
||||
attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('pricing_type', '=', 'task_rate')]}"
|
||||
string="Sales Order"
|
||||
groups="sales_team.group_sale_salesman"/>
|
||||
</div>
|
||||
@@ -22,14 +22,13 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="display_create_order" invisible="1"/>
|
||||
<field name="bill_type" widget="radio"/>
|
||||
<field name="pricing_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
|
||||
<field name="timesheet_product_id" string="Service" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), ('bill_type', '!=', 'customer_task')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
|
||||
<field name="sale_order_id" attrs="{'invisible': [('bill_type', '!=', 'customer_project')], 'readonly': [('sale_order_id', '!=', False)]}" force_save="1" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': [('bill_type', '!=', 'customer_project')]}" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="pricing_type" attrs="{'invisible': [('allow_billable', '=', False)], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
|
||||
<field name="timesheet_product_id" string="Service" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), ('pricing_type', '!=', 'task_rate')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
|
||||
<field name="sale_order_id" attrs="{'invisible': [('pricing_type', '=', 'task_rate')], 'readonly': [('sale_order_id', '!=', False)]}" force_save="1" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
<field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': [('pricing_type', '=', 'task_rate')]}" options="{'no_create': True, 'no_edit': True, 'delete': False, 'no_open': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="sale_line_employee_ids" attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}">
|
||||
<field name="sale_line_employee_ids" attrs="{'invisible': [('pricing_type', '!=', 'employee_rate')]}">
|
||||
<tree editable="top">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
@@ -142,7 +141,6 @@
|
||||
<field name="has_multi_sol" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_phone']" position="after">
|
||||
<field name="bill_type" invisible="1"/>
|
||||
<field name="pricing_type" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']" position="attributes">
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
result['project_id'] = active_id
|
||||
if not result.get('partner_id', False):
|
||||
result['partner_id'] = project.partner_id.id
|
||||
if project.bill_type == 'customer_project' and not result.get('line_ids', False):
|
||||
if project.pricing_type != 'task_rate' and not result.get('line_ids', False):
|
||||
if project.pricing_type == 'employee_rate':
|
||||
default_product = self.env.ref('sale_timesheet.time_product', False)
|
||||
result['line_ids'] = [
|
||||
|
||||
Reference in New Issue
Block a user