Squashed 'pm/' content from commit 91f3271

git-subtree-dir: pm
git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
This commit is contained in:
MMD
2026-10-07 15:14:20 +03:30
commit 934b74ebbd
87 changed files with 23922 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""PM backend tools (seed etc.)."""
+304
View File
@@ -0,0 +1,304 @@
"""Seed PM with the 8 standard construction QC templates.
Each template now carries three stages:
* pre — آمادگی شروع کار (readiness; a task cannot start until these pass)
* during — کنترل حین اجرا
* after — کنترل نهایی
Idempotent — run any time: python -m tools.seed
"""
import asyncio
import logging
from sqlalchemy import select
from app.db import SessionFactory, engine
from app.models import Base, QCQuestion, QCQuestionPhase, QCTemplate
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("pm.seed")
# (activity_key, name, pre checklist, during checklist, after checklist)
TEMPLATES: list[tuple[str, str, list[str], list[str], list[str]]] = [
(
"concrete",
"بتن‌ریزی",
[
"آرماتوربندی و قالب‌بندی تأیید شده است؟",
"سطح قالب تمیز و روغن‌کاری شده است؟",
"بتن سفارش‌شده و پمپ/ویبره هماهنگ شده است؟",
"دسترسی و مسیر تخلیه بتن آماده است؟",
],
[
"مشخصات بتن (عیار/روانی) مطابق طراحی است؟",
"ویبره بدون جداشدگی انجام شد؟",
"ضخامت و تراز حین اجرا کنترل شد؟",
],
[
"سطح یکنواخت و بدون جداشدگی است؟",
"ترک مویی یا ریزش لبه وجود ندارد؟",
"عمل‌آوری (کیورینگ) طبق برنامه انجام شد؟",
],
),
(
"plaster",
"گچ‌کاری",
[
"سطح آماده‌سازی (شستشو/چکش‌کاری) انجام شده؟",
"داربست/چهارپایه سالم آماده است؟",
"مصالح (گچ/پودر) و آب در دسترس است؟",
],
[
"ضخامت گچ طبق مشخصات است؟",
"شاقول و تراز رعایت شده؟",
"درزها و اتصالات کنترل شد؟",
],
[
"سطح یکنواخت و بدون موج است؟",
"ترک ندارد؟",
"گوشه‌ها و لبه‌ها سالم هستند؟",
],
),
(
"rebar",
"آرماتوربندی",
[
"نقشه‌های اجرایی و لیست برش آماده است؟",
"میلگرد مورد نیاز در کارگاه موجود است؟",
"سطح کارگاه و مسیر دسترسی آماده است؟",
],
[
"قطر و تعداد میلگرد طبق نقشه است؟",
"فاصله خاموت‌ها رعایت شده؟",
"پوشش بتن (کاور) مناسب است؟",
],
[
"اتصالات (بست/جوش/درزگیری) مطابق آیین‌نامه است؟",
"آرماتور اضافی و براده‌ها جمع شده؟",
"آماده بتن‌ریزی است؟",
],
),
(
"formwork",
"قالب‌بندی",
[
"نقشه قالب‌بندی و ابعاد مشخص است؟",
"قالب و شمع‌بندی سالم و کافی موجود است؟",
"روغن قالب آماده است؟",
],
[
"ابعاد و تراز طبق نقشه است؟",
"شمع‌بندی و مهاربندی کافی است؟",
"درزهای قالب آب‌بند است؟",
],
["قالب بدون تغییر شکل است؟", "سطح تماس مناسب بتن‌ریزی است؟"],
),
(
"blockwork",
"تیغه‌چینی",
[
"آجر/بلوک و ملات در پای کار موجود است؟",
"خط‌اندازی و نقشه چیدمان انجام شده؟",
"سطح زیرکار آماده و تمیز است؟",
],
[
"خط و تراز دیوار تنظیم شده؟",
"مقاومت ملات مناسب است؟",
"بندکشی و فاصله درزها صحیح است؟",
],
[
"سطح دیوار صاف و شاقول است؟",
"درزها پر و یکنواخت است؟",
"نازک‌کاری بعدی بدون اصلاح ممکن است؟",
],
),
(
"isogam",
"ایزوگام",
[
"سطح بام خشک و تمیز است؟",
"پرایمر و مشعل آماده است؟",
"هواشناسی برای اجرا مناسب است؟",
],
[
"پرایمر اجرا شده؟",
"همپوشانی نوارها حداقل ۱۰ سانتی‌متر است؟",
"حرارت شعله مناسب و بدون سوختگی است؟",
],
["چکه و تورفتگی ندارد؟", "لبه‌ها و اتصالات کاملاً چسبیده؟", "آزمون آب (باران مصنوعی) پاس شد؟"],
),
(
"tiling",
"کاشی‌کاری",
[
"زیرکار تراز، خشک و آماده است؟",
"کاشی/دوغاب/چسب در پای کار موجود است؟",
"نقشه چیدمان و بندبندی مشخص شده است؟",
],
[
"دوغاب و ملات مناسب است؟",
"بندبندی یکنواخت است؟",
"کاشی‌های شکسته کنار گذاشته شده؟",
],
["صدای توخالی ندارد؟", "تراز و پلمپ رعایت شده؟", "بندها تمیز و هم‌رنگ است؟"],
),
(
"mep",
"تأسیسات",
[
"نقشه‌های مکانیکال/الکتریکال آماده است؟",
"مقطع و اتصالات پیش‌بینی‌شده موجود است؟",
"مسیر و برش‌های مورد نیاز هماهنگ شده؟",
],
[
"مسیر لوله‌کشی طبق نقشه است؟",
"آزمون فشار حین اجرا انجام شد؟",
"ایزولاسیون لوله‌ها اجرا شده؟",
],
["آزمون نهایی سیستم انجام شد؟", "برچسب‌گذاری و شناسایی کامل است؟", "تحویل به ناظر ثبت شد؟"],
),
(
"excavation",
"گودبرداری و سازه نگهبان",
[
"نقشه‌های مصوب سازه نگهبان و مجوزهای گودبرداری اخذ شده است؟",
"وضعیت ابنیه و تأسیسات همجوار و چاه‌های مجاور بررسی و صورتجلسه شده است؟",
"ایستگاه‌های پایش و مانیتورینگ تغییرشکل دیواره‌ها نصب شده است؟",
"ماشین‌آلات و پمپ‌های زهکشی و هدایت آب سطحی مستقر است؟",
],
[
"خاکبرداری مرحله‌ای طبق ترازهای مصوب طرح سازه نگهبان انجام می‌شود؟",
"المان‌های سازه نگهبان (سولجر، نیلینگ، استرات و مهارها) به‌موقع اجرا شدند؟",
"رفتارنگاری روزانه دیواره‌ها و ساختمان‌های مجاور ثبت و کنترل می‌شود؟",
],
[
"تراز نهایی کف گود و رقوم ارتفاعی فونداسیون کنترل و تأیید شد؟",
"دیواره‌ها و ترانشه‌های گود کاملاً پایدار تحویل شدند؟",
"صورتجلسه تحویل بستر پی به آزمایشگاه مکانیک خاک و دستگاه نظارت تنظیم شد؟",
],
),
(
"steel_structure",
"اسکلت فولادی و اتصالات جوشی/پیچی",
[
"نقشه‌های شاپ‌دراویینگ (Shop Drawings) و دستورکار اتصالات ابلاغ شده است؟",
"گواهی‌های فنی متریال فولادی، الکترودها و پیچ‌های پرمقاومت بررسی و تأیید شد؟",
"صلاحیت جوشکاران (کارت کد) و کالیبراسیون تجهیزات جوش کنترل گردید؟",
"آکس‌بندی، تراز بیس‌پلیت‌ها و استحکام انکربولت‌ها بررسی شد؟",
],
[
"رواداری شاقولی ستون‌ها و تراز پل‌ها و تیرها در حین برپایی کنترل شد؟",
"سایز، بعد، طول و توالی پاس‌های جوشکاری طبق WPS اجرا گردید؟",
"گشتاور پیچ‌های پرمقاومت اصطکاکی با ترکمتر کالیبره کنترل شد؟",
],
[
"نتایج آزمون‌های غیرمخرب جوش (VT/PT/UT/MT) مطابق ضوابط اخذ و تأیید شد؟",
"تمیزکاری سرباره جوش، پاشش‌ها و سندبلاست موضعی انجام گرفت؟",
"پوشش ضدزنگ و رنگ‌آمیزی حفاظتی طبق ضخامت مشخصات فنی اجرا شد؟",
],
),
(
"joist_slab",
"اجرای سقف تیرچه بلوک و دال بتنی",
[
"شمع‌بندی، پایه‌های اطمینان و مهاربندی زیر سقف با تراکم استاندارد اجرا شد؟",
"تیرچه‌ها سالم، بدون ترک‌خوردگی و با خیز منفی مناسب نصب شده‌اند؟",
"مسیرهای عبور لوله‌های تأسیسات برقی و مکانیکی جاگذاری و تثبیت شدند؟",
],
[
"فاصله تیرچه‌ها، جانمایی کلاف میانی (تای‌بیم) و میلگردهای ممان منفی کنترل شد؟",
"آرماتورهای حرارتی با پوشش کاور و همپوشانی مجاز نصب گردیدند؟",
"بلوک‌ها/یونولیت‌های سقفی سالم و کاملاً جذب بدون درز چیده شدند؟",
],
[
"ضخامت بتن دال روی تیرچه‌ها در تمام نقاط کنترل و تأیید شد؟",
"خیز سقف پس از بتن‌ریزی و برداشتن شمع‌ها در حد مجاز آیین‌نامه است؟",
"کیورینگ و مرطوب نگه‌داشتن دال بتنی طبق برنامه زمان‌بندی انجام گرفت؟",
],
),
(
"stone_facade",
"سنگ‌کاری نما و کف",
[
"شاسی‌کشی فلزی و زیرسازی نما طبق جزئیات اجرایی تراز و شاقول است؟",
"سنگ‌ها سالم، هم‌رنگ و بدون رگه‌های سست و ترک انتخاب شده‌اند؟",
"کلیه سنگ‌های نما شیارزنی شده و دارای اسکوپ سیمی یا مکانیکی مطمئن هستند؟",
],
[
"ملات ماسه سیمان پشت سنگ پرعیار و به‌صورت یکنواخت بدون پوکی پر می‌شود؟",
"شاقول، تراز و ریسمان‌کشی بندهای افقی و قائم به طور مداوم کنترل می‌گردد؟",
"درزهای انبساط حرارتی نما در فواصل پیش‌بینی‌شده اجرا شد؟",
],
[
"سطح سنگ‌کاری شاقول، گونیا، تخت و بدون پله‌شدن لبه‌هاست؟",
"بندکشی توپر، صاف و با ملات ضدرطوبت همرنگ اجرا شد؟",
"تست چسبندگی سنگ‌ها و شستشوی نهایی سطح با موفقیت انجام گرفت؟",
],
),
(
"doors_windows",
"نصب درب و پنجره‌های دوجداره",
[
"ابعاد و شاقولی ساب‌فریم‌ها (فریم‌های انتظار) کنترل و رنگ ضدزنگ خورده است؟",
"پروفیل‌ها، یراق‌آلات و شیشه‌های دوجداره سالم و استاندارد تخلیه شده‌اند؟",
],
[
"شاقول و تراز بودن فریم‌ها در سه محور و فواصل پیچ‌های بولت کنترل شد؟",
"تزریق فوم پلی‌اورتان برای هوابندی و عایق صوتی دور فریم انجام گرفت؟",
"درزگیری محیط خارجی پنجره با چسب سیلیکون خنثی آنتی‌یووی (UV) اجرا شد؟",
],
[
"رگلاژ یراق‌آلات و روانی بازشوها کنترل و قفل‌ها به درستی عمل می‌کنند؟",
"تست عدم نفوذ آب باران و باد انجام و تأیید گردید؟",
"پوشش‌های محافظ و چسب‌های سطحی پاکسازی و فریم‌ها تمیز شدند؟",
],
),
(
"painting",
"نقاشی و رنگ‌آمیزی ساختمان",
[
"سطح گچ‌کاری کاملاً خشک، بدون رطوبت، پوکی یا شوره است؟",
"روغن زیرکار (پرایمر نفوذگر) متناسب با نوع رنگ زیرین اعمال گردیده است؟",
"کاورکشی و حفاظت از کف، پنجره‌ها و چهارچوب‌ها انجام شده است؟",
],
[
"بتونه‌کاری سراسری، لکه‌گیری و سنباده‌زنی یکدست با نور موضعی انجام شد؟",
"تعداد لایه‌های رنگ و فاصله زمانی کافی برای خشک‌شدن لایه‌ها رعایت گردید؟",
],
[
"سطح نهایی صاف، یکدست و بدون اثر قلم‌مو، شره و سایه‌روشن است؟",
"تطابق کد رنگ اجرا شده با شید رنگ مصوب کارفرما تأیید شد؟",
"تمیزکاری نهایی محیط از پاشش ذرات رنگ انجام گردید؟",
],
),
]
async def seed() -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with SessionFactory() as db:
for key, name, pre, during, after in TEMPLATES:
existing = await db.scalar(select(QCTemplate).where(QCTemplate.name == name))
if existing is not None:
continue
template = QCTemplate(name=name, activity_type=key, description=f"چک‌لیست کنترل کیفیت {name}")
db.add(template)
await db.flush()
for phase, items in (
(QCQuestionPhase.pre, pre),
(QCQuestionPhase.during, during),
(QCQuestionPhase.after, after),
):
for index, text in enumerate(items):
db.add(
QCQuestion(template_id=template.id, phase=phase, text=text, sort_order=index)
)
log.info("QC template seeded: %s (pre=%d during=%d after=%d)", name, len(pre), len(during), len(after))
await db.commit()
log.info("seed done")
if __name__ == "__main__":
asyncio.run(seed())
+115
View File
@@ -0,0 +1,115 @@
"""Bootstrap the PM→ERP service account (one-off, idempotent).
1. Creates Casdoor user `pm-service` (org `inventory`) via the Casdoor admin API.
2. Performs a password grant so ERP auto-provisions the users row.
3. Promotes that row to role `admin` (read-only across all projects —
the ERP inventory router lets admin read without a project scope).
Usage (from the pm-api container):
PM_ERP_SERVICE_PASSWORD=... python -m tools.seed_erp_service
"""
import asyncio
import base64
import json
import logging
import os
import urllib.request
from app.config import settings
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("pm.seed_erp")
ORG = settings.ERP_CASDOOR_ORGANIZATION
USERNAME = settings.ERP_SERVICE_USERNAME
PASSWORD = settings.ERP_SERVICE_PASSWORD or os.environ.get("PM_ERP_SERVICE_PASSWORD", "pm-service-2026")
EMAIL = f"{USERNAME}@sample.ir"
ERP_DSN = "postgresql://postgres:postgres@procurement-postgres:5432/procurement"
def _post(url: str, body: dict, headers: dict | None = None) -> dict:
req = urllib.request.Request(
url,
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", **(headers or {})},
method="POST",
)
resp = urllib.request.urlopen(req, timeout=15)
return json.loads(resp.read())
def casdoor_admin_cookie() -> str:
resp = urllib.request.urlopen(urllib.request.Request(
f"{settings.ERP_CASDOOR_ENDPOINT}/api/login",
data=json.dumps({
"type": "login", "organization": "built-in",
"username": "admin", "password": "123", "application": "app-built-in",
}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
), timeout=15)
data = json.loads(resp.read())
if data.get("status") != "ok":
raise SystemExit(f"casdoor admin login failed: {data}")
return (resp.headers.get("Set-Cookie") or "").split(";")[0]
async def main() -> None:
# 1) casdoor user (idempotent — casdoor rejects duplicates, that's fine)
cookie = casdoor_admin_cookie()
for path, body in [
("/api/add-user", {
"owner": ORG, "name": USERNAME, "displayName": "PM Service",
"firstName": "PM", "lastName": "Service", "email": EMAIL,
"password": PASSWORD, "passwordType": "plain", "type": "normal-user",
}),
(f"/api/update-user?id={ORG}/{USERNAME}&columns=password_type", {"passwordType": "plain"}),
]:
try:
result = _post(f"{settings.ERP_CASDOOR_ENDPOINT}{path}", body, {"Cookie": cookie})
log.info("%s → %s", path.split("?")[0], result.get("msg", result.get("status")))
except Exception as exc: # noqa: BLE001
log.warning("%s failed: %s", path.split("?")[0], exc)
# 2) password grant (PM never verifies this token — it just forwards it)
grant = _post(f"{settings.ERP_CASDOOR_ENDPOINT}/api/login/oauth/access_token", {
"grant_type": "password",
"client_id": settings.ERP_CASDOOR_CLIENT_ID,
"client_secret": settings.ERP_CASDOOR_CLIENT_SECRET,
"username": USERNAME,
"password": PASSWORD,
})
token = grant.get("access_token")
if not token:
raise SystemExit(f"password grant failed: {grant}")
claims = json.loads(base64.urlsafe_b64decode(token.split(".")[1] + "=" * (-len(token.split(".")[1]) % 4)))
casdoor_id = str(claims.get("sub") or claims.get("id") or "")
log.info("grant ok, casdoor_id=%s", casdoor_id[:8])
# trigger ERP auto-provisioning
me_req = urllib.request.Request(
f"{settings.ERP_BASE_URL}/api/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
)
try:
urllib.request.urlopen(me_req, timeout=15).read()
log.info("ERP provisioning call ok")
except Exception as exc: # noqa: BLE001
log.warning("provisioning call failed (row may already exist): %s", exc)
# 3) promote in ERP DB (direct SQL — this service account is PM infrastructure)
import asyncpg
conn = await asyncpg.connect(ERP_DSN)
try:
updated = await conn.execute(
"UPDATE users SET role='admin' WHERE casdoor_id=$1", casdoor_id
)
log.info("ERP role promotion: %s", updated)
finally:
await conn.close()
log.info("service account ready: %s / ***", USERNAME)
if __name__ == "__main__":
asyncio.run(main())