Files
trueconf_bot/lk/payslip_service.py

322 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# /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"""
<tr>
<td colspan="6" class="section-title">1. Начислено</td>
<td colspan="4" class="section-title">2. Удержано</td>
</tr>
"""
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'<td class="left-align">{earn_name}</td><td class="center-align">{earn_days}</td><td class="center-align">{earn_hours}</td><td class="center-align">{earn_prc}</td><td class="center-align">{earn_period}</td><td>{earn_summa}</td>'
else:
earn_html = '<td style="border-bottom: none; border-left: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none; border-right: none;"></td>'
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'<td class="left-align">{ded_name}</td><td class="center-align">{ded_prc}</td><td class="center-align">{ded_period}</td><td>{ded_summa}</td>'
else:
ded_html = '<td style="border-bottom: none; border-left: none; border-right: none;" colspan="4"></td>'
tbody_html += f"<tr>{earn_html}{ded_html}</tr>"
tbody_html += f"""
<tr class="totals">
<td class="left-align" colspan="5">Всего начислено</td><td>{format_currency(total_earned)}</td>
<td class="left-align" colspan="3">Всего удержано</td><td>{format_currency(total_withheld)}</td>
</tr>
<tr>
<td colspan="6" class="section-title">3. Доходы в неденежной форме</td>
<td colspan="4" class="section-title">4. Выплачено</td>
</tr>
"""
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'<td class="left-align" colspan="4">{nc_name}</td><td class="center-align">{nc_period}</td><td>{nc_summa}</td>'
else:
nc_html = '<td style="border-top: none; border-bottom: none; border-left: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none; border-right: none;"></td>'
if pay:
pay_name = str(pay[0]).strip()
pay_period = translate_period(pay[1] or "")
pay_summa = format_currency(pay[2])
pay_html = f'<td class="left-align" colspan="2">{pay_name}</td><td class="center-align">{pay_period}</td><td>{pay_summa}</td>'
else:
pay_html = '<td style="border-top: none; border-bottom: none; border-left: none; border-right: none;" colspan="4"></td>'
tbody_html += f"<tr>{nc_html}{pay_html}</tr>"
tbody_html += f"""
<tr class="totals">
<td class="left-align" colspan="5">Всего доходов в неденежной форме</td><td>{format_currency(total_noncash)}</td>
<td class="left-align" colspan="3">Всего выплачено</td><td>{format_currency(total_payouts)}</td>
</tr>
<tr>
<td class="left-align" colspan="5">Долг за предприятием на начало месяца</td><td>{format_currency(debt_comp_start)}</td>
<td class="left-align" colspan="3">Долг за предприятием на конец месяца</td><td>{format_currency(debt_comp_end)}</td>
</tr>
<tr>
<td class="left-align" colspan="5">Долг за работником на начало месяца</td><td>{format_currency(debt_emp_start)}</td>
<td class="left-align" colspan="3">Долг за работником на конец месяца</td><td>{format_currency(debt_emp_end)}</td>
</tr>
<tr class="totals">
<td colspan="6" class="empty-cell"></td>
<td class="left-align section-title" colspan="3"><b>5. Выплачено денежными средствами</b></td><td class="section-title"><b>{format_currency(net_pay)}</b></td>
</tr>
"""
html_template = f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<style>
@page {{ size: A4 landscape; margin: 15mm; background-color: #ffffff; }}
body {{ font-family: Arial, sans-serif; font-size: 11px; color: #333; margin: 0; padding: 0; }}
h2 {{ text-align: center; margin-bottom: 15px; font-size: 16px; font-weight: normal; }}
table {{ width: 100%; border-collapse: collapse; margin-bottom: 10px; }}
th, td {{ border: 1px solid #a0a0a0; padding: 5px 8px; text-align: right; vertical-align: middle; }}
th {{ background-color: #f2f2f2; text-align: center; font-weight: normal; color: #555; }}
.header-table td {{ border: 1px solid #a0a0a0; text-align: left; background-color: #fcfcfc; }}
.left-align {{ text-align: left; }}
.center-align {{ text-align: center; }}
.section-title {{ background-color: #fcfcfc; font-weight: bold; text-align: left; padding-top: 10px; padding-bottom: 10px; }}
.totals {{ background-color: #fcfcfc; font-weight: bold; }}
.empty-cell {{ border: none; }}
</style>
</head>
<body>
<h2><b>Расчетный лист за {month_name} {target_tyear} г.</b></h2>
<table class="header-table" style="margin-bottom: 15px;">
<tr><td style="width: 60%;">Система оплаты: {header_sys}</td><td style="width: 40%;">Оклад: {format_currency(header_wage)}</td></tr>
<tr><td colspan="2">Стандартные вычеты за месяц: {format_currency(header_deduct)}</td></tr>
<tr><td colspan="2">Облагаемая база по ставке 13%: {format_currency(header_base)}</td></tr>
</table>
<table>
<thead>
<tr>
<th style="width: 20%;">Вид</th><th>Дни</th><th>Часы</th><th>%</th><th>Период</th><th>Сумма</th>
<th style="width: 20%;">Вид</th><th>%</th><th>Период</th><th>Сумма</th>
</tr>
</thead>
<tbody>
{tbody_html}
</tbody>
</table>
</body>
</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"💰 <b>Расчетный листок за {month_name} {tyear} г.</b>\n\n<b>1. Начислено</b>\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}: <b>{format_currency(row[5])} руб.</b>\n"
response += f"<b>Всего начислено: {format_currency(total_earned)} руб.</b>\n\n<b>2. Удержано</b>\n"
for row in deductions_rows:
prc_str = f" ({int(row[1])}%)" if row[1] else ""
response += f"🔹 {str(row[0]).strip()}{prc_str}: <b>{format_currency(row[3])} руб.</b>\n"
response += f"<b>Всего удержано: {format_currency(total_withheld)} руб.</b>\n\n<b>3. Доходы в неденежной форме:</b> {format_currency(total_noncash)} руб.\n\n<b>4. Выплачено</b>\n"
for row in payout_rows:
response += f"🔹 {str(row[0]).strip()}: <b>{format_currency(row[2])} руб.</b>\n"
response += f"<b>Всего выплачено: {format_currency(total_payouts)} руб.</b>\n\n<i>Долг за предприятием на конец месяца: {format_currency(debt_comp_end)} руб.</i>\n<i>Долг за работником на конец месяца: {format_currency(debt_emp_end)} руб.</i>\n\n<b>5. Выплачено денежными средствами: {format_currency(net_pay)} руб.</b>"
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