diff --git a/fieldservice/models/fsm_location.py b/fieldservice/models/fsm_location.py
index 2a97a62728..ef5dbab71f 100644
--- a/fieldservice/models/fsm_location.py
+++ b/fieldservice/models/fsm_location.py
@@ -69,6 +69,11 @@ class FSMLocation(models.Model):
complete_name = fields.Char(
compute="_compute_complete_name", recursive=True, store=True
)
+ complete_direction = fields.Char(
+ compute="_compute_complete_direction",
+ store=True,
+ recursive=True,
+ )
@api.model_create_multi
def create(self, vals):
@@ -94,6 +99,13 @@ def _compute_complete_name(self):
else:
loc.complete_name = loc.partner_id.name
+ @api.depends("direction", "fsm_parent_id.complete_direction")
+ def _compute_complete_direction(self):
+ for rec in self:
+ parent_direction = rec.fsm_parent_id.complete_direction
+ complete_direction = (parent_direction or "") + (rec.direction or "")
+ rec.complete_direction = complete_direction or False
+
@api.onchange("fsm_parent_id")
def _onchange_fsm_parent_id(self):
self.owner_id = self.fsm_parent_id.owner_id or False
diff --git a/fieldservice/models/fsm_order.py b/fieldservice/models/fsm_order.py
index ca634655bd..b933ad9613 100644
--- a/fieldservice/models/fsm_order.py
+++ b/fieldservice/models/fsm_order.py
@@ -1,9 +1,10 @@
# Copyright (C) 2018 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import warnings
from datetime import datetime, timedelta
-from odoo import _, api, fields, models
+from odoo import Command, _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from . import fsm_stage
@@ -112,7 +113,12 @@ def _track_subtype(self, init_values):
location_id = fields.Many2one(
"fsm.location", string="Location", index=True, required=True
)
- location_directions = fields.Char()
+ location_directions = fields.Char(
+ compute="_compute_location_directions",
+ precompute=True,
+ store=True,
+ readonly=False,
+ )
request_early = fields.Datetime(
string="Earliest Request Date", default=datetime.now()
)
@@ -151,32 +157,37 @@ def _calc_request_late(self, vals):
return vals
request_late = fields.Datetime(string="Latest Request Date")
- description = fields.Text()
+ description = fields.Text(
+ compute="_compute_description",
+ precompute=True,
+ store=True,
+ readonly=False,
+ )
person_ids = fields.Many2many("fsm.person", string="Field Service Workers")
- @api.onchange("location_id")
- def _onchange_location_id_customer(self):
- if self.location_id:
- self.territory_id = self.location_id.territory_id or False
- self.branch_id = self.location_id.branch_id or False
- self.district_id = self.location_id.district_id or False
- self.region_id = self.location_id.region_id or False
- self.copy_notes()
- if self.company_id.auto_populate_equipments_on_order:
- fsm_equipment_rec = self.env["fsm.equipment"].search(
- [("current_location_id", "=", self.location_id.id)]
- )
- self.equipment_ids = [(6, 0, fsm_equipment_rec.ids)]
-
# Planning
- person_id = fields.Many2one("fsm.person", string="Assigned To", index=True)
+ person_id = fields.Many2one(
+ "fsm.person",
+ string="Assigned To",
+ compute="_compute_person_id",
+ precompute=True,
+ store=True,
+ readonly=False,
+ index=True,
+ )
person_phone = fields.Char(related="person_id.phone", string="Worker Phone")
scheduled_date_start = fields.Datetime(string="Scheduled Start (ETA)")
scheduled_duration = fields.Float(help="Scheduled duration of the work in" " hours")
scheduled_date_end = fields.Datetime(string="Scheduled End")
sequence = fields.Integer(default=10)
- todo = fields.Text(string="Instructions")
+ todo = fields.Text(
+ string="Instructions",
+ compute="_compute_todo",
+ precompute=True,
+ store=True,
+ readonly=False,
+ )
# Execution
resolution = fields.Text()
@@ -191,19 +202,24 @@ def _onchange_location_id_customer(self):
# Location
territory_id = fields.Many2one(
- "res.territory",
- string="Territory",
related="location_id.territory_id",
+ precompute=True,
store=True,
)
branch_id = fields.Many2one(
- "res.branch", string="Branch", related="location_id.branch_id", store=True
+ related="location_id.branch_id",
+ precompute=True,
+ store=True,
)
district_id = fields.Many2one(
- "res.district", string="District", related="location_id.district_id", store=True
+ related="location_id.district_id",
+ precompute=True,
+ store=True,
)
region_id = fields.Many2one(
- "res.region", string="Region", related="location_id.region_id", store=True
+ related="location_id.region_id",
+ precompute=True,
+ store=True,
)
# Fields for Geoengine Identify
@@ -229,11 +245,77 @@ def _onchange_location_id_customer(self):
equipment_id = fields.Many2one("fsm.equipment", string="Equipment")
# Equipment used for all other Service Orders
- equipment_ids = fields.Many2many("fsm.equipment", string="Equipments")
+ equipment_ids = fields.Many2many(
+ "fsm.equipment",
+ string="Equipments",
+ compute="_compute_equipment_ids",
+ precompute=True,
+ store=True,
+ readonly=False,
+ )
type = fields.Many2one("fsm.order.type")
internal_type = fields.Selection(related="type.internal_type")
+ @api.depends("company_id")
+ def _compute_equipment_ids(self):
+ for rec in self:
+ # Clear equipments that no longer match the order company
+ to_remove = rec.equipment_ids.filtered(
+ lambda equipment, rec=rec: equipment.company_id != rec.company_id
+ )
+ if to_remove:
+ rec.equipment_ids = [
+ Command.unlink(equipment.id) for equipment in to_remove
+ ]
+ # If we have no equipments, auto populate if needed
+ if (
+ rec.company_id.auto_populate_equipments_on_order
+ and not rec.equipment_ids
+ ):
+ rec.equipment_ids = self.env["fsm.equipment"].search(
+ [
+ ("current_location_id", "=", rec.location_id.id),
+ ("company_id", "=", rec.company_id.id),
+ ]
+ )
+
+ @api.depends("location_id")
+ def _compute_location_directions(self):
+ for rec in self:
+ rec.location_directions = rec.location_id.complete_direction
+
+ @api.depends("template_id")
+ def _compute_todo(self):
+ for rec in self:
+ if rec.template_id:
+ rec.todo = rec.template_id.instructions
+
+ @api.depends("equipment_ids", "equipment_id", "type")
+ def _compute_description(self):
+ for rec in self:
+ if rec.description:
+ continue
+ equipments = (
+ rec.equipment_ids
+ if rec.type and rec.internal_type not in ("repair", "maintenance")
+ else rec.equipment_id
+ )
+ rec.description = "\n".join(
+ equipment.notes for equipment in equipments if equipment.notes
+ )
+
+ @api.depends("territory_id")
+ def _compute_person_id(self):
+ """Compute the person from the territory"""
+ for rec in self:
+ # If the person is one of the territory's workers, keep it.
+ if rec.person_id in rec.territory_id.person_ids:
+ continue
+ # If the territory has a primary assignment, use it.
+ if rec.territory_id.person_id:
+ rec.person_id = rec.territory_id.person_id
+
@api.model
def _read_group_stage_ids(self, stages, domain, order):
search_domain = [("stage_type", "=", "order")]
@@ -358,51 +440,25 @@ def onchange_scheduled_duration(self):
else:
self.scheduled_date_end = self.scheduled_date_start
- def copy_notes(self):
- old_desc = self.description
- self.location_directions = ""
- if self.type and self.type.name not in ["repair", "maintenance"]:
- for equipment_id in self.equipment_ids.filtered(lambda eq: eq.notes):
- desc = self.description or ""
- self.description = desc + equipment_id.notes + "\n "
- else:
- if self.equipment_id.notes:
- desc = self.description if self.description else ""
- self.description = desc + self.equipment_id.notes + "\n "
- if self.location_id:
- self.location_directions = self._get_location_directions(self.location_id)
- if self.template_id:
- self.todo = self.template_id.instructions
- if old_desc:
- self.description = old_desc
-
- @api.onchange("equipment_ids")
- def onchange_equipment_ids(self):
- self.copy_notes()
-
@api.onchange("template_id")
def _onchange_template_id(self):
if self.template_id:
self.category_ids = self.template_id.category_ids
self.scheduled_duration = self.template_id.duration
- self.copy_notes()
if self.template_id.type_id:
self.type = self.template_id.type_id
if self.template_id.team_id:
self.team_id = self.template_id.team_id
- def _get_location_directions(self, location_id):
- self.location_directions = ""
- s = self.location_id.direction or ""
- parent_location = self.location_id.fsm_parent_id
- # ps => Parent Location Directions
- # s => String to Return
- while parent_location.id is not False:
- ps = parent_location.direction
- if ps:
- s += parent_location.direction
- parent_location = parent_location.fsm_parent_id
- return s
+ def _get_location_directions(self, location_id): # pragma: no cover
+ # TODO(migration): Remove this method
+ warnings.warn(
+ "Deprecated fsm.order._get_location_directions(), "
+ "use location.complete_direction instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return location_id.complete_direction
@api.constrains("scheduled_date_start")
def check_day(self):
diff --git a/fieldservice/tests/test_fsm_order.py b/fieldservice/tests/test_fsm_order.py
index a845df9bf7..015999b260 100644
--- a/fieldservice/tests/test_fsm_order.py
+++ b/fieldservice/tests/test_fsm_order.py
@@ -13,7 +13,11 @@ class TestFSMOrder(TransactionCase):
def setUpClass(cls):
super().setUpClass()
cls.Order = cls.env["fsm.order"]
+ cls.person_1 = cls.env.ref("fieldservice.person_1")
+ cls.person_2 = cls.env.ref("fieldservice.person_2")
+ cls.person_3 = cls.env.ref("fieldservice.person_3")
cls.test_location = cls.env.ref("fieldservice.test_location")
+ cls.test_territory = cls.env.ref("base_territory.test_territory")
cls.stage1 = cls.env.ref("fieldservice.fsm_stage_completed")
cls.stage2 = cls.env.ref("fieldservice.fsm_stage_cancelled")
cls.init_values = {
@@ -172,7 +176,6 @@ def test_fsm_order(self):
order4.action_complete()
order3.action_cancel()
self.env.user.company_id.auto_populate_equipments_on_order = True
- order._onchange_location_id_customer()
self.assertEqual(order.custom_color, order.stage_id.custom_color)
# Test _compute_duration
self.assertEqual(order.duration, hours_diff)
@@ -239,17 +242,20 @@ def test_fsm_order(self):
}
)
order.description = "description"
- order.copy_notes()
+ order.equipment_ids = equipment
+ self.assertEqual(order.description, "description", "Shouldn't have changed")
order.description = False
- order.copy_notes()
- order.type = False
- order.equipment_id = equipment.id
- order.onchange_equipment_ids()
+ equipment.notes = "equipment notes"
+ order.equipment_ids = equipment
+ self.assertEqual(
+ order.description,
+ equipment.notes,
+ "Description should be set from equipment",
+ )
order.type = False
order.description = False
self.location_1.direction = "Test Direction"
order2.location_id.fsm_parent_id = self.location_1.id
- order.copy_notes()
data = (
self.env["fsm.order"]
.with_context(**{"default_team_id": self.test_team.id})
@@ -287,3 +293,53 @@ def test_order_unlink(self):
order.stage_id.stage_type = "location"
order.can_unlink()
order.unlink()
+
+ def test_order_person_from_territory(self):
+ self.test_territory.person_ids = self.person_1 | self.person_2
+ self.test_territory.person_id = self.person_1
+ order = self.env["fsm.order"].create(
+ {
+ "location_id": self.test_location.id,
+ "stage_id": self.stage1.id,
+ }
+ )
+ self.assertEqual(
+ order.person_id,
+ self.person_1,
+ "Person should be assigned from territory",
+ )
+ # Other location with no territory, person should be kept
+ order.location_id = self.env.ref("fieldservice.location_1")
+ self.assertEqual(
+ order.person_id,
+ self.person_1,
+ "Person should be kept, because no territory is set",
+ )
+ # Other location with territory, matching person
+ other_location = self.env.ref("fieldservice.location_2")
+ other_location.territory_id = self.test_territory.copy(
+ {
+ "person_ids": (self.person_1 + self.person_2 + self.person_3).ids,
+ "person_id": self.person_3.id,
+ }
+ )
+ order.location_id = other_location
+ self.assertEqual(
+ order.person_id,
+ self.person_1,
+ "Person should be kept, because it's a territory worker",
+ )
+ # Other location with territory, unmatching person
+ other_location = self.env.ref("fieldservice.location_3")
+ other_location.territory_id = self.test_territory.copy(
+ {
+ "person_ids": (self.person_2 + self.person_3).ids,
+ "person_id": self.person_3.id,
+ }
+ )
+ order.location_id = other_location
+ self.assertEqual(
+ order.person_id,
+ self.person_3,
+ "Person should be assigned from territory",
+ )
diff --git a/fieldservice/tests/test_fsm_order_template_onchange.py b/fieldservice/tests/test_fsm_order_template_onchange.py
index decb126473..39dda46fd5 100644
--- a/fieldservice/tests/test_fsm_order_template_onchange.py
+++ b/fieldservice/tests/test_fsm_order_template_onchange.py
@@ -17,7 +17,7 @@ def setUpClass(cls):
def test_fsm_order_onchange_template(self):
"""Test the onchange function for FSM Template
- Category IDs, Scheduled Duration,and Type should update
- - The copy_notes() method should be called and instructions copied
+ - The instructions should be copied
"""
categories = []
categories.append(self.fsm_category_a.id)
diff --git a/fieldservice_recurring/models/fsm_recurring.py b/fieldservice_recurring/models/fsm_recurring.py
index a5613c0e33..f49dbc71f4 100644
--- a/fieldservice_recurring/models/fsm_recurring.py
+++ b/fieldservice_recurring/models/fsm_recurring.py
@@ -78,10 +78,28 @@ def _default_team_id(self):
tracking=True,
)
person_id = fields.Many2one(
- "fsm.person", string="Assigned To", index=True, tracking=True
+ "fsm.person",
+ string="Assigned To",
+ compute="_compute_person_id",
+ precompute=True,
+ store=True,
+ readonly=False,
+ index=True,
+ tracking=True,
)
equipment_ids = fields.Many2many("fsm.equipment")
+ @api.depends("location_id")
+ def _compute_person_id(self):
+ """Compute the person from the location's territory"""
+ for rec in self:
+ # If the person is one of the territory's workers, keep it.
+ if rec.person_id in rec.location_id.territory_id.person_ids:
+ continue
+ # If the territory has a primary assignment, use it.
+ if rec.location_id.territory_id.person_id:
+ rec.person_id = rec.location_id.territory_id.person_id
+
@api.depends("fsm_order_ids")
def _compute_order_count(self):
data = self.env["fsm.order"].read_group(
diff --git a/fieldservice_size/models/fsm_order.py b/fieldservice_size/models/fsm_order.py
index 8064656d69..1a10d11c58 100644
--- a/fieldservice_size/models/fsm_order.py
+++ b/fieldservice_size/models/fsm_order.py
@@ -6,17 +6,48 @@
class FSMOrder(models.Model):
_inherit = "fsm.order"
- def _default_size_id(self):
- size = False
- if self.type:
- size = self.env["fsm.size"].search(
- [("type_id", "=", self.type.id), ("is_order_size", "=", True)], limit=1
- )
- return size
+ size_id = fields.Many2one(
+ "fsm.size",
+ compute="_compute_size_id",
+ precompute=True,
+ readonly=False,
+ store=True,
+ )
+ size_value = fields.Float(
+ string="Order Size",
+ compute="_compute_size_value",
+ precompute=True,
+ readonly=False,
+ store=True,
+ )
+ size_uom_category = fields.Many2one(
+ string="Unit of Measure Category",
+ related="size_id.uom_id.category_id",
+ )
+ size_uom = fields.Many2one(
+ "uom.uom",
+ string="Unit of Measure",
+ domain="[('category_id', '=?', size_uom_category)]",
+ compute="_compute_size_uom",
+ precompute=True,
+ readonly=False,
+ store=True,
+ )
- def _default_size_value(self):
- size_value = 0
- if self.size_id:
+ @api.depends("type")
+ def _compute_size_id(self):
+ for rec in self:
+ if rec.type:
+ rec.size_id = self.env["fsm.size"].search(
+ [("type_id", "=", rec.type.id), ("is_order_size", "=", True)],
+ limit=1,
+ )
+
+ @api.depends("size_id", "location_id")
+ def _compute_size_value(self):
+ for rec in self:
+ if not rec.size_id or not rec.location_id:
+ continue
size = self.env["fsm.location.size"].search(
[
("location_id", "=", self.location_id.id),
@@ -24,34 +55,9 @@ def _default_size_value(self):
],
limit=1,
)
- if size:
- size_value = size.quantity
- return size_value
-
- def _default_size_uom(self):
- return self.size_id.uom_id if self.size_id else False
-
- size_id = fields.Many2one("fsm.size", default=_default_size_id)
- size_value = fields.Float(string="Order Size", default=_default_size_value)
- size_uom = fields.Many2one(
- "uom.uom", string="Unit of Measure", default=_default_size_uom
- )
-
- @api.onchange("location_id")
- def _onchange_location_id_customer(self):
- res = super()._onchange_location_id_customer()
- self.size_id = self._default_size_id()
- self.size_value = self._default_size_value()
- self.size_uom = self._default_size_uom()
- return res
-
- @api.onchange("type")
- def onchange_type(self):
- self.size_id = self._default_size_id()
- self.size_value = self._default_size_value()
- self.size_uom = self._default_size_uom()
+ rec.size_value = size.quantity
- @api.onchange("size_id")
- def onchange_size_id(self):
- self.size_value = self._default_size_value()
- self.size_uom = self._default_size_uom()
+ @api.depends("size_id")
+ def _compute_size_uom(self):
+ for rec in self:
+ rec.size_uom = rec.size_id.uom_id
diff --git a/fieldservice_size/models/fsm_size.py b/fieldservice_size/models/fsm_size.py
index d99cc072cd..081e7705da 100644
--- a/fieldservice_size/models/fsm_size.py
+++ b/fieldservice_size/models/fsm_size.py
@@ -8,7 +8,7 @@ class FSMSize(models.Model):
_name = "fsm.size"
_description = "Field Service Size"
- name = fields.Char(required="True")
+ name = fields.Char(required=True)
type_id = fields.Many2one("fsm.order.type", string="Order Type")
parent_id = fields.Many2one("fsm.size", string="Parent Size", index=True)
uom_id = fields.Many2one("uom.uom", string="Unit of Measure")
diff --git a/fieldservice_size/tests/test_fsm_order.py b/fieldservice_size/tests/test_fsm_order.py
index 0de657c44a..acdb5f5868 100644
--- a/fieldservice_size/tests/test_fsm_order.py
+++ b/fieldservice_size/tests/test_fsm_order.py
@@ -52,9 +52,6 @@ def test_order_onchange_location(self):
"location_id": self.test_location.id,
}
)
- order._onchange_location_id_customer()
- order.onchange_type()
- order.onchange_size_id()
self.assertTrue(order.size_id, self.size_a.id)
self.assertTrue(order.size_value, 24.5)
self.assertTrue(order.size_uom, self.size_a.uom_id)
diff --git a/fieldservice_size/views/fsm_order.xml b/fieldservice_size/views/fsm_order.xml
index 5c0c69780f..3a58d21020 100644
--- a/fieldservice_size/views/fsm_order.xml
+++ b/fieldservice_size/views/fsm_order.xml
@@ -13,6 +13,7 @@
class="oe_inline"
style="text-align:right;"
/>
+
diff --git a/fieldservice_stock/models/fsm_order.py b/fieldservice_stock/models/fsm_order.py
index 35d49ce187..4698981a82 100644
--- a/fieldservice_stock/models/fsm_order.py
+++ b/fieldservice_stock/models/fsm_order.py
@@ -7,14 +7,6 @@
class FSMOrder(models.Model):
_inherit = "fsm.order"
- @api.model
- def _default_warehouse_id(self):
- company = self.env.user.company_id
- warehouse_ids = self.env["stock.warehouse"].search(
- [("company_id", "=", company.id)], limit=1
- )
- return warehouse_ids and warehouse_ids.id
-
@api.model
def _get_move_domain(self):
return [("picking_id.picking_type_id.code", "in", ("outgoing", "incoming"))]
@@ -32,8 +24,11 @@ def _get_move_domain(self):
warehouse_id = fields.Many2one(
"stock.warehouse",
string="Warehouse",
+ compute="_compute_warehouse_id",
+ precompute=True,
+ store=True,
+ readonly=False,
required=True,
- default=_default_warehouse_id,
help="Warehouse used to ship the materials",
)
return_count = fields.Integer(
@@ -43,6 +38,20 @@ def _get_move_domain(self):
"stock.move", "fsm_order_id", string="Operations", domain=_get_move_domain
)
+ @api.depends("company_id", "territory_id")
+ def _compute_warehouse_id(self):
+ """Compute the warehouse from the territory"""
+ for rec in self:
+ # If the territory's warehouse is set and it matches the company, use it.
+ if rec.territory_id.warehouse_id.company_id == rec.company_id:
+ rec.warehouse_id = rec.territory_id.warehouse_id
+ continue
+ # Otherwise, use the company's warehouse
+ company = rec.company_id or self.env.user.company_id
+ rec.warehouse_id = self.env["stock.warehouse"].search(
+ [("company_id", "=", company.id)], limit=1
+ )
+
@api.depends("picking_ids")
def _compute_picking_ids(self):
for order in self:
diff --git a/fieldservice_stock/tests/test_fsm_stock.py b/fieldservice_stock/tests/test_fsm_stock.py
index 1f1c4be2ba..f416aa442f 100644
--- a/fieldservice_stock/tests/test_fsm_stock.py
+++ b/fieldservice_stock/tests/test_fsm_stock.py
@@ -14,10 +14,14 @@ def setUp(self):
self.location = self.env["fsm.location"]
self.FSMOrder = self.env["fsm.order"]
self.Product = self.env["product.product"].search([], limit=1)
+ self.warehouse = self.env["stock.warehouse"].search(
+ [("company_id", "=", self.env.company.id)], limit=1
+ )
self.stock_cust_loc = self.env.ref("stock.stock_location_customers")
self.stock_location = self.env.ref("stock.stock_location_stock")
self.customer_location = self.env.ref("stock.stock_location_customers")
self.test_location = self.env.ref("fieldservice.test_location")
+ self.test_territory = self.env.ref("base_territory.test_territory")
self.partner_1 = (
self.env["res.partner"]
.with_context(tracking_disable=True)
@@ -144,8 +148,32 @@ def test_fsm_orders(self):
order.picking_ids = [(6, 0, order_pick_list2)]
order._compute_picking_ids()
order.location_id._onchange_fsm_parent_id()
- order._default_warehouse_id()
order.action_view_delivery()
order2.action_view_delivery()
order3.action_view_returns()
order.action_view_returns()
+
+ def test_order_warehouse_from_territory(self):
+ self.test_territory.warehouse_id = self.warehouse
+ order = self.env["fsm.order"].create(
+ {
+ "location_id": self.test_location.id,
+ }
+ )
+ self.assertEqual(
+ order.warehouse_id,
+ self.test_territory.warehouse_id,
+ "Warehouse should be assigned from territory",
+ )
+
+ def test_order_warehouse_default_from_company(self):
+ order = self.env["fsm.order"].create(
+ {
+ "location_id": self.test_location.id,
+ }
+ )
+ self.assertEqual(
+ order.warehouse_id,
+ self.warehouse,
+ "Warehouse should have a default value from company",
+ )