punch-list-manager
skillManage construction punch lists for project closeout. Track deficiencies, assign corrections, and monitor completion status.
apm::install
apm install @datadrivenconstruction/punch-list-managerapm::skill.md
---
name: "punch-list-manager"
description: "Manage construction punch lists for project closeout. Track deficiencies, assign corrections, and monitor completion status."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "✅", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# 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
```python
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.DataFrame:
"""Generate report for specific contractor."""
items = self.get_items_by_assignee(assignee)
return pd.DataFrame([
{
'Item ID': i.item_id,
'Location': i.location,
'Description': i.description,
'Priority': i.priority.value,
'Status': i.status.value,
'Due Date': i.due_date,
'Days Overdue': max(0, (date.today() - i.due_date).days) if i.status != PunchItemStatus.VERIFIED else 0
}
for i in items
])
def forecast_completion(self) -> Dict[str, Any]:
"""Forecast closeout completion."""
summary = self.get_summary()
remaining = summary.total_items - summary.verified
if remaining == 0:
return {
'status': 'COMPLETE',
'remaining_items': 0,
'on_track': True
}
# Calculate completion rate (items verified per day)
verified_items = self.get_items_by_status(PunchItemStatus.VERIFIED)
if verified_items:
dates = [i.verified_date for i in verified_items if i.verified_date]
if dates:
days_active = (max(dates) - min(dates)).days + 1
rate = len(dates) / days_active if days_active > 0 else 1
else:
rate = 1
else:
rate = 1
days_needed = remaining / rate if rate > 0 else remaining
projected_completion = date.today() + timedelta(days=int(days_needed))
return {
'status': 'IN_PROGRESS',
'remaining_items': remaining,
'completion_rate_per_day': round(rate, 1),
'days_needed': round(days_needed, 0),
'projected_completion': projected_completion,
'target_date': self.target_date,
'on_track': projected_completion <= self.target_date
}
def export_to_excel(self, output_path: str) -> str:
"""Export punch list to Excel."""
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Summary
summary = self.get_summary()
summary_df = pd.DataFrame([{
'Project': self.project_name,
'Target Closeout': self.target_date,
'Total Items': summary.total_items,
'Open': summary.open_items,
'In Progress': summary.in_progress,
'Completed': summary.completed,
'Verified': summary.verified,
'Completion %': summary.completion_rate,
'Overdue': summary.overdue_count
}])
summary_df.to_excel(writer, sheet_name='Summary', index=False)
# All items
items_df = pd.DataFrame([
{
'Item ID': i.item_id,
'Location': i.location,
'Description': i.description,
'Category': i.category.value,
'Priority': i.priority.value,
'Status': i.status.value,
'Assigned To': i.assigned_to,
'Created': i.created_date,
'Due': i.due_date,
'Completed': i.completed_date,
'Verified': i.verified_date,
'Notes': i.notes
}
for i in self.items.values()
])
items_df.to_excel(writer, sheet_name='All Items', index=False)
# By Assignee
assignee_df = pd.DataFrame([
{'Assignee': k, 'Open Items': v}
for k, v in summary.by_assignee.items()
])
if not assignee_df.empty:
assignee_df.to_excel(writer, sheet_name='By Assignee', index=False)
# Overdue
overdue = self.get_overdue_items()
if overdue:
overdue_df = pd.DataFrame([
{
'Item ID': i.item_id,
'Location': i.location,
'Description': i.description,
'Assigned To': i.assigned_to,
'Due': i.due_date,
'Days Overdue': (date.today() - i.due_date).days
}
for i in overdue
])
overdue_df.to_excel(writer, sheet_name='Overdue', index=False)
return output_path
```
## Quick Start
```python
from datetime import date, timedelta
# Initialize manager
punch = PunchListManager("Office Building A", target_closeout_date=date(2024, 12, 31))
# Add items
punch.add_item(
location="Level 3, Room 301",
description="Ceiling tile damaged",
category=PunchItemCategory.ARCHITECTURAL,
priority=PunchItemPriority.MEDIUM,
assigned_to="ABC Ceilings"
)
punch.add_item(
location="Lobby",
description="Fire alarm not functioning",
category=PunchItemCategory.FIRE_PROTECTION,
priority=PunchItemPriority.CRITICAL,
assigned_to="XYZ Fire Protection"
)
# Update status
punch.update_status("PL-0001", PunchItemStatus.COMPLETED)
punch.update_status("PL-0001", PunchItemStatus.VERIFIED, verified_by="John Smith")
```
## Common Use Cases
### 1. Summary Report
```python
summary = punch.get_summary()
print(f"Total: {summary.total_items}")
print(f"Completion: {summary.completion_rate}%")
print(f"Overdue: {summary.overdue_count}")
```
### 2. Contractor Report
```python
report = punch.generate_contractor_report("ABC Ceilings")
print(report)
```
### 3. Forecast Completion
```python
forecast = punch.forecast_completion()
print(f"On Track: {forecast['on_track']}")
print(f"Projected: {forecast['projected_completion']}")
```
## Resources
- **DDC Book**: Chapter 5.1 - Project Closeout