v1.1.0: 版本更新 - 增强规格解析能力、修复条码映射功能、改进特殊条码处理

This commit is contained in:
侯欢 2025-05-30 10:24:30 +08:00
parent c0fceea9dc
commit b3c175836a
10 changed files with 350 additions and 77 deletions

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Python缓存文件
__pycache__/
*.py[cod]
*$py.class
# 虚拟环境
venv/
env/
ENV/
# 日志文件
logs/*.log
logs/*.active
*.log.*
# 临时文件和缓存
data/temp/
data/*.bak
*.bak
.DS_Store
# 输出文件(可选是否忽略)
# data/output/
# IDE文件
.idea/
.vscode/
*.swp
*.swo

28
CHANGELOG.md Normal file
View File

@ -0,0 +1,28 @@
# 更新日志
## v1.1.0 (2025-05-30)
### 新特性
- 添加对特殊条码6958620703716的处理支持同时设置规格和条码映射
- 增强不规范规格格式的解析能力(如"IL*12"、"6oo*12"等)
- 支持带重量单位的规格解析(如"5kg*6"
### 修复
- 修复条码映射功能在特殊处理后不生效的问题
- 修复OrderService中缺少merge_all_purchase_orders方法导致合并采购单报错的问题
- 修复了条码映射对话框无法同时添加特殊处理和映射的问题
### 改进
- 改进了BarcodeMapper类使其支持同时进行特殊处理和条码映射
- 改进了规格解析逻辑,增加了对各种单位和格式的支持
- 添加条码映射对话框中可视化标记映射关系
- 更新了条码映射配置文件,增加了更多特殊条码处理
## v1.0.0 (2025-05-01)
### 初始版本
- 基础OCR识别功能
- Excel处理功能
- 采购单合并功能
- 烟草订单处理功能
- 图形用户界面

View File

@ -1,80 +1,43 @@
# 益选-OCR订单处理系统 # 益选-OCR订单处理系统
## 项目简介 一个集OCR识别、Excel处理和订单合并功能于一体的采购单处理系统。
益选-OCR订单处理系统是一款基于Python的图形化本地订单自动化处理工具支持采购单图片OCR识别、Excel数据处理、采购单合并、烟草订单专用处理等功能适用于中小型企业、商超、烟草公司等场景。
## 主要功能 ## 主要功能
- 图片采购单OCR识别自动生成标准Excel采购单
- Excel采购单智能处理与格式转换
- 多采购单合并为总单,支持批量处理
- 烟草公司订单明细专用处理与格式转换
- 条码映射与单位转换规则自定义
- 图形化界面,支持批量、单文件、完整流程一键处理
- 系统设置界面支持API、路径、性能等参数自定义
- 日志管理与处理结果预览
- 键盘快捷键支持
## 安装与运行 - **OCR识别**:识别图片中的商品信息,包括条码、名称、数量、单价等
### 1. 环境准备 - **Excel处理**将OCR识别结果处理成规范的Excel采购单
- 推荐Python 3.8及以上版本 - **采购单合并**:合并多个采购单,汇总相同商品
- Windows 10/11推荐支持部分Linux发行版 - **条码映射**:支持将特定条码映射为其他条码,适应不同系统要求
- **规格处理**:智能解析商品规格,实现单位自动转换
- **烟草订单处理**:专门处理烟草公司订单
### 2. 安装依赖 ## 技术特点
```bash
pip install -r requirements.txt
```
### 3. 启动程序 - 基于Python开发使用Tkinter构建图形界面
- 图形界面启动: - 采用模块化设计,易于扩展和维护
```bash - 自动处理各种不规范数据格式
python 启动器.py - 配置文件支持,可自定义各种处理参数
``` - 日志记录,便于问题排查
- 命令行模式:
```bash ## 使用方法
python run.py --help
``` 1. 运行`启动器.py`打开主界面
2. 根据需要选择相应功能按钮
3. 按照提示操作,完成数据处理
## 系统要求
## 依赖环境
- Python 3.8+ - Python 3.8+
- 主要依赖库tkinter、pandas、numpy、xlrd、xlwt、xlutils、requests、openpyxl 等 - 所需第三方库:详见`requirements.txt`
- 详见 requirements.txt
## 目录结构 ## 最近更新
```
├── app/ # 主程序模块
│ ├── config/ # 配置管理
│ ├── core/ # 核心功能OCR、Excel、工具等
│ ├── services/ # 服务层(业务逻辑)
│ └── ...
├── data/ # 输入输出与缓存目录
├── templates/ # Excel模板文件
├── logs/ # 日志文件
├── run.py # 命令行主入口
├── 启动器.py # 图形界面主入口
├── requirements.txt # 依赖包列表
├── README.md # 使用说明
├── 更新日志.md # 更新日志
└── ...
```
## 常见问题 请查看[更新日志](CHANGELOG.md)了解最新版本变更。
- **Q: 启动时报错缺少依赖?**
A: 请先运行 `pip install -r requirements.txt` 安装所有依赖。
- **Q: OCR识别失败或API报错**
A: 请在系统设置中正确填写API Key和Secret Key并确保网络畅通。
- **Q: 处理结果找不到?**
A: 默认输出在 `data/output/` 目录,可在系统设置中自定义。
- **Q: 如何自定义条码映射和单位规则?**
A: 通过"编辑条码映射"按钮进入图形化编辑界面。
- **Q: 其他问题?**
A: 请查看日志窗口或logs目录下日志文件或联系作者。
## 联系方式 ## 贡献者
- 作者:欢欢欢
- 邮箱huanhuanhuan@example.com
- QQ123456789
- Issues反馈请在项目仓库提交Issue
--- - 欢欢欢
© 2025 益选-OCR订单处理系统 by 欢欢欢 ## 版权
© 2025 益选-OCR订单处理系统

View File

@ -297,6 +297,17 @@ class UnitConverter:
except ValueError: except ValueError:
pass pass
# 处理带重量单位的规格如5kg*6、500g*12等
weight_match = re.match(r'([\d\.]+)(?:kg|g|克|千克|公斤)[*](\d+)', spec, re.IGNORECASE)
if weight_match:
try:
# 对于重量单位使用1作为一级包装后面的数字作为二级包装
level2 = int(weight_match.group(2))
logger.info(f"解析重量规格: {spec} -> 1*{level2}")
return 1, level2, None
except ValueError:
pass
# 处理带容量单位的规格如500ml*15, 1L*12等 # 处理带容量单位的规格如500ml*15, 1L*12等
ml_match = re.match(r'(\d+)(?:ml|毫升)[*](\d+)', spec, re.IGNORECASE) ml_match = re.match(r'(\d+)(?:ml|毫升)[*](\d+)', spec, re.IGNORECASE)
if ml_match: if ml_match:
@ -341,6 +352,17 @@ class UnitConverter:
except ValueError: except ValueError:
pass pass
# 处理不规范格式如IL*12, 6oo*12等从中提取数字部分作为包装数量
# 只要规格中包含*和数字,就尝试提取*后面的数字作为件数
irregular_match = re.search(r'[^0-9]*\*(\d+)', spec)
if irregular_match:
try:
level2 = int(irregular_match.group(1))
logger.info(f"解析不规范规格: {spec} -> 1*{level2}")
return 1, level2, None
except ValueError:
pass
# 默认值 # 默认值
logger.warning(f"无法解析规格: {spec}使用默认值1*1") logger.warning(f"无法解析规格: {spec}使用默认值1*1")
return 1, 1, None return 1, 1, None
@ -440,6 +462,12 @@ class UnitConverter:
'6923644268923': { '6923644268923': {
'map_to': '6923644268480', 'map_to': '6923644268480',
'description': '条码映射6923644268923 -> 6923644268480' 'description': '条码映射6923644268923 -> 6923644268480'
},
# 添加特殊条码6958620703716既需要特殊处理又需要映射
'6958620703716': {
'specification': '1*14',
'map_to': '6958620703907',
'description': '特殊处理: 规格1*14同时映射到6958620703907'
} }
} }

View File

@ -45,12 +45,6 @@ class BarcodeMapper:
special_config = self.special_barcodes[barcode] special_config = self.special_barcodes[barcode]
# 处理条码映射
if 'map_to' in special_config:
new_barcode = special_config['map_to']
logger.info(f"条码映射: {barcode} -> {new_barcode}")
result['barcode'] = new_barcode
# 处理特殊倍数 # 处理特殊倍数
if 'multiplier' in special_config: if 'multiplier' in special_config:
multiplier = special_config.get('multiplier', 1) multiplier = special_config.get('multiplier', 1)
@ -80,4 +74,10 @@ class BarcodeMapper:
result['price'] = new_price result['price'] = new_price
result['unit'] = target_unit result['unit'] = target_unit
# 处理条码映射 - 放在后面以便可以同时进行特殊处理和条码映射
if 'map_to' in special_config:
new_barcode = special_config['map_to']
logger.info(f"条码映射: {barcode} -> {new_barcode}")
result['barcode'] = new_barcode
return result return result

View File

@ -9,7 +9,7 @@
import os import os
import tkinter as tk import tkinter as tk
from tkinter import messagebox, ttk from tkinter import messagebox, ttk, simpledialog
from datetime import datetime from datetime import datetime
def create_custom_dialog(title="提示", message="", result_file=None, time_info=None, def create_custom_dialog(title="提示", message="", result_file=None, time_info=None,
@ -363,6 +363,45 @@ def create_barcode_mapping_dialog(parent=None, on_save=None, current_mappings=No
remove_special_btn = tk.Button(special_btn_frame, text="删除特殊处理", command=remove_special) remove_special_btn = tk.Button(special_btn_frame, text="删除特殊处理", command=remove_special)
remove_special_btn.pack(side=tk.LEFT, padx=5) remove_special_btn.pack(side=tk.LEFT, padx=5)
# 添加映射到特殊处理的功能
def add_mapping_to_special():
selected = special_tree.selection()
if not selected:
messagebox.showwarning("未选择", "请先选择要添加映射的特殊处理条目")
return
# 获取选中项
item = special_tree.item(selected[0])
barcode = item['values'][0]
# 弹出对话框输入映射目标
target_barcode = tk.simpledialog.askstring("添加映射", f"为条码 {barcode} 添加映射目标条码:")
if not target_barcode:
return
# 更新特殊处理列表中的项
for i, (b, mult, unit, price, spec, desc) in enumerate(special_list):
if b == barcode:
# 如果描述中已有映射信息,更新它
if "映射到:" in desc:
desc = desc.split("映射到:")[0].strip()
# 添加映射信息到描述
new_desc = f"{desc} 映射到: {target_barcode}"
special_list[i] = (b, mult, unit, price, spec, new_desc)
# 更新显示
special_tree.item(selected[0], values=(b, mult, unit, price, spec, new_desc))
# 标记该条码有映射
special_tree.item(selected[0], tags=("mapped",))
special_tree.tag_configure("mapped", foreground="blue")
break
map_special_btn = tk.Button(special_btn_frame, text="添加条码映射", command=add_mapping_to_special)
map_special_btn.pack(side=tk.LEFT, padx=5)
# 底部按钮区域 # 底部按钮区域
bottom_frame = tk.Frame(dialog) bottom_frame = tk.Frame(dialog)
bottom_frame.pack(fill=tk.X, padx=10, pady=10) bottom_frame.pack(fill=tk.X, padx=10, pady=10)
@ -380,7 +419,9 @@ def create_barcode_mapping_dialog(parent=None, on_save=None, current_mappings=No
# 添加特殊处理 # 添加特殊处理
for barcode, multiplier, unit, price, spec, desc in special_list: for barcode, multiplier, unit, price, spec, desc in special_list:
mappings[barcode] = {} # 检查该条码是否已存在
if barcode not in mappings:
mappings[barcode] = {}
if multiplier: if multiplier:
try: try:
@ -411,7 +452,19 @@ def create_barcode_mapping_dialog(parent=None, on_save=None, current_mappings=No
if spec: if spec:
mappings[barcode]['specification'] = spec mappings[barcode]['specification'] = spec
if desc: # 检查描述中是否包含映射信息
if desc and "映射到:" in desc:
parts = desc.split("映射到:")
base_desc = parts[0].strip()
target_barcode = parts[1].strip()
# 设置基本描述
if base_desc:
mappings[barcode]['description'] = base_desc
# 设置映射目标
mappings[barcode]['map_to'] = target_barcode
elif desc:
mappings[barcode]['description'] = desc mappings[barcode]['description'] = desc
# 调用保存回调 # 调用保存回调

View File

@ -69,6 +69,29 @@ class OrderService:
""" """
return self.order_merger.get_purchase_orders() return self.order_merger.get_purchase_orders()
def merge_purchase_orders(self, file_paths: List[str]) -> Optional[str]:
"""
合并指定的采购单文件
Args:
file_paths: 采购单文件路径列表
Returns:
合并后的采购单文件路径如果合并失败则返回None
"""
logger.info(f"OrderService开始合并指定采购单: {file_paths}")
return self.merge_orders(file_paths)
def merge_all_purchase_orders(self) -> Optional[str]:
"""
合并所有可用的采购单文件
Returns:
合并后的采购单文件路径如果合并失败则返回None
"""
logger.info("OrderService开始合并所有采购单")
return self.merge_orders(None)
def merge_orders(self, file_paths: Optional[List[str]] = None) -> Optional[str]: def merge_orders(self, file_paths: Optional[List[str]] = None) -> Optional[str]:
""" """
合并采购单 合并采购单

88
clean.py Normal file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
清理脚本 - 用于删除无关的文件和日志
"""
import os
import shutil
import glob
def clean_logs():
"""清理日志文件"""
print("清理日志文件...")
# 删除.active文件
active_files = glob.glob("logs/*.active")
for file in active_files:
try:
os.remove(file)
print(f"已删除: {file}")
except Exception as e:
print(f"删除文件时出错 {file}: {e}")
# 保留最新的日志,删除旧的备份
log_files = glob.glob("logs/*.log.*")
for file in log_files:
try:
os.remove(file)
print(f"已删除: {file}")
except Exception as e:
print(f"删除文件时出错 {file}: {e}")
def clean_temp_files():
"""清理临时文件"""
print("清理临时文件...")
# 清空临时目录
temp_dir = "data/temp"
if os.path.exists(temp_dir):
for file in os.listdir(temp_dir):
file_path = os.path.join(temp_dir, file)
try:
if os.path.isfile(file_path):
os.remove(file_path)
print(f"已删除: {file_path}")
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
print(f"已删除目录: {file_path}")
except Exception as e:
print(f"删除文件时出错 {file_path}: {e}")
# 删除备份文件
backup_files = glob.glob("data/*.bak") + glob.glob("config/*.bak")
for file in backup_files:
try:
os.remove(file)
print(f"已删除: {file}")
except Exception as e:
print(f"删除文件时出错 {file}: {e}")
def clean_pycache():
"""清理Python缓存文件"""
print("清理Python缓存文件...")
# 查找并删除所有__pycache__目录
for root, dirs, files in os.walk("."):
for dir in dirs:
if dir == "__pycache__":
cache_dir = os.path.join(root, dir)
try:
shutil.rmtree(cache_dir)
print(f"已删除目录: {cache_dir}")
except Exception as e:
print(f"删除目录时出错 {cache_dir}: {e}")
def main():
"""主函数"""
print("开始清理无关文件...")
clean_logs()
clean_temp_files()
clean_pycache()
print("清理完成!")
if __name__ == "__main__":
main()

View File

@ -99,6 +99,61 @@
"map_to": "6935205372772", "map_to": "6935205372772",
"description": "条码映射6921734908735 -> 6935205372772" "description": "条码映射6921734908735 -> 6935205372772"
}, },
"6923644248222": {
"map_to": "6923644248208",
"description": "条码映射6923644248222 -> 6923644248208"
},
"6902083881122": {
"map_to": "6902083881085",
"description": "条码映射6902083881122 -> 6902083881085"
},
"6907992501857": {
"map_to": "6907992500010",
"description": "条码映射6907992501857 -> 6907992500010"
},
"6902083891015": {
"map_to": "6902083890636",
"description": "条码映射6902083891015 -> 6902083890636"
},
"6923450605240": {
"map_to": "6923450605226",
"description": "条码映射6923450605240 -> 6923450605226"
},
"6923450605196": {
"map_to": "6923450614624",
"description": "条码映射6923450605196 -> 6923450614624"
},
"6923450665213": {
"map_to": "6923450665206",
"description": "条码映射6923450665213 -> 6923450665206"
},
"6923450666821": {
"map_to": "6923450666838",
"description": "条码映射6923450666821 -> 6923450666838"
},
"6923450661505": {
"map_to": "6923450661499",
"description": "条码映射6923450661505 -> 6923450661499"
},
"6923450676103": {
"map_to": "6923450676097",
"description": "条码映射6923450676103 -> 6923450676097"
},
"6923450614631": {
"map_to": "6923450614624",
"description": "条码映射6923450614631 -> 6923450614624"
},
"6901424334174": {
"map_to": "6973730760015",
"description": "条码映射6901424334174 -> 6973730760015"
},
"6958620703716": {
"multiplier": 14,
"target_unit": "个",
"specification": "1*14",
"map_to": "6958620703907",
"description": "友臣肉松棒规格1*14映射到6958620703907"
},
"6925019900087": { "6925019900087": {
"multiplier": 10, "multiplier": 10,
"target_unit": "瓶", "target_unit": "瓶",
@ -115,5 +170,11 @@
"fixed_price": 3.7333333333333334, "fixed_price": 3.7333333333333334,
"specification": "1*30", "specification": "1*30",
"description": "特殊处理: 规格1*30数量*30单价=112/30" "description": "特殊处理: 规格1*30数量*30单价=112/30"
},
"6958620703907": {
"multiplier": 14,
"target_unit": "个",
"specification": "1*14",
"description": "友臣肉松1盒14个"
} }
} }

View File

@ -711,7 +711,7 @@ def main():
# 创建窗口 # 创建窗口
root = tk.Tk() root = tk.Tk()
root.title("益选-OCR订单处理系统 v1.0") root.title("益选-OCR订单处理系统 v1.1.0")
root.geometry("1200x650") # 增加窗口高度以容纳更多元素 root.geometry("1200x650") # 增加窗口高度以容纳更多元素
# 创建主区域分割 # 创建主区域分割
@ -930,7 +930,7 @@ def main():
).pack(side=tk.LEFT, padx=button_padx) ).pack(side=tk.LEFT, padx=button_padx)
# 底部说明 # 底部说明
tk.Label(left_frame, text="© 2025 益选-OCR订单处理系统 v1.0 by 欢欢欢", font=("Arial", 9)).pack(side=tk.BOTTOM, pady=10) tk.Label(left_frame, text="© 2025 益选-OCR订单处理系统 v1.1.0 by 欢欢欢", font=("Arial", 9)).pack(side=tk.BOTTOM, pady=10)
# 绑定键盘快捷键 # 绑定键盘快捷键
bind_keyboard_shortcuts(root, log_text, status_bar) bind_keyboard_shortcuts(root, log_text, status_bar)