summaryrefslogtreecommitdiff
path: root/addons/sale_timesheet/models/account.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_timesheet/models/account.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sale_timesheet/models/account.py')
-rw-r--r--addons/sale_timesheet/models/account.py155
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()