From 8dd533f89a75fb59ec00aeec1ebf23437350a48a Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:36:40 +0700 Subject: [PATCH] Add work types and in-form technician slot suggestions --- .../dsrpt_repair_config/__manifest__.py | 4 + .../dsrpt_repair_config/models/__init__.py | 1 + .../dsrpt_repair_config/models/work_type.py | 12 + .../security/ir.model.access.csv | 2 + .../addons/dsrpt_repair_config/views/menu.xml | 7 + .../views/repair_work_type_action_main.xml | 8 + .../views/repair_work_type_view_form.xml | 19 ++ .../views/repair_work_type_view_kanban.xml | 25 ++ .../views/repair_work_type_view_list.xml | 14 + .../models/technician.py | 2 + .../security/ir.model.access.csv | 2 + .../views/repair_technician_view_form.xml | 2 + .../views/repair_technician_view_kanban.xml | 4 + .../views/repair_technician_view_list.xml | 2 + .../dsrpt_repair_work_orders/__manifest__.py | 1 + .../data/slot_horizon_settings.xml | 7 + .../models/work_order.py | 258 +++++++++++++++++- .../security/ir.model.access.csv | 4 + .../views/repair_work_order_view_form.xml | 74 +++-- .../views/repair_work_order_view_kanban.xml | 8 + .../views/repair_work_order_view_list.xml | 2 + 21 files changed, 424 insertions(+), 34 deletions(-) create mode 100644 odoo/addons/dsrpt_repair_config/models/work_type.py create mode 100644 odoo/addons/dsrpt_repair_config/views/repair_work_type_action_main.xml create mode 100644 odoo/addons/dsrpt_repair_config/views/repair_work_type_view_form.xml create mode 100644 odoo/addons/dsrpt_repair_config/views/repair_work_type_view_kanban.xml create mode 100644 odoo/addons/dsrpt_repair_config/views/repair_work_type_view_list.xml create mode 100644 odoo/addons/dsrpt_repair_work_orders/data/slot_horizon_settings.xml diff --git a/odoo/addons/dsrpt_repair_config/__manifest__.py b/odoo/addons/dsrpt_repair_config/__manifest__.py index 9502f24..80ed437 100644 --- a/odoo/addons/dsrpt_repair_config/__manifest__.py +++ b/odoo/addons/dsrpt_repair_config/__manifest__.py @@ -15,6 +15,10 @@ "views/repair_fsm_zone_view_form.xml", "views/repair_fsm_zone_view_kanban.xml", "views/repair_fsm_zone_action_main.xml", + "views/repair_work_type_view_list.xml", + "views/repair_work_type_view_form.xml", + "views/repair_work_type_view_kanban.xml", + "views/repair_work_type_action_main.xml", "views/menu.xml", ], "assets": { diff --git a/odoo/addons/dsrpt_repair_config/models/__init__.py b/odoo/addons/dsrpt_repair_config/models/__init__.py index 588c333..f8ab3e7 100644 --- a/odoo/addons/dsrpt_repair_config/models/__init__.py +++ b/odoo/addons/dsrpt_repair_config/models/__init__.py @@ -1 +1,2 @@ from . import fsm_zone +from . import work_type diff --git a/odoo/addons/dsrpt_repair_config/models/work_type.py b/odoo/addons/dsrpt_repair_config/models/work_type.py new file mode 100644 index 0000000..f01f635 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/models/work_type.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class RepairWorkType(models.Model): + _name = "repair.work.type" + _description = "Repair Work Type" + _order = "name" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(required=True, tracking=True) + duration_min = fields.Integer(default=120, required=True, tracking=True) + active = fields.Boolean(default=True, tracking=True) diff --git a/odoo/addons/dsrpt_repair_config/security/ir.model.access.csv b/odoo/addons/dsrpt_repair_config/security/ir.model.access.csv index 56cc552..6a46944 100644 --- a/odoo/addons/dsrpt_repair_config/security/ir.model.access.csv +++ b/odoo/addons/dsrpt_repair_config/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_repair_fsm_zone_user,repair.fsm.zone user,model_repair_fsm_zone,dsrpt_repair_config.group_dsrpt_repair_config_user,1,1,1,0 access_repair_fsm_zone_manager,repair.fsm.zone manager,model_repair_fsm_zone,dsrpt_repair_config.group_dsrpt_repair_config_manager,1,1,1,1 +access_repair_work_type_user,repair.work.type user,model_repair_work_type,dsrpt_repair_config.group_dsrpt_repair_config_user,1,1,1,0 +access_repair_work_type_manager,repair.work.type manager,model_repair_work_type,dsrpt_repair_config.group_dsrpt_repair_config_manager,1,1,1,1 diff --git a/odoo/addons/dsrpt_repair_config/views/menu.xml b/odoo/addons/dsrpt_repair_config/views/menu.xml index 53c054f..a01516b 100644 --- a/odoo/addons/dsrpt_repair_config/views/menu.xml +++ b/odoo/addons/dsrpt_repair_config/views/menu.xml @@ -13,4 +13,11 @@ 10 + + + Work Types + + + 20 + diff --git a/odoo/addons/dsrpt_repair_config/views/repair_work_type_action_main.xml b/odoo/addons/dsrpt_repair_config/views/repair_work_type_action_main.xml new file mode 100644 index 0000000..1901e4b --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/views/repair_work_type_action_main.xml @@ -0,0 +1,8 @@ + + + + Work Types + repair.work.type + list,kanban,form + + diff --git a/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_form.xml b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_form.xml new file mode 100644 index 0000000..87ae727 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_form.xml @@ -0,0 +1,19 @@ + + + + repair.work.type.form + repair.work.type + +
+ + + + + + + + + +
+
+
diff --git a/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_kanban.xml b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_kanban.xml new file mode 100644 index 0000000..d6c4f0e --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_kanban.xml @@ -0,0 +1,25 @@ + + + + repair.work.type.kanban + repair.work.type + + + + + + +
+
+ +
+
+ min +
+
+
+
+
+
+
+
diff --git a/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_list.xml b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_list.xml new file mode 100644 index 0000000..818f6e3 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/views/repair_work_type_view_list.xml @@ -0,0 +1,14 @@ + + + + repair.work.type.list + repair.work.type + + + + + + + + + diff --git a/odoo/addons/dsrpt_repair_technicians/models/technician.py b/odoo/addons/dsrpt_repair_technicians/models/technician.py index df8304a..adace1c 100644 --- a/odoo/addons/dsrpt_repair_technicians/models/technician.py +++ b/odoo/addons/dsrpt_repair_technicians/models/technician.py @@ -11,6 +11,8 @@ class RepairTechnician(models.Model): name = fields.Char(required=True, tracking=True) user_id = fields.Many2one("res.users", string="User", tracking=True) zone_ids = fields.Many2many("repair.fsm.zone", string="FSM Zones", tracking=True) + work_type_ids = fields.Many2many("repair.work.type", string="Work Types", tracking=True) + available_until = fields.Date(string="Available Until", tracking=True) state = fields.Selection( selection=[ ("draft", "Draft"), diff --git a/odoo/addons/dsrpt_repair_technicians/security/ir.model.access.csv b/odoo/addons/dsrpt_repair_technicians/security/ir.model.access.csv index aa7fed1..850cabb 100644 --- a/odoo/addons/dsrpt_repair_technicians/security/ir.model.access.csv +++ b/odoo/addons/dsrpt_repair_technicians/security/ir.model.access.csv @@ -5,3 +5,5 @@ access_repair_technician_schedule_user,repair.technician.schedule user,model_rep access_repair_technician_schedule_manager,repair.technician.schedule manager,model_repair_technician_schedule,dsrpt_repair_technicians.group_dsrpt_repair_technicians_manager,1,1,1,1 access_repair_technician_exception_user,repair.technician.exception user,model_repair_technician_exception,dsrpt_repair_technicians.group_dsrpt_repair_technicians_user,1,1,1,0 access_repair_technician_exception_manager,repair.technician.exception manager,model_repair_technician_exception,dsrpt_repair_technicians.group_dsrpt_repair_technicians_manager,1,1,1,1 +access_repair_work_type_for_technician_user,repair.work.type for technician user,model_repair_work_type,dsrpt_repair_technicians.group_dsrpt_repair_technicians_user,1,0,0,0 +access_repair_work_type_for_technician_manager,repair.work.type for technician manager,model_repair_work_type,dsrpt_repair_technicians.group_dsrpt_repair_technicians_manager,1,0,0,0 diff --git a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_form.xml b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_form.xml index 21fb04e..a5462a7 100644 --- a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_form.xml +++ b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_form.xml @@ -16,6 +16,8 @@ + + diff --git a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_kanban.xml b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_kanban.xml index 00933d9..bb24383 100644 --- a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_kanban.xml +++ b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_kanban.xml @@ -8,6 +8,7 @@ + @@ -21,6 +22,9 @@
+
+ +
diff --git a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_list.xml b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_list.xml index 169de95..ab62c1d 100644 --- a/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_list.xml +++ b/odoo/addons/dsrpt_repair_technicians/views/repair_technician_view_list.xml @@ -8,6 +8,8 @@ + + diff --git a/odoo/addons/dsrpt_repair_work_orders/__manifest__.py b/odoo/addons/dsrpt_repair_work_orders/__manifest__.py index 519aabb..6f7912c 100644 --- a/odoo/addons/dsrpt_repair_work_orders/__manifest__.py +++ b/odoo/addons/dsrpt_repair_work_orders/__manifest__.py @@ -16,6 +16,7 @@ "data": [ "security/groups.xml", "security/ir.model.access.csv", + "data/slot_horizon_settings.xml", "data/sequence.xml", "views/repair_work_order_view_list.xml", "views/repair_work_order_view_form.xml", diff --git a/odoo/addons/dsrpt_repair_work_orders/data/slot_horizon_settings.xml b/odoo/addons/dsrpt_repair_work_orders/data/slot_horizon_settings.xml new file mode 100644 index 0000000..2dbbc59 --- /dev/null +++ b/odoo/addons/dsrpt_repair_work_orders/data/slot_horizon_settings.xml @@ -0,0 +1,7 @@ + + + + dsrpt_repair_work_orders.slot_horizon_days + 15 + + diff --git a/odoo/addons/dsrpt_repair_work_orders/models/work_order.py b/odoo/addons/dsrpt_repair_work_orders/models/work_order.py index 95b1a1d..3b1080d 100644 --- a/odoo/addons/dsrpt_repair_work_orders/models/work_order.py +++ b/odoo/addons/dsrpt_repair_work_orders/models/work_order.py @@ -1,4 +1,8 @@ -from odoo import api, fields, models +from datetime import datetime, time, timedelta + +import pytz + +from odoo import Command, api, fields, models from odoo.exceptions import ValidationError @@ -24,11 +28,20 @@ class RepairWorkOrder(models.Model): readonly=True, tracking=True, ) + work_type_id = fields.Many2one("repair.work.type", string="Work Type", tracking=True) description = fields.Text(tracking=True) requested_datetime = fields.Datetime(default=fields.Datetime.now, tracking=True) - scheduled_datetime = fields.Datetime(tracking=True) + scheduled_datetime = fields.Datetime(string="Scheduled Start", tracking=True) + scheduled_end = fields.Datetime(string="Scheduled End", tracking=True) technician_id = fields.Many2one("repair.technician", tracking=True) assigned_user_id = fields.Many2one("res.users", related="technician_id.user_id", store=True) + slot_day = fields.Date(default=fields.Date.context_today, tracking=True) + available_slot_ids = fields.One2many( + "repair.work.order.slot", + "work_order_id", + string="Available Slots", + copy=False, + ) state = fields.Selection( selection=[ ("draft", "Draft"), @@ -52,7 +65,24 @@ class RepairWorkOrder(models.Model): for vals in vals_list: if vals.get("name", "New") == "New": vals["name"] = self.env["ir.sequence"].next_by_code("repair.work.order") or "New" - return super().create(vals_list) + records = super().create(vals_list) + records._refresh_available_slots_saved() + return records + + def write(self, vals): + result = super().write(vals) + trigger_fields = { + "work_type_id", + "slot_day", + "contact_address_id", + "zone_id", + "scheduled_datetime", + "scheduled_end", + "technician_id", + } + if trigger_fields.intersection(vals): + self._refresh_available_slots_saved() + return result def _group_expand_states(self, states, domain, order): return [key for key, _label in self._fields["state"].selection] @@ -66,8 +96,7 @@ class RepairWorkOrder(models.Model): def _find_zone_for_point(self, latitude, longitude): if latitude is False or longitude is False or latitude is None or longitude is None: return self.env["repair.fsm.zone"] - zone_domain = [("state", "=", "active")] if "state" in self.env["repair.fsm.zone"]._fields else [] - zones = self.env["repair.fsm.zone"].search(zone_domain) + zones = self.env["repair.fsm.zone"].search([("state", "=", "active")]) for zone in zones: if zone.contains_point(latitude, longitude): return zone @@ -88,6 +117,189 @@ class RepairWorkOrder(models.Model): if rec.contact_address_id and rec.contact_address_id.contact_id != rec.contact_id: rec.contact_address_id = False + @api.onchange("work_type_id", "slot_day", "contact_address_id", "zone_id") + def _onchange_recompute_available_slots(self): + for rec in self: + slot_values = rec._build_available_slot_values() + rec.available_slot_ids = [Command.clear(), *[Command.create(vals) for vals in slot_values]] + + @staticmethod + def _merge_intervals(intervals): + if not intervals: + return [] + sorted_intervals = sorted(intervals, key=lambda item: item[0]) + merged = [sorted_intervals[0]] + for start_dt, end_dt in sorted_intervals[1:]: + last_start, last_end = merged[-1] + if start_dt <= last_end: + merged[-1] = (last_start, max(last_end, end_dt)) + else: + merged.append((start_dt, end_dt)) + return merged + + @staticmethod + def _subtract_intervals(source, cutouts): + if not source: + return [] + if not cutouts: + return source + result = [] + for src_start, src_end in source: + segments = [(src_start, src_end)] + for cut_start, cut_end in cutouts: + next_segments = [] + for seg_start, seg_end in segments: + if cut_end <= seg_start or cut_start >= seg_end: + next_segments.append((seg_start, seg_end)) + continue + if cut_start > seg_start: + next_segments.append((seg_start, min(cut_start, seg_end))) + if cut_end < seg_end: + next_segments.append((max(cut_end, seg_start), seg_end)) + segments = next_segments + if not segments: + break + result.extend(segments) + return RepairWorkOrder._merge_intervals(result) + + def _get_slot_horizon_days(self): + raw_value = self.env["ir.config_parameter"].sudo().get_param("dsrpt_repair_work_orders.slot_horizon_days", "15") + try: + parsed = int(raw_value) + except (TypeError, ValueError): + parsed = 15 + return max(parsed, 1) + + def _is_day_in_horizon(self, day_value): + if not day_value: + return False + today = fields.Date.context_today(self) + horizon_end = today + timedelta(days=self._get_slot_horizon_days()) + return today <= day_value <= horizon_end + + def _day_bounds_utc(self, day_value): + user_tz_name = self.env.user.tz or "UTC" + user_tz = pytz.timezone(user_tz_name) + local_start = user_tz.localize(datetime.combine(day_value, time.min)) + local_end = local_start + timedelta(days=1) + utc_start = local_start.astimezone(pytz.UTC).replace(tzinfo=None) + utc_end = local_end.astimezone(pytz.UTC).replace(tzinfo=None) + return utc_start, utc_end, user_tz + + @staticmethod + def _hour_to_utc(day_value, float_hour, user_tz): + minutes = int(round(float(float_hour or 0.0) * 60)) + local_dt = user_tz.localize(datetime.combine(day_value, time.min) + timedelta(minutes=minutes)) + return local_dt.astimezone(pytz.UTC).replace(tzinfo=None) + + def _technician_day_availability(self, technician, day_value, day_start_utc, day_end_utc, user_tz): + weekday = str(day_value.weekday()) + base_intervals = [] + for schedule in technician.schedule_ids.filtered(lambda rec: rec.day_of_week == weekday): + start_dt = self._hour_to_utc(day_value, schedule.hour_from, user_tz) + end_dt = self._hour_to_utc(day_value, schedule.hour_to, user_tz) + if end_dt > start_dt: + base_intervals.append((start_dt, end_dt)) + available = self._merge_intervals(base_intervals) + if not available: + return [] + + exc_domain = [ + ("technician_id", "=", technician.id), + ("start_datetime", "<", day_end_utc), + ("end_datetime", ">", day_start_utc), + ] + exceptions = self.env["repair.technician.exception"].search(exc_domain) + negative = [] + positive = [] + for exc in exceptions: + interval = (max(exc.start_datetime, day_start_utc), min(exc.end_datetime, day_end_utc)) + if interval[1] <= interval[0]: + continue + if exc.exception_type == "negative": + negative.append(interval) + else: + positive.append(interval) + available = self._subtract_intervals(available, self._merge_intervals(negative)) + available = self._merge_intervals([*available, *positive]) + + busy_domain = [ + ("technician_id", "=", technician.id), + ("scheduled_datetime", "!=", False), + ("scheduled_datetime", "<", day_end_utc), + ("state", "not in", ["cancelled"]), + ] + if self.id: + busy_domain.append(("id", "!=", self.id)) + busy_orders = self.env["repair.work.order"].search(busy_domain) + busy = [] + for order in busy_orders: + start_dt = order.scheduled_datetime + duration_min = order.work_type_id.duration_min or self.work_type_id.duration_min or 120 + end_dt = order.scheduled_end or (start_dt + timedelta(minutes=duration_min)) + if end_dt <= day_start_utc or start_dt >= day_end_utc: + continue + busy.append((max(start_dt, day_start_utc), min(end_dt, day_end_utc))) + return self._subtract_intervals(available, self._merge_intervals(busy)) + + def _candidate_technicians(self, day_value): + domain = [("state", "=", "active")] + if self.zone_id: + domain.append(("zone_ids", "in", self.zone_id.id)) + if self.work_type_id: + domain.append(("work_type_ids", "in", self.work_type_id.id)) + if day_value: + domain.extend(["|", ("available_until", "=", False), ("available_until", ">=", day_value)]) + return self.env["repair.technician"].search(domain) + + def _build_available_slot_values(self): + self.ensure_one() + if not self.work_type_id or not self.zone_id or not self.slot_day: + return [] + if not self._is_day_in_horizon(self.slot_day): + return [] + + duration = timedelta(minutes=max(self.work_type_id.duration_min or 0, 15)) + step = timedelta(minutes=30) + day_start_utc, day_end_utc, user_tz = self._day_bounds_utc(self.slot_day) + technicians = self._candidate_technicians(self.slot_day) + candidates = [] + for technician in technicians: + free_intervals = self._technician_day_availability(technician, self.slot_day, day_start_utc, day_end_utc, user_tz) + for interval_start, interval_end in free_intervals: + cursor = interval_start + while cursor + duration <= interval_end: + candidates.append( + { + "technician_id": technician.id, + "start_datetime": cursor, + "end_datetime": cursor + duration, + "duration_min": int(duration.total_seconds() // 60), + } + ) + cursor += step + + candidates.sort(key=lambda item: (item["start_datetime"], item["technician_id"])) + return candidates[:3] + + def _refresh_available_slots_saved(self): + for rec in self: + if not rec.id: + continue + values = rec._build_available_slot_values() + rec.available_slot_ids.unlink() + if values: + self.env["repair.work.order.slot"].create( + [ + { + "work_order_id": rec.id, + "sequence": index, + **slot_vals, + } + for index, slot_vals in enumerate(values, start=1) + ] + ) + def action_confirm(self): self.write({"state": "confirmed"}) @@ -110,6 +322,42 @@ class RepairWorkOrder(models.Model): self.write({"state": "draft"}) +class RepairWorkOrderSlot(models.Model): + _name = "repair.work.order.slot" + _description = "Repair Work Order Available Slot" + _order = "sequence, start_datetime, id" + + work_order_id = fields.Many2one("repair.work.order", required=True, ondelete="cascade") + sequence = fields.Integer(default=10) + technician_id = fields.Many2one("repair.technician", required=True) + start_datetime = fields.Datetime(required=True) + end_datetime = fields.Datetime(required=True) + duration_min = fields.Integer(required=True) + + @api.constrains("start_datetime", "end_datetime") + def _check_datetime_order(self): + for rec in self: + if rec.end_datetime <= rec.start_datetime: + raise ValidationError("Slot end must be after slot start.") + + def action_book(self): + self.ensure_one() + order = self.work_order_id + next_state = order.state + if order.state in ("draft", "confirmed"): + next_state = "assigned" + order.write( + { + "technician_id": self.technician_id.id, + "scheduled_datetime": self.start_datetime, + "scheduled_end": self.end_datetime, + "state": next_state, + } + ) + order._refresh_available_slots_saved() + return {"type": "ir.actions.client", "tag": "reload"} + + class RepairWorkOrderTime(models.Model): _name = "repair.work.order.time" _description = "Repair Work Order Time" diff --git a/odoo/addons/dsrpt_repair_work_orders/security/ir.model.access.csv b/odoo/addons/dsrpt_repair_work_orders/security/ir.model.access.csv index cb4163f..427f771 100644 --- a/odoo/addons/dsrpt_repair_work_orders/security/ir.model.access.csv +++ b/odoo/addons/dsrpt_repair_work_orders/security/ir.model.access.csv @@ -5,3 +5,7 @@ access_repair_work_order_time_user,repair.work.order.time user,model_repair_work access_repair_work_order_time_manager,repair.work.order.time manager,model_repair_work_order_time,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_manager,1,1,1,1 access_repair_work_order_material_user,repair.work.order.material user,model_repair_work_order_material,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_user,1,1,1,0 access_repair_work_order_material_manager,repair.work.order.material manager,model_repair_work_order_material,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_manager,1,1,1,1 +access_repair_work_order_slot_user,repair.work.order.slot user,model_repair_work_order_slot,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_user,1,1,1,1 +access_repair_work_order_slot_manager,repair.work.order.slot manager,model_repair_work_order_slot,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_manager,1,1,1,1 +access_repair_work_type_for_work_orders_user,repair.work.type for work orders user,model_repair_work_type,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_user,1,0,0,0 +access_repair_work_type_for_work_orders_manager,repair.work.type for work orders manager,model_repair_work_type,dsrpt_repair_work_orders.group_dsrpt_repair_work_orders_manager,1,0,0,0 diff --git a/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml b/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml index 788a359..c38dec5 100644 --- a/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml +++ b/odoo/addons/dsrpt_repair_work_orders/views/repair_work_order_view_form.xml @@ -15,49 +15,65 @@ - + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + +