diff options
| author | jbm-odoo <jbm@odoo.com> | 2020-08-21 13:57:30 +0000 |
|---|---|---|
| committer | jbm-odoo <jbm@odoo.com> | 2020-09-01 10:00:47 +0000 |
| commit | ed2e18f3c95de49083adf63a83b85253828b6d64 (patch) | |
| tree | 3a6e3d500b3d89ceb124aee72c5505c0559c7102 | |
| parent | 8aee4992778fee5e2f146c4cff8b35e83c7f56b3 (diff) | |
[FIX] sale_timesheet: Billable project
taskID 2323997
closes odoo/odoo#56311
Related: odoo/enterprise#12682
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
10 files changed, 118 insertions, 36 deletions
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index d0932372954..6a4d33209ea 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -658,8 +658,8 @@ <group> <field name="active" invisible="1"/> <field name="partner_id" class="o_task_customer_field"/> - <field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/> <field name="partner_email" widget="email" attrs="{'invisible': [('partner_id', '=', False)]}"/> + <field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/> <field name="legend_blocked" invisible="1"/> <field name="legend_normal" invisible="1"/> <field name="legend_done" invisible="1"/> 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 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='service_type']" position="after"> <field name="service_tracking" widget="radio" attrs="{'invisible': [('type','!=','service')]}"/> - <field name="project_id" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/> - <field name="project_template_id" context="{'active_test': False}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/> + <field name="project_id" context="{'default_allow_billable': True, 'default_bill_type': 'customer_task'}" attrs="{'invisible':[('service_tracking','!=','task_global_project')], 'required':[('service_tracking','==','task_global_project')]}"/> + <field name="project_template_id" context="{'active_test': False, 'default_allow_billable': True, 'default_bill_type': 'customer_project', 'default_pricing_type': 'fixed_rate'}" attrs="{'invisible':[('service_tracking','not in',['task_in_project', 'project_only'])]}"/> </xpath> </field> </record> 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"/> </div> - <xpath expr="//field[@name='partner_email']" position="after"> + <xpath expr="//field[@name='partner_phone']" position="after"> <field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False), '&', ('sale_order_id', '!=', False), ('sale_line_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/> <field name="sale_order_id" attrs="{'invisible': [('project_id', '!=', False)]}" groups="sales_team.group_sale_salesman"/> <field name="commercial_partner_id" invisible="1" /> 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 @@ <field name="arch" type="xml"> <div class="oe_button_box" position="inside"> <button string="Project Overview" class="oe_stat_button" type="object" name="action_view_timesheet" icon="fa-puzzle-piece" attrs="{'invisible': [('allow_billable', '=', False)]}"/> + <button class="d-none d-md-inline oe_stat_button" + type="object" name="action_view_so" icon="fa-dollar" + attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project')]}" + string="Sales Order" + groups="sales_team.group_sale_salesman"/> </div> <xpath expr="//header" position="inside"> <button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman"/> @@ -18,9 +23,13 @@ <field name="display_create_order" invisible="1"/> <field name="bill_type" widget="radio"/> <field name="pricing_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/> - <field name="timesheet_product_id" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service'}"/> - <field name="sale_order_id" attrs="{'invisible': ['|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project')], 'readonly': [('sale_order_id', '!=', False)]}" context="{'default_partner_id': partner_id}"/> - <field name="sale_line_id" attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/> + <div class="o_td_label" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}"> + <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_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/> + <field name="sale_order_id" invisible="1"/> + <field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/> </group> <field name="sale_line_employee_ids" attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}"> <tree editable="top"> @@ -64,7 +73,11 @@ <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"/> - <field name="timesheet_product_id" string="Default Service" attrs="{'invisible': ['|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}"/> + <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> </field> </record> @@ -80,7 +93,7 @@ </xpath> <xpath expr="//div[hasclass('o_kanban_card_manage_pane')]" position="after"> <t t-if="record.warning_employee_rate.raw_value"> - <i class="float-right mx-4 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="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."/> </t> </xpath> <xpath expr="//a[@name='action_view_account_analytic_line']" position="attributes"> @@ -125,15 +138,16 @@ <field name="is_project_map_empty" invisible="1"/> <field name="has_multi_sol" invisible="1"/> </xpath> - <xpath expr="//field[@name='partner_email']" position="after"> + <xpath expr="//field[@name='partner_phone']" position="after"> <field name="bill_type" invisible="1"/> <field name="pricing_type" invisible="1"/> - <field name="timesheet_product_id" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('partner_id', '=', False), ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}" context="{'default_type': 'service'}" placeholder="Leave empty if non-billable"/> + <field name="timesheet_product_id" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&', ('partner_id', '=', False), ('bill_type', '=', 'customer_project'), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}" placeholder="Leave empty if non-billable"/> + <field name="non_allow_billable" attrs="{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('pricing_type', '!=', 'employee_rate'), ('bill_type', '=', 'customer_task')]}"/> </xpath> <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.pricing_type', '=', 'employee_rate'), '&', ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False)]}" 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"/> </xpath> </field> </record> @@ -145,7 +159,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_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)]} </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 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 @@ <field name="line_ids" nolabel="1" attrs="{'required': [('pricing_type', '=', 'employee_rate')]}"> <tree editable="bottom"> <field name="employee_id" options="{'no_create_edit': True, 'no_create': True}" attrs="{'column_invisible': [('parent.pricing_type', '=', 'fixed_rate')], 'required': [('parent.pricing_type', '=', 'employee_rate')]}"/> - <field name="sale_line_id" options="{'no_create_edit': True, 'no_create': True}" domain="[('is_service', '=', True), ('order_id', '=', parent.sale_order_id)]" attrs="{'column_invisible': ['|', ('parent.link_selection', '!=', 'link'), ('parent.pricing_type', '=', 'fixed_rate')], 'required': ['&', ('parent.link_selection', '=', 'link'), ('parent.pricing_type', '=', 'employee_rate')]}"/> + <field name="sale_line_id" options="{'no_create_edit': True, 'no_create': True, 'no_open': True}" domain="[('is_service', '=', True), ('order_id', '=', parent.sale_order_id)]" attrs="{'column_invisible': ['|', ('parent.link_selection', '!=', 'link'), ('parent.pricing_type', '=', 'fixed_rate')], 'required': ['&', ('parent.link_selection', '=', 'link'), ('parent.pricing_type', '=', 'employee_rate')]}"/> <field name="product_id" options="{'no_create_edit': True, 'no_create': True}" attrs="{'column_invisible': [('parent.link_selection', '=', 'link')], 'required': [('parent.link_selection', '!=', 'link')]}"/> <field name="price_unit" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('parent.link_selection', '=', 'link')]}"/> <field name="currency_id" invisible="1"/> </tree> </field> </group> + <group attrs="{'invisible': [('info_invoice', '=', False)]}"> + <field name="info_invoice" nolabel="1"/> + </group> <footer> <button string="Link to Sales Order" type="object" name="action_link_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '!=', 'link')]}"/> <button string="Create Sales Order" type="object" name="action_create_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '=', 'link')]}"/> 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 a88bebf3166..cc9d9f6dcc8 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order.py @@ -50,14 +50,17 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): domain = self.env['sale.order.line']._timesheet_compute_delivered_quantity_domain() timesheet = self.env['account.analytic.line'].read_group(domain + [('task_id', '=', self.task_id.id), ('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' and line.price_unit: - line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order Item.", amount=unit_amount, label=label) + 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 Item.", amount=unit_amount, label=label) + 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 diff --git a/addons/sale_timesheet/wizard/project_task_create_sale_order_views.xml b/addons/sale_timesheet/wizard/project_task_create_sale_order_views.xml index 8f172bb723e..340b71551dd 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order_views.xml +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order_views.xml @@ -24,7 +24,9 @@ <field name="sale_line_id" options="{'no_create': True, 'no_create_edit': True}" attrs="{'required': [('link_selection', '=', 'link')]}"/> <field name="commercial_partner_id" invisible="1"/> </group> - <field name="info_invoice"/> + </group> + <group attrs="{'invisible': [('info_invoice', '=', False)]}"> + <field name="info_invoice" nolabel="1"/> </group> <footer> <button string="Link to Sales Order" type="object" name="action_link_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '!=', 'link')]}"/> |
