update dashboard

This commit is contained in:
fatemeh sane
2026-10-21 16:04:07 +03:30
parent 41f93acec5
commit 8b2970cdb5
12 changed files with 1621 additions and 450 deletions
+121
View File
@@ -0,0 +1,121 @@
"""نام‌گذاری متریال — تفکیک «خانواده» از «مشخصه/واریانت» به‌صورت عمومی.
برخی ردیف‌های کاتالوگ، سایز/گرید را داخل نام ماده دارند
(«بلوک سیمانی ۲۰×۲۰×۴۰»، «لوله پلی‌اتیلن PE100 سایز ۲۵»). این ماژول با قواعد
الگویی (ابعاد NxNxN، کلیدواژه‌های مشخصه، کدهای گرید لاتین) نام را به
«خانواده» و «واریانت» می‌شکند — بدون هیچ هاردکدِ نام محصول مشخصی.
"""
import re
_DIGIT_MAP = str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩", "01234567890123456789")
# واژه‌هایی که شروع بخش مشخصه را نشان می‌دهند (ابعاد/درجه/تیپ/مدل/…)
_ATTRIBUTE_WORDS = {
"سایز",
"تیپ",
"مدل",
"درجه",
"ضخامت",
"قطر",
"ابعاد",
"طول",
"عرض",
"ارتفاع",
"رنگ",
"وزن",
"ظرفیت",
"شماره",
"کلاس",
}
# ابعاد ترکیبی: ۲۰×۲۰×۴۰ / ۲×۲٫۵ / 60x60 / 2*2.5
_DIMENSION_RE = re.compile(r"^\d+(?:[×xX*]\d+(?:[.,٫/]\d+)?)+$")
# کد گرید: PE100 / PN10 / DN50 / A3 / SN4 / ST37
_GRADE_RE = re.compile(r"^(?:PN|PE|DN|SN|SCH|ST|A|B|C)[-]?\d+$")
_NUMBER_RE = re.compile(r"^\d+(?:[.,٫/]\d+)?$")
_LATIN_WORD_RE = re.compile(r"^[A-Za-z]{2,6}$")
STANDARD_VARIANT_LABEL = "استاندارد"
def normalize_digits(text: str) -> str:
return (text or "").translate(_DIGIT_MAP)
def _is_spec_start(tokens: list[str], index: int) -> bool:
token = tokens[index]
normalized = normalize_digits(token)
if normalized in _ATTRIBUTE_WORDS: # «سایز»، «تیپ»، «مدل»، …
return True
if _DIMENSION_RE.match(normalized): # ۲۰×۲۰×۴۰
return True
if _GRADE_RE.match(token): # PE100 / PN10 / A3
return True
# کد لاتین کوتاه که پشتش عدد می‌آید: «IPE ۱۴»
if (
_LATIN_WORD_RE.match(token)
and index + 1 < len(tokens)
and _NUMBER_RE.match(normalize_digits(tokens[index + 1]))
):
return True
# عددِ تنها در انتهای نام (بدون کلیدواژه): «لوله مسی ۱۵»
if _NUMBER_RE.match(normalized) and index == len(tokens) - 1 and index >= 1:
return True
return False
def split_material_name(name: str) -> tuple[str, str | None]:
"""«لوله پلی‌اتیلن PE100 سایز ۲۵» → («لوله پلی‌اتیلن», «PE100 سایز ۲۵»)."""
tokens = [token for token in (name or "").split() if token]
for index in range(1, len(tokens)):
if _is_spec_start(tokens, index):
family = " ".join(tokens[:index]).strip()
variant = " ".join(tokens[index:]).strip()
if family and variant:
return family, variant
return " ".join(tokens).strip(), None
def material_family(name: str) -> str:
"""نام خانواده (بدون واریانت)."""
family, _ = split_material_name(name)
return " ".join(family.split())
def material_family_key(name: str) -> str:
"""شناسه‌ی پایدار خانواده — همان نام نرمال‌شده (بدون UUID اختصاصی)."""
return material_family(name)
def variant_label(name: str) -> str:
"""برچسب واریانت؛ در نبود واریانت، «استاندارد»."""
_, variant = split_material_name(name)
return variant or STANDARD_VARIANT_LABEL
def spec_label_from_specs(specs: dict | None) -> str | None:
"""برچسب کوتاه مشخصات از JSON موجودی (فقط مقدارها، نه «کلید: مقدار»)."""
if not specs:
return None
values = [str(value).strip() for value in specs.values() if str(value).strip()]
return "، ".join(values) if values else None
def material_entry_labels(material_name: str, specs: dict | None) -> dict:
"""برچسب‌های نمایشی یک ردیف کاتالوگ: خانواده، واریانت، مشخصه.
`spec_label` (برچسب محور X) = مقدار مشخصات JSON اگر موجود باشد، وگرنه
واریانتِ استخراج‌شده از نام، وگرنه «استاندارد».
"""
family = material_family(material_name)
variant = variant_label(material_name)
spec_from_json = spec_label_from_specs(specs)
# برچسب محور X: واریانت (سایز/گرید) ترجیح دارد؛ در نبود آن مقدار مشخصات
axis_label = variant if variant != STANDARD_VARIANT_LABEL else (spec_from_json or STANDARD_VARIANT_LABEL)
return {
"family_id": material_family_key(material_name),
"family_name": family,
"variant_label": variant,
"spec_label": axis_label,
"spec_detail": spec_from_json,
}