diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 3023fdcea59..68a64a7fbde 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -30,25 +30,32 @@ class AccountAnalyticLine(models.Model): @api.model def create(self, vals): - vals = self._update_timesheet_values(vals) - if not vals.get('employee_id') and vals.get('project_id'): # compute employee only for timesheet lines, makes no sense for other lines - if vals.get('user_id'): - ts_user_id = vals['user_id'] - else: - ts_user_id = self.env.user.id - vals['employee_id'] = self.env['hr.employee'].search([('user_id', '=', ts_user_id)], limit=1).id + vals = self._timesheet_preprocess(vals) return super(AccountAnalyticLine, self).create(vals) @api.multi def write(self, vals): - vals = self._update_timesheet_values(vals) + vals = self._timesheet_preprocess(vals) return super(AccountAnalyticLine, self).write(vals) - def _update_timesheet_values(self, vals): - if vals.get('project_id'): + def _timesheet_preprocess(self, vals): + """ Deduce other field values from the one given. + Overrride this to compute on the fly some field that can not be computed fields. + :param values: dict values for `create`or `write`. + """ + # project implies analytic account + if vals.get('project_id') and not vals.get('account_id'): project = self.env['project.project'].browse(vals.get('project_id')) vals['account_id'] = project.analytic_account_id.id - if vals.get('employee_id'): + # employee implies user + if vals.get('employee_id') and not vals.get('user_id'): employee = self.env['hr.employee'].browse(vals['employee_id']) vals['user_id'] = employee.user_id.id + # compute employee only for timesheet lines, makes no sense for other lines + if not vals.get('employee_id') and vals.get('project_id'): + if vals.get('user_id'): + ts_user_id = vals['user_id'] + else: + ts_user_id = self._default_user() + vals['employee_id'] = self.env['hr.employee'].search([('user_id', '=', ts_user_id)], limit=1).id return vals diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index d6da4910a36..96f4aa23bd9 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -17,67 +17,55 @@ class AccountAnalyticLine(models.Model): @api.model def create(self, values): - if values.get('task_id'): - task = self.env['project.task'].browse(values['task_id']) - values['so_line'] = task.sale_line_id.id or values.get('so_line', False) - values.update(self._get_timesheet_values(values)) - values.update(self._get_timesheet_billing_values(values)) - return super(AccountAnalyticLine, self).create(values) + result = super(AccountAnalyticLine, self).create(values) + # applied only for timesheet + if result.project_id: + result._timesheet_postprocess(values) + return result @api.multi def write(self, values): # prevent to update invoiced timesheets if one line is of type delivery if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id): if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]): - raise UserError(_('You can not modify already invoiced timesheets.')) - - if values.get('task_id'): - task = self.env['project.task'].browse(values['task_id']) - values['so_line'] = task.sale_line_id.id or values.get('so_line', False) - for line in self: - values.update(line._get_timesheet_values(values)) - values.update(line._get_timesheet_billing_values(values)) - super(AccountAnalyticLine, line).write(values) - - return True - - def _get_timesheet_values(self, values): - result = {} - values = values if values is not None else {} - if values.get('project_id') or self.project_id: - if values.get('amount'): - return {} - unit_amount = values.get('unit_amount', 0.0) or self.unit_amount - - # find the employee - employee_id = values.get('employee_id') or self.employee_id.id or (self.env.user.employee_ids and self.env.user.employee_ids[0].id or False) - employee = self.env['hr.employee'].browse(employee_id) if employee_id else self.env['hr.employee'] - if not employee: - user_id = values.get('user_id') or self.user_id.id or self._default_user() - employee = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1) - - # find the analytic account - account_id = values.get('account_id') or self.account_id.id - if not account_id and values.get('project_id'): - project = self.env['project.project'].browse(values['project_id']) - account_id = project.analytic_account_id.id - analytic_account = self.env['account.analytic.account'].browse(account_id) if account_id else self.env['account.analytic.account'] - - # convert employee cost into timesheet (analytic account) currency - uom = employee.company_id.project_time_mode_id - cost = employee.timesheet_cost or 0.0 - amount = -unit_amount * cost - amount_converted = employee.currency_id.compute(amount, analytic_account.currency_id) - - result.update({ - 'amount': amount_converted, - 'product_uom_id': uom.id, - 'account_id': account_id, - }) + raise UserError(_('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).')) + result = super(AccountAnalyticLine, self).write(values) + # applied only for timesheet + self.filtered(lambda t: t.project_id)._timesheet_postprocess(values) return result - def _get_timesheet_billing_values(self, values): - """ + @api.model + def _timesheet_preprocess(self, values): + values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) + # task implies so line + if 'task_id' in values: + task = self.env['project.task'].sudo().browse(values['task_id']) + values['so_line'] = task.sale_line_id.id or values.get('so_line', False) + return values + + @api.multi + def _timesheet_postprocess(self, values): + sudo_self = self.sudo() # this creates only one env for all operation that required sudo() + # (re)compute the amount (depending on unit_amount, employee_id for the cost, and account_id for currency) + if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'account_id']]): + for timesheet in sudo_self: + uom = timesheet.employee_id.company_id.project_time_mode_id + cost = timesheet.employee_id.timesheet_cost or 0.0 + amount = -timesheet.unit_amount * cost + amount_converted = timesheet.employee_id.currency_id.compute(amount, timesheet.account_id.currency_id) + timesheet.write({ + 'amount': amount_converted, + 'product_uom_id': uom.id, + }) + # (re)compute the theorical revenue + if any([field_name in values for field_name in ['so_line', 'unit_amount', 'account_id']]): + sudo_self._timesheet_compute_theorical_revenue() + return values + + @api.multi + def _timesheet_compute_theorical_revenue(self): + """ This method set the theorical revenue on the current timesheet lines. + If invoice on delivered quantity: timesheet hours * (SO Line Price) * (1- discount), elif invoice on ordered quantities & create task: @@ -88,53 +76,50 @@ class AccountAnalyticLine(models.Model): else: 0 """ - result = {} - if self.env.context.get('create'): # avoid bad loop - return result + for timesheet in self: + # find the timesheet UoM + timesheet_uom = timesheet.product_uom_id + if not timesheet_uom: # fallback on default company timesheet UoM + timesheet_uom = self.env.user.company_id.project_time_mode_id - # find the timesheet UoM - timesheet_uom = self.product_uom_id - if values.get('product_uom_id'): - timesheet_uom = self.env['product.uom'].browse(values['product_uom_id']) - if not timesheet_uom: # fallback on default company timesheet UoM - timesheet_uom = self.env.user.company_id.project_time_mode_id + # default values + billable_type = 'non_billable' + unit_amount = timesheet.unit_amount + so_line = timesheet.so_line + revenue = 0.0 + billable_type = 'non_billable' - unit_amount = values.get('unit_amount', 0.0) or self.unit_amount - billable_type = 'non_billable' - revenue = 0.0 + # set the revenue and billable type according to the product and the SO line + if so_line.product_id.type == 'service' and so_line.product_id.service_type == 'timesheet': + # find the analytic account to convert revenue into its currency + analytic_account = timesheet.account_id + # convert the unit of mesure into hours + sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom) + sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency - # set the revenue and billable type according to the product and the SO line - so_line_id = values.get('so_line') or self.so_line.id - so_line = self.env['sale.order.line'].browse(so_line_id).sudo() if so_line_id else self.env['sale.order.line'].browse().sudo() - if so_line.product_id.type == 'service': - # find the analytic account to convert revenue into its currency - account_id = values.get('account_id') or self.account_id.id - analytic_account = self.env['account.analytic.account'].browse(account_id) - # convert the unit of mesure into hours - sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom) - sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency - # calculate the revenue on the timesheet - if so_line.product_id.invoice_policy == 'delivery': - revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100))) - billable_type = 'billable_time' - elif so_line.product_id.invoice_policy == 'order': - quantity_hour = unit_amount - if so_line.product_uom.category_id == timesheet_uom.category_id: + # calculate the revenue on the timesheet + if so_line.product_id.invoice_policy == 'delivery': + revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100))) + billable_type = 'billable_time' + elif so_line.product_id.invoice_policy == 'order': quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom) - # compute the total revenue the SO since we are in fixed price - total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100))) - # compute the total revenue already existing (without the current timesheet line) - domain = [('so_line', '=', so_line.id)] - if self.ids: - domain += [('id', 'not in', self.ids)] - analytic_lines = self.sudo().search(domain) - total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue')) - # compute (new) revenue of current timesheet line - revenue = min( - analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-(so_line.discount/100))), - total_revenue_so - total_revenue_invoiced - ) - billable_type = 'billable_fixed' - result['timesheet_revenue'] = revenue - result['timesheet_invoice_type'] = billable_type - return result + # compute the total revenue the SO since we are in fixed price + total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100))) + # compute the total revenue already existing (without the current timesheet line) + domain = [('so_line', '=', so_line.id)] + if timesheet.ids: + domain += [('id', 'not in', timesheet.ids)] + analytic_lines = timesheet.search(domain) + total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue')) + # compute (new) revenue of current timesheet line + revenue = min( + analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)), + total_revenue_so - total_revenue_invoiced + ) + billable_type = 'billable_fixed' + + timesheet.write({ + 'timesheet_revenue': revenue, + 'timesheet_invoice_type': billable_type, + }) + return True diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py index 27dcd59243c..c4eb3d46c38 100644 --- a/addons/sale_timesheet/models/account_invoice.py +++ b/addons/sale_timesheet/models/account_invoice.py @@ -59,7 +59,7 @@ class AccountInvoice(models.Model): ('project_id', '!=', False), ('timesheet_invoice_id', '=', False), ('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed']) - ]).with_context(create=True) # context key required to avoid loop + ]) # NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs) if uninvoiced_timesheet_lines: