# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models from odoo.fields import Domain from odoo.tools import SQL class ProjectProject(models.Model): _inherit = "project.project" allow_material = fields.Boolean("Products on Tasks", compute="_compute_allow_material", store=True, readonly=False) allow_quotations = fields.Boolean( "Extra Quotations", compute="_compute_allow_quotations", inverse="_inverse_allow_quotations", store=True, readonly=False) allow_billable = fields.Boolean( compute='_compute_allow_billable', store=True, readonly=False) sale_line_id = fields.Many2one( compute="_compute_sale_line_id", store=True, readonly=False) hide_price = fields.Boolean("Hide price on customer report and portal", compute="_compute_hide_price", store=True, readonly=False) _material_imply_billable = models.Constraint( "CHECK((allow_material = 't' AND allow_billable = 't') OR (allow_material = 'f'))", "The material can be allowed only when the task can be billed.", ) _fsm_imply_task_rate = models.Constraint( "CHECK((is_fsm = 't' AND sale_line_id IS NULL) OR (is_fsm = 'f'))", "An FSM project must be billed at task rate or employee rate.", ) _timesheet_product_required_if_billable_and_time = models.Constraint( """ CHECK( (allow_billable = 't' AND allow_timesheets = 't' AND is_fsm = 't' AND timesheet_product_id IS NOT NULL) OR (allow_billable IS NOT TRUE) OR (allow_timesheets IS NOT TRUE) OR (is_fsm IS NOT TRUE) OR (allow_billable IS NULL) OR (allow_timesheets IS NULL) OR (is_fsm IS NULL) )""", "The timesheet product is required when the fsm project can be billed and timesheets are allowed.", ) def _get_hide_partner(self): return super()._get_hide_partner() and not self.is_fsm @api.model def default_get(self, fields): defaults = super().default_get(fields) if 'allow_quotations' in fields and 'allow_quotations' not in defaults and defaults.get('is_fsm'): defaults['allow_quotations'] = self.env.user.has_group('industry_fsm.group_fsm_quotation_from_task') return defaults @api.depends('is_fsm', 'allow_material') def _compute_hide_price(self): for project in self: if not project.allow_material or not project.is_fsm: project.hide_price = False @api.depends('is_fsm') def _compute_allow_quotations(self): if not self.env.user.has_group('industry_fsm.group_fsm_quotation_from_task'): self.allow_quotations = False else: for project in self: project.allow_quotations = project.is_fsm @api.depends('is_fsm', 'allow_material') def _compute_allow_billable(self): for project in self: project.allow_billable = project.allow_billable or project.is_fsm or project.allow_material @api.depends('allow_billable', 'is_fsm') def _compute_allow_material(self): for project in self: project.allow_material = project.allow_billable and project.is_fsm def _inverse_allow_quotations(self): self._check_project_group_with_field('allow_quotations', 'industry_fsm.group_fsm_quotation_from_task') @api.ondelete(at_uninstall=False) def _check_project_fsm_group_at_removal(self): self._check_project_group_with_field('allow_quotations', 'industry_fsm.group_fsm_quotation_from_task') def _get_project_features_mapping(self): return { **super()._get_project_features_mapping(), 'allow_quotations': 'industry_fsm.group_fsm_quotation_from_task', } def flush_model(self, fnames=None): if fnames is not None: # force 'allow_billable' and 'allow_material' to be flushed # altogether in order to satisfy the SQL constraint above fnames = set(fnames) if 'allow_billable' in fnames or 'allow_material' in fnames: fnames.add('allow_billable') fnames.add('allow_material') return super().flush_model(fnames) def flush_recordset(self, fnames=None): if fnames is not None: # force 'allow_billable' and 'allow_material' to be flushed # altogether in order to satisfy the SQL constraint above fnames = set(fnames) if 'allow_billable' in fnames or 'allow_material' in fnames: fnames.add('allow_billable') fnames.add('allow_material') return super().flush_recordset(fnames) @api.depends('sale_line_id', 'sale_line_employee_ids', 'allow_billable', 'is_fsm') def _compute_pricing_type(self): fsm_projects = self.filtered(lambda project: project.allow_billable and project.is_fsm) for fsm_project in fsm_projects: if fsm_project.sale_line_employee_ids: fsm_project.update({'pricing_type': 'employee_rate'}) else: fsm_project.update({'pricing_type': 'task_rate'}) super(ProjectProject, self - fsm_projects)._compute_pricing_type() def _search_pricing_type(self, operator, value): if operator != 'in': # let the parent implementation dispatch the search return super()._search_pricing_type(operator, value) values = set(value) domains = [] if 'fixed_rate' in values: values.discard('fixed_rate') domain = super()._search_pricing_type(operator, {'fixed_rate'}) domains.append(Domain.AND([domain, [('is_fsm', operator, [False])]])) if other_rates := {rate for rate in ('task_rate', 'employee_rate') if rate in values}: values.difference_update(other_rates) domain = super()._search_pricing_type(operator, other_rates) fsm_domain = [ ('is_fsm', '=', True), ('allow_billable', '=', True), ] if len(other_rates) == 1: fsm_domain.append(('sale_line_employee_ids', '!=' if 'employee_rate' in other_rates else '=', False)) domains.append(Domain.OR([domain, fsm_domain])) if values: domains.append(super()._search_pricing_type(operator, values)) return Domain.OR(domains) @api.depends('is_fsm') def _compute_sale_line_id(self): # We cannot have a SOL in the fsm project fsm_projects = self.filtered('is_fsm') fsm_projects.update({'sale_line_id': False}) super(ProjectProject, self - fsm_projects)._compute_sale_line_id() @api.depends('sale_line_employee_ids.sale_line_id', 'sale_line_id') def _compute_partner_id(self): basic_projects = self.filtered(lambda project: not project.is_fsm) super(ProjectProject, basic_projects)._compute_partner_id() @api.depends('is_fsm') def _compute_display_sales_stat_buttons(self): fsm_projects = self.filtered('is_fsm') for project in fsm_projects: project.display_sales_stat_buttons = bool(project.sale_order_count) super(ProjectProject, self - fsm_projects)._compute_display_sales_stat_buttons() def _get_profitability_sale_order_items_domain(self, domain=None): quotation_projects = self.filtered('allow_quotations') if quotation_projects: include_additional_sale_orders = [ ('order_id', 'in', quotation_projects._get_additional_quotations([ ('state', '=', 'sale'), ]).ids) ] domain = include_additional_sale_orders \ if domain is None \ else Domain.OR([domain, include_additional_sale_orders]) return super()._get_profitability_sale_order_items_domain(domain) def _get_additional_quotations_query(self, domain=None): domain = Domain(domain or Domain.TRUE) & Domain('task_id', '!=', False) SaleOrder = self.env['sale.order'] query = SaleOrder._search(domain) task_alias = query.make_alias(SaleOrder._table, 'task_id') query.add_join("JOIN", task_alias, 'project_task', SQL( "%s = %s AND %s IN %s", SQL.identifier(SaleOrder._table, 'task_id'), SQL.identifier(task_alias, 'id'), SQL.identifier(task_alias, 'project_id'), tuple(self.ids), )) return query def _get_additional_quotations(self, domain=None): return self.env['sale.order'].browse(self._get_additional_quotations_query(domain)) def _get_sale_order_items_query(self, domain_per_model=None): basic_project_domain = [('is_fsm', '=', False)] # when the project is a fsm one, no SOL is linked to that project. employee_mapping_domain = [('project_id.is_fsm', '=', False)] if domain_per_model is None: domain_per_model = { 'project.project': basic_project_domain, 'project.sale.line.employee.map': employee_mapping_domain, } else: domain_per_model['project.project'] = Domain.AND([ domain_per_model.get('project.project', []), basic_project_domain, ]) domain_per_model['project.sale.line.employee.map'] = Domain.AND([ domain_per_model.get('project.sale.line.employee.map', []), employee_mapping_domain, ]) return super()._get_sale_order_items_query(domain_per_model) def _update_action_context(self, action): if self.is_fsm: action['context'].update({'create': False}) return action def action_view_sols(self): return self._update_action_context(super().action_view_sols()) def action_view_sos(self): return self._update_action_context(super().action_view_sos())