diff --git a/bemade_fsm/__manifest__.py b/bemade_fsm/__manifest__.py
index 254f486..b44bb86 100644
--- a/bemade_fsm/__manifest__.py
+++ b/bemade_fsm/__manifest__.py
@@ -27,7 +27,8 @@
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'OPL-1',
- 'depends': ['project',
+ 'depends': ['project_enterprise',
+ 'project_forecast',
'stock',
'sale',
'sale_project',
diff --git a/bemade_fsm/models/product_template.py b/bemade_fsm/models/product_template.py
index c7ecb0a..d3df23f 100644
--- a/bemade_fsm/models/product_template.py
+++ b/bemade_fsm/models/product_template.py
@@ -5,3 +5,6 @@ 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
diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py
index 59af3eb..177f0fe 100644
--- a/bemade_fsm/models/sale_order.py
+++ b/bemade_fsm/models/sale_order.py
@@ -89,6 +89,14 @@ class SaleOrderLine(models.Model):
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)
+
+ @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):
diff --git a/bemade_fsm/models/task.py b/bemade_fsm/models/task.py
index ef32d55..9563766 100644
--- a/bemade_fsm/models/task.py
+++ b/bemade_fsm/models/task.py
@@ -1,5 +1,7 @@
from odoo import fields, models, api, Command, _
-from odoo.exceptions import ValidationError
+from odoo.exceptions import ValidationError, UserError
+from odoo.osv import expression
+from collections import defaultdict, namedtuple
class Task(models.Model):
@@ -10,7 +12,7 @@ class Task(models.Model):
column1="task_id",
column2="equipment_id",
string="Equipment to Service",
- tracking=True,)
+ tracking=True, )
work_order_contacts = fields.Many2many(comodel_name="res.partner",
relation="task_work_order_contact_rel",
@@ -28,13 +30,64 @@ class Task(models.Model):
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)
+
# 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",)
+ compute="_compute_allow_billable", )
visit_id = fields.Many2one(comodel_name='bemade_fsm.visit')
- @api.depends('sale_line_id.order_id.site_contacts', 'sale_line_id.order_id.work_order_contacts')
+
+ def _get_related_planning_slots(self):
+ domain = expression.AND([
+ self._get_domain_compute_forecast_hours(),
+ [('task_id', 'in', self.ids + self._get_all_subtasks().ids)]
+ ])
+ return self.env['planning.slot'].search(domain)
+
+ @api.depends('forecast_hours')
+ def _compute_planned_dates(self):
+ forecast_data = self._get_related_planning_slots()
+ mapped_data = {}
+ TimeSpan = namedtuple('timespan', ['start', 'end'])
+ for d in forecast_data:
+ if d not in mapped_data:
+ mapped_data.update({d: TimeSpan(d.start_datetime, d.end_datetime)})
+ continue
+ if mapped_data[d].start > d.start_datetime:
+ mapped_data[d].start = d.start_datetime
+ if mapped_data[d].end < d.end_datetime:
+ mapped_data[d].end = d.end_datetime
+ for rec in self:
+ if rec not in mapped_data:
+ if not rec.planned_date_end:
+ rec.planned_date_end = False
+ if not rec.planned_date_begin:
+ rec.planned_date_begin = False
+ continue
+ rec.planned_date_begin, rec.planned_date_end = mapped_data[rec]
+
+ def _inverse_planned_dates(self):
+ """ Modifying the planned dates for tasks with existing planning records
+ (planning.slot) has no defined safe behaviour, so we block it."""
+ if self._get_related_planning_slots():
+ raise UserError(_("Modifying the planned start or end time on a task is not "
+ "permitted when that task already has planning records "
+ "associated to it. Please modify or delete the planning "
+ "records instead."))
+
+ @api.depends('sale_line_id.order_id.site_contacts',
+ 'sale_line_id.order_id.work_order_contacts')
def _compute_contacts(self):
""" The work order contacts and site contacts for a given task are taken from the sale order if the task
is related to one, and from the task's customer if there is no sale order related to the task."""
diff --git a/bemade_fsm/views/product_views.xml b/bemade_fsm/views/product_views.xml
index 7183ac5..340de8c 100644
--- a/bemade_fsm/views/product_views.xml
+++ b/bemade_fsm/views/product_views.xml
@@ -11,6 +11,9 @@
+
+
+