# /opt/trueconf_bot/lk/payslip_service.py
import calendar
import tempfile
from itertools import zip_longest
from weasyprint import HTML
import config.config as config
# Перевод английских системных месяцев в русские наименования для бланка PDF
ENG_TO_RU_MONTHS = {
"January": "Январь", "February": "Февраль", "March": "Март",
"April": "Апрель", "May": "Май", "June": "Июнь",
"July": "Июль", "August": "Август", "September": "Сентябрь",
"October": "Октябрь", "November": "Ноябрь", "December": "Декабрь"
}
def format_currency(summa):
try:
if summa is None:
return "0,00"
f_summa = float(summa)
return f"{f_summa:,.2f}".replace(",", " ").replace(".", ",")
except:
return str(summa)
def translate_period(period_str):
if not period_str:
return ""
for eng, ru in ENG_TO_RU_MONTHS.items():
if eng in period_str:
return period_str.replace(eng, ru)
return period_str
def get_latest_payslip_months(cursor, card_id: str) -> list:
query = """
SELECT DISTINCT TOP 6 (t.TYEAR * 12 + t.TMONTH) as CMONTH
FROM UOV_SELFSERVICE_PR_EMP e
JOIN UOV_SELFSERVICE_PR_CARD c on c.ID = e.CARD_ID
JOIN UOV_SELFSERVICE_SL_YTAX t on t.CARD_ID = e.CARD_ID
JOIN UOV_SELFSERVICE_PR_TRANS tr on tr.EMP_ID = e.ID
JOIN UOV_SELFSERVICE_ST_APPOINT a on a.ID = tr.APPOINT_ID
JOIN UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID
WHERE c.ID = %s
ORDER BY CMONTH DESC
"""
cursor.execute(query, (card_id,))
months = cursor.fetchall()
if not months:
raise Exception("Отсутствуют расчетные периоды.")
return [m[0] for m in months]
def create_payslip_pdf(month_name, target_tyear,
header_sys, header_wage, header_deduct, header_base,
earnings_rows, deductions_rows, noncash_rows, payout_rows,
total_earned, total_withheld, total_noncash, total_payouts,
debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay):
tbody_html = ""
tbody_html += f"""
| 1. Начислено |
2. Удержано |
"""
for earn, ded in zip_longest(earnings_rows, deductions_rows):
if earn:
earn_name = str(earn[0]).strip()
earn_days = f"{earn[1]:g}".replace('.', ',') if earn[1] else ""
earn_hours = f"{earn[2]:g}".replace('.', ',') if earn[2] else ""
earn_prc = f"{earn[3]:g}" if earn[3] else ""
earn_period = translate_period(earn[4] or "")
earn_summa = format_currency(earn[5])
earn_html = f'{earn_name} | {earn_days} | {earn_hours} | {earn_prc} | {earn_period} | {earn_summa} | '
else:
earn_html = ' | | | | | | '
if ded:
ded_name = str(ded[0]).strip()
ded_prc = f"{ded[1]:.0f}" if ded[1] else ""
ded_period = translate_period(ded[2] or "")
ded_summa = format_currency(ded[3])
ded_html = f'{ded_name} | {ded_prc} | {ded_period} | {ded_summa} | '
else:
ded_html = ' | '
tbody_html += f"{earn_html}{ded_html}
"
tbody_html += f"""
| Всего начислено | {format_currency(total_earned)} |
Всего удержано | {format_currency(total_withheld)} |
| 3. Доходы в неденежной форме |
4. Выплачено |
"""
for nc, pay in zip_longest(noncash_rows, payout_rows):
if nc:
nc_name = str(nc[0]).strip()
nc_period = translate_period(nc[1] or "")
nc_summa = format_currency(nc[2])
nc_html = f'{nc_name} | {nc_period} | {nc_summa} | '
else:
nc_html = ' | | | | | | '
if pay:
pay_name = str(pay[0]).strip()
pay_period = translate_period(pay[1] or "")
pay_summa = format_currency(pay[2])
pay_html = f'{pay_name} | {pay_period} | {pay_summa} | '
else:
pay_html = ' | '
tbody_html += f"{nc_html}{pay_html}
"
tbody_html += f"""
| Всего доходов в неденежной форме | {format_currency(total_noncash)} |
Всего выплачено | {format_currency(total_payouts)} |
| Долг за предприятием на начало месяца | {format_currency(debt_comp_start)} |
Долг за предприятием на конец месяца | {format_currency(debt_comp_end)} |
| Долг за работником на начало месяца | {format_currency(debt_emp_start)} |
Долг за работником на конец месяца | {format_currency(debt_emp_end)} |
|
5. Выплачено денежными средствами | {format_currency(net_pay)} |
"""
html_template = f"""
Расчетный лист за {month_name} {target_tyear} г.
| Вид | Дни | Часы | % | Период | Сумма |
Вид | % | Период | Сумма |
{tbody_html}
"""
temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
HTML(string=html_template).write_pdf(temp_pdf.name)
return temp_pdf.name
def build_payslip_data(cursor, card_id: str, target_cmonth: int) -> tuple:
tyear = (target_cmonth - 1) // 12
tmonth = (target_cmonth - 1) % 12 + 1
_, last_day = calendar.monthrange(tyear, tmonth)
date_start = f"{tyear}-{tmonth:02d}-01"
date_end = f"{tyear}-{tmonth:02d}-{last_day:02d}"
date_params = (date_start, date_start, date_end, date_end, date_end)
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь",
7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
month_name = months_ru.get(tmonth, f"{tmonth:02d}")
# FIRM_ID по c.ID
q_firm = """
select e.FIRM_ID, f.SNAME from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e
join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s
group by e.ID, e.FIRM_ID ) as e
join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID
join UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID
where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31'))
group by e.FIRM_ID, f.SNAME
"""
cursor.execute(q_firm, (card_id, *date_params))
firm_row = cursor.fetchone()
if not firm_row:
raise Exception("Не удалось определить FIRM_ID для сотрудника в указанном периоде.")
firm_id = firm_row[0]
# ШАПКА по c.ID
q_header = """
select s.NAME as [NAME], ROUND(t.WAGE, 2), isnull(sum(yt.u21 + yt.u30), 0), ROUND(isnull(sum(yt.n200 + yt.n5100 - yt.u21 - yt.u30 - yt.u42 - yt.u99), 0), 2)
from (select e.ID, e.FIRM_ID, e.CARD_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID
where c.ID = %s and e.FIRM_ID=%s group by e.ID, e.FIRM_ID, e.CARD_ID ) as e
join UOV_SELFSERVICE_pr_trans t on t.EMP_ID = e.ID join UOV_SELFSERVICE_hr_syspay s on s.ID = t.SYSPAY_ID
join UOV_SELFSERVICE_sl_ytax yt on yt.CARD_ID = e.CARD_ID and yt.FIRM_ID = e.FIRM_ID
where e.FIRM_ID = %s and yt.tmonth = %s and yt.tyear = %s
and ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31'))
and t.tp_abbr='CUR' group by s.NAME, t.WAGE, t.D_FROM order by t.D_FROM desc
"""
cursor.execute(q_header, (card_id, firm_id, firm_id, tmonth, tyear, *date_params))
h_row = cursor.fetchone()
header_sys = str(h_row[0]) if h_row else "Неизвестная система"
header_wage = float(h_row[1]) if h_row and h_row[1] else 0.0
header_deduct = float(h_row[2]) if h_row and h_row[2] else 0.0
header_base = float(h_row[3]) if h_row and h_row[3] else 0.0
# НАЧИСЛЕНО по c.ID
q_earn = """
select p.NAME AS [NAME], ROUND(l.days, 1), ROUND(l.hours, 1), CAST(l.prc as INT), convert(varchar, i.D_FROM,104)+'-'+convert(varchar, i.D_TO,104), ROUND(l.summa, 2)
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID join UOV_SELFSERVICE_pr_ill i on i.ID=l.ID_MAIN
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm = 6 and l.cmonth = %s
UNION ALL
select max(p.NAME) AS [NAME], sum(l.days), ROUND(sum(l.hours), 1), CAST(l.prc as INT), (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(isnull(sum(l.summa),0), 2)
from(select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID
where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm <> 6 and l.cmonth = %s
group by l.emp_id,l.pay_id,l.tmonth,l.prc, case l.PAY_ID when 17 then l.id else 0 end, l.TYEAR order by 6 DESC
"""
cursor.execute(q_earn, (card_id, firm_id, *date_params, target_cmonth, card_id, firm_id, *date_params, target_cmonth))
earnings_rows = cursor.fetchall()
# УДЕРЖАНО по c.ID
q_deduct = """
select max(p.NAME) AS [NAME], CAST(l.prc as INT), ISNULL(DATENAME(month, DATEADD(month, l.tmonth, 0) - 1) + ' ' + CAST(NULLIF(l.TYEAR, 0) AS varchar), ''), ROUND(isnull(sum(l.summa),0), 2)
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
JOIN UOV_SELFSERVICE_sl_lic l ON l.EMP_ID = e.ID JOIN UOV_SELFSERVICE_sl_pay p ON p.ID = l.PAY_ID
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
where l.t_pay=2 and l.pay_id not in (301,490) and l.pay_id not in (305, 308, 316, 328, 360, 364) and l.cmonth = %s
group by l.emp_id,l.pay_id,l.tmonth,l.prc, l.id, l.TYEAR order by 4 DESC
"""
cursor.execute(q_deduct, (card_id, firm_id, *date_params, target_cmonth))
deductions_rows = cursor.fetchall()
# ДОЛГИ по c.ID
q_debts = """
select l.pay_id, ROUND(isnull(sum(l.summa),0), 2) from (select e.ID, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.id join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
where l.cmonth = %s and l.pay_id in (264, 301, 490) group by l.pay_id
"""
cursor.execute(q_debts, (card_id, firm_id, *date_params, target_cmonth))
debt_rows = cursor.fetchall()
debt_comp_start, debt_emp_start, debt_emp_end = 0.0, 0.0, 0.0
for r in debt_rows:
if r[0] == 264: debt_comp_start = float(r[1])
elif r[0] == 301: debt_emp_start = float(r[1])
elif r[0] == 490: debt_emp_end = float(r[1])
# НЕ ДЕНЕЖНЫЕ по c.ID
q_noncash = """
select t.NAME AS [NAME], (DateName(month, CAST(%s as date)) +' '+ DateName(YEAR, CAST(%s as date))), ROUND(isnull(p.summa, 0), 2)
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
join UOV_SELFSERVICE_sl_profit p on p.EMP_ID = e.ID join UOV_SELFSERVICE_sl_tprofit t on t.ID = p.TPROFIT_ID
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID
where p.cmonth = %s and t.id != 9999
"""
cursor.execute(q_noncash, (date_start, date_start, card_id, firm_id, *date_params, target_cmonth))
noncash_rows = cursor.fetchall()
# ВЫПЛАЧЕНО по c.ID
q_payout = """
select p.NAME AS [NAME], (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(sum(isnull(l.summa, 0)), 2)
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID =l.PAY_ID
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID
where l.t_pay = 2 and l.cmonth = %s and l.pay_id in (305, 308, 316, 328, 360, 364) group by p.NAME, l.TMONTH, l.TYEAR
"""
cursor.execute(q_payout, (card_id, firm_id, *date_params, target_cmonth))
payout_rows = cursor.fetchall()
total_earned = sum(float(r[5]) for r in earnings_rows if r[5])
total_withheld = sum(float(r[3]) for r in deductions_rows if r[3])
total_noncash = sum(float(r[2]) for r in noncash_rows if r[2])
total_payouts = sum(float(r[2]) for r in payout_rows if r[2])
# Расчет долга по формуле СибЦема
debt_comp_end = total_earned - total_withheld - total_payouts
net_pay = total_earned - total_withheld
response = f"💰 Расчетный листок за {month_name} {tyear} г.\n\n1. Начислено\n"
for row in earnings_rows:
time_spent = f" ({float(row[1]):g} дн. / {float(row[2]):g} ч.)" if (row[1] or row[2]) else ""
response += f"🔹 {str(row[0]).strip()}{time_spent}: {format_currency(row[5])} руб.\n"
response += f"Всего начислено: {format_currency(total_earned)} руб.\n\n2. Удержано\n"
for row in deductions_rows:
prc_str = f" ({int(row[1])}%)" if row[1] else ""
response += f"🔹 {str(row[0]).strip()}{prc_str}: {format_currency(row[3])} руб.\n"
response += f"Всего удержано: {format_currency(total_withheld)} руб.\n\n3. Доходы в неденежной форме: {format_currency(total_noncash)} руб.\n\n4. Выплачено\n"
for row in payout_rows:
response += f"🔹 {str(row[0]).strip()}: {format_currency(row[2])} руб.\n"
response += f"Всего выплачено: {format_currency(total_payouts)} руб.\n\nДолг за предприятием на конец месяца: {format_currency(debt_comp_end)} руб.\nДолг за работником на конец месяца: {format_currency(debt_emp_end)} руб.\n\n5. Выплачено денежными средствами: {format_currency(net_pay)} руб."
pdf_path = create_payslip_pdf(
month_name, tyear, header_sys, header_wage, header_deduct, header_base,
earnings_rows, deductions_rows, noncash_rows, payout_rows,
total_earned, total_withheld, total_noncash, total_payouts,
debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay
)
return response, pdf_path