init
This commit is contained in:
233
README.md
233
README.md
@@ -1,2 +1,233 @@
|
|||||||
# CrewChronicle
|
# Weekly Crew Schedule Generator
|
||||||
|
|
||||||
|
A small Python utility that generates a **weekly crew scheduling workbook** (`.xlsx`) for **Monday–Friday**.
|
||||||
|
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
10
config.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"start_time": "08:00",
|
||||||
|
"end_time": "17:00",
|
||||||
|
"slot_minutes": 60,
|
||||||
|
"crews": [
|
||||||
|
"James S",
|
||||||
|
"Noah B",
|
||||||
|
"Stephen"
|
||||||
|
]
|
||||||
|
}
|
||||||
205
generate_week_schedule_xlsx.py
Normal file
205
generate_week_schedule_xlsx.py
Normal 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())
|
||||||
Reference in New Issue
Block a user