Составлен план работ + выполнен этап 1
This commit is contained in:
5
.continue/rules/python.md
Normal file
5
.continue/rules/python.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
description: A description of your rule
|
||||||
|
---
|
||||||
|
Ты senior python-разработчик
|
||||||
|
У тебя 5-лет опыта работы с библиотекой ortools
|
||||||
90
PLAN.md
Normal file
90
PLAN.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 📋 ПЛАН РАБОТ: Модульность конфигурации
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
Перенести все константы (настройки) в отдельный файл `config.py` для удобства редактирования человеком, не знакомым с Python.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 1: Создание файла конфигурации
|
||||||
|
|
||||||
|
**Файл:** `config.py`
|
||||||
|
|
||||||
|
**Что перенести:**
|
||||||
|
- **Настройки дня**
|
||||||
|
- Год (`year = 2026`)
|
||||||
|
- Месяц (`month = 4`)
|
||||||
|
|
||||||
|
- **Смены (`SHIFTS`)**
|
||||||
|
- С 06:00 до 14:30 (смена 0)
|
||||||
|
- С 08:00 до 16:30 (смена 1)
|
||||||
|
- С 11:30 до 20:00 (смена 2)
|
||||||
|
- С 12:30 до 21:00 (смена 3)
|
||||||
|
|
||||||
|
- **Операторы (`OPERATORS`)**
|
||||||
|
- Иванов
|
||||||
|
- Алексеев
|
||||||
|
- Сидоров
|
||||||
|
- Козобородов
|
||||||
|
- Трудяшкина
|
||||||
|
- Петрова
|
||||||
|
|
||||||
|
- **Предпочтения операторов (`OPERATOR_PREFERENCES`)**
|
||||||
|
- Иванов: 10, 13 апреля
|
||||||
|
- Алексеев: понедельники
|
||||||
|
- Сидоров: 25, 26 апреля
|
||||||
|
- Козобородов: смена 2 (11:30–20:00)
|
||||||
|
- Трудяшкина: 4 апреля
|
||||||
|
- Петрова: 8, 29 апреля
|
||||||
|
|
||||||
|
**Как редактировать:**
|
||||||
|
1. Откройте `config.py` в текстовом редакторе
|
||||||
|
2. Изменяйте значения в квадратных скобках или кавычках
|
||||||
|
3. Для добавления/удаления дней используйте списки `[25, 26]`
|
||||||
|
4. Сохраните файл
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 2: Обновление `models.py`
|
||||||
|
|
||||||
|
**Что делать:**
|
||||||
|
- Удалить все константы из `models.py`
|
||||||
|
- Добавить импорт: `from config import *`
|
||||||
|
- Удалить определения `SHIFTS`, `OPERATORS`, `OPERATOR_PREFERENCES`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 3: Обновление `main.py`
|
||||||
|
|
||||||
|
**Что делать:**
|
||||||
|
- Удалить встроенную генерацию дат
|
||||||
|
- Использовать импорт из `config.py`
|
||||||
|
- Добавить вывод в CSV
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 4: Создание документации
|
||||||
|
|
||||||
|
**Файл:** `USER_GUIDE.md`
|
||||||
|
|
||||||
|
**Что включить:**
|
||||||
|
- Как изменить даты
|
||||||
|
- Как добавить/удалить операторов
|
||||||
|
- Как изменить смены
|
||||||
|
- Как добавить новые предпочтения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 5: Тестирование
|
||||||
|
|
||||||
|
**Проверить:**
|
||||||
|
1. Запуск `python main.py`
|
||||||
|
2. Корректное считывание дат из `config.py`
|
||||||
|
3. Генерация расписания
|
||||||
|
4. Вывод в CSV
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
- Конфигурация в одном файле `config.py`
|
||||||
|
- Простое редактирование без знания Python
|
||||||
|
- Четкая документация в `USER_GUIDE.md`
|
||||||
46
config.py
Normal file
46
config.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
config.py - Настройки графика смен
|
||||||
|
Редактируйте этот файл БЕЗ знания Python!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 📅 НАСТРОЙКИ ДАТ
|
||||||
|
YEAR = 2026 # Год
|
||||||
|
MONTH = 4 # Месяц (апрель)
|
||||||
|
|
||||||
|
# 🕒 СМЕНЫ (время начала, время окончания)
|
||||||
|
# format: (начало_часа, окончание_часа)
|
||||||
|
SHIFTS = [
|
||||||
|
(6, 14.5), # Смена 0: 06:00 - 14:30
|
||||||
|
(8, 16.5), # Смена 1: 08:00 - 16:30
|
||||||
|
(11.5, 20), # Смена 2: 11:30 - 20:00
|
||||||
|
(12.5, 21), # Смена 3: 12:30 - 21:00
|
||||||
|
]
|
||||||
|
|
||||||
|
# 👷 ИМЕНА ОПЕРАТОРОВ
|
||||||
|
# Меняйте здесь имена операторов!
|
||||||
|
OPERATORS = [
|
||||||
|
"Иванов",
|
||||||
|
"Алексеев",
|
||||||
|
"Сидоров",
|
||||||
|
"Козобородов",
|
||||||
|
"Трудяшкина",
|
||||||
|
"Петрова",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 💖 ПРЕДПОЧТЕНИЯ ОПЕРАТОРОВ
|
||||||
|
# format: {имя_оператора: список_дней_или_смена}
|
||||||
|
OPERATOR_PREFERENCES = {
|
||||||
|
"Иванов": [10, 13], # 10 и 13 апреля
|
||||||
|
"Алексеев": "понедельник", # только понедельники
|
||||||
|
"Сидоров": [25, 26], # 25 и 26 апреля
|
||||||
|
"Козобородов": [2], # только смена 2 (11:30–20:00)
|
||||||
|
"Трудяшкина": [4], # 4 апреля
|
||||||
|
"Петрова": [8, 29], # 8 и 29 апреля
|
||||||
|
}
|
||||||
|
|
||||||
|
# 📝 ПОЯСНЕНИЯ ДЛЯ РЕДАКТИРОВАНИЯ:
|
||||||
|
# - Дни месяца указываются по числам: [10, 13] = 10 и 13 апреля
|
||||||
|
# - "понедельник" означает работать только в понедельник
|
||||||
|
# - Числа 0-3 в списках указывают на номер смены (0 = первая смена)
|
||||||
|
# - Для добавления/удаления дней меняйте списки в квадратных скобках
|
||||||
|
# - Для изменения имени оператора меняйте строку в OPERATORS
|
||||||
153
main.py
153
main.py
@@ -1,101 +1,82 @@
|
|||||||
from ortools.sat.python import cp_model
|
"""
|
||||||
|
main.py - Основ
|
||||||
|
|
||||||
|
ный скрипт для создания графика смен
|
||||||
|
Использует модуль models.py для создания и решения модели
|
||||||
|
"""
|
||||||
|
from models import create_model, solve_model, get_solution, SHIFTS, OPERATORS
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
# Определяем даты апреля 2026 года
|
def generate_csv_report(schedule: dict, filename: str = 'shift_schedule.csv'):
|
||||||
days = [
|
"""
|
||||||
"2026-04-{:02d}".format(i) for i in range(1, 31)
|
Генерирует CSV-отчет на основе расписания.
|
||||||
]
|
|
||||||
|
|
||||||
# Смены
|
Аргументы:
|
||||||
shifts = [
|
schedule: словарь расписания из get_solution()
|
||||||
(6, 14.5), # с 06:00 до 14:30
|
filename: имя файла для вывода CSV
|
||||||
(8, 16.5), # с 08:00 до 16:30
|
|
||||||
(11.5, 20), # с 11:30 до 20:00
|
|
||||||
(12.5, 21) # с 12:30 до 21:00
|
|
||||||
]
|
|
||||||
|
|
||||||
# Операторы и их пожелания
|
Возвращает:
|
||||||
operators = [
|
True если файл создан успешно
|
||||||
"Иванов",
|
"""
|
||||||
"Алексеев",
|
with open(filename, 'w', newline='', encoding='utf-8') as file:
|
||||||
"Сидоров",
|
|
||||||
"Козобородов",
|
|
||||||
"Трудяшкина",
|
|
||||||
"Петрова"
|
|
||||||
]
|
|
||||||
|
|
||||||
operator_preferences = {
|
|
||||||
"Иванов": ["2026-04-10", "2026-04-13"],
|
|
||||||
"Алексеев": [(i, 0) for i in days if (int(i[-2:]) - 1) % 7 == 0], # Понедельники
|
|
||||||
"Сидоров": ["2026-04-25", "2026-04-26"],
|
|
||||||
"Козобородов": [(i, 2) for i in days], # Предпочитает смену 11:30–20:00
|
|
||||||
"Трудяшкина": ["2026-04-04"],
|
|
||||||
"Петрова": ["2026-04-08", "2026-04-29"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Создаем модель
|
|
||||||
model = cp_model.CpModel()
|
|
||||||
|
|
||||||
# Переменные: определяем, работает ли оператор на смене в день
|
|
||||||
works = {}
|
|
||||||
for day in days:
|
|
||||||
for shift_idx in range(len(shifts)):
|
|
||||||
for op in operators:
|
|
||||||
works[(day, shift_idx, op)] = model.NewBoolVar(f"{op}_{day}_shift_{shift_idx}")
|
|
||||||
|
|
||||||
# # Пожелания операторов
|
|
||||||
for op, prefs in operator_preferences.items():
|
|
||||||
if isinstance(prefs, list):
|
|
||||||
for pref in prefs:
|
|
||||||
if isinstance(pref, tuple):
|
|
||||||
day, shift = pref
|
|
||||||
model.AddHint(works[day, shift, op], 1)
|
|
||||||
else:
|
|
||||||
day = pref
|
|
||||||
model.Add(sum(works[day, shift_idx, op] for shift_idx in range(len(shifts))) == 0)
|
|
||||||
|
|
||||||
# Условие 1: На каждую смену должен быть назначен оператор
|
|
||||||
for day in days:
|
|
||||||
for shift_idx in range(len(shifts)):
|
|
||||||
model.Add(sum(works[day, shift_idx, op] for op in operators) == 1)
|
|
||||||
|
|
||||||
# Условие 2: У каждого оператора должно быть 8 выходных
|
|
||||||
for op in operators:
|
|
||||||
model.Add(sum(works[day, shift_idx, op] for day in days for shift_idx in range(len(shifts))) == 20 )
|
|
||||||
|
|
||||||
# Условие 3: Оператор не должен работать больше 5 дней подряд
|
|
||||||
for op in operators:
|
|
||||||
for day_idx in range(len(days) - 4):
|
|
||||||
model.Add(sum(works[days[day_idx + i], shift_idx, op] for i in range(5) for shift_idx in range(len(shifts))) <= 4)
|
|
||||||
|
|
||||||
# Условие 4: В день оператор может работать только одну смену
|
|
||||||
for day in days:
|
|
||||||
for op in operators:
|
|
||||||
model.Add(sum(works[day, shift_idx, op] for shift_idx in range(len(shifts))) <= 1)
|
|
||||||
|
|
||||||
# Решаем модель
|
|
||||||
solver = cp_model.CpSolver()
|
|
||||||
status = solver.Solve(model)
|
|
||||||
|
|
||||||
if status != cp_model.OPTIMAL and status != cp_model.FEASIBLE:
|
|
||||||
print("Невозможно составить график")
|
|
||||||
else:
|
|
||||||
# Создаем CSV файл с результатами
|
|
||||||
with open('shift_schedule.csv', 'w', newline='', encoding='utf-8') as file:
|
|
||||||
writer = csv.writer(file)
|
writer = csv.writer(file)
|
||||||
writer.writerow(['Day'] + [f'{i+1} shift' for i in range(len(shifts))])
|
# Заголовок
|
||||||
|
writer.writerow(['День'] + [f'Смена {i+1}' for i in range(len(SHIFTS))])
|
||||||
|
|
||||||
for day in days:
|
for day, day_shifts in schedule.items():
|
||||||
row = [day]
|
row = [day]
|
||||||
for shift_idx in range(len(shifts)):
|
for shift_idx in range(len(SHIFTS)):
|
||||||
op = None
|
op = None
|
||||||
for op_name in operators:
|
for op_name in OPERATORS:
|
||||||
if solver.Value(works[day, shift_idx, op_name]) == 1:
|
if schedule[day][shift_idx].get(op_name, False):
|
||||||
op = op_name
|
op = op_name
|
||||||
break
|
break
|
||||||
row.append(op)
|
row.append(op)
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
|
|
||||||
print("График успешно составлен и сохранен в 'shift_schedule.csv'")
|
print(f"График успешно составлен и сохранен в '{filename}'")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Главная функция: создает и решает модель, затем генерирует CSV-отчет.
|
||||||
|
"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Создание графика смен")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Шаг 1: Создаем модель
|
||||||
|
print("\n[1] Создание модели...")
|
||||||
|
model, variables, operators_set = create_model()
|
||||||
|
print(f" Модель создана с {len(variables)} переменными")
|
||||||
|
print(f" Операторы: {', '.join(operators_set)}")
|
||||||
|
|
||||||
|
# Шаг 2: Решаем модель
|
||||||
|
print("\n[2] Решение модели...")
|
||||||
|
success = solve_model()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(" ✅ Модель решена успешно!")
|
||||||
|
|
||||||
|
# Шаг 3: Получаем решение
|
||||||
|
print("\n[3] Получение решения...")
|
||||||
|
schedule = get_solution()
|
||||||
|
print(f" Решение получено для {len(schedule)} дней")
|
||||||
|
|
||||||
|
# Шаг 4: Генерируем CSV-отчет
|
||||||
|
print("\n[4] Генерация CSV-отчета...")
|
||||||
|
generate_csv_report(schedule, 'shift_schedule.csv')
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(" ❌ Не удалось составить график!")
|
||||||
|
print(" Проверьте ограничения и предпочтения операторов.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Готово!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
141
models.py
Normal file
141
models.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
Модуль для создания и решения модели CP-SAT
|
||||||
|
"""
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
|
||||||
|
# Дни работы
|
||||||
|
def get_days(year: int = 2026, month: int = 4) -> List[str]:
|
||||||
|
"""Генерирует список дат месяца"""
|
||||||
|
return [f"{year}-{month:02d}-{i:02d}" for i in range(1, 31)]
|
||||||
|
|
||||||
|
# Смены
|
||||||
|
SHIFTS = [
|
||||||
|
(6, 14.5), # с 06:00 до 14:30
|
||||||
|
(8, 16.5), # с 08:00 до 16:30
|
||||||
|
(11.5, 20), # с 11:30 до 20:00
|
||||||
|
(12.5, 21) # с 12:30 до 21:00
|
||||||
|
]
|
||||||
|
|
||||||
|
# Операторы
|
||||||
|
OPERATORS = [
|
||||||
|
"Иванов",
|
||||||
|
"Алексеев",
|
||||||
|
"Сидоров",
|
||||||
|
"Козобородов",
|
||||||
|
"Трудяшкина",
|
||||||
|
"Петрова"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Предпочтения операторов
|
||||||
|
OPERATOR_PREFERENCES = {
|
||||||
|
"Иванов": ["2026-04-10", "2026-04-13"],
|
||||||
|
"Алексеев": [(i, 0) for i in get_days() if (int(i[-2:]) - 1) % 7 == 0], # Понедельники
|
||||||
|
"Сидоров": ["2026-04-25", "2026-04-26"],
|
||||||
|
"Козобородов": [(i, 2) for i in get_days()], # Предпочитает смену 11:30–20:00
|
||||||
|
"Трудяшкина": ["2026-04-04"],
|
||||||
|
"Петрова": ["2026-04-08", "2026-04-29"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_model() -> Tuple[cp_model.CpModel, Dict, Dict]:
|
||||||
|
"""
|
||||||
|
Создаёт и настраивает CP-модель.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
model: CP-модель
|
||||||
|
variables: словарь переменных работы
|
||||||
|
operators_set: множество операторов для удобства
|
||||||
|
"""
|
||||||
|
model = cp_model.CpModel()
|
||||||
|
variables = {}
|
||||||
|
days = get_days()
|
||||||
|
|
||||||
|
# Определяем переменные: works[(day, shift_idx, op)]
|
||||||
|
for day in days:
|
||||||
|
for shift_idx in range(len(SHIFTS)):
|
||||||
|
for op in OPERATORS:
|
||||||
|
var_name = f"{op}_{day}_shift_{shift_idx}"
|
||||||
|
variables[(day, shift_idx, op)] = model.NewBoolVar(var_name)
|
||||||
|
|
||||||
|
# Добавляем ограничения
|
||||||
|
|
||||||
|
# 1. Учёт предпочтений операторов
|
||||||
|
for op, prefs in OPERATOR_PREFERENCES.items():
|
||||||
|
if isinstance(prefs, list):
|
||||||
|
for pref in prefs:
|
||||||
|
if isinstance(pref, tuple):
|
||||||
|
day, shift = pref
|
||||||
|
day_key = f"{day}"
|
||||||
|
shift_key = shift
|
||||||
|
# Создаем ключ для переменных, используя стандартный формат дней
|
||||||
|
# Нужно найти соответствующий день в списке days
|
||||||
|
for d in days:
|
||||||
|
if d.endswith(day.split('-')[2]) and d.startswith(f"{day.split('-')[0]}-{day.split('-')[1]}"):
|
||||||
|
if f"{d}-{shift_key}" in pref:
|
||||||
|
continue
|
||||||
|
day, shift_idx_pref = pref
|
||||||
|
model.AddHint(variables[(day, shift_idx_pref, op)], 1)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
day = pref
|
||||||
|
model.Add(sum(variables[(day, shift_idx, op)] for shift_idx in range(len(SHIFTS))) == 0)
|
||||||
|
|
||||||
|
# Условие 1: На каждую смену должен быть назначен оператор
|
||||||
|
for day in days:
|
||||||
|
for shift_idx in range(len(SHIFTS)):
|
||||||
|
model.Add(sum(variables[(day, shift_idx, op)] for op in OPERATORS) == 1)
|
||||||
|
|
||||||
|
# Условие 2: У каждого оператора должно быть 8 выходных (20 рабочих дней)
|
||||||
|
for op in OPERATORS:
|
||||||
|
model.Add(sum(variables[(day, shift_idx, op)] for day in days for shift_idx in range(len(SHIFTS))) == 20)
|
||||||
|
|
||||||
|
# Условие 3: Оператор не должен работать больше 5 дней подряд
|
||||||
|
for op in OPERATORS:
|
||||||
|
for day_idx in range(len(days) - 4):
|
||||||
|
model.Add(sum(variables[(days[day_idx + i], shift_idx, op)]
|
||||||
|
for i in range(5)
|
||||||
|
for shift_idx in range(len(SHIFTS))) <= 4)
|
||||||
|
|
||||||
|
# Условие 4: В день оператор может работать только одну смену
|
||||||
|
for day in days:
|
||||||
|
for op in OPERATORS:
|
||||||
|
model.Add(sum(variables[(day, shift_idx, op)] for shift_idx in range(len(SHIFTS))) <= 1)
|
||||||
|
|
||||||
|
return model, variables, set(OPERATORS)
|
||||||
|
|
||||||
|
def solve_model():
|
||||||
|
"""
|
||||||
|
Решает модель.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
True - если решение найдено
|
||||||
|
False - если не удалось составить график
|
||||||
|
"""
|
||||||
|
model, variables, operators_set = create_model()
|
||||||
|
|
||||||
|
solver = cp_model.CpSolver()
|
||||||
|
status = solver.Solve(model)
|
||||||
|
|
||||||
|
return status in (cp_model.OPTIMAL, cp_model.FEASIBLE)
|
||||||
|
|
||||||
|
def get_solution():
|
||||||
|
"""
|
||||||
|
Получает решение модели.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
Словарь с расписанием: {день: {смена: {оператор: True/False}}}
|
||||||
|
"""
|
||||||
|
model, variables, operators_set = create_model()
|
||||||
|
solver = cp_model.CpSolver()
|
||||||
|
solver.Solve(model)
|
||||||
|
|
||||||
|
schedule = {}
|
||||||
|
for day in get_days():
|
||||||
|
schedule[day] = {
|
||||||
|
shift_idx: {op: solver.Value(variables[(day, shift_idx, op)])
|
||||||
|
for op in OPERATORS}
|
||||||
|
for shift_idx in range(len(SHIFTS))
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedule
|
||||||
Reference in New Issue
Block a user