This commit is contained in:
2026-03-10 11:55:30 -04:00
parent 614d6ad8f9
commit ee4297e1eb
3 changed files with 447 additions and 1 deletions

233
README.md
View File

@@ -1,2 +1,233 @@
# CrewChronicle # Weekly Crew Schedule Generator
A small Python utility that generates a **weekly crew scheduling workbook** (`.xlsx`) for **MondayFriday**.
Each day gets its own sheet with **time slots** and **one column per crew lead / supervisor**.
The workbook is designed to be:
* **Easy to edit**
* **Clean when printed**
* **Simple to export to PDF**
This is useful for scheduling **customer visits, job assignments, or crew work plans**.
---
# Features
* Generates **one sheet per weekday**
* Monday
* Tuesday
* Wednesday
* Thursday
* Friday
* Configurable via `config.json`
* Start time
* End time
* Slot size (minutes)
* Crew / supervisor names
* Default schedule:
* **08:00 → 17:00**
* **60 minute slots**
* Clean printable layout
* Works with:
* **LibreOffice Calc**
* **Microsoft Excel**
* **OnlyOffice**
* Easily **exportable to PDF**
---
# Example Layout
```
-----------------------------------------------------------
| Time Slot | Crew 1 | Crew 2 | Crew 3 | Crew 4 | Crew 5 |
-----------------------------------------------------------
| 08:00-09:00 | | | | | |
| 09:00-10:00 | | | | | |
| 10:00-11:00 | | | | | |
| ... |
-----------------------------------------------------------
```
Each **row** represents a time slot.
Each **column** represents a crew lead or supervisor.
You can write things like:
```
Customer Name
Address
Notes
```
inside each cell.
---
# Requirements
Python **3.8+**
Python package:
```
openpyxl
```
Install it with:
```
pip install openpyxl
```
---
# Project Structure
```
crew_schedule/
├─ generate_week_schedule_xlsx.py
├─ config.json
├─ README.md
```
---
# Configuration
Edit `config.json` to define crews and schedule hours.
Example:
```json
{
"start_time": "08:00",
"end_time": "17:00",
"slot_minutes": 60,
"crews": [
"Crew 1",
"Crew 2",
"Crew 3"
]
}
```
## Example With Named Supervisors
```json
{
"start_time": "07:00",
"end_time": "16:00",
"slot_minutes": 60,
"crews": [
"Mike",
"Sara",
"John",
"Chris",
"Dana"
]
}
```
---
# Usage
Generate the schedule for the **current week**:
```
python generate_week_schedule_xlsx.py
```
Generate a specific week:
```
python generate_week_schedule_xlsx.py 2026-03-09
```
The script automatically aligns the provided date to **Monday**.
---
# Output
Example output file:
```
weekly_schedule_2026-03-09.xlsx
```
Inside the workbook:
```
Monday
Tuesday
Wednesday
Thursday
Friday
```
Each sheet contains:
* Time slots
* Crew columns
* Large writable cells
---
# Exporting to PDF
### LibreOffice
1. Open the `.xlsx` file
2. Fill in the schedule
3. Click:
```
File → Export As → Export as PDF
```
### Microsoft Excel
```
File → Export → Create PDF/XPS
```
---
# Customization Ideas
Possible improvements:
* Color coding for crews
* Customer / Supervisor / Notes sub-lines
* Automatic job numbering
* Multiple weeks generated at once
* Direct PDF generation
* ICS calendar export
---
# License
MIT License
Use freely, modify freely.
---
# Author
Internal scheduling utility designed for **crew-based weekly planning**.

10
config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"start_time": "08:00",
"end_time": "17:00",
"slot_minutes": 60,
"crews": [
"James S",
"Noah B",
"Stephen"
]
}

