diff --git a/code/pep-stats-report/app/db_helper.py b/code/pep-stats-report/app/db_helper.py index 377c60a..b57bb0a 100644 --- a/code/pep-stats-report/app/db_helper.py +++ b/code/pep-stats-report/app/db_helper.py @@ -1,7 +1,13 @@ -def querystring_user_procinst_duration(start, end, except_senior, department): - filter_user = "and u.name not in ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥', '邱峰')" if except_senior else "" - filter_department = "and department_name='{}'".format(department) if department else "" - return ''' +SENIORS = ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥', '邱峰', '姚文婷') +IGNORE_PERSONS = ('黄红梅',) +IGNORE_PERSONS_SUBQUERY = f"and u.name != '{IGNORE_PERSONS[0]}'" if len(IGNORE_PERSONS) == 1 else f"and u.name not in {IGNORE_PERSONS}" +IGNORE_DEPARTMENTS = ('汇派-质量部', '汇派-生产部', '汇派-计划部', '汇派-人事部', '汇派-采购部', + '党建工会', '工程项目中心', '北京技术中心', '政委', '总经办-培训中心') + + +def querystring_department_user_procinst_duration(start, end, exclude_senior): + filter_user = f"and u.name not in {SENIORS + IGNORE_PERSONS}" if exclude_senior else IGNORE_PERSONS_SUBQUERY + return f''' select department_name, user_name, procinst_id, @@ -22,192 +28,84 @@ from ( inner join workflow_process_history as wph on wpa.procinst_id=wph.procinst_id inner join workflow_process_version as wpv on wph.version_id=wpv.id inner join workflow_process as wp on wpv.process_id=wp.id - inner join user as u on wpa.deal_user_id=u.id + inner join "user" as u on wpa.deal_user_id=u.id inner join department_user as du on u.id=du."user" inner join department as d on du.department=d.id - where wpa.end_time >='{}' and wpa.end_time < '{}' and wp.deleted=false and wp.is_enable=true and d.delete=0 and u.delete=0 and u.state=1 and u.active_status=1 - {} {} + where wpa.end_time >='{start}' and wpa.end_time < '{end}' and wp.deleted=false and wp.is_enable=true + and d.delete=0 and u.delete=0 and u.state=1 and u.active_status=1 + and d.name not in {IGNORE_DEPARTMENTS} + {filter_user} ) as r group by department_name, user_name, procinst_id - '''.format(start, end, filter_user, filter_department) + ''' -def querystring_senior_procinst_duration(start, end): - return ''' +def querystring_user_procinst_duration(start, end, senior): + filter_user = f"and u.name in {SENIORS}" if senior else IGNORE_PERSONS_SUBQUERY + return f''' select user_name, procinst_id, sum(duration_in_minutes) as procinst_duration_in_minutes from ( - select distinct u.id as user_id, - u.name as user_name, - wp.name as process_name, - wpa.procinst_id as procinst_id, - wpa.task_id as taskinst_id, - wpa.task_name as task_name, - wpa.start_time as start_time, - wpa.end_time as end_time, - wpa.total_minutes as duration_in_minutes - from workflow_process_achievements as wpa - inner join workflow_process_history as wph on wpa.procinst_id=wph.procinst_id - inner join workflow_process_version as wpv on wph.version_id=wpv.id - inner join workflow_process as wp on wpv.process_id=wp.id - inner join user as u on wpa.deal_user_id=u.id - where wpa.end_time >='{}' and wpa.end_time < '{}' and wp.deleted=false and wp.is_enable=true and u.delete=0 and u.state=1 and u.active_status=1 - and u.name in ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥', '邱峰') + select distinct u.id as user_id, + u.name as user_name, + wp.name as process_name, + wpa.procinst_id as procinst_id, + wpa.task_id as taskinst_id, + wpa.task_name as task_name, + wpa.start_time as start_time, + wpa.end_time as end_time, + wpa.total_minutes as duration_in_minutes + from workflow_process_achievements as wpa + inner join workflow_process_history as wph on wpa.procinst_id=wph.procinst_id + inner join workflow_process_version as wpv on wph.version_id=wpv.id + inner join workflow_process as wp on wpv.process_id=wp.id + inner join "user" as u on wpa.deal_user_id=u.id + inner join department_user as du on u.id=du."user" + inner join department as d on du.department=d.id + where wpa.end_time >='{start}' and wpa.end_time < '{end}' and wp.deleted=false and wp.is_enable=true + and d.delete=0 and u.delete=0 and u.state=1 and u.active_status=1 + and d.name not in {IGNORE_DEPARTMENTS} + {filter_user} ) as r group by user_name, procinst_id - '''.format(start, end) + ''' # 按月统计项企平台使用人数 def querystring_user_count(start, end): - return ''' + return f''' select count(distinct deal_user_id) as pep_using_user_count from workflow_process_achievements as wpa -inner join user as u on wpa.deal_user_id=u.id -where wpa.end_time >='{}' and wpa.end_time < '{}' and u.delete=0 and u.state=1 and u.active_status=1 - '''.format(start, end) - - -# 个人处理流程平均耗时 -def querystring_procinst_duration_by_company(start, end): - return ''' -SELECT ROUND(AVG(procinst_duration_in_minutes_by_user) /60, 2) as procinst_duration_in_hours from ( - SELECT user_name, - ROUND(AVG(procinst_duration_in_minutes), 2) as procinst_duration_in_minutes_by_user - from ({}) as r2 - GROUP by r2.user_name -) as r3 - '''.format(querystring_user_procinst_duration(start, end, False, '')) +inner join "user" as u on wpa.deal_user_id=u.id +inner join department_user as du on u.id=du."user" +inner join department as d on du.department=d.id +where wpa.end_time >='{start}' and wpa.end_time < '{end}' and u.delete=0 and u.state=1 and u.active_status=1 + and d.name not in {IGNORE_DEPARTMENTS} + {IGNORE_PERSONS_SUBQUERY} + ''' -# 部门数(高于公司平均数) -def querystring_department_count_gt_avg(start, end, avg): - return ''' -SELECT count(*) from ( - SELECT department_name, - ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department - from ( - SELECT department_name, - user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - GROUP by r2.department_name, r2.user_name - ) as r3 - GROUP by r3.department_name -) as r4 -WHERE procinst_duration_in_hours_by_department > {} - '''.format(querystring_user_procinst_duration(start, end, True, ''), avg) - - -# 用户数(高于公司平均数) -def querystring_user_count_gt_avg(start, end, avg): - return ''' -SELECT count(*) from ( - SELECT user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - group by r2.user_name -) as r3 -WHERE r3.procinst_duration_in_hours_by_user > {} - '''.format(querystring_user_procinst_duration(start, end, False, ''), avg) - - -# 部门处理流程平均耗时 -def querystring_procinst_duration_by_department(start, end): - return """ -SELECT department_name, - ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department -from ( - SELECT department_name, - user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - GROUP by r2.department_name, r2.user_name -) as r3 -GROUP by r3.department_name -order by procinst_duration_in_hours_by_department DESC - """.format(querystring_user_procinst_duration(start, end, True, '')) - - -# 部门处理流程平均耗时(高管) -def querystring_procinst_duration_by_senior(start, end): - return ''' -SELECT ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department -from ( - SELECT user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - GROUP by r2.user_name -) as r3 - '''.format(querystring_senior_procinst_duration(start, end)) - - -# 用户单流程处理耗时(高管) -def querystring_procinst_duration_by_user_senior(start, end): +# 用户处理流程耗时+用户处理流程条数 +def querystring_procinst_by_user(start, end, senior=False): return ''' SELECT user_name, + count(*) as procinst_count_by_user, ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user from ({}) as r2 -GROUP by r2.user_name -order by procinst_duration_in_hours_by_user DESC - '''.format(querystring_senior_procinst_duration(start, end)) - - -# 各部门耗时较长用户(高管) -def querystring_user_gt_senior_avg(start, end, avg): - return ''' -SELECT * from ( - SELECT user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - GROUP by r2.user_name -) as r3 -WHERE r3.procinst_duration_in_hours_by_user > {} +group by r2.user_name order by procinst_duration_in_hours_by_user DESC - '''.format(querystring_senior_procinst_duration(start, end), avg) + '''.format(querystring_user_procinst_duration(start, end, senior)) -# 各部门耗时较长用户 -def querystring_user_gt_department_avg(start, end, department, avg): +# 各部门用户处理流程耗时+各部门用户处理流程条数 +def querystring_procinst_by_department_user(start, end): return """ -SELECT * from ( - SELECT user_name, - ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user - from ({}) as r2 - group by r2.user_name -) as r3 -WHERE r3.procinst_duration_in_hours_by_user > {} -order by procinst_duration_in_hours_by_user desc - """.format(querystring_user_procinst_duration(start, end, True, department), avg) - - -# 按部门统计用户单流程处理耗时 -def querystring_procinst_duration_by_user(start, end, department): - return """ -SELECT user_name, +SELECT department_name, + user_name, + count(*) as procinst_count_by_user, ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user from ({}) as r2 -group by r2.user_name +group by r2.department_name, r2.user_name order by procinst_duration_in_hours_by_user DESC - """.format(querystring_user_procinst_duration(start, end, True, department)) - - -# 按部门统计用户处理流程数量 -def querystring_procinst_count_by_user(start, end, department): - return ''' -SELECT user_name, - COUNT(*) as procinst_count_by_user -from ({}) as r2 -group by r2.user_name - '''.format(querystring_user_procinst_duration(start, end, True, department)) - - -# 用户处理流程数量(高管) -def querystring_procinst_count_by_senior(start, end): - return ''' -SELECT user_name, - COUNT(*) as procinst_count_by_user -from ({}) as r2 -group by r2.user_name - '''.format(querystring_senior_procinst_duration(start, end)) + """.format(querystring_department_user_procinst_duration(start, end, True)) diff --git a/code/pep-stats-report/config.ini b/code/pep-stats-report/config.ini index 9cda0fd..d5611ca 100644 --- a/code/pep-stats-report/config.ini +++ b/code/pep-stats-report/config.ini @@ -1,9 +1,9 @@ [clickhouse] -host = 10.8.30.161 +host = 10.8.30.95 port = 30900 username = default password = -database = pepca9 +database = pepca [qiniu] access-key=5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu diff --git a/code/pep-stats-report/main.py b/code/pep-stats-report/main.py index efacb36..4e23b8d 100644 --- a/code/pep-stats-report/main.py +++ b/code/pep-stats-report/main.py @@ -1,12 +1,17 @@ import os import datetime import configparser +import math +from decimal import Decimal, ROUND_HALF_UP from clickhouse_driver import Client from docx import Document -from docx.shared import Inches, Pt, RGBColor +from docx.shared import Inches, Cm, Pt, RGBColor from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING -from docx.oxml.ns import qn +from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT +from docx.oxml.ns import qn, nsdecls +from docx.oxml import parse_xml import matplotlib.pyplot as plt +import matplotlib.ticker as ticker import numpy as np from app import db_helper, image_helper import qiniu @@ -16,6 +21,46 @@ REPORT_DIR = 'output' LOG_DIR = 'log' +# 小于十万的整数转换 +def number_to_chinese(number): + chinese_digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'] + chinese_units = ['', '十', '百', '千', '万'] + + if number == 0: + return chinese_digits[0] + + result = '' + unit_index = 0 + last_digit_is_zero = False # 记录上一位数字是否为零 + while number > 0: + digit = number % 10 + if digit != 0: + if last_digit_is_zero: + result = chinese_digits[0] + result # 添加零 + result = chinese_digits[digit] + chinese_units[unit_index] + result + last_digit_is_zero = False + else: + # 如果当前位是零,并且结果不为空,则记录上一位为零 + if result != '': + last_digit_is_zero = True + + unit_index += 1 + number //= 10 + + # 如果结果以'一十'开头,则去掉'一',只保留'十' + if result[:2] == chinese_digits[1] + chinese_units[1]: + result = result[1:] + + return result + + +def round_to_integer(v): + if math.isnan(v): + return v + integer_in_decimal = Decimal(v).quantize(Decimal('0'), rounding=ROUND_HALF_UP) # 取整,如:2.5 -> 3 (not 2), 3.5 -> 4 + return int(integer_in_decimal) + + def get_report_year_month(): # 获取上月的最后一天 last_day = datetime.datetime.now().replace(day=1) - datetime.timedelta(days=1) @@ -36,9 +81,9 @@ def get_report_start_end(): def set_paragraph_space(p): # 设置间距 - p.paragraph_format.space_before = 0 # 段前 - p.paragraph_format.space_after = 0 # 段后 - p.paragraph_format.line_spacing = Pt(20) # 设置段落行距为20磅 + p.paragraph_format.space_before = Pt(6) # 段前 + p.paragraph_format.space_after = Pt(6) # 段后 + p.paragraph_format.line_spacing = 1.5 # 设置段落行距:1.5倍行距 def set_paragraph_format(p): @@ -47,23 +92,23 @@ def set_paragraph_format(p): p.paragraph_format.first_line_indent = Inches(0.3) -def set_heading_format(h, r): +def set_heading_format(h, r, head_level=1): set_paragraph_space(h) r.font.name = 'Times New Roman' r._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') - r.font.size = Pt(10.5) # 五号 + r.font.size = Pt(16) if head_level == 1 else Pt(15) # 三号 -> Pt(16), 小三 -> Pt(15) r.font.color.rgb = RGBColor(0, 0, 0) r.bold = True def set_title_format(h, r): - h.paragraph_format.space_before = Pt(12) # 段前1行 - h.paragraph_format.space_after = Pt(12) # 段后1行 - h.paragraph_format.line_spacing = Pt(20) # 设置段落行距为20磅 + h.paragraph_format.space_before = Pt(6) # 段前0.5行 + h.paragraph_format.space_after = Pt(6) # 段后0.5行 + h.paragraph_format.line_spacing = 1.5 # 1.5倍行距 h.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER r.font.name = u'Times New Roman' r._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') - r.font.size = Pt(10.5) # 五号 + r.font.size = Pt(18) # 小二 r.font.color.rgb = RGBColor(0, 0, 0) r.bold = True @@ -72,206 +117,287 @@ def set_global_normal_style(doc): style = doc.styles['Normal'] style.font.name = 'Times New Roman' # 英文、数字字体 style._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') # 中文字体 - style.font.size = Pt(10.5) # 五号 - style.paragraph_format.space_before = 0 # 段前 - style.paragraph_format.space_after = 0 # 段后 - style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE # 单倍行距 + style.font.size = Pt(14) # 四号 + style.paragraph_format.space_before = Pt(6) # 段前0.5行 + style.paragraph_format.space_after = Pt(6) # 段后0.5行 + style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE # 1.5倍行距 -def create_bar_chart(doc, x, y, mean, title, filename): - plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置字体 - plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 +# 设置单元格底纹 +def set_table_cell_shading_color(table_cell, color): + shading_elm = parse_xml(r''.format(nsdecls('w'), bgColor=color)) + table_cell._tc.get_or_add_tcPr().append(shading_elm) - if title == '部门流程处理平均耗时': - plt.figure(figsize=(12, 6), layout='tight') - plt.margins(x=0.01) - else: - plt.figure(figsize=(6.4, 3.4), layout='tight') - # 绘制柱状图 - plt.bar(x, y, color=(31 / 255, 168 / 255, 201 / 255)) - # 绘制均值线 - plt.axhline(mean, linestyle='dashed', color='#FF8C00') - # 添加标题 - plt.title(title) - # 设置x轴标签旋转角度 - plt.xticks(rotation=-30, ha='left') - # 添加图例 - plt.legend(['均值:{}'.format(mean)], loc='upper right') - # 保存图形 - plt.savefig('{}/{}'.format(REPORT_DIR, filename)) - plt.close() - # 插入图形到 Word 文档中 - p = doc.add_paragraph() - p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER - r = p.add_run() - r.add_picture('{}/{}'.format(REPORT_DIR, filename), width=Inches(6)) +# 设置表格行底纹 +def set_table_row_shading_color(table_row, color): + shading_dict = {} + for i, cell in enumerate(table_row.cells): + shading_dict['shading_elm_' + str(i)] = parse_xml(r''.format(nsdecls('w'), bgColor=color)) + cell._tc.get_or_add_tcPr().append(shading_dict['shading_elm_' + str(i)]) + + +def add_heading(doc, head_level, head_content): + h = doc.add_heading(level=head_level) + r = h.add_run(head_content) + set_heading_format(h, r, head_level) -def create_bar_twinx_chart(doc, x, y1, y2, mean, title, filename): +def create_twinx_chart(department, title, filename): plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置字体 plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 + x = [] + y1 = [] + y2 = [] + for user in department['d_users']: + x.append(user[0]) # 用户名 + y1.append(user[2]) # 用户处理流程耗时 + y2.append(user[1]) # 用户处理流程条数 + x_range = np.arange(len(x)) - width = 0.25 # the width of the bars + bgcolor = '#d9d9d9' - fig, ax1 = plt.subplots() - ax1.set_ylabel('耗时') - rects = ax1.bar(x_range, y1, width, color=(31 / 255, 168 / 255, 201 / 255), label='耗时') - ax1.bar_label(rects, padding=3) - ax1.set_xticks(x_range + width / 2, x) + fig, ax1 = plt.subplots(figsize=(8, 5), layout='constrained', facecolor=bgcolor) + ax1.set_ylabel('平均处理流程时间(小时)') + y1_bar = ax1.bar(x_range, y1, width=0.25, label='流程处理时间', color=(91 / 255, 155 / 255, 213 / 255)) + + ax1.set_xticks(x_range, x) # 设置x轴标签旋转角度 plt.xticks(rotation=-30, ha='left') - # 绘制均值线 - plt.axhline(mean, linestyle='dashed', color='#FF8C00', label=f'均值:{mean}') ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis - ax2.set_ylabel('流程数') - rects = ax2.bar(x_range + width, y2, width, color='#5AC189', label='流程数') - ax2.bar_label(rects, padding=3) + y2_line, = ax2.plot(x_range, y2, label='处理流程条数', color='#ff0000', marker='o', linestyle='dashed', linewidth=2) + ax2.set_ylabel('处理流程条数(条)') + ax2.set_ylim(0, max(y2) + 1) + ax2.yaxis.set_major_locator(ticker.MaxNLocator(integer=True)) # 设置整数刻度 + + annotate_kwargs = {'color': '#ffffff', 'arrowprops': dict(color='#a5a5a5', arrowstyle='-')} + + # y2_line:添加数据标签 + # UserWarning: constrained_layout not applied because axes sizes collapsed to zero. + for x, y in zip(x_range, y2): + xtext_offset = 0.01 if len(x_range) == 1 else 0.1 + ax2.annotate(f"{y}", xy=(x, y), xytext=(x + xtext_offset, y + 0.1), backgroundcolor='#000000', **annotate_kwargs) + + # 绘制均值线:相对于左侧坐标轴"平均处理流程时间(小时)"绘制 + hline_kwargs = {'linestyle': 'solid', 'linewidth': 2} + axhline1 = ax1.axhline(department['d_elapse'], label='部门平均时间', color='#ed7d31', **hline_kwargs) + axhline2 = ax1.axhline(procinst_duration_by_company, label='公司平均时间', color='#a5a5a5', **hline_kwargs) + + # 获取 y1 轴两个刻度之间的距离 + y1_ticks = ax1.get_yticks() + y1_ticks_distance = y1_ticks[1] - y1_ticks[0] if len(y1_ticks) > 1 else y1_ticks[0] + + # y1_bar:添加数据标签 + bar_label_kwargs = {'color': '#ffffff', 'backgroundcolor': '#8ba7de'} + y1_threshold = y1_ticks_distance / 2 + y1_bar_large = [v if v > y1_threshold else '' for v in y1] + y1_bar_small = [v if v <= y1_threshold else '' for v in y1] + ax1.bar_label(y1_bar, y1_bar_large, padding=-20, **bar_label_kwargs) + ax1.bar_label(y1_bar, y1_bar_small, padding=3, **bar_label_kwargs) + + # 均值线:添加数据标签 + x_pos = 0 if len(x_range) == 1 else 1 + xtext_benchmark = 0 if len(x_range) == 1 else 1 + xtext_offset = 0.03 if len(x_range) == 1 else 0.3 + ytext_offset = y1_ticks_distance / 3 + ax1.annotate(f"{department['d_elapse']}", xy=(x_pos, department['d_elapse']), + xytext=(xtext_benchmark - xtext_offset, department['d_elapse'] + ytext_offset), + backgroundcolor='#f5b482', **annotate_kwargs) + ax1.annotate(f"{procinst_duration_by_company}", xy=(x_pos, procinst_duration_by_company), + xytext=(xtext_benchmark + xtext_offset, procinst_duration_by_company + ytext_offset), + backgroundcolor='#ef95a0', **annotate_kwargs) + + plt.title(title, fontsize=14, pad=30) # pad: 标题与坐标轴顶部的偏移量 + fig.legend(handles=[y1_bar, axhline1, axhline2, y2_line], loc='outside lower center', ncols=4, facecolor=bgcolor, edgecolor=bgcolor) + + ax1.set_xlabel(department['d_name'], fontsize=12, labelpad=8) # 在图形下方显示部门名称 + ax1.set_facecolor(bgcolor) # 设置绘图区域的背景色 + # 去掉绘图区域的边框线和坐标轴刻度线 + axes = [ax1, ax2] + for ax in axes: + ax.spines['top'].set_visible(False) + ax.spines['bottom'].set_visible(False) + ax.spines['left'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.tick_params(axis='both', which='both', length=0) + + # 设置绘图区与图片边缘的距离 + # plt.subplots_adjust(left=0.08, right=0.94, top=0.9, bottom=0.24) - plt.title(title) - fig.legend(loc='upper right', ncols=1, bbox_to_anchor=(1, 1), bbox_transform=ax1.transAxes) - fig.tight_layout() # otherwise the right y-label is slightly clipped # 保存图形 plt.savefig('{}/{}'.format(REPORT_DIR, filename)) plt.close() - # 插入图形到 Word 文档中 - p = doc.add_paragraph() - p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER - r = p.add_run() - r.add_picture('{}/{}'.format(REPORT_DIR, filename), width=Inches(6)) -def add_section(doc, rank, department, d_elapse): - h = doc.add_heading(level=2) - r = h.add_run('{}. {}'.format(rank, department)) - set_heading_format(h, r) - - try: - qs = db_helper.querystring_user_gt_senior_avg( - start_time, end_time, d_elapse - ) if department == '高管' else db_helper.querystring_user_gt_department_avg( - start_time, end_time, department, d_elapse) - user_gt_department_avg = client.execute(qs) - - qs = db_helper.querystring_procinst_duration_by_user_senior( - start_time, end_time - ) if department == '高管' else db_helper.querystring_procinst_duration_by_user( - start_time, end_time, department) - procinst_duration_by_user = client.execute(qs) - - qs = db_helper.querystring_procinst_count_by_senior( - start_time, end_time - ) if department == '高管' else db_helper.querystring_procinst_count_by_user( - start_time, end_time, department) - procinst_count_by_user = client.execute(qs) - except Exception as error: - print('数据库查询错误:', error) - logging.error('数据库查询错误:{}'.format(error)) - raise RuntimeError('数据库查询错误:', error) +def add_section(doc, department_data): + d_name = department_data['d_name'] + d_elapse = department_data['d_elapse'] + d_elapse_rank = department_data['d_elapse_rank'] + d_count = department_data['d_count'] + d_count_rank = department_data['d_count_rank'] + d_users = department_data['d_users'] - users = [] - for user, _ in user_gt_department_avg: - users.append(user) + d_users_gt_company_avg_duration = [u[0] for u in d_users if u[2] > procinst_duration_by_company] - p = doc.add_paragraph() - p.add_run('{}{}月份个人处理流程平均耗时时长在公司排名为'.format(department, last_month)) - p.add_run('{}'.format(rank)).underline = True - if len(procinst_duration_by_user) == 1: - p.add_run(',{}公司平均水平。'.format( - '高于' if d_elapse > procinst_duration_by_company else - '低于' if d_elapse < procinst_duration_by_company else '等于')) - else: - p.add_run(',其中耗时较长的有') - p.add_run('{}'.format('、'.join(users))).underline = True - p.add_run(',{}({}小时/流程)。' - .format('高于公司平均水平和部门平均水平' if d_elapse >= procinst_duration_by_company else '低于公司平均水平但高于部门平均水平', - d_elapse)) - set_paragraph_format(p) + add_heading(doc, 2, '({}){}'.format(number_to_chinese(d_elapse_rank), d_name)) - x = [] - y1 = [] - y2 = [] + p = doc.add_paragraph(f'部门人均处理流程平均耗时时长排名:{d_elapse_rank};', style='List Bullet') + set_paragraph_space(p) - for user, elapse in procinst_duration_by_user: - x.append(user) - y1.append(elapse) + operator = '>' if d_elapse > procinst_duration_by_company else '<' if d_elapse < procinst_duration_by_company else '==' + p = doc.add_paragraph(f'部门处理流程平均耗时时长:{d_elapse}小时{operator}公司平均{procinst_duration_by_company}小时;', style='List Bullet') + set_paragraph_space(p) - for name in x: - for user, count in procinst_count_by_user: - if user == name: - y2.append(count) - break + users_count = len(d_users_gt_company_avg_duration) + p = doc.add_paragraph(style='List Bullet') + p.add_run(f'部门处理流程平均耗时大于公司平均水平的有{users_count}人') + p.add_run(f":{'、'.join(d_users_gt_company_avg_duration)};" if users_count > 0 else ";") + set_paragraph_space(p) - create_bar_twinx_chart(doc, x, y1, y2, d_elapse, "个人流程处理平均耗时", "{}-流程处理平均耗时.png".format(department)) - caption = doc.add_paragraph('图 2-{} {}个人处理流程平均耗时'.format(rank, department)) - caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + p = doc.add_paragraph(f'人均处理流程条数排名:{d_count_rank};', style='List Bullet') + set_paragraph_space(p) + operator = '>' if d_count > procinst_count_by_company else '<' if d_count < procinst_count_by_company else '==' + p = doc.add_paragraph(f'人均处理流程条数:{d_count}条{operator}公司平均{procinst_count_by_company}条;', style='List Bullet') + set_paragraph_space(p) -def add_chapter_2(doc): - h = doc.add_heading(level=1) - r = h.add_run('二、各部门成员详情') - set_heading_format(h, r) + pic_name = f"{d_name}-平均处理流程时间及流程处理条数.png" + create_twinx_chart(department_data, "部门成员平均处理流程时间及流程处理条数", pic_name) - departments_excluded = ['汇派-质量部', '汇派-生产部', '汇派-计划部', '汇派-人事部', '汇派-采购部', '党建工会', '工程项目中心', '北京技术中心'] + # 插入图形到 Word 文档中 + p = doc.add_paragraph() + p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run() + r.add_picture('{}/{}'.format(REPORT_DIR, pic_name), width=Inches(6)) - d = [] - for department, elapse in procinst_duration_by_department: - if department not in departments_excluded: - d.append((department, elapse)) - for index, (department, elapse) in enumerate(d): - add_section(doc, index + 1, department, elapse) +def add_chapter_2(doc): + add_heading(doc, 1, '二、各部门成员详情') + + # 部门流程处理条数排行 + department_rank_by_count = [item[0] for item in procinst_by_department] # procinst_by_department 默认按照流程条数倒序排列 + + # 部门流程处理耗时排行 + department_sorted_by_elapse = sorted(procinst_by_department, key=lambda d: d[2], reverse=True) + department_rank_by_elapse = [item[0] for item in department_sorted_by_elapse] + + for item in department_sorted_by_elapse: + department = item[0] + d_count = item[1] + d_elapse = item[2] + d_count_rank = department_rank_by_count.index(department) + 1 + d_elapse_rank = department_rank_by_elapse.index(department) + 1 + + add_section(doc, {'d_name': department, + 'd_elapse': d_elapse, + 'd_elapse_rank': d_elapse_rank, + 'd_count': d_count, + 'd_count_rank': d_count_rank, + 'd_users': item[3]}) + + +def insert_table(doc, table_headers, col_width_dict, table_data, table_style): + # 在文档中添加一个空表格 + table = doc.add_table(rows=0, cols=len(table_headers), style='Table Grid') + + # 添加表头 + heading_cells = table.add_row().cells + for i, header in enumerate(table_headers): + heading_cells[i].text = header + heading_cells[i].paragraphs[0].runs[0].bold = True # 字体加粗 + + # 循环添加数据 + avg_row_added = False + for i, row_data in enumerate(table_data): + if table_style in ['style2', 'style3']: + avg_value = {'style2': procinst_duration_by_company, 'style3': procinst_count_by_company} + if not avg_row_added and row_data[1] < avg_value[table_style]: + # 添加“平均”值行 + row = table.add_row() + row_cells = row.cells + row_cells[0].merge(row_cells[1]).text = '平均' # 合并单元格 + row_cells[2].text = str(avg_value[table_style]) + set_table_row_shading_color(row, 'ff0000') + row_cells[0].paragraphs[0].runs[0].bold = True # 字体加粗 + row_cells[2].paragraphs[0].runs[0].bold = True + avg_row_added = True + + row = table.add_row() + row_cells = row.cells + + row_cells[0].text = str(i + 1) + for j, cell_value in enumerate(row_data): + col_index = j + 1 + row_cells[col_index].text = str(cell_value) + + if table_style == 'style1': + if row_data[1] >= procinst_count_by_company: + set_table_cell_shading_color(row_cells[2], '#fedb61') + if row_data[2] >= procinst_duration_by_company: + set_table_cell_shading_color(row_cells[3], '#ff0000') + elif table_style == 'style2' and row_data[1] >= procinst_duration_by_company: + set_table_row_shading_color(row, 'fedb61') + + if table_style == 'style1': + # 统计值:平均值 + row_cells = table.add_row().cells + avg_data = [len(table_data) + 1, '公司平均', procinst_count_by_company, procinst_duration_by_company] + for i, cell_value in enumerate(avg_data): + row_cells[i].text = str(cell_value) + row_cells[i].paragraphs[0].runs[0].bold = True # 字体加粗 + + # 设置表格垂直对齐方式 + table.alignment = WD_TABLE_ALIGNMENT.CENTER + + # 设置单元格样式 + for row in table.rows: + for cell in row.cells: + cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER # 垂直居中 + cell.paragraphs[0].alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 水平居中 + for col_index, col_width in col_width_dict.items(): # 设置列宽 + row.cells[col_index].width = Cm(col_width) + row.height = Cm(0.7) # 设置行高 def add_chapter_1(doc): - h = doc.add_heading(level=1) - r = h.add_run('一、公司各部门横向比较情况') - set_heading_format(h, r) - try: qs = db_helper.querystring_user_count(start_time, end_time) user_count = client.execute(qs)[0][0] - - qs = db_helper.querystring_department_count_gt_avg(start_time, end_time, procinst_duration_by_company) - department_count_gt_avg = client.execute(qs)[0][0] - - qs = db_helper.querystring_user_count_gt_avg(start_time, end_time, procinst_duration_by_company) - user_count_gt_avg = client.execute(qs)[0][0] except Exception as error: print('数据库查询错误:', error) logging.error('数据库查询错误:{}'.format(error)) raise RuntimeError('数据库查询错误:', error) - if procinst_duration_by_senior is not None and procinst_duration_by_senior > procinst_duration_by_company: - department_count_gt_avg += 1 + department_count_gt_avg_count = len(list(filter(lambda d: d[1] > procinst_count_by_company, procinst_by_department))) + department_count_gt_avg_duration = len(list(filter(lambda d: d[2] > procinst_duration_by_company, procinst_by_department))) + user_count_gt_avg_count = len(list(filter(lambda d: d[1] > procinst_count_by_company, procinst_by_user))) + user_count_gt_avg_duration = len(list(filter(lambda d: d[2] > procinst_duration_by_company, procinst_by_user))) + add_heading(doc, 1, '一、公司各部门横向比较情况') + add_heading(doc, 2, '(一)一览表') p = doc.add_paragraph() - p.add_run('{}年{}月,公司共计'.format(last_year, last_month)) - p.add_run('{}'.format(user_count)).underline = True - p.add_run('人使用项企平台,个人处理流程平均耗时') - p.add_run('{}'.format(procinst_duration_by_company)).underline = True - p.add_run('小时/流程,其中,平均单一流程处理耗时高于公司平均数的有') - p.add_run('{}'.format(department_count_gt_avg)).underline = True - p.add_run('个部门,') - p.add_run('{}'.format(user_count_gt_avg)).underline = True - p.add_run('人。') + p.add_run('{}年{}月,公司共计{}人使用项企平台,'.format(last_year, last_month, user_count)) + p.add_run('个人处理流程平均耗时{}小时/流程,平均单一流程处理耗时高于公司平均数的有{}个部门,{}人。'.format( + procinst_duration_by_company, department_count_gt_avg_duration, user_count_gt_avg_duration)) + p.add_run('个人处理流程平均条数为{}条,个人处理流程平均条数高于公司平均数的有{}个部门,{}人。'.format( + procinst_count_by_company, department_count_gt_avg_count, user_count_gt_avg_count)) set_paragraph_format(p) - # 准备数据 - x = [] - y = [] - for department, elapse in procinst_duration_by_department: - x.append(department) - y.append(elapse) + table_data = list(map(lambda d: (d[0], d[1], d[2]), procinst_by_department)) + insert_table(doc, ['序号', '部门', '人均处理流程条数', '平均单条流程处理时间'], {0: 1.5, 1: 5.8, 2: 3.6, 3: 4.3}, table_data, 'style1') - # 创建柱状图并保存为文件 - create_bar_chart(doc, x, y, procinst_duration_by_company, "部门流程处理平均耗时", "部门流程处理平均耗时.png") - caption = doc.add_paragraph('图 1 部门流程处理平均耗时') - caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + add_heading(doc, 2, '(二)部门平均单一流程处理耗时排行') + table_data = list(map(lambda d: (d[0], d[2]), procinst_by_department)) + table_data.sort(key=lambda d: d[1], reverse=True) + insert_table(doc, ['排名', '部门', '平均单条流程处理时间'], {0: 1.5, 1: 5.8, 2: 4.3}, table_data, 'style2') + + add_heading(doc, 2, '(三)部门人均处理流程平均条数排行') + table_data = list(map(lambda d: (d[0], d[1]), procinst_by_department)) + insert_table(doc, ['排名', '部门', '人均处理流程条数'], {0: 1.5, 1: 5.8, 2: 4.3}, table_data, 'style3') def upload_to_qiniu(filename): @@ -321,14 +447,15 @@ def generate_word_report(): # 添加标题 h = doc.add_heading(level=0) - r = h.add_run('项企流程效能分析结果公示') + r = h.add_run('{}月项企流程效能分析结果公示'.format(last_month)) set_title_format(h, r) # 添加段落 p_first = doc.add_paragraph( - '为了提升公司整体工作效率,确保跨部门工作高效推进,充分发挥我司自研平台的优势,现基于项企系统数据,利用Superset工具打造了项企流程效能分析平台,能够对我司项企平台的数据进行分析,进而通过量化数据发现我司办公流程流转中的问题,' \ - '现将{}年{}月公司员工处理流程平均响应时长(响应时长为从流程流转到当前节点到当前节点处理完成的耗时,单位:小时)情况结果进行公示如下:' - .format(last_year, last_month)) + '为了提升公司整体工作效率,确保跨部门工作高效推进,充分发挥我司自研平台的优势,现基于项企系统数据对我司项企平台的使用数据进行分析,' + '进而通过量化数据发现我司办公流程流转中的问题,' + '现将{}年{}月公司员工处理流程平均响应时长(响应时长为从流程流转到当前节点到当前节点处理完成的耗时,单位:小时)' + '以及处理流程条数情况结果进行公示如下:'.format(last_year, last_month)) set_paragraph_format(p_first) # 一、公司各部门横向比较情况 @@ -338,11 +465,13 @@ def generate_word_report(): add_chapter_2(doc) p_last = doc.add_paragraph( - '现项企业流程效能分析是公司管理工具逐步完善尝试和摸索的阶段,只是单一评估维度,大家若对评估维度有相应意见,欢迎积极提出,共同促进项企流程效能分析不断完善,进而提升公司整体效率!') + '针对以上数据,请各部门负责人认真对待并针对部门情况进行分析。' + '现项企业流程效能分析是公司管理工具逐步完善尝试和摸索的阶段,评估维度、呈现形式均在调整优化中,' + '欢迎大家对效能评估维提出建设意见,共同促进项企流程效能分析不断完善,进而提升公司整体效率!') set_paragraph_format(p_last) # 保存文档 - report_file_name = '项企流程效能分析结果公示(全员)-{}年{}月.docx'.format(last_year, last_month) + report_file_name = '项企流程效能分析结果公示-{}年{}月.docx'.format(last_year, last_month) report_file = f'{REPORT_DIR}/{report_file_name}' doc.save(report_file) logging.info('本地报表创建成功:{}'.format(report_file)) @@ -375,6 +504,41 @@ def try_create_dir(dir_name): print(f"目录'{dir_name}'创建成功!") +def generate_procinst_by_department(procinst_by_senior, procinst_by_department_user): + departments = {} # 存储每个部门的信息 + + procinst_by_senior_user = [('高管', item[0], item[1], item[2]) for item in procinst_by_senior] + all_departments = procinst_by_department_user + procinst_by_senior_user + + for item in all_departments: + department = item[0] + user_name = item[1] + procinst_count_by_user = item[2] + procinst_duration_by_user = item[3] + + if department not in departments: + departments[department] = { + 'd_users': [], + 'd_count': [], + 'd_duration': [] + } + + departments[department]['d_users'].append((user_name, procinst_count_by_user, procinst_duration_by_user)) + departments[department]['d_count'].append(procinst_count_by_user) + departments[department]['d_duration'].append(procinst_duration_by_user) + + rslt = [] + + for department, info in departments.items(): + d_count_average = round_to_integer(np.mean(info['d_count'])) + d_duration_average = round(np.mean(info['d_duration']), 2) + rslt.append((department, d_count_average, d_duration_average, info['d_users'])) + + rslt.sort(key=lambda d: d[1], reverse=True) # 按照部门处理流程条数倒序排列 + + return rslt + + if __name__ == '__main__': try: try_create_dir(REPORT_DIR) @@ -391,19 +555,14 @@ if __name__ == '__main__': last_year, last_month = get_report_year_month() start_time, end_time = get_report_start_end() - qs1 = db_helper.querystring_procinst_duration_by_company(start_time, end_time) - procinst_duration_by_company = client.execute(qs1)[0][0] - - qs2 = db_helper.querystring_procinst_duration_by_department(start_time, end_time) - procinst_duration_by_department = client.execute(qs2) - - qs3 = db_helper.querystring_procinst_duration_by_senior(start_time, end_time) - procinst_duration_by_senior = client.execute(qs3)[0][0] + qs1 = db_helper.querystring_procinst_by_user(start_time, end_time) + procinst_by_user = client.execute(qs1) + procinst_count_by_company = round_to_integer(np.mean([item[1] for item in procinst_by_user])) + procinst_duration_by_company = round(np.mean([item[2] for item in procinst_by_user]), 2) - for index, (department, elapse) in enumerate(procinst_duration_by_department): - if procinst_duration_by_senior is not None and procinst_duration_by_senior >= elapse: - procinst_duration_by_department.insert(index, ('高管', procinst_duration_by_senior)) - break + qs2 = db_helper.querystring_procinst_by_user(start_time, end_time, senior=True) + qs3 = db_helper.querystring_procinst_by_department_user(start_time, end_time) + procinst_by_department = generate_procinst_by_department(client.execute(qs2), client.execute(qs3)) generate_word_report() # 生成报表文件 except Exception as error: diff --git a/code/pep-stats-report/requirements.txt b/code/pep-stats-report/requirements.txt index e91d0da..6e2eda5 100644 --- a/code/pep-stats-report/requirements.txt +++ b/code/pep-stats-report/requirements.txt @@ -11,7 +11,7 @@ fonttools==4.39.3 idna==3.4 importlib-resources==5.12.0 kiwisolver==1.4.4 -lxml==4.9.2 +lxml==4.9.3 matplotlib==3.7.1 numpy==1.24.2 packaging==23.0 @@ -22,7 +22,7 @@ pyinstaller-hooks-contrib==2023.2 PyMuPDF==1.22.3 pyparsing==3.0.9 python-dateutil==2.8.2 -python-docx==0.8.11 +python-docx==1.1.0 pytz==2023.3 pytz-deprecation-shim==0.1.0.post0 pywin32==306 @@ -31,6 +31,7 @@ qiniu==7.10.0 requests==2.28.2 self==2020.12.3 six==1.16.0 +typing_extensions==4.8.0 tzdata==2023.3 tzlocal==4.3 urllib3==1.26.15 diff --git a/code/pep-stats-report/requirements_linux.txt b/code/pep-stats-report/requirements_linux.txt index 05d878e..582cf08 100644 --- a/code/pep-stats-report/requirements_linux.txt +++ b/code/pep-stats-report/requirements_linux.txt @@ -11,7 +11,7 @@ fonttools==4.39.3 idna==3.4 importlib-resources==5.12.0 kiwisolver==1.4.4 -lxml==4.9.2 +lxml==4.9.3 matplotlib==3.7.1 numpy==1.24.2 packaging==23.0 @@ -22,7 +22,7 @@ pyinstaller-hooks-contrib==2023.2 PyMuPDF==1.22.3 pyparsing==3.0.9 python-dateutil==2.8.2 -python-docx==0.8.11 +python-docx==1.1.0 pytz==2023.3 pytz-deprecation-shim==0.1.0.post0 pywin32-ctypes==0.2.0 @@ -30,6 +30,7 @@ qiniu==7.10.0 requests==2.28.2 self==2020.12.3 six==1.16.0 +typing_extensions==4.8.0 tzdata==2023.3 tzlocal==4.3 urllib3==1.26.15