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

View File

@ -297,6 +297,17 @@ class UnitConverter:
except ValueError:
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等
ml_match = re.match(r'(\d+)(?:ml|毫升)[*](\d+)', spec, re.IGNORECASE)
if ml_match:
@ -341,6 +352,17 @@ class UnitConverter:
except ValueError:
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")
return 1, 1, None
@ -440,6 +462,12 @@ class UnitConverter:
'6923644268923': {
'map_to': '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]
# 处理条码映射
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:
multiplier = special_config.get('multiplier', 1)
@ -80,4 +74,10 @@ class BarcodeMapper:
result['price'] = new_price
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

View File

@ -9,7 +9,7 @@
import os
import tkinter as tk
from tkinter import messagebox, ttk
from tkinter import messagebox, ttk, simpledialog
from datetime import datetime
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.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.pack(fill=tk.X, padx=10, pady=10)
@ -380,6 +419,8 @@ def create_barcode_mapping_dialog(parent=None, on_save=None, current_mappings=No
# 添加特殊处理
for barcode, multiplier, unit, price, spec, desc in special_list:
# 检查该条码是否已存在
if barcode not in mappings:
mappings[barcode] = {}
if multiplier:
@ -411,7 +452,19 @@ def create_barcode_mapping_dialog(parent=None, on_save=None, current_mappings=No
if 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
# 调用保存回调

View File

@ -69,6 +69,29 @@ class OrderService:
"""
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]:
"""
合并采购单

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",
"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": {
"multiplier": 10,
"target_unit": "瓶",
@ -115,5 +170,11 @@
"fixed_price": 3.7333333333333334,
"specification": "1*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.title("益选-OCR订单处理系统 v1.0")
root.title("益选-OCR订单处理系统 v1.1.0")
root.geometry("1200x650") # 增加窗口高度以容纳更多元素
# 创建主区域分割
@ -930,7 +930,7 @@ def main():
).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)