diff --git a/bemade_fsm/models/__init__.py b/bemade_fsm/models/__init__.py
index 3cb9595..aef9e8c 100644
--- a/bemade_fsm/models/__init__.py
+++ b/bemade_fsm/models/__init__.py
@@ -1,7 +1,10 @@
from . import task_template
from . import product_template
+from . import sale_order_line
from . import sale_order
from . import equipment
+from . import equipment_type
+from . import equipment_tag
from . import task
from . import res_partner
from . import fsm_visit
diff --git a/bemade_fsm/models/equipment.py b/bemade_fsm/models/equipment.py
index 9eb3160..dc08495 100644
--- a/bemade_fsm/models/equipment.py
+++ b/bemade_fsm/models/equipment.py
@@ -1,26 +1,6 @@
from odoo import api, fields, models
-class EquipmentTag(models.Model):
- _name = "bemade_fsm.equipment.tag"
- _description = 'Field service equipment category'
-
- name = fields.Char('Name', required=True, translate=True)
- color = fields.Integer('Color Index', default=10)
-
- _sql_constraints = [
- ('name_uniq', 'unique (name)', "Tag name already exists !"),
- ]
-
-
-class EquipmentType(models.Model):
- _name = 'bemade_fsm.equipment.type'
- _description = 'Field service equipment type'
- _order = 'id'
-
- name = fields.Char(string='Intervention Name', required=True, translate=True)
-
-
class Equipment(models.Model):
_name = 'bemade_fsm.equipment'
_rec_name = 'complete_name'
@@ -33,26 +13,30 @@ class Equipment(models.Model):
complete_name = fields.Char(string="Equipment Name", compute="_compute_complete_name", store=True)
- tag_ids = fields.Many2many('bemade_fsm.equipment.tag',
- string='Application',
- help="Classify and analyze your equipment categories like: Boiler, Laboratory, "
- "Waste water, Pure water")
+ tag_ids = fields.Many2many(
+ comodel_name='bemade_fsm.equipment.tag',
+ string='Application',
+ help="Classify and analyze your equipment categories like: Boiler, Laboratory, Waste water, Pure water"
+ )
- description = fields.Text(string="Description",
- tracking=True)
+ description = fields.Text(string="Description", tracking=True)
- partner_location_id = fields.Many2one('res.partner',
- string="Physical Address",
- tracking=True,
- ondelete='cascade')
+ partner_location_id = fields.Many2one(
+ comodel_name='res.partner',
+ string="Physical Address",
+ tracking=True,
+ ondelete='cascade'
+ )
- location_notes = fields.Text(string="Physical Location Notes",
- tracking=True)
- task_ids = fields.Many2many(comodel_name='project.task',
- relation="bemade_fsm_task_equipment_rel",
- column1="equipment_id",
- column2="task_id",
- string='Interventions')
+ location_notes = fields.Text(string="Physical Location Notes", tracking=True)
+
+ task_ids = fields.Many2many(
+ comodel_name='project.task',
+ relation="bemade_fsm_task_equipment_rel",
+ column1="equipment_id",
+ column2="task_id",
+ string='Interventions'
+ )
@api.depends('partner_location_id')
def _compute_partner(self):
diff --git a/bemade_fsm/models/equipment_tag.py b/bemade_fsm/models/equipment_tag.py
new file mode 100644
index 0000000..3f9a82f
--- /dev/null
+++ b/bemade_fsm/models/equipment_tag.py
@@ -0,0 +1,13 @@
+from odoo import api, fields, models
+
+
+class EquipmentTag(models.Model):
+ _name = "bemade_fsm.equipment.tag"
+ _description = 'Field service equipment category'
+
+ name = fields.Char('Name', required=True, translate=True)
+ color = fields.Integer('Color Index', default=10)
+
+ _sql_constraints = [
+ ('name_uniq', 'unique (name)', "Tag name already exists !"),
+ ]
diff --git a/bemade_fsm/models/equipment_type.py b/bemade_fsm/models/equipment_type.py
new file mode 100644
index 0000000..8cd6623
--- /dev/null
+++ b/bemade_fsm/models/equipment_type.py
@@ -0,0 +1,9 @@
+from odoo import api, fields, models
+
+
+class EquipmentType(models.Model):
+ _name = 'bemade_fsm.equipment.type'
+ _description = 'Field service equipment type'
+ _order = 'id'
+
+ name = fields.Char(string='Intervention Name', required=True, translate=True)
diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py
index 319e72a..988d556 100644
--- a/bemade_fsm/models/fsm_visit.py
+++ b/bemade_fsm/models/fsm_visit.py
@@ -5,31 +5,43 @@ class FSMVisit(models.Model):
_name = "bemade_fsm.visit"
_description = 'Represents a single visit by assigned service personnel.'
- label = fields.Text(string="Label",
- required=True,
- related='so_section_id.name',
- readonly=False)
+ label = fields.Text(string="Label", required=True, related='so_section_id.name', readonly=False)
+
approx_date = fields.Date(string='Approximate Date')
- so_section_id = fields.Many2one(comodel_name="sale.order.line",
- string="Sale Order Section",
- help="The section on the sale order that represents the labour and parts for "
- "this visit",
- ondelete="cascade")
- sale_order_id = fields.Many2one(comodel_name="sale.order",
- string="Sales Order",
- required=True)
- is_completed = fields.Boolean(string="Completed",
- related="so_section_id.is_fully_delivered")
- is_invoiced = fields.Boolean(string="Invoiced",
- related="so_section_id.is_fully_delivered_and_invoiced")
- summarized_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
- string="Equipment to Service",
- compute="_compute_summarized_equipment_ids")
- task_id = fields.Many2one(comodel_name="project.task",
- compute="_compute_task_id",
- string="Service Visit",)
- task_ids = fields.One2many(comodel_name="project.task",
- inverse_name="visit_id")
+
+ so_section_id = fields.Many2one(
+ comodel_name="sale.order.line",
+ string="Sale Order Section",
+ help="The section on the sale order that represents the labour and parts for this visit",
+ ondelete="cascade"
+ )
+
+ sale_order_id = fields.Many2one(
+ comodel_name="sale.order",
+ string="Sales Order",
+ required=True
+ )
+
+ is_completed = fields.Boolean(string="Completed", related="so_section_id.is_fully_delivered")
+
+ is_invoiced = fields.Boolean(string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced")
+
+ summarized_equipment_ids = fields.Many2many(
+ comodel_name="bemade_fsm.equipment",
+ string="Equipment to Service",
+ compute="_compute_summarized_equipment_ids"
+ )
+
+ task_id = fields.Many2one(
+ comodel_name="project.task",
+ compute="_compute_task_id",
+ string="Service Visit",
+ )
+
+ task_ids = fields.One2many(
+ comodel_name="project.task",
+ inverse_name="visit_id"
+ )
@api.depends('task_ids')
def _compute_task_id(self):
diff --git a/bemade_fsm/models/product_template.py b/bemade_fsm/models/product_template.py
index d3df23f..9ea7dbb 100644
--- a/bemade_fsm/models/product_template.py
+++ b/bemade_fsm/models/product_template.py
@@ -4,7 +4,13 @@ from odoo import fields, models, api
class ProductTemplate(models.Model):
_inherit = 'product.template'
- task_template_id = fields.Many2one("project.task.template", string="Task Template", ondelete='restrict')
- is_field_service = fields.Boolean("Plan as field service",
- help="Products planned as field service will have travel time "
- "considered in planning.")
\ No newline at end of file
+ task_template_id = fields.Many2one(
+ comodel_name="project.task.template",
+ string="Task Template",
+ ondelete='restrict'
+ )
+
+ is_field_service = fields.Boolean(
+ string="Plan as field service",
+ help="Products planned as field service will have travel time considered in planning."
+ )
diff --git a/bemade_fsm/models/res_partner.py b/bemade_fsm/models/res_partner.py
index 3c5427e..2baa4b2 100644
--- a/bemade_fsm/models/res_partner.py
+++ b/bemade_fsm/models/res_partner.py
@@ -4,45 +4,60 @@ from odoo import api, fields, models, Command
class Partner(models.Model):
_inherit = 'res.partner'
- equipment_count = fields.Integer(compute='_compute_equipment_count',
- string='Equipment Count')
+ equipment_count = fields.Integer(compute='_compute_equipment_count', string='Equipment Count')
- owned_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
- compute="_compute_owned_equipment_ids",
- string="Owned Equipments")
+ owned_equipment_ids = fields.One2many(
+ comodel_name="bemade_fsm.equipment",
+ compute="_compute_owned_equipment_ids",
+ string="Owned Equipments"
+ )
- equipment_ids = fields.One2many(comodel_name='bemade_fsm.equipment',
- inverse_name='partner_location_id',
- string='Site Equipment')
+ equipment_ids = fields.One2many(
+ comodel_name='bemade_fsm.equipment',
+ inverse_name='partner_location_id',
+ string='Site Equipment'
+ )
- is_site_contact = fields.Boolean(string='Is a site contact',
- compute="_compute_is_site_contact",
- search="_search_is_site_contact", )
+ is_site_contact = fields.Boolean(
+ string='Is a site contact',
+ compute="_compute_is_site_contact",
+ search="_search_is_site_contact",
+ )
- site_ids = fields.Many2many(string='Work Sites',
- comodel_name='res.partner',
- relation='res_partner_site_contact_rel',
- column1='site_contact_id',
- column2='site_id',
- tracking=True)
+ site_ids = fields.Many2many(
+ string='Work Sites',
+ comodel_name='res.partner',
+ relation='res_partner_site_contact_rel',
+ column1='site_contact_id',
+ column2='site_id',
+ tracking=True
+ )
- site_contacts = fields.Many2many(string='Site Contacts',
- comodel_name='res.partner',
- relation='res_partner_site_contact_rel',
- column1='site_id',
- column2='site_contact_id',
- domain=[('is_company', '=', False)],
- tracking=True)
+ site_contacts = fields.Many2many(
+ string='Site Contacts',
+ comodel_name='res.partner',
+ relation='res_partner_site_contact_rel',
+ column1='site_id',
+ column2='site_contact_id',
+ domain=[('is_company', '=', False)],
+ tracking=True
+ )
- work_order_contacts = fields.Many2many(string='Work Order Recipients',
- comodel_name='res.partner',
- relation='res_partner_work_order_contacts_rel',
- column1='res_partner_id',
- column2='work_order_contact_id',
- domain=[('is_company', '=', False)],
- tracking=True)
+ work_order_contacts = fields.Many2many(
+ string='Work Order Recipients',
+ comodel_name='res.partner',
+ relation='res_partner_work_order_contacts_rel',
+ column1='res_partner_id',
+ column2='work_order_contact_id',
+ domain=[('is_company', '=', False)],
+ tracking=True
+ )
- @api.depends('equipment_ids', 'child_ids.company_type', 'child_ids.equipment_ids')
+ @api.depends(
+ 'equipment_ids',
+ 'child_ids.company_type',
+ 'child_ids.equipment_ids'
+ )
def _compute_owned_equipment_ids(self):
for rec in self:
ids = rec.equipment_ids | rec.child_ids.filtered(
diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py
index d686c19..cd69b0a 100644
--- a/bemade_fsm/models/sale_order.py
+++ b/bemade_fsm/models/sale_order.py
@@ -7,35 +7,48 @@ import re
class SaleOrder(models.Model):
_inherit = 'sale.order'
- valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
- related="partner_id.owned_equipment_ids")
- default_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
- string="Default Equipment to Service",
- help="The default equipment to service for new sale order lines.",
- compute="_compute_default_equipment",
- inverse="_inverse_default_equipment",
- store=True, )
+ valid_equipment_ids = fields.One2many(
+ comodel_name="bemade_fsm.equipment",
+ related="partner_id.owned_equipment_ids"
+ )
- summary_equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
- string="Equipment Being Serviced",
- compute="_compute_summary_equipment_ids")
+ default_equipment_ids = fields.Many2many(
+ comodel_name="bemade_fsm.equipment",
+ string="Default Equipment to Service",
+ help="The default equipment to service for new sale order lines.",
+ compute="_compute_default_equipment",
+ inverse="_inverse_default_equipment",
+ store=True
+ )
- site_contacts = fields.Many2many(comodel_name='res.partner',
- relation="sale_order_site_contacts_rel",
- compute="_compute_default_contacts",
- inverse="_inverse_default_contacts",
- string='Site Contacts',
- store=True)
+ summary_equipment_ids = fields.Many2many(
+ comodel_name="bemade_fsm.equipment",
+ string="Equipment Being Serviced",
+ compute="_compute_summary_equipment_ids")
- work_order_contacts = fields.Many2many(comodel_name='res.partner',
- relation='sale_order_work_order_contacts_rel',
- compute='_compute_default_contacts',
- inverse='_inverse_default_contacts',
- string='Work Order Recipients',
- store=True)
- visit_ids = fields.One2many(comodel_name='bemade_fsm.visit',
- inverse_name="sale_order_id",
- readonly=False)
+ site_contacts = fields.Many2many(
+ comodel_name='res.partner',
+ relation="sale_order_site_contacts_rel",
+ compute="_compute_default_contacts",
+ inverse="_inverse_default_contacts",
+ string='Site Contacts',
+ store=True
+ )
+
+ work_order_contacts = fields.Many2many(
+ comodel_name='res.partner',
+ relation='sale_order_work_order_contacts_rel',
+ compute='_compute_default_contacts',
+ inverse='_inverse_default_contacts',
+ string='Work Order Recipients',
+ store=True
+ )
+
+ visit_ids = fields.One2many(
+ comodel_name='bemade_fsm.visit',
+ inverse_name="sale_order_id",
+ readonly=False
+ )
@api.depends('order_line.task_id')
def get_relevant_order_lines(self, task_id):
@@ -67,9 +80,12 @@ class SaleOrder(models.Model):
def _inverse_default_contacts(self):
pass
- @api.depends('partner_id', 'partner_shipping_id',
- 'partner_shipping_id.equipment_ids',
- 'partner_id.owned_equipment_ids')
+ @api.depends(
+ 'partner_id',
+ 'partner_shipping_id',
+ 'partner_shipping_id.equipment_ids',
+ 'partner_id.owned_equipment_ids'
+ )
def _compute_default_equipment(self):
for rec in self:
if rec.partner_shipping_id.equipment_ids:
@@ -82,241 +98,3 @@ class SaleOrder(models.Model):
pass
-class SaleOrderLine(models.Model):
- _inherit = 'sale.order.line'
- valid_equipment_ids = fields.One2many(comodel_name="bemade_fsm.equipment",
- related="order_id.valid_equipment_ids")
- visit_ids = fields.One2many(comodel_name="bemade_fsm.visit",
- inverse_name="so_section_id", )
- visit_id = fields.Many2one(comodel_name="bemade_fsm.visit",
- compute="_compute_visit_id",
- string="Visit",
- ondelete='cascade',
- store=True)
- is_fully_delivered = fields.Boolean(string="Fully Delivered",
- compute="_compute_is_fully_delivered",
- help="Indicates whether a line or all the lines in a section have been"
- " entirely delivered.")
- is_fully_delivered_and_invoiced = fields.Boolean(string="Fully Invoiced",
- compute="_compute_is_fully_invoiced",
- help="Indicates whether a line or all the lines in a section have been"
- " entirely delivered and invoiced.")
- equipment_ids = fields.Many2many(string="Equipment to Service",
- comodel_name="bemade_fsm.equipment",
- relation="bemade_fsm_equipment_sale_order_line_rel",
- column1="sale_order_line_id",
- column2="equipment_id")
- is_field_service = fields.Boolean(string="Is Field Service",
- compute="_compute_is_field_service",
- store=True)
-
- task_duration = fields.Float(string="Estimated Duration",
- compute="_compute_task_duration", )
-
- @api.depends('visit_ids')
- def _compute_visit_id(self):
- for rec in self:
- rec.visit_id = rec.visit_ids and rec.visit_ids[0]
-
- @api.depends('product_id')
- def _compute_is_field_service(self):
- for rec in self:
- rec.is_field_service = rec.product_id.is_field_service
-
- @api.model_create_multi
- def create(self, vals):
- recs = super().create(vals)
- for rec in recs:
- if rec.order_id.default_equipment_ids and not rec.equipment_ids:
- rec.equipment_ids = rec.order_id.default_equipment_ids
- return recs
-
- def _timesheet_create_task(self, project):
- """ Generate task for the given so line, and link it.
- :param project: record of project.project in which the task should be created
- :return task: record of the created task
-
- Override to add the logic needed to implement task templates and equipment linkages."""
-
- def _create_task_from_template(project, template, parent):
- """ Recursively generates the task and any subtasks from a project.task.template.
-
- :param project: project.project record to set on the task's project_id field.
- :param template: project.task.template to use to create the task.
- :param parent: project.task to set as the parent to this task.
- """
- values = _timesheet_create_task_prepare_values_from_template(project,
- template,
- parent)
- task = self.env['project.task'].sudo().create(values)
- subtasks = []
- for t in template.subtasks:
- subtask = _create_task_from_template(project, t, task)
- subtasks.append(subtask)
- task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
- # We don't want to see the sub-tasks on the SO
- task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
- return task
-
- def _timesheet_create_task_prepare_values_from_template(project, template,
- parent):
- """ Copies the values from a project.task.template over to the set of values used to create a project.task.
-
- :param project: project.project record to set on the task's project_id field.
- Pass the project.project model or an empty recordset to leave task project_id blank.
- DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
- :param template: project.task.template to use to create the task.
- :param parent: project.task to set as the parent to this task.
- """
- vals = self._timesheet_create_task_prepare_values(project)
- vals['name'] = template.name
- vals['description'] = template.description or '' if parent else vals['description']
- vals['parent_id'] = parent and parent.id
- vals['user_ids'] = template.assignees.ids
- vals['tag_ids'] = template.tags.ids
- vals['planned_hours'] = template.planned_hours
- vals['sequence'] = template.sequence
- if template.equipment_ids:
- vals['equipment_ids'] = template.equipment_ids.ids
- return vals
-
- tmpl = self.product_id.task_template_id
- if not tmpl:
- task = super()._timesheet_create_task(project)
- else:
- task = _create_task_from_template(project, tmpl, None)
- self.write({'task_id': task.id})
- # post message on task
- task_msg = _(
- "This task has been created from: %s (%s)") % (
- self.order_id.id, self.order_id.name, self.product_id.name)
- task.message_post(body=task_msg)
- if not task.equipment_ids and self.equipment_ids:
- task.equipment_ids = self.equipment_ids.ids
- task.planned_hours = self.task_duration
- return task
-
- def _timesheet_service_generation(self):
- super()._timesheet_service_generation()
- visit_lines = self.filtered(lambda l: l.visit_id)
- for line in visit_lines:
- task_ids = line.get_section_line_ids().mapped('task_id')
- if not task_ids:
- continue
- if len(set([task.project_id for task in task_ids])) > 1:
- # Can't group up the tasks if they're part of different projects
- return
- project_id = task_ids[0].project_id
- line.visit_id.task_id = line._generate_task_for_visit_line(project_id)
- task_ids.write({'parent_id': line.visit_id.task_id.id})
- self.mapped('task_id').synchronize_name_fsm()
-
- def _generate_task_for_visit_line(self, project):
- self.ensure_one()
-
- task = self.env['project.task'].create({
- 'name': 'Temp Name',
- 'description': f"Parent task for {self.order_id.name}, visit {self.name}",
- 'project_id': project.id,
- 'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids,
- 'sale_order_id': self.order_id.id,
- 'partner_id': self.order_id.partner_shipping_id.id,
- 'visit_id': self.visit_id.id,
- 'date_deadline': self.visit_id.approx_date,
- 'planned_hours': self.task_duration,
- 'user_ids': False, # Force to empty or it uses the current user
- })
- return task
-
- @api.depends('order_id.order_line', 'display_type', 'qty_to_deliver',
- 'order_id.order_line.qty_to_deliver',
- 'order_id.order_line.display_type')
- def _compute_is_fully_delivered(self):
- for rec in self:
- rec.is_fully_delivered = rec._iterate_items_compute_bool(
- lambda l: l.qty_to_deliver == 0)
-
- @api.depends('is_fully_delivered')
- def _compute_is_fully_invoiced(self):
- for rec in self:
- if not rec.is_fully_delivered:
- rec.is_fully_delivered_and_invoiced = False
- return
- rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
- lambda l: l.qty_to_invoice == 0)
-
- def get_section_line_ids(self):
- self.ensure_one()
- assert self.display_type == 'line_section', "Cannot get section lines for a non-section."
- found = False
- lines = []
- for line in self.order_id.order_line.sorted(lambda l: l.sequence):
- if line == self:
- found = True
- continue
- if not found:
- continue
- if line.display_type == 'line_section': # Stop when we hit the next section
- break
- else:
- lines.append(line)
- return self.env['sale.order.line'].union(*lines)
-
- def _iterate_items_compute_bool(self, single_line_func):
- if not self.display_type:
- return single_line_func(self)
- elif self.display_type == 'line_note':
- return True
- else:
- for line in self.order_id.order_line:
- found = False
- if line == self:
- found = True
- if not found:
- continue
- if found and line.display_type == 'line_section':
- return True
- val = single_line_func(self)
- if not val:
- return val
- return True
-
- @api.depends('task_duration', 'product_id', 'product_id.planning_enabled', 'state')
- def _compute_planning_hours_to_plan(self):
- # Override the method from sale_planning to use time estimates from the task
- # template if appropriate. Also compute the value for visit lines.
- planning_lines = self.filtered_domain([
- ('product_id.planning_enabled', '=', True),
- ('state', 'not in', ['draft', 'sent'])
- ])
- for line in planning_lines:
- line.planning_hours_to_plan = line.task_duration
- for line in self - planning_lines:
- line.planning_hours_to_plan = 0.0
-
- @api.depends('product_id', 'visit_id')
- def _compute_task_duration(self):
- uom_hour = self.env.ref('uom.product_uom_hour')
- uom_unit = self.env.ref('uom.product_uom_unit')
- templated_lines = self.filtered(lambda l: l.product_id.task_template_id
- and l.product_id.task_template_id.
- planned_hours)
- visit_lines = self.filtered(lambda l: l.visit_id)
- regular_lines = self - templated_lines - visit_lines
- for line in regular_lines:
- if line.product_uom == uom_hour or line.product_uom == uom_unit:
- line.task_duration = line.product_uom_qty
- else:
- line.task_duration = float_round(
- line.product_uom._compute_quantity(line.product_uom_qty, uom_hour,
- raise_if_failure=False),
- precision_digits=2)
- for line in templated_lines:
- line.task_duration = line.product_id.task_template_id.planned_hours
- if line.product_uom_category_id == self.env.ref(
- 'uom.product_uom_unit').category_id:
- line.task_duration *= line.product_uom_qty
- visit_lines = self.filtered(lambda l: l.visit_id)
- for line in visit_lines:
- line.task_duration = sum(line.get_section_line_ids().
- mapped('task_duration'))
diff --git a/bemade_fsm/models/sale_order_line.py b/bemade_fsm/models/sale_order_line.py
new file mode 100644
index 0000000..b1cdcee
--- /dev/null
+++ b/bemade_fsm/models/sale_order_line.py
@@ -0,0 +1,267 @@
+from odoo import fields, models, api, _, Command
+from odoo.exceptions import ValidationError
+from odoo.tools import float_round
+import re
+
+
+class SaleOrderLine(models.Model):
+ _inherit = 'sale.order.line'
+ valid_equipment_ids = fields.One2many(
+ comodel_name="bemade_fsm.equipment",
+ related="order_id.valid_equipment_ids"
+ )
+
+ visit_ids = fields.One2many(
+ comodel_name="bemade_fsm.visit",
+ inverse_name="so_section_id",
+ string="Visits"
+ )
+
+ visit_id = fields.Many2one(
+ comodel_name="bemade_fsm.visit",
+ compute="_compute_visit_id",
+ string="Visit",
+ ondelete='cascade',
+ store=True
+ )
+
+ is_fully_delivered = fields.Boolean(
+ string="Fully Delivered",
+ compute="_compute_is_fully_delivered",
+ help="Indicates whether a line or all the lines in a section have been entirely delivered."
+ )
+
+ is_fully_delivered_and_invoiced = fields.Boolean(
+ string="Fully Invoiced",
+ compute="_compute_is_fully_invoiced",
+ help="Indicates whether a line or all the lines in a section have been entirely delivered and invoiced."
+ )
+
+ equipment_ids = fields.Many2many(
+ string="Equipment to Service",
+ comodel_name="bemade_fsm.equipment",
+ relation="bemade_fsm_equipment_sale_order_line_rel",
+ column1="sale_order_line_id",
+ column2="equipment_id"
+ )
+
+ is_field_service = fields.Boolean(
+ string="Is Field Service",
+ compute="_compute_is_field_service",
+ store=True
+ )
+
+ task_duration = fields.Float(
+ string="Estimated Duration",
+ compute="_compute_task_duration"
+ )
+
+ @api.depends('visit_ids')
+ def _compute_visit_id(self):
+ for rec in self:
+ rec.visit_id = rec.visit_ids and rec.visit_ids[0]
+
+ @api.depends('product_id')
+ def _compute_is_field_service(self):
+ for rec in self:
+ rec.is_field_service = rec.product_id.is_field_service
+
+ @api.model_create_multi
+ def create(self, vals):
+ recs = super().create(vals)
+ for rec in recs:
+ if rec.order_id.default_equipment_ids and not rec.equipment_ids:
+ rec.equipment_ids = rec.order_id.default_equipment_ids
+ return recs
+
+ def _timesheet_create_task(self, project):
+ """ Generate task for the given so line, and link it.
+ :param project: record of project.project in which the task should be created
+ :return task: record of the created task
+
+ Override to add the logic needed to implement task templates and equipment linkages."""
+
+ def _create_task_from_template(project, template, parent):
+ """ Recursively generates the task and any subtasks from a project.task.template.
+
+ :param project: project.project record to set on the task's project_id field.
+ :param template: project.task.template to use to create the task.
+ :param parent: project.task to set as the parent to this task.
+ """
+ values = _timesheet_create_task_prepare_values_from_template(project, template, parent)
+ task = self.env['project.task'].sudo().create(values)
+ subtasks = []
+ for t in template.subtasks:
+ subtask = _create_task_from_template(project, t, task)
+ subtasks.append(subtask)
+ task.write({'child_ids': [Command.set([t.id for t in subtasks])]})
+ # We don't want to see the sub-tasks on the SO
+ task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
+ return task
+
+ def _timesheet_create_task_prepare_values_from_template(project, template,
+ parent):
+ """ Copies the values from a project.task.template over to the set of values used to create a project.task.
+
+ :param project: project.project record to set on the task's project_id field.
+ Pass the project.project model or an empty recordset to leave task project_id blank.
+ DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
+ :param template: project.task.template to use to create the task.
+ :param parent: project.task to set as the parent to this task.
+ """
+ vals = self._timesheet_create_task_prepare_values(project)
+ vals['name'] = template.name
+ vals['description'] = template.description or '' if parent else vals['description']
+ vals['parent_id'] = parent and parent.id
+ vals['user_ids'] = template.assignees.ids
+ vals['tag_ids'] = template.tags.ids
+ vals['planned_hours'] = template.planned_hours
+ vals['sequence'] = template.sequence
+ if template.equipment_ids:
+ vals['equipment_ids'] = template.equipment_ids.ids
+ return vals
+
+ tmpl = self.product_id.task_template_id
+ if not tmpl:
+ task = super()._timesheet_create_task(project)
+ else:
+ task = _create_task_from_template(project, tmpl, None)
+ self.write({'task_id': task.id})
+ # post message on task
+ task_msg = _(
+ "This task has been created from: %s (%s)") % (
+ self.order_id.id, self.order_id.name, self.product_id.name)
+ task.message_post(body=task_msg)
+ if not task.equipment_ids and self.equipment_ids:
+ task.equipment_ids = self.equipment_ids.ids
+ task.planned_hours = self.task_duration
+ return task
+
+ def _timesheet_service_generation(self):
+ super()._timesheet_service_generation()
+ visit_lines = self.filtered(lambda l: l.visit_id)
+ for line in visit_lines:
+ task_ids = line.get_section_line_ids().mapped('task_id')
+ if not task_ids:
+ continue
+ if len(set([task.project_id for task in task_ids])) > 1:
+ # Can't group up the tasks if they're part of different projects
+ return
+ project_id = task_ids[0].project_id
+ line.visit_id.task_id = line._generate_task_for_visit_line(project_id)
+ task_ids.write({'parent_id': line.visit_id.task_id.id})
+ self.mapped('task_id').synchronize_name_fsm()
+
+ def _generate_task_for_visit_line(self, project):
+ self.ensure_one()
+
+ task = self.env['project.task'].create({
+ 'name': 'Temp Name',
+ 'description': f"Parent task for {self.order_id.name}, visit {self.name}",
+ 'project_id': project.id,
+ 'equipment_ids': self.get_section_line_ids().mapped('equipment_ids').ids,
+ 'sale_order_id': self.order_id.id,
+ 'partner_id': self.order_id.partner_shipping_id.id,
+ 'visit_id': self.visit_id.id,
+ 'date_deadline': self.visit_id.approx_date,
+ 'planned_hours': self.task_duration,
+ 'user_ids': False, # Force to empty or it uses the current user
+ })
+ return task
+
+ @api.depends(
+ 'order_id.order_line',
+ 'display_type',
+ 'qty_to_deliver',
+ 'order_id.order_line.qty_to_deliver',
+ 'order_id.order_line.display_type'
+ )
+ def _compute_is_fully_delivered(self):
+ for rec in self:
+ rec.is_fully_delivered = rec._iterate_items_compute_bool(
+ lambda l: l.qty_to_deliver == 0)
+
+ @api.depends('is_fully_delivered')
+ def _compute_is_fully_invoiced(self):
+ for rec in self:
+ if not rec.is_fully_delivered:
+ rec.is_fully_delivered_and_invoiced = False
+ return
+ rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
+ lambda l: l.qty_to_invoice == 0)
+
+ def get_section_line_ids(self):
+ self.ensure_one()
+ assert self.display_type == 'line_section', "Cannot get section lines for a non-section."
+ found = False
+ lines = []
+ for line in self.order_id.order_line.sorted(lambda l: l.sequence):
+ if line == self:
+ found = True
+ continue
+ if not found:
+ continue
+ if line.display_type == 'line_section': # Stop when we hit the next section
+ break
+ else:
+ lines.append(line)
+ return self.env['sale.order.line'].union(*lines)
+
+ def _iterate_items_compute_bool(self, single_line_func):
+ if not self.display_type:
+ return single_line_func(self)
+ elif self.display_type == 'line_note':
+ return True
+ else:
+ for line in self.order_id.order_line:
+ found = False
+ if line == self:
+ found = True
+ if not found:
+ continue
+ if found and line.display_type == 'line_section':
+ return True
+ val = single_line_func(self)
+ if not val:
+ return val
+ return True
+
+ @api.depends('task_duration', 'product_id', 'product_id.planning_enabled', 'state')
+ def _compute_planning_hours_to_plan(self):
+ # Override the method from sale_planning to use time estimates from the task
+ # template if appropriate. Also compute the value for visit lines.
+ planning_lines = self.filtered_domain([
+ ('product_id.planning_enabled', '=', True),
+ ('state', 'not in', ['draft', 'sent'])
+ ])
+ for line in planning_lines:
+ line.planning_hours_to_plan = line.task_duration
+ for line in self - planning_lines:
+ line.planning_hours_to_plan = 0.0
+
+ @api.depends('product_id', 'visit_id')
+ def _compute_task_duration(self):
+ uom_hour = self.env.ref('uom.product_uom_hour')
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ templated_lines = self.filtered(lambda l: l.product_id.task_template_id
+ and l.product_id.task_template_id.
+ planned_hours)
+ visit_lines = self.filtered(lambda l: l.visit_id)
+ regular_lines = self - templated_lines - visit_lines
+ for line in regular_lines:
+ if line.product_uom == uom_hour or line.product_uom == uom_unit:
+ line.task_duration = line.product_uom_qty
+ else:
+ line.task_duration = float_round(
+ line.product_uom._compute_quantity(line.product_uom_qty, uom_hour,
+ raise_if_failure=False),
+ precision_digits=2)
+ for line in templated_lines:
+ line.task_duration = line.product_id.task_template_id.planned_hours
+ if line.product_uom_category_id == self.env.ref(
+ 'uom.product_uom_unit').category_id:
+ line.task_duration *= line.product_uom_qty
+ visit_lines = self.filtered(lambda l: l.visit_id)
+ for line in visit_lines:
+ line.task_duration = sum(line.get_section_line_ids().
+ mapped('task_duration'))
diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py
index 3f0c519..c8a65a2 100644
--- a/bemade_fsm/models/task.py
+++ b/bemade_fsm/models/task.py
@@ -8,51 +8,71 @@ import re
class Task(models.Model):
_inherit = "project.task"
- equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
- relation="bemade_fsm_task_equipment_rel",
- column1="task_id",
- column2="equipment_id",
- string="Equipment to Service",
- tracking=True, )
+ equipment_ids = fields.Many2many(
+ comodel_name="bemade_fsm.equipment",
+ relation="bemade_fsm_task_equipment_rel",
+ column1="task_id",
+ column2="equipment_id",
+ string="Equipment to Service",
+ tracking=True,
+ )
- work_order_contacts = fields.Many2many(comodel_name="res.partner",
- relation="task_work_order_contact_rel",
- column1="task_id",
- column2="res_partner_id",
- compute="_compute_contacts",
- inverse="_inverse_contacts",
- store=True)
+ work_order_contacts = fields.Many2many(
+ comodel_name="res.partner",
+ relation="task_work_order_contact_rel",
+ column1="task_id",
+ column2="res_partner_id",
+ compute="_compute_contacts",
+ inverse="_inverse_contacts",
+ store=True
+ )
- site_contacts = fields.Many2many(comodel_name="res.partner",
- relation="task_site_contact_rel",
- column1="task_id",
- column2="res_partner_id",
- compute="_compute_contacts",
- inverse="_inverse_contacts",
- store=True)
+ site_contacts = fields.Many2many(
+ comodel_name="res.partner",
+ relation="task_site_contact_rel",
+ column1="task_id",
+ column2="res_partner_id",
+ compute="_compute_contacts",
+ inverse="_inverse_contacts",
+ store=True
+ )
- planned_date_begin = fields.Datetime("Start Date", tracking=True,
- task_dependency_tracking=True,
- compute="_compute_planned_dates",
- inverse="_inverse_planned_dates",
- store=True)
- planned_date_end = fields.Datetime("End Date", tracking=True,
- task_dependency_tracking=True,
- compute="_compute_planned_dates",
- inverse="_inverse_planned_dates",
- store=True)
+ planned_date_begin = fields.Datetime(
+ string="Start Date",
+ tracking=True,
+ task_dependency_tracking=True,
+ compute="_compute_planned_dates",
+ inverse="_inverse_planned_dates",
+ store=True
+ )
+
+ planned_date_end = fields.Datetime(
+ string="End Date",
+ tracking=True,
+ task_dependency_tracking=True,
+ compute="_compute_planned_dates",
+ inverse="_inverse_planned_dates",
+ store=True
+ )
# Override related field to make it return false if this is an FSM subtask
- allow_billable = fields.Boolean(string="Can be billed",
- related=False,
- compute="_compute_allow_billable",
- store=True)
+ allow_billable = fields.Boolean(
+ string="Can be billed",
+ related=False,
+ compute="_compute_allow_billable",
+ store=True
+ )
- visit_id = fields.Many2one(comodel_name='bemade_fsm.visit')
+ visit_id = fields.Many2one(
+ comodel_name='bemade_fsm.visit',
+ string="Visit",
+ )
- relevant_order_lines = fields.Many2many(comodel_name='sale.order.line',
- store=False,
- compute='_compute_relevant_order_lines', )
+ relevant_order_lines = fields.Many2many(
+ comodel_name='sale.order.line',
+ store=False,
+ compute='_compute_relevant_order_lines',
+ )
work_order_number = fields.Char(readonly=True)
diff --git a/bemade_fsm/models/task_template.py b/bemade_fsm/models/task_template.py
index 0a2b181..7253fbf 100644
--- a/bemade_fsm/models/task_template.py
+++ b/bemade_fsm/models/task_template.py
@@ -10,25 +10,62 @@ class TaskTemplate(models.Model):
return self.env.company
name = fields.Char(string="Task Title", required=True)
+
description = fields.Html(string="Description")
- assignees = fields.Many2many("res.users", string="Default Assignees",
- help="Employees assigned to tasks created from this template.")
- customer = fields.Many2one("res.partner", string="Default Customer",
- help="Default customer for tasks created from this template.")
- project = fields.Many2one("project.project", string="Default Project",
- help="Default project for tasks created from this template.")
- tags = fields.Many2many("project.tags", string="Default Tags",
- help="Default tags for tasks created from this template.")
- parent = fields.Many2one("project.task.template", string="Parent Task Template", ondelete='cascade')
- subtasks = fields.One2many("project.task.template", inverse_name="parent", string="Subtask Templates")
+
+ assignees = fields.Many2many(
+ comodel_name="res.users",
+ string="Default Assignees",
+ help="Employees assigned to tasks created from this template."
+ )
+
+ customer = fields.Many2one(
+ comodel_name="res.partner",
+ string="Default Customer",
+ help="Default customer for tasks created from this template."
+ )
+
+ project = fields.Many2one(
+ comodel_name="project.project",
+ string="Default Project",
+ help="Default project for tasks created from this template."
+ )
+
+ tags = fields.Many2many(
+ comodel_name="project.tags",
+ string="Default Tags",
+ help="Default tags for tasks created from this template."
+ )
+
+ parent = fields.Many2one(
+ comodel_name="project.task.template",
+ string="Parent Task Template",
+ ondelete='cascade'
+ )
+
+ subtasks = fields.One2many(
+ comodel_name="project.task.template",
+ inverse_name="parent",
+ string="Subtask Templates"
+ )
sequence = fields.Integer(string="Sequence")
- company_id = fields.Many2one("res.company", string="Company", index=1, default=_current_company)
+
+ company_id = fields.Many2one(
+ comodel_name="res.company",
+ string="Company",
+ index=1,
+ default=_current_company
+ )
+
planned_hours = fields.Float("Initially Planned Hours")
- equipment_ids = fields.Many2many(comodel_name="bemade_fsm.equipment",
- relation="bemade_fsm_task_template_equipment_rel",
- column1="task_template_id",
- column2="equipment_id",
- string="Equipment to Service",)
+
+ equipment_ids = fields.Many2many(
+ comodel_name="bemade_fsm.equipment",
+ relation="bemade_fsm_task_template_equipment_rel",
+ column1="task_template_id",
+ column2="equipment_id",
+ string="Equipment to Service",
+ )
def action_open_task(self):
return {