Browse Source

项企效能统计分析工具:生成《项企流程效能分析结果公示(全员)》报表

main
Julin 2 years ago
parent
commit
47fc52eda4
  1. 21
      code/pep-stats-report/README.md
  2. 184
      code/pep-stats-report/app/db_helper.py
  3. 12
      code/pep-stats-report/config.ini
  4. 346
      code/pep-stats-report/main.py
  5. 36
      code/pep-stats-report/requirements.txt

21
code/pep-stats-report/README.md

@ -0,0 +1,21 @@
## 配置文件
`config.ini`为项目配置文件。
- `[clickhouse]`配置节为 ClickHouse 数据库的配置信息:
- `port`为数据库的`TCP`端口。
- `password`为数据库密码,如果没有密码,则不需要配置任何值。
- `[qiniu]`配置节为七牛云的配置信息:
- `access-key`和`secret-key`为七牛云密钥。
- `bucket`为七牛云存储空间。
- `domain`为存储空间对应的外链域名。
## requirements.txt
项目所需的依赖库。
```
pip freeze > requirements.txt
```
## 打包`.exe`文件
使用`--distpath`选项指定输出目录的路径。例如,将`.exe`文件输出至当前文件夹下:
```
(venv) E:\WorkSpace\FS-Git\fs-pep-stats-report>pyinstaller -F main.py --distpath=.
```

184
code/pep-stats-report/app/db_helper.py

