diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83c4521 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5bed6d3 --- /dev/null +++ b/CHANGELOG.md @@ -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处理功能 +- 采购单合并功能 +- 烟草订单处理功能 +- 图形用户界面 \ No newline at end of file diff --git a/README.md b/README.md index 7725977..4774349 100644 --- a/README.md +++ b/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 欢欢欢 \ No newline at end of file +## 版权 + +© 2025 益选-OCR订单处理系统 \ No newline at end of file diff --git a/app/core/excel/converter.py b/app/core/excel/converter.py index 5cba87f..b2682ee 100644 --- a/app/core/excel/converter.py +++ b/app/core/excel/converter.py @@ -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: @@ -340,6 +351,17 @@ class UnitConverter: return 1, quantity, None 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") @@ -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' } } diff --git a/app/core/excel/handlers/barcode_mapper.py b/app/core/excel/handlers/barcode_mapper.py index b51c761..979cded 100644 --- a/app/core/excel/handlers/barcode_mapper.py +++ b/app/core/excel/handlers/barcode_mapper.py @@ -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) @@ -79,5 +73,11 @@ class BarcodeMapper: result['quantity'] = new_quantity 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 \ No newline at end of file diff --git a/app/core/utils/dialog_utils.py b/app/core/utils/dialog_utils.py index e7e2141..3831a81 100644 --- a/app/core/utils/dialog_utils.py +++ b/app/core/utils/dialog_utils.py @@ -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,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: - mappings[barcode] = {} + # 检查该条码是否已存在 + if barcode not in mappings: + mappings[barcode] = {} if multiplier: try: @@ -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 # 调用保存回调 diff --git a/app/services/order_service.py b/app/services/order_service.py index 6d66481..bd6b934 100644 --- a/app/services/order_service.py +++ b/app/services/order_service.py @@ -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]: """ 合并采购单 diff --git a/clean.py b/clean.py new file mode 100644 index 0000000..e84db86 --- /dev/null +++ b/clean.py @@ -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() \ No newline at end of file diff --git a/config/barcode_mappings.json b/config/barcode_mappings.json index 5eadd51..732ef81 100644 --- a/config/barcode_mappings.json +++ b/config/barcode_mappings.json @@ -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个" } } \ No newline at end of file diff --git a/启动器.py b/启动器.py index 455c5fc..40296dc 100644 --- a/启动器.py +++ b/启动器.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)