[FIX] sale_timesheet: Billable project

taskID 2323997

closes odoo/odoo#56798

Related: odoo/enterprise#12793
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
jbm-odoo
2020-08-31 12:02:56 +00:00
parent 8d1918d82c
commit fd6644e4cb
10 changed files with 118 additions and 36 deletions
+1 -1
View File
@@ -666,8 +666,8 @@
<group>
<field name="active" invisible="1"/>
<field name="partner_id" class="o_task_customer_field"/>
<field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="partner_email" widget="email" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/>
<field name="legend_blocked" invisible="1"/>
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
+2 -2
View File
@@ -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" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/>
<field name="project_template_id" context="{'active_test': False}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/>
<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'])]}"/>
</xpath>
</field>
</record>
@@ -26,7 +26,7 @@
string="Sales Order"
groups="sales_team.group_sale_salesman"/>
</div>
<xpath expr="//field[@name='partner_email']" position="after">
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False), '&amp;', ('sale_order_id', '!=', False), ('sale_line_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/>
<field name="sale_order_id" attrs="{'invisible': [('project_id', '!=', False)]}" groups="sales_team.group_sale_salesman"/>
<field name="commercial_partner_id" invisible="1" />
+6 -3
View File
@@ -23,11 +23,14 @@ class AccountAnalyticLine(models.Model):
timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet")
non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheet will not be billed.")
@api.depends('so_line.product_id', 'project_id', 'task_id', 'non_allow_billable')
@api.depends('so_line.product_id', 'project_id', 'task_id', 'non_allow_billable', 'task_id.bill_type', 'task_id.pricing_type', 'task_id.non_allow_billable')
def _compute_timesheet_invoice_type(self):
non_allowed_billable = self.filtered('non_allow_billable')
non_allowed_billable.timesheet_invoice_type = 'non_billable_timesheet'
for timesheet in self - non_allowed_billable:
non_allowed_billable_task = (self - non_allowed_billable).filtered(lambda t: t.task_id.bill_type == 'customer_project' and t.task_id.pricing_type == 'employee_rate' and t.task_id.non_allow_billable)
non_allowed_billable_task.timesheet_invoice_type = 'non_billable'
for timesheet in self - non_allowed_billable - non_allowed_billable_task:
if timesheet.project_id: # AAL will be set to False
invoice_type = 'non_billable_project' if not timesheet.task_id else 'non_billable'
if timesheet.task_id and timesheet.so_line.product_id.type == 'service':
@@ -116,7 +119,7 @@ class AccountAnalyticLine(models.Model):
return task.sale_line_id
if task.pricing_type == 'fixed_rate':
return task.sale_line_id
elif task.pricing_type == 'employee_rate':
elif task.pricing_type == 'employee_rate' and not task.non_allow_billable:
map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', task.project_id.id), ('employee_id', '=', employee.id)])
if map_entry:
return map_entry.sale_line_id
+33 -3
View File
@@ -82,7 +82,8 @@ class Project(models.Model):
@api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id', 'bill_type')
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')
employees = self.env['account.analytic.line'].read_group([('project_id', 'in', projects.ids)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
tasks = projects.task_ids.filtered(lambda t: not t.non_allow_billable)
employees = self.env['account.analytic.line'].read_group([('task_id', 'in', tasks.ids)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
dict_project_employee = defaultdict(list)
for line in employees:
dict_project_employee[line['project_id'][0]] += [line['employee_id'][0]]
@@ -171,6 +172,18 @@ class Project(models.Model):
},
}
def action_view_so(self):
self.ensure_one()
action_window = {
"type": "ir.actions.act_window",
"res_model": "sale.order",
"name": "Sales Order",
"views": [[False, "form"]],
"context": {"create": False, "show_sale": True},
"res_id": self.sale_order_id.id
}
return action_window
class ProjectTask(models.Model):
_inherit = "project.task"
@@ -204,6 +217,8 @@ class ProjectTask(models.Model):
'|', ('company_id', '=', False), ('company_id', '=', company_id)]""",
help='Select a Service product with which you would like to bill your time spent on this task.')
non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheets linked to this task will not be billed.")
@api.depends(
'allow_billable', 'allow_timesheets', 'sale_order_id')
def _compute_display_create_order(self):
@@ -247,10 +262,12 @@ class ProjectTask(models.Model):
for task in self:
task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active
@api.depends('sale_line_id', 'project_id', 'allow_billable', 'bill_type')
@api.depends('sale_line_id', 'project_id', 'allow_billable', 'bill_type', 'pricing_type', 'non_allow_billable')
def _compute_sale_order_id(self):
for task in self:
if task.allow_billable and task.bill_type == 'customer_project':
if task.allow_billable and task.bill_type == 'customer_project' and task.pricing_type == 'employee_rate' and task.non_allow_billable:
task.sale_order_id = False
elif task.allow_billable and task.bill_type == 'customer_project':
task.sale_order_id = task.project_id.sale_order_id
elif task.allow_billable and task.bill_type == 'customer_task':
task.sale_order_id = task.sale_line_id.sudo().order_id
@@ -298,6 +315,19 @@ class ProjectTask(models.Model):
project = self.env['project.project'].browse(values.get('project_id'))
if project.allow_timesheets:
timesheet_ids.write({'project_id': values.get('project_id')})
if 'non_allow_billable' in values and self.filtered('allow_timesheets').sudo().timesheet_ids:
timesheet_ids = self.filtered('allow_timesheets').timesheet_ids.filtered(
lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel')
)
if values['non_allow_billable']:
timesheet_ids.write({'so_line': False})
else:
# We write project on timesheet lines to call _timesheet_preprocess. This function will set correct the SOL
for project in timesheet_ids.project_id:
current_timesheet_ids = timesheet_ids.filtered(lambda t: t.project_id == project)
for employee in current_timesheet_ids.employee_id:
current_timesheet_ids.filtered(lambda t: t.employee_id == employee).write({'project_id': project.id})
return res
def action_make_billable(self):
@@ -8,6 +8,11 @@
<field name="arch" type="xml">
<div class="oe_button_box" position="inside">
<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')]}"
string="Sales Order"
groups="sales_team.group_sale_salesman"/>
</div>
<xpath expr="//header" position="inside">
<button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman"/>
@@ -18,9 +23,13 @@
<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': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
<field name="timesheet_product_id" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service'}"/>
<field name="sale_order_id" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project')], 'readonly': [('sale_order_id', '!=', False)]}" context="{'default_partner_id': partner_id}"/>
<field name="sale_line_id" attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
<div class="o_td_label" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}">
<label for="timesheet_product_id" string="Default Service" attrs="{'invisible': [('bill_type', '!=', 'customer_task')]}"/>
<label for="timesheet_product_id" string="Service" attrs="{'invisible': [('bill_type', '=', 'customer_task')]}"/>
</div>
<field name="timesheet_product_id" nolabel="1" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
<field name="sale_order_id" invisible="1"/>
<field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
</group>
<field name="sale_line_employee_ids" attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}">
<tree editable="top">
@@ -64,7 +73,11 @@
<field name="bill_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('allow_timesheets', '=', False)], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
<field name="partner_id" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('bill_type', '!=', 'customer_project')], 'required': [('bill_type', '=', 'customer_project')]}"/>
<field name="pricing_type" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
<field name="timesheet_product_id" string="Default Service" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&amp;', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}"/>
<div class="o_td_label" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&amp;', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')]}">
<label for="timesheet_product_id" string="Default Service" attrs="{'invisible': [('bill_type', '!=', 'customer_task')]}"/>
<label for="timesheet_product_id" string="Service" attrs="{'invisible': [('bill_type', '=', 'customer_task')]}"/>
</div>
<field name="timesheet_product_id" nolabel="1" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&amp;', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')], 'required': ['&amp;', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
</field>
</field>
</record>
@@ -80,7 +93,7 @@
</xpath>
<xpath expr="//div[hasclass('o_kanban_card_manage_pane')]" position="after">
<t t-if="record.warning_employee_rate.raw_value">
<i class="float-right mx-4 fa fa-exclamation-triangle text-danger" role="img" title="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable." aria-label="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable."/>
<i class="float-right mr-5 mt-1 fa fa-exclamation-triangle text-danger" role="img" title="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable." aria-label="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable."/>
</t>
</xpath>
<xpath expr="//a[@name='action_view_account_analytic_line']" position="attributes">
@@ -125,15 +138,16 @@
<field name="is_project_map_empty" invisible="1"/>
<field name="has_multi_sol" invisible="1"/>
</xpath>
<xpath expr="//field[@name='partner_email']" position="after">
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="bill_type" invisible="1"/>
<field name="pricing_type" invisible="1"/>
<field name="timesheet_product_id" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('partner_id', '=', False), ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}" context="{'default_type': 'service'}" placeholder="Leave empty if non-billable"/>
<field name="timesheet_product_id" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&amp;', ('partner_id', '=', False), ('bill_type', '=', 'customer_project'), ('sale_order_id', '!=', False), '&amp;', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}" placeholder="Leave empty if non-billable"/>
<field name="non_allow_billable" attrs="{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('pricing_type', '!=', 'employee_rate'), ('bill_type', '=', 'customer_task')]}"/>
</xpath>
<xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
<field name="timesheet_invoice_id" invisible="1"/>
<field name="so_line" readonly="1" attrs="{'column_invisible': [('parent.has_multi_sol', '=', False), '|', ('parent.is_project_map_empty', '=', True), ('parent.pricing_type', '!=', 'employee_rate')]}" optional="hide"/>
<field name="non_allow_billable" attrs="{'column_invisible': ['|', ('parent.pricing_type', '=', 'employee_rate'), '&amp;', ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False)]}" optional="hide"/>
<field name="non_allow_billable" attrs="{'column_invisible': ['|', '&amp;', ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&amp;', '&amp;', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate'), ('parent.non_allow_billable', '=', True)]}" optional="hide"/>
</xpath>
</field>
</record>
@@ -145,7 +159,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='sale_line_id']" position="attributes">
<attribute name="attrs">
{'invisible': ['|', '|', ('sale_order_id', '=', False), '&amp;', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)], 'readonly': [('sale_order_id', '=', False)]}
{'invisible': ['|', '|', '&amp;', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_task'), '&amp;', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]}
</attribute>
<attribute name="placeholder">Leave empty if non-billable</attribute>
</xpath>
@@ -32,7 +32,7 @@ class ProjectCreateSalesOrder(models.TransientModel):
(0, 0, {
'employee_id': e.employee_id.id,
'product_id': e.timesheet_product_id.id or default_product.id,
'price_unit': e.price_unit or default_product.lst_price
'price_unit': e.price_unit if e.timesheet_product_id else default_product.lst_price
}) for e in project.sale_line_employee_ids]
employee_from_timesheet = project.task_ids.timesheet_ids.employee_id - project.sale_line_employee_ids.employee_id
result['line_ids'] += [
@@ -61,6 +61,28 @@ class ProjectCreateSalesOrder(models.TransientModel):
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
line_ids = fields.One2many('project.create.sale.order.line', 'wizard_id', string='Lines')
info_invoice = fields.Char(compute='_compute_info_invoice')
@api.depends('sale_order_id', 'link_selection')
def _compute_info_invoice(self):
for line in self:
tasks = line.project_id.tasks.filtered(lambda t: not t.non_allow_billable)
domain = self.env['sale.order.line']._timesheet_compute_delivered_quantity_domain()
timesheet = self.env['account.analytic.line'].read_group(domain + [('task_id', 'in', tasks.ids), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)], ['unit_amount'], ['task_id'])
unit_amount = round(timesheet[0].get('unit_amount', 0), 2) if timesheet else 0
if not unit_amount:
line.info_invoice = False
continue
company_uom = self.env.company.timesheet_encode_uom_id
label = _("hours")
if company_uom == self.env.ref('uom.product_uom_day'):
label = _("days")
if line.link_selection == 'create':
line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order.", amount=unit_amount, label=label)
elif line.sale_order_id:
line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order.", amount=unit_amount, label=label)
else:
line.info_invoice = False
@api.onchange('partner_id')
def _onchange_partner_id(self):
@@ -90,9 +112,12 @@ class ProjectCreateSalesOrder(models.TransientModel):
lines_already_present[wizard_line.employee_id.id].write({
'sale_line_id': wizard_line.sale_line_id.id
})
self.project_id.tasks.filtered(lambda task: task.non_allow_billable).sale_line_id = False
tasks = self.project_id.tasks.filtered(lambda t: not t.non_allow_billable)
# assign SOL to timesheets
for map_entry in self.project_id.sale_line_employee_ids:
self.env['account.analytic.line'].search([('task_id', 'in', self.project_id.tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
self.env['account.analytic.line'].search([('task_id', 'in', tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
'so_line': map_entry.sale_line_id.id
})
else:
@@ -219,6 +244,7 @@ class ProjectCreateSalesOrder(models.TransientModel):
lines_already_present = dict([(l.employee_id.id, l) for l in self.project_id.sale_line_employee_ids])
non_billable_tasks = self.project_id.tasks.filtered(lambda task: not task.sale_line_id)
non_allow_billable_tasks = self.project_id.tasks.filtered(lambda task: task.non_allow_billable)
map_entries = self.env['project.sale.line.employee.map']
EmployeeMap = self.env['project.sale.line.employee.map'].sudo()
@@ -264,10 +290,12 @@ class ProjectCreateSalesOrder(models.TransientModel):
'partner_id': sale_order.partner_id.id,
'email_from': sale_order.partner_id.email,
})
non_allow_billable_tasks.sale_line_id = False
tasks = self.project_id.tasks.filtered(lambda t: not t.non_allow_billable)
# assign SOL to timesheets
for map_entry in map_entries:
self.env['account.analytic.line'].search([('task_id', 'in', self.project_id.tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
self.env['account.analytic.line'].search([('task_id', 'in', tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
'so_line': map_entry.sale_line_id.id
})
@@ -282,7 +310,7 @@ class ProjectCreateSalesOrderLine(models.TransientModel):
wizard_id = fields.Many2one('project.create.sale.order', required=True)
product_id = fields.Many2one('product.product', domain=[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')], string="Service",
help="Product of the sales order item. Must be a service invoiced based on timesheets on tasks.")
price_unit = fields.Float("Unit Price", compute="_compute_price_unit", store=True, readonly=False, default=1.0, help="Unit price of the sales order item.")
price_unit = fields.Float("Unit Price", help="Unit price of the sales order item.")
currency_id = fields.Many2one('res.currency', string="Currency")
employee_id = fields.Many2one('hr.employee', string="Employee", help="Employee that has timesheets on the project.")
sale_line_id = fields.Many2one('sale.order.line', "Sale Order Item", compute='_compute_sale_line_id', store=True, readonly=False)
@@ -291,15 +319,14 @@ class ProjectCreateSalesOrderLine(models.TransientModel):
('unique_employee_per_wizard', 'UNIQUE(wizard_id, employee_id)', "An employee cannot be selected more than once in the mapping. Please remove duplicate(s) and try again."),
]
@api.depends('product_id', 'sale_line_id')
def _compute_price_unit(self):
for line in self:
if line.wizard_id.link_selection == 'link':
line.price_unit = line.sale_line_id.price_unit
line.currency_id = line.sale_line_id.currency_id
else:
line.price_unit = line.product_id.lst_price or 0
line.currency_id = line.product_id.currency_id
@api.onchange('product_id', 'sale_line_id')
def _onchange_product_id(self):
if self.wizard_id.link_selection == 'link':
self.price_unit = self.sale_line_id.price_unit
self.currency_id = self.sale_line_id.currency_id
else:
self.price_unit = self.product_id.lst_price or 0
self.currency_id = self.product_id.currency_id
@api.depends('wizard_id.sale_order_id')
def _compute_sale_line_id(self):
@@ -25,13 +25,16 @@
<field name="line_ids" nolabel="1" attrs="{'required': [('pricing_type', '=', 'employee_rate')]}">
<tree editable="bottom">
<field name="employee_id" options="{'no_create_edit': True, 'no_create': True}" attrs="{'column_invisible': [('parent.pricing_type', '=', 'fixed_rate')], 'required': [('parent.pricing_type', '=', 'employee_rate')]}"/>
<field name="sale_line_id" options="{'no_create_edit': True, 'no_create': True}" domain="[('is_service', '=', True), ('order_id', '=', parent.sale_order_id)]" attrs="{'column_invisible': ['|', ('parent.link_selection', '!=', 'link'), ('parent.pricing_type', '=', 'fixed_rate')], 'required': ['&amp;', ('parent.link_selection', '=', 'link'), ('parent.pricing_type', '=', 'employee_rate')]}"/>
<field name="sale_line_id" options="{'no_create_edit': True, 'no_create': True, 'no_open': True}" domain="[('is_service', '=', True), ('order_id', '=', parent.sale_order_id)]" attrs="{'column_invisible': ['|', ('parent.link_selection', '!=', 'link'), ('parent.pricing_type', '=', 'fixed_rate')], 'required': ['&amp;', ('parent.link_selection', '=', 'link'), ('parent.pricing_type', '=', 'employee_rate')]}"/>
<field name="product_id" options="{'no_create_edit': True, 'no_create': True}" attrs="{'column_invisible': [('parent.link_selection', '=', 'link')], 'required': [('parent.link_selection', '!=', 'link')]}"/>
<field name="price_unit" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('parent.link_selection', '=', 'link')]}"/>
<field name="currency_id" invisible="1"/>
</tree>
</field>
</group>
<group attrs="{'invisible': [('info_invoice', '=', False)]}">
<field name="info_invoice" nolabel="1"/>
</group>
<footer>
<button string="Link to Sales Order" type="object" name="action_link_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '!=', 'link')]}"/>
<button string="Create Sales Order" type="object" name="action_create_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '=', 'link')]}"/>
@@ -50,14 +50,17 @@ class ProjectTaskCreateSalesOrder(models.TransientModel):
domain = self.env['sale.order.line']._timesheet_compute_delivered_quantity_domain()
timesheet = self.env['account.analytic.line'].read_group(domain + [('task_id', '=', self.task_id.id), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)], ['unit_amount'], ['task_id'])
unit_amount = round(timesheet[0].get('unit_amount', 0), 2) if timesheet else 0
if not unit_amount:
line.info_invoice = False
continue
company_uom = self.env.company.timesheet_encode_uom_id
label = _("hours")
if company_uom == self.env.ref('uom.product_uom_day'):
label = _("days")
if line.link_selection == 'create' and line.price_unit:
line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order Item.", amount=unit_amount, label=label)
line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order.", amount=unit_amount, label=label)
elif line.sale_line_id:
line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order Item.", amount=unit_amount, label=label)
line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order.", amount=unit_amount, label=label)
else:
line.info_invoice = False
@@ -24,7 +24,9 @@
<field name="sale_line_id" options="{'no_create': True, 'no_create_edit': True}" attrs="{'required': [('link_selection', '=', 'link')]}"/>
<field name="commercial_partner_id" invisible="1"/>
</group>
<field name="info_invoice"/>
</group>
<group attrs="{'invisible': [('info_invoice', '=', False)]}">
<field name="info_invoice" nolabel="1"/>
</group>
<footer>
<button string="Link to Sales Order" type="object" name="action_link_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '!=', 'link')]}"/>