From c7d0a1be2b4f51d99da15ed1b188718294040dcd Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 9 Sep 2025 11:38:26 -0400 Subject: [PATCH] [MIG] fsm_equipment to 18.0 --- fsm_equipment/__init__.py | 1 + fsm_equipment/__manifest__.py | 38 ++++ fsm_equipment/models/__init__.py | 4 + fsm_equipment/models/equipment.py | 144 ++++++++++++++++ fsm_equipment/models/equipment_tag.py | 19 ++ fsm_equipment/models/res_partner.py | 51 ++++++ fsm_equipment/models/task.py | 14 ++ fsm_equipment/security/ir.model.access.csv | 3 + fsm_equipment/views/equipment_views.xml | 192 +++++++++++++++++++++ fsm_equipment/views/project_task_views.xml | 14 ++ fsm_equipment/views/res_partner_views.xml | 57 ++++++ 11 files changed, 537 insertions(+) create mode 100644 fsm_equipment/__init__.py create mode 100644 fsm_equipment/__manifest__.py create mode 100644 fsm_equipment/models/__init__.py create mode 100644 fsm_equipment/models/equipment.py create mode 100644 fsm_equipment/models/equipment_tag.py create mode 100644 fsm_equipment/models/res_partner.py create mode 100644 fsm_equipment/models/task.py create mode 100644 fsm_equipment/security/ir.model.access.csv create mode 100644 fsm_equipment/views/equipment_views.xml create mode 100644 fsm_equipment/views/project_task_views.xml create mode 100644 fsm_equipment/views/res_partner_views.xml diff --git a/fsm_equipment/__init__.py b/fsm_equipment/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/fsm_equipment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fsm_equipment/__manifest__.py b/fsm_equipment/__manifest__.py new file mode 100644 index 0000000..681ab0c --- /dev/null +++ b/fsm_equipment/__manifest__.py @@ -0,0 +1,38 @@ +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the GNU Lesser General Public License, +# version 3. +# +# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + "name": "FSM Equipment", + "version": "18.0.0.2.0", + "summary": "Add the notion of client equipment for Field Service", + "category": "Services/Field Service", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["industry_fsm", "account", "contacts", "incrementing_sequence_mixin"], + "data": [ + "security/ir.model.access.csv", + "views/equipment_views.xml", + "views/res_partner_views.xml", + "views/project_task_views.xml", + ], + "assets": {}, + "installable": True, + "auto_install": False, +} diff --git a/fsm_equipment/models/__init__.py b/fsm_equipment/models/__init__.py new file mode 100644 index 0000000..0df2c04 --- /dev/null +++ b/fsm_equipment/models/__init__.py @@ -0,0 +1,4 @@ +from . import equipment_tag +from . import equipment +from . import res_partner +from . import task diff --git a/fsm_equipment/models/equipment.py b/fsm_equipment/models/equipment.py new file mode 100644 index 0000000..18cd5c9 --- /dev/null +++ b/fsm_equipment/models/equipment.py @@ -0,0 +1,144 @@ +from odoo import models, fields, api, _ +from odoo.osv import expression +from odoo.exceptions import ValidationError + + +class Equipment(models.Model): + _name = "fsm.equipment" + _description = "Partner-Owned Equipment" + _inherit = ["mail.thread", "mail.activity.mixin", "incrementing.sequence.mixin"] + _sequence_group = "parent_id" + + code = fields.Char( + tracking=True, + ) + name = fields.Char( + tracking=True, + required=True, + ) + + tag_ids = fields.Many2many( + comodel_name="fsm.equipment.tag", + string="Tags", + ) + + description = fields.Text(tracking=True) + + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Physical Address", + tracking=True, + ondelete="restrict", + required=False, + ) + + location_notes = fields.Text( + string="Physical Location Notes", + tracking=True, + help="Any useful information about physical location " + "(door codes, directions, etc.).", + ) + + task_ids = fields.Many2many( + comodel_name="project.task", + relation="fsm_task_equipment_rel", + column1="equipment_id", + column2="task_id", + string="Interventions", + ) + + active = fields.Boolean( + default=True, + tracking=True, + ) + + parent_id = fields.Many2one( + "fsm.equipment", + tracking=True, + ) + inherited_partner_id = fields.Many2one( + comodel_name="res.partner", + string="Main Equipment Location", + readonly=True, + compute="_compute_inherited_partner_id", + recursive=True, + ) + + child_ids = fields.One2many( + "fsm.equipment", + inverse_name="parent_id", + string="Components", + tracking=True, + ) + + product_id = fields.Many2one( + "product.product", + ondelete="restrict", + help="The product that represents this equipment, if any.", + ) + + @api.depends("parent_id", "parent_id.partner_id") + def _compute_inherited_partner_id(self): + for rec in self: + if not rec.parent_id: + rec.inherited_partner_id = False + continue + rec.inherited_partner_id = ( + rec.parent_id.partner_id or rec.parent_id.inherited_partner_id + ) + + @api.constrains("product_id", "partner_id") + def _constrain_only_root_has_partner(self): + for rec in self: + if rec.partner_id and rec.parent_id: + raise ValidationError( + _("Only top-level (root) equipments can be linked to a partner.") + ) + if not rec.parent_id and not rec.partner_id: + raise ValidationError( + _("Top-level (root) equipments must be linked to a partner.") + ) + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + + args = args or [] + domain = expression.AND( + [ + args, + [ + "|", + "|", + ("code", operator, name), + ("name", operator, name), + ("partner_id.name", operator, name), + ], + ] + ) + + if name: + equipments = self.search( + domain, + limit=limit, + ) + else: + equipments = self.search(args, limit=limit) + return [(equipment.id, equipment.display_name) for equipment in equipments] + + def action_view_equipment(self): + return { + "name": "Equipment", + "view_type": "form", + "view_mode": "form", + "res_id": self.id, + "context": self.env.context, + "res_model": "fsm.equipment", + "type": "ir.actions.act_window", + } + + @api.depends("code", "name") + def _compute_display_name(self): + for rec in self: + tag_part = "[%s] " % rec.code if rec.code else "" + name = rec.name or "" + rec.display_name = tag_part + name diff --git a/fsm_equipment/models/equipment_tag.py b/fsm_equipment/models/equipment_tag.py new file mode 100644 index 0000000..678eb99 --- /dev/null +++ b/fsm_equipment/models/equipment_tag.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class EquipmentTag(models.Model): + _name = "fsm.equipment.tag" + _description = "Field service equipment category" + + name = fields.Char( + required=True, + translate=True, + ) + color = fields.Integer( + "Color Index", + default=10, + ) + + _sql_constraints = [ + ("name_uniq", "unique (name)", "Tag name already exists !"), + ] diff --git a/fsm_equipment/models/res_partner.py b/fsm_equipment/models/res_partner.py new file mode 100644 index 0000000..8cc0f30 --- /dev/null +++ b/fsm_equipment/models/res_partner.py @@ -0,0 +1,51 @@ +from odoo import models, fields, api +from odoo.osv import expression + + +class Partner(models.Model): + _inherit = "res.partner" + + equipment_count = fields.Integer(compute="_compute_equipment_count") + + owned_equipment_ids = fields.One2many( + comodel_name="fsm.equipment", + compute="_compute_owned_equipment_ids", + string="Owned Equipments", + ) + + equipment_ids = fields.One2many( + comodel_name="fsm.equipment", + inverse_name="partner_id", + string="Site Equipment", + ) + + is_service_site = fields.Boolean( + compute="_compute_is_service_site", + help="A partner is a service site if it has one or more equipments.", + store=True, + compute_sudo=True, + ) + + @api.depends( + "equipment_ids", + "child_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.mapped("equipment_ids") + rec.owned_equipment_ids = ids or False + + @api.depends("equipment_ids") + def _compute_equipment_count(self): + for rec in self: + all_equipment_ids = self.env["fsm.equipment"].search( + [("partner_id", "=", rec.id), ("parent_id", "=", False)] + ) + rec.equipment_count = len(all_equipment_ids) + + @api.depends("equipment_ids") + def _compute_is_service_site(self): + for rec in self: + rec.is_service_site = bool(rec.equipment_ids) diff --git a/fsm_equipment/models/task.py b/fsm_equipment/models/task.py new file mode 100644 index 0000000..194a5b9 --- /dev/null +++ b/fsm_equipment/models/task.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class Task(models.Model): + _inherit = "project.task" + + equipment_ids = fields.Many2many( + comodel_name="fsm.equipment", + relation="fsm_task_equipment_rel", + column1="task_id", + column2="equipment_id", + string="Equipment to Service", + tracking=True, + ) diff --git a/fsm_equipment/security/ir.model.access.csv b/fsm_equipment/security/ir.model.access.csv new file mode 100644 index 0000000..1a784e3 --- /dev/null +++ b/fsm_equipment/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_equipment,fsm_equipment,model_fsm_equipment,base.group_user,1,1,1,1 +access_fsm_equipment_tag,fsm_equipment_tag,model_fsm_equipment_tag,base.group_user,1,1,1,1 diff --git a/fsm_equipment/views/equipment_views.xml b/fsm_equipment/views/equipment_views.xml new file mode 100644 index 0000000..5a4db15 --- /dev/null +++ b/fsm_equipment/views/equipment_views.xml @@ -0,0 +1,192 @@ + + + + + fsm.equipment.form + fsm.equipment + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+ + + fsm.equipment.list + fsm.equipment + + + + + + + + + + + + + fsm.equipment.list.editable + fsm.equipment + + + + + + + + + + + + + fsm.equipment.view.list.editable.no.partner + + fsm.equipment + + + True + + + +