отпускные вычитаются дважды из аванса и из оклада, вычитай только из одной выплаты, которая будет следовать за отпуском:  import tkinter as tk
from tkinter import ttk, messagebox
from tkcalendar import Calendar
from datetime import datetime, timedelta
import calendar
import json
import os
from workalendar.europe import Russia

class SalaryApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Калькулятор зарплаты с учётом отпусков и реальных рабочих дней")
        self.vacations = []  # Список отпусков
        self.calendars = {}  # Словарь для хранения календарей по месяцам
        self.data_file = "vacations.json"
        self.salary_file = "salary.json"  # Файл для сохранения оклада
        self.selected_dates = []  # Для выбора начала и конца отпуска
        self.cal_obj = Russia()
        self.gross_salary = self.load_salary()  # Загрузка сохранённого оклада

        # Основной фрейм для отображения календарей
        frame_calendars = ttk.Frame(root, padding="10")
        frame_calendars.grid(row=0, column=0, sticky='nsew')

        # Создание сетки для календарей (3 строки по 4 календари)
        current_year = datetime.now().year
        months = range(1, 13)
        rows = 3
        cols = 4
        for idx, month in enumerate(months):
            row = idx // cols
            col = idx % cols
            frame = ttk.Frame(frame_calendars, borderwidth=1, relief="solid", padding=5)
            frame.grid(row=row, column=col, padx=5, pady=5, sticky='nsew')
            cal = Calendar(frame, selectmode='day', year=current_year, month=month, day=1, date_pattern='y-mm-dd')
            cal.pack(fill='both', expand=True)
            cal.bind("<<CalendarSelected>>", self.on_date_select)
            self.mark_non_working_days(cal, current_year, month)  # Отметка нерабочих дней
            self.calendars[month] = cal

        # Оклад
        frame_salary = ttk.Frame(root, padding="10")
        frame_salary.grid(row=1, column=0, sticky='w')

        ttk.Label(frame_salary, text="Gross оклад (руб.):").pack(side='left')
        self.gross_var = tk.DoubleVar(value=self.gross_salary)  # Инициализация с сохранённым окладом
        ttk.Entry(frame_salary, textvariable=self.gross_var, width=15).pack(side='left', padx=5)

        # Выбор месяца для расчёта
        frame_month = ttk.Frame(root, padding="10")
        frame_month.grid(row=2, column=0, sticky='w')

        ttk.Label(frame_month, text="Выберите год для расчёта:").pack(side='left')
        self.year_var = tk.IntVar(value=datetime.now().year)  # Устанавливаем текущий год по умолчанию
        self.year_combo = ttk.Combobox(frame_month, values=list(range(2020,datetime.now().year+2)), textvariable=self.year_var, state='readonly', width=15)  # Добавляем диапазон доступных годов
        self.year_combo.pack(side='left', padx=5)
        ttk.Button(frame_month, text="Рассчитать выплаты", command=self.calculate_yearly_payments).pack(side='left', padx=5)

        # Кнопки управления отпусками
        frame_buttons = ttk.Frame(root, padding="10")
        frame_buttons.grid(row=3, column=0, sticky='w')

        ttk.Button(frame_buttons, text="Сохранить отпуск", command=self.save_selected_vacation).pack(side='left', padx=5)
        ttk.Button(frame_buttons, text="Удалить выбранный отпуск", command=self.delete_vacation).pack(side='left', padx=5)

        # Список отпусков
        frame_vacations = ttk.Frame(root, padding="10")
        frame_vacations.grid(row=4, column=0, sticky='we')

        ttk.Label(frame_vacations, text="Список отпусков:").pack(anchor='w')
        self.vacation_list = tk.Listbox(frame_vacations, height=10)
        self.vacation_list.pack(fill='both', expand=True)

        # Список выплат
        frame_payments = ttk.Frame(root, padding="10")
        frame_payments.grid(row=5, column=0, sticky='we')

        ttk.Label(frame_payments, text="Список выплат:").pack(anchor='w')
        self.payment_list = tk.Listbox(frame_payments, height=10)
        self.payment_list.pack(fill='both', expand=True)

        # Загрузка сохраненных отпусков
        self.load_vacations()

        # Добавляем визуальное выделение текущего дня
        self.highlight_today()

    def highlight_today(self):
        """Выделение текущего дня желтым цветом."""
        current_day = datetime.now().date()
        current_month = current_day.month
        if current_month in self.calendars:
            cal = self.calendars[current_month]
            try:
                cal.calevent_create(current_day, "Сегодня", "today")
                cal.tag_config('today', background='yellow', foreground='black')
            except Exception as e:
                print(f"Ошибка выделения сегодняшнего дня: {e}")

    def mark_non_working_days(self, cal, year, month):
        """Отметить нерабочие дни красным цветом."""
        first_day = datetime(year, month, 1).date()
        last_day = datetime(year, month, calendar.monthrange(year, month)[1]).date()
        current = first_day
        while current <= last_day:
            if not self.cal_obj.is_working_day(current):
                cal.calevent_create(current, "Нерабочий день", "non_working")
            current += timedelta(days=1)
        cal.tag_config('non_working', background='red', foreground='white')

    def delete_vacation(self):
        """Удаление выбранного отпуска."""
        selected = self.vacation_list.curselection()
        if not selected:
            messagebox.showerror("Ошибка", "Пожалуйста, выберите отпуск для удаления.")
            return
        index = selected[0]
        del self.vacations[index]
        self.vacation_list.delete(index)
        self.clear_vacation_from_calendars()
        self.display_vacations()
        self.save_vacations_to_file()

    def clear_vacation_from_calendars(self):
        """Очистка интервала отпуска из календаря."""
        for cal in self.calendars.values():
            cal.calevent_remove('vacation')
            cal.calevent_remove('start_end')
            cal.calevent_remove('start')
            cal.calevent_remove('end')

    def save_selected_vacation(self):
        """Сохранение выбранного отпуска."""
        if len(self.selected_dates) != 2:
            messagebox.showerror("Ошибка", "Пожалуйста, выберите две даты для отпуска.")
            return
        date1, date2 = self.selected_dates
        start_date = min(date1, date2)
        end_date = max(date1, date2)
        # Проверка пересечения отпусков
        for vac in self.vacations:
            if (start_date <= vac[1] and end_date >= vac[0]):
                messagebox.showerror("Ошибка", f"Отпуск пересекается с уже существующим: {vac[0]} - {vac[1]}")
                return
        self.vacations.append((start_date, end_date))
        self.vacation_list.insert(tk.END, f"{start_date} - {end_date}")
        self.display_vacations()
        self.save_vacations_to_file()
        self.selected_dates = []

    def display_vacations(self):
            """Отображение всех отпусков на календарях."""
            # Очистить предыдущие отметки отпусков
            for cal in self.calendars.values():
                cal.calevent_remove('vacation')
                cal.calevent_remove('start')
                cal.calevent_remove('end')
                cal.tag_config('vacation', background='green', foreground='black')
                cal.tag_config('start', background='blue', foreground='white')
                cal.tag_config('end', background='pink', foreground='black')

            # Отобразить новые отпуска
            for vac in self.vacations:
                start, end = vac
                current = start
                while current <= end:
                    month = current.month
                    if month in self.calendars:
                        if current == start:
                            self.calendars[month].calevent_create(current, "Начало отпуска", "start")
                        elif current == end:
                            self.calendars[month].calevent_create(current, "Конец отпуска", "end")
                        elif self.cal_obj.is_working_day(current):
                            if not self.cal_obj.is_holiday(current):
                                self.calendars[month].calevent_create(current, "Отпуск", "vacation")
                            else:
                                self.calendars[month].calevent_create(current, "Праздничный день", "non_working")
                    current += timedelta(days=1)

    def calculate_yearly_payments(self):
        """Расчёт выплат за год."""
        try:
            gross = self.gross_var.get()
            if gross <= 0:
                raise ValueError
        except:
            messagebox.showerror("Ошибка", "Пожалуйста, введите корректный оклад.")
            return

        self.gross_salary = gross  # Сохраняем оклад
        self.save_salary()  # Сохраняем оклад в файл

        selected_year = self.year_var.get()
        self.payment_list.delete(0, tk.END)  # Очищаем список выплат

        payments = self.calculate_payments_for_year(selected_year, gross)

        for payment in payments:
            self.payment_list.insert(tk.END, payment)

    def calculate_payments_for_year(self, year, gross_salary):
        """Расчёт выплат на год с учётом отпусков."""
        payments = []
        for month in range(1, 13):
            first_day = datetime(year, month, 1).date()
            last_day = datetime(year, month, calendar.monthrange(year, month)[1]).date()

            working_days_delta = self.cal_obj.get_working_days_delta(first_day, last_day)
            working_days_in_month = working_days_delta + 1

            vacation_days = 0
            vacation_deduction = 0
            for vac in self.vacations:
                start, end = vac
                if start <= last_day and end >= first_day:
                    start_vac = max(start, first_day)
                    end_vac = min(end, last_day)
                    days_in_vacation_delta = self.cal_obj.get_working_days_delta(start_vac, end_vac)
                    days_in_vacation = days_in_vacation_delta + 1
                    vacation_days += days_in_vacation
                    # Выплата отпускных за неделю до начала отпуска
                    vacation_pay_date = start - timedelta(days=7)
                    daily_rate = gross_salary / working_days_in_month
                    vacation_deduction += daily_rate * days_in_vacation
                    payments.append(f"Выплата отпускных ({start} - {end}): {daily_rate * days_in_vacation:.2f} руб. (Дата выплаты: {vacation_pay_date.strftime('%Y-%m-%d')})")

            effective_working_days = working_days_in_month - vacation_days
            if effective_working_days < 0:
                effective_working_days = 0
            daily_rate = gross_salary / working_days_in_month
            net_salary = daily_rate * effective_working_days
            advance = net_salary / 2
            main_payment = net_salary / 2

            # Вычет отпускных из аванса и основной части зарплаты
            if vacation_deduction > 0:
                advance = max(0, advance - vacation_deduction / 2)
                main_payment = max(0, main_payment - vacation_deduction / 2)

            # Выплата аванса 15-го числа следующего месяца
            advance_pay_date = datetime(year, month, 15) + timedelta(days=30)
            if advance_pay_date.month != month + 1:
                advance_pay_date = datetime(year + 1, 1, 15)
            payments.append(f"Аванс за {calendar.month_name[month]} {year}: {advance:.2f} руб. (Дата выплаты: {advance_pay_date.strftime('%Y-%m-%d')})")

            # Выплата оклада 30-го числа следующего месяца (или последнего дня месяца, если его нет)
            last_day_of_month = calendar.monthrange(year, month)[1]
            main_pay_date = datetime(year, month, last_day_of_month) + timedelta(days=30)
            if main_pay_date.month != month + 1:
                main_pay_date = datetime(year + 1, 1, last_day_of_month)
            payments.append(f"Оклад за {calendar.month_name[month]} {year}: {main_payment:.2f} руб. (Дата выплаты: {main_pay_date.strftime('%Y-%m-%d')})")

        return payments


    def load_salary(self):
        """Загрузка сохранённого оклада из файла."""
        if os.path.exists(self.salary_file):
            try:
                with open(self.salary_file, "r", encoding='utf-8') as f:
                    data = json.load(f)
                    return data.get('gross_salary', 0.0)
            except (json.JSONDecodeError, FileNotFoundError):
                return 0.0  # Если файла нет или ошибка чтения, возвращаем 0
        return 0.0

    def save_salary(self):
        """Сохранение оклада в файл."""
        data = {'gross_salary': self.gross_salary}
        with open(self.salary_file, "w", encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

    def load_vacations(self):
        """Загрузка отпусков из файла."""
        if os.path.exists(self.data_file):
            try:
                with open(self.data_file, "r", encoding='utf-8') as f:
                    data = json.load(f)
                    for vac in data:
                        start_date = datetime.strptime(vac['start'], "%Y-%m-%d").date()
                        end_date = datetime.strptime(vac['end'], "%Y-%m-%d").date()
                        self.vacations.append((start_date, end_date))
                        self.vacation_list.insert(tk.END, f"{start_date} - {end_date}")
                self.display_vacations()
            except (json.JSONDecodeError, FileNotFoundError):
                pass  # Если файла нет или ошибка чтения, игнорируем ошибку

    def save_vacations_to_file(self):
        """Сохранение отпусков в файл."""
        data = [{'start': vac[0].strftime("%Y-%m-%d"), 'end': vac[1].strftime("%Y-%m-%d")} for vac in self.vacations]
        with open(self.data_file, "w", encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

    def on_date_select(self, event):
        """Обработка выбора даты для отпуска."""
        cal = event.widget
        try:
            selected_date = cal.selection_get()
        except:
            return

        if not self.cal_obj.is_working_day(selected_date):
            messagebox.showerror("Ошибка", "Нельзя выбирать нерабочие дни для отпуска.")
            return

        for idx, vac in enumerate(self.vacations):
            if selected_date in vac:
                 # Удаляем отпуск
                 del self.vacations[idx]
                 self.vacation_list.delete(idx)
                 self.clear_vacation_from_calendars()
                 self.display_vacations()
                 self.save_vacations_to_file()
                 # Очищаем выделение
                 for c in self.calendars.values():
                    c.calevent_remove('temp_start')
                 self.selected_dates = []
                 return

        self.selected_dates.append(selected_date)
        if len(self.selected_dates) == 1:
            cal.calevent_create(selected_date, "Выбор начала/окончания отпуска", "temp_start")
            cal.tag_config('temp_start', background='purple', foreground='white')
        elif len(self.selected_dates) == 2:
            date1, date2 = self.selected_dates
            start_date = min(date1, date2)
            end_date = max(date1, date2)
            for vac in self.vacations:
                if (start_date <= vac[1] and end_date >= vac[0]):
                    messagebox.showerror("Ошибка", f"Отпуск пересекается с уже существующим: {vac[0]} - {vac[1]}")
                    # Удаляем временные выделения
                    for c in self.calendars.values():
                       c.calevent_remove('temp_start')
                    self.selected_dates = []
                    return
            self.vacations.append((start_date, end_date))
            self.vacation_list.insert(tk.END, f"{start_date} - {end_date}")
            self.display_vacations()
            self.save_vacations_to_file()
            for c in self.calendars.values():
                c.calevent_remove('temp_start')
            self.selected_dates = []


if __name__ == "__main__":
    root = tk.Tk()
    root.geometry("1600x900")
    app = SalaryApp(root)
    root.mainloop()