- 统一移除手动创建的数据库session,统一使用models模块中的db.session - 修正项目创建接口,增加开始和结束日期的格式验证与处理 - 更新导入项目接口,使用枚举类型校验项目类型并优化异常处理 - 更新统计接口,避免多次查询假期数据,优化日期字符串处理 - 删除回滚前多余的session关闭调用,改为使用db.session.rollback() - app.py中重构数据库初始化:统一配置SQLAlchemy,动态创建数据库路径和表 - 项目模型新增开始日期和结束日期字段支持 - 添加导入批次历史记录模型支持 - 优化工具函数中日期类型提示,移除无用导入 - 更新requirements.txt依赖版本回退,确保兼容性 - 前端菜单添加导入历史导航入口,实现页面访问路由绑定
247 lines
11 KiB
Python
247 lines
11 KiB
Python
from flask import Blueprint, request, jsonify
|
||
from sqlalchemy import and_
|
||
from models.models import db, TimeRecord, Project, CutoffPeriod, Holiday
|
||
from models.utils import *
|
||
from datetime import datetime, date, timedelta
|
||
from collections import defaultdict
|
||
|
||
statistics_bp = Blueprint('statistics', __name__)
|
||
|
||
@statistics_bp.route('/api/statistics/weekly', methods=['GET'])
|
||
def get_weekly_statistics():
|
||
"""获取周统计数据"""
|
||
try:
|
||
# 获取查询参数
|
||
period_id = request.args.get('period_id')
|
||
start_date_str = request.args.get('start_date')
|
||
end_date_str = request.args.get('end_date')
|
||
|
||
# 如果指定了周期ID,使用周期的日期范围
|
||
if period_id:
|
||
period = db.session.query(CutoffPeriod).get(int(period_id))
|
||
if not period:
|
||
return jsonify({'success': False, 'error': '周期不存在'}), 404
|
||
|
||
start_date = period.start_date
|
||
end_date = period.end_date
|
||
target_hours = period.target_hours
|
||
period_info = period.to_dict()
|
||
else:
|
||
# 使用指定的日期范围
|
||
if not start_date_str or not end_date_str:
|
||
return jsonify({'success': False, 'error': '请提供开始日期和结束日期'}), 400
|
||
|
||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||
target_hours = 40 # 默认目标工时
|
||
period_info = {
|
||
'period_name': f"{start_date.strftime('%m月%d日')}-{end_date.strftime('%m月%d日')}",
|
||
'start_date': start_date.isoformat(),
|
||
'end_date': end_date.isoformat(),
|
||
'target_hours': target_hours
|
||
}
|
||
|
||
# 查询该时间范围内的所有工时记录
|
||
records = db.session.query(TimeRecord).filter(
|
||
and_(TimeRecord.date >= start_date, TimeRecord.date <= end_date)
|
||
).order_by(TimeRecord.date).all()
|
||
|
||
# 生成完整的日期范围
|
||
current_date = start_date
|
||
daily_records = []
|
||
record_dict = {}
|
||
|
||
# 将记录按日期分组
|
||
for record in records:
|
||
if record.date not in record_dict:
|
||
record_dict[record.date] = []
|
||
record_dict[record.date].append(record)
|
||
|
||
# 获取时间范围内的所有假期定义,避免在循环中重复查询
|
||
holidays_in_range = db.session.query(Holiday).all()
|
||
|
||
# 生成每日汇总
|
||
while current_date <= end_date:
|
||
day_records = record_dict.get(current_date, [])
|
||
|
||
if day_records:
|
||
# 如果有记录,汇总该日的数据
|
||
total_hours = sum([format_hours_to_decimal(r.hours) for r in day_records if r.hours not in ['-', '0:00']])
|
||
event_descriptions = [r.event_description for r in day_records if r.event_description and r.event_description != '-']
|
||
activity_nums = [r.activity_num for r in day_records if r.activity_num and r.activity_num != '-']
|
||
project_names = []
|
||
|
||
for r in day_records:
|
||
if r.project and r.project.project_name:
|
||
if r.project.project_type.value == 'traditional':
|
||
project_names.append(f"{r.project.customer_name} {r.project.project_code}")
|
||
else:
|
||
project_names.append(f"{r.project.customer_name} {r.project.contract_number}")
|
||
|
||
# 获取第一条记录的时间信息
|
||
first_record = day_records[0]
|
||
|
||
daily_record = {
|
||
'date': current_date.isoformat(),
|
||
'day_of_week': get_day_of_week_chinese(current_date),
|
||
'event': '; '.join(event_descriptions) if event_descriptions else '-',
|
||
'project': '; '.join(set(project_names)) if project_names else '-',
|
||
'start_time': first_record.start_time.strftime('%H:%M') if first_record.start_time else '-',
|
||
'end_time': first_record.end_time.strftime('%H:%M') if first_record.end_time else '-',
|
||
'activity_num': '; '.join(activity_nums) if activity_nums else '-',
|
||
'hours': format_decimal_to_hours(total_hours) if total_hours > 0 else ('0:00' if first_record.is_holiday else '-'),
|
||
'is_holiday': first_record.is_holiday,
|
||
'holiday_type': first_record.holiday_type,
|
||
'is_working_on_holiday': any(r.is_working_on_holiday for r in day_records)
|
||
}
|
||
else:
|
||
# 如果没有记录,生成默认记录
|
||
holiday_info = is_holiday(current_date, holidays_in_range)
|
||
|
||
daily_record = {
|
||
'date': current_date.isoformat(),
|
||
'day_of_week': get_day_of_week_chinese(current_date),
|
||
'event': '-',
|
||
'project': '-',
|
||
'start_time': '-',
|
||
'end_time': '-',
|
||
'activity_num': '-',
|
||
'hours': '0:00' if holiday_info['is_holiday'] else '-',
|
||
'is_holiday': holiday_info['is_holiday'],
|
||
'holiday_type': holiday_info['holiday_type'],
|
||
'is_working_on_holiday': False
|
||
}
|
||
|
||
daily_records.append(daily_record)
|
||
current_date += timedelta(days=1)
|
||
|
||
# 计算统计数据
|
||
workday_total = 0
|
||
holiday_total = 0
|
||
working_days = 0
|
||
holiday_work_days = 0
|
||
rest_days = 0
|
||
|
||
for daily in daily_records:
|
||
hours_decimal = format_hours_to_decimal(daily['hours'])
|
||
if daily['is_holiday']:
|
||
if daily['is_working_on_holiday']:
|
||
holiday_total += hours_decimal
|
||
holiday_work_days += 1
|
||
else:
|
||
rest_days += 1
|
||
else:
|
||
if hours_decimal > 0:
|
||
workday_total += hours_decimal
|
||
working_days += 1
|
||
else:
|
||
rest_days += 1
|
||
|
||
# 项目工时统计
|
||
project_hours = defaultdict(float)
|
||
for record in records:
|
||
if record.project and record.hours not in ['-', '0:00']:
|
||
hours_decimal = format_hours_to_decimal(record.hours)
|
||
if record.project.project_type.value == 'traditional':
|
||
project_key = f"{record.project.customer_name} {record.project.project_code}"
|
||
else:
|
||
project_key = f"{record.project.customer_name} {record.project.contract_number}"
|
||
project_hours[project_key] += hours_decimal
|
||
|
||
total_project_hours = sum(project_hours.values())
|
||
project_stats = []
|
||
for project_name, hours in project_hours.items():
|
||
percentage = (hours / total_project_hours * 100) if total_project_hours > 0 else 0
|
||
project_stats.append({
|
||
'project': project_name,
|
||
'hours': format_decimal_to_hours(hours),
|
||
'percentage': round(percentage, 1)
|
||
})
|
||
|
||
project_stats.sort(key=lambda x: x['percentage'], reverse=True)
|
||
|
||
result = {
|
||
'period': period_info,
|
||
'daily_records': daily_records,
|
||
'project_hours': project_stats,
|
||
'workday_total': format_decimal_to_hours(workday_total),
|
||
'holiday_total': format_decimal_to_hours(holiday_total),
|
||
'weekly_total': format_decimal_to_hours(workday_total + holiday_total),
|
||
'working_days': working_days,
|
||
'holiday_work_days': holiday_work_days,
|
||
'rest_days': rest_days,
|
||
'target_hours': target_hours,
|
||
'completion_rate': round((workday_total + holiday_total) / target_hours * 100, 1) if target_hours > 0 else 0
|
||
}
|
||
|
||
return jsonify({'success': True, 'data': result})
|
||
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@statistics_bp.route('/api/statistics/periods', methods=['GET'])
|
||
def get_cutoff_periods():
|
||
"""获取Cut-Off周期列表"""
|
||
try:
|
||
periods = db.session.query(CutoffPeriod).order_by(CutoffPeriod.start_date.desc()).all()
|
||
result = [period.to_dict() for period in periods]
|
||
return jsonify({'success': True, 'data': result})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@statistics_bp.route('/api/statistics/periods', methods=['POST'])
|
||
def create_cutoff_period():
|
||
"""创建Cut-Off周期"""
|
||
try:
|
||
data = request.json
|
||
|
||
# 验证必填字段
|
||
if not all(key in data for key in ['period_name', 'start_date', 'end_date']):
|
||
return jsonify({'success': False, 'error': '周期名称、开始日期和结束日期为必填项'}), 400
|
||
|
||
start_date = datetime.strptime(data['start_date'], '%Y-%m-%d').date()
|
||
end_date = datetime.strptime(data['end_date'], '%Y-%m-%d').date()
|
||
|
||
if start_date >= end_date:
|
||
return jsonify({'success': False, 'error': '开始日期必须早于结束日期'}), 400
|
||
|
||
# 计算周数
|
||
days_diff = (end_date - start_date).days + 1
|
||
weeks = round(days_diff / 7)
|
||
|
||
period = CutoffPeriod(
|
||
period_name=data['period_name'],
|
||
start_date=start_date,
|
||
end_date=end_date,
|
||
target_hours=data.get('target_hours', weeks * 40),
|
||
weeks=weeks,
|
||
year=start_date.year,
|
||
month=start_date.month
|
||
)
|
||
|
||
db.session.add(period)
|
||
db.session.commit()
|
||
|
||
result = period.to_dict()
|
||
return jsonify({'success': True, 'data': result})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'error': str(e)}), 500
|
||
|
||
@statistics_bp.route('/api/statistics/periods/<int:period_id>', methods=['DELETE'])
|
||
def delete_cutoff_period(period_id):
|
||
"""删除Cut-Off周期"""
|
||
try:
|
||
period = db.session.query(CutoffPeriod).get(period_id)
|
||
if not period:
|
||
return jsonify({'success': False, 'error': '周期不存在'}), 404
|
||
|
||
db.session.delete(period)
|
||
db.session.commit()
|
||
|
||
return jsonify({'success': True, 'message': '周期已删除'})
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
return jsonify({'success': False, 'error': str(e)}), 500 |