diff options
| author | jbm-odoo <jbm@odoo.com> | 2020-09-15 14:44:03 +0000 |
|---|---|---|
| committer | jbm-odoo <jbm@odoo.com> | 2020-09-18 14:21:46 +0000 |
| commit | 81b6e13cc6d2065c9274a50719acfeebe0d7cf99 (patch) | |
| tree | 1429e90ac2dd9d7320808ca9770160e23937bd19 | |
| parent | 255eb91dd5e9fb39a7625064d1ba8e0b492bfb85 (diff) | |
[FIX] sale_timesheet: Billable project
taskId 2338803
closes odoo/odoo#57761
Related: odoo/enterprise#13241
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
6 files changed, 23 insertions, 25 deletions
diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index ffeeae5ffa4..3a2a2866f92 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -147,7 +147,7 @@ class SaleOrderLine(models.Model): # changing the ordered quantity should change the planned hours on the # task, whatever the SO state. It will be blocked by the super in case # of a locked sale order. - if 'product_uom_qty' in values: + if 'product_uom_qty' in values and not self.env.context.get('no_update_planned_hours', False): for line in self: if line.task_id: planned_hours = line._convert_qty_company_hours(line.task_id.company_id) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 229594df6e1..92bf39ea41c 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -83,7 +83,7 @@ class Project(models.Model): 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') 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) + employees = self.env['account.analytic.line'].read_group([('task_id', 'in', tasks.ids), ('non_allow_billable', '=', False)], ['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]] @@ -225,7 +225,7 @@ class ProjectTask(models.Model): for task in self: show = True if not task.allow_billable or not task.allow_timesheets or \ - (task.bill_type != 'customer_task' and not task.timesheet_product_id) or not task.partner_id or \ + (task.bill_type != 'customer_task' and not task.timesheet_product_id) or (not task.partner_id and task.bill_type != 'customer_task') or \ task.sale_order_id or (task.bill_type != 'customer_task' and task.pricing_type != 'employee_rate'): show = False task.display_create_order = show @@ -321,10 +321,12 @@ class ProjectTask(models.Model): ) if values['non_allow_billable']: timesheet_ids.write({'so_line': False}) + self.sale_line_id = 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) + current_timesheet_ids.task_id.update({'sale_line_id': project.sale_line_id.id}) for employee in current_timesheet_ids.employee_id: current_timesheet_ids.filtered(lambda t: t.employee_id == employee).write({'project_id': project.id}) diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index 9570595ca30..50ab8abbdac 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -239,8 +239,8 @@ class TestProjectBilling(TestCommonSaleTimesheet): self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project") self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project") self.assertEqual(line1.price_unit, 15, "The unit price of SOL 1 should be 15") - self.assertEqual(line1.product_uom_qty, 0, "The ordered qty of SOL 1 should be one") - self.assertEqual(line2.product_uom_qty, 0, "The ordered qty of SOL 1 should be one") + self.assertEqual(line1.product_uom_qty, 3, "The ordered qty of SOL 1 should be 3") + self.assertEqual(line2.product_uom_qty, 2, "The ordered qty of SOL 2 should be 2") self.assertEqual(self.project_non_billable.sale_line_employee_ids.mapped('sale_line_id'), sale_order.order_line, "The SO lines of the map should be the same of the sales order") self.assertEqual(timesheet1.so_line, line1, "Timesheet1 should be linked to sale line 1, as employee manager create the timesheet") diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index ed0df2ab2f3..d59c3fb37b1 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -73,11 +73,7 @@ <field name="bill_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('allow_timesheets', '=', False)], 'required': ['&', ('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': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/> - <div class="o_td_label" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&', ('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), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/> + <field name="timesheet_product_id" string="Service" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/> </field> </field> </record> @@ -91,9 +87,9 @@ <field name="allow_billable"/> <field name="warning_employee_rate"/> </xpath> - <xpath expr="//div[hasclass('o_kanban_card_manage_pane')]" position="after"> + <xpath expr="//div[hasclass('o_primary')]//span" position="inside"> <t t-if="record.warning_employee_rate.raw_value"> - <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."/> + <i class="ml-2 fa fa-exclamation-triangle text-danger small" 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"> @@ -147,7 +143,7 @@ <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.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&', '&', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate'), ('parent.non_allow_billable', '=', True)]}" optional="hide"/> + <field name="non_allow_billable" attrs="{'column_invisible': ['|', '|', ('parent.allow_billable', '=', False), '&', '&', '|', ('parent.bill_type', '!=', 'customer_project'), ('parent.pricing_type', '!=', 'employee_rate'), ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&', '&', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate'), ('parent.non_allow_billable', '=', True)]}" optional="hide"/> </xpath> </field> </record> @@ -159,7 +155,7 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='sale_line_id']" position="attributes"> <attribute name="attrs"> - {'invisible': ['|', '|', '&', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_task'), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} + {'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} </attribute> <attribute name="placeholder">Leave empty if non-billable</attribute> </xpath> diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index 6a5ce482428..a9f20e45ecc 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -69,7 +69,7 @@ class ProjectCreateSalesOrder(models.TransientModel): 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 + unit_amount = round(sum(t.get('unit_amount', 0) for t in timesheet), 2) if timesheet else 0 if not unit_amount: line.info_invoice = False continue @@ -79,10 +79,8 @@ class ProjectCreateSalesOrder(models.TransientModel): 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 + line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order.", amount=unit_amount, label=label) @api.onchange('partner_id') def _onchange_partner_id(self): @@ -224,6 +222,9 @@ class ProjectCreateSalesOrder(models.TransientModel): self.env['account.analytic.line'].search([('task_id', 'in', task_ids.ids), ('so_line', '=', False)]).write({ 'so_line': sale_order_line.id }) + sale_order_line.with_context({'no_update_planned_hours': True}).write({ + 'product_uom_qty': sale_order_line.qty_delivered + }) # link the project to the SO line self.project_id.write({ @@ -298,6 +299,9 @@ class ProjectCreateSalesOrder(models.TransientModel): 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 }) + map_entry.sale_line_id.with_context({'no_update_planned_hours': True}).write({ + 'product_uom_qty': map_entry.sale_line_id.qty_delivered + }) return map_entries diff --git a/addons/sale_timesheet/wizard/project_task_create_sale_order.py b/addons/sale_timesheet/wizard/project_task_create_sale_order.py index cc9d9f6dcc8..02dbda23ab7 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order.py @@ -30,7 +30,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): link_selection = fields.Selection([('create', 'Create a new sales order'), ('link', 'Link to an existing sales order')], required=True, default='create') task_id = fields.Many2one('project.task', "Task", domain=[('sale_line_id', '=', False)], help="Task for which we are creating a sales order", required=True) - partner_id = fields.Many2one('res.partner', string="Customer", help="Customer of the sales order") + partner_id = fields.Many2one('res.partner', string="Customer", help="Customer of the sales 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. The existing timesheet will be linked to this product.", required=True) price_unit = fields.Float("Unit Price", help="Unit price of the sales order item.") currency_id = fields.Many2one('res.currency', string="Currency", related='product_id.currency_id', readonly=False) @@ -59,10 +59,8 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): 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.", 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.", amount=unit_amount, label=label) else: - line.info_invoice = False + line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order.", amount=unit_amount, label=label) @api.onchange('product_id') def _onchange_product_id(self): @@ -111,8 +109,6 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): if self.task_id.sale_line_id: raise UserError(_("The task is already linked to a sales order item.")) - timesheet_with_so_line = self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '!=', False), ('project_id', '!=', False)]) - # create SO sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_id.id, @@ -131,7 +127,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): 'price_unit': self.price_unit, 'project_id': self.task_id.project_id.id, # prevent to re-create a project on confirmation 'task_id': self.task_id.id, - 'product_uom_qty': self.task_id.total_hours_spent - round(sum(timesheet_with_so_line.mapped('unit_amount')), 2), + 'product_uom_qty': round(sum(self.task_id.timesheet_ids.filtered(lambda t: not t.non_allow_billable and not t.so_line).mapped('unit_amount')), 2), }) # link task to SOL |
