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", + )