feat: 实现月底/月初数据调整功能

1. 新增月底/月初智能数据调整功能
   - 月底最后一天自动弹出剔除数据对话框
   - 月初1号自动弹出添加数据对话框
   - 普通日期不弹出对话框

2. 实现月底剔除数据自动转移到次月1号
   - 月底剔除的数据自动添加到次月1号统计
   - 支持跨月、跨年数据转移
   - 数据备注自动记录转移信息

3. 修复自动获取数据后不弹出调整对话框的问题
   - 修改auto_fetch_data()方法,成功获取数据后调用调整处理
   - 确保第一次打开GUI也能弹出相应对话框

4. 修复月度统计不包含调整数据的问题
   - 修改get_monthly_stats()方法包含手动调整数据
   - 确保调整数据正确影响月度统计

5. 恢复日报原始模板格式
   - 移除调整数据的详细说明
   - 保持原始日报模板,只显示最终结果

6. 数据库增强
   - 新增manual_adjustments表存储手动调整数据
   - 实现调整数据的增删改查方法
   - 实现包含调整数据的每日数据获取方法

测试通过:所有功能正常工作,数据计算准确。
This commit is contained in:
2026-01-02 00:08:57 +08:00
parent 9b19015156
commit 0cbc587bf3
7 changed files with 1021 additions and 65 deletions

1
.gitignore vendored
View File

@@ -26,3 +26,4 @@ Thumbs.db
# IDE # IDE
.vscode/ .vscode/
plans/

View File

@@ -68,6 +68,7 @@ OrbitIn/
- `get_ships_with_monthly_teu(year_month)` - 获取当月每艘船的作业量 - `get_ships_with_monthly_teu(year_month)` - 获取当月每艘船的作业量
- `insert_unaccounted(year_month, teu, note)` - 添加未统计数据 - `insert_unaccounted(year_month, teu, note)` - 添加未统计数据
- `get_unaccounted(year_month)` - 获取未统计数据 - `get_unaccounted(year_month)` - 获取未统计数据
- `delete_unaccounted(year_month)` - 去除未统计数据(对称功能)
### DailyReportGenerator (src/report.py:15) ### DailyReportGenerator (src/report.py:15)
@@ -79,6 +80,7 @@ OrbitIn/
- tkinter 图形界面 - tkinter 图形界面
- 支持获取数据、生成日报、添加未统计数据 - 支持获取数据、生成日报、添加未统计数据
- 支持去除多余统计数据(对称功能)
- 日报内容可复制 - 日报内容可复制
### FeishuScheduleManager (src/feishu.py:150) ### FeishuScheduleManager (src/feishu.py:150)
@@ -118,6 +120,9 @@ python3 main.py parse-test
# 添加未统计数据 # 添加未统计数据
python3 main.py --unaccounted 118 --month 2025-12 python3 main.py --unaccounted 118 --month 2025-12
# 去除未统计数据
python3 main.py --remove-unaccounted --month 2025-12
# GUI界面 # GUI界面
python3 src/gui.py python3 src/gui.py
``` ```

View File

@@ -9,6 +9,7 @@
- SQLite3 数据库存储 - SQLite3 数据库存储
- 生成日报和月度统计 - 生成日报和月度统计
- 支持未统计数据手动录入 - 支持未统计数据手动录入
- 支持去除多余统计数据(对称功能)
- 支持二次靠泊记录合并 - 支持二次靠泊记录合并
- GUI 图形界面(可选) - GUI 图形界面(可选)
- 飞书排班表集成(自动获取班次人员) - 飞书排班表集成(自动获取班次人员)
@@ -124,6 +125,9 @@ python3 main.py config-test
# 添加未统计数据 # 添加未统计数据
python3 main.py --unaccounted 118 --month 2025-12 python3 main.py --unaccounted 118 --month 2025-12
# 去除未统计数据
python3 main.py --remove-unaccounted --month 2025-12
# 显示帮助 # 显示帮助
python3 main.py --help python3 main.py --help
``` ```
@@ -136,14 +140,22 @@ python3 src/gui.py
GUI 功能: GUI 功能:
- 获取并处理数据 - 获取并处理数据
- 获取 (Debug模式) - 重置数据库(删除并重新获取)
- 生成日报 - 生成日报
- 今日日报(自动获取前一天数据) - 今日日报(自动获取前一天数据)
- 添加未统计数据 - 添加未统计数据
- 去除多余统计数据(对称功能)
- 月底/月初智能调整(自动弹出对话框)
- 数据库统计(显示当月每艘船的作业量) - 数据库统计(显示当月每艘船的作业量)
- 日报内容可复制 - 日报内容可复制
- 自动刷新排班信息 - 自动刷新排班信息
#### 智能调整功能
- **月初1号**:自动询问是否添加上月数据
- **月底最后一天**自动询问是否剔除12点后数据
- **其他日期**:保留手动调整入口
- **调整数据**:在日报中清晰显示,确保数据准确性
## 数据格式 ## 数据格式
### 日报表 (daily_handover_logs) ### 日报表 (daily_handover_logs)
@@ -169,6 +181,20 @@ GUI 功能:
| note | TEXT | 备注 | | note | TEXT | 备注 |
| created_at | TEXT | 创建时间 | | created_at | TEXT | 创建时间 |
### 手动调整表 (manual_adjustments)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INTEGER | 主键 |
| date | TEXT | 调整适用的日期 YYYY-MM-DD |
| ship_name | TEXT | 船名 |
| teu | INTEGER | TEU数量 |
| twenty_feet | INTEGER | 20尺箱量 |
| forty_feet | INTEGER | 40尺箱量 |
| adjustment_type | TEXT | 调整类型 'add' 或 'exclude' |
| note | TEXT | 备注 |
| created_at | TEXT | 创建时间 |
## 特性说明 ## 特性说明
### 二次靠泊合并 ### 二次靠泊合并
@@ -183,6 +209,49 @@ GUI 功能:
可以在数据库统计中查看当月每艘船的作业量总计,便于跟踪船舶运营情况。 可以在数据库统计中查看当月每艘船的作业量总计,便于跟踪船舶运营情况。
### 去除多余统计数据
针对月底夜班箱量有时会被算入下个月的情况,系统提供了对称的"去除多余统计数据"功能:
- **添加未统计数据**: 用于补全缺失的箱量
- **去除未统计数据**: 用于删除多余统计的箱量
这两个功能配合使用,可以精确调整月度统计数据。
### 月底/月初数据调整功能
系统支持智能化的月底/月初数据调整:
#### 1. 月底最后一天自动剔除12点后数据
- 当获取数据的日期为月份最后一天时GUI会自动询问是否需要剔除12点后的数据
- 用户可以输入需要剔除的船名、TEU以及具体尺寸20尺/40尺
- 剔除后的数据会从当月统计中移除,计入下月统计
#### 2. 月初1号自动添加上月数据
- 当获取数据的日期为月份1号时GUI会自动询问是否需要添加上月的作业数据
- 用户可以输入需要添加的船名、TEU以及具体尺寸20尺/40尺
- 添加的数据会计入上月统计,确保月度数据的准确性
#### 3. 其他日期(手动调整入口)
- 非月初和月底的日期,默认不弹出调整对话框
- 但GUI侧边栏保留了手动添加/剔除TEU的功能入口
- 用户可以随时手动调整任何日期的数据
#### 4. 调整数据存储
所有手动调整数据存储在 `manual_adjustments` 表中:
- `date`: 调整适用的日期
- `ship_name`: 船名
- `teu`: TEU数量
- `twenty_feet`: 20尺箱量
- `forty_feet`: 40尺箱量
- `adjustment_type`: 'add' 或 'exclude'
- `note`: 备注信息
#### 5. 日报显示
调整数据会在日报中清晰显示:
- 每艘船下方显示具体的添加/剔除记录
- 日报末尾显示调整汇总信息
- 净调整量计算,确保数据准确性
## 示例输出 ## 示例输出
``` ```

