from odoo import fields, models from odoo.tools.safe_eval import safe_eval import logging _logger = logging.getLogger(__name__) class DirectivePolicy(models.Model): _name = "directive.policy" _description = "Directive Policy" _order = "sequence, id" name = fields.Char(required=True) active = fields.Boolean(default=True) sequence = fields.Integer(default=10) origin_model = fields.Char( string="Origin model", required=True, index=True, help="Техническое имя модели источника, например 'project.task'", ) event = fields.Selection( [ ("manual", "Manual"), ("on_create", "On Create"), ("on_write", "On Write"), ("on_stage", "On Stage Change"), ("on_cron", "On Cron"), ("on_callback", "При вызове метода (on_callback)"), ], required=True, index=True, default="manual", ) mode = fields.Selection( [ ("create_once", "Create once"), ("create_each_time", "Create each time"), ], required=True, default="create_once", ) use_record_template_group = fields.Boolean( default=True, help="Если включено, группа берётся из поля записи-источника directive_template_group_id.", ) template_group_id = fields.Many2one( "directive.template.group", string="Fixed template group", help="Если указано и use_record_template_group=False — будет использоваться эта группа.", ) trigger_field = fields.Char( help="Имя поля источника, например 'stage_id'." ) trigger_value_int = fields.Integer( help="Для Many2one/Integer: ожидаемое значение (например ID стадии)." ) trigger_value_char = fields.Char( help="Для Char/Selection-полей (например mrp.workorder.state='progress'): " "ожидаемое строковое значение. Для Many2one/Integer использовать trigger_value_int.", ) domain_filter = fields.Char( string="Доменный фильтр источника", help="Odoo-домен (строка), применяется к записи-источнику перед генерацией. " "Пусто — без фильтра.", ) def _policy_should_fire(self, record, payload: dict | None = None) -> bool: self.ensure_one() # (1) Доменный фильтр источника применяется ко ВСЕМ событиям (вкл. on_callback). if self.domain_filter: try: domain = safe_eval(self.domain_filter) or [] except Exception as e: _logger.error("[directive.policy %s] bad domain_filter: %s", self.name, e) return False if domain and not record.filtered_domain(domain): return False # (2) Для on_callback факт вызова метода - это уже триггер (домен уже отфильтровал). if self.event == 'on_callback': return True if not self.trigger_field: return True value = getattr(record, self.trigger_field, False) # (3a) Many2one: сравнение по id с trigger_value_int. if hasattr(value, "id"): return bool(value and value.id == self.trigger_value_int) # (3b) Char/Selection: сравнение с trigger_value_char. ПРАВИЛО fail-loud: # если trigger_value_char пуст, НЕ проваливаемся в целочисленную ветку # (сравнение 'progress' == 0 всегда False) — логируем и возвращаем False. if isinstance(value, str): if self.trigger_value_char: return value == self.trigger_value_char _logger.warning( "[directive.policy %s] trigger_field '%s' вернул строку '%s', но " "trigger_value_char не задан — политика не срабатывает.", self.name, self.trigger_field, value, ) return False # (3c) Integer/число: сравнение с trigger_value_int (как сейчас — test_01 'color'). try: return value == self.trigger_value_int except Exception as e: _logger.error("[directive.policy] Error comparing values: %s", e) return False def _resolve_template_group(self, record): self.ensure_one() if self.use_record_template_group: return record.directive_template_group_id return self.template_group_id def apply_to_records(self, records, payload: dict | None = None): self.ensure_one() if not records: return 0 created_total = 0 for rec in records: if rec._name != self.origin_model: continue if not self._policy_should_fire(rec, payload=payload): continue group = self._resolve_template_group(rec) if not group: continue if self.mode == "create_once": existing = rec.env["directive.directive"].search_count([ ("origin_id", "=", rec.directive_origin_id.id), ("policy_id", "=", self.id), ]) if existing: continue rec._directive_create_from_group(group, policy=self) created_total += 1 return created_total