v1.1.0: 版本更新 - 增强规格解析能力、修复条码映射功能、改进特殊条码处理
This commit is contained in:
parent
c0fceea9dc
commit
b3c175836a
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal 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
28
CHANGELOG.md
Normal 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处理功能
|
||||
- 采购单合并功能
|
||||
- 烟草订单处理功能
|
||||
- 图形用户界面
|
||||
95
README.md
95
README.md
@ -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
|
||||
- QQ:123456789
|
||||
- Issues反馈:请在项目仓库提交Issue
|
||||
## 贡献者
|
||||
|
||||
---
|
||||
- 欢欢欢
|
||||
|
||||
© 2025 益选-OCR订单处理系统 by 欢欢欢
|
||||
## 版权
|
||||
|
||||
© 2025 益选-OCR订单处理系统
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
# 调用保存回调
|
||||
|
||||
@ -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
88
clean.py
Normal 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()
|
||||
@ -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个"
|
||||
}
|
||||
}
|
||||
4
启动器.py
4
启动器.py
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user