From dadcec593d8328727b17f218c69fa1bc1433686f Mon Sep 17 00:00:00 2001 From: "Yannick Tivisse (yti)" Date: Tue, 28 Apr 2020 09:13:36 +0000 Subject: [PATCH] [IMP] project: Improve global UX (back2basics) Improve the onboarding of new users and clean a few screens. Task ID: 2237996 closes odoo/odoo#50284 Related: odoo/enterprise#10536 Signed-off-by: Yannick Tivisse (yti) Co-authored-by: dmonzonis --- .../hr_timesheet/data/hr_timesheet_data.xml | 5 - addons/hr_timesheet/models/project.py | 16 + .../static/src/js/timesheet_uom_timer.js | 25 +- addons/hr_timesheet/views/project_views.xml | 43 ++- addons/portal_rating/models/rating.py | 2 +- addons/portal_rating/views/rating_views.xml | 4 +- addons/project/data/project_data.xml | 202 ---------- addons/project/models/project.py | 90 +++-- .../project/report/project_report_views.xml | 29 -- .../static/src/js/project_rating_reporting.js | 69 ++++ addons/project/views/project_assets.xml | 1 + addons/project/views/project_views.xml | 98 +++-- addons/project/views/rating_views.xml | 158 +++++++- .../views/res_config_settings_views.xml | 4 +- addons/rating/models/rating.py | 33 +- addons/rating/views/rating_rating_views.xml | 17 +- .../sale_project/views/project_task_views.xml | 6 +- .../data/sale_timesheet_filters.xml | 12 - .../sale_timesheet/models/project_overview.py | 52 ++- addons/sale_timesheet/models/sale_order.py | 10 +- .../project_profitability_report_analysis.py | 355 ++++++++++-------- ...ct_profitability_report_analysis_views.xml | 2 +- .../static/src/scss/sale_timesheet.scss | 3 + .../views/hr_timesheet_templates.xml | 82 ++-- .../views/project_task_views.xml | 15 +- .../sale_timesheet/views/sale_order_views.xml | 2 +- .../static/tests/tours/main_flow.js | 2 +- 27 files changed, 731 insertions(+), 606 deletions(-) create mode 100644 addons/project/static/src/js/project_rating_reporting.js diff --git a/addons/hr_timesheet/data/hr_timesheet_data.xml b/addons/hr_timesheet/data/hr_timesheet_data.xml index 0bb3c0dd642..fb2d8e194df 100644 --- a/addons/hr_timesheet/data/hr_timesheet_data.xml +++ b/addons/hr_timesheet/data/hr_timesheet_data.xml @@ -24,9 +24,4 @@ 15 - - - - - diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 92d01991325..0ee5fd58103 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -24,6 +24,10 @@ class Project(models.Model): help="Use a timer to record timesheets on tasks") timesheet_ids = fields.One2many('account.analytic.line', 'project_id', 'Associated Timesheets') + timesheet_encode_uom_id = fields.Many2one('uom.uom', related='company_id.timesheet_encode_uom_id') + total_timesheet_time = fields.Integer( + compute='_compute_total_timesheet_time', + help="Total number of time (in the proper UoM) recorded in the project, rounded to the unit.") _sql_constraints = [ ('timer_only_when_timesheet', "CHECK((allow_timesheets = 'f' AND allow_timesheet_timer = 'f') OR (allow_timesheets = 't'))", 'The timesheet timer can only be activated on project allowing timesheets.'), @@ -45,6 +49,18 @@ class Project(models.Model): for project in self: project.allow_timesheet_timer = project.allow_timesheets + @api.depends('timesheet_ids') + def _compute_total_timesheet_time(self): + for project in self: + total_time = 0.0 + for timesheet in project.timesheet_ids: + # Timesheets may be stored in a different unit of measure, so first + # we convert all of them to the reference unit + total_time += timesheet.unit_amount * timesheet.product_uom_id.factor_inv + # Now convert to the proper unit of measure set in the settings + total_time *= project.timesheet_encode_uom_id.factor + project.total_timesheet_time = int(round(total_time)) + @api.model def name_create(self, name): """ Create a project with name_create should generate analytic account creation """ diff --git a/addons/hr_timesheet/static/src/js/timesheet_uom_timer.js b/addons/hr_timesheet/static/src/js/timesheet_uom_timer.js index 1355ea009a5..165dfa6eb92 100644 --- a/addons/hr_timesheet/static/src/js/timesheet_uom_timer.js +++ b/addons/hr_timesheet/static/src/js/timesheet_uom_timer.js @@ -2,6 +2,7 @@ odoo.define('hr_timesheet.timesheet_uom_timer', function (require) { "use strict"; const fieldRegistry = require('web.field_registry'); +const fieldUtils = require('web.field_utils'); const TimesheetUom = require('hr_timesheet.timesheet_uom'); const { _lt } = require('web.core'); const session = require('web.session'); @@ -16,8 +17,8 @@ const FieldTimesheetTimeTimer = TimesheetUom.FieldTimesheetTime.extend({ this.isTimerRunning = this.record.data.is_timer_running; }, - _render: function () { - this._super.apply(this, arguments); + _render: async function () { + await this._super.apply(this, arguments); const my_timesheets = this.record.getContext().my_timesheet_display_timer; const display_timer = this.record.data.display_timer; if (my_timesheets && display_timer) { @@ -99,6 +100,26 @@ if (widgetName === 'float_toggle') { } fieldRegistry.add('timesheet_uom_timer', FieldTimesheetUom); +// bind the formatter and parser method, and tweak the options +const _tweak_options = function(options) { + if (!_.contains(options, 'factor')) { + options.factor = session.timesheet_uom_factor; + } + return options; +}; + +fieldUtils.format.timesheet_uom_timer = function(value, field, options) { + options = _tweak_options(options || {}); + const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType]; + return formatter(value, field, options); +}; + +fieldUtils.parse.timesheet_uom_timer = function(value, field, options) { + options = _tweak_options(options || {}); + const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType]; + return parser(value, field, options); +}; + return { FieldTimesheetTimeTimer, diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 1e7cc08f383..a1cfd5eb110 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -2,7 +2,7 @@ - Activities + Timesheets account.analytic.line tree,form @@ -39,7 +39,19 @@ 24
-
@@ -65,17 +77,17 @@ -
-
- -
-
-
+ @@ -293,6 +292,16 @@ +
+
+
@@ -345,25 +354,6 @@
- - - - - - - - -
@@ -385,6 +375,7 @@ + @@ -399,17 +390,16 @@ project.project.tree project.project - + - + - + - @@ -453,16 +443,19 @@
- - - - + + +
+
@@ -571,13 +564,6 @@
- - Project: Activate Sample Project - - code - action = model.activate_sample_project() - - Projects project.project @@ -588,9 +574,9 @@ main