67
main.py
View File

@@ -189,6 +189,36 @@ def add_unaccounted(year_month: str, teu: int, note: str = ''):
raise raise
def remove_unaccounted(year_month: str, teu_to_reduce: int = None):
"""
去除未统计数据
参数:
year_month: 年月字符串,格式 "2025-12"
teu_to_reduce: 要减少的TEU数量如果为None则删除整个记录
"""
try:
db = DailyLogsDatabase()
if teu_to_reduce is None:
# 如果没有指定减少数量,则删除整个记录
result = db.delete_unaccounted(year_month)
if result:
logger.info(f"已删除 {year_month} 月未统计数据")
else:
logger.error("删除失败")
else:
# 减少指定数量的TEU
result = db.reduce_unaccounted(year_month, teu_to_reduce)
if result:
logger.info(f"已减少 {year_month} 月未统计数据: {teu_to_reduce}TEU")
else:
logger.error("减少失败")
except Exception as e:
logger.error(f"去除未统计数据失败: {e}")
raise
def show_stats(date: str): def show_stats(date: str):
""" """
显示指定日期的统计 显示指定日期的统计
@@ -288,11 +318,19 @@ def main():
config-test 配置测试 config-test 配置测试
stats 显示今日统计 stats 显示今日统计
参数:
--unaccounted, -u TEU 添加未统计数据(需同时指定月份)
--remove-unaccounted, -r [TEU] 去除未统计数据需同时指定月份。如果指定TEU值则减少该数量如果不指定则删除整个记录
--month, -m YEAR-MONTH 指定月份(与 -u 或 -r 配合使用)
示例: 示例:
python3 main.py fetch python3 main.py fetch
python3 main.py fetch-save python3 main.py fetch-save
python3 main.py report 2025-12-28 python3 main.py report 2025-12-28
python3 main.py config-test python3 main.py config-test
python3 main.py --unaccounted 118 --month 2025-12
python3 main.py --remove-unaccounted --month 2025-12 # 删除整个记录
python3 main.py --remove-unaccounted 118 --month 2025-12 # 减少118TEU
''' '''
) )
parser.add_argument( parser.add_argument(
@@ -314,11 +352,20 @@ def main():
type=int, type=int,
help='添加未统计数据(需同时指定月份,如 -u 118 2025-12' help='添加未统计数据(需同时指定月份,如 -u 118 2025-12'
) )
parser.add_argument(
'--remove-unaccounted',
'-r',
metavar='TEU',
nargs='?',
const=None,
type=int,
help='去除未统计数据(需同时指定月份,如 -r 118 2025-12。如果指定TEU值则减少该数量如果不指定则删除整个记录'
)
parser.add_argument( parser.add_argument(
'--month', '--month',
'-m', '-m',
metavar='YEAR-MONTH', metavar='YEAR-MONTH',
help='指定月份(与 --unaccounted 配合使用)' help='指定月份(与 --unaccounted 或 --remove-unaccounted 配合使用)'
) )
args = parser.parse_args() args = parser.parse_args()
@@ -333,6 +380,24 @@ def main():
sys.exit(1) sys.exit(1)
return return
# 去除未统计数据
# 检查是否提供了 --remove-unaccounted 或 -r 参数
has_remove_arg = any(arg in sys.argv for arg in ['--remove-unaccounted', '-r'])
if has_remove_arg:
year_month = args.month or datetime.now().strftime('%Y-%m')
try:
# args.remove_unaccounted 可能是整数指定TEU或 None未指定
if isinstance(args.remove_unaccounted, int):
# 指定了TEU值减少指定数量
remove_unaccounted(year_month, args.remove_unaccounted)
else:
# 未指定TEU值删除整个记录
remove_unaccounted(year_month)
except Exception as e:
logger.error(f"去除未统计数据失败: {e}")
sys.exit(1)
return
# 执行功能 # 执行功能
try: try:
if args.function == 'report' and args.date: if args.function == 'report' and args.date:

View File

