Add work types and in-form technician slot suggestions
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from . import fsm_zone
|
||||
from . import work_type
|
||||
|
||||
12
odoo/addons/dsrpt_repair_config/models/work_type.py
Normal file
12
odoo/addons/dsrpt_repair_config/models/work_type.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
@@ -13,4 +13,11 @@
|
||||
<field name="action" eval="'ir.actions.act_window,%d' % ref('action_repair_fsm_zone')"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="menu_repair_work_type" model="ir.ui.menu">
|
||||
<field name="name">Work Types</field>
|
||||
<field name="parent_id" ref="dsrpt_repair_config.menu_repair_root"/>
|
||||
<field name="action" eval="'ir.actions.act_window,%d' % ref('action_repair_work_type')"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="action_repair_work_type" model="ir.actions.act_window">
|
||||
<field name="name">Work Types</field>
|
||||
<field name="res_model">repair.work.type</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_repair_work_type_form" model="ir.ui.view">
|
||||
<field name="name">repair.work.type.form</field>
|
||||
<field name="model">repair.work.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="duration_min"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_repair_work_type_kanban" model="ir.ui.view">
|
||||
<field name="name">repair.work.type.kanban</field>
|
||||
<field name="model">repair.work.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<field name="duration_min"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_title">
|
||||
<strong><field name="name"/></strong>
|
||||
</div>
|
||||
<div>
|
||||
<field name="duration_min"/> min
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_repair_work_type_list" model="ir.ui.view">
|
||||
<field name="name">repair.work.type.list</field>
|
||||
<field name="model">repair.work.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="duration_min" optional="show"/>
|
||||
<field name="active" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -16,6 +16,8 @@
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="zone_ids" widget="many2many_tags"/>
|
||||
<field name="work_type_ids" widget="many2many_tags"/>
|
||||
<field name="available_until"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="name"/>
|
||||
<field name="user_id"/>
|
||||
<field name="zone_ids"/>
|
||||
<field name="work_type_ids"/>
|
||||
<field name="state"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
@@ -21,6 +22,9 @@
|
||||
<div>
|
||||
<field name="zone_ids" widget="many2many_tags"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="work_type_ids" widget="many2many_tags"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<field name="name"/>
|
||||
<field name="user_id" optional="show"/>
|
||||
<field name="zone_ids" widget="many2many_tags" optional="show"/>
|
||||
<field name="work_type_ids" widget="many2many_tags" optional="show"/>
|
||||
<field name="available_until" optional="show"/>
|
||||
<field name="state" widget="badge" optional="show"/>
|
||||
<field name="active" optional="hide"/>
|
||||
</list>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="param_slot_horizon_days" model="ir.config_parameter">
|
||||
<field name="key">dsrpt_repair_work_orders.slot_horizon_days</field>
|
||||
<field name="value">15</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -15,49 +15,65 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,in_progress,done,cancelled" options="{'clickable': '1'}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group col="3">
|
||||
<group>
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="contact_id"/>
|
||||
<field name="contact_address_id" domain="[('contact_id', '=', contact_id)]" options="{'no_create': True, 'no_create_edit': True}"/>
|
||||
<field name="zone_id" readonly="1"/>
|
||||
<field name="work_type_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="requested_datetime"/>
|
||||
<field name="scheduled_datetime"/>
|
||||
<field name="scheduled_end"/>
|
||||
<field name="technician_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="zone_id" readonly="1"/>
|
||||
<field name="slot_day"/>
|
||||
<field name="assigned_user_id" readonly="1"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Time Logs">
|
||||
<field name="time_line_ids" context="{'default_work_order_id': id, 'default_technician_id': technician_id}">
|
||||
<list editable="bottom">
|
||||
<field name="description"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="hours"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Material Logs">
|
||||
<field name="material_line_ids" context="{'default_work_order_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="material_id"/>
|
||||
<field name="qty"/>
|
||||
<field name="unit_cost" readonly="1"/>
|
||||
<field name="subtotal" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Totals">
|
||||
<group>
|
||||
<field name="total_time_hours" readonly="1"/>
|
||||
<field name="total_material_cost" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<group string="Available Slots">
|
||||
<field name="available_slot_ids" nolabel="1" context="{'default_work_order_id': id}">
|
||||
<list create="false" edit="false" delete="false">
|
||||
<field name="sequence" optional="hide"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="start_datetime"/>
|
||||
<field name="end_datetime"/>
|
||||
<field name="duration_min"/>
|
||||
<button name="action_book" type="object" string="Book" class="btn-primary"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
<group string="Time Logs">
|
||||
<field name="time_line_ids" context="{'default_work_order_id': id, 'default_technician_id': technician_id}">
|
||||
<list editable="bottom">
|
||||
<field name="description"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="hours"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
<group string="Material Logs">
|
||||
<field name="material_line_ids" context="{'default_work_order_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="material_id"/>
|
||||
<field name="qty"/>
|
||||
<field name="unit_cost" readonly="1"/>
|
||||
<field name="subtotal" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
<group string="Summary">
|
||||
<field name="total_time_hours" readonly="1"/>
|
||||
<field name="total_material_cost" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<field name="name"/>
|
||||
<field name="contact_id"/>
|
||||
<field name="contact_address_id"/>
|
||||
<field name="work_type_id"/>
|
||||
<field name="scheduled_datetime"/>
|
||||
<field name="scheduled_end"/>
|
||||
<field name="technician_id"/>
|
||||
<field name="state"/>
|
||||
<templates>
|
||||
@@ -23,9 +25,15 @@
|
||||
<div>
|
||||
<field name="contact_address_id"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="work_type_id"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="scheduled_datetime"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="scheduled_end"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="technician_id"/>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
<field name="name"/>
|
||||
<field name="contact_id" optional="show"/>
|
||||
<field name="contact_address_id" optional="show"/>
|
||||
<field name="work_type_id" optional="show"/>
|
||||
<field name="zone_id" optional="show"/>
|
||||
<field name="scheduled_datetime" optional="show"/>
|
||||
<field name="scheduled_end" optional="show"/>
|
||||
<field name="technician_id" optional="show"/>
|
||||
<field name="state" widget="badge" optional="show"/>
|
||||
<field name="total_time_hours" optional="show"/>
|
||||
|
||||
Reference in New Issue
Block a user