# -*- coding: utf-8 -*- """Template & parser Excel nilai per mahasiswa per Sub-CPMK.""" import io import openpyxl from openpyxl.styles import Alignment, Font, PatternFill from openpyxl.utils import get_column_letter BIRU = "1F4E79" BIRU_MUDA = "D6E4F0" def template_mahasiswa(daftar_mk: list[dict], tahun: str) -> bytes: """Template impor daftar mahasiswa: NIM | Nama | Kode MK | Kelas.""" wb = openpyxl.Workbook() ws = wb.active ws.title = "Mahasiswa" ws.append([f"DAFTAR MAHASISWA MENGONTRAK MK — {tahun}"]) ws.append(["Petunjuk: satu baris per kontrak (mahasiswa yang mengambil 5 MK " "berarti 5 baris). Kode MK harus sesuai sheet 'Kode MK'."]) ws.append([]) ws.append(["NIM", "Nama Mahasiswa", "Kode MK", "Kelas"]) for ci, lebar in enumerate((16, 30, 14, 10), 1): c = ws.cell(row=4, column=ci) c.font = Font(bold=True, color="FFFFFF") c.fill = PatternFill("solid", fgColor=BIRU) c.alignment = Alignment(horizontal="center") ws.column_dimensions[get_column_letter(ci)].width = lebar ws.freeze_panes = "A5" rujukan = wb.create_sheet("Kode MK") rujukan.append(["Kode MK", "Nama Mata Kuliah"]) for ci in (1, 2): c = rujukan.cell(row=1, column=ci) c.font = Font(bold=True, color="FFFFFF") c.fill = PatternFill("solid", fgColor=BIRU) for mk in daftar_mk: rujukan.append([mk["kode"], mk["nama"]]) rujukan.column_dimensions["A"].width = 14 rujukan.column_dimensions["B"].width = 46 buf = io.BytesIO() wb.save(buf) return buf.getvalue() def parse_mahasiswa(isi: bytes, kode_mk_valid: set[str]) -> tuple[list[dict], list[str]]: """Baca xlsx daftar mahasiswa → (baris kontrak, peringatan). Header dicari otomatis (baris yang memuat 'NIM'); kolom dikenali dari judulnya: NIM, Nama, Kode MK, Kelas. """ wb = openpyxl.load_workbook(io.BytesIO(isi), read_only=True, data_only=True) ws = wb["Mahasiswa"] if "Mahasiswa" in wb.sheetnames else wb.active rows = list(ws.iter_rows(values_only=True)) kol = {} header_i = None for i, row in enumerate(rows): vals = [str(v).strip().lower() if v is not None else "" for v in row] if "nim" in vals or "npm" in vals: header_i = i for ci, v in enumerate(vals): if v in ("nim", "npm"): # 'npm' — kompatibilitas file lama kol["nim"] = ci elif "nama" in v: kol.setdefault("nama", ci) elif "kode" in v or v == "mk": kol.setdefault("mk", ci) elif "kelas" in v: kol.setdefault("kelas", ci) break if header_i is None or "nim" not in kol or "mk" not in kol: return [], ["Header tidak dikenali: pastikan ada kolom NIM, Nama, " "Kode MK, dan Kelas (pakai template dari aplikasi)."] def ambil(vals, kunci): ci = kol.get(kunci) if ci is None or ci >= len(vals) or vals[ci] is None: return "" v = vals[ci] # NIM/kelas numerik dari Excel → buang '.0' if isinstance(v, float) and v.is_integer(): v = int(v) return str(v).strip() baris, peringatan, tak_dikenal = [], [], set() terlihat = set() for row in rows[header_i + 1:]: vals = list(row) nim = ambil(vals, "nim") mk = ambil(vals, "mk").upper() if not nim or not mk: continue if mk not in kode_mk_valid: tak_dikenal.add(mk) continue if (nim, mk) in terlihat: continue terlihat.add((nim, mk)) baris.append({"nim": nim, "nama": ambil(vals, "nama"), "mk_kode": mk, "kelas": ambil(vals, "kelas")}) if tak_dikenal: peringatan.append( f"{len(tak_dikenal)} kode MK tidak dikenal dan dilewati: " + ", ".join(sorted(tak_dikenal)[:6]) + ("…" if len(tak_dikenal) > 6 else "")) if not baris: peringatan.append("Tidak ada baris kontrak yang terbaca.") return baris, peringatan