@ -0,0 +1,184 @@
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 '''
select department_name,
user_name,
procinst_id,
sum(duration_in_minutes) as procinst_duration_in_minutes
from (
select distinct d.id as department_id,
d.name as department_name,
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 >='{}' 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
{} {}
) 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 '''
SELECT user_name,
ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user
from (
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 ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥')
) as r
group by user_name, procinst_id
) as r2
GROUP by r2.user_name
'''.format(start, end)
# 按月统计项企平台使用人数
def querystring_user_count(start, end):
return '''
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, ''))
# 部门数(高于公司平均数)
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 ({}) as r3
'''.format(querystring_senior_procinst_duration(start, end))
# 用户单流程处理耗时(高管)
def querystring_procinst_duration_by_user_senior(start, end):
return '''
{}
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 ({}) as r3
WHERE r3.procinst_duration_in_hours_by_user > {}
order by procinst_duration_in_hours_by_user DESC
'''.format(querystring_senior_procinst_duration(start, end), avg)
# 各部门耗时较长用户
def querystring_user_gt_department_avg(start, end, department, 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 > {}
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,
ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user
from ({}) as r2
group by r2.user_name
""".format(querystring_user_procinst_duration(start, end, True, department))

12
code/pep-stats-report/config.ini

@ -0,0 +1,12 @@
[clickhouse]
host = 10.8.30.161
port = 30900
username = default
password =
database = pepca8
[qiniu]
access-key=5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu
secret-key=w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5
bucket=pep-resource
domain=https://pepsource.anxinyun.cn

346
code/pep-stats-report/main.py

@ -0,0 +1,346 @@
import os
import datetime
import configparser
from clickhouse_driver import Client
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING
from docx.oxml.ns import qn
import matplotlib.pyplot as plt
from app import db_helper
import qiniu
import logging
REPORT_DIR = 'output'
LOG_DIR = 'log'
def get_report_year_month():
# 获取上月的最后一天
last_day = datetime.datetime.now().replace(day=1) - datetime.timedelta(days=1)
return last_day.year, last_day.month
def get_report_start_end():
# 获取当前日期
now = datetime.datetime.now()
# 获取本月第一天
this_month_start = datetime.datetime(now.year, now.month, 1)
# 获取上个月最后一天
last_month_end = this_month_start - datetime.timedelta(days=1)
# 获取上个月第一天
last_month_start = datetime.datetime(last_month_end.year, last_month_end.month, 1)
return last_month_start, this_month_start
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磅
def set_paragraph_format(p):
set_paragraph_space(p)
# 设置首行缩进2个字符
p.paragraph_format.first_line_indent = Inches(0.3)
def set_heading_format(h, r):
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.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.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.color.rgb = RGBColor(0, 0, 0)
r.bold = True
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 # 单倍行距
def create_bar_chart(doc, x, y, mean, title, filename):
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置字体
plt.rcParams["axes.unicode_minus"] = False # 正常显示负号
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 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)
except Exception as error:
print('数据库查询错误:', error)
logging.error('数据库查询错误:{}'.format(error))
raise RuntimeError('数据库查询错误:', error)
users = []
for user, _ in user_gt_department_avg:
users.append(user)
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)
x = []
y = []
for user, elapse in procinst_duration_by_user:
x.append(user)
y.append(elapse)
create_bar_chart(doc, x, y, d_elapse, "个人流程处理平均耗时", "{}-流程处理平均耗时.png".format(department))
caption = doc.add_paragraph('图 2-{} {}个人处理流程平均耗时'.format(rank, department))
caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
def add_chapter_2(doc):
h = doc.add_heading(level=1)
r = h.add_run('二、各部门成员详情')
set_heading_format(h, r)
departments_excluded = ['汇派-质量部', '汇派-生产部', '汇派-计划部', '汇派-人事部', '汇派-采购部', '党建工会', '工程项目中心', '北京技术中心']
for index, (department, elapse) in enumerate(procinst_duration_by_department):
if department not in departments_excluded:
add_section(doc, index + 1, department, elapse)
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 > procinst_duration_by_company:
department_count_gt_avg += 1
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('人。')
set_paragraph_format(p)
# 准备数据
x = []
y = []
for department, elapse in procinst_duration_by_department:
x.append(department)
y.append(elapse)
# 创建柱状图并保存为文件
create_bar_chart(doc, x, y, procinst_duration_by_company, "部门流程处理平均耗时", "部门流程处理平均耗时.png")
caption = doc.add_paragraph('图 1 部门流程处理平均耗时')
caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
def upload_to_qiniu(filename):
logging.info('上传报表至七牛云...')
# 获取 qiniu 配置参数
qn_cfg = config['qiniu']
access_key = qn_cfg['access-key']
secret_key = qn_cfg['secret-key']
bucket_name = qn_cfg['bucket']
q = qiniu.Auth(access_key, secret_key)
bucket = qiniu.BucketManager(q)
key = 'pep-stats-report/{}'.format(filename)
localfile = '{}/{}'.format(REPORT_DIR, filename)
ret, info = bucket.stat(bucket_name, key)
if info.status_code == 200:
print('文件已存在,删除文件...')
ret, info = bucket.delete(bucket_name, key)
if info.status_code == 200:
print('删除成功')
else:
print('删除失败')
print('上传文件...')
token = q.upload_token(bucket_name, key)
ret, info = qiniu.put_file(token, key, localfile)
if info.status_code == 200:
print('上传成功')
logging.info('上传成功')
# 获取文件访问 URL
url = '{}/{}?attname='.format(qn_cfg['domain'], ret['key'])
print('文件访问 URL:', url)
logging.info('文件访问 URL:{}'.format(url))
else:
print('上传失败,错误信息:', info.error)
logging.error('上传失败,错误信息:{}'.format(error))
def generate_word_report():
# 创建一个新的Word文档
doc = Document()
set_global_normal_style(doc)
# 添加标题
h = doc.add_heading(level=0)
r = h.add_run('项企流程效能分析结果公示')
set_title_format(h, r)
# 添加段落
p1 = doc.add_paragraph(
'为了提升公司整体工作效率,确保跨部门工作高效推进,充分发挥我司自研平台的优势,现基于项企系统数据,利用Superset工具打造了项企流程效能分析平台,能够对我司项企平台的数据进行分析,进而通过量化数据发现我司办公流程流转中的问题,' \
'现将{}{}月公司员工处理流程平均响应时长(响应时长为从流程流转到当前节点到当前节点处理完成的耗时,单位:小时)情况结果进行公示如下:'
.format(last_year, last_month))
set_paragraph_format(p1)
# 一、公司各部门横向比较情况
add_chapter_1(doc)
# 二、各部门成员详情
add_chapter_2(doc)
# 保存文档
report_path = '{}/项企流程效能分析结果公示(全员)-{}{}月.docx'.format(REPORT_DIR, last_year, last_month)
doc.save(report_path)
logging.info('本地报表创建成功:{}'.format(report_path))
# 上传七牛云
upload_to_qiniu('项企流程效能分析结果公示(全员)-{}{}月.docx'.format(last_year, last_month))
def create_clickhouse_client():
# 获取 clickhouse 配置参数
ch_cfg = config['clickhouse']
host = ch_cfg['host']
port = ch_cfg.getint('port')
username = ch_cfg['username']
password = ch_cfg['password']
database = ch_cfg['database']
# 创建 ClickHouse 客户端对象
return Client(host=host, port=port, user=username, password=password, database=database)
def try_create_dir(dir_name):
# 判断目录是否存在,如果不存在,则创建目录
if not os.path.exists(dir_name):
print(f"目录'{dir_name}'不存在,即将创建...")
os.makedirs(dir_name)
print(f"目录'{dir_name}'创建成功!")
if __name__ == '__main__':
try:
try_create_dir(REPORT_DIR)
try_create_dir(LOG_DIR)
logging.basicConfig(filename='{}/runtime.log'.format(LOG_DIR), level=logging.INFO,
format='%(asctime)s.%(msecs)03d - %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
config = configparser.ConfigParser()
config.read('config.ini')
client = create_clickhouse_client()
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]
for index, (department, elapse) in enumerate(procinst_duration_by_department):
if procinst_duration_by_senior >= elapse:
procinst_duration_by_department.insert(index, ('高管', procinst_duration_by_senior))
break
generate_word_report() # 生成报表文件
except Exception as error:
print('程序运行出错:', error)
logging.error('程序运行出错:{}'.format(error))
# input("请按任意键退出...")

36
code/pep-stats-report/requirements.txt

@ -0,0 +1,36 @@
altgraph==0.17.3
APScheduler==3.10.1
backports.zoneinfo==0.2.1
certifi==2022.12.7
charset-normalizer==3.1.0
clickhouse-driver==0.2.5
contourpy==1.0.7
cycler==0.11.0
decorator==5.1.1
fonttools==4.39.3
idna==3.4
importlib-resources==5.12.0
kiwisolver==1.4.4
logging==0.4.9.6
lxml==4.9.2
matplotlib==3.7.1
numpy==1.24.2
packaging==23.0
pefile==2023.2.7
Pillow==9.5.0
pyinstaller==5.10.1
pyinstaller-hooks-contrib==2023.2
pyparsing==3.0.9
python-dateutil==2.8.2
python-docx==0.8.11
pytz==2023.3
pytz-deprecation-shim==0.1.0.post0
pywin32-ctypes==0.2.0
qiniu==7.10.0
requests==2.28.2
self==2020.12.3
six==1.16.0
tzdata==2023.3
tzlocal==4.3
urllib3==1.26.15
zipp==3.15.0
Loading…
Cancel
Save