diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index 69e0f37594f..263a14be0c1 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -666,8 +666,8 @@
-
+
diff --git a/addons/sale_project/views/product_views.xml b/addons/sale_project/views/product_views.xml
index ddee48ed4b6..47a0d2f1250 100644
--- a/addons/sale_project/views/product_views.xml
+++ b/addons/sale_project/views/product_views.xml
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml
index 04cbd469e83..b4310ef327e 100644
--- a/addons/sale_project/views/project_task_views.xml
+++ b/addons/sale_project/views/project_task_views.xml
@@ -26,7 +26,7 @@
string="Sales Order"
groups="sales_team.group_sale_salesman"/>
-
+
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index eb575a7941e..94744922a6c 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -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
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index 36098ebd814..229594df6e1 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -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):
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
index 081e61397d1..ed0df2ab2f3 100644
--- a/addons/sale_timesheet/views/project_task_views.xml
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -8,6 +8,11 @@
+
@@ -18,9 +23,13 @@
-
-
-
+
+
+
+
+
+
+
@@ -64,7 +73,11 @@
-
+
+
+
+
+
@@ -80,7 +93,7 @@
-
+
@@ -125,15 +138,16 @@
-
+
-
+
+
-
+
@@ -145,7 +159,7 @@
- {'invisible': ['|', '|', ('sale_order_id', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)], 'readonly': [('sale_order_id', '=', False)]}
+ {'invisible': ['|', '|', '&', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_task'), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]}
Leave empty if non-billable
diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py
index b56a8458fa1..6a5ce482428 100644
--- a/addons/sale_timesheet/wizard/project_create_sale_order.py
+++ b/addons/sale_timesheet/wizard/project_create_sale_order.py
@@ -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):
diff --git a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
index 5d16ce78e59..73ebfcd8974 100644
--- a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
+++ b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
@@ -25,13 +25,16 @@
-
+
+
+
+