from odoo import api, fields, models from odoo.exceptions import UserError from .directive_constants import ( STAGE_CONFIRMED, STAGE_DRAFT, STAGE_IN_PROGRESS, STAGE_PAUSED, STAGE_PLANNED, STAGE_DONE, STAGE_CANCELLED, ) import logging _logger = logging.getLogger(__name__) class DirectiveMixin(models.AbstractModel): _name = "directive.mixin" _description = "Directive Mixin" directive_origin_id = fields.Many2one( "directive.origin", string="Directive Origin", readonly=True, index=True, ondelete="set null", ) directive_ids = fields.One2many( "directive.directive", string="Директивы", related="directive_origin_id.directive_ids", readonly=True, ) directive_execution_status = fields.Char( string="Текущий статус исполнения", compute="_compute_directive_execution_status", store=True, ) directive_template_group_id = fields.Many2one( "directive.template.group", string="Группа шаблонов директив", ) @api.model_create_multi def create(self, vals_list): records = super().create(vals_list) records._ensure_directive_origin() return records def write(self, vals): _logger.debug( "[directive] write() called. Model: %s, IDs: %s, Keys in vals: %s", self._name, self.ids, list(vals.keys()) ) policies = self.env['directive.policy'].sudo().search([ ('origin_model', '=', self._name), ('event', '=', 'on_write'), ('active', '=', True) ]) relevant_policies = policies.filtered( lambda p: p.trigger_field and p.trigger_field in vals ) before_map = {} if relevant_policies: watched_fields = set(relevant_policies.mapped('trigger_field')) for rec in self: before_map[rec.id] = {f: rec[f] for f in watched_fields} res = super().write(vals) self._ensure_directive_origin() if relevant_policies: for policy in relevant_policies: field = policy.trigger_field records_to_process = self.env[self._name] for rec in self: before_val = before_map[rec.id].get(field) after_val = rec[field] def _norm(v): return v.id if hasattr(v, 'id') else v b_val = _norm(before_val) a_val = _norm(after_val) if b_val != a_val and policy._policy_should_fire(rec): records_to_process |= rec if records_to_process: _logger.info( "[directive] applying policy '%s' for model=%s, ids=%s", policy.name, self._name, records_to_process.ids ) policy.sudo().apply_to_records(records_to_process) return res def _ensure_directive_origin(self): for rec in self: if not rec.id: continue if rec.directive_origin_id: continue origin = self.env["directive.origin"].sudo().search( [("res_model", "=", rec._name), ("res_id", "=", rec.id)], limit=1, ) if not origin: origin = self.env["directive.origin"].sudo().create( {"res_model": rec._name, "res_id": rec.id} ) rec.sudo().write({"directive_origin_id": origin.id}) @api.depends( "directive_ids.stage_id", "directive_ids.stage_id.code", "directive_ids.deadline_at", "directive_ids.planned_end_at", ) def _compute_directive_execution_status(self): for rec in self: directives = rec.directive_ids.filtered( lambda d: d.stage_id.code not in (STAGE_DONE, STAGE_CANCELLED) ) if not directives: rec.directive_execution_status = "" continue stage_priority = { STAGE_PAUSED: 1, STAGE_IN_PROGRESS: 2, STAGE_PLANNED: 3, STAGE_CONFIRMED: 3, STAGE_DRAFT: 4, } def stage_rank(d): return stage_priority.get(d.stage_id.code, 99) current_directive = sorted(directives, key=stage_rank)[0] stage_name = current_directive.stage_id.name deadlines = directives.mapped("deadline_at") nearest_deadline = min(deadlines) if deadlines else None planned_ends = directives.mapped("planned_end_at") planned_ends = [d for d in planned_ends if d] expected_end = max(planned_ends) if planned_ends else max(deadlines) parts = [stage_name] if nearest_deadline: parts.append( f"до {fields.Datetime.to_string(nearest_deadline)[:10]}" ) if expected_end: parts.append( f"ожидается завершение к {fields.Datetime.to_string(expected_end)[:10]}" ) rec.directive_execution_status = ", ".join(parts) def _directive_run_callback_policies(self, callback_name): """Прогоняет активные политики on_callback для данного метода-источника. Вызывается ЯВНО из override штатного метода в модели-источнике (directive_common), после super(). Никакого monkey-patch. """ policies = self.env['directive.policy'].sudo().search([ ('origin_model', '=', self._name), ('event', '=', 'on_callback'), ('active', '=', True), ('trigger_field', '=', callback_name), ]) for policy in policies: policy.sudo().apply_to_records(self) def _directive_create_from_group(self, template_group, generator_key=None, policy=None): self.ensure_one() self._ensure_directive_origin() if not template_group: raise UserError("Template group is required.") draft_stage = self.env["directive.stage"].search([("code", "=", "draft")], limit=1) if not draft_stage: raise UserError("Stage with code 'draft' not found.") lines = template_group.template_line_ids.sorted(key=lambda l: (l.sequence, l.id)) if not lines: raise UserError("Template group has no lines.") directives_vals = [] for line in lines: tmpl = line.template_id directives_vals.append({ "generator_key": generator_key or False, "name": tmpl.name, "deadline_at": fields.Datetime.now(), "planned_hours": tmpl.planned_hours, "load_percent": tmpl.load_percent, "priority": tmpl.priority, "work_type_id": tmpl.work_type_id.id, "executor_role_id": tmpl.executor_role_id.id or False, "requester_role_id": tmpl.requester_role_id.id or False, "stage_id": draft_stage.id, "executor_stage_id": False, "requester_stage_id": False, "origin_id": self.directive_origin_id.id, "template_id": tmpl.id, "policy_id": policy.id if policy else False, }) return self.env["directive.directive"].sudo().create(directives_vals) def action_choose_directive_group(self): self.ensure_one() self._ensure_directive_origin() return { "type": "ir.actions.act_window", "name": "Выбор группы директив", "res_model": "directive.choose.group.wizard", "view_mode": "form", "target": "new", "context": { "active_model": self._name, "active_id": self.id, }, } def action_open_directives(self): self.ensure_one() self._ensure_directive_origin() return { "type": "ir.actions.act_window", "name": "Директивы", "res_model": "directive.directive", "view_mode": "kanban,list,form", "domain": [("origin_id", "=", self.directive_origin_id.id)], "context": { "default_origin_id": self.directive_origin_id.id, }, }