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:
@@ -0,0 +1 @@
|
||||
"""PM backend tools (seed etc.)."""
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user