diff --git a/addons/sale_timesheet/controllers/portal.py b/addons/sale_timesheet/controllers/portal.py index 3ebc91e5931..a705576852f 100644 --- a/addons/sale_timesheet/controllers/portal.py +++ b/addons/sale_timesheet/controllers/portal.py @@ -14,4 +14,5 @@ class PortalAccount(PortalAccount): domain = request.env['account.analytic.line']._timesheet_get_portal_domain() domain = expression.AND([domain, [('timesheet_invoice_id', '=', invoice.id)]]) values['timesheets'] = request.env['account.analytic.line'].sudo().search(domain) + values['is_uom_day'] = request.env['account.analytic.line'].sudo()._is_timesheet_encode_uom_day() return values diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py index bd02f092307..e8052912d8c 100644 --- a/addons/sale_timesheet/models/project_overview.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -30,6 +30,8 @@ class Project(models.Model): def _plan_prepare_values(self): currency = self.env.company.currency_id uom_hour = self.env.ref('uom.product_uom_hour') + company_uom = self.env.company.timesheet_encode_uom_id + is_uom_day = company_uom == self.env.ref('uom.product_uom_day') hour_rounding = uom_hour.rounding billable_types = ['non_billable', 'non_billable_project', 'billable_time', 'billable_fixed'] @@ -39,13 +41,14 @@ class Project(models.Model): 'timesheet_domain': [('project_id', 'in', self.ids)], 'profitability_domain': [('project_id', 'in', self.ids)], 'stat_buttons': self._plan_get_stat_button(), + 'is_uom_day': is_uom_day, } # # Hours, Rates and Profitability # dashboard_values = { - 'hours': dict.fromkeys(billable_types + ['total'], 0.0), + 'time': dict.fromkeys(billable_types + ['total'], 0.0), 'rates': dict.fromkeys(billable_types + ['total'], 0.0), 'profit': { 'invoiced': 0.0, @@ -58,8 +61,12 @@ 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) + canceled_hours = float_round(total_canceled_hours, precision_rounding=hour_rounding) + if is_uom_day: + # convert time from hours to days + canceled_hours = round(uom_hour._compute_quantity(canceled_hours, company_uom, raise_if_failure=False), 2) + dashboard_values['time']['canceled'] = canceled_hours + dashboard_values['time']['total'] += canceled_hours # hours (from timesheet) and rates (by billable type) dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type @@ -67,12 +74,17 @@ class Project(models.Model): 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) - dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding) + amount = float_round(data.get('unit_amount'), precision_rounding=hour_rounding) + if is_uom_day: + # convert time from hours to days + amount = round(uom_hour._compute_quantity(amount, company_uom, raise_if_failure=False), 2) + dashboard_values['time'][billable_type] = amount + dashboard_values['time']['total'] += amount # rates rate = round(data.get('unit_amount') / dashboard_total_hours * 100, 2) if dashboard_total_hours else 0.0 dashboard_values['rates'][billable_type] = rate dashboard_values['rates']['total'] += rate + dashboard_values['time']['total'] = round(dashboard_values['time']['total'], 2) # 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) @@ -148,6 +160,11 @@ class Project(models.Model): # 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, 'canceled']]) + if is_uom_day: + # convert all times from hours to days + for time_type in ['non_billable_project', 'non_billable', 'billable_time', 'billable_fixed', 'canceled', 'total']: + if repartition_employee[employee_id][time_type]: + repartition_employee[employee_id][time_type] = round(uom_hour._compute_quantity(repartition_employee[employee_id][time_type], company_uom, raise_if_failure=False), 2) 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 @@ -166,6 +183,8 @@ class Project(models.Model): return False uom_hour = self.env.ref('uom.product_uom_hour') + company_uom = self.env.company.timesheet_encode_uom_id + is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day') # build SQL query and fetch raw data query, query_params = self._table_rows_sql_query() @@ -250,6 +269,11 @@ class Project(models.Model): sale_line_row[0]['has_children'] = True timesheet_forecast_table_rows.append(employee_row) + if is_uom_day: + # convert all values from hours to days + for row in timesheet_forecast_table_rows: + for index in range(1, len(row)): + row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2) # complete table data return { 'header': self._table_header(), diff --git a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss index b1c2aa57392..5d2bbc4b09d 100644 --- a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss +++ b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss @@ -89,7 +89,7 @@ $canceled-color: gray; border-radius: 3px; } - >.o_timesheet_plan_badge { + .o_timesheet_plan_badge { margin-bottom: 10px; > .badge { border: none; @@ -100,14 +100,16 @@ $canceled-color: gray; } } - .table { - margin-top:15px; - } - .table > thead > tr > th, .table > thead > tr > td, + .table > thead > tr > td, .table > tbody > tr > th, .table > tbody > tr > td { border: none; padding: 0px; } + + .table > thead > tr > th { + border: none; + padding-left: 0px;; + } } .o_project_plan_project_timesheet_forecast { diff --git a/addons/sale_timesheet/tests/test_project_overview.py b/addons/sale_timesheet/tests/test_project_overview.py index df4325d29d7..1aed5561507 100644 --- a/addons/sale_timesheet/tests/test_project_overview.py +++ b/addons/sale_timesheet/tests/test_project_overview.py @@ -89,11 +89,11 @@ class TestSaleProject(TestReporting): project_invoiced = self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty * timesheet1.unit_amount project_timesheet_cost = timesheet2.amount + timesheet3.amount + timesheet4.amount + timesheet5.amount + timesheet1.amount - self.assertEqual(float_compare(vals['dashboard']['hours']['non_billable'], timesheet5.unit_amount, precision_rounding=rounding), 0, "The hours non-billable should be the one from the SO2 line, as we are in ordered quantity") - self.assertEqual(float_compare(vals['dashboard']['hours']['non_billable_project'], timesheet2.unit_amount, precision_rounding=rounding), 0, "The hours non-billable-project should be the one from the SO2 line, as we are in ordered quantity") - self.assertEqual(float_compare(vals['dashboard']['hours']['billable_time'], timesheet1.unit_amount, precision_rounding=rounding), 0, "The hours billable-time should be the one from the SO2 line, as we are in ordered quantity") - self.assertEqual(float_compare(vals['dashboard']['hours']['billable_fixed'], project_so_timesheet_sold_unit, precision_rounding=rounding), 0, "The hours billable-fixed should be the one from the SO2 line, as we are in ordered quantity") - self.assertEqual(float_compare(vals['dashboard']['hours']['total'], dashboard_value, precision_rounding=rounding), 0, "The total hours should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['time']['non_billable'], timesheet5.unit_amount, precision_rounding=rounding), 0, "The hours non-billable should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['time']['non_billable_project'], timesheet2.unit_amount, precision_rounding=rounding), 0, "The hours non-billable-project should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['time']['billable_time'], timesheet1.unit_amount, precision_rounding=rounding), 0, "The hours billable-time should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['time']['billable_fixed'], project_so_timesheet_sold_unit, precision_rounding=rounding), 0, "The hours billable-fixed should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['time']['total'], dashboard_value, precision_rounding=rounding), 0, "The total hours should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['dashboard']['rates']['non_billable'], project_rate_non_billable, precision_rounding=rounding), 0, "The rate non-billable should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['dashboard']['rates']['non_billable_project'], project_rate_non_billable_project, precision_rounding=rounding), 0, "The rate non-billable-project should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['dashboard']['rates']['billable_time'], project_rate_billable_time, precision_rounding=rounding), 0, "The rate billable-time should be the one from the SO2 line, as we are in ordered quantity") diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index c0437ab0a18..d3b057cf615 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -64,7 +64,8 @@
-

Recorded Hours and Profitability

+

Recorded Days and Profitability

+

Recorded Hours and Profitability

@@ -74,11 +75,17 @@ - + - + - + @@ -227,28 +243,38 @@

There are no timesheets for now.

-
- Hours + + Days recorded + Hours recorded +
- + + ( %) @@ -89,7 +96,8 @@
- + + ( %) @@ -98,9 +106,10 @@ Billed at a Fixed price
- + + ( %) @@ -111,7 +120,8 @@
- + + ( %) @@ -130,9 +140,10 @@
- + + ( %) @@ -142,7 +153,12 @@
+ + + + + Total
+ + + + + + + + + @@ -262,7 +288,8 @@
EmployeeDays SpentHours Spent + +
- + +
@@ -367,15 +394,24 @@ - + + + + - + + + + - + + + + @@ -406,7 +442,9 @@