Загрузка данных
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
# =====================================================================
# ЯДРО РАСЧЕТА ПРЕМИИ (СОТ 2ГИС г. СОЧИ от 02.02.2026)
# =====================================================================
class SochiBonusCalculator:
K1_PAYMENTS = 0.0145 # 1.45% - Сочи (Оплаты)
K2_VOLUME = 0.0150 # 1.50% - Сочи (Общий объем)
K3_GROWTH = 0.20 # 20.0% - Сочи (Прирост объема)
NEW_SALES_RATES = {
"КП от 2 мес. (30%)": 0.30,
"КП от 3 мес. (40%)": 0.40,
"КП от 4 мес. (50%)": 0.50,
"КП от 5 мес. (55%)": 0.55,
"КП от 6 мес. (65%)": 0.65,
"КП от 12 мес. (80%)": 0.80,
}
CPC_LIMIT = 1_000_000.0
CPC_RATE_LOW = 0.0145 # 1.45% до 1 млн
CPC_RATE_HIGH = 0.003625 # 0.3625% свыше 1 млн
SPEC_PROJECT_RATE = 0.10 # 10% спецпроекты
INTERN_THRESHOLD = 50_000.0
INTERN_MIN_PAYMENT = 5_000.0
INTERN_MAX_BONUS = 10_000.0
@staticmethod
def calc_p_coeff(fact: float, plan: float) -> float:
"""Расчет коэффициента P1 или P2 с учетом порогов 90% и 150%."""
if plan <= 0 or fact <= 0:
return 0.0
ratio = fact / plan
if ratio < 0.90:
return 0.0
if ratio >= 1.50:
return 1.50
return ratio
@classmethod
def calc_payments_bonus(cls, plan_money: float, fact_money: float) -> dict:
p1 = cls.calc_p_coeff(fact_money, plan_money)
pct = (fact_money / plan_money * 100.0) if plan_money > 0 else 0.0
bonus = p1 * cls.K1_PAYMENTS * fact_money
return {"pct": pct, "p1": p1, "bonus": bonus}
@classmethod
def calc_volume_bonus(cls, plan_vol: float, fact_shipped_vol: float, total_vol: float) -> dict:
p2 = cls.calc_p_coeff(fact_shipped_vol, plan_vol)
pct = (fact_shipped_vol / plan_vol * 100.0) if plan_vol > 0 else 0.0
bonus = p2 * cls.K2_VOLUME * total_vol
return {"pct": pct, "p2": p2, "bonus": bonus}
@classmethod
def calc_growth_bonus(cls, total_vol_current: float, total_vol_prev: float) -> dict:
diff = total_vol_current - total_vol_prev
bonus = diff * cls.K3_GROWTH if diff > 0 else 0.0
return {"diff": diff, "bonus": bonus}
@classmethod
def calc_cpc_bonus(cls, cpc_payments: float) -> float:
if cpc_payments <= 0:
return 0.0
if cpc_payments <= cls.CPC_LIMIT:
return cpc_payments * cls.CPC_RATE_LOW
return (cls.CPC_LIMIT * cls.CPC_RATE_LOW) + ((cpc_payments - cls.CPC_LIMIT) * cls.CPC_RATE_HIGH)
@classmethod
def calc_intern_threshold_bonus(cls, fact_money: float) -> dict:
if fact_money < cls.INTERN_MIN_PAYMENT:
return {"r_po": 0.0, "bonus": 0.0}
r_po = fact_money / cls.INTERN_THRESHOLD
if r_po >= 1.0:
r_po = 1.0
return {"r_po": r_po, "bonus": r_po * cls.INTERN_MAX_BONUS}
def run_self_tests():
"""Автоматическая проверка математического ядра перед запуском."""
# Тест 1: 110% по деньгам (550к из 500к)
res_pay = SochiBonusCalculator.calc_payments_bonus(500_000, 550_000)
assert abs(res_pay["bonus"] - 8772.50) < 0.01, "Ошибка в расчете бонуса от оплат"
# Тест 2: < 90% (440к из 500к -> 0 руб)
res_zero = SochiBonusCalculator.calc_payments_bonus(500_000, 440_000)
assert res_zero["bonus"] == 0.0, "Ошибка порога 90%"
# Тест 3: > 150% по объему (510к из 300к -> P2=1.5)
res_vol = SochiBonusCalculator.calc_volume_bonus(300_000, 510_000, 510_000)
assert abs(res_vol["bonus"] - 11475.00) < 0.01, "Ошибка ограничения 150%"
# Тест 4: Прирост 50к -> 10 000 руб
res_gr = SochiBonusCalculator.calc_growth_bonus(400_000, 350_000)
assert abs(res_gr["bonus"] - 10000.00) < 0.01, "Ошибка расчета прироста"
# =====================================================================
# ГРАФИЧЕСКИЙ ИНТЕРФЕЙС WINDOWS (TKINTER)
# =====================================================================
class BonusApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Калькулятор премии МПП 2ГИС (г. Сочи — СОТ 2026)")
self.geometry("1180x780")
self.minsize(1050, 700)
self.configure(bg="#f4f6f9")
self._updating_sim_money = False
self._updating_sim_vol = False
self._setup_styles()
self._build_ui()
self.calculate_main()
self.update_simulator_ranges()
def _setup_styles(self):
style = ttk.Style(self)
if "vista" in style.theme_names():
style.theme_use("vista")
elif "clam" in style.theme_names():
style.theme_use("clam")
style.configure("TFrame", background="#f4f6f9")
style.configure("Card.TLabelframe", background="#ffffff", padding=12)
style.configure("Card.TLabelframe.Label", font=("Segoe UI", 10, "bold"), foreground="#1a3b5c", background="#ffffff")
style.configure("Sim.TLabelframe", background="#eef6ff", padding=12)
style.configure("Sim.TLabelframe.Label", font=("Segoe UI", 10, "bold"), foreground="#0052cc", background="#eef6ff")
style.configure("TLabel", background="#ffffff", font=("Segoe UI", 9))
style.configure("Sim.TLabel", background="#eef6ff", font=("Segoe UI", 9))
style.configure("Header.TLabel", font=("Segoe UI", 13, "bold"), foreground="#1a3b5c", background="#f4f6f9")
style.configure("BigResult.TLabel", font=("Segoe UI", 12, "bold"), foreground="#0b6e4f", background="#ffffff")
style.configure("SimResult.TLabel", font=("Segoe UI", 11, "bold"), foreground="#0052cc", background="#eef6ff")
style.configure("Accent.TButton", font=("Segoe UI", 10, "bold"))
@staticmethod
def _parse_float(val_str: str) -> float:
if not val_str:
return 0.0
cleaned = str(val_str).replace(" ", "").replace("\xa0", "").replace(",", ".")
try:
return float(cleaned)
except ValueError:
return 0.0
@staticmethod
def _fmt_money(val: float) -> str:
return f"{val:,.2f} ₽".replace(",", " ")
def _build_ui(self):
# Верхний заголовок
top_bar = ttk.Frame(self, padding=(15, 10))
top_bar.pack(fill="x")
ttk.Label(
top_bar,
text="Расчет заработной платы и бонуса МПП 2ГИС (г. Сочи)",
style="Header.TLabel"
).pack(side="left")
# Основной контейнер (Левая колонка — Основной расчет, Правая — Симулятор и Итоги)
main_frame = ttk.Frame(self, padding=(15, 5))
main_frame.pack(fill="both", expand=True)
main_frame.columnconfigure(0, weight=5)
main_frame.columnconfigure(1, weight=5)
# ==================== ЛЕВАЯ КОЛОНКА ====================
left_col = ttk.Frame(main_frame)
left_col.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
# 1. Оклад и режим работы
frm_base = ttk.LabelFrame(left_col, text="1. Базовая часть и статус сотрудника", style="Card.TLabelframe")
frm_base.pack(fill="x", pady=(0, 8))
ttk.Label(frm_base, text="Статус МПП:").grid(row=0, column=0, sticky="w", pady=3)
self.var_mode = tk.StringVar(value="standard")
modes = [
("Действующий МПП (со 2-го месяца — полная СОТ)", "standard"),
("Новичок 1-й месяц (только бонус от оплат)", "first_month"),
("Стажер 1-й месяц (без объема, с порогом оплат 50 000 ₽)", "intern"),
]
for idx, (text, val) in enumerate(modes):
rb = ttk.Radiobutton(frm_base, text=text, variable=self.var_mode, value=val, command=self.calculate_main)
rb.grid(row=idx, column=1, columnspan=3, sticky="w", pady=2)
ttk.Label(frm_base, text="Оклад (₽):").grid(row=3, column=0, sticky="w", pady=(8, 2))
self.ent_salary = ttk.Entry(frm_base, width=16)
self.ent_salary.insert(0, "40000")
self.ent_salary.grid(row=3, column=1, sticky="w", padx=5, pady=(8, 2))
ttk.Label(frm_base, text="Квал. надбавка (₽):").grid(row=3, column=2, sticky="w", padx=(10, 0), pady=(8, 2))
self.ent_qual = ttk.Entry(frm_base, width=16)
self.ent_qual.insert(0, "0")
self.ent_qual.grid(row=3, column=3, sticky="w", padx=5, pady=(8, 2))
# 2. План.Деньги и План.Объем (Основной расчет)
frm_plans = ttk.LabelFrame(left_col, text="2. Показатели: План.Деньги, План.Объем и Прирост", style="Card.TLabelframe")
frm_plans.pack(fill="x", pady=(0, 8))
# План.Деньги
ttk.Label(frm_plans, text="План.Деньги:", font=("Segoe UI", 9, "bold")).grid(row=0, column=0, sticky="w", pady=4)
self.ent_plan_money = ttk.Entry(frm_plans, width=16)
self.ent_plan_money.insert(0, "500000")
self.ent_plan_money.grid(row=0, column=1, padx=5, pady=4)
ttk.Label(frm_plans, text="Факт.Деньги (Оплаты):").grid(row=0, column=2, sticky="w", padx=(10, 0), pady=4)
self.ent_fact_money = ttk.Entry(frm_plans, width=16)
self.ent_fact_money.insert(0, "550000")
self.ent_fact_money.grid(row=0, column=3, padx=5, pady=4)
# План.Объем
ttk.Label(frm_plans, text="План. Объем:", font=("Segoe UI", 9, "bold")).grid(row=1, column=0, sticky="w", pady=4)
self.ent_plan_vol = ttk.Entry(frm_plans, width=16)
self.ent_plan_vol.insert(0, "400000")
self.ent_plan_vol.grid(row=1, column=1, padx=5, pady=4)
ttk.Label(frm_plans, text="Факт отгруж. объем:").grid(row=1, column=2, sticky="w", padx=(10, 0), pady=4)
self.ent_fact_vol = ttk.Entry(frm_plans, width=16)
self.ent_fact_vol.insert(0, "400000")
self.ent_fact_vol.grid(row=1, column=3, padx=5, pady=4)
# Общий объем в выпуске и прошлый месяц
self.var_same_vol = tk.BooleanVar(value=True)
chk_same = ttk.Checkbutton(
frm_plans,
text="Общий объем в выпуске равен Факт. отгруженному объему",
variable=self.var_same_vol,
command=self._toggle_total_vol
)
chk_same.grid(row=2, column=0, columnspan=4, sticky="w", pady=(4, 2))
ttk.Label(frm_plans, text="Общий объем (тек.):").grid(row=3, column=0, sticky="w", pady=4)
self.ent_total_vol = ttk.Entry(frm_plans, width=16, state="disabled")
self.ent_total_vol.insert(0, "400000")
self.ent_total_vol.grid(row=3, column=1, padx=5, pady=4)
ttk.Label(frm_plans, text="Объем прошл. мес.:").grid(row=3, column=2, sticky="w", padx=(10, 0), pady=4)
self.ent_prev_vol = ttk.Entry(frm_plans, width=16)
self.ent_prev_vol.insert(0, "350000")
self.ent_prev_vol.grid(row=3, column=3, padx=5, pady=4)
# 3. Новые продажи и Доп. продукты
frm_new = ttk.LabelFrame(left_col, text="3. Бонус за Новые продажи (по КП), CPC/CPA и Спецпроекты", style="Card.TLabelframe")
frm_new.pack(fill="both", expand=True, pady=(0, 8))
ttk.Label(
frm_new,
text="Укажите сумму месячного объема новых БЗ по коэффициенту предоплаты (КП):",
foreground="#555555"
).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 6))
self.new_sales_entries = {}
rates_items = list(SochiBonusCalculator.NEW_SALES_RATES.items())
for i, (label_text, rate) in enumerate(rates_items):
r = (i // 2) + 1
c = (i % 2) * 2
ttk.Label(frm_new, text=f"{label_text}:").grid(row=r, column=c, sticky="w", pady=3, padx=(0 if c == 0 else 10, 2))
ent = ttk.Entry(frm_new, width=14)
ent.insert(0, "20000" if i == 2 else "0") # По умолчанию пример в КП от 4 мес
ent.grid(row=r, column=c + 1, sticky="w", pady=3, padx=3)
self.new_sales_entries[label_text] = (ent, rate)
ttk.Separator(frm_new, orient="horizontal").grid(row=4, column=0, columnspan=4, sticky="ew", pady=8)
ttk.Label(frm_new, text="Оплаты CPC/CPA (₽):").grid(row=5, column=0, sticky="w", pady=3)
self.ent_cpc = ttk.Entry(frm_new, width=14)
self.ent_cpc.insert(0, "0")
self.ent_cpc.grid(row=5, column=1, sticky="w", padx=3, pady=3)
ttk.Label(frm_new, text="Спецпроекты 10% (₽):").grid(row=5, column=2, sticky="w", padx=(10, 2), pady=3)
self.ent_spec = ttk.Entry(frm_new, width=14)
self.ent_spec.insert(0, "0")
self.ent_spec.grid(row=5, column=3, sticky="w", padx=3, pady=3)
# Привязка пересчета при вводе в поля основного расчета
for entry in [
self.ent_salary, self.ent_qual, self.ent_plan_money, self.ent_fact_money,
self.ent_plan_vol, self.ent_fact_vol, self.ent_total_vol, self.ent_prev_vol,
self.ent_cpc, self.ent_spec
] + [item[0] for item in self.new_sales_entries.values()]:
entry.bind("<KeyRelease>", self._on_main_entry_change)
# ==================== ПРАВАЯ КОЛОНКА ====================
right_col = ttk.Frame(main_frame)
right_col.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
# БЛОК ОТДЕЛЬНОГО СИМУЛЯТОРА С ПОЛЗУНКАМИ
frm_sim = ttk.LabelFrame(
right_col,
text="4. ОТДЕЛЬНЫЙ СИМУЛЯТОР: Прогноз премии ползунком («Что если?»)",
style="Sim.TLabelframe"
)
frm_sim.pack(fill="x", pady=(0, 10))
ttk.Label(
frm_sim,
text="Двигайте ползунок влево-вправо ИЛИ впишите любую сумму вручную.\n"
"Расчет идет отдельно от основного факта (на базе указанных План.Деньги и План.Объем).",
style="Sim.TLabel",
foreground="#334e68"
).pack(anchor="w", pady=(0, 8))
# --- Ползунок 1: Симулятор План.Деньги ---
sim_money_box = ttk.Frame(frm_sim)
sim_money_box.pack(fill="x", pady=4)
ttk.Label(sim_money_box, text="Симуляция ОПЛАТ (₽):", style="Sim.TLabel", font=("Segoe UI", 9, "bold")).grid(row=0, column=0, sticky="w")
self.ent_sim_money = ttk.Entry(sim_money_box, width=16, font=("Segoe UI", 9, "bold"))
self.ent_sim_money.insert(0, "550000")
self.ent_sim_money.grid(row=0, column=1, padx=6)
self.ent_sim_money.bind("<KeyRelease>", self._on_sim_money_entry)
self.lbl_sim_money_pct = ttk.Label(sim_money_box, text="Выполнение: 110.0% (P1 = 1.10)", style="Sim.TLabel")
self.lbl_sim_money_pct.grid(row=0, column=2, padx=6, sticky="w")
self.scale_sim_money = tk.Scale(
frm_sim, from_=0, to=1000000, orient="horizontal", resolution=1000,
bg="#eef6ff", highlightthickness=0, troughcolor="#cce0ff",
command=self._on_sim_money_slider
)
self.scale_sim_money.set(550000)
self.scale_sim_money.pack(fill="x", pady=(0, 2))
self.lbl_sim_money_res = ttk.Label(frm_sim, text="Бонус за деньги по ползунку: 8 772.50 ₽", style="SimResult.TLabel")
self.lbl_sim_money_res.pack(anchor="w", pady=(0, 8))
ttk.Separator(frm_sim, orient="horizontal").pack(fill="x", pady=4)
# --- Ползунок 2: Симулятор План.Объем ---
sim_vol_box = ttk.Frame(frm_sim)
sim_vol_box.pack(fill="x", pady=4)
ttk.Label(sim_vol_box, text="Симуляция ОБЪЕМА (₽):", style="Sim.TLabel", font=("Segoe UI", 9, "bold")).grid(row=0, column=0, sticky="w")
self.ent_sim_vol = ttk.Entry(sim_vol_box, width=16, font=("Segoe UI", 9, "bold"))
self.ent_sim_vol.insert(0, "400000")
self.ent_sim_vol.grid(row=0, column=1, padx=6)
self.ent_sim_vol.bind("<KeyRelease>", self._on_sim_vol_entry)
self.lbl_sim_vol_pct = ttk.Label(sim_vol_box, text="Выполнение: 100.0% (P2 = 1.00)", style="Sim.TLabel")
self.lbl_sim_vol_pct.grid(row=0, column=2, padx=6, sticky="w")
self.scale_sim_vol = tk.Scale(
frm_sim, from_=0, to=800000, orient="horizontal", resolution=1000,
bg="#eef6ff", highlightthickness=0, troughcolor="#cce0ff",
command=self._on_sim_vol_slider
)
self.scale_sim_vol.set(400000)
self.scale_sim_vol.pack(fill="x", pady=(0, 2))
self.lbl_sim_vol_res = ttk.Label(frm_sim, text="Бонус за объем: 6 000.00 ₽ | Прирост (20%): 10 000.00 ₽", style="SimResult.TLabel")
self.lbl_sim_vol_res.pack(anchor="w", pady=(0, 6))
self.lbl_sim_total = ttk.Label(
frm_sim,
text="ИТОГО по симулятору (Деньги + Объем + Прирост): 24 772.50 ₽",
font=("Segoe UI", 11, "bold"), foreground="#0b6e4f", background="#eef6ff"
)
self.lbl_sim_total.pack(anchor="w", pady=(4, 0))
# БЛОК 5: ДЕТАЛЬНЫЙ ИТОГ ОСНОВНОГО РАСЧЕТА
frm_res = ttk.LabelFrame(right_col, text="5. Итоговый расчет премии и зарплаты (по основным полям)", style="Card.TLabelframe")
frm_res.pack(fill="both", expand=True, pady=(0, 8))
self.txt_report = tk.Text(frm_res, height=13, font=("Consolas", 9), bg="#fafbfc", relief="flat", padx=8, pady=8)
self.txt_report.pack(fill="both", expand=True)
btn_frame = ttk.Frame(frm_res)
btn_frame.pack(fill="x", pady=(8, 0))
ttk.Button(btn_frame, text="Пересчитать всё", style="Accent.TButton", command=self.calculate_main).pack(side="left", padx=(0, 8))
ttk.Button(btn_frame, text="Сохранить расчет в файл (.txt)", command=self.save_report).pack(side="left")
def _toggle_total_vol(self):
if self.var_same_vol.get():
self.ent_total_vol.configure(state="normal")
self.ent_total_vol.delete(0, tk.END)
self.ent_total_vol.insert(0, self.ent_fact_vol.get())
self.ent_total_vol.configure(state="disabled")
else:
self.ent_total_vol.configure(state="normal")
self.calculate_main()
def _on_main_entry_change(self, event=None):
if self.var_same_vol.get():
self.ent_total_vol.configure(state="normal")
self.ent_total_vol.delete(0, tk.END)
self.ent_total_vol.insert(0, self.ent_fact_vol.get())
self.ent_total_vol.configure(state="disabled")
self.update_simulator_ranges()
self.calculate_main()
# =====================================================================
# ЛОГИКА СИМУЛЯТОРА (ДВУСТОРОННЯЯ СВЯЗЬ ПОЛЗУНОК <-> ПОЛЕ ВВОДА)
# =====================================================================
def update_simulator_ranges(self):
plan_m = max(self._parse_float(self.ent_plan_money.get()), 100_000.0)
plan_v = max(self._parse_float(self.ent_plan_vol.get()), 100_000.0)
max_m = int(plan_m * 2.0)
max_v = int(plan_v * 2.0)
self.scale_sim_money.configure(to=max_m)
self.scale_sim_vol.configure(to=max_v)
self._recalc_simulator()
def _on_sim_money_slider(self, val):
if self._updating_sim_money:
return
self._updating_sim_money = True
amount = float(val)
self.ent_sim_money.delete(0, tk.END)
self.ent_sim_money.insert(0, f"{int(amount)}")
self._recalc_simulator()
self._updating_sim_money = False
def _on_sim_money_entry(self, event=None):
if self._updating_sim_money:
return
self._updating_sim_money = True
amount = self._parse_float(self.ent_sim_money.get())
if amount > self.scale_sim_money.cget("to"):
self.scale_sim_money.configure(to=int(amount * 1.2))
self.scale_sim_money.set(amount)
self._recalc_simulator()
self._updating_sim_money = False
def _on_sim_vol_slider(self, val):
if self._updating_sim_vol:
return
self._updating_sim_vol = True
amount = float(val)
self.ent_sim_vol.delete(0, tk.END)
self.ent_sim_vol.insert(0, f"{int(amount)}")
self._recalc_simulator()
self._updating_sim_vol = False
def _on_sim_vol_entry(self, event=None):
if self._updating_sim_vol:
return
self._updating_sim_vol = True
amount = self._parse_float(self.ent_sim_vol.get())
if amount > self.scale_sim_vol.cget("to"):
self.scale_sim_vol.configure(to=int(amount * 1.2))
self.scale_sim_vol.set(amount)
self._recalc_simulator()
self._updating_sim_vol = False
def _recalc_simulator(self):
plan_m = self._parse_float(self.ent_plan_money.get())
sim_m = self._parse_float(self.ent_sim_money.get())
res_m = SochiBonusCalculator.calc_payments_bonus(plan_m, sim_m)
status_m = " (< 90% — не начисляется)" if (0 < res_m["pct"] < 90) else (" (Макс. порог 1.5)" if res_m["pct"] >= 150 else "")
self.lbl_sim_money_pct.configure(text=f"Выполнение: {res_m['pct']:.1f}% | P1 = {res_m['p1']:.2f}{status_m}")
self.lbl_sim_money_res.configure(text=f"Бонус за деньги по ползунку: {self._fmt_money(res_m['bonus'])}")
plan_v = self._parse_float(self.ent_plan_vol.get())
prev_v = self._parse_float(self.ent_prev_vol.get())
sim_v = self._parse_float(self.ent_sim_vol.get())
res_v = SochiBonusCalculator.calc_volume_bonus(plan_v, sim_v, sim_v)
res_gr = SochiBonusCalculator.calc_growth_bonus(sim_v, prev_v)
status_v = " (< 90% — 0 ₽)" if (0 < res_v["pct"] < 90) else (" (Макс. порог 1.5)" if res_v["pct"] >= 150 else "")
self.lbl_sim_vol_pct.configure(text=f"Выполнение: {res_v['pct']:.1f}% | P2 = {res_v['p2']:.2f}{status_v}")
self.lbl_sim_vol_res.configure(
text=f"Бонус за объем: {self._fmt_money(res_v['bonus'])} | Бонус за прирост (20%): {self._fmt_money(res_gr['bonus'])}"
)
sim_total = res_m["bonus"] + res_v["bonus"] + res_gr["bonus"]
self.lbl_sim_total.configure(text=f"ИТОГО по симулятору (Деньги + Объем + Прирост): {self._fmt_money(sim_total)}")
# =====================================================================
# ОСНОВНОЙ РАСЧЕТ И ФОРМИРОВАНИЕ ОТЧЕТА
# =====================================================================
def calculate_main(self):
mode = self.var_mode.get()
salary = self._parse_float(self.ent_salary.get())
qual = self._parse_float(self.ent_qual.get())
plan_money = self._parse_float(self.ent_plan_money.get())
fact_money = self._parse_float(self.ent_fact_money.get())
plan_vol = self._parse_float(self.ent_plan_vol.get())
fact_vol = self._parse_float(self.ent_fact_vol.get())
total_vol = fact_vol if self.var_same_vol.get() else self._parse_float(self.ent_total_vol.get())
prev_vol = self._parse_float(self.ent_prev_vol.get())
# 1. Бонус от оплат
res_pay = SochiBonusCalculator.calc_payments_bonus(plan_money, fact_money)
bonus_pay = res_pay["bonus"]
# 2. Бонус от объема, прироста и новых продаж (со 2-го месяца)
if mode == "standard":
res_vol = SochiBonusCalculator.calc_volume_bonus(plan_vol, fact_vol, total_vol)
bonus_vol = res_vol["bonus"]
res_growth = SochiBonusCalculator.calc_growth_bonus(total_vol, prev_vol)
bonus_growth = res_growth["bonus"]
bonus_new = 0.0
for _, (ent, rate) in self.new_sales_entries.items():
bonus_new += self._parse_float(ent.get()) * rate
else:
res_vol = {"pct": 0.0, "p2": 0.0, "bonus": 0.0}
res_growth = {"diff": 0.0, "bonus": 0.0}
bonus_vol = 0.0
bonus_growth = 0.0
bonus_new = 0.0
# 3. Бонус стажера за порог оплат (50 000 руб.)
if mode == "intern":
res_intern = SochiBonusCalculator.calc_intern_threshold_bonus(fact_money)
bonus_intern = res_intern["bonus"]
else:
res_intern = {"r_po": 0.0, "bonus": 0.0}
bonus_intern = 0.0
# 4. CPC/CPA и Спецпроекты
cpc_pay = self._parse_float(self.ent_cpc.get())
bonus_cpc = SochiBonusCalculator.calc_cpc_bonus(cpc_pay)
spec_pay = self._parse_float(self.ent_spec.get())
bonus_spec = spec_pay * SochiBonusCalculator.SPEC_PROJECT_RATE
# Итоги
total_bonus = bonus_pay + bonus_vol + bonus_growth + bonus_new + bonus_intern + bonus_cpc + bonus_spec
gross_total = salary + qual + total_bonus
ndfl = gross_total * 0.13
net_total = gross_total - ndfl
# Формирование текста отчета
lines = [
f"=== ДЕТАЛЬНЫЙ РАСЧЕТ ПРЕМИИ МПП (г. СОЧИ) ===",
f"План.Деньги: {self._fmt_money(plan_money)} | Факт: {self._fmt_money(fact_money)} ({res_pay['pct']:.1f}%, P1={res_pay['p1']:.2f})",
f" -> 1. Бонус от оплат (1.45%): {self._fmt_money(bonus_pay)}",
]
if mode == "standard":
lines.extend([
f"План. Объем: {self._fmt_money(plan_vol)} | Факт: {self._fmt_money(fact_vol)} ({res_vol['pct']:.1f}%, P2={res_vol['p2']:.2f})",
f" -> 2. Бонус от общего объема (1.50%): {self._fmt_money(bonus_vol)}",
f" -> 3. Бонус за прирост ({self._fmt_money(res_growth['diff'])}, 20%): {self._fmt_money(bonus_growth)}",
f" -> 4. Бонус за новые продажи (по КП): {self._fmt_money(bonus_new)}",
])
elif mode == "first_month":
lines.append(" * Режим 1-го месяца: объем, прирост и новые продажи начисляются со 2-го мес.")
elif mode == "intern":
lines.append(
f" -> Порог оплат стажера ({res_intern['r_po']*100:.1f}% от 50 000 ₽): {self._fmt_money(bonus_intern)}"
)
if bonus_cpc > 0 or bonus_spec > 0:
lines.append(f" -> 5. Бонус CPC/CPA и Спецпроекты: {self._fmt_money(bonus_cpc + bonus_spec)}")
lines.extend([
"-" * 58,
f"ИТОГО БОНУС (до НДФЛ): {self._fmt_money(total_bonus)}",
f"Оклад + Квал. надбавка: {self._fmt_money(salary + qual)}",
f"ВСЕГО НАЧИСЛЕНО (до НДФЛ): {self._fmt_money(gross_total)}",
f"НА РУКИ (за вычетом НДФЛ 13%): {self._fmt_money(net_total)}",
])
self.txt_report.delete("1.0", tk.END)
self.txt_report.insert(tk.END, "\n".join(lines))
def save_report(self):
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Текстовый файл", "*.txt")],
initialfile=f"Расчет_премии_Сочи_{datetime.now().strftime('%Y-%m-%d')}.txt"
)
if file_path:
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.txt_report.get("1.0", tk.END))
messagebox.showinfo("Сохранено", f"Отчет успешно сохранен:\n{file_path}")
if __name__ == "__main__":
run_self_tests()
app = BonusApp()
app.mainloop()