~/physio-assessment/generate_physio_pdf.py
"""
Physiotherapy Pre/Post Assessment PDF Template
Patient: Akash Chaouhan | 19Y/M | MYH Indore
Dates: Pre: 17/07/2026 | Post: 07/08/2026
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus.flowables import KeepTogether
from reportlab.lib.colors import HexColor
# ── Colour palette ──────────────────────────────────────────────────────────
DARK_BLUE = HexColor('#1A3A5C')
MID_BLUE = HexColor('#2E6DA4')
LIGHT_BLUE = HexColor('#D6E8F7')
ACCENT_TEAL = HexColor('#17A589')
WARN_RED = HexColor('#C0392B')
WARN_LIGHT = HexColor('#FADBD8')
GOLD = HexColor('#D4AC0D')
LIGHT_GREY = HexColor('#F2F3F4')
MID_GREY = HexColor('#85929E')
WHITE = colors.white
BLACK = colors.black
PAGE_W, PAGE_H = A4
L_MARGIN = R_MARGIN = 15*mm
T_MARGIN = B_MARGIN = 15*mm
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def s(name, **kw):
base = styles['Normal']
return ParagraphStyle(name, parent=base, **kw)
H1 = s('H1', fontName='Helvetica-Bold', fontSize=15, textColor=WHITE,
alignment=TA_CENTER, spaceAfter=2)
H2 = s('H2', fontName='Helvetica-Bold', fontSize=10, textColor=DARK_BLUE,
spaceBefore=8, spaceAfter=3)
H3 = s('H3', fontName='Helvetica-Bold', fontSize=8, textColor=MID_BLUE,
spaceBefore=4, spaceAfter=2)
BODY = s('BODY', fontName='Helvetica', fontSize=7.5, textColor=BLACK,
leading=11)
SMALL= s('SMALL',fontName='Helvetica', fontSize=6.5, textColor=MID_GREY,
leading=9)
NOTE = s('NOTE', fontName='Helvetica-Oblique', fontSize=7, textColor=WARN_RED,
leading=10)
CELL = s('CELL', fontName='Helvetica', fontSize=7.5, alignment=TA_CENTER,
leading=10)
CELLL= s('CELLL',fontName='Helvetica', fontSize=7.5, alignment=TA_LEFT,
leading=10)
CELLB= s('CELLB',fontName='Helvetica-Bold', fontSize=7.5, alignment=TA_CENTER,
leading=10)
CELLBL=s('CELLBL',fontName='Helvetica-Bold',fontSize=7.5, alignment=TA_LEFT,
leading=10)
# ── Helpers ──────────────────────────────────────────────────────────────────
def blank(): return Paragraph(" ", BODY)
def section_header(title, color=DARK_BLUE):
t = Table([[Paragraph(title, ParagraphStyle(
'SH', fontName='Helvetica-Bold', fontSize=9,
textColor=WHITE, alignment=TA_LEFT))]],
colWidths=[PAGE_W - L_MARGIN - R_MARGIN])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), color),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING',(0,0),(-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 8),
]))
return t
def base_table_style(header_rows=1):
return [
# Header row(s)
('BACKGROUND', (0,0), (-1, header_rows-1), DARK_BLUE),
('TEXTCOLOR', (0,0), (-1, header_rows-1), WHITE),
('FONTNAME', (0,0), (-1, header_rows-1), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,-1), 7.5),
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('GRID', (0,0), (-1,-1), 0.4, HexColor('#BDC3C7')),
('ROWBACKGROUNDS',(0,header_rows),(-1,-1),[WHITE, LIGHT_GREY]),
('TOPPADDING', (0,0), (-1,-1), 3),
('BOTTOMPADDING',(0,0), (-1,-1), 3),
('LEFTPADDING', (0,0), (-1,-1), 4),
('RIGHTPADDING', (0,0), (-1,-1), 4),
]
def grade_cell(grade, is_post=False, highlight=False):
"""Return a Paragraph styled for MMT grade cells."""
if is_post and highlight:
st = ParagraphStyle('GC', fontName='Helvetica-Bold', fontSize=7.5,
textColor=ACCENT_TEAL, alignment=TA_CENTER)
elif is_post:
st = ParagraphStyle('GC2', fontName='Helvetica-Bold', fontSize=7.5,
textColor=MID_BLUE, alignment=TA_CENTER)
else:
st = CELL
return Paragraph(str(grade), st)
def warn_cell(text):
st = ParagraphStyle('WC', fontName='Helvetica-Bold', fontSize=7.5,
textColor=WARN_RED, alignment=TA_CENTER)
return Paragraph(text, st)
def normal_cell(text):
st = ParagraphStyle('NC', fontName='Helvetica', fontSize=7.5,
textColor=ACCENT_TEAL, alignment=TA_CENTER)
return Paragraph(text, st)
# ─────────────────────────────────────────────────────────────────────────────
# DATA
# ─────────────────────────────────────────────────────────────────────────────
MMT_GRADE_SCALE = [
['Grade', 'Description'],
['0', 'No contraction'],
['1', 'Visible/palpable contraction, no movement'],
['2', 'Full ROM – gravity eliminated'],
['3', 'Full ROM against gravity, no resistance'],
['3+', 'Full ROM against gravity + minimal resistance'],
['4−', 'Full ROM + moderate resistance (slight break)'],
['4', 'Full ROM + moderate resistance'],
['4+', 'Full ROM + near-full resistance'],
['5', 'Normal – full ROM + full resistance'],
]
# MMT data: (Region, Muscle/Group, Nerve Root, Movement, Pre, Post, Flag)
# Flag: True = clinically significant (L5/S1 involvement)
MMT_UL = [
('Upper Limb', 'Deltoid', 'C5', 'Shoulder Abduction', '3+', '4', False),
('Upper Limb', 'Biceps Brachii', 'C5-C6', 'Elbow Flexion', '3+', '4', False),
('Upper Limb', 'Wrist Extensors (ECRL)','C6', 'Wrist Extension', '3+', '4', False),
('Upper Limb', 'Triceps Brachii', 'C7', 'Elbow Extension', '3+', '4', False),
('Upper Limb', 'Wrist Flexors (FCR)', 'C7', 'Wrist Flexion', '3+', '4', False),
('Upper Limb', 'Finger Extensors (EDC)','C7-C8','Finger Extension', '3+', '4', False),
('Upper Limb', 'Hand Interossei', 'T1', 'Finger Abduction', '3+', '4', False),
]
MMT_TRUNK = [
('Trunk/Core', 'Rectus Abdominis', 'T6-T12','Trunk Flexion', '3+', '4', False),
('Trunk/Core', 'Obliques', 'T7-T12','Trunk Rotation', '3+', '4−', False),
('Trunk/Core', 'Transversus Abdominis', 'T7-L1', 'Core Stabilization', '3+', '4', False),
('Trunk/Core', 'Erector Spinae', 'L1-L5', 'Trunk Extension', '3+', '4−', False),
('Trunk/Core', 'Lumbar Multifidus', 'L1-L5', 'Segmental Stability','3+', '4−', False),
('Trunk/Core', 'Pelvic Floor', 'S2-S4', 'Pelvic Floor Activation','3+','4', False),
('Trunk/Core', 'Diaphragm', 'C3-C5', 'Breathing', '3+', '4+', False),
]
MMT_LL = [
('Lower Limb', 'Iliopsoas', 'L1-L3', 'Hip Flexion', '3+', '4', False),
('Lower Limb', 'Hip Adductors', 'L2-L4', 'Hip Adduction', '3+', '4', False),
('Lower Limb', 'Quadriceps Femoris', 'L3-L4', 'Knee Extension', '3+', '4', False),
('Lower Limb', 'Tibialis Anterior', 'L4', 'Ankle Dorsiflexion', '3+', '4', False),
('Lower Limb', '★ Ext. Hallucis Longus','L5', 'Great Toe Extension', '3+', '4−', True),
('Lower Limb', '★ Gluteus Medius', 'L5', 'Hip Abduction', '3+', '4−', True),
('Lower Limb', 'Gluteus Maximus', 'L5-S1', 'Hip Extension', '3+', '4', False),
('Lower Limb', '★ Hamstrings', 'L5-S1', 'Knee Flexion', '3+', '4', True),
('Lower Limb', '★ Gastrocnemius/Soleus','S1-S2', 'Ankle Plantarflexion','3+','4', True),
('Lower Limb', 'Peroneus Longus/Brevis','L5-S1', 'Foot Eversion', '3+', '4', False),
]
DERMATOME_DATA = [
# (Level, Area, Pre, Post, Flag)
# Cervical
('C4', 'Shoulder cape area', 'Normal', 'Normal', False),
('C5', 'Lateral arm – deltoid patch', 'Normal', 'Normal', False),
('C6', 'Lateral forearm + thumb/index', 'Normal', 'Normal', False),
('C7', 'Middle finger + dorsal forearm', 'Normal', 'Normal', False),
('C8', 'Ring + little finger, medial forearm', 'Normal','Normal', False),
('T1', 'Medial arm', 'Normal', 'Normal', False),
# Thoracic/Lumbar
('T6', 'Xiphoid process level', 'Normal', 'Normal', False),
('T10', 'Umbilicus level', 'Normal', 'Normal', False),
('T12', 'Inguinal region', 'Normal', 'Normal', False),
('L1', 'Groin / upper medial thigh', 'Normal', 'Normal', False),
('L2', 'Anterior upper thigh', 'Normal', 'Normal', False),
('L3', 'Anterior knee / medial thigh', 'Normal', 'Normal', False),
('L4', 'Medial leg + medial foot', 'Normal', 'Normal', False),
('★ L5','Dorsum foot + 1st webspace + lateral leg', 'Assess', 'Assess', True),
('★ S1','Lateral foot + sole + small toe','Assess', 'Assess', True),
('S2', 'Posterior thigh', 'Normal', 'Normal', False),
('S3', 'Inner thigh / perineum', 'Normal', 'Normal', False),
('S4-S5','Perianal / saddle area ⚠', 'Assess', 'Assess', True),
]
MYOTOME_DATA = [
# (Root, Key Movement, Muscle, Pre, Post, Flag)
('C3-C4', 'Shoulder Shrug', 'Trapezius', '3+', '4+', False),
('C5', 'Shoulder Abduction', 'Deltoid', '3+', '4', False),
('C6', 'Elbow Flexion', 'Biceps', '3+', '4', False),
('C7', 'Elbow Extension', 'Triceps', '3+', '4', False),
('C8', 'Finger Flexion', 'Flexor Digitorum', '3+', '4', False),
('T1', 'Finger Abduction', 'Interossei', '3+', '4', False),
('L2', 'Hip Flexion', 'Iliopsoas', '3+', '4', False),
('L3', 'Knee Extension', 'Quadriceps', '3+', '4', False),
('L4', 'Ankle Dorsiflexion', 'Tibialis Anterior', '3+', '4', False),
('★ L5', 'Big Toe Extension', 'Ext. Hallucis Longus', '3+', '4−', True),
('★ S1', 'Ankle Plantarflexion', 'Gastrocnemius', '3+', '4', True),
('S1-S2', 'Knee Flexion', 'Hamstrings', '3+', '4', False),
('S3-S4', 'Pelvic Floor Contraction', 'Pelvic Floor', '3+', '4', False),
]
REFLEX_DATA = [
# (Reflex, Root, Pre, Post, Flag)
('Biceps Jerk', 'C5-C6', '++ (Normal)', '++ (Normal)', False),
('Triceps Jerk', 'C7', '++ (Normal)', '++ (Normal)', False),
('Supinator Jerk', 'C6', '++ (Normal)', '++ (Normal)', False),
('Knee Jerk (Patellar)', 'L3-L4', '++ (Normal)', '++ (Normal)', False),
('★ Ankle Jerk (Achilles)','S1-S2','+ (Assess)', '++ (Assess)', True),
('Babinski Sign', 'UMN', 'Negative', 'Negative', False),
]
FUNCTIONAL_DATA = [
# (Parameter, Pre, Post, Target)
('VAS Pain Score', '7/10', '___/10', '<3/10'),
('Global MMT (Average)', '3+', '___', '5'),
('Lumbar ROM', 'Unable / Very painful','___', 'Full'),
('Cervical ROM', 'Painful', '___', 'Full'),
('Posture', 'Kyphoscoliotic', '___', 'Neutral'),
('Walking Tolerance', 'Severely limited', '___', '30 min flat'),
('SLR Test (R/L)', '___/___°', '___/___°', 'Negative'),
('ODI Score', '____%', '____%', '<20%'),
('SRS-22r Score', '___/110', '___/110', '>85/110'),
('Micheli Functional Scale', '___/5', '___/5', '0/5'),
]
# ─────────────────────────────────────────────────────────────────────────────
# PAGE TEMPLATE
# ─────────────────────────────────────────────────────────────────────────────
def header_footer(canvas, doc):
canvas.saveState()
W, H = A4
# ── Top header bar ──
canvas.setFillColor(DARK_BLUE)
canvas.rect(0, H - 22*mm, W, 22*mm, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont('Helvetica-Bold', 13)
canvas.drawCentredString(W/2, H - 11*mm,
'PHYSIOTHERAPY ASSESSMENT RECORD – PRE & POST TREATMENT')
canvas.setFont('Helvetica', 8)
canvas.drawCentredString(W/2, H - 17*mm,
'MGM Medical College & Hospitals – MYH Indore | Physiotherapy OPD/IPD')
# ── Accent line ──
canvas.setFillColor(ACCENT_TEAL)
canvas.rect(0, H - 23.5*mm, W, 1.5*mm, fill=1, stroke=0)
# ── Footer ──
canvas.setFillColor(LIGHT_GREY)
canvas.rect(0, 0, W, 10*mm, fill=1, stroke=0)
canvas.setFillColor(MID_GREY)
canvas.setFont('Helvetica', 7)
canvas.drawString(L_MARGIN, 3.5*mm,
'★ = Clinically significant level (L5-S1 spondylolisthesis) | '
'⚠ = Red flag – assess for cauda equina syndrome | '
'All shaded cells require examiner to fill in actual values')
canvas.drawRightString(W - R_MARGIN, 3.5*mm,
f'Page {doc.page}')
canvas.restoreState()
# ─────────────────────────────────────────────────────────────────────────────
# BUILD
# ─────────────────────────────────────────────────────────────────────────────
def build_pdf(outpath):
doc = SimpleDocTemplate(
outpath,
pagesize=A4,
leftMargin=L_MARGIN, rightMargin=R_MARGIN,
topMargin=27*mm, bottomMargin=14*mm,
title='Physiotherapy Assessment – Akash Chaouhan',
author='MYH Indore Physiotherapy Dept',
)
W = PAGE_W - L_MARGIN - R_MARGIN # usable width
story = []
# ══════════════════════════════════════════════════════════════════════════
# PATIENT INFO BOX
# ══════════════════════════════════════════════════════════════════════════
pi_data = [
[Paragraph('<b>Patient Name:</b> Akash Chaouhan', BODY),
Paragraph('<b>Age/Sex:</b> 19 Years / Male', BODY),
Paragraph('<b>UHID:</b> 260300340857', BODY)],
[Paragraph('<b>Diagnosis:</b> Post-traumatic L5-S1 High-Grade Spondylolisthesis + Kyphoscoliotic Posture', BODY),
Paragraph('<b>Dept:</b> MYH Physiotherapy', BODY),
Paragraph('<b>OPD No:</b> 202601744418', BODY)],
[Paragraph('<b>H/O:</b> Fall from bike – 5 months ago', BODY),
Paragraph('<b>Pre-Assessment Date:</b> 17/07/2026', BODY),
Paragraph('<b>Post-Assessment Date:</b> 07/08/2026', BODY)],
[Paragraph('<b>Referred By:</b> _______________', BODY),
Paragraph('<b>Treating PT:</b> _______________', BODY),
Paragraph('<b>Supervisor Sign:</b> _______________', BODY)],
]
pi_table = Table(pi_data, colWidths=[W*0.45, W*0.3, W*0.25])
pi_table.setStyle(TableStyle([
('BOX', (0,0), (-1,-1), 1, DARK_BLUE),
('INNERGRID', (0,0), (-1,-1), 0.3, HexColor('#BDC3C7')),
('BACKGROUND', (0,0), (-1,-1), LIGHT_BLUE),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING',(0,0),(-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 6),
('SPAN', (0,1), (0,1)),
]))
story.append(pi_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# MMT GRADING SCALE LEGEND
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' MMT GRADING SCALE REFERENCE'))
story.append(Spacer(1, 2*mm))
legend_header = [Paragraph(c, CELLB) for c in ['Grade', 'Description']]
legend_rows = [legend_header]
for row in MMT_GRADE_SCALE[1:]:
legend_rows.append([Paragraph(row[0], CELLB), Paragraph(row[1], CELLL)])
legend_table = Table(legend_rows, colWidths=[W*0.12, W*0.88])
ls = base_table_style()
for i, row in enumerate(MMT_GRADE_SCALE[1:], 1):
g = row[0]
if g in ('5',):
ls.append(('BACKGROUND', (0,i), (-1,i), HexColor('#D5F5E3')))
elif g in ('3+',):
ls.append(('BACKGROUND', (0,i), (-1,i), HexColor('#FEF9E7')))
ls.append(('TEXTCOLOR', (0,i), (0,i), WARN_RED))
legend_table.setStyle(TableStyle(ls))
story.append(legend_table)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# MMT CHARTS
# ══════════════════════════════════════════════════════════════════════════
col_labels = ['Muscle / Group', 'Nerve Root', 'Movement',
'Pre\n17/07/26', 'Post\n07/08/26', 'Change']
cw = [W*0.23, W*0.10, W*0.23, W*0.12, W*0.12, W*0.10]
def build_mmt_section(title, data, color=MID_BLUE):
story.append(section_header(f' MANUAL MUSCLE TESTING – {title}', color))
story.append(Spacer(1, 2*mm))
rows = [[Paragraph(c, CELLB) for c in col_labels]]
for (region, muscle, root, move, pre, post, flag) in data:
name_style = ParagraphStyle('MS', fontName='Helvetica-Bold' if flag else 'Helvetica',
fontSize=7.5, textColor=WARN_RED if flag else BLACK,
alignment=TA_LEFT)
change = '↑' if (pre != post) else '–'
rows.append([
Paragraph(muscle, name_style),
Paragraph(root, CELL),
Paragraph(move, CELLL),
grade_cell(pre),
grade_cell(post, is_post=True, highlight=(pre != post)),
Paragraph(change, ParagraphStyle('CHG', fontName='Helvetica-Bold',
fontSize=9, textColor=ACCENT_TEAL, alignment=TA_CENTER)),
])
t = Table(rows, colWidths=cw)
ts = base_table_style()
for i, (region, muscle, root, move, pre, post, flag) in enumerate(data, 1):
if flag:
ts.append(('BACKGROUND', (0,i), (2,i), HexColor('#FEF5E7')))
ts.append(('BACKGROUND', (3,i), (3,i), HexColor('#FDEDEC')))
ts.append(('BACKGROUND', (4,i), (4,i), HexColor('#EBF5FB')))
t.setStyle(TableStyle(ts))
story.append(t)
story.append(Spacer(1, 3*mm))
build_mmt_section('UPPER LIMB (C5 – T1)', MMT_UL)
build_mmt_section('TRUNK & CORE (T6 – S4)', MMT_TRUNK, ACCENT_TEAL)
build_mmt_section('LOWER LIMB (L1 – S2) ★ Critical Levels Highlighted', MMT_LL)
# ══════════════════════════════════════════════════════════════════════════
# REFLEX CHART
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' DEEP TENDON REFLEXES (DTR)'))
story.append(Spacer(1, 2*mm))
ref_headers = ['Reflex', 'Root Level', 'Pre (17/07/26)', 'Post (07/08/26)', 'Notes']
ref_cw = [W*0.28, W*0.12, W*0.22, W*0.22, W*0.16]
ref_rows = [[Paragraph(c, CELLB) for c in ref_headers]]
for (reflex, root, pre, post, flag) in REFLEX_DATA:
name_style = ParagraphStyle('RS', fontName='Helvetica-Bold' if flag else 'Helvetica',
fontSize=7.5, textColor=WARN_RED if flag else BLACK,
alignment=TA_LEFT)
ref_rows.append([
Paragraph(reflex, name_style),
Paragraph(root, CELL),
Paragraph(pre, CELL),
Paragraph(post, ParagraphStyle('RP', fontName='Helvetica-Bold', fontSize=7.5,
textColor=MID_BLUE, alignment=TA_CENTER)),
Paragraph('Monitor closely' if flag else '', SMALL),
])
ref_t = Table(ref_rows, colWidths=ref_cw)
ref_ts = base_table_style()
for i, (reflex, root, pre, post, flag) in enumerate(REFLEX_DATA, 1):
if flag:
ref_ts.append(('BACKGROUND', (0,i), (-1,i), HexColor('#FEF5E7')))
ref_t.setStyle(TableStyle(ref_ts))
story.append(ref_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# PAGE BREAK → Dermatome / Myotome page
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# DERMATOME CHART
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' DERMATOME SENSORY ASSESSMENT CHART'))
story.append(Spacer(1, 2*mm))
derm_note = (
'Sensory Test Key: <b>Normal</b> = intact light touch & pinprick | '
'<b>Reduced</b> = diminished sensation | '
'<b>Absent</b> = no sensation | '
'<b>Hyperaesthesia</b> = heightened/painful | '
'<b>Assess</b> = examiner must record actual finding in blank cell'
)
story.append(Paragraph(derm_note, SMALL))
story.append(Spacer(1, 2*mm))
derm_headers = ['Root\nLevel', 'Dermatome Region', 'Test Method',
'Pre\n17/07/26', 'Post\n07/08/26', 'Remarks']
derm_cw = [W*0.08, W*0.30, W*0.18, W*0.14, W*0.14, W*0.16]
derm_rows = [[Paragraph(c, CELLB) for c in derm_headers]]
for (level, area, pre, post, flag) in DERMATOME_DATA:
lv_style = ParagraphStyle('DL', fontName='Helvetica-Bold' if flag else 'Helvetica-Bold',
fontSize=7.5, textColor=WARN_RED if flag else MID_BLUE,
alignment=TA_CENTER)
pre_p = Paragraph('________', CELL) if pre == 'Assess' else Paragraph(pre, normal_cell('') if pre == 'Normal' else CELL)
post_p = Paragraph('________', CELL) if post == 'Assess' else Paragraph(post, CELL)
# Use actual normal_cell colour trick
if pre == 'Normal':
pre_p = Paragraph('Normal', ParagraphStyle('NM', fontName='Helvetica',
fontSize=7.5, textColor=ACCENT_TEAL, alignment=TA_CENTER))
elif pre == 'Assess':
pre_p = Paragraph('________', ParagraphStyle('AS', fontName='Helvetica',
fontSize=8, textColor=WARN_RED, alignment=TA_CENTER))
if post == 'Normal':
post_p = Paragraph('Normal', ParagraphStyle('NM2', fontName='Helvetica',
fontSize=7.5, textColor=ACCENT_TEAL, alignment=TA_CENTER))
elif post == 'Assess':
post_p = Paragraph('________', ParagraphStyle('AS2', fontName='Helvetica',
fontSize=8, textColor=WARN_RED, alignment=TA_CENTER))
remark = '⚠ L5-S1 root' if flag and 'S4' not in level and 'S3' not in level else ('⚠ Cauda equina!' if 'S4' in level else '')
derm_rows.append([
Paragraph(level, lv_style),
Paragraph(area, CELLL),
Paragraph('Light touch + Pinprick', SMALL),
pre_p,
post_p,
Paragraph(remark, NOTE if remark else SMALL),
])
derm_t = Table(derm_rows, colWidths=derm_cw)
derm_ts = base_table_style()
for i, (level, area, pre, post, flag) in enumerate(DERMATOME_DATA, 1):
if flag:
derm_ts.append(('BACKGROUND', (0,i), (-1,i), WARN_LIGHT))
derm_t.setStyle(TableStyle(derm_ts))
story.append(derm_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# MYOTOME CHART
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' MYOTOME ASSESSMENT CHART (Resisted Isometric Testing)', ACCENT_TEAL))
story.append(Spacer(1, 2*mm))
myo_headers = ['Root\nLevel', 'Key Test Movement', 'Muscle Tested',
'Pre\n17/07/26', 'Post\n07/08/26', 'Change', 'Notes']
myo_cw = [W*0.08, W*0.22, W*0.22, W*0.10, W*0.10, W*0.08, W*0.20]
myo_rows = [[Paragraph(c, CELLB) for c in myo_headers]]
for (root, move, muscle, pre, post, flag) in MYOTOME_DATA:
root_style = ParagraphStyle('ML', fontName='Helvetica-Bold', fontSize=7.5,
textColor=WARN_RED if flag else MID_BLUE,
alignment=TA_CENTER)
change = '↑' if pre != post else '–'
chg_style = ParagraphStyle('MC', fontName='Helvetica-Bold', fontSize=9,
textColor=ACCENT_TEAL, alignment=TA_CENTER)
note = '⚠ Monitor – spondylolisthesis level' if flag else ''
myo_rows.append([
Paragraph(root, root_style),
Paragraph(move, CELLL),
Paragraph(muscle, CELLL),
grade_cell(pre),
grade_cell(post, is_post=True, highlight=(pre != post)),
Paragraph(change, chg_style),
Paragraph(note, NOTE if note else SMALL),
])
myo_t = Table(myo_rows, colWidths=myo_cw)
myo_ts = base_table_style()
for i, (root, move, muscle, pre, post, flag) in enumerate(MYOTOME_DATA, 1):
if flag:
myo_ts.append(('BACKGROUND', (0,i), (-1,i), HexColor('#FEF5E7')))
myo_t.setStyle(TableStyle(myo_ts))
story.append(myo_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# PAGE BREAK → Functional outcomes page
# ══════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════════════
# FUNCTIONAL OUTCOME MEASURES
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' FUNCTIONAL OUTCOME MEASURES & SCALES'))
story.append(Spacer(1, 2*mm))
func_headers = ['Parameter', 'Pre-Treatment\n17/07/2026', 'Post-Treatment\n07/08/2026', 'Discharge Target']
func_cw = [W*0.35, W*0.22, W*0.22, W*0.21]
func_rows = [[Paragraph(c, CELLB) for c in func_headers]]
for (param, pre, post, target) in FUNCTIONAL_DATA:
func_rows.append([
Paragraph(param, CELLBL),
Paragraph(pre, CELL),
Paragraph(post, ParagraphStyle('FP', fontName='Helvetica-Bold', fontSize=7.5,
textColor=MID_BLUE, alignment=TA_CENTER)),
Paragraph(target, ParagraphStyle('FT', fontName='Helvetica', fontSize=7.5,
textColor=ACCENT_TEAL, alignment=TA_CENTER)),
])
func_t = Table(func_rows, colWidths=func_cw)
func_t.setStyle(TableStyle(base_table_style()))
story.append(func_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# RECOMMENDED OUTCOME SCALES BOX
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' RECOMMENDED OUTCOME SCALES FOR THIS CASE', ACCENT_TEAL))
story.append(Spacer(1, 2*mm))
scale_data = [
['Scale', 'Abbreviation', 'Domains', 'Frequency', 'Why This Case'],
['Visual Analogue Scale', 'VAS', 'Pain 0–10', 'Every session', 'Quick pain monitoring; VAS 7/10 at baseline'],
['Numeric Pain Rating Scale', 'NPRS', 'Pain 0–10', 'Every session', 'Parallel with VAS; more verbal-friendly'],
['Oswestry Disability Index', 'ODI', '10 domains – LBP disability', 'Baseline, 4 wk, 8 wk, Discharge', 'Gold standard LBP functional outcome'],
['Micheli Functional Scale', 'MFS', 'Spondylolysis-specific ADL', 'Baseline & Discharge', 'Validated for 19yr / spondylolisthesis – avoids ODI floor effect'],
['Scoliosis Research Society-22r', 'SRS-22r', 'Pain, Function, Self-image, Mental health, Satisfaction', 'Baseline & Discharge', 'Covers kyphoscoliosis component + psychosocial impact'],
['Patient-Specific Functional Scale', 'PSFS', 'Patient-chosen activities (0–10)', 'Baseline, 4 wk, Discharge', 'Tailored to this patient – standing & walking difficulty'],
['Roland-Morris Disability Questionnaire', 'RMDQ', '24-item LBP disability', 'Monthly', 'Simple alternative to ODI for progress tracking'],
['Functional Independence Measure', 'FIM', 'ADL independence', 'Baseline & Discharge', 'Global deconditioning with muscle wasting – tracks ADL recovery'],
]
scale_cw = [W*0.24, W*0.10, W*0.22, W*0.16, W*0.28]
scale_rows = []
for i, row in enumerate(scale_data):
style = CELLB if i == 0 else CELLL
style_c = CELLB if i == 0 else CELL
scale_rows.append([
Paragraph(row[0], style),
Paragraph(row[1], style_c),
Paragraph(row[2], CELLL if i > 0 else CELLB),
Paragraph(row[3], style_c),
Paragraph(row[4], CELLL if i > 0 else CELLB),
])
scale_t = Table(scale_rows, colWidths=scale_cw)
scale_ts = base_table_style()
scale_ts += [
('BACKGROUND', (0,1), (-1,1), HexColor('#EBF5FB')),
('BACKGROUND', (0,2), (-1,2), WHITE),
('BACKGROUND', (0,3), (-1,3), HexColor('#D5F5E3')),
('BACKGROUND', (0,4), (-1,4), WARN_LIGHT),
('BACKGROUND', (0,5), (-1,5), HexColor('#FEF9E7')),
]
scale_t.setStyle(TableStyle(scale_ts))
story.append(scale_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# TREATMENT LOG
# ══════════════════════════════════════════════════════════════════════════
story.append(section_header(' TREATMENT LOG (17/07/2026 – 07/08/2026)'))
story.append(Spacer(1, 2*mm))
tx_data = [
['#', 'Intervention', 'Sets × Reps / Duration', 'Dates Given', 'Progress Notes'],
['1', 'Kegel Exercises (Pelvic Floor)', '3 × 10 holds', '17/07 – 07/08', ''],
['2', 'Hamstring Curls (Prone)', '3 × 10', '17/07 – 07/08', ''],
['3', 'SLR – Supine & Side-lying', '3 × 10 each', '17/07 – 07/08', ''],
['4', 'Bridging Exercise', '3 × 10', '17/07 – 07/08', ''],
['5', 'Swiss Ball Press', '3 × 10', '17/07 – 07/08', ''],
['6', 'Trunk Control Exercises', '3 × 10 min', '17/07 – 07/08', ''],
['7', 'Shoulder ROM + Balance + Proprioception', '2 × 10 min', '17/07 – 07/08', ''],
['8', 'Core Activation (TrA / Multifidus)', '3 × 10', '17/07 – 07/08', ''],
['9', 'Diaphragmatic Breathing', '3 × 10 breaths','17/07 – 07/08', ''],
['10','Postural Education + Ergonomics', 'Daily', '17/07 – 07/08', ''],
]
tx_cw = [W*0.04, W*0.28, W*0.18, W*0.16, W*0.34]
tx_rows = []
for i, row in enumerate(tx_data):
if i == 0:
tx_rows.append([Paragraph(c, CELLB) for c in row])
else:
tx_rows.append([
Paragraph(row[0], CELL),
Paragraph(row[1], CELLL),
Paragraph(row[2], CELL),
Paragraph(row[3], CELL),
Paragraph(row[4], CELL),
])
tx_t = Table(tx_rows, colWidths=tx_cw)
tx_t.setStyle(TableStyle(base_table_style()))
story.append(tx_t)
story.append(Spacer(1, 4*mm))
# ══════════════════════════════════════════════════════════════════════════
# SIGNATURE BOX
# ══════════════════════════════════════════════════════════════════════════
sig_data = [[
Paragraph('<b>Treating Physiotherapist</b><br/><br/><br/>'
'Name: _____________________<br/>'
'Sign: _____________________<br/>'
'Date: _____________________', BODY),
Paragraph('<b>HOD / Supervisor</b><br/><br/><br/>'
'Name: _____________________<br/>'
'Sign: _____________________<br/>'
'Date: _____________________', BODY),
Paragraph('<b>Patient / Guardian Signature</b><br/><br/><br/>'
'Name: _____________________<br/>'
'Sign: _____________________<br/>'
'Date: _____________________', BODY),
]]
sig_t = Table(sig_data, colWidths=[W/3]*3)
sig_t.setStyle(TableStyle([
('BOX', (0,0),(-1,-1), 1, DARK_BLUE),
('INNERGRID', (0,0),(-1,-1), 0.5, MID_GREY),
('VALIGN', (0,0),(-1,-1), 'TOP'),
('TOPPADDING', (0,0),(-1,-1), 8),
('BOTTOMPADDING',(0,0),(-1,-1),8),
('LEFTPADDING',(0,0),(-1,-1), 8),
]))
story.append(sig_t)
# ── Build ──
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
print(f"PDF saved to: {outpath}")
if __name__ == '__main__':
outpath = '/tmp/workspace/physio-assessment/Akash_Chaouhan_Physio_Assessment.pdf'
build_pdf(outpath)