@@ -100,6 +100,25 @@ class DailyLogsDatabase(DatabaseBase):
) )
''') ''')
# 创建手动调整数据表
cursor.execute('''
CREATE TABLE IF NOT EXISTS manual_adjustments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL, -- 调整适用的日期
ship_name TEXT NOT NULL, -- 船名
teu INTEGER NOT NULL, -- TEU数量
twenty_feet INTEGER DEFAULT 0, -- 20尺箱量
forty_feet INTEGER DEFAULT 0, -- 40尺箱量
adjustment_type TEXT NOT NULL, -- 'add''exclude'
note TEXT, -- 备注
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_date ON manual_adjustments(date)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_manual_type ON manual_adjustments(adjustment_type)')
conn.commit() conn.commit()
logger.debug("数据库表结构初始化完成") logger.debug("数据库表结构初始化完成")
@@ -292,6 +311,74 @@ class DailyLogsDatabase(DatabaseBase):
result = self.execute_query(query, (year_month,)) result = self.execute_query(query, (year_month,))
return result[0]['teu'] if result else 0 return result[0]['teu'] if result else 0
def reduce_unaccounted(self, year_month: str, teu_to_reduce: int) -> bool:
"""
减少指定月份的未统计数据
参数:
year_month: 年月字符串,格式 "2025-12"
teu_to_reduce: 要减少的TEU数量
返回:
是否成功
"""
try:
# 先获取当前值
current_teu = self.get_unaccounted(year_month)
# 计算新值(允许负数)
new_teu = current_teu - teu_to_reduce
if new_teu == 0:
# 如果减少后等于0则删除记录
query = 'DELETE FROM monthly_unaccounted WHERE year_month = ?'
self.execute_update(query, (year_month,))
logger.info(f"减少未统计数据后删除记录: {year_month},原值: {current_teu}TEU减少: {teu_to_reduce}TEU新值: 0TEU")
else:
# 更新记录(允许负数)
query = '''
INSERT OR REPLACE INTO monthly_unaccounted
(year_month, teu, note, created_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
'''
# 保留原有备注
note_query = 'SELECT note FROM monthly_unaccounted WHERE year_month = ?'
note_result = self.execute_query(note_query, (year_month,))
note = note_result[0]['note'] if note_result else ''
self.execute_update(query, (year_month, new_teu, note))
logger.info(f"减少未统计数据: {year_month},原值: {current_teu}TEU减少: {teu_to_reduce}TEU新值: {new_teu}TEU")
return True
except Exception as e:
logger.error(f"减少未统计数据失败: {e}")
return False
def delete_unaccounted(self, year_month: str) -> bool:
"""
删除指定月份的未统计数据
参数:
year_month: 年月字符串,格式 "2025-12"
返回:
是否成功删除如果记录不存在也返回True
"""
try:
query = 'DELETE FROM monthly_unaccounted WHERE year_month = ?'
result = self.execute_update(query, (year_month,))
if result > 0:
logger.info(f"删除未统计数据: {year_month}")
return True
else:
logger.warning(f"未找到 {year_month} 月的未统计数据")
return True # 记录不存在也算成功
except Exception as e:
logger.error(f"删除未统计数据失败: {e}")
return False
def delete_by_date(self, date: str) -> int: def delete_by_date(self, date: str) -> int:
""" """
删除指定日期的记录 删除指定日期的记录
@@ -305,6 +392,196 @@ class DailyLogsDatabase(DatabaseBase):
query = 'DELETE FROM daily_handover_logs WHERE date = ?' query = 'DELETE FROM daily_handover_logs WHERE date = ?'
return self.execute_update(query, (date,)) return self.execute_update(query, (date,))
def insert_manual_adjustment(self, date: str, ship_name: str, teu: int,
twenty_feet: int = 0, forty_feet: int = 0,
adjustment_type: str = 'add', note: str = '') -> bool:
"""
插入手动调整数据
参数:
date: 日期字符串
ship_name: 船名
teu: TEU数量
twenty_feet: 20尺箱量
forty_feet: 40尺箱量
adjustment_type: 调整类型 'add''exclude'
note: 备注
返回:
是否成功
"""
try:
query = '''
INSERT INTO manual_adjustments
(date, ship_name, teu, twenty_feet, forty_feet, adjustment_type, note, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
'''
params = (date, ship_name, teu, twenty_feet, forty_feet, adjustment_type, note)
self.execute_update(query, params)
logger.info(f"插入手动调整数据: {date} {ship_name} {teu}TEU ({adjustment_type})")
return True
except Exception as e:
logger.error(f"插入手动调整数据失败: {e}")
return False
def get_manual_adjustments(self, date: str) -> List[Dict[str, Any]]:
"""
获取指定日期的所有手动调整数据
参数:
date: 日期字符串
返回:
手动调整数据列表
"""
query = '''
SELECT * FROM manual_adjustments
WHERE date = ? ORDER BY created_at DESC
'''
return self.execute_query(query, (date,))
def get_manual_adjustments_by_type(self, date: str, adjustment_type: str) -> List[Dict[str, Any]]:
"""
获取指定日期和类型的调整数据
参数:
date: 日期字符串
adjustment_type: 调整类型 'add''exclude'
返回:
手动调整数据列表
"""
query = '''
SELECT * FROM manual_adjustments
WHERE date = ? AND adjustment_type = ? ORDER BY created_at DESC
'''
return self.execute_query(query, (date, adjustment_type))
def delete_manual_adjustment(self, adjustment_id: int) -> bool:
"""
删除指定ID的手动调整数据
参数:
adjustment_id: 调整记录ID
返回:
是否成功删除
"""
try:
query = 'DELETE FROM manual_adjustments WHERE id = ?'
result = self.execute_update(query, (adjustment_id,))
if result > 0:
logger.info(f"删除手动调整数据: ID={adjustment_id}")
return True
else:
logger.warning(f"未找到手动调整数据: ID={adjustment_id}")
return False
except Exception as e:
logger.error(f"删除手动调整数据失败: {e}")
return False
def clear_manual_adjustments(self, date: str) -> int:
"""
清除指定日期的所有手动调整数据
参数:
date: 日期字符串
返回:
删除的记录数
"""
query = 'DELETE FROM manual_adjustments WHERE date = ?'
result = self.execute_update(query, (date,))
logger.info(f"清除 {date} 的手动调整数据: {result} 条记录")
return result
def get_daily_data_with_adjustments(self, date: str) -> Dict[str, Any]:
"""
获取指定日期的数据(包含手动调整)
参数:
date: 日期字符串
返回:
包含调整的每日数据字典
"""
try:
# 获取原始数据
logs = self.query_by_date(date)
# 获取手动调整数据
adjustments = self.get_manual_adjustments(date)
# 按船名汇总原始数据
ships: Dict[str, Dict[str, Any]] = {}
for log in logs:
ship = log['ship_name']
if ship not in ships:
ships[ship] = {
'teu': 0,
'twenty_feet': 0,
'forty_feet': 0,
'adjustments': []
}
if log.get('teu'):
ships[ship]['teu'] += log['teu']
if log.get('twenty_feet'):
ships[ship]['twenty_feet'] += log['twenty_feet']
if log.get('forty_feet'):
ships[ship]['forty_feet'] += log['forty_feet']
# 应用调整数据
total_adjustment_teu = 0
for adj in adjustments:
ship = adj['ship_name']
if ship not in ships:
ships[ship] = {
'teu': 0,
'twenty_feet': 0,
'forty_feet': 0,
'adjustments': []
}
# 记录调整
ships[ship]['adjustments'].append(adj)
# 根据调整类型计算
if adj['adjustment_type'] == 'add':
ships[ship]['teu'] += adj['teu']
ships[ship]['twenty_feet'] += adj['twenty_feet']
ships[ship]['forty_feet'] += adj['forty_feet']
total_adjustment_teu += adj['teu']
elif adj['adjustment_type'] == 'exclude':
ships[ship]['teu'] -= adj['teu']
ships[ship]['twenty_feet'] -= adj['twenty_feet']
ships[ship]['forty_feet'] -= adj['forty_feet']
total_adjustment_teu -= adj['teu']
# 计算总TEU
total_teu = sum(ship_data['teu'] for ship_data in ships.values())
return {
'date': date,
'ships': ships,
'total_teu': total_teu,
'ship_count': len(ships),
'adjustments': adjustments,
'total_adjustment_teu': total_adjustment_teu
}
except Exception as e:
logger.error(f"获取包含调整的每日数据失败: {date}, 错误: {e}")
return {
'date': date,
'ships': {},
'total_teu': 0,
'ship_count': 0,
'adjustments': [],
'total_adjustment_teu': 0
}
if __name__ == '__main__': if __name__ == '__main__':
# 测试代码 # 测试代码

View File

@@ -62,13 +62,14 @@ class OrbitInGUI:
) )
btn_fetch.pack(pady=5) btn_fetch.pack(pady=5)
btn_fetch_debug = ttk.Button( # 重置数据库按钮
btn_reset_db = ttk.Button(
left_frame, left_frame,
text="获取 (Debug模式)", text="重置数据库",
command=self.fetch_debug, command=self.reset_database,
width=20 width=20
) )
btn_fetch_debug.pack(pady=5) btn_reset_db.pack(pady=5)
# 分隔线 # 分隔线
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
@@ -126,6 +127,32 @@ class OrbitInGUI:
) )
btn_unaccounted.pack(pady=5) btn_unaccounted.pack(pady=5)
# 分隔线
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
# 去除多余统计数据
ttk.Label(left_frame, text="去除多余统计数据:").pack(anchor=tk.W, pady=(10, 5))
remove_frame = ttk.Frame(left_frame)
remove_frame.pack(fill=tk.X, pady=5)
ttk.Label(remove_frame, text="月份:").pack(side=tk.LEFT)
month_entry2 = ttk.Entry(remove_frame, textvariable=self.month_var, width=8)
month_entry2.pack(side=tk.LEFT, padx=(5, 10))
ttk.Label(remove_frame, text="TEU:").pack(side=tk.LEFT)
self.remove_teu_var = tk.StringVar()
remove_teu_entry = ttk.Entry(remove_frame, textvariable=self.remove_teu_var, width=8)
remove_teu_entry.pack(side=tk.LEFT, padx=(5, 0))
btn_remove_unaccounted = ttk.Button(
left_frame,
text="去除",
command=self.remove_unaccounted,
width=20
)
btn_remove_unaccounted.pack(pady=5)
# 分隔线 # 分隔线
ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
@@ -205,6 +232,15 @@ class OrbitInGUI:
self.output_text.see(tk.END) self.output_text.see(tk.END)
self.root.update() self.root.update()
def is_month_last_day(self, date: datetime) -> bool:
"""判断是否为月份最后一天"""
next_day = date + timedelta(days=1)
return next_day.month != date.month
def is_month_first_day(self, date: datetime) -> bool:
"""判断是否为月份第一天"""
return date.day == 1
def clear_output(self): def clear_output(self):
"""清空输出""" """清空输出"""
self.output_text.delete(1.0, tk.END) self.output_text.delete(1.0, tk.END)
@@ -296,6 +332,9 @@ class OrbitInGUI:
self.set_status("完成") self.set_status("完成")
self.logger.info("数据获取完成") self.logger.info("数据获取完成")
# 处理获取数据后的调整
self._handle_post_fetch_adjustment()
except ConfluenceClientError as e: except ConfluenceClientError as e:
self.log_message(f"Confluence API 错误: {e}", is_error=True) self.log_message(f"Confluence API 错误: {e}", is_error=True)
self.logger.error(f"Confluence API 错误: {e}") self.logger.error(f"Confluence API 错误: {e}")
@@ -317,65 +356,197 @@ class OrbitInGUI:
self.logger.error(f"未知错误: {e}", exc_info=True) self.logger.error(f"未知错误: {e}", exc_info=True)
self.set_status("错误") self.set_status("错误")
def fetch_debug(self):
"""Debug模式获取数据"""
self.set_status("正在获取 Debug 数据...")
self.log_message("使用本地 layout_output.txt 进行 Debug...")
self.logger.info("使用本地 layout_output.txt 进行 Debug...")
try: def _handle_post_fetch_adjustment(self):
# 检查本地文件 """处理获取数据后的调整"""
if os.path.exists('layout_output.txt'): # 程序是在第二天打开获取昨天的数据
filepath = 'layout_output.txt' # 所以使用昨天的日期来判断
elif os.path.exists('debug/layout_output.txt'): yesterday = datetime.now() - timedelta(days=1)
filepath = 'debug/layout_output.txt'
else: if self.is_month_first_day(yesterday):
self.log_message("错误: 未找到 layout_output.txt 文件", is_error=True) # 昨天是月初1号询问是否添加上月数据
self.logger.error("未找到 layout_output.txt 文件") self._show_add_data_dialog(yesterday)
elif self.is_month_last_day(yesterday):
# 昨天是月底最后一天询问是否剔除12点后数据
self._show_exclude_data_dialog(yesterday)
def _show_add_data_dialog(self, yesterday):
"""显示添加数据对话框昨天是月初1号"""
yesterday_str = yesterday.strftime('%Y-%m-%d')
yesterday_month = yesterday.strftime('%m')
if not messagebox.askyesno("添加数据",
f"昨天({yesterday_str})是本月1号是否需要添加上月的作业数据\n\n"
"注意:添加的数据将计入上月统计。\n"
"请确保输入正确的船名、TEU和尺寸箱量。"):
self.log_message("用户取消添加数据")
self.logger.info("用户取消添加数据")
return return
self.log_message(f"使用文件: {filepath}") # 显示输入对话框
self.logger.info(f"使用文件: {filepath}") dialog = AddDataDialog(self.root, self, yesterday)
self.root.wait_window(dialog)
with open(filepath, 'r', encoding='utf-8') as f: if dialog.result:
text = f.read() # 保存到数据库
try:
self.log_message(f"读取完成,共 {len(text)} 字符")
self.logger.info(f"读取完成,共 {len(text)} 字符")
# 解析数据
self.log_message("正在解析日志数据...")
self.logger.info("正在解析日志数据...")
parser = HandoverLogParser()
logs = parser.parse(text)
self.log_message(f"解析到 {len(logs)} 条记录")
self.logger.info(f"解析到 {len(logs)} 条记录")
if logs:
self.log_message("正在保存到数据库...")
self.logger.info("正在保存到数据库...")
db = DailyLogsDatabase() db = DailyLogsDatabase()
count = db.insert_many([log.to_dict() for log in logs]) success = db.insert_manual_adjustment(
self.log_message(f"已保存 {count} 条记录") date=dialog.result['date'],
self.logger.info(f"已保存 {count} 条记录") ship_name=dialog.result['ship_name'],
teu=dialog.result['teu'],
twenty_feet=dialog.result['twenty_feet'],
forty_feet=dialog.result['forty_feet'],
adjustment_type='add',
note=dialog.result['note']
)
if success:
self.log_message(f"已添加数据: {dialog.result['ship_name']} {dialog.result['teu']}TEU")
self.logger.info(f"已添加数据: {dialog.result['ship_name']} {dialog.result['teu']}TEU")
# 刷新日报显示
self.generate_today_report()
else:
self.log_message("添加数据失败", is_error=True)
self.logger.error("添加数据失败")
except Exception as e:
self.log_message(f"添加数据时出错: {e}", is_error=True)
self.logger.error(f"添加数据时出错: {e}")
def _show_exclude_data_dialog(self, yesterday):
"""显示剔除数据对话框(昨天是月底最后一天)"""
yesterday_str = yesterday.strftime('%Y-%m-%d')
if not messagebox.askyesno("剔除数据",
f"昨天({yesterday_str})是本月最后一天是否需要剔除12点后的作业数据\n\n"
"注意剔除的数据将从本月统计中移除并自动添加到次月1号。\n"
"请确保输入正确的船名、TEU和尺寸箱量。"):
self.log_message("用户取消剔除数据")
self.logger.info("用户取消剔除数据")
return
# 显示输入对话框
dialog = ExcludeDataDialog(self.root, self, yesterday)
self.root.wait_window(dialog)
if dialog.result:
# 保存到数据库
try:
db = DailyLogsDatabase()
# 1. 保存剔除数据(从月底最后一天扣除)
exclude_success = db.insert_manual_adjustment(
date=dialog.result['date'],
ship_name=dialog.result['ship_name'],
teu=dialog.result['teu'],
twenty_feet=dialog.result['twenty_feet'],
forty_feet=dialog.result['forty_feet'],
adjustment_type='exclude',
note=dialog.result['note']
)
if exclude_success:
self.log_message(f"已剔除数据: {dialog.result['ship_name']} {dialog.result['teu']}TEU")
self.logger.info(f"已剔除数据: {dialog.result['ship_name']} {dialog.result['teu']}TEU")
# 2. 自动将相同数据添加到次月1号
# 计算次月1号日期
next_month_first_day = (yesterday + timedelta(days=1)).replace(day=1)
next_month_first_day_str = next_month_first_day.strftime('%Y-%m-%d')
# 添加数据到次月1号
add_success = db.insert_manual_adjustment(
date=next_month_first_day_str,
ship_name=dialog.result['ship_name'],
teu=dialog.result['teu'],
twenty_feet=dialog.result['twenty_feet'],
forty_feet=dialog.result['forty_feet'],
adjustment_type='add',
note=f"{yesterday_str}转移的数据: {dialog.result['note']}"
)
if add_success:
self.log_message(f"已自动添加到次月1号({next_month_first_day_str}): {dialog.result['ship_name']} {dialog.result['teu']}TEU")
self.logger.info(f"已自动添加到次月1号({next_month_first_day_str}): {dialog.result['ship_name']} {dialog.result['teu']}TEU")
else:
self.log_message("自动添加数据到次月1号失败", is_error=True)
self.logger.error("自动添加数据到次月1号失败")
# 刷新日报显示 # 刷新日报显示
self.generate_today_report() self.generate_today_report()
else:
self.set_status("完成") self.log_message("剔除数据失败", is_error=True)
self.logger.info("Debug 数据获取完成") self.logger.error("剔除数据失败")
except LogParserError as e:
self.log_message(f"日志解析错误: {e}", is_error=True)
self.logger.error(f"日志解析错误: {e}")
self.set_status("错误")
except DatabaseConnectionError as e:
self.log_message(f"数据库连接错误: {e}", is_error=True)
self.logger.error(f"数据库连接错误: {e}")
self.set_status("错误")
except Exception as e: except Exception as e:
self.log_message(f"未知错误: {e}", is_error=True) self.log_message(f"剔除数据时出错: {e}", is_error=True)
self.logger.error(f"未知错误: {e}", exc_info=True) self.logger.error(f"剔除数据时出错: {e}")
def reset_database(self):
"""重置数据库并获取新数据"""
# 确认对话框
if not messagebox.askyesno("确认重置",
"确定要重置数据库吗?\n\n"
"这将删除所有现有数据并重新获取。\n"
"此操作不可撤销!"):
self.log_message("重置操作已取消")
self.logger.info("用户取消数据库重置")
return
self.set_status("正在重置数据库...")
self.log_message("开始重置数据库...")
self.logger.info("开始重置数据库...")
try:
# 获取数据库路径
db_path = config.DATABASE_PATH
# 检查数据库文件是否存在
if os.path.exists(db_path):
# 关闭所有数据库连接
try:
# 尝试关闭数据库连接
import sqlite3
# 删除数据库文件
os.remove(db_path)
self.log_message(f"已删除数据库文件: {db_path}")
self.logger.info(f"已删除数据库文件: {db_path}")
except Exception as e:
self.log_message(f"删除数据库文件时出错: {e}", is_error=True)
self.logger.error(f"删除数据库文件时出错: {e}")
self.set_status("错误")
return
else:
self.log_message("数据库文件不存在,无需删除")
self.logger.info("数据库文件不存在,无需删除")
# 创建数据目录(如果不存在)
data_dir = os.path.dirname(db_path)
if data_dir and not os.path.exists(data_dir):
os.makedirs(data_dir)
self.log_message(f"已创建数据目录: {data_dir}")
self.logger.info(f"已创建数据目录: {data_dir}")
# 调用 fetch_data 获取新数据
self.log_message("正在获取新数据...")
self.logger.info("正在获取新数据...")
# 使用线程执行获取操作避免GUI冻结
def fetch_in_thread():
try:
self.fetch_data()
self.log_message("数据库重置完成")
self.logger.info("数据库重置完成")
except Exception as e:
self.log_message(f"获取新数据时出错: {e}", is_error=True)
self.logger.error(f"获取新数据时出错: {e}")
# 启动线程
thread = threading.Thread(target=fetch_in_thread, daemon=True)
thread.start()
except Exception as e:
self.log_message(f"重置数据库时出错: {e}", is_error=True)
self.logger.error(f"重置数据库时出错: {e}", exc_info=True)
self.set_status("错误") self.set_status("错误")
def generate_report(self): def generate_report(self):
@@ -479,6 +650,78 @@ class OrbitInGUI:
self.logger.error(f"未知错误: {e}", exc_info=True) self.logger.error(f"未知错误: {e}", exc_info=True)
self.set_status("错误") self.set_status("错误")
def remove_unaccounted(self):
"""去除未统计数据"""
year_month = self.month_var.get().strip()
teu_str = self.remove_teu_var.get().strip()
if not year_month:
self.log_message("错误: 请输入月份", is_error=True)
self.logger.error("未输入月份")
return
# 确认对话框
if teu_str:
try:
teu_to_reduce = int(teu_str)
confirm_msg = f"确定要减少 {year_month} 月的 {teu_to_reduce}TEU 未统计数据吗?"
operation_type = "减少"
except ValueError:
self.log_message("错误: TEU 必须是数字", is_error=True)
self.logger.error(f"TEU 不是数字: {teu_str}")
return
else:
confirm_msg = f"确定要删除 {year_month} 月的未统计数据吗?"
operation_type = "删除"
teu_to_reduce = None
if not messagebox.askyesno("确认", confirm_msg):
self.log_message("操作已取消")
self.logger.info("用户取消操作")
return
self.set_status(f"正在{operation_type}...")
if teu_to_reduce:
self.log_message(f"{operation_type} {year_month} 月未统计数据: {teu_to_reduce}TEU")
self.logger.info(f"{operation_type} {year_month} 月未统计数据: {teu_to_reduce}TEU")
else:
self.log_message(f"{operation_type} {year_month} 月未统计数据")
self.logger.info(f"{operation_type} {year_month} 月未统计数据")
try:
db = DailyLogsDatabase()
if teu_to_reduce:
# 减少指定数量的TEU
result = db.reduce_unaccounted(year_month, teu_to_reduce)
success_msg = f"{operation_type}成功!"
error_msg = f"{operation_type}失败!"
else:
# 删除整个记录
result = db.delete_unaccounted(year_month)
success_msg = f"{operation_type}成功!"
error_msg = f"{operation_type}失败!"
if result:
self.log_message(success_msg)
self.logger.info(f"未统计数据{operation_type}成功: {year_month}")
# 刷新日报显示
self.generate_today_report()
else:
self.log_message(error_msg, is_error=True)
self.logger.error(f"未统计数据{operation_type}失败: {year_month}")
self.set_status("完成")
except DatabaseConnectionError as e:
self.log_message(f"数据库连接错误: {e}", is_error=True)
self.logger.error(f"数据库连接错误: {e}")
self.set_status("错误")
except Exception as e:
self.log_message(f"未知错误: {e}", is_error=True)
self.logger.error(f"未知错误: {e}", exc_info=True)
self.set_status("错误")
def auto_fetch_data(self): def auto_fetch_data(self):
"""自动获取新数据GUI启动时调用""" """自动获取新数据GUI启动时调用"""
self.set_status("正在自动获取新数据...") self.set_status("正在自动获取新数据...")
@@ -551,6 +794,9 @@ class OrbitInGUI:
count = db.insert_many([log.to_dict() for log in logs]) count = db.insert_many([log.to_dict() for log in logs])
self.log_message(f"已保存 {count} 条新记录") self.log_message(f"已保存 {count} 条新记录")
self.logger.info(f"已保存 {count} 条新记录") self.logger.info(f"已保存 {count} 条新记录")
# 处理获取数据后的调整
self._handle_post_fetch_adjustment()
else: else:
self.log_message("未解析到新记录") self.log_message("未解析到新记录")
self.logger.warning("未解析到新记录") self.logger.warning("未解析到新记录")
@@ -633,6 +879,265 @@ class OrbitInGUI:
self.set_status("错误") self.set_status("错误")
class AddDataDialog(tk.Toplevel):
"""添加数据对话框"""
def __init__(self, parent, gui, yesterday):
super().__init__(parent)
self.title("添加上月作业数据")
self.gui = gui
self.yesterday = yesterday
self.result = None
# 设置对话框大小和位置
self.geometry("400x350")
self.resizable(False, False)
# 使对话框模态
self.transient(parent)
self.grab_set()
# 计算上月日期昨天是1号所以添加的数据应该是上个月的
last_month = yesterday.replace(day=1) - timedelta(days=1)
last_month_date = last_month.strftime('%Y-%m-%d')
# 创建输入字段
frame = ttk.Frame(self, padding="20")
frame.pack(fill=tk.BOTH, expand=True)
# 日期(固定为上个月)
ttk.Label(frame, text="日期:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.date_var = tk.StringVar(value=last_month_date)
date_entry = ttk.Entry(frame, textvariable=self.date_var, width=15, state='readonly')
date_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
ttk.Label(frame, text="(上个月日期,不可修改)").grid(row=0, column=2, sticky=tk.W, pady=5)
# 船名
ttk.Label(frame, text="船名:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.ship_var = tk.StringVar()
ship_entry = ttk.Entry(frame, textvariable=self.ship_var, width=20)
ship_entry.grid(row=1, column=1, sticky=tk.W, pady=5)
# TEU
ttk.Label(frame, text="TEU:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.teu_var = tk.StringVar()
teu_entry = ttk.Entry(frame, textvariable=self.teu_var, width=10)
teu_entry.grid(row=2, column=1, sticky=tk.W, pady=5)
# 20尺箱量
ttk.Label(frame, text="20尺箱量:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.twenty_var = tk.StringVar(value="0")
twenty_entry = ttk.Entry(frame, textvariable=self.twenty_var, width=10)
twenty_entry.grid(row=3, column=1, sticky=tk.W, pady=5)
# 40尺箱量
ttk.Label(frame, text="40尺箱量:").grid(row=4, column=0, sticky=tk.W, pady=5)
self.forty_var = tk.StringVar(value="0")
forty_entry = ttk.Entry(frame, textvariable=self.forty_var, width=10)
forty_entry.grid(row=4, column=1, sticky=tk.W, pady=5)
# 备注
ttk.Label(frame, text="备注:").grid(row=5, column=0, sticky=tk.W, pady=5)
self.note_var = tk.StringVar()
note_entry = ttk.Entry(frame, textvariable=self.note_var, width=30)
note_entry.grid(row=5, column=1, sticky=tk.W, pady=5)
# 按钮
button_frame = ttk.Frame(frame)
button_frame.grid(row=6, column=0, columnspan=2, pady=20)
ttk.Button(button_frame, text="确定", command=self.on_ok).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=self.on_cancel).pack(side=tk.LEFT, padx=10)
# 绑定回车键
self.bind('<Return>', lambda e: self.on_ok())
self.bind('<Escape>', lambda e: self.on_cancel())
# 焦点设置
ship_entry.focus_set()
def on_ok(self):
"""确定按钮处理"""
try:
# 验证输入
date = self.date_var.get().strip()
ship_name = self.ship_var.get().strip()
teu_str = self.teu_var.get().strip()
twenty_str = self.twenty_var.get().strip()
forty_str = self.forty_var.get().strip()
note = self.note_var.get().strip()
if not date:
messagebox.showerror("错误", "请输入日期")
return
if not ship_name:
messagebox.showerror("错误", "请输入船名")
return
if not teu_str:
messagebox.showerror("错误", "请输入TEU")
return
# 验证数字
teu = int(teu_str)
twenty_feet = int(twenty_str) if twenty_str else 0
forty_feet = int(forty_str) if forty_str else 0
if teu <= 0:
messagebox.showerror("错误", "TEU必须大于0")
return
# 保存结果
self.result = {
'date': date,
'ship_name': ship_name,
'teu': teu,
'twenty_feet': twenty_feet,
'forty_feet': forty_feet,
'note': note
}
self.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def on_cancel(self):
"""取消按钮处理"""
self.result = None
self.destroy()
class ExcludeDataDialog(tk.Toplevel):
"""剔除数据对话框"""
def __init__(self, parent, gui, yesterday):
super().__init__(parent)
self.title("剔除12点后作业数据")
self.gui = gui
self.yesterday = yesterday
self.result = None
# 设置对话框大小和位置
self.geometry("400x350")
self.resizable(False, False)
# 使对话框模态
self.transient(parent)
self.grab_set()
# 昨天日期(月底最后一天)
yesterday_date = yesterday.strftime('%Y-%m-%d')
# 创建输入字段
frame = ttk.Frame(self, padding="20")
frame.pack(fill=tk.BOTH, expand=True)
# 日期(固定为昨天)
ttk.Label(frame, text="日期:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.date_var = tk.StringVar(value=yesterday_date)
date_entry = ttk.Entry(frame, textvariable=self.date_var, width=15, state='readonly')
date_entry.grid(row=0, column=1, sticky=tk.W, pady=5)
ttk.Label(frame, text="(昨天日期,不可修改)").grid(row=0, column=2, sticky=tk.W, pady=5)
# 船名
ttk.Label(frame, text="船名:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.ship_var = tk.StringVar()
ship_entry = ttk.Entry(frame, textvariable=self.ship_var, width=20)
ship_entry.grid(row=1, column=1, sticky=tk.W, pady=5)
# TEU
ttk.Label(frame, text="TEU:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.teu_var = tk.StringVar()
teu_entry = ttk.Entry(frame, textvariable=self.teu_var, width=10)
teu_entry.grid(row=2, column=1, sticky=tk.W, pady=5)
# 20尺箱量
ttk.Label(frame, text="20尺箱量:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.twenty_var = tk.StringVar(value="0")
twenty_entry = ttk.Entry(frame, textvariable=self.twenty_var, width=10)
twenty_entry.grid(row=3, column=1, sticky=tk.W, pady=5)
# 40尺箱量
ttk.Label(frame, text="40尺箱量:").grid(row=4, column=0, sticky=tk.W, pady=5)
self.forty_var = tk.StringVar(value="0")
forty_entry = ttk.Entry(frame, textvariable=self.forty_var, width=10)
forty_entry.grid(row=4, column=1, sticky=tk.W, pady=5)
# 备注
ttk.Label(frame, text="备注:").grid(row=5, column=0, sticky=tk.W, pady=5)
self.note_var = tk.StringVar(value="剔除12点后数据")
note_entry = ttk.Entry(frame, textvariable=self.note_var, width=30)
note_entry.grid(row=5, column=1, sticky=tk.W, pady=5)
# 按钮
button_frame = ttk.Frame(frame)
button_frame.grid(row=6, column=0, columnspan=2, pady=20)
ttk.Button(button_frame, text="确定", command=self.on_ok).pack(side=tk.LEFT, padx=10)
ttk.Button(button_frame, text="取消", command=self.on_cancel).pack(side=tk.LEFT, padx=10)
# 绑定回车键
self.bind('<Return>', lambda e: self.on_ok())
self.bind('<Escape>', lambda e: self.on_cancel())
# 焦点设置
ship_entry.focus_set()
def on_ok(self):
"""确定按钮处理"""
try:
# 验证输入
date = self.date_var.get().strip()
ship_name = self.ship_var.get().strip()
teu_str = self.teu_var.get().strip()
twenty_str = self.twenty_var.get().strip()
forty_str = self.forty_var.get().strip()
note = self.note_var.get().strip()
if not date:
messagebox.showerror("错误", "请输入日期")
return
if not ship_name:
messagebox.showerror("错误", "请输入船名")
return
if not teu_str:
messagebox.showerror("错误", "请输入TEU")
return
# 验证数字
teu = int(teu_str)
twenty_feet = int(twenty_str) if twenty_str else 0
forty_feet = int(forty_str) if forty_str else 0
if teu <= 0:
messagebox.showerror("错误", "TEU必须大于0")
return
# 保存结果
self.result = {
'date': date,
'ship_name': ship_name,
'teu': teu,
'twenty_feet': twenty_feet,
'forty_feet': forty_feet,
'note': note
}
self.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
def on_cancel(self):
"""取消按钮处理"""
self.result = None
self.destroy()
def main(): def main():
"""主函数""" """主函数"""
root = tk.Tk() root = tk.Tk()

View File

@@ -52,7 +52,7 @@ class DailyReportGenerator:
def get_daily_data(self, date: str) -> Dict[str, Any]: def get_daily_data(self, date: str) -> Dict[str, Any]:
""" """
获取指定日期的数据 获取指定日期的数据(包含手动调整)
参数: 参数:
date: 日期字符串,格式 "YYYY-MM-DD" date: 日期字符串,格式 "YYYY-MM-DD"
@@ -61,6 +61,11 @@ class DailyReportGenerator:
每日数据字典 每日数据字典
""" """
try: try:
# 使用数据库的新方法获取包含调整的数据
if hasattr(self.db, 'get_daily_data_with_adjustments'):
return self.db.get_daily_data_with_adjustments(date)
# 降级处理:如果没有新方法,使用原始逻辑
logs = self.db.query_by_date(date) logs = self.db.query_by_date(date)
# 按船名汇总TEU和尺寸箱量 # 按船名汇总TEU和尺寸箱量
@@ -86,7 +91,9 @@ class DailyReportGenerator:
'date': date, 'date': date,
'ships': ships, 'ships': ships,
'total_teu': total_teu, 'total_teu': total_teu,
'ship_count': len(ships) 'ship_count': len(ships),
'adjustments': [],
'total_adjustment_teu': 0
} }
except Exception as e: except Exception as e:
@@ -95,7 +102,9 @@ class DailyReportGenerator:
'date': date, 'date': date,
'ships': {}, 'ships': {},
'total_teu': 0, 'total_teu': 0,
'ship_count': 0 'ship_count': 0,
'adjustments': [],
'total_adjustment_teu': 0
} }
def get_monthly_stats(self, date: str) -> Dict[str, Any]: def get_monthly_stats(self, date: str) -> Dict[str, Any]:
@@ -121,7 +130,7 @@ class DailyReportGenerator:
and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date and datetime.strptime(log['date'], '%Y-%m-%d').date() <= target_date
] ]
# 按日期汇总 # 按日期汇总原始数据
daily_totals: Dict[str, int] = {} daily_totals: Dict[str, int] = {}
for log in monthly_logs: for log in monthly_logs:
d = log['date'] d = log['date']
@@ -130,6 +139,25 @@ class DailyReportGenerator:
if log.get('teu'): if log.get('teu'):
daily_totals[d] += log['teu'] daily_totals[d] += log['teu']
# 获取当月所有日期的调整数据
total_adjustment_teu = 0
adjustment_details: Dict[str, Dict[str, int]] = {}
# 获取当月所有日期的调整数据
for day in range(1, target_date.day + 1):
day_str = f"{year_month}-{day:02d}"
if day_str <= date: # 只统计到指定日期
# 获取该日期的调整数据
if hasattr(self.db, 'get_daily_data_with_adjustments'):
daily_data = self.db.get_daily_data_with_adjustments(day_str)
adjustment_teu = daily_data.get('total_adjustment_teu', 0)
if adjustment_teu != 0:
total_adjustment_teu += adjustment_teu
adjustment_details[day_str] = {
'adjustment_teu': adjustment_teu,
'total_teu': daily_data.get('total_teu', 0)
}
# 计算当月天数(已过的天数) # 计算当月天数(已过的天数)
current_date = datetime.strptime(date, '%Y-%m-%d') current_date = datetime.strptime(date, '%Y-%m-%d')
if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL: if current_date.day == config.FIRST_DAY_OF_MONTH_SPECIAL:
@@ -141,7 +169,8 @@ class DailyReportGenerator:
unaccounted = self.db.get_unaccounted(year_month) unaccounted = self.db.get_unaccounted(year_month)
planned = days_passed * config.DAILY_TARGET_TEU planned = days_passed * config.DAILY_TARGET_TEU
actual = sum(daily_totals.values()) + unaccounted # 实际作业量 = 原始数据总计 + 未统计数据 + 调整数据总计
actual = sum(daily_totals.values()) + unaccounted + total_adjustment_teu
completion = round(actual / planned * 100, 2) if planned > 0 else 0 completion = round(actual / planned * 100, 2) if planned > 0 else 0
@@ -151,8 +180,10 @@ class DailyReportGenerator:
'planned': planned, 'planned': planned,
'actual': actual, 'actual': actual,
'unaccounted': unaccounted, 'unaccounted': unaccounted,
'adjustment_total': total_adjustment_teu,
'completion': completion, 'completion': completion,
'daily_totals': daily_totals 'daily_totals': daily_totals,
'adjustment_details': adjustment_details
} }
except Exception as e: except Exception as e:
@@ -163,8 +194,10 @@ class DailyReportGenerator:
'planned': 0, 'planned': 0,
'actual': 0, 'actual': 0,
'unaccounted': 0, 'unaccounted': 0,
'adjustment_total': 0,
'completion': 0, 'completion': 0,
'daily_totals': {} 'daily_totals': {},
'adjustment_details': {}
} }
def get_shift_personnel(self, date: str) -> Dict[str, str]: def get_shift_personnel(self, date: str) -> Dict[str, str]:
@@ -279,6 +312,7 @@ class DailyReportGenerator:
ship_lines.append(f"作业量:{teu}TEU{size_str}") ship_lines.append(f"作业量:{teu}TEU{size_str}")
else: else:
ship_lines.append(f"作业量:{teu}TEU") ship_lines.append(f"作业量:{teu}TEU")
lines.extend(ship_lines) lines.extend(ship_lines)
lines.append("") lines.append("")