fix: 修复月份选择器问题,确保12月正确显示

- 修复跨年月份计算逻辑(1月时正确计算为去年12月)
- 改进_get_month_list()方法,生成正确的近12个月列表
- 增加Combobox宽度以完整显示月份值如'2025-12'
- 优化手动剔除次月多统计的船对话框
This commit is contained in:
2026-01-02 02:46:56 +08:00
parent 53eef800b4
commit bb3f25a643
4 changed files with 663 additions and 168 deletions

View File

@@ -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

113
main.py
View File

@@ -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:

View File

@@ -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的手动调整数据

View File

@@ -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("正在自动获取新数据...")
@@ -943,6 +787,11 @@ class OrbitInGUI:
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('<<ComboboxSelected>>', 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('<Return>', lambda e: self.on_ok())
self.bind('<Escape>', lambda e: self.on_cancel())
# 绑定Treeview选择事件
self.tree.bind('<<TreeviewSelect>>', 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()