diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 81895718a96..6838b6b37cd 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -15,9 +15,9 @@ class AccountAnalyticLine(models.Model):
return expression.OR([domain, [('qty_delivered_method', '=', 'timesheet')]])
timesheet_invoice_type = fields.Selection([
- ('billable_time', 'Billable Time'),
- ('billable_fixed', 'Billable Fixed'),
- ('non_billable', 'Non Billable'),
+ ('billable_time', 'Billed on Timesheets'),
+ ('billable_fixed', 'Billed at a Fixed price'),
+ ('non_billable', 'Non Billable Tasks'),
('non_billable_project', 'No task found')], string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True)
timesheet_invoice_id = fields.Many2one('account.invoice', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet")
diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py
index 13f975b086d..b68bc998f95 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -53,10 +53,16 @@ class Project(models.Model):
}
}
+ # hours from non-invoiced timesheets that are linked to canceled so
+ canceled_hours_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), ('so_line.state', '=', 'cancel')]
+ total_canceled_hours = sum(self.env['account.analytic.line'].search(canceled_hours_domain).mapped('unit_amount'))
+ dashboard_values['hours']['canceled'] = float_round(total_canceled_hours, precision_rounding=hour_rounding)
+ dashboard_values['hours']['total'] += float_round(total_canceled_hours, precision_rounding=hour_rounding)
+
# hours (from timesheet) and rates (by billable type)
- dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False)] # force billable type
+ dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
- dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data])
+ dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) + total_canceled_hours
for data in dashboard_data:
billable_type = data['timesheet_invoice_type']
dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
@@ -66,6 +72,9 @@ class Project(models.Model):
dashboard_values['rates'][billable_type] = rate
dashboard_values['rates']['total'] += rate
+ # rates from non-invoiced timesheets that are linked to canceled so
+ dashboard_values['rates']['canceled'] = float_round(100 * total_canceled_hours / (dashboard_total_hours or 1), precision_rounding=hour_rounding)
+
# profitability, using profitability SQL report
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0)
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id'])
@@ -90,7 +99,11 @@ class Project(models.Model):
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id')
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
- repartition_data = self.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
+ # repartition data, without timesheet on cancelled so
+ repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
+ # read timesheet on cancelled so
+ cancelled_so_timesheet = self.env['account.analytic.line'].read_group(repartition_domain + [('so_line.state', '=', 'cancel')], ['employee_id', 'unit_amount'], ['employee_id'], lazy=False)
+ repartition_data += [{**canceled, 'timesheet_invoice_type': 'canceled'} for canceled in cancelled_so_timesheet]
# set repartition per type per employee
repartition_employee = {}
@@ -102,6 +115,7 @@ class Project(models.Model):
non_billable=0.0,
billable_time=0.0,
billable_fixed=0.0,
+ canceled=0.0,
total=0.0,
)
for data in repartition_data:
@@ -113,14 +127,13 @@ class Project(models.Model):
non_billable=0.0,
billable_time=0.0,
billable_fixed=0.0,
+ canceled=0.0,
total=0.0,
))[data['timesheet_invoice_type']] = float_round(data.get('unit_amount', 0.0), precision_rounding=hour_rounding)
repartition_employee[employee_id]['__domain_' + data['timesheet_invoice_type']] = data['__domain']
-
# compute total
for employee_id, vals in repartition_employee.items():
- repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in billable_types])
-
+ repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in [*billable_types, 'canceled']])
hours_per_employee = [repartition_employee[employee_id]['total'] for employee_id in repartition_employee]
values['repartition_employee_max'] = (max(hours_per_employee) if hours_per_employee else 1) or 1
values['repartition_employee'] = repartition_employee
@@ -158,8 +171,10 @@ class Project(models.Model):
if row[0]['sale_order_id']:
sale_order_ids.add(row[0]['sale_order_id'])
+ sale_orders = self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)
sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids)
- map_so_names = {so.id: so.name for so in self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)}
+ map_so_names = {so.id: so.name for so in sale_orders}
+ map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders}
map_sol = {sol.id: sol for sol in sale_order_lines}
map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines}
map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines}
@@ -199,7 +214,7 @@ class Project(models.Model):
sale_order_id = row_key[0]
# sale order row
if sale_order_id not in rows_sale_order:
- rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
+ rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
for index in range(len(rows_sale_line[row_key])):
if index != 0:
@@ -239,7 +254,7 @@ class Project(models.Model):
month_index = fields.Date.from_string(date).month
return babel.dates.get_month_names('abbreviated', locale=self.env.context.get('lang', 'en_US'))[month_index]
- header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Done'), _('Sold'), _('Remaining')]
+ header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')]
result = []
for name in header_names:
diff --git a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss
index 0b2280bf1dd..edc98499354 100644
--- a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss
+++ b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss
@@ -1,6 +1,13 @@
+$canceled-color: gray;
+
.o_timesheet_plan_sale_timesheet {
+ .o_title {
+ margin-top: 30px;
+ margin-bottom: 30px;
+ }
+
[type='action'] {
color: $o-brand-primary;
cursor: pointer;
@@ -12,6 +19,19 @@
width: auto;
}
+ .o_profitability_wrapper {
+ display: flex;
+ flex: 1;
+ flex-wrap: wrap;
+
+ .o_profitability_section {
+ display: flex;
+ flex: 1;
+ justify-content: flex-start;
+ }
+
+ }
+
.o_timesheet_plan_sale_timesheet_dashboard {
.table {
@@ -32,6 +52,10 @@
text-align: right;
}
+ .section {
+
+ }
+
}
.o_timesheet_plan_sale_timesheet_people_time {
@@ -52,7 +76,12 @@
cursor: pointer;
}
.o_progress_non_billable {
- background-color: gray;
+ background-color: purple;
+ color: white;
+ cursor: pointer;
+ }
+ .o_progress_canceled {
+ background-color: $canceled-color;
color: white;
cursor: pointer;
}
@@ -78,6 +107,9 @@
}
.o_project_plan_project_timesheet_forecast {
+
+ margin-top: 50px;
+
th{
text-align: center;
vertical-align: top;
@@ -111,6 +143,12 @@
.o_timesheet_plan_redirect {
cursor: pointer;
}
+
+ .o_canceled_tag {
+ background-color: $canceled-color;
+ color: white;
+ border: 0px;
+ }
}
}
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 200d9af637d..17b0bc2799f 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -46,146 +46,175 @@
+
+
Hours recorded and Profitability
+
+
-
-
-
-
- |
- Hours
- |
-
-
- |
- Timesheets |
-
- Rates
- |
-
- %
- |
- Timesheets |
-
- Profitability
- |
-
-
- |
- Time invoiced |
-
-
- |
-
- |
- Fixed |
-
- %
- |
- Fixed |
-
-
- |
- Time to invoice |
-
-
- |
-
- |
- No task found |
-
- %
- |
- No task found |
-
-
- |
- Timesheet costs |
-
-
- |
-
- |
- Non billable |
-
- %
- |
- Non billable |
-
-
-
- |
- Other costs |
-
-
-
-
- |
- Total |
-
-
-
- |
-
- |
- Total |
- |
- |
-
-
-
- |
- Re-invoiced costs |
-
-
- |
- |
-
-
-
- |
- |
- |
- |
-
-
-
- |
- Total |
-
-
- |
- |
-
-
-
-
+
+
+
+
+
+ |
+ Hours
+ |
+
+ |
+
+ |
+
+ ( %)
+ |
+
+ Billed on Timesheets
+ |
+
+
+ |
+
+ |
+
+ ( %)
+ |
+
+ Billed at a Fixed price
+ |
+
+
+ |
+
+ |
+
+ ( %)
+ |
+
+ No task found
+ |
+
+
+ |
+
+ |
+
+ ( %)
+ |
+
+
+
+ Non Billable Tasks
+
+
+ |
+
+
+ |
+
+ |
+
+ ( %)
+ |
+
+ Cancelled
+ |
+
+
+ |
+ Total |
+ |
+
+
+
+
+
+
+
+
+
+ |
+ Profitability
+ |
+
+ |
+
+ |
+ Invoiced |
+
+
+ |
+
+ |
+ To invoice |
+
+
+ |
+
+ |
+
+ Timesheet costs
+ |
+
+
+ |
+
+ |
+ Other costs |
+
+
+ |
+
+ |
+ Re-invoiced costs |
+
+
+ |
+
+
+
+ |
+ Total |
+
+
+
+
+
+
+
Time by people
+
+
-
Time by people
There is no timesheet for now.
@@ -206,21 +235,25 @@
- Billable fixed
- billable_fixed
+ Billed on Timesheets
+ billable_time
- Billable time
- billable_time
+ Billed at a Fixed price
+ billable_fixed
No task found
non_billable_project
- Non billable
+ Non billable tasks
non_billable
+
+ Cancelled
+ canceled
+
@@ -231,13 +264,21 @@
+
+
Timesheets
+
+
- Timesheets
+
+ |
+ Timesheets |
+ |
+
|
@@ -261,6 +302,9 @@
+
+ Cancelled
+
@@ -309,10 +353,13 @@
:
+ >
+
+
+
+
diff --git a/addons/sale_timesheet/views/hr_timesheet_views.xml b/addons/sale_timesheet/views/hr_timesheet_views.xml
index a561a6e35fa..4a1f0d45619 100644
--- a/addons/sale_timesheet/views/hr_timesheet_views.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_views.xml
@@ -9,9 +9,9 @@
-
-
-
+
+
+
|