chore(pm): vendor the Aramesh/PM platform under pm/ (squash of Aramesh/PM @ 91f3271)

The PM panel (PmPanelPage -> iframe /pm/) needs the project-management
platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM,
branch feature/project-management-platform. Update via:
git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
This commit is contained in:
MMD
2026-10-07 15:14:20 +03:30
87 changed files with 23922 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
"""Integration tests for native Projects, Tasks hierarchy, and QC gating invariants."""
import uuid
from datetime import date
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
import pytest_asyncio
from app.models import (
Base,
PMUser,
Project,
ProjectMember,
ProjectStatus,
QCAnswer,
QCQuestionPhase,
Task,
TaskPriority,
TaskQCDefinition,
TaskQCQuestion,
TaskQCResult,
TaskStatus,
)
from app.qc_service import build_state, load_definitions
@pytest_asyncio.fixture
async def async_db():
engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session
await engine.dispose()
@pytest.mark.asyncio
async def test_project_and_members_creation(async_db: AsyncSession):
# 1. Create a user
user = PMUser(
username="manager1",
password_hash="fake",
display_name="مهندس مدیر",
role="project_manager",
)
async_db.add(user)
await async_db.commit()
# 2. Create a project
project = Project(
name="پروژه برج بهارستان",
description="ساخت مجتمع ۲۰ طبقه",
location="تهران",
project_manager="مهندس مدیر",
status=ProjectStatus.ACTIVE.value,
planned_start=date(2026, 9, 1),
planned_end=date(2027, 9, 1),
)
async_db.add(project)
await async_db.flush()
# 3. Add a project member
member = ProjectMember(
project_id=project.id,
user_id=user.id,
role="project_manager",
)
async_db.add(member)
await async_db.commit()
# Verify project
loaded = await async_db.scalar(select(Project).where(Project.id == project.id))
assert loaded is not None
assert loaded.name == "پروژه برج بهارستان"
assert loaded.status == "ACTIVE"
@pytest.mark.asyncio
async def test_hierarchical_tasks(async_db: AsyncSession):
# Setup project
project = Project(name="پروژه تست ساختار شکست")
async_db.add(project)
await async_db.commit()
# 1. Phase 1 (Root Task)
phase1 = Task(
project_id=project.id,
parent_id=None,
title="فاز ۱: اسکلت فلزی",
status=TaskStatus.TODO.value,
)
async_db.add(phase1)
await async_db.flush()
# 2. Activity (Subtask under Phase 1)
activity1 = Task(
project_id=project.id,
parent_id=phase1.id,
title="فعالیت ۱.۱: نصب ستون‌های طبقه اول",
status=TaskStatus.TODO.value,
)
async_db.add(activity1)
await async_db.flush()
# 3. Subtask under Activity
subtask1 = Task(
project_id=project.id,
parent_id=activity1.id,
title="زیرتسک ۱.۱.۱: بولت‌گذاری و تراز پایه",
status=TaskStatus.TODO.value,
)
async_db.add(subtask1)
await async_db.commit()
# Query hierarchy
root = await async_db.scalar(select(Task).where(Task.id == phase1.id))
assert root is not None
assert root.parent_id is None
leaf = await async_db.scalar(select(Task).where(Task.id == subtask1.id))
assert leaf is not None
assert leaf.parent_id == activity1.id
@pytest.mark.asyncio
async def test_qc_gating_invariants(async_db: AsyncSession):
project = Project(name="پروژه تست کیفیت")
async_db.add(project)
await async_db.commit()
task = Task(
project_id=project.id,
title="بتن‌ریزی فونداسیون",
status=TaskStatus.TODO.value,
)
async_db.add(task)
await async_db.flush()
# Create task QC definition with 1 pre-execution question and 1 execution question
qc_def = TaskQCDefinition(task_id=task.id, name="چک‌لیست بتن‌ریزی")
async_db.add(qc_def)
await async_db.flush()
q_pre = TaskQCQuestion(
definition_id=qc_def.id,
phase=QCQuestionPhase.pre,
text="آیا آرماتوربندی و قالب‌بندی بازرسی و تأیید شد؟",
required=True,
sort_order=1,
)
q_exec = TaskQCQuestion(
definition_id=qc_def.id,
phase=QCQuestionPhase.during,
text="آیا اسلامپ بتن در محدوده استاندارد است؟",
required=True,
sort_order=2,
)
async_db.add_all([q_pre, q_exec])
await async_db.commit()
task_id = task.id
# Invariant Rule 1 check: Before answering pre-QC, can_start must be FALSE
defs = await load_definitions(async_db, task_id)
state = build_state(defs)
assert state.can_start is False
assert state.can_complete is False
# Now pass the pre-QC question
res_pre = TaskQCResult(question_id=q_pre.id, question=q_pre, answer=QCAnswer.pass_)
async_db.add(res_pre)
await async_db.commit()
# Load fresh definitions in new query
defs = await load_definitions(async_db, task_id)
state = build_state(defs)
# can_start is now TRUE! Progress can become > 0
assert state.can_start is True
# But can_complete is still FALSE because execution question is not yet answered!
assert state.can_complete is False
# Now pass the execution question as well
res_exec = TaskQCResult(question_id=q_exec.id, question=q_exec, answer=QCAnswer.pass_)
async_db.add(res_exec)
await async_db.commit()
defs = await load_definitions(async_db, task_id)
state = build_state(defs)
# Both can_start and can_complete are now TRUE!
assert state.can_start is True
assert state.can_complete is True