financial_system/financial_system/io/exporter.py
2026-06-29 14:49:32 +08:00

233 lines
7.3 KiB
Python
Raw 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.

from __future__ import annotations
from pathlib import Path
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
from ..core.config import AppConfig
from ..domain.models import PayrollResult
from .parser import iter_leave_event_descriptions
SUMMARY_HEADERS = [
"姓名",
"部门",
"职位",
"考勤组",
"工资月份",
"薪资模式",
"出勤天数",
"缺勤天数",
"实际工作工时",
"休息天数",
"系统加班总时长",
"打卡推算加班工时",
"请假/调休工时",
"剩余加班工时",
"工作日加班工时",
"周末加班工时",
"节假日加班工时",
"5分钟内迟到次数",
"超过5分钟迟到次数",
"计扣迟到次数",
"迟到扣款",
"缺卡次数",
"缺卡扣款",
"扣款合计",
"底薪",
"模式基础工资",
"提成",
"工作日加班单价",
"周末加班单价",
"节假日加班单价",
"加班费",
"应发工资",
"实发工资",
"备注",
]
DAILY_HEADERS = [
"姓名",
"日期",
"原始考勤结果",
"上班打卡",
"下班打卡",
"打卡推算加班工时",
"迟到分钟",
"缺卡次数",
"请假/调休工时(本格)",
"请假/调休记录",
]
SALARY_MODE_LABELS = {
"monthly": "包月",
"hourly": "计时",
"piecework": "计件",
"probation": "试用期",
}
def export_payroll_results(results: list[PayrollResult], config: AppConfig, output_path: str | Path) -> Path:
"""导出工资汇总、每日明细和规则说明三张表。"""
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
workbook = Workbook()
summary_sheet = workbook.active
summary_sheet.title = "工资汇总"
daily_sheet = workbook.create_sheet("每日明细")
rules_sheet = workbook.create_sheet("规则说明")
_write_summary(summary_sheet, results)
_write_daily_details(daily_sheet, results)
_write_rules(rules_sheet, config)
for sheet in workbook.worksheets:
_style_sheet(sheet)
workbook.save(output_path)
return output_path
def localize_payroll_export_salary_modes(source_path: str | Path, output_path: str | Path) -> Path:
"""复制工资表并把汇总页的薪资模式编码转换为中文标签。"""
source_path = Path(source_path)
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
workbook = load_workbook(source_path)
if "工资汇总" in workbook.sheetnames:
sheet = workbook["工资汇总"]
salary_mode_column = _find_header_column(sheet, "薪资模式")
if salary_mode_column is not None:
for row_idx in range(2, sheet.max_row + 1):
cell = sheet.cell(row=row_idx, column=salary_mode_column)
if cell.value is not None:
cell.value = _salary_mode_label(str(cell.value))
workbook.save(output_path)
return output_path
def _write_summary(sheet, results: list[PayrollResult]) -> None:
sheet.append(SUMMARY_HEADERS)
for result in results:
employee = result.employee
row = [
employee.name,
employee.department,
employee.position,
employee.attendance_group,
result.salary_month,
_salary_mode_label(result.salary_mode),
result.attendance_days,
result.absence_days,
result.actual_work_hours,
employee.summary_fields.get("休息天数", 0),
employee.summary_fields.get("加班总时长", 0),
result.punch_overtime_hours,
result.leave_hours,
result.remaining_overtime_hours,
result.workday_overtime_hours,
result.weekend_overtime_hours,
result.holiday_overtime_hours,
result.minor_late_count,
result.major_late_count,
result.penalized_late_count,
result.late_deduction,
result.missing_card_count,
result.missing_card_deduction,
result.total_deduction,
result.base_salary,
result.base_pay,
result.commission_amount,
result.overtime_rate,
result.weekend_overtime_rate,
result.holiday_overtime_rate,
result.overtime_pay,
result.gross_salary,
result.net_salary,
result.note,
]
sheet.append(row)
def _write_daily_details(sheet, results: list[PayrollResult]) -> None:
sheet.append(DAILY_HEADERS)
for result in results:
for record in result.employee.daily_records:
sheet.append(
[
result.employee.name,
record.work_date,
record.raw_text,
_format_time(record.first_punch),
_format_time(record.last_punch),
record.punch_overtime_hours,
",".join(str(minutes) for minutes in record.late_minutes),
record.missing_card_count,
record.leave_hours_in_cell,
"".join(iter_leave_event_descriptions(record.leave_events)),
]
)
def _write_rules(sheet, config: AppConfig) -> None:
rows = [
("下班时间", config.attendance.off_work_time),
("加班取整分钟", config.attendance.overtime_round_minutes),
("加班计算", "以下班打卡时间为准,超过下班时间后按取整分钟向下取整"),
("5分钟内迟到免扣次数", config.attendance.minor_late_free_times),
("迟到分钟阈值", config.attendance.minor_late_minutes),
("迟到扣款/次", config.attendance.late_penalty),
("缺卡扣款/次", config.attendance.missing_card_penalty),
("请假/调休关键字", "".join(config.attendance.leave_keywords)),
("剩余加班工时", "打卡推算加班工时 - 去重后的请假/调休工时"),
]
sheet.append(["规则", ""])
for row in rows:
sheet.append(row)
def _style_sheet(sheet) -> None:
header_fill = PatternFill("solid", fgColor="1F4E78")
header_font = Font(color="FFFFFF", bold=True)
for cell in sheet[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center")
sheet.freeze_panes = "A2"
for row in sheet.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(vertical="top", wrap_text=True)
for col_idx, cells in enumerate(sheet.columns, start=1):
max_length = 0
for cell in cells:
value = cell.value
if value is None:
continue
text = str(value)
max_length = max(max_length, min(len(text), 45))
sheet.column_dimensions[get_column_letter(col_idx)].width = max(10, max_length + 2)
def _format_time(value) -> str:
if value is None:
return ""
return value.strftime("%H:%M")
def _find_header_column(sheet, header: str) -> int | None:
for cell in sheet[1]:
if cell.value == header:
return cell.column
return None
def _salary_mode_label(value: str) -> str:
return SALARY_MODE_LABELS.get(value, value)