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