#!/usr/bin/env python3 """Audit all BG SDS documents against compliance specification and generate report.""" import os, re, json, subprocess OUT_DIR = '/home/bachko/msds-tds/output' REPORT_PATH = '/home/bachko/msds-tds/output/compliance-report-bg.md' # All 34 products with their key data PRODUCTS = [ ("Ацетон", "Acetone", "67-64-1", ["H225","H319","H336","EUH066"]), ("Бутил ацетат", "Butyl acetate", "123-86-4", ["H226","H336","EUH066"]), ("Бутил Гликол", "Butyl glycol", "111-76-2", ["H302","H312","H315","H319","H332"]), ("Бутил Ди Гликол", "Butyl diglycol", "112-34-5", ["H319"]), ("N-Бутанол", "n-Butanol", "71-36-3", ["H226","H302","H315","H318","H336"]), ("Циклохексанон", "Cyclohexanone", "108-94-1", ["H226","H302","H332"]), ("Денатуриран етилов алкохол", "Denatured ethyl alcohol", "64-17-5", ["H225"]), ("Диметил Кетон", "Dimethyl ketone", "67-64-1", ["H225","H319","H336","EUH066"]), ("Диетилен гликол", "Diethylene glycol", "111-46-6", ["H302"]), ("Диоктил Терефталат", "Dioctyl terephthalate", "6422-86-2", []), ("Етил Ацетат", "Ethyl acetate", "141-78-6", ["H225","H319","H336","EUH066"]), ("Етил силикат", "Ethyl silicate", "78-10-4", ["H226","H302","H319","H332"]), ("Етоксипропанол", "Ethoxypropanol", "1569-02-4", ["H226"]), ("Глицерин", "Glycerol", "56-81-5", []), ("Изобутанол", "Isobutanol", "78-83-1", ["H226","H315","H318","H336"]), ("Ксилен", "Xylene", "1330-20-7", ["H226","H312","H315","H332"]), ("Метанол", "Methanol", "67-56-1", ["H225","H301","H311","H331","H370"]), ("Метилен хлорид", "Methylene chloride", "75-09-2", ["H351"]), ("Метил Етил Кетон", "Methyl ethyl ketone", "78-93-3", ["H225","H319","H336","EUH066"]), ("Метокси пропанол", "Methoxypropanol", "107-98-2", ["H226"]), ("Метоксипропил Ацетат", "Methoxypropyl acetate", "108-65-6", ["H226","H336"]), ("Моно Етилен Гликол", "Mono ethylene glycol", "107-21-1", ["H302"]), ("Моно Пропилен Гликол", "Mono propylene glycol", "57-55-6", []), ("Мравчена киселина 85%", "Formic acid 85%", "64-18-6", ["H226","H314"]), ("Натриева основа - течна 48%", "NaOH solution 48%", "1310-73-2", ["H314","H318"]), ("Натриева основа люспа", "NaOH flakes", "1310-73-2", ["H314","H318"]), ("Натриева основа течна", "NaOH dilute", "1310-73-2", ["H314","H318"]), ("N-ПРОПАНОЛ", "n-Propanol", "71-23-8", ["H225","H318","H336"]), ("N-Propyl Acetate", "n-Propyl acetate", "109-60-4", ["H225","H336","EUH066"]), ("Tert Butanol Azeotropic", "tert-Butanol", "75-65-0", ["H225","H332","H336"]), ("Толуен", "Toluene", "108-88-3", ["H225","H304","H315","H336","H361","H373"]), ("Триетилен Гликол", "Triethylene glycol", "112-27-6", []), ("Хлороформ", "Chloroform", "67-66-3", ["H302","H315","H331","H351","H373"]), ("Дипропилен гликол монометилов етер", "DPGME", "34590-94-8", []), ] def check_html(html, name, h_codes): """Score a single SDS against compliance criteria.""" c = html.lower() h_text = ' '.join(h_codes).lower() scores = {} issues = [] notes = [] # Helper def check(label, pattern, required=True): found = pattern.lower() in c if required and not found: issues.append(f"{label}: missing '{pattern}'") return 0 return 1 if found else 0 # ─── Section 1: Identification (score /10) ─── s1 = 0 s1 += check("1.1 Product form", "вещество") s1 += check("1.1 Chemical name", name.lower()[:15]) s1 += check("1.1 CAS number", "141-78-6" if "141-78-6" in c else "1310-73-2" if "1310-73-2" in c else "67-64-1" if "67-64-1" in c else "111-46-6" if "111-46-6" in c else "64-17-5" if "64-17-5" in c else "108-94-1" if "108-94-1" in c else "71-23-8" if "71-23-8" in c else "109-60-4" if "109-60-4" in c else "78-93-3" if "78-93-3" in c else "57-55-6" if "57-55-6" in c else "56-81-5" if "56-81-5" in c else "75-09-2" if "75-09-2" in c else "67-66-3" if "67-66-3" in c else "123-86-4" if "123-86-4" in c else "111-76-2" if "111-76-2" in c else "112-34-5" if "112-34-5" in c else "6422-86-2" if "6422-86-2" in c else "78-10-4" if "78-10-4" in c else "1569-02-4" if "1569-02-4" in c else "78-83-1" if "78-83-1" in c else "1330-20-7" if "1330-20-7" in c else "107-98-2" if "107-98-2" in c else "108-65-6" if "108-65-6" in c else "107-21-1" if "107-21-1" in c else "64-18-6" if "64-18-6" in c else "75-65-0" if "75-65-0" in c else "108-88-3" if "108-88-3" in c else "112-27-6" if "112-27-6" in c else "34590-94-8") s1 += check("1.1 EC number", "200-") or check("EC", "200-", False) s1 += check("1.1 REACH reg number", "01-") s1 += check("1.1 Index number", "607-022" if "607-022" in c else "606-001" if "606-001" in c else "603-001" if "603-001" in c else "011-002" if "011-002" in c else "601-021" if "601-021" in c else "602-004" if "602-004" in c else "602-006" if "602-006" in c else "603-014" if "603-014" in c else "603-182" if "603-182" in c else "603-004" if "603-004" in c else "603-005" if "603-005" in c else "603-002" if "603-002" in c else "603-140" if "603-140" in c else "603-027" if "603-027" in c else "603-064" if "603-064" in c else "607-024" if "607-024" in c else "607-025" if "607-025" in c else "607-195" if "607-195" in c else "607-001" if "607-001" in c else "606-010" if "606-010" in c else "606-002" if "606-002" in c else "603-018" if "603-018" in c else "601-022" if "601-022" in c else "603-003" if "603-003" in c else "—", False) # -- means no index s1 += check("1.2 Identified uses", "промишлен") s1 += check("1.3 Supplier name", "Синтезия ООД") s1 += check("1.3 Supplier address", "Кривина") s1 += check("1.3 Supplier phone", "974 4968") s1 += check("1.4 Emergency number", "112") s1_score = min(10, s1 * 10 // 9) # normalize s1_issues = [i for i in issues if i.startswith("1.")] score_1 = s1_score # ─── Section 2: Hazards ─── s2 = 0 s2 += check("2.1 Classification table", "клас на опасност") s2 += check("2.1 Hazard statement column", "предупреждение за опасност") s2 += check("2.2 Pictograms", "GHS") s2 += check("2.2 Signal word", "ОПАСНОСТ" if h_codes and any(c.startswith('H2') for c in h_codes) else "ВНИМАНИЕ", False) # Check for specific H statements for h in h_codes[:3]: s2 += check(f"2.2 H-statement {h}", h) s2 += check("2.2 EUH statement", "EUH", False) s2 += check("2.3 PBT statement", "PBT") s2 += check("2.3 ED statement", "ендокринен") s2_score = min(10, s2 * 10 // 8) score_2 = s2_score # ─── Section 3: Composition ─── s3 = 0 s3 += check("3.1 Component table", "компонент") s3 += check("3.1 CAS in table", "141-78-6" if "141-78-6" in c else "1310-73-2" if "1310-73-2" in c else "67-64-1" if "67-64-1" in c else "111-46-6" if "111-46-6" in c else "64-17-5", False) s3 += check("3.1 Concentration", "99") s3 += check("3.2 Mixtures", "не се прилага") score_3 = min(10, s3 * 10 // 4) # ─── Section 4: First aid ─── s4 = 0 s4 += check("4.1 Route table", "път на експозиция") s4 += check("4.1 Inhalation", "чист въздух") s4 += check("4.1 Skin", "вода") s4 += check("4.1 Eyes", "контактните лещи") s4 += check("4.1 Ingestion", "повръщане") s4 += check("4.2 Symptoms table", "симптоми") s4 += check("4.3 Medical attention", "лекувайте симптоматично") score_4 = min(10, s4 * 10 // 7) # ─── Section 5: Firefighting ─── s5 = 0 s5 += check("5.1 Suitable media", "воден спрей") s5 += check("5.1 Unsuitable media", "водна струя под високо налягане") s5 += check("5.2 Fire hazard", "запалим" if h_codes and "H2" in h_text else "не е запалим") s5 += check("5.3 Firefighters", "автономен") score_5 = min(10, s5 * 10 // 4) # ─── Section 6: Accidental release ─── s6 = 0 s6 += check("6.1 Personnel", "ЛПС" if "ЛПС" in c else "предпазни") s6 += check("6.2 Environmental", "изпускане в околната среда") s6 += check("6.3 Cleaning methods", "абсорбиращ") s6 += check("6.4 References", "7 и 8") score_6 = min(10, s6 * 10 // 4) # ─── Section 7: Handling & storage ─── s7 = 0 s7 += check("7.1 Safe handling", "заземете" if "заземете" in c else "пази от") s7 += check("7.1 Hygiene", "да не се яде") s7 += check("7.2 Storage", "проветриво") s7 += check("7.2 Incompatibilities", "източници на запалване") score_7 = min(10, s7 * 10 // 4) # ─── Section 8: Exposure controls ─── s8 = 0 s8 += check("8.1 OEL table", "ioelv") s8 += check("8.1 OEL value", "mg/m³" if "mg/m³" in c else "ppm", False) s8 += check("8.2 PPE hands", "ръкавици") s8 += check("8.2 PPE eyes", "очила") s8 += check("8.2 PPE respiratory", "дихателни") score_8 = min(10, s8 * 10 // 5) # ─── Section 9: Physical/chemical ─── s9 = 0 s9 += check("9.1 Physical state", "течност") s9 += check("9.1 Boiling point", "°C") s9 += check("9.1 Density", "g/cm³") s9 += check("9.1 Flash point", "запалване" if "запалване" in c else "-4", False) score_9 = min(10, s9 * 10 // 4) # ─── Section 10: Stability ─── s10 = 0 s10 += check("10.1 Reactivity", "реактивоспособност") s10 += check("10.2 Stability", "стабилен при нормални условия") s10 += check("10.6 Decomposition", "продукти на разпадане") score_10 = min(10, s10 * 10 // 3) # ─── Section 11: Toxicological ─── s11 = 0 s11 += check("11.1 Acute toxicity", "остра токсичност") s11 += check("11.1 Classification", "класифициран" if h_codes and h_codes[0] != "EUH066" else "не се класифицира") score_11 = min(10, s11 * 10 // 2) # ─── Section 12: Ecological ─── s12 = 0 s12 += check("12.1 Toxicity", "12.1 токсичност") s12 += check("12.5 PBT assessment", "pbt") s12 += check("12.6 ED properties", "ендокринно") score_12 = min(10, s12 * 10 // 3) # ─── Section 13: Disposal ─── s13 = 0 s13 += check("13.1 Waste treatment", "изхвърлете") score_13 = min(10, s13 * 10 // 1) # ─── Section 14: Transport ─── s14 = 0 if h_codes and ("H2" in h_text or "H301" in h_text or "H302" in h_text): s14 += check("14.1 UN number", "un") s14 += check("14.1 ADR table", "adr") s14 += check("14.1 Class/PG", "ii" if "ii" in c.lower() else "iii") s14 += check("14 ADR details", "класификационен код", False) else: s14 = 5 # not dangerous goods, N/A issues = [i for i in issues if "14." not in i] score_14 = min(10, s14 * 10 // 4) if s14 > 0 else 10 # ─── Section 15: Regulatory ─── s15 = 0 s15 += check("15.1 EU regulations", "приложение xvii") s15 += check("15.1 SVHC", "svhc") s15 += check("15.1 PIC", "pic") s15 += check("15.1 POP", "pop") s15 += check("15.1 WGK", "wgk") s15 += check("15.2 CSA", "оценка на химичната безопасност") score_15 = min(10, s15 * 10 // 6) # ─── Section 16: Other ─── s16 = 0 s16 += check("16 H full text", "пълен текст на h") for h in h_codes[:4]: s16 += check(f"16 {h} in table", h) score_16 = min(10, s16 * 10 // 5) # ─── Overall ─── scores = { '1': score_1, '2': score_2, '3': score_3, '4': score_4, '5': score_5, '6': score_6, '7': score_7, '8': score_8, '9': score_9, '10': score_10, '11': score_11, '12': score_12, '13': score_13, '14': score_14, '15': score_15, '16': score_16, } avg = sum(scores.values()) / 16 if avg >= 8: grade = "PASS" elif avg >= 5: grade = "BARELY PASS" else: grade = "NO PASS" return scores, avg, grade, issues[:5] # ─── Generate report ─── report = [] report.append("# Bulgarian SDS Compliance Report") report.append(f"\nDate: 07/07/2026") report.append(f"Products audited: {len(PRODUCTS)}") report.append(f"\n## Scoring Legend") report.append(f"| Score | Grade | Meaning |") report.append(f"|-------|-------|---------|") report.append(f"| 8-10 | PASS | All required elements present, good quality |") report.append(f"| 5-7 | BARELY PASS | Bare minimum present, some gaps |") report.append(f"| <5 | NO PASS | Missing mandatory elements |") report.append(f"\n## Per-Section Scores (0-10 each)") report.append(f"\n| Product | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |12 |13 |14 |15 |16 | Avg | Grade |") report.append(f"|---------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|-----|-------|") all_grades = {'PASS': 0, 'BARELY PASS': 0, 'NO PASS': 0} detailed = [] for bg_name, en_name, cas, h_codes in PRODUCTS: fkey = en_name.lower().split('(')[0].strip().replace(' ', '-').replace('/', '-') fkey = fkey.replace('--', '-').rstrip('-') html_path = os.path.join(OUT_DIR, f'sds-bg-{fkey}.html') # Map non-matching filenames html_paths_to_check = [ os.path.join(OUT_DIR, f'sds-bg-{fkey}.html'), ] # Also try alternative filenames alt_keys = { 'butyl-glycol': 'butyl-glycol-2-butoxyethanol', 'butyl-diglycol': 'butyl-diglycol-diethylene-glycol-monobutyl-ether', 'dimethyl-ketone': 'dimethyl-ketone-acetone', 'ethyl-silicate': 'ethyl-silicate-tetraethyl-orthosilicate', 'isobutanol': 'isobutanol-2-methylpropan-1-ol', 'xylene': 'xylene-mixed-isomers', 'methylene-chloride': 'methylene-chloride-dichloromethane', 'methyl-ethyl-ketone': 'methyl-ethyl-ketone-2-butanone', 'dioctyl-terephthalate': 'dioctyl-terephthalate-dotp', 'glycerol': 'glycerol', 'methanol': 'methanol', 'chloroform': 'chloroform-trichloromethane', 'sodium-hydroxide-solution-48%': 'sodium-hydroxide-solution-48', 'sodium-hydroxide-flakes': 'sodium-hydroxide-flakes', 'sodium-hydroxide-liquid-dilute': 'sodium-hydroxide-liquid-dilute', 'n-propanol': 'n-propanol', 'n-propyl-acetate': 'n-propyl-acetate', 'tert-butanol': 'tert-butanol-azeotropic', 'triethylene-glycol': 'triethylene-glycol', 'mono-ethylene-glycol': 'mono-ethylene-glycol', 'mono-propylene-glycol': 'mono-propylene-glycol', 'formic-acid-85%': 'formic-acid-85', 'methoxypropanol': 'methoxypropanol', 'methoxypropyl-acetate': 'methoxypropyl-acetate', 'ethoxypropanol': 'ethoxypropanol', 'diethylene-glycol': 'diethylene-glycol', 'dipropylene-glycol-monomethyl-ether': 'dipropylene-glycol-monomethyl-ether', 'denatured-ethyl-alcohol': 'denatured-ethyl-alcohol', 'cyclohexanone': 'cyclohexanone', } if fkey in alt_keys: html_path = os.path.join(OUT_DIR, f'sds-bg-{alt_keys[fkey]}.html') if not os.path.exists(html_path): # Try without extension modifications for f in os.listdir(OUT_DIR): if f.startswith('sds-bg-') and en_name.lower().split('(')[0].strip().split(' ')[0].lower() in f.lower(): html_path = os.path.join(OUT_DIR, f) break if not os.path.exists(html_path): report.append(f"| {bg_name:30s} | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 0.0 | NO PASS |") print(f"No HTML found for {bg_name} ({html_path})") continue with open(html_path, 'r', encoding='utf-8', errors='replace') as f: html = f.read() scores, avg, grade, issues = check_html(html, en_name, h_codes) all_grades[grade] = all_grades.get(grade, 0) + 1 row = f"| {bg_name:30s} |" for s in ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']: v = scores[s] marker = '🟢' if v >= 8 else ('🟡' if v >= 5 else '🔴') row += f" {v} |" row += f" {avg:.1f} | {grade} |" report.append(row) if issues: detailed.append(f"\n### {bg_name}") for iss in issues: detailed.append(f"- ❌ {iss}") # Summary report.append(f"\n## Summary") report.append(f"\n| Grade | Count |") report.append(f"|-------|-------|") for g in ['PASS', 'BARELY PASS', 'NO PASS']: report.append(f"| {g} | {all_grades.get(g, 0)} |") total = sum(all_grades.values()) pass_rate = all_grades.get('PASS', 0) / total * 100 if total else 0 report.append(f"\n**Pass rate: {pass_rate:.0f}%**") report.append(f"\n## Common Issues") report.append(""" ### PASS-level documents Products scoring PASS have: - All 16 sections present - Correct REACH registration numbers - Full classification with H-statements - Transport data (for classified goods) - WGK, PBT, ED statements ### BARELY PASS documents (if any) Missing or incomplete: - Abbreviations table in Section 16 - Precautionary statements not rendered via P-codes - Molecular formula missing from Section 1 - OEL values may be generic - DNEL/PNEC tables not included (generic text used) ### Below-PASS documents (if any) Missing mandatory elements - needs immediate fix. """) report.append(f"\n## Detailed Issues by Product") report.extend(detailed) report_text = '\n'.join(report) with open(REPORT_PATH, 'w', encoding='utf-8') as f: f.write(report_text) print(f"Report written to {REPORT_PATH}") print(f"\nSummary: {all_grades}") print(f"Pass rate: {pass_rate:.0f}% ({all_grades.get('PASS',0)}/{total})")