Spaces:
Sleeping
Sleeping
| """Rules Engine: pure expansion of a BOQ item into activity + dependency drafts. | |
| Template-driven (NO hardcoded if/else on procurement_type or category). Reads | |
| activity_templates + dependency_templates via a repo; never writes to the DB. | |
| """ | |
| import re | |
| from app.rules.errors import NoTemplateError | |
| from app.rules.templates_repo import TemplatesRepo | |
| from app.rules.types import ActivityDraft, BOQItemInput, DependencyDraft, ExpandResult | |
| _PLACEHOLDER = re.compile(r"\{(\w+)\}") | |
| def _render_template(template: str, boq: BOQItemInput) -> str: | |
| return _PLACEHOLDER.sub(lambda m: str(getattr(boq, m.group(1), "") or ""), template) | |
| def _num(value) -> float | None: | |
| return None if value is None else float(value) | |
| def expand(boq: BOQItemInput, repo: TemplatesRepo) -> ExpandResult: | |
| templates = repo.find_activity_templates( | |
| category_id=boq.category_id, | |
| procurement_type=boq.procurement_type, | |
| active=True, | |
| ) | |
| if not templates: | |
| raise NoTemplateError(boq.category_id, boq.procurement_type) | |
| override_effort = boq.metadata.get("override_effort_days") | |
| override_lead = boq.metadata.get("override_lead_time_days") | |
| activities: list[ActivityDraft] = [ | |
| ActivityDraft( | |
| boq_item_id=boq.id, | |
| template_id=t["id"], | |
| kind=t["kind"], | |
| name=_render_template(t["name_template"], boq), | |
| effort_days=_num(override_effort) if override_effort is not None else _num(t["default_effort_days"]), | |
| lead_time_days=_num(override_lead) if override_lead is not None else _num(t["default_lead_time_days"]), | |
| clock=t["clock"], | |
| noisy=bool(t["noisy"]), | |
| responsibility=( | |
| boq.procurement_responsibility if t["kind"] == "PROCUREMENT" else "CONTRACTOR" | |
| ), | |
| _tag=f"{boq.id}:{t['kind']}", | |
| ) | |
| for t in templates | |
| ] | |
| dep_rows = repo.find_dependency_templates(category_id=boq.category_id, active=True) | |
| dependencies: list[DependencyDraft] = [] | |
| for d in dep_rows: | |
| pred = next((a for a in activities if a.kind == d["predecessor_kind"]), None) | |
| succ = next((a for a in activities if a.kind == d["successor_kind"]), None) | |
| if pred and succ: | |
| dependencies.append( | |
| DependencyDraft( | |
| predecessor_tag=pred._tag, | |
| successor_tag=succ._tag, | |
| dep_type=d["dep_type"], | |
| lag_days=float(d["lag_days"]), | |
| origin="TEMPLATE", | |
| ) | |
| ) | |
| return ExpandResult(activities=activities, dependencies=dependencies) | |