import json from urllib.parse import quote from odoo import api, fields, models from odoo.exceptions import ValidationError class RepairFsmZone(models.Model): _name = "repair.fsm.zone" _description = "FSM Zone" _order = "name" _inherit = ["mail.thread", "mail.activity.mixin"] name = fields.Char(required=True, tracking=True) code = fields.Char(tracking=True) polygon_geojson = fields.Text( string="Polygon (GeoJSON)", tracking=True, help="GeoJSON Polygon geometry. Coordinates order: [longitude, latitude].", ) polygon_map_preview = fields.Html( string="Polygon Map Preview", compute="_compute_polygon_map_preview", sanitize=False, ) state = fields.Selection( selection=[ ("draft", "Draft"), ("active", "Active"), ("archived", "Archived"), ], default="draft", tracking=True, group_expand="_group_expand_states", ) active = fields.Boolean(default=True, tracking=True) def _group_expand_states(self, states, domain, order): return [key for key, _label in self._fields["state"].selection] def action_set_active(self): for rec in self: if not rec.polygon_geojson: raise ValidationError("Polygon is required before activating the FSM Zone.") rec._extract_polygon_points() self.write({"state": "active", "active": True}) def action_archive(self): self.write({"state": "archived", "active": False}) def action_reset_draft(self): self.write({"state": "draft", "active": True}) @staticmethod def _point_in_polygon(longitude, latitude, points): inside = False j = len(points) - 1 for i, (xi, yi) in enumerate(points): xj, yj = points[j] intersects = ((yi > latitude) != (yj > latitude)) and ( longitude < (xj - xi) * (latitude - yi) / ((yj - yi) or 1e-12) + xi ) if intersects: inside = not inside j = i return inside def _extract_polygon_points(self): self.ensure_one() if not self.polygon_geojson: raise ValidationError("Polygon is required.") try: data = json.loads(self.polygon_geojson or "") except json.JSONDecodeError as exc: raise ValidationError(f"Invalid GeoJSON: {exc.msg}") from exc ring = [] if isinstance(data, dict): if data.get("type") == "Feature": data = data.get("geometry") or {} if data.get("type") != "Polygon": raise ValidationError("GeoJSON must be of type Polygon.") coords = data.get("coordinates") or [] if not coords or not isinstance(coords[0], list): raise ValidationError("Polygon coordinates are missing.") ring = coords[0] elif isinstance(data, list): ring = data else: raise ValidationError("Polygon must be a GeoJSON object or an array of points.") points = [] for pair in ring: if not isinstance(pair, (list, tuple)) or len(pair) < 2: raise ValidationError("Each polygon point must be [longitude, latitude].") points.append((float(pair[0]), float(pair[1]))) if len(points) >= 2 and points[0] == points[-1]: points = points[:-1] if len(points) < 3: raise ValidationError("Polygon must contain at least 3 points.") return points def contains_point(self, latitude, longitude): self.ensure_one() if latitude is None or longitude is None: return False if not self.polygon_geojson: return False points = self._extract_polygon_points() return self._point_in_polygon(float(longitude), float(latitude), points) def action_open_polygon_in_map(self): self.ensure_one() encoded = quote(self.polygon_geojson or '{"type":"Polygon","coordinates":[[]]}') return { "type": "ir.actions.act_url", "url": f"https://geojson.io/#data=data:application/json,{encoded}", "target": "new", } def action_open_point_picker_map(self): return { "type": "ir.actions.act_url", "url": "https://www.openstreetmap.org", "target": "new", } @api.depends("polygon_geojson") def _compute_polygon_map_preview(self): for rec in self: if not rec.polygon_geojson: rec.polygon_map_preview = "