diff --git a/.continue/rules/python.md b/.continue/rules/python.md new file mode 100644 index 0000000..7e8d5ef --- /dev/null +++ b/.continue/rules/python.md @@ -0,0 +1,5 @@ +--- +description: A description of your rule +--- +Ты senior python-разработчик +У тебя 5-лет опыта работы с библиотекой ortools diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..8218e43 --- /dev/null +++ b/PLAN.md @@ -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` diff --git a/config.py b/config.py new file mode 100644 index 0000000..a4a9c31 --- /dev/null +++ b/config.py @@ -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 \ No newline at end of file diff --git a/main.py b/main.py index 0d19fcb..e5a79d8 100644 --- a/main.py +++ b/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 -# Определяем даты апреля 2026 года -days = [ - "2026-04-{:02d}".format(i) 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 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: +def generate_csv_report(schedule: dict, filename: str = 'shift_schedule.csv'): + """ + Генерирует CSV-отчет на основе расписания. + + Аргументы: + schedule: словарь расписания из get_solution() + filename: имя файла для вывода CSV + + Возвращает: + True если файл создан успешно + """ + with open(filename, 'w', newline='', encoding='utf-8') as 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] - for shift_idx in range(len(shifts)): + for shift_idx in range(len(SHIFTS)): op = None - for op_name in operators: - if solver.Value(works[day, shift_idx, op_name]) == 1: + for op_name in OPERATORS: + if schedule[day][shift_idx].get(op_name, False): op = op_name break row.append(op) writer.writerow(row) + + print(f"График успешно составлен и сохранен в '{filename}'") + return True - print("График успешно составлен и сохранен в 'shift_schedule.csv'") - \ No newline at end of file +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() \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..3cca7b3 --- /dev/null +++ b/models.py @@ -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 \ No newline at end of file