diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_timesheet/models/account.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/sale_timesheet/models/account.py')
| -rw-r--r-- | addons/sale_timesheet/models/account.py | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py new file mode 100644 index 00000000..df56dbd2 --- /dev/null +++ b/addons/sale_timesheet/models/account.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.exceptions import UserError, ValidationError + +from odoo import api, fields, models, _ +from odoo.osv import expression + + +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + def _default_sale_line_domain(self): + domain = super(AccountAnalyticLine, self)._default_sale_line_domain() + return expression.OR([domain, [('qty_delivered_method', '=', 'timesheet')]]) + + timesheet_invoice_type = fields.Selection([ + ('billable_time', 'Billed on Timesheets'), + ('billable_fixed', 'Billed at a Fixed price'), + ('non_billable', 'Non Billable Tasks'), + ('non_billable_timesheet', 'Non Billable Timesheet'), + ('non_billable_project', 'No task found')], string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) + 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.") + so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False) + + # TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to bill_type and pricing_type of project (See in master) + @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' + 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': + if timesheet.so_line.product_id.invoice_policy == 'delivery': + if timesheet.so_line.product_id.service_type == 'timesheet': + invoice_type = 'billable_time' + else: + invoice_type = 'billable_fixed' + elif timesheet.so_line.product_id.invoice_policy == 'order': + invoice_type = 'billable_fixed' + timesheet.timesheet_invoice_type = invoice_type + else: + timesheet.timesheet_invoice_type = False + + @api.onchange('employee_id') + def _onchange_task_id_employee_id(self): + if self.project_id and self.task_id.allow_billable: # timesheet only + if self.task_id.bill_type == 'customer_task' or self.task_id.pricing_type == 'fixed_rate': + self.so_line = self.task_id.sale_line_id + elif self.task_id.pricing_type == 'employee_rate': + self.so_line = self._timesheet_determine_sale_line(self.task_id, self.employee_id, self.project_id) + else: + self.so_line = False + + @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id', 'project_id.allow_billable') + def _compute_so_line(self): + for timesheet in self._get_not_billed(): # Get only the timesheets are not yet invoiced + timesheet.so_line = timesheet.project_id.allow_billable and timesheet._timesheet_determine_sale_line(timesheet.task_id, timesheet.employee_id, timesheet.project_id) + + def _get_not_billed(self): + return self.filtered(lambda t: not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') + + def _check_timesheet_can_be_billed(self): + return self.so_line in self.project_id.mapped('sale_line_employee_ids.sale_line_id') | self.task_id.sale_line_id | self.project_id.sale_line_id + + @api.constrains('so_line', 'project_id') + def _check_sale_line_in_project_map(self): + if not all(t._check_timesheet_can_be_billed() for t in self._get_not_billed().filtered(lambda t: t.project_id and t.so_line)): + raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) + + def write(self, values): + # prevent to update invoiced timesheets if one line is of type delivery + self._check_can_write(values) + result = super(AccountAnalyticLine, self).write(values) + return result + + def _check_can_write(self, values): + if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda t: t.timesheet_invoice_id and t.timesheet_invoice_id.state != 'cancel'): + if any(field_name in values for field_name in ['unit_amount', 'employee_id', 'project_id', 'task_id', 'so_line', 'amount', 'date']): + raise UserError(_('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).')) + + @api.model + def _timesheet_preprocess(self, values): + if values.get('task_id') and not values.get('account_id'): + task = self.env['project.task'].browse(values.get('task_id')) + if task.analytic_account_id: + values['account_id'] = task.analytic_account_id.id + values['company_id'] = task.analytic_account_id.company_id.id + values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) + return values + + @api.model + def _timesheet_determine_sale_line(self, task, employee, project): + """ Deduce the SO line associated to the timesheet line: + 1/ timesheet on task rate: the so line will be the one from the task + 2/ timesheet on employee rate task: find the SO line in the map of the project (even for subtask), or fallback on the SO line of the task, or fallback + on the one on the project + """ + if not task: + if project.bill_type == 'customer_project' and project.pricing_type == 'employee_rate': + map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', project.id), ('employee_id', '=', employee.id)]) + if map_entry: + return map_entry.sale_line_id + if project.sale_line_id: + return project.sale_line_id + if task.allow_billable and task.sale_line_id: + if task.bill_type == 'customer_task': + return task.sale_line_id + if task.pricing_type == 'fixed_rate': + return task.sale_line_id + elif task.pricing_type == 'employee_rate' and not task.non_allow_billable: + map_entry = project.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == employee) + if map_entry: + return map_entry.sale_line_id + if task.sale_line_id or project.sale_line_id: + return task.sale_line_id or project.sale_line_id + return self.env['sale.order.line'] + + def _timesheet_get_portal_domain(self): + """ Only the timesheets with a product invoiced on delivered quantity are concerned. + since in ordered quantity, the timesheet quantity is not invoiced, + thus there is no meaning of showing invoice with ordered quantity. + """ + domain = super(AccountAnalyticLine, self)._timesheet_get_portal_domain() + return expression.AND([domain, [('timesheet_invoice_type', 'in', ['billable_time', 'non_billable', 'billable_fixed'])]]) + + @api.model + def _timesheet_get_sale_domain(self, order_lines_ids, invoice_ids): + if not invoice_ids: + return [('so_line', 'in', order_lines_ids.ids)] + + return [ + '|', + '&', + ('timesheet_invoice_id', 'in', invoice_ids.ids), + # TODO : Master: Check if non_billable should be removed ? + ('timesheet_invoice_type', 'in', ['billable_time', 'non_billable']), + '&', + ('timesheet_invoice_type', '=', 'billable_fixed'), + ('so_line', 'in', order_lines_ids.ids) + ] + + def _get_timesheets_to_merge(self): + res = super(AccountAnalyticLine, self)._get_timesheets_to_merge() + return res.filtered(lambda l: not l.timesheet_invoice_id or l.timesheet_invoice_id.state != 'posted') + + def unlink(self): + if any(line.timesheet_invoice_id and line.timesheet_invoice_id.state == 'posted' for line in self): + raise UserError(_('You cannot remove a timesheet that has already been invoiced.')) + return super(AccountAnalyticLine, self).unlink() |