- Create a new project + Create a project

- Or activate a sample project to play with. + Projects regroup tasks on the same topic and each have their own dashboard.

@@ -606,10 +592,10 @@ {} -

- Create a new project +

+ Create a project

- Or activate a sample project to play with. + Projects regroup tasks on the same topic and each have their own dashboard.

@@ -628,6 +614,9 @@ attrs="{'invisible' : [('user_id', '!=', False)]}"/> +
@@ -70,7 +70,7 @@
diff --git a/addons/rating/models/rating.py b/addons/rating/models/rating.py index 7c0ee789177..3d3ed658c29 100644 --- a/addons/rating/models/rating.py +++ b/addons/rating/models/rating.py @@ -31,17 +31,28 @@ class Rating(models.Model): def _default_access_token(self): return uuid.uuid4().hex + @api.model + def _selection_target_model(self): + return [(model.model, model.name) for model in self.env['ir.model'].search([])] + + create_date = fields.Datetime(string="Submitted on") res_name = fields.Char(string='Resource name', compute='_compute_res_name', store=True, help="The name of the rated resource.") res_model_id = fields.Many2one('ir.model', 'Related Document Model', index=True, ondelete='cascade', help='Model of the followed resource') res_model = fields.Char(string='Document Model', related='res_model_id.model', store=True, index=True, readonly=True) res_id = fields.Integer(string='Document', required=True, help="Identifier of the rated object", index=True) + resource_ref = fields.Reference( + string='Resource Ref', selection='_selection_target_model', + compute='_compute_resource_ref', readonly=True) parent_res_name = fields.Char('Parent Document Name', compute='_compute_parent_res_name', store=True) parent_res_model_id = fields.Many2one('ir.model', 'Parent Related Document Model', index=True, ondelete='cascade') parent_res_model = fields.Char('Parent Document Model', store=True, related='parent_res_model_id.model', index=True, readonly=False) parent_res_id = fields.Integer('Parent Document', index=True) + parent_ref = fields.Reference( + string='Parent Ref', selection='_selection_target_model', + compute='_compute_parent_ref', readonly=True) rated_partner_id = fields.Many2one('res.partner', string="Rated person", help="Owner of the rated resource") partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating") - rating = fields.Float(string="Rating Number", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 5=Happy") + rating = fields.Float(string="Rating Value", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 5=Happy") rating_image = fields.Binary('Image', compute='_compute_rating_image') rating_text = fields.Selection([ ('satisfied', 'Satisfied'), @@ -50,13 +61,29 @@ class Rating(models.Model): ('no_rating', 'No Rating yet')], string='Rating', store=True, compute='_compute_rating_text', readonly=True) feedback = fields.Text('Comment', help="Reason of the rating") message_id = fields.Many2one( - 'mail.message', string="Linked message", + 'mail.message', string="Message", index=True, ondelete='cascade', help="Associated message when posting a review. Mainly used in website addons.") - is_internal = fields.Boolean('Employee Only', readonly=False, related='message_id.is_internal', store=True) + is_internal = fields.Boolean('Visible Internally Only', readonly=False, related='message_id.is_internal', store=True) access_token = fields.Char('Security Token', default=_default_access_token, help="Access token to set the rating of the value") consumed = fields.Boolean(string="Filled Rating", help="Enabled if the rating has been filled.") + @api.depends('res_model', 'res_id') + def _compute_resource_ref(self): + for rating in self: + if rating.res_model and rating.res_model in self.env: + rating.resource_ref = '%s,%s' % (rating.res_model, rating.res_id or 0) + else: + rating.resource_ref = None + + @api.depends('parent_res_model', 'parent_res_id') + def _compute_parent_ref(self): + for rating in self: + if rating.parent_res_model and rating.parent_res_model in self.env: + rating.parent_ref = '%s,%s' % (rating.parent_res_model, rating.parent_res_id or 0) + else: + rating.parent_ref = None + @api.depends('parent_res_model', 'parent_res_id') def _compute_parent_res_name(self): for rating in self: diff --git a/addons/rating/views/rating_rating_views.xml b/addons/rating/views/rating_rating_views.xml index 9ed2ca1029e..06c566c2f2f 100644 --- a/addons/rating/views/rating_rating_views.xml +++ b/addons/rating/views/rating_rating_views.xml @@ -12,7 +12,7 @@ - +
@@ -23,17 +23,12 @@
-
- -
- - + + + + @@ -46,7 +41,7 @@ - + diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index 0a02c40b85f..04cbd469e83 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -8,7 +8,7 @@ - + @@ -56,10 +56,6 @@ - - - - diff --git a/addons/sale_timesheet/data/sale_timesheet_filters.xml b/addons/sale_timesheet/data/sale_timesheet_filters.xml index e66c608bd93..71aa9f43d3d 100644 --- a/addons/sale_timesheet/data/sale_timesheet_filters.xml +++ b/addons/sale_timesheet/data/sale_timesheet_filters.xml @@ -13,16 +13,4 @@ } - - Product by Customer - project.profitability.report - - { - 'group_by': ['partner_id', 'product_id'], - 'graph_measure': 'amount_untaxed_to_invoice', - 'graph_mode': 'bar', - 'graph_groupbys': ['partner_id', 'product_id'] - } - - diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py index cb3853a44d5..17327779f36 100644 --- a/addons/sale_timesheet/models/project_overview.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -396,24 +396,19 @@ class Project(models.Model): def _plan_get_stat_button(self): stat_buttons = [] - if len(self) == 1: - edit_project = self.env.ref('project.edit_project') - stat_buttons.append({ - 'name': _('Project'), - 'icon': 'fa fa-puzzle-piece', - 'action': _to_action_data('project.project', res_id=self.id, - views=[[edit_project.id, 'form']]) - }) - ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') - ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') + num_projects = len(self) + if num_projects == 1: + action_data = _to_action_data('project.project', res_id=self.id, + views=[[self.env.ref('project.edit_project').id, 'form']]) + else: + action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config'), + domain=[('id', 'in', self.ids)]) + stat_buttons.append({ - 'name': _('Timesheets'), - 'icon': 'fa fa-calendar', - 'action': _to_action_data( - 'account.analytic.line', - domain=[('project_id', 'in', self.ids)], - views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], - ) + 'name': _('Project') if num_projects == 1 else _('Projects'), + 'count': num_projects, + 'icon': 'fa fa-puzzle-piece', + 'action': action_data }) # if only one project, add it in the context as default value @@ -439,7 +434,7 @@ class Project(models.Model): ) }) stat_buttons.append({ - 'name': _("Late Tasks"), + 'name': [_("Tasks"), _("Late")], 'count': self.env['project.task'].search_count(late_tasks_domain), 'icon': 'fa fa-tasks', 'action': _to_action_data( @@ -449,7 +444,7 @@ class Project(models.Model): ), }) stat_buttons.append({ - 'name': _("Tasks in Overtime"), + 'name': [_("Tasks"), _("in Overtime")], 'count': self.env['project.task'].search_count(overtime_tasks_domain), 'icon': 'fa fa-tasks', 'action': _to_action_data( @@ -495,6 +490,25 @@ class Project(models.Model): context={'create': False, 'delete': False} ) }) + + ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') + ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') + if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): + timesheet_label = [_('Days'), _('Recorded')] + else: + timesheet_label = [_('Hours'), _('Recorded')] + + stat_buttons.append({ + 'name': timesheet_label, + 'count': sum(self.mapped('total_timesheet_time')), + 'icon': 'fa fa-calendar', + 'action': _to_action_data( + 'account.analytic.line', + domain=[('project_id', 'in', self.ids)], + views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], + ) + }) + return stat_buttons diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 9fabfbf9880..4078537eb7a 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -14,7 +14,7 @@ class SaleOrder(models.Model): # override domain project_id = fields.Many2one(domain="[('billable_type', 'in', ('no', '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.Float("Timesheet Total Duration", compute='_compute_timesheet_total_duration', help="Total recorded duration, expressed in the encoding UoM") + 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") @api.depends('analytic_account_id.line_ids') def _compute_timesheet_ids(self): @@ -32,7 +32,13 @@ class SaleOrder(models.Model): def _compute_timesheet_total_duration(self): for sale_order in self: timesheets = sale_order.timesheet_ids if self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') else sale_order.timesheet_ids.filtered(lambda t: t.user_id.id == self.env.uid) - sale_order.timesheet_total_duration = sum([timesheet.unit_amount for timesheet in timesheets]) + total_time = 0.0 + for timesheet in timesheets: + # Timesheets may be stored in a different unit of measure, so first we convert all of them to the reference unit + total_time += timesheet.unit_amount * timesheet.product_uom_id.factor_inv + # Now convert to the proper unit of measure + total_time *= sale_order.timesheet_encode_uom_id.factor + sale_order.timesheet_total_duration = total_time def action_view_project_ids(self): self.ensure_one() diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis.py b/addons/sale_timesheet/report/project_profitability_report_analysis.py index ef884e55421..865ce757695 100644 --- a/addons/sale_timesheet/report/project_profitability_report_analysis.py +++ b/addons/sale_timesheet/report/project_profitability_report_analysis.py @@ -18,9 +18,9 @@ class ProfitabilityAnalysis(models.Model): user_id = fields.Many2one('res.users', string='Project Manager', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) # cost - timesheet_unit_amount = fields.Float("Timesheet Unit Amount", digits=(16, 2), readonly=True, group_operator="sum") + timesheet_unit_amount = fields.Float("Timesheet Duration", digits=(16, 2), readonly=True, group_operator="sum") timesheet_cost = fields.Float("Timesheet Cost", digits=(16, 2), readonly=True, group_operator="sum") - expense_cost = fields.Float("Other Cost", digits=(16, 2), readonly=True, group_operator="sum") + expense_cost = fields.Float("Other Costs", digits=(16, 2), readonly=True, group_operator="sum") # sale revenue order_confirmation_date = fields.Datetime('Sales Order Confirmation Date', readonly=True) sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True) @@ -30,189 +30,216 @@ class ProfitabilityAnalysis(models.Model): amount_untaxed_to_invoice = fields.Float("Untaxed Amount To Invoice", digits=(16, 2), readonly=True, group_operator="sum") amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum") expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum") - expense_amount_untaxed_invoiced = fields.Float("Untaxed Re-invoiced Amount", digits=(16, 2), readonly=True, group_operator="sum") + expense_amount_untaxed_invoiced = fields.Float("Untaxed Amount Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum") other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum", help="All revenues that are not from timesheets and that are linked to the analytic account of the project.") + margin = fields.Float("Margin", digits=(16, 2), readonly=True, group_operator="sum") def init(self): tools.drop_view_if_exists(self._cr, self._table) query = """ CREATE VIEW %s AS ( SELECT - ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id, - P.id AS project_id, - P.user_id AS user_id, - SOL.id AS sale_line_id, - P.analytic_account_id AS analytic_account_id, - P.partner_id AS partner_id, - C.id AS company_id, - C.currency_id AS currency_id, - S.id AS sale_order_id, - S.date_order AS order_confirmation_date, - SOL.product_id AS product_id, - SOL.qty_delivered_method AS sale_qty_delivered_method, - CASE - WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) - ELSE 0.0 - END AS expense_amount_untaxed_to_invoice, - CASE - WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no' - THEN - CASE - WHEN T.expense_policy = 'sales_price' - THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced - ELSE -COST_SUMMARY.expense_cost - END - ELSE 0.0 - END AS expense_amount_untaxed_invoiced, - CASE - WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) - ELSE 0.0 - END AS amount_untaxed_to_invoice, - CASE - WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, COST_SUMMARY.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) - ELSE 0.0 - END AS amount_untaxed_invoiced, - COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount, - COST_SUMMARY.timesheet_cost AS timesheet_cost, - COST_SUMMARY.expense_cost AS expense_cost, - COST_SUMMARY.other_revenues AS other_revenues - FROM project_project P - JOIN res_company C ON C.id = P.company_id - LEFT JOIN ( - SELECT - project_id, - analytic_account_id, - sale_line_id, - SUM(timesheet_unit_amount) AS timesheet_unit_amount, - SUM(timesheet_cost) AS timesheet_cost, - SUM(expense_cost) AS expense_cost, - SUM(other_revenues) AS other_revenues, - SUM(downpayment_invoiced) AS downpayment_invoiced - FROM ( + sub.id as id, + sub.project_id as project_id, + sub.user_id as user_id, + sub.sale_line_id as sale_line_id, + sub.analytic_account_id as analytic_account_id, + sub.partner_id as partner_id, + sub.company_id as company_id, + sub.currency_id as currency_id, + sub.sale_order_id as sale_order_id, + sub.order_confirmation_date as order_confirmation_date, + sub.product_id as product_id, + sub.sale_qty_delivered_method as sale_qty_delivered_method, + sub.expense_amount_untaxed_to_invoice as expense_amount_untaxed_to_invoice, + sub.expense_amount_untaxed_invoiced as expense_amount_untaxed_invoiced, + sub.amount_untaxed_to_invoice as amount_untaxed_to_invoice, + sub.amount_untaxed_invoiced as amount_untaxed_invoiced, + sub.timesheet_unit_amount as timesheet_unit_amount, + sub.timesheet_cost as timesheet_cost, + sub.expense_cost as expense_cost, + sub.other_revenues as other_revenues, + (sub.expense_amount_untaxed_to_invoice + sub.expense_amount_untaxed_invoiced + sub.amount_untaxed_to_invoice + + sub.amount_untaxed_invoiced + sub.other_revenues + sub.timesheet_cost + sub.expense_cost) + as margin + FROM ( + SELECT + ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id, + P.id AS project_id, + P.user_id AS user_id, + SOL.id AS sale_line_id, + P.analytic_account_id AS analytic_account_id, + P.partner_id AS partner_id, + C.id AS company_id, + C.currency_id AS currency_id, + S.id AS sale_order_id, + S.date_order AS order_confirmation_date, + SOL.product_id AS product_id, + SOL.qty_delivered_method AS sale_qty_delivered_method, + CASE + WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) + ELSE 0.0 + END AS expense_amount_untaxed_to_invoice, + CASE + WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no' + THEN + CASE + WHEN T.expense_policy = 'sales_price' + THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced + ELSE -COST_SUMMARY.expense_cost + END + ELSE 0.0 + END AS expense_amount_untaxed_invoiced, + CASE + WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) + ELSE 0.0 + END AS amount_untaxed_to_invoice, + CASE + WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, COST_SUMMARY.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) + ELSE 0.0 + END AS amount_untaxed_invoiced, + COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount, + COST_SUMMARY.timesheet_cost AS timesheet_cost, + COST_SUMMARY.expense_cost AS expense_cost, + COST_SUMMARY.other_revenues AS other_revenues + FROM project_project P + JOIN res_company C ON C.id = P.company_id + LEFT JOIN ( SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - TS.so_line AS sale_line_id, - SUM(TS.unit_amount) AS timesheet_unit_amount, - SUM(TS.amount) AS timesheet_cost, - 0.0 AS other_revenues, - 0.0 AS expense_cost, - 0.0 AS downpayment_invoiced - FROM account_analytic_line TS, project_project P - WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't' - GROUP BY P.id, TS.so_line + project_id, + analytic_account_id, + sale_line_id, + SUM(timesheet_unit_amount) AS timesheet_unit_amount, + SUM(timesheet_cost) AS timesheet_cost, + SUM(expense_cost) AS expense_cost, + SUM(other_revenues) AS other_revenues, + SUM(downpayment_invoiced) AS downpayment_invoiced + FROM ( + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + TS.so_line AS sale_line_id, + SUM(TS.unit_amount) AS timesheet_unit_amount, + SUM(TS.amount) AS timesheet_cost, + 0.0 AS other_revenues, + 0.0 AS expense_cost, + 0.0 AS downpayment_invoiced + FROM account_analytic_line TS, project_project P + WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't' + GROUP BY P.id, TS.so_line - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - AAL.so_line AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - SUM(AAL.amount) AS other_revenues, - 0.0 AS expense_cost, - 0.0 AS downpayment_invoiced - FROM project_project P - LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id - LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id - WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' - GROUP BY P.id, AA.id, AAL.so_line + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + AAL.so_line AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + SUM(AAL.amount) AS other_revenues, + 0.0 AS expense_cost, + 0.0 AS downpayment_invoiced + FROM project_project P + LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id + LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id + WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' + GROUP BY P.id, AA.id, AAL.so_line - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - AAL.so_line AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - 0.0 AS other_revenues, - SUM(AAL.amount) AS expense_cost, - 0.0 AS downpayment_invoiced - FROM project_project P - LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id - LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id - WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' - GROUP BY P.id, AA.id, AAL.so_line + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + AAL.so_line AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + 0.0 AS other_revenues, + SUM(AAL.amount) AS expense_cost, + 0.0 AS downpayment_invoiced + FROM project_project P + LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id + LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id + WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' + GROUP BY P.id, AA.id, AAL.so_line - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - MY_SOLS.id AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - 0.0 AS other_revenues, - 0.0 AS expense_cost, - CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced - FROM project_project P - LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id - LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id - LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id - WHERE MY_SOLS.is_downpayment = 't' - GROUP BY P.id, MY_SOLS.id + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + MY_SOLS.id AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + 0.0 AS other_revenues, + 0.0 AS expense_cost, + CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced + FROM project_project P + LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id + LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id + LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id + WHERE MY_SOLS.is_downpayment = 't' + GROUP BY P.id, MY_SOLS.id - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - OLIS.id AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - 0.0 AS other_revenues, - OLIS.price_reduce AS expense_cost, - 0.0 AS downpayment_invoiced - FROM project_project P - LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id - LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id - LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id - LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id - LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id - WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' - GROUP BY P.id, OLIS.id + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + OLIS.id AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + 0.0 AS other_revenues, + OLIS.price_reduce AS expense_cost, + 0.0 AS downpayment_invoiced + FROM project_project P + LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id + LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id + LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id + LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id + LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id + WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' + GROUP BY P.id, OLIS.id - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - SOL.id AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - 0.0 AS other_revenues, - 0.0 AS expense_cost, - 0.0 AS downpayment_invoiced - FROM sale_order_line SOL - INNER JOIN project_project P ON SOL.project_id = P.id - WHERE P.active = 't' AND P.allow_timesheets = 't' + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + SOL.id AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + 0.0 AS other_revenues, + 0.0 AS expense_cost, + 0.0 AS downpayment_invoiced + FROM sale_order_line SOL + INNER JOIN project_project P ON SOL.project_id = P.id + WHERE P.active = 't' AND P.allow_timesheets = 't' - UNION + UNION - SELECT - P.id AS project_id, - P.analytic_account_id AS analytic_account_id, - SOL.id AS sale_line_id, - 0.0 AS timesheet_unit_amount, - 0.0 AS timesheet_cost, - 0.0 AS other_revenues, - 0.0 AS expense_cost, - 0.0 AS downpayment_invoiced - FROM sale_order_line SOL - INNER JOIN project_task T ON SOL.task_id = T.id - INNER JOIN project_project P ON P.id = T.project_id - WHERE P.active = 't' AND P.allow_timesheets = 't' - ) SUB_COST_SUMMARY - GROUP BY project_id, analytic_account_id, sale_line_id - ) COST_SUMMARY ON COST_SUMMARY.project_id = P.id - LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id - LEFT JOIN sale_order S ON SOL.order_id = S.id - LEFT JOIN product_product PP on (SOL.product_id=PP.id) - LEFT JOIN product_template T on (PP.product_tmpl_id=T.id) - WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL + SELECT + P.id AS project_id, + P.analytic_account_id AS analytic_account_id, + SOL.id AS sale_line_id, + 0.0 AS timesheet_unit_amount, + 0.0 AS timesheet_cost, + 0.0 AS other_revenues, + 0.0 AS expense_cost, + 0.0 AS downpayment_invoiced + FROM sale_order_line SOL + INNER JOIN project_task T ON SOL.task_id = T.id + INNER JOIN project_project P ON P.id = T.project_id + WHERE P.active = 't' AND P.allow_timesheets = 't' + ) SUB_COST_SUMMARY + GROUP BY project_id, analytic_account_id, sale_line_id + ) COST_SUMMARY ON COST_SUMMARY.project_id = P.id + LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id + LEFT JOIN sale_order S ON SOL.order_id = S.id + LEFT JOIN product_product PP on (SOL.product_id=PP.id) + LEFT JOIN product_template T on (PP.product_tmpl_id=T.id) + WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL + ) AS sub ) """ % self._table self._cr.execute(query) diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml b/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml index abf410810c5..1f60f4d7944 100644 --- a/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml +++ b/addons/sale_timesheet/report/project_profitability_report_analysis_views.xml @@ -39,7 +39,7 @@ - + diff --git a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss index 9560403f764..b1c2aa57392 100644 --- a/addons/sale_timesheet/static/src/scss/sale_timesheet.scss +++ b/addons/sale_timesheet/static/src/scss/sale_timesheet.scss @@ -85,6 +85,9 @@ $canceled-color: gray; color: white; cursor: pointer; } + .progress { + border-radius: 3px; + } >.o_timesheet_plan_badge { margin-bottom: 10px; diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index 45fd6bcd408..61051d404e7 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -37,12 +37,27 @@ >
- - - - - - + + + + + + + + + +
+ + + + + + +
+ + + +
@@ -238,38 +253,43 @@ - + + + + - + - -
+ +
+
- - - Billed on Timesheets - billable_time - - - Billed at a Fixed price - billable_fixed - - - No task found - non_billable_project - - - Non billable tasks - non_billable - - - Cancelled - canceled - + + + Billed on Timesheets + billable_time + + + Billed at a Fixed price + billable_fixed + + + No task found + non_billable_project + + + Non billable tasks + non_billable + + + Cancelled + canceled + +
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index d933a9e872e..c57cfac4ca7 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -6,10 +6,13 @@ project.project +
+