222 lines
9.8 KiB
Python
222 lines
9.8 KiB
Python
# 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())
|