View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
from datetime import date, datetime, time, timedelta
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
CONFIG_FILE = Path("config.json")
def parse_date(value: str) -> date:
return datetime.strptime(value, "%Y-%m-%d").date()
def parse_time(value: str) -> time:
return datetime.strptime(value, "%H:%M").time()
def get_monday(any_day: date) -> date:
return any_day - timedelta(days=any_day.weekday())
def load_config(path: Path) -> dict:
if not path.exists():
raise FileNotFoundError(f"Missing config file: {path}")
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
required_keys = ["start_time", "end_time", "slot_minutes", "crews"]
for key in required_keys:
if key not in data:
raise ValueError(f"config.json is missing '{key}'")
crews = data["crews"]
if not isinstance(crews, list) or not crews:
raise ValueError("'crews' must be a non-empty array")
clean_crews: list[str] = []
for crew in crews:
if not isinstance(crew, str):
raise ValueError("Each crew name must be a string")
crew_name = crew.strip()
if not crew_name:
raise ValueError("Crew names cannot be empty")
clean_crews.append(crew_name)
slot_minutes = int(data["slot_minutes"])
if slot_minutes <= 0:
raise ValueError("'slot_minutes' must be greater than zero")
return {
"start_time": parse_time(data["start_time"]),
"end_time": parse_time(data["end_time"]),
"slot_minutes": slot_minutes,
"crews": clean_crews,
}
def generate_time_slots(start: time, end: time, slot_minutes: int) -> list[str]:
start_dt = datetime.combine(date.today(), start)
end_dt = datetime.combine(date.today(), end)
if end_dt <= start_dt:
raise ValueError("'end_time' must be later than 'start_time'")
slots: list[str] = []
current = start_dt
while current < end_dt:
next_slot = current + timedelta(minutes=slot_minutes)
slots.append(
f"{current.strftime('%I:%M %p')} - {next_slot.strftime('%I:%M %p')}"
)
current = next_slot
return slots
def apply_page_setup(worksheet) -> None:
worksheet.page_setup.orientation = "landscape"
worksheet.page_setup.fitToWidth = 1
worksheet.page_setup.fitToHeight = 0
worksheet.print_options.horizontalCentered = True
worksheet.sheet_view.showGridLines = False
worksheet.freeze_panes = "B4"
def style_sheet(worksheet, schedule_date: date, crews: list[str], time_slots: list[str]) -> None:
thin_side = Side(style="thin", color="000000")
border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
title_fill = PatternFill(fill_type="solid", fgColor="D9EAF7")
header_fill = PatternFill(fill_type="solid", fgColor="EDEDED")
title_font = Font(size=18, bold=True)
header_font = Font(size=11, bold=True)
cell_font = Font(size=10)
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
top_wrap = Alignment(horizontal="left", vertical="top", wrap_text=True)
sheet_title = schedule_date.strftime("%A").upper()
sheet_date = schedule_date.strftime("%B %d, %Y")
last_col = 1 + len(crews)
last_col_letter = get_column_letter(last_col)
worksheet.merge_cells(f"A1:{last_col_letter}1")
worksheet["A1"] = f"{sheet_title} - {sheet_date}"
worksheet["A1"].font = title_font
worksheet["A1"].alignment = center
worksheet["A1"].fill = title_fill
worksheet["A1"].border = border
worksheet.row_dimensions[1].height = 28
worksheet["A3"] = "Time Slot"
worksheet["A3"].font = header_font
worksheet["A3"].alignment = center
worksheet["A3"].fill = header_fill
worksheet["A3"].border = border
for crew_index, crew_name in enumerate(crews, start=2):
cell = worksheet.cell(row=3, column=crew_index)
cell.value = crew_name
cell.font = header_font
cell.alignment = center
cell.fill = header_fill
cell.border = border
worksheet.column_dimensions["A"].width = 18
for crew_index in range(2, last_col + 1):
worksheet.column_dimensions[get_column_letter(crew_index)].width = 28
row_index = 4
for slot in time_slots:
time_cell = worksheet.cell(row=row_index, column=1)
time_cell.value = slot
time_cell.font = header_font
time_cell.alignment = center
time_cell.border = border
for crew_index in range(2, last_col + 1):
cell = worksheet.cell(row=row_index, column=crew_index)
cell.value = ""
cell.font = cell_font
cell.alignment = top_wrap
cell.border = border
worksheet.row_dimensions[row_index].height = 70
row_index += 1
apply_page_setup(worksheet)
def build_workbook(monday: date, crews: list[str], time_slots: list[str]) -> Workbook:
workbook = Workbook()
default_sheet = workbook.active
workbook.remove(default_sheet)
for day_offset in range(5):
schedule_date = monday + timedelta(days=day_offset)
sheet_name = schedule_date.strftime("%A")
worksheet = workbook.create_sheet(title=sheet_name)
style_sheet(worksheet, schedule_date, crews, time_slots)
return workbook
def main() -> int:
try:
input_date = parse_date(sys.argv[1]) if len(sys.argv) > 1 else date.today()
monday = get_monday(input_date)
config = load_config(CONFIG_FILE)
time_slots = generate_time_slots(
config["start_time"],
config["end_time"],
config["slot_minutes"],
)
workbook = build_workbook(monday, config["crews"], time_slots)
output_path = Path(f"weekly_schedule_{monday.isoformat()}.xlsx")
workbook.save(output_path)
print(f"Created: {output_path.resolve()}")
return 0
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())