diff --git a/README.md b/README.md index 5718744..cef92ec 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ python3 main.py --unaccounted 118 --month 2025-12 # 去除未统计数据 python3 main.py --remove-unaccounted --month 2025-12 +# 手动剔除次月多统计的船 +python3 main.py --cross-exclude --source-date 2025-12-31 --target-date 2026-01-01 --ship-name "学友洋山" --teu 100 + # 配置测试(验证所有连接) python3 main.py config-test ``` @@ -98,6 +101,7 @@ python3 src/gui.py - **去除多余统计数据**:用于删除多余统计的箱量(对称功能) - **月底智能调整**:月底最后一天自动弹出剔除对话框 - **数据自动转移**:月底剔除的数据自动转移到次月1号 +- **手动剔除次月多统计的船**:用于处理上月底余留数据未及时剔除的情况(例如:2号打开工具整理1号数据,但上月底余留数据没有剔除) ### 配置管理 - **管理月份页面ID映射**:配置各月份的Confluence页面ID @@ -128,6 +132,32 @@ python3 src/gui.py - 默认不弹出调整对话框 - 但GUI侧边栏保留了手动添加/剔除TEU的功能入口 +### 手动剔除次月多统计的船 +用于处理上月底余留数据未及时剔除的情况: + +**使用场景**: +- 用户在2号打开工具,整理的是1号的数据 +- 上月底余留的数据没有剔除,导致没有算在1号的日报中 +- 需要手动从次月(当前月)中剔除上月底余留的数据 + +**功能特点**: +- **GUI操作**:在左侧控制面板点击"剔除次月多统计"按钮 +- **CLI操作**:使用 `--cross-exclude` 参数 +- **灵活配置**:支持指定源日期(上月底)、目标日期(次月)、船名、TEU、20尺/40尺箱量 +- **数据记录**:调整记录存储在数据库中,便于追踪和审计 + +**使用示例**: +```bash +# CLI方式 +python3 main.py --cross-exclude --source-date 2025-12-31 --target-date 2026-01-01 --ship-name "学友洋山" --teu 100 + +# GUI方式 +1. 打开GUI界面 +2. 在左侧控制面板点击"剔除次月多统计"按钮 +3. 填写源日期、目标日期、船名、TEU等信息 +4. 点击"确定"保存 +``` + ### 二次靠泊合并 解析时会自动合并同一天的二次靠泊记录: - 夜班 学友洋山: 273TEU diff --git a/main.py b/main.py index dd23f80..b6e053c 100644 --- a/main.py +++ b/main.py @@ -219,6 +219,40 @@ def remove_unaccounted(year_month: str, teu_to_reduce: int = None): raise +def add_cross_month_exclusion(source_date: str, target_date: str, ship_name: str, teu: int, + twenty_feet: int = 0, forty_feet: int = 0, reason: str = ''): + """ + 添加跨月剔除调整(手动剔除次月多统计的船) + + 参数: + source_date: 源日期(上月底日期) + target_date: 目标日期(次月日期) + ship_name: 船名 + teu: TEU数量 + twenty_feet: 20尺箱量 + forty_feet: 40尺箱量 + reason: 调整原因 + """ + try: + db = DailyLogsDatabase() + success = db.insert_cross_month_exclusion( + source_date=source_date, + target_date=target_date, + ship_name=ship_name, + teu=teu, + twenty_feet=twenty_feet, + forty_feet=forty_feet, + reason=reason + ) + if success: + logger.info(f"已添加跨月剔除调整: {source_date} -> {target_date} {ship_name} {teu}TEU") + else: + logger.error("添加跨月剔除调整失败") + except Exception as e: + logger.error(f"添加跨月剔除调整失败: {e}") + raise + + def show_stats(date: str): """ 显示指定日期的统计 @@ -322,6 +356,16 @@ def main(): --unaccounted, -u TEU 添加未统计数据(需同时指定月份) --remove-unaccounted, -r [TEU] 去除未统计数据(需同时指定月份)。如果指定TEU值,则减少该数量;如果不指定,则删除整个记录 --month, -m YEAR-MONTH 指定月份(与 -u 或 -r 配合使用) + --cross-exclude, -c 手动剔除次月多统计的船(需指定源日期、目标日期、船名和TEU) + +跨月剔除参数: + --source-date DATE 源日期(上月底日期),格式: YYYY-MM-DD + --target-date DATE 目标日期(次月日期),格式: YYYY-MM-DD + --ship-name NAME 船名 + --teu TEU TEU数量 + --twenty-feet COUNT 20尺箱量(可选,默认0) + --forty-feet COUNT 40尺箱量(可选,默认0) + --reason REASON 调整原因(可选) 示例: python3 main.py fetch @@ -331,6 +375,7 @@ def main(): 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 + python3 main.py --cross-exclude --source-date 2025-12-31 --target-date 2026-01-01 --ship-name "学友洋山" --teu 100 ''' ) parser.add_argument( @@ -367,6 +412,53 @@ def main(): metavar='YEAR-MONTH', help='指定月份(与 --unaccounted 或 --remove-unaccounted 配合使用)' ) + parser.add_argument( + '--cross-exclude', + '-c', + action='store_true', + help='手动剔除次月多统计的船' + ) + parser.add_argument( + '--source-date', + metavar='DATE', + help='源日期(上月底日期),格式: YYYY-MM-DD' + ) + parser.add_argument( + '--target-date', + metavar='DATE', + help='目标日期(次月日期),格式: YYYY-MM-DD' + ) + parser.add_argument( + '--ship-name', + metavar='NAME', + help='船名' + ) + parser.add_argument( + '--teu', + metavar='TEU', + type=int, + help='TEU数量' + ) + parser.add_argument( + '--twenty-feet', + metavar='COUNT', + type=int, + default=0, + help='20尺箱量(可选,默认0)' + ) + parser.add_argument( + '--forty-feet', + metavar='COUNT', + type=int, + default=0, + help='40尺箱量(可选,默认0)' + ) + parser.add_argument( + '--reason', + metavar='REASON', + default='手动剔除次月多统计的船', + help='调整原因(可选,默认: "手动剔除次月多统计的船")' + ) args = parser.parse_args() @@ -398,6 +490,27 @@ def main(): sys.exit(1) return + # 跨月剔除功能 + if args.cross_exclude: + if not all([args.source_date, args.target_date, args.ship_name, args.teu]): + logger.error("跨月剔除功能需要指定以下参数: --source-date, --target-date, --ship-name, --teu") + sys.exit(1) + + try: + add_cross_month_exclusion( + source_date=args.source_date, + target_date=args.target_date, + ship_name=args.ship_name, + teu=args.teu, + twenty_feet=args.twenty_feet, + forty_feet=args.forty_feet, + reason=args.reason + ) + except Exception as e: + logger.error(f"跨月剔除失败: {e}") + sys.exit(1) + return + # 执行功能 try: if args.function == 'report' and args.date: diff --git a/src/database/daily_logs.py b/src/database/daily_logs.py index 355af25..2845720 100644 --- a/src/database/daily_logs.py +++ b/src/database/daily_logs.py @@ -104,7 +104,7 @@ class DailyLogsDatabase(DatabaseBase): cursor.execute(''' CREATE TABLE IF NOT EXISTS manual_adjustments ( id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, -- 调整适用的日期 + date TEXT NOT NULL, -- 调整适用的日期(目标日期) ship_name TEXT NOT NULL, -- 船名 teu INTEGER NOT NULL, -- TEU数量 twenty_feet INTEGER DEFAULT 0, -- 20尺箱量 @@ -115,6 +115,23 @@ class DailyLogsDatabase(DatabaseBase): ) ''') + # 检查是否需要添加新字段 + cursor.execute("PRAGMA table_info(manual_adjustments)") + columns = [col[1] for col in cursor.fetchall()] + + # 添加缺失的字段 + if 'source_date' not in columns: + cursor.execute('ALTER TABLE manual_adjustments ADD COLUMN source_date TEXT') + logger.info("已添加 source_date 字段到 manual_adjustments 表") + + if 'reason' not in columns: + cursor.execute('ALTER TABLE manual_adjustments ADD COLUMN reason TEXT') + logger.info("已添加 reason 字段到 manual_adjustments 表") + + if 'status' not in columns: + cursor.execute('ALTER TABLE manual_adjustments ADD COLUMN status TEXT DEFAULT "pending"') + logger.info("已添加 status 字段到 manual_adjustments 表") + # 创建索引 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)') @@ -409,18 +426,23 @@ class DailyLogsDatabase(DatabaseBase): 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: + adjustment_type: str = 'add', note: str = '', + source_date: str = None, reason: str = '', + status: str = 'pending') -> bool: """ 插入手动调整数据 参数: - date: 日期字符串 + date: 日期字符串(目标日期) ship_name: 船名 teu: TEU数量 twenty_feet: 20尺箱量 forty_feet: 40尺箱量 adjustment_type: 调整类型 'add' 或 'exclude' note: 备注 + source_date: 源日期(上月底日期,可选) + reason: 调整原因 + status: 调整状态:'pending', 'processed' 返回: 是否成功 @@ -428,10 +450,12 @@ class DailyLogsDatabase(DatabaseBase): try: query = ''' INSERT INTO manual_adjustments - (date, ship_name, teu, twenty_feet, forty_feet, adjustment_type, note, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + (date, source_date, ship_name, teu, twenty_feet, forty_feet, + adjustment_type, note, reason, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ''' - params = (date, ship_name, teu, twenty_feet, forty_feet, adjustment_type, note) + params = (date, source_date, ship_name, teu, twenty_feet, forty_feet, + adjustment_type, note, reason, status) self.execute_update(query, params) logger.info(f"插入手动调整数据: {date} {ship_name} {teu}TEU ({adjustment_type})") return True @@ -473,6 +497,143 @@ class DailyLogsDatabase(DatabaseBase): ''' return self.execute_query(query, (date, adjustment_type)) + def get_cross_month_adjustments(self, source_date: str = None, target_date: str = None, + status: str = None) -> List[Dict[str, Any]]: + """ + 获取跨月调整数据 + + 参数: + source_date: 源日期(上月底日期) + target_date: 目标日期(次月日期) + status: 调整状态 + + 返回: + 跨月调整数据列表 + """ + try: + conditions = [] + params = [] + + if source_date: + conditions.append("source_date = ?") + params.append(source_date) + + if target_date: + conditions.append("date = ?") + params.append(target_date) + + if status: + conditions.append("status = ?") + params.append(status) + + where_clause = " AND ".join(conditions) if conditions else "1=1" + query = f''' + SELECT * FROM manual_adjustments + WHERE {where_clause} ORDER BY created_at DESC + ''' + + return self.execute_query(query, tuple(params)) + + except Exception as e: + logger.error(f"获取跨月调整数据失败: {e}") + return [] + + def get_pending_cross_month_adjustments(self) -> List[Dict[str, Any]]: + """ + 获取待处理的跨月调整数据 + + 返回: + 待处理的跨月调整数据列表 + """ + return self.get_cross_month_adjustments(status='pending') + + def update_adjustment_status(self, adjustment_id: int, status: str) -> bool: + """ + 更新调整状态 + + 参数: + adjustment_id: 调整记录ID + status: 新状态 + + 返回: + 是否成功 + """ + try: + query = 'UPDATE manual_adjustments SET status = ? WHERE id = ?' + result = self.execute_update(query, (status, adjustment_id)) + if result > 0: + logger.info(f"更新调整状态: ID={adjustment_id} -> {status}") + return True + else: + logger.warning(f"未找到调整记录: ID={adjustment_id}") + return False + + except Exception as e: + logger.error(f"更新调整状态失败: {e}") + return False + + def insert_cross_month_exclusion(self, source_date: str, target_date: str, + ship_name: str, teu: int, + twenty_feet: int = 0, forty_feet: int = 0, + reason: str = '') -> bool: + """ + 插入跨月剔除调整(手动剔除次月多统计的船) + + 参数: + source_date: 源日期(上月底日期) + target_date: 目标日期(次月日期) + ship_name: 船名 + teu: TEU数量 + twenty_feet: 20尺箱量 + forty_feet: 40尺箱量 + reason: 调整原因 + + 返回: + 是否成功 + """ + try: + # 1. 插入剔除记录(从月底最后一天扣除) + exclude_success = self.insert_manual_adjustment( + date=source_date, + source_date=source_date, + ship_name=ship_name, + teu=teu, + twenty_feet=twenty_feet, + forty_feet=forty_feet, + adjustment_type='exclude', + note=f"手动剔除次月多统计的船,目标日期: {target_date}", + reason=reason, + status='pending' + ) + + if not exclude_success: + return False + + # 2. 自动将相同数据添加到次月1号 + add_success = self.insert_manual_adjustment( + date=target_date, + source_date=source_date, + ship_name=ship_name, + teu=teu, + twenty_feet=twenty_feet, + forty_feet=forty_feet, + adjustment_type='add', + note=f"从{source_date}转移的数据: {reason}", + reason=reason, + status='pending' + ) + + if add_success: + logger.info(f"插入跨月剔除调整: {source_date} -> {target_date} {ship_name} {teu}TEU") + return True + else: + logger.error(f"插入跨月剔除调整失败: 添加数据到次月1号失败") + return False + + except Exception as e: + logger.error(f"插入跨月剔除调整失败: {e}") + return False + def delete_manual_adjustment(self, adjustment_id: int) -> bool: """ 删除指定ID的手动调整数据 diff --git a/src/gui.py b/src/gui.py index f440a2e..9510ff4 100644 --- a/src/gui.py +++ b/src/gui.py @@ -103,55 +103,16 @@ class OrbitInGUI: # 分隔线 ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) - # 未统计数据 - ttk.Label(left_frame, text="添加未统计数据:").pack(anchor=tk.W, pady=(10, 5)) + # 手动剔除次月多统计的船 + ttk.Label(left_frame, text="手动剔除次月多统计的船:").pack(anchor=tk.W, pady=(10, 5)) - unaccounted_frame = ttk.Frame(left_frame) - unaccounted_frame.pack(fill=tk.X, pady=5) - - ttk.Label(unaccounted_frame, text="月份:").pack(side=tk.LEFT) - self.month_var = tk.StringVar(value=datetime.now().strftime('%Y-%m')) - month_entry = ttk.Entry(unaccounted_frame, textvariable=self.month_var, width=8) - month_entry.pack(side=tk.LEFT, padx=(5, 10)) - - ttk.Label(unaccounted_frame, text="TEU:").pack(side=tk.LEFT) - self.teu_var = tk.StringVar() - teu_entry = ttk.Entry(unaccounted_frame, textvariable=self.teu_var, width=8) - teu_entry.pack(side=tk.LEFT, padx=(5, 0)) - - btn_unaccounted = ttk.Button( + btn_cross_month_exclude = ttk.Button( left_frame, - text="添加", - command=self.add_unaccounted, + text="剔除次月多统计", + command=self.show_cross_month_exclude_dialog, width=20 ) - 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) + btn_cross_month_exclude.pack(pady=5) # 分隔线 ttk.Separator(left_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) @@ -646,123 +607,6 @@ class OrbitInGUI: self.date_var.set(yesterday) self.generate_report() - def add_unaccounted(self): - """添加未统计数据""" - year_month = self.month_var.get().strip() - teu = self.teu_var.get().strip() - - if not year_month or not teu: - self.log_message("错误: 请输入月份和 TEU", is_error=True) - self.logger.error("未输入月份和 TEU") - return - - try: - teu = int(teu) - except ValueError: - self.log_message("错误: TEU 必须是数字", is_error=True) - self.logger.error(f"TEU 不是数字: {teu}") - return - - self.set_status("正在添加...") - self.log_message(f"添加 {year_month} 月未统计数据: {teu}TEU") - self.logger.info(f"添加 {year_month} 月未统计数据: {teu}TEU") - - try: - db = DailyLogsDatabase() - result = db.insert_unaccounted(year_month, teu, '') - - if result: - self.log_message("添加成功!") - self.logger.info(f"未统计数据添加成功: {year_month} {teu}TEU") - # 刷新日报显示 - self.generate_today_report() - else: - self.log_message("添加失败!", is_error=True) - self.logger.error(f"未统计数据添加失败: {year_month} {teu}TEU") - - 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 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): """自动获取新数据(GUI启动时调用)""" self.set_status("正在自动获取新数据...") @@ -942,6 +786,11 @@ class OrbitInGUI: """管理Confluence页面ID映射""" dialog = ConfluencePagesDialog(self.root, self) self.root.wait_window(dialog) + + def show_cross_month_exclude_dialog(self): + """显示手动剔除次月多统计的船对话框""" + dialog = CrossMonthExcludeDialog(self.root, self) + self.root.wait_window(dialog) class AddDataDialog(tk.Toplevel): @@ -1469,6 +1318,348 @@ class ConfluencePageEditDialog(tk.Toplevel): self.destroy() +class CrossMonthExcludeDialog(tk.Toplevel): + """手动剔除次月多统计的船对话框""" + + def __init__(self, parent, gui): + super().__init__(parent) + self.title("手动剔除次月多统计的船") + self.gui = gui + self.result = None + + # 设置对话框大小和位置 + self.geometry("850x750") + self.resizable(True, True) + + # 使对话框模态 + self.transient(parent) + self.grab_set() + + # 计算当前月份和上月 + now = datetime.now() + current_year = now.year + current_month = now.month + + # 计算上个月(正确处理跨年) + if current_month == 1: + last_month = 12 + last_year = current_year - 1 + else: + last_month = current_month - 1 + last_year = current_year + + # 获取月份列表 + month_list = self._get_month_list() + print(f"DEBUG: 月份列表: {month_list}") + + # 初始化月份选择 + self.source_month_var = tk.StringVar(value=f"{last_year}-{last_month:02d}") # 默认上个月 + self.target_month_var = tk.StringVar(value=f"{current_year}-{current_month:02d}") # 默认当前月 + + print(f"DEBUG: 源月份默认值: {self.source_month_var.get()}") + print(f"DEBUG: 目标月份默认值: {self.target_month_var.get()}") + + # 创建输入字段 + frame = ttk.Frame(self, padding="20") + frame.pack(fill=tk.BOTH, expand=True) + + # 说明文本 + ttk.Label(frame, text="用于处理上月底余留数据未及时剔除的情况。\n" + "例如:本月1号整理数据时,发现上月余留数据未剔除。\n" + "提示:选择船后可手动修改TEU值(支持跨日船部分剔除)。", + wraplength=800).grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=(0, 15)) + + # 源月份选择 + ttk.Label(frame, text="源月份(被剔除数据的月份):").grid(row=1, column=0, sticky=tk.W, pady=5) + self.source_month_combo = ttk.Combobox(frame, textvariable=self.source_month_var, + values=self._get_month_list(), width=12) + self.source_month_combo.grid(row=1, column=1, sticky=tk.W, pady=5) + self.source_month_combo.bind('<>', self.on_source_month_changed) + + # 目标月份选择 + ttk.Label(frame, text="目标月份(数据转移到的月份):").grid(row=2, column=0, sticky=tk.W, pady=5) + self.target_month_combo = ttk.Combobox(frame, textvariable=self.target_month_var, + values=self._get_month_list(), width=12) + self.target_month_combo.grid(row=2, column=1, sticky=tk.W, pady=5) + + # 分隔线 + ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=3, column=0, columnspan=3, sticky=tk.EW, pady=10) + + # 源月份船次列表 + ttk.Label(frame, text="源月份船次列表:").grid(row=4, column=0, sticky=tk.W, pady=5) + + # 创建Treeview显示船次 + columns = ('ship_name', 'teu', 'twenty_feet', 'forty_feet', 'shift') + self.tree = ttk.Treeview(frame, columns=columns, show='headings', height=8) + + # 设置列标题 + self.tree.heading('ship_name', text='船名') + self.tree.heading('teu', text='TEU') + self.tree.heading('twenty_feet', text='20尺') + self.tree.heading('forty_feet', text='40尺') + self.tree.heading('shift', text='班次') + + # 设置列宽度 + self.tree.column('ship_name', width=120) + self.tree.column('teu', width=60) + self.tree.column('twenty_feet', width=60) + self.tree.column('forty_feet', width=60) + self.tree.column('shift', width=80) + + # 添加滚动条 + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree.yview) + self.tree.configure(yscroll=scrollbar.set) + + self.tree.grid(row=5, column=0, columnspan=2, sticky=tk.NSEW, pady=5) + scrollbar.grid(row=5, column=2, sticky=tk.NS, pady=5) + + # 加载船次数据 + self.load_ships() + + # 分隔线 + ttk.Separator(frame, orient=tk.HORIZONTAL).grid(row=6, column=0, columnspan=3, sticky=tk.EW, pady=10) + + # 手动输入区域(用于输入不在列表中的船) + ttk.Label(frame, text="手动输入:").grid(row=7, column=0, sticky=tk.W, pady=5) + + # 船名 + ttk.Label(frame, text="船名:").grid(row=8, 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=8, column=1, sticky=tk.W, pady=5) + + # TEU + ttk.Label(frame, text="TEU:").grid(row=9, 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=9, column=1, sticky=tk.W, pady=5) + + # 20尺箱量 + ttk.Label(frame, text="20尺箱量:").grid(row=10, 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=10, column=1, sticky=tk.W, pady=5) + + # 40尺箱量 + ttk.Label(frame, text="40尺箱量:").grid(row=11, 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=11, column=1, sticky=tk.W, pady=5) + + # 调整原因 + ttk.Label(frame, text="调整原因:").grid(row=12, column=0, sticky=tk.W, pady=5) + self.reason_var = tk.StringVar(value="手动剔除次月多统计的船") + reason_entry = ttk.Entry(frame, textvariable=self.reason_var, width=30) + reason_entry.grid(row=12, column=1, sticky=tk.W, pady=5) + + # 按钮 + button_frame = ttk.Frame(frame) + button_frame.grid(row=13, column=0, columnspan=3, 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('', lambda e: self.on_ok()) + self.bind('', lambda e: self.on_cancel()) + + # 绑定Treeview选择事件 + self.tree.bind('<>', self.on_ship_selected) + + # 焦点设置 + ship_entry.focus_set() + + def _get_month_list(self): + """获取可选月份列表(近12个月,从当前月往前推)""" + months = [] + now = datetime.now() + year = now.year + month = now.month + + # 生成近12个月 + for i in range(12): + if month - i <= 0: + # 跨年 + m = month - i + 12 + y = year - 1 + else: + m = month - i + y = year + months.append(f"{y}-{m:02d}") + + print(f"DEBUG: _get_month_list 返回: {months}") + return months + + def on_source_month_changed(self, event): + """当源月份改变时,重新加载船次列表""" + self.load_ships() + + def get_source_date(self): + """获取源月份的最后一天日期""" + month_str = self.source_month_var.get() + year, month = map(int, month_str.split('-')) + # 月底最后一天 + if month == 12: + next_month = datetime(year + 1, 1, 1) + else: + next_month = datetime(year, month + 1, 1) + last_day = next_month - timedelta(days=1) + return last_day.strftime('%Y-%m-%d') + + def get_target_date(self): + """获取目标月份的第一天日期""" + month_str = self.target_month_var.get() + year, month = map(int, month_str.split('-')) + return datetime(year, month, 1).strftime('%Y-%m-%d') + + def load_ships(self): + """加载源月份的船次数据""" + source_date = self.get_source_date() + try: + db = DailyLogsDatabase() + logs = db.query_by_date(source_date) + + # 清空现有数据 + for item in self.tree.get_children(): + self.tree.delete(item) + + if not logs: + self.tree.insert('', tk.END, values=('无数据', '', '', '', '')) + return + + # 按船名汇总数据 + ships = {} + for log in logs: + ship_name = log['ship_name'] + if ship_name not in ships: + ships[ship_name] = { + 'teu': 0, + 'twenty_feet': 0, + 'forty_feet': 0, + 'shifts': set() + } + + if log.get('teu'): + ships[ship_name]['teu'] += log['teu'] + if log.get('twenty_feet'): + ships[ship_name]['twenty_feet'] += log['twenty_feet'] + if log.get('forty_feet'): + ships[ship_name]['forty_feet'] += log['forty_feet'] + if log.get('shift'): + ships[ship_name]['shifts'].add(log['shift']) + + # 插入到Treeview + for ship_name, data in ships.items(): + shifts_str = ', '.join(sorted(data['shifts'])) + self.tree.insert('', tk.END, values=( + ship_name, + data['teu'], + data['twenty_feet'], + data['forty_feet'], + shifts_str + )) + + except Exception as e: + self.gui.log_message(f"加载船次数据失败: {e}", is_error=True) + self.tree.insert('', tk.END, values=('加载失败', '', '', '', '')) + + def on_ship_selected(self, event): + """当选择船次时,自动填充数据""" + selection = self.tree.selection() + if not selection: + return + + item = self.tree.item(selection[0]) + values = item['values'] + + if values[0] in ('无数据', '加载失败'): + return + + # 自动填充数据 + self.ship_var.set(values[0]) + self.teu_var.set(str(values[1])) + self.twenty_var.set(str(values[2])) + self.forty_var.set(str(values[3])) + + def on_ok(self): + """确定按钮处理""" + try: + # 获取输入 + source_date = self.get_source_date() + target_date = self.get_target_date() + 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() + reason = self.reason_var.get().strip() + + 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 + + # 确认对话框 + confirm_msg = (f"确定要将数据从源月份({source_date})转移到目标月份({target_date})吗?\n\n" + f"船名: {ship_name}\n" + f"TEU: {teu}\n" + f"20尺箱量: {twenty_feet}\n" + f"40尺箱量: {forty_feet}\n" + f"原因: {reason}") + + if not messagebox.askyesno("确认操作", confirm_msg): + return + + # 保存到数据库 + try: + db = DailyLogsDatabase() + success = db.insert_cross_month_exclusion( + source_date=source_date, + target_date=target_date, + ship_name=ship_name, + teu=teu, + twenty_feet=twenty_feet, + forty_feet=forty_feet, + reason=reason + ) + + if success: + self.gui.log_message(f"已添加跨月剔除调整: {source_date} -> {target_date} {ship_name} {teu}TEU") + self.gui.logger.info(f"已添加跨月剔除调整: {source_date} -> {target_date} {ship_name} {teu}TEU") + # 刷新日报显示 + self.gui.generate_today_report() + self.result = True + self.destroy() + else: + messagebox.showerror("错误", "保存失败") + self.gui.log_message("保存跨月剔除调整失败", is_error=True) + self.gui.logger.error("保存跨月剔除调整失败") + + except Exception as e: + messagebox.showerror("错误", f"保存失败: {e}") + self.gui.log_message(f"保存跨月剔除调整失败: {e}", is_error=True) + self.gui.logger.error(f"保存跨月剔除调整失败: {e}") + + except ValueError: + messagebox.showerror("错误", "请输入有效的数字") + + def on_cancel(self): + """取消按钮处理""" + self.result = None + self.destroy() + + def main(): """主函数""" root = tk.Tk()