Files
public/directive_layer/models/directive.py

362 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from odoo.exceptions import UserError
from datetime import datetime, timedelta
import pytz
from odoo import api, fields, models
TRAFFIC_LIGHT = [
("white", "White"),
("black", "Black"),
("green", "Green"),
("yellow", "Yellow"),
("red", "Red"),
]
class Directive(models.Model):
_name = "directive.directive"
_description = "Directive"
_order = "deadline_at asc, priority desc, id desc"
is_simulation = fields.Boolean(
string="Simulation Task",
default=False,
help="Флаг для отделения тестовых данных от реальных"
)
PRIORITY = [
("0", "Low"),
("1", "Normal"),
("2", "High"),
]
name = fields.Char(string="Название директивы", required=True)
active = fields.Boolean(string="Активна", default=True)
deadline_at = fields.Datetime(string="Дедлайн исполнения", required=True)
planned_hours = fields.Float(string="Трудоёмкость", required=True)
load_percent = fields.Integer(string="Процент загрузки", default=10)
executor_type = fields.Selection([
('human', 'Человек'),
('machine', 'Оборудование / Станки'),
('software', 'Программный агент / API')
], string="Тип исполнителя", default='human', required=True)
success_probability = fields.Float(string="Вероятность успеха (%)", default=98.0)
resource_cost = fields.Float(string="Стоимость ресурса (у.е.)", default=0.0)
retry_policy = fields.Selection([
('none', 'Остановиться и звать человека'),
('retry_3', 'Повторить 3 раза'),
('backoff', 'Экспоненциальная задержка')
], string="Политика отказа", default='none')
priority = fields.Selection(
selection=PRIORITY,
string="Приоритет",
default="0",
required=True,
)
work_type_id = fields.Many2one(
"directive.work.type",
string="Тип работ",
required=True,
ondelete="restrict",
index=True,
)
executor_role_id = fields.Many2one(
"res.users.role",
string="Роль исполнителя",
ondelete="restrict",
index=True,
)
requester_role_id = fields.Many2one(
"res.users.role",
string="Роль заявителя",
ondelete="restrict",
index=True,
)
stage_id = fields.Many2one(
"directive.stage",
string="Статус (для диспетчера)",
default=lambda self: self.env["directive.stage"].search([("code", "=", "draft")], limit=1),
ondelete="restrict",
index=True,
group_expand="_group_expand_stage_id"
)
state = fields.Char(
related="stage_id.code"
)
executor_stage_id = fields.Many2one(
"directive.stage",
string="Статус для исполнителя",
ondelete="restrict",
index=True,
)
requester_stage_id = fields.Many2one(
"directive.stage",
string="Статус для заявителя",
ondelete="restrict",
index=True,
)
executor_traffic_light = fields.Selection(
selection=TRAFFIC_LIGHT,
string="Светофор контроля исполнителя",
default="white",
required=True,
)
resource_traffic_light = fields.Selection(
selection=TRAFFIC_LIGHT,
string="Светофор контроля ресурсов",
default="white",
required=True,
)
planned_start_at = fields.Datetime(string="Планируемая дата начала")
planned_end_at = fields.Datetime(string="Планируемая дата окончания")
executor_user_id = fields.Many2one(
"res.users",
string="Назначенный исполнитель",
ondelete="set null",
index=True,
)
origin_id = fields.Many2one(
"directive.origin",
string="Источник (породивший документ)",
required=True,
ondelete="cascade",
index=True,
)
template_id = fields.Many2one(
"directive.template",
string="Шаблон директивы",
ondelete="set null",
index=True,
)
policy_id = fields.Many2one(
"directive.policy",
string="Policy",
ondelete="set null",
index=True,
)
stage_code = fields.Char(related="stage_id.code", store=True, readonly=True)
generator_key = fields.Char(index=True)
deadline_display = fields.Char(compute="_compute_ui_fields")
time_left_display = fields.Char(compute="_compute_ui_fields")
is_overdue = fields.Boolean(compute="_compute_ui_fields")
priority_stars = fields.Char(compute="_compute_ui_fields")
sequence = fields.Integer(string="Planner Sequence", default=10)
predecessor_ids = fields.Many2many('directive.directive', 'directive_dependencies_rel', 'directive_id',
'predecessor_id', string="Dependencies")
@api.depends("deadline_at", "priority")
def _compute_ui_fields(self):
tz = self.env.user.tz or "UTC"
tzinfo = pytz.timezone(tz)
now = datetime.now(tzinfo)
for rec in self:
if rec.deadline_at:
dt = fields.Datetime.context_timestamp(rec.with_context(tz=tz), rec.deadline_at)
rec.deadline_display = dt.strftime("%d.%m.%y %-H:%M")
rec.is_overdue = dt < now
delta = dt - now
total_min = int(delta.total_seconds() // 60)
if total_min < 0:
total_min = abs(total_min)
sign = "-"
else:
sign = ""
days = total_min // (60 * 24)
hours = (total_min % (60 * 24)) // 60
mins = total_min % 60
parts = []
if days:
parts.append(f"{days}д")
if hours:
parts.append(f"{hours}ч")
parts.append(f"{mins}м")
rec.time_left_display = f"{sign}{' '.join(parts)}"
else:
rec.deadline_display = ""
rec.time_left_display = ""
rec.is_overdue = False
p = int(rec.priority or 0)
rec.priority_stars = "" * (p + 1)
def write(self, vals):
if "stage_id" in vals:
new_stage = self.env["directive.stage"].browse(vals["stage_id"])
for rec in self:
rec._check_stage_transition(rec.stage_id, new_stage)
return super().write(vals)
def _check_stage_transition(self, from_stage, to_stage):
from_code = (from_stage.code or "").strip()
to_code = (to_stage.code or "").strip()
allowed = {
"draft": {"confirmed", "planned", "cancelled"},
"confirmed": {"planned", "in_progress", "cancelled"},
"planned": {"in_progress", "cancelled"},
"in_progress": {"paused", "done"},
"paused": {"in_progress", "cancelled"},
"done": set(),
"cancelled": set(),
}
if from_code and to_code and to_code not in allowed.get(from_code, set()):
raise UserError("Нельзя переместить из '%s' в '%s'.") % (from_stage.name, to_stage.name)
@api.model
def _group_expand_stage_id(self, stages, domain, order=None):
return stages.search([], order=order or "id asc")
def action_cancel(self):
cancelled = self.env["directive.stage"].search([("code", "=", "cancelled")], limit=1)
if not cancelled:
raise UserError("Stage with code 'cancelled' not found.")
self.write({
"stage_id": cancelled.id,
"executor_stage_id": cancelled.id,
"requester_stage_id": cancelled.id,
})
return True
def action_open_delete_wizard(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": "Удаление директивы",
"res_model": "directive.delete.wizard",
"view_mode": "form",
"target": "new",
"context": {
"active_model": self._name,
"active_id": self.id,
"active_ids": self.ids,
},
}
def _action_archive(self):
self.write({"active": False})
return True
def _action_delete_hard(self):
self.unlink()
return True
def _build_payload(self, event: str) -> dict:
self.ensure_one()
tmpl = self.template_id
return {
"event": event,
"directive_id": self.id,
"directive_name": self.name,
"template_id": tmpl.id if tmpl else None,
"template_code": tmpl.code if tmpl else None,
"work_type_id": self.work_type_id.id if self.work_type_id else None,
"executor_user_id": self.executor_user_id.id if self.executor_user_id else None,
"planned_start_at": self.planned_start_at,
"planned_end_at": self.planned_end_at,
"stage_code": self.stage_id.code if self.stage_id else None,
"priority": self.priority,
"load_percent": self.load_percent,
}
def _stage_by_code(self, code: str):
stage = self.env["directive.stage"].search([("code", "=", code)], limit=1)
if not stage:
raise UserError(f"Directive stage '{code}' not found.")
return stage
def _set_stage(self, code: str):
stage = self._stage_by_code(code)
self.write({
"stage_id": stage.id,
"executor_stage_id": stage.id,
"requester_stage_id": stage.id,
})
def _get_origin_record(self):
self.ensure_one()
origin = self.origin_id
if not origin or not origin.res_model or not origin.res_id:
return self.env[self._name].browse() # empty
return self.env[origin.res_model].browse(origin.res_id).exists()
def action_assign_to_me(self):
for rec in self:
if rec.executor_user_id:
raise UserError("Директива уже назначена исполнителю.")
if rec.stage_code not in ("confirmed", "planned"):
raise UserError("Назначить на себя можно только подтвержденную или запланированную директиву.")
rec.write({"executor_user_id": rec.env.user.id})
return True
def action_start(self):
now = fields.Datetime.now()
for rec in self:
if not rec.executor_user_id:
raise UserError("Нельзя начать директиву без назначенного исполнителя.")
if rec.stage_code not in ("confirmed", "planned"):
raise UserError("Начать можно только подтвержденную или запланированную директиву.")
if rec.planned_start_at and now < rec.planned_start_at:
raise UserError("Начать нельзя раньше планируемой даты начала.")
rec._set_stage("in_progress")
return True
def action_pause(self):
for rec in self:
if rec.stage_code != "in_progress":
raise UserError("Поставить на паузу можно только директиву в состоянии 'В работе'.")
rec._set_stage("paused")
return True
def action_done(self):
for rec in self:
if rec.stage_code != "in_progress":
raise UserError("Завершить можно только директиву в состоянии 'В работе'.")
rec._set_stage("done")
tmpl = rec.template_id
handler_key = (tmpl.handler_key or "").strip() if tmpl else ""
if handler_key:
origin_record = rec._get_origin_record()
if origin_record:
payload = rec._build_payload("done")
method = getattr(origin_record, handler_key, None)
if not method:
raise UserError(f"Handler '{handler_key}' не найден в модели источника ({origin_record._name}).")
method(payload)
return True
@api.model
def action_quick_change_stage(self, directive_id, target_code):
""" Публичный метод для вызова из JS """
directive = self.browse(directive_id)
if directive.exists():
directive._set_stage(target_code)
return True
return False