Punch List Manager
Business Case
Problem Statement
Project closeout challenges:
- Tracking hundreds of items
- Assigning responsibility
- Monitoring completion
- Documentation for handover
Solution
Systematic punch list management to track deficiencies, assignments, and completion through project closeout.
Technical Implementation
import pandas as pd
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
from datetime import date, timedelta
from enum import Enum
class PunchItemStatus(Enum):
OPEN = "open"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
VERIFIED = "verified"
REJECTED = "rejected"
class PunchItemPriority(Enum):
CRITICAL = "critical" # Life safety, code violation
HIGH = "high" # Functionality impaired
MEDIUM = "medium" # Cosmetic/minor
LOW = "low" # Nice to have
class PunchItemCategory(Enum):
STRUCTURAL = "structural"
ARCHITECTURAL = "architectural"
MECHANICAL = "mechanical"
ELECTRICAL = "electrical"
PLUMBING = "plumbing"
FIRE_PROTECTION = "fire_protection"
EXTERIOR = "exterior"
SITE = "site"
GENERAL = "general"
@dataclass
class PunchItem:
item_id: str
location: str
description: str
category: PunchItemCategory
priority: PunchItemPriority
status: PunchItemStatus
assigned_to: str
created_date: date
due_date: date
completed_date: Optional[date] = None
verified_date: Optional[date] = None
verified_by: str = ""
photos: List[str] = field(default_factory=list)
notes: str = ""
@dataclass
class PunchListSummary:
total_items: int
open_items: int
in_progress: int
completed: int
verified: int
rejected: int
completion_rate: float
by_category: Dict[str, int]
by_priority: Dict[str, int]
by_assignee: Dict[str, int]
overdue_count: int
class PunchListManager:
"""Manage construction punch lists."""
def __init__(self, project_name: str, target_closeout_date: date):
self.project_name = project_name
self.target_date = target_closeout_date
self.items: Dict[str, PunchItem] = {}
self._next_id = 1
def add_item(self,
location: str,
description: str,
category: PunchItemCategory,
priority: PunchItemPriority,
assigned_to: str,
due_date: date = None,
notes: str = "") -> PunchItem:
"""Add punch list item."""
item_id = f"PL-{self._next_id:04d}"
self._next_id += 1
if due_date is None:
# Default based on priority
if priority == PunchItemPriority.CRITICAL:
due_date = date.today() + timedelta(days=3)
elif priority == PunchItemPriority.HIGH:
due_date = date.today() + timedelta(days=7)
else:
due_date = date.today() + timedelta(days=14)
item = PunchItem(
item_id=item_id,
location=location,
description=description,
category=category,
priority=priority,
status=PunchItemStatus.OPEN,
assigned_to=assigned_to,
created_date=date.today(),
due_date=due_date,
notes=notes
)
self.items[item_id] = item
return item
def update_status(self,
item_id: str,
status: PunchItemStatus,
verified_by: str = ""):
"""Update item status."""
if item_id not in self.items:
return
item = self.items[item_id]
item.status = status
if status == PunchItemStatus.COMPLETED:
item.completed_date = date.today()
elif status == PunchItemStatus.VERIFIED:
item.verified_date = date.today()
item.verified_by = verified_by
def reassign_item(self, item_id: str, new_assignee: str, new_due_date: date = None):
"""Reassign item to different contractor."""
if item_id not in self.items:
return
item = self.items[item_id]
item.assigned_to = new_assignee
if new_due_date:
item.due_date = new_due_date
item.status = PunchItemStatus.OPEN
def add_note(self, item_id: str, note: str):
"""Add note to item."""
if item_id in self.items:
self.items[item_id].notes += f"\n{date.today()}: {note}"
def add_photo(self, item_id: str, photo_path: str):
"""Add photo reference to item."""
if item_id in self.items:
self.items[item_id].photos.append(photo_path)
def get_summary(self) -> PunchListSummary:
"""Get punch list summary."""
items = list(self.items.values())
today = date.today()
# Status counts
open_items = sum(1 for i in items if i.status == PunchItemStatus.OPEN)
in_progress = sum(1 for i in items if i.status == PunchItemStatus.IN_PROGRESS)
completed = sum(1 for i in items if i.status == PunchItemStatus.COMPLETED)
verified = sum(1 for i in items if i.status == PunchItemStatus.VERIFIED)
rejected = sum(1 for i in items if i.status == PunchItemStatus.REJECTED)
# Completion rate (verified / total)
completion_rate = (verified / len(items) * 100) if items else 0
# By category
by_category = {}
for cat in PunchItemCategory:
count = sum(1 for i in items if i.category == cat and i.status != PunchItemStatus.VERIFIED)
if count > 0:
by_category[cat.value] = count
# By priority
by_priority = {}
for pri in PunchItemPriority:
count = sum(1 for i in items if i.priority == pri and i.status != PunchItemStatus.VERIFIED)
if count > 0:
by_priority[pri.value] = count
# By assignee
by_assignee = {}
for item in items:
if item.status not in [PunchItemStatus.VERIFIED, PunchItemStatus.COMPLETED]:
if item.assigned_to not in by_assignee:
by_assignee[item.assigned_to] = 0
by_assignee[item.assigned_to] += 1
# Overdue
overdue = sum(
1 for i in items
if i.due_date < today and i.status not in [PunchItemStatus.VERIFIED, PunchItemStatus.COMPLETED]
)
return PunchListSummary(
total_items=len(items),
open_items=open_items,
in_progress=in_progress,
completed=completed,
verified=verified,
rejected=rejected,
completion_rate=round(completion_rate, 1),
by_category=by_category,
by_priority=by_priority,
by_assignee=by_assignee,
overdue_count=overdue
)
def get_items_by_status(self, status: PunchItemStatus) -> List[PunchItem]:
"""Get items by status."""
return [i for i in self.items.values() if i.status == status]
def get_items_by_assignee(self, assignee: str) -> List[PunchItem]:
"""Get items assigned to specific contractor."""
return [i for i in self.items.values() if i.assigned_to == assignee]
def get_overdue_items(self) -> List[PunchItem]:
"""Get overdue items."""
today = date.today()
return [
i for i in self.items.values()
if i.due_date < today and i.status not in [PunchItemStatus.VERIFIED, PunchItemStatus.COMPLETED]
]
def get_critical_items(self) -> List[PunchItem]:
"""Get critical priority items."""
return [
i for i in self.items.values()
if i.priority == PunchItemPriority.CRITICAL
and i.status not in [PunchItemStatus.VERIFIED]
]
def generate_contractor_report(self, assignee: str) -> pd.DataF