QMS 專案架構與目錄結構
專案概述
本品質管理系統(QMS)基於 Pear Admin Flask 框架,採用 RBAC 架構與插件模式,實現進貨檢驗、製程巡檢、出貨檢驗、不良品管理、來料異常處理、供應商品質管理、量測儀器管理、品質異常與8D改善、統計報表與分析、製程站設定、ERP/MES 串接及 QR Code 掃碼功能。系統符合 ISO 9001 標準,支援資料追溯與審計。
功能模組說明:
- 進貨檢驗(IQC):檢驗來料,支援 QR Code 掃描與 ERP 串接。
- 製程巡檢(IPQC):工站巡檢,與 MES 工單串接,支援 QR Code 掃描。
- 出貨檢驗(OQC):檢查成品,支援 QR Code 掃描與 ERP 串接。
- 不良品管理(NG品處理):記錄不良品,與 ERP 成本系統連結。
- 來料異常處理(MRB):處理來料不合格,生成退貨單。
- 供應商品質管理(SPC + 稽核):管理供應商評比與 SPC 分析。
- 量測儀器管理(含校正):管理儀器校正與狀態。
- 品質異常與8D改善:執行8D流程,追蹤異常改善。
- 統計報表與分析:生成多維度報表,整合 ERP/MES 資料。
- 製程站設定:配置工站與檢查項目,與 MES 工單整合。
- ERP/MES 串接:交換工單、物料、檢驗結果。
- QR Code 掃碼進表單:掃描 QR Code 跳轉表單並自動填充資料。
🛠 功能細節說明
1. 進貨檢驗(IQC)
- 對供應商來料進行檢驗(含尺寸、外觀、功能等)
- 建立檢驗標準及抽樣方式(如 AQL)
- 檢驗記錄電子化、可上傳照片
- 可追蹤供應商的來料良率與不良原因
- 檢驗結果自動轉入供應商評比系統
2. 製程巡檢(IPQC)
- 生產過程中之品質點檢與巡檢作業
- 定時提醒檢查項目(與工單連動)
- 可設定不同工站的檢查項目與標準
- 巡檢紀錄可統一管理、生成報表
- 異常可即時通報,與8D系統串接
3. 出貨檢驗(OQC)
- 成品出貨前檢查(外觀、功能、包裝)
- 客戶驗收標準建檔與比對
- 可根據不同客戶設定不同的檢驗程序
- 不良品記錄與追蹤(客訴來源分析)
4. 不良品管理(NG品處理)
- 生產中或檢驗中不良品分類記錄
- 可標記不良原因、責任歸屬
- 提供報廢、返工、讓步接受等處置選項
- 與成本系統連結,計算品質損失成本
5. 來料異常處理(MRB)
- 來料檢驗不合格時,啟動 MRB 流程
- 判定責任歸屬:供應商、物流、內部
- 提出處理方式(退貨、重工、報廢)
- 可生成通知單、退貨單、臨時檢驗單等
6. 供應商品質管理(SPC + 稽核)
- 建立供應商資料庫與評分機制
- 每月/季/年供應商品質評比
- 稽核排程管理(稽核表、紀錄、缺失追蹤)
- SPC 分析供應商交貨品質穩定性
7. 量測儀器管理(含校正)
- 儀器建檔(型號、使用單位、精度等)
- 校正週期提醒與紀錄
- 可上傳校正報告或外部證書
- 儀器狀態管理(可用、維修中、報廢)
8. 品質異常與 8D 改善
- 建立異常單(來源:製程、客訴、內稽)
- 啟動8D問題解決流程(D1~D8)
- 責任人指派與改善進度追蹤
- 結案需上傳佐證文件與預防措施
9. 統計報表與分析
-
可根據不同維度(時間、產品、客戶)分析:
- 不良率趨勢
- 客訴數據
- 各站品質表現
- 供應商品質走勢
- 圖表自動生成(折線圖、長條圖、派圖)
✅ 各模組額外欄位與操作
📦 IQC 模組(進貨檢驗)
補充欄位:
- 檢驗依據標準:如 ASTM D4169、MIL-STD-105
- 驗收等級:如 N=125,AQL=0.65
- 檢驗人/複驗人:簽核軌跡
- 評等紀錄:供應商歷史績效
操作補充:
- 異常件自動進入 MRB 流程模組
- 可設定特定物料免檢或全檢規則
🛠 IPQC 模組(製程檢驗)
補充欄位:
- 檢驗頻率設定(如每1hr/每10pcs)
- 基準樣標記/圖片參照
- 巡檢人員班別、簽名欄
操作補充:
- 支援現場平板或工站終端快速錄入
- 巡檢異常自動觸發 CAPA 事件
🚚 OQC 模組(出貨檢驗)
補充欄位:
- 最終品項審核表(包含包裝、貼標)
- 出貨預警(如近90日出貨異常率超標提醒)
操作補充:
- 客戶規格比對提醒
- 自動生成帶簽章的電子驗收報告
🔍 CAPA 模組(8D+矯正預防)
補充欄位:
- 客戶/稽核單位名稱(來源明確化)
- CAPA 類型(立即處置/長期改善)
- 改善結果驗證欄(含文件上傳/簽核)
操作補充:
- 可設定多階簽核流程(如 品保→工程→品保主管→結案)
- 系統自動提醒改善進度,逾期紅字標示
📊 Analysis 模組(統計分析)
補充欄位/功能:
- 報表版本控制與簽核機制
- 報表出處標記(如 ISO/TS 規範出處)
- 支援 CPK、PPK 自動化計算與圖示
操作補充:
- 圖表導出可含公司 Logo 與稽核用標準語言
- 支援客製化 KPI 儀表板(如良率、異常比例)
📷 Scan_input 模組(掃碼錄入)
補充欄位:
- 設備序號綁定欄(追蹤量測設備用)
- 操作者 RFID/指紋登入紀錄欄(符合 21 CFR Part 11)
操作補充:
- 自動紀錄掃碼→輸入→提交的完整操作軌跡
- 與實驗室儀器串接(如量測工具的自動資料回填)
📌 系統整體 ISO 相容性建議
標準項目 | 建議對應做法 |
---|---|
ISO 9001:2015 7.5 文件控制 | 每張表單皆保留歷史版本與編修紀錄,自動打流水號 |
ISO 9001:2015 8.7 不合格處理 | IQC/OQC/IPQC 異常皆能觸發 CAPA 與 MRB 流程 |
ISO 17025 7.11 記錄保存 | 每筆檢驗或掃碼紀錄須含簽名、時間戳記與追蹤碼 |
21 CFR Part 11(如適用醫療) | 符合電子簽名、操作軌跡不可竄改、帳號分權控管 |
目錄結構
qms-project/ ├── application/ # 核心應用資料夾(不得直接修改) │ ├── __init__.py # 應用初始化 │ ├── utils/ # 共用函式庫 │ │ ├── __init__.py │ │ ├── rbac.py # RBAC 存取控制工具 │ │ ├── data_processor.py # 資料驗證與處理工具 │ │ ├── report_generator.py # 報表生成工具 │ │ ├── file_handler.py # 檔案上傳與管理工具 │ │ ├── qr_code.py # QR Code 掃描與解析工具 │ │ ├── erp_mes_api.py # ERP/MES API 串接工具 │ ├── config.py # 系統配置(資料庫、密鑰、API 端點等) ├── plugs/ # 插件資料夾 │ ├── qms_auth/ # 認證插件 │ │ ├── __init__.py │ │ ├── routes.py # 認證路由 │ │ ├── templates/ # 認證模板 │ │ │ ├── login.html │ │ │ ├── register.html │ │ │ ├── unauthorized.html │ │ ├── models/ # 認證模型 │ │ │ ├── user.py │ │ │ ├── role.py │ │ │ ├── permission.py │ ├── qms_iqc/ # 進貨檢驗插件 │ │ ├── __init__.py │ │ ├── routes.py # IQC 路由(含 QR Code 掃描) │ │ ├── templates/ # IQC 模板 │ │ │ ├── iqc_entry.html │ │ │ ├── iqc_view.html │ │ │ ├── qr_scan.html │ │ ├── models/ # IQC 模型 │ │ │ ├── iqc_record.py │ │ ├── static/ # IQC 專屬靜態檔案 │ │ │ ├── js/iqc_qr_scan.js │ │ │ ├── css/iqc.css │ ├── qms_ipqc/ # 製程巡檢插件 │ │ ├── __init__.py │ │ ├── routes.py # IPQC 路由(含 QR Code 掃描) │ │ ├── templates/ # IPQC 模板 │ │ │ ├── ipqc_entry.html │ │ │ ├── ipqc_view.html │ │ │ ├── qr_scan.html │ │ ├── models/ # IPQC 模型 │ │ │ ├── ipqc_record.py │ │ ├── static/ # IPQC 專屬靜態檔案 │ │ │ ├── js/ipqc_qr_scan.js │ │ │ ├── css/ipqc.css │ ├── qms_oqc/ # 出貨檢驗插件 │ │ ├── __init__.py │ │ ├── routes.py # OQC 路由(含 QR Code 掃描) │ │ ├── templates/ # OQC 模板 │ │ │ ├── oqc_entry.html │ │ │ ├── oqc_view.html │ │ │ ├── qr_scan.html │ │ ├── models/ # OQC 模型 │ │ │ ├── oqc_record.py │ │ ├── static/ # OQC 專屬靜態檔案 │ │ │ ├── js/oqc_qr_scan.js │ │ │ ├── css/oqc.css │ ├── qms_ng/ # 不良品管理插件 │ │ ├── __init__.py │ │ ├── routes.py # NG品路由 │ │ ├── templates/ # NG品模板 │ │ │ ├── ng_entry.html │ │ │ ├── ng_view.html │ │ ├── models/ # NG品模型 │ │ │ ├── ng_record.py │ │ ├── static/ # NG品專屬靜態檔案 │ │ │ ├── css/ng.css │ ├── qms_mrb/ # 來料異常處理插件 │ │ ├── __init__.py │ │ ├── routes.py # MRB 路由 │ │ ├── templates/ # MRB 模板 │ │ │ ├── mrb_entry.html │ │ │ ├── mrb_view.html │ │ ├── models/ # MRB 模型 │ │ │ ├── mrb_record.py │ │ ├── static/ # MRB 專屬靜態檔案 │ │ │ ├── css/mrb.css │ ├── qms_supplier/ # 供應商品質管理插件 │ │ ├── __init__.py │ │ ├── routes.py # 供應商路由 │ │ ├── templates/ # 供應商模板 │ │ │ ├── supplier_profile.html │ │ │ ├── audit_schedule.html │ │ │ ├── spc_analysis.html │ │ ├── models/ # 供應商模型 │ │ │ ├── supplier.py │ │ │ ├── audit_record.py │ │ ├── static/ # 供應商專屬靜態檔案 │ │ │ ├── css/supplier.css │ ├── qms_instrument/ # 量測儀器管理插件 │ │ ├── __init__.py │ │ ├── routes.py # 儀器管理路由 │ │ ├── templates/ # 儀器管理模板 │ │ │ ├── instrument_entry.html │ │ │ ├── instrument_view.html │ │ ├── models/ # 儀器管理模型 │ │ │ ├── instrument.py │ │ │ ├── calibration_record.py │ │ ├── static/ # 儀器管理專屬靜態檔案 │ │ │ ├── css/instrument.css │ ├── qms_8d/ # 品質異常與8D改善插件 │ │ ├── __init__.py │ │ ├── routes.py # 8D 路由 │ │ ├── templates/ # 8D 模板 │ │ │ ├── 8d_entry.html │ │ │ ├── 8d_view.html │ │ ├── models/ # 8D 模型 │ │ │ ├── 8d_record.py │ │ ├── static/ # 8D 專屬靜態檔案 │ │ │ ├── css/8d.css │ ├── qms_report/ # 統計報表與分析插件 │ │ ├── __init__.py │ │ ├── routes.py # 報表路由 │ │ ├── templates/ # 報表模板 │ │ │ ├── report_view.html │ │ ├── models/ # 報表模型 │ │ │ ├── report.py │ │ ├── static/ # 報表專屬靜態檔案 │ │ │ ├── css/report.css │ ├── qms_station/ # 製程站設定插件 │ │ ├── __init__.py │ │ ├── routes.py # 製程站路由 │ │ ├── templates/ # 製程站模板 │ │ │ ├── station_entry.html │ │ │ ├── station_view.html │ │ ├── models/ # 製程站模型 │ │ │ ├── station.py │ │ ├── static/ # 製程站專屬靜態檔案 │ │ │ ├── css/station.css │ ├── qms_admin/ # 管理員插件 │ │ ├── __init__.py │ │ ├── routes.py # 管理員路由 │ │ ├── templates/ # 管理員模板 │ │ │ ├── dashboard.html │ │ │ ├── user_management.html │ │ ├── static/ # 管理員專屬靜態檔案 │ │ │ ├── css/admin.css ├── migrations/ # 資料庫遷移腳本 ├── static/ # 共用靜態檔案 │ ├── css/ │ │ ├── tailwind.min.css # Tailwind CSS │ ├── js/ │ │ ├── jsQR.min.js # QR Code 掃描庫 ├── tests/ # 測試檔案 │ ├── test_auth.py │ ├── test_iqc.py │ ├── test_ipqc.py │ ├── test_oqc.py │ ├── test_ng.py │ ├── test_mrb.py │ ├── test_supplier.py │ ├── test_instrument.py │ ├── test_8d.py │ ├── test_report.py │ ├── test_station.py ├── requirements.txt # 專案依賴 ├── run.py # 應用啟動入口 ├── README.md # 專案說明文件
角色與權限
RBAC 模型定義以下角色及其權限:
- 管理員(Admin):
- 權限:all_*(完整存取)。
- 功能:管理使用者、角色、權限、製程站設定、ERP/MES API 配置,查看所有報表。
- 品質經理(Quality Manager):
- 權限:iqc_view、iqc_edit、ipqc_view、ipqc_edit、oqc_view、oqc_edit、ng_view、ng_edit、mrb_view、mrb_edit、8d_view、8d_edit、report_view。
- 功能:管理品質資料與報表,無製程站編輯或 API 配置權限。
- 操作員(Operator):
- 權限:iqc_edit、ipqc_edit、oqc_edit。
- 功能:輸入品質資料(支援 QR Code 掃描),查看分配的製程站資訊。
- 供應商管理員(Supplier Manager):
- 權限:supplier_view、supplier_edit、audit_view、audit_edit。
- 功能:管理供應商資料與稽核,查看 SPC 報表。
- 製程工程師(Process Engineer):
- 權限:station_view、station_edit。
- 功能:管理製程站設定,與 MES 工單同步。
ISO 合規性
- 客戶導向:ERP/MES 串接確保資料與客戶需求一致。
- 流程導向:模組化插件與 QR Code 掃描優化流程。
- 持續改進:8D流程與審計日誌支援改進。
- 證據決策:多維度報表與資料追溯。
🔸 IQC/IPQC/OQC 檢驗模組
資料表:inspection_form
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
form_type | ENUM | iqc/ipqc/oqc |
work_order_no | VARCHAR | 工單編號 |
product_code | VARCHAR | 產品型號 |
equipment_id | VARCHAR | 儀器來源 |
inspector_id | UUID | 使用者ID |
status | ENUM | draft/confirmed/verified |
created_at | DATETIME | 建立時間 |
updated_at | DATETIME | 最後更新時間 |
資料表:inspection_item
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
form_id | UUID | 外鍵(對應 inspection_form) |
item_code | VARCHAR | 測項代碼 |
measured_value | FLOAT | 實際值 |
usl/lsl/target | FLOAT | 規格上下限與中心值 |
result | ENUM | OK/NG |
source | ENUM | manual/machine |
操作流程:
- 使用者選擇工單與產品型號,系統載入預設檢驗項目。
- 儀器自動回填測值或由人工錄入。
- 每筆數據自動比較 LSL/USL 判定 OK/NG。
- 表單提交後,鎖定欄位,進入 SPC 統計流程。
🔸 CAPA 模組
資料表:capa_record
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
source_type | ENUM | complaint/audit/inspection |
issue_summary | TEXT | 缺失說明 |
root_cause | TEXT | 原因分析 |
action_plan | TEXT | 改善對策 |
verification_note | TEXT | 改善驗證說明 |
file_path | VARCHAR | 附加文件 |
status | ENUM | new/in_progress/completed/overdue |
created_by | UUID | 建立者 |
deadline | DATE | 回覆期限 |
操作流程:
- 系統或使用者建立 CAPA 表單。
- 負責人填寫原因與對策,主管驗證改善成效。
- 系統根據 deadline 計算是否逾期。
- 完成後列入改善紀錄供未來稽核追蹤。
🔸 SPC 模組
資料表:spc_entry
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
form_id | UUID | 對應檢驗表單 |
item_code | VARCHAR | 檢驗項目代碼 |
measured_value | FLOAT | 測值 |
batch_no | VARCHAR | 批次號 |
timestamp | DATETIME | 測量時間 |
資料表:spc_result
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
item_code | VARCHAR | 測項代碼 |
cp | FLOAT | 製程能力 |
cpk | FLOAT | 製程能力(考慮偏移) |
avg | FLOAT | 平均值 |
stddev | FLOAT | 標準差 |
lsl/usl | FLOAT | 規格上下限 |
updated_at | DATETIME | 更新時間 |
操作流程:
- 每筆檢驗數據(SPC Entry)即時寫入資料庫。
- 定期或依表單完成觸發計算 SPC 統計結果。
- 結果供圖表展示與異常報表匯出。
🔸 稽核模組(Audit)
資料表:audit_record
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
audit_type | ENUM | internal/client/third_party |
items | JSON | 檢查項目與對應結果 |
auditor_id | UUID | 稽核員 |
result | ENUM | pass/fail/partial |
linked_capa_id | UUID | 不符合時關聯 CAPA |
report_path | VARCHAR | PDF 匯出報告 |
🔸 掃碼模組
資料表:scan_log
欄位 | 類型 | 說明 |
---|---|---|
id | UUID | 主鍵 |
code | VARCHAR | 條碼或 QRCode 資料 |
form_id | UUID | 關聯表單 |
scanned_by | UUID | 操作者 |
scanned_at | DATETIME | 掃描時間 |
matched_fields | JSON | 自動填入之表單欄位與值 |
共用函式庫
RBAC 工具(application/utils/rbac.py)
from functools import wraps from flask import session, redirect, url_for, current_app from plugs.qms_auth.models.user import User from plugs.qms_auth.models.role import Role from typing import Callable def requires_permission(permission: str) -> Callable: """檢查使用者是否具有指定權限 Args: permission (str): 權限名稱,例如 'iqc_edit' Returns: Callable: 裝飾器函式 """ def decorator(f: Callable) -> Callable: @wraps(f) def decorated_function(*args, **kwargs): user_id = session.get('user_id') if not user_id: current_app.logger.warning("未登入使用者嘗試存取受限路由") return redirect(url_for('qms_auth.login')) user = User.query.get(user_id) role = Role.query.get(user.role_id) if permission not in role.permissions: current_app.logger.warning(f"使用者 {user_id} 無權限 {permission}") return redirect(url_for('qms_auth.unauthorized')) return f(*args, **kwargs) return decorated_function return decorator
資料處理工具(application/utils/data_processor.py)
from typing import Optional def validate_quality_data(value: str) -> Optional[float]: """驗證品質資料格式 Args: value (str): 輸入資料,例如 '1.5' 或 '3/4' Returns: Optional[float]: 驗證後的數值,或 None(無效) """ try: if isinstance(value, str) and '/' in value: num, denom = map(int, value.split('/')) return num / denom return float(value) except (ValueError, ZeroDivisionError): return None
QR Code 處理工具(application/utils/qr_code.py)
import json from typing import Optional, Dict from flask import current_app def parse_qr_code(qr_data: str) -> Optional[Dict[str, str]]: """解析 QR Code 資料 Args: qr_data (str): QR Code 內容 Returns: Optional[Dict[str, str]]: 解析結果,包含 'type' 和 'id',或 None(無效) """ try: data = json.loads(qr_data) return { 'type': data.get('type'), # 例如 'iqc', 'ipqc', 'oqc' 'id': data.get('id'), # 工單或物料 ID } except json.JSONDecodeError: current_app.logger.error(f"QR Code 解析失敗: {qr_data}") return None
ERP/MES API 串接工具(application/utils/erp_mes_api.py)
import requests from flask import current_app from typing import Optional, Dict, Any def fetch_work_order(work_order_id: str) -> Optional[Dict[str, Any]]: """從 MES 獲取工單資料 Args: work_order_id (str): 工單 ID Returns: Optional[Dict[str, Any]]: 工單資料,或 None(失敗) """ try: url = f"{current_app.config['MES_API_URL']}/work_orders/{work_order_id}" response = requests.get(url, headers={'Authorization': current_app.config['MES_API_KEY']}) response.raise_for_status() return response.json() except requests.RequestException as e: current_app.logger.error(f"MES API 錯誤: {e}") return None def send_inspection_result(data: Dict[str, Any]) -> bool: """將檢驗結果傳送至 ERP Args: data (Dict[str, Any]): 檢驗結果資料 Returns: bool: 傳送是否成功 """ try: url = f"{current_app.config['ERP_API_URL']}/inspection_results" response = requests.post(url, json=data, headers={'Authorization': current_app.config['ERP_API_KEY']}) response.raise_for_status() return True except requests.RequestException as e: current_app.logger.error(f"ERP API 錯誤: {e}") return False
報表生成工具(application/utils/report_generator.py)
import matplotlib.pyplot as plt import numpy as np from typing import List def generate_trend_chart(data: List[float], title: str, x_label: str, y_label: str, filename: str) -> None: """生成趨勢折線圖 Args: data (List[float]): 資料點 title (str): 圖表標題 x_label (str): X 軸標籤 y_label (str): Y 軸標籤 filename (str): 輸出檔案路徑 """ x = np.arange(len(data)) plt.figure(figsize=(10, 6)) plt.plot(x, data, 'b-', label=title) plt.title(title) plt.xlabel(x_label) plt.ylabel(y_label) plt.grid(True) plt.legend() plt.savefig(filename) plt.close()
檔案處理工具(application/utils/file_handler.py)
import os from flask import current_app from typing import Optional def save_file(file: Any, filename: str) -> Optional[str]: """儲存上傳檔案 Args: file (Any): 上傳的檔案物件 filename (str): 檔案名稱 Returns: Optional[str]: 檔案儲存路徑,或 None(失敗) """ try: upload_folder = current_app.config['UPLOAD_FOLDER'] os.makedirs(upload_folder, exist_ok=True) file_path = os.path.join(upload_folder, filename) file.save(file_path) return file_path except Exception as e: current_app.logger.error(f"檔案儲存失敗: {e}") return None
插件程式碼實現
以下為各插件的關鍵程式碼,包含後台路由、模型、前台模板與 JavaScript,僅展示代表性模組(IQC、IPQC、Station),其他模組遵循相同規範。
進貨檢驗(IQC)插件
模型(plugs/qms_iqc/models/iqc_record.py)
from application import db from datetime import datetime from typing import Optional class IQCRecord(db.Model): """進貨檢驗記錄模型""" __tablename__ = 'iqc_records' id = db.Column(db.Integer, primary_key=True) value = db.Column(db.Float, nullable=False) # 檢驗數值 material_id = db.Column(db.String(50), nullable=False) # 物料 ID photo_path = db.Column(db.String(255)) # 照片路徑 user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) user = db.relationship('User', backref='iqc_records')
路由(plugs/qms_iqc/routes.py)
from flask import Blueprint, render_template, request, redirect, url_for, session from plugs.qms_iqc.models.iqc_record import IQCRecord from application.utils.rbac import requires_permission from application.utils.data_processor import validate_quality_data from application.utils.qr_code import parse_qr_code from application.utils.file_handler import save_file from application.utils.erp_mes_api import fetch_work_order, send_inspection_result from application import db from typing import Optional iqc = Blueprint('qms_iqc', __name__, url_prefix='/iqc') @iqc.route('/qr_scan', methods=['GET', 'POST']) @requires_permission('iqc_edit') def qr_scan() -> str: """QR Code 掃描頁面""" if request.method == 'POST': qr_data = request.form.get('qr_data') parsed_data = parse_qr_code(qr_data) if parsed_data and parsed_data['type'] == 'iqc': session['material_id'] = parsed_data['id'] return redirect(url_for('qms_iqc.iqc_entry')) return render_template('qms_iqc/qr_scan.html', error='無效的 QR Code') return render_template('qms_iqc/qr_scan.html') @iqc.route('/entry', methods=['GET', 'POST']) @requires_permission('iqc_edit') def iqc_entry() -> str: """進貨檢驗資料輸入""" material_id = session.get('material_id') if not material_id: return redirect(url_for('qms_iqc.qr_scan')) if request.method == 'POST': value = request.form.get('value') validated_value = validate_quality_data(value) if validated_value is None: return render_template('qms_iqc/iqc_entry.html', error='無效的資料格式', material_id=material_id) photo = request.files.get('photo') photo_path = None if photo: photo_path = save_file(photo, photo.filename) record = IQCRecord( value=validated_value, material_id=material_id, photo_path=photo_path, user_id=session['user_id'] ) db.session.add(record) db.session.commit() # 回傳檢驗結果至 ERP send_inspection_result({ 'material_id': material_id, 'value': validated_value, 'user_id': session['user_id'] }) return redirect(url_for('qms_iqc.iqc_view')) return render_template('qms_iqc/iqc_entry.html', material_id=material_id) @iqc.route('/view') @requires_permission('iqc_view') def iqc_view() -> str: """進貨檢驗資料查看""" records = IQCRecord.query.all() return render_template('qms_iqc/iqc_view.html', records=records)
模板(plugs/qms_iqc/templates/iqc_entry.html)
{% extends "base.html" %} {% block content %} <h1>進貨檢驗資料輸入</h1> {% if error %} <p style="color: red;">{{ error }}</p> {% endif %} <form method="POST" enctype="multipart/form-data"> <label for="material_id">物料 ID</label> <input type="text" id="material_id" name="material_id" value="{{ material_id }}" readonly aria-label="物料 ID"> <label for="value">檢驗數值</label> <input type="text" id="value" name="value" required aria-label="檢驗數值"> <label for="photo">上傳照片</label> <input type="file" id="photo" name="photo" accept="image/*" aria-label="上傳照片"> <button type="submit">提交</button> </form> {% endblock %}
QR 掃描模板(plugs/qms_iqc/templates/qr_scan.html)
{% extends "base.html" %} {% block content %} <h1>掃描 QR Code</h1> {% if error %} <p style="color: red;">{{ error }}</p> {% endif %} <video id="video" width="300" height="200"></video> <canvas id="canvas" style="display: none;"></canvas> <form id="qr_form" method="POST" action="{{ url_for('qms_iqc.qr_scan') }}"> <input type="hidden" name="qr_data" id="qr_data"> </form> <script src="{{ url_for('static', filename='js/jsQR.min.js') }}"></script> <script src="{{ url_for('qms_iqc.static', filename='js/iqc_qr_scan.js') }}"></script> {% endblock %}
JavaScript(plugs/qms_iqc/static/js/iqc_qr_scan.js)
document.addEventListener('DOMContentLoaded', () => { const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const context = canvas.getContext('2d'); navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) .then(stream => { video.srcObject = stream; video.play(); scanQRCode(); }) .catch(err => { console.error('相機存取失敗:', err); alert('無法存取相機,請檢查權限'); }); function scanQRCode() { canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { document.getElementById('qr_data').value = code.data; document.getElementById('qr_form').submit(); } else { requestAnimationFrame(scanQRCode); } } });
CSS(plugs/qms_iqc/static/css/iqc.css)
form { display: flex; flex-direction: column; gap: 1rem; max-width: 400px; margin: 0 auto; } input, button { padding: 0.5rem; font-size: 1rem; } button { background-color: #2563eb; color: white; border: none; cursor: pointer; } button:hover { background-color: #1e40af; }
製程巡檢(IPQC)插件
模型(plugs/qms_ipqc/models/ipqc_record.py)
from application import db from datetime import datetime from typing import Optional class IPQCRecord(db.Model): """製程巡檢記錄模型""" __tablename__ = 'ipqc_records' id = db.Column(db.Integer, primary_key=True) value = db.Column(db.Float, nullable=False) # 檢驗數值 station_id = db.Column(db.Integer, db.ForeignKey('stations.id'), nullable=False) work_order_id = db.Column(db.String(50), nullable=False) # 工單 ID user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) station = db.relationship('Station', backref='ipqc_records') user = db.relationship('User', backref='ipqc_records')
路由(plugs/qms_ipqc/routes.py)
from flask import Blueprint, render_template, request, redirect, url_for, session from plugs.qms_ipqc.models.ipqc_record import IPQCRecord from plugs.qms_station.models.station import Station from application.utils.rbac import requires_permission from application.utils.data_processor import validate_quality_data from application.utils.qr_code import parse_qr_code from application.utils.erp_mes_api import fetch_work_order, send_inspection_result from application import db from typing import Optional ipqc = Blueprint('qms_ipqc', __name__, url_prefix='/ipqc') @ipqc.route('/qr_scan', methods=['GET', 'POST']) @requires_permission('ipqc_edit') def qr_scan() -> str: """QR Code 掃描頁面""" if request.method == 'POST': qr_data = request.form.get('qr_data') parsed_data = parse_qr_code(qr_data) if parsed_data and parsed_data['type'] == 'ipqc': session['work_order_id'] = parsed_data['id'] return redirect(url_for('qms_ipqc.ipqc_entry')) return render_template('qms_ipqc/qr_scan.html', error='無效的 QR Code') return render_template('qms_ipqc/qr_scan.html') @ipqc.route('/entry', methods=['GET', 'POST']) @requires_permission('ipqc_edit') tandard: str) -> str: """製程巡檢資料輸入""" work_order_id = session.get('work_order_id') if not work_order_id: return redirect(url_for('qms_ipqc.qr_scan')) work_order = fetch_work_order(work_order_id) if not work_order: return render_template('qms_ipqc/ipqc_entry.html', error='無效的工單 ID') stations = Station.query.filter_by(work_order_id=work_order_id).all() if request.method == 'POST': value = request.form.get('value') station_id = request.form.get('station_id') validated_value = validate_quality_data(value) if validated_value is None: return render_template('qms_ipqc/ipqc_entry.html', error='無效的資料格式', stations=stations) record = IPQCRecord( value=validated_value, station_id=station_id, work_order_id=work_order_id, user_id=session['user_id'] ) db.session.add(record) db.session.commit() # 回傳檢驗結果至 MES send_inspection_result({ 'work_order_id': work_order_id, 'station_id': station_id, 'value': validated_value, 'user_id': session['user_id'] }) return redirect(url_for('qms_ipqc.ipqc_view')) return render_template('qms_ipqc/ipqc_entry.html', stations=stations) @ipqc.route('/view') @requires_permission('ipqc_view') def ipqc_view() -> str: """製程巡檢資料查看""" records = IPQCRecord.query.all() return render_template('qms_ipqc/ipqc_view.html', records=records)
模板(plugs/qms_ipqc/templates/ipqc_entry.html)
{% extends "base.html" %} {% block content %} <h1>製程巡檢資料輸入</h1> {% if error %} <p style="color: red;">{{ error }}</p> {% endif %} <form method="POST"> <label for="work_order_id">工單 ID</label> <input type="text" id="work_order_id" name="work_order_id" value="{{ session.work_order_id }}" readonly aria-label="工單 ID"> <label for="station_id">工站</label> <select id="station_id" name="station_id" required aria-label="選擇工站"> {% for station in stations %} <option value="{{ station.id }}">{{ station.name }}</option> {% endfor %} </select> <label for="value">檢驗數值</label> <input type="text" id="value" name="value" required aria-label="檢驗數值"> <button type="submit">提交</button> </form> {% endblock %}
JavaScript(plugs/qms_ipqc/static/js/ipqc_qr_scan.js)
document.addEventListener('DOMContentLoaded', () => { const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const context = canvas.getContext('2d'); navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) .then(stream => { video.srcObject = stream; video.play(); scanQRCode(); }) .catch(err => { console.error('相機存取失敗:', err); alert('無法存取相機,請檢查權限'); }); function scanQRCode() { canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { document.getElementById('qr_data').value = code.data; document.getElementById('qr_form').submit(); } else { requestAnimationFrame(scanQRCode); } } });
CSS(plugs/qms_ipqc/static/css/ipqc.css)
form { display: flex; flex-direction: column; gap: 1rem; max-width: 400px; margin: 0 auto; } select, input, button { padding: 0.5rem; font-size: 1rem; } button { background-color: #2563eb; color: white; border: none; cursor: pointer; } button:hover { background-color: #1e40af; }
製程站設定插件
模型(plugs/qms_station/models/station.py)
from application import db from datetime import datetime from typing import Optional class Station(db.Model): """製程站模型""" __tablename__ = 'stations' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) # 工站名稱 sequence = db.Column(db.Integer, nullable=False) # 工站序號 work_order_id = db.Column(db.String(50), nullable=False) # 工單 ID created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
路由(plugs/qms_station/routes.py)
from flask import Blueprint, render_template, request, redirect, url_for from plugs.qms_station.models.station import Station from application.utils.rbac import requires_permission from application.utils.erp_mes_api import fetch_work_order from application import db from typing import Optional station = Blueprint('qms_station', __name__, url_prefix='/station') @station.route('/entry', methods=['GET', 'POST']) @requires_permission('station_edit') def station_entry() -> str: """製程站設定輸入""" if request.method == 'POST': name = request.form.get('name') sequence = request.form.get('sequence') work_order_id = request.form.get('work_order_id') # 從 MES 驗證工單 work_order = fetch_work_order(work_order_id) if not work_order: return render_template('qms_station/station_entry.html', error='無效的工單 ID') station = Station(name=name, sequence=sequence, work_order_id=work_order_id) db.session.add(station) db.session.commit() return redirect(url_for('qms_station.station_view')) return render_template('qms_station/station_entry.html') @station.route('/view') @requires_permission('station_view') def station_view() -> str: """製程站查看""" stations = Station.query.all() return render_template('qms_station/station_view.html', stations=stations)
模板(plugs/qms_station/templates/station_entry.html)
{% extends "base.html" %} {% block content %} <h1>製程站設定</h1> {% if error %} <p style="color: red;">{{ error }}</p> {% endif %} <form method="POST"> <label for="name">工站名稱</label> <input type="text" id="name" name="name" required aria-label="工站名稱"> <label for="sequence">工站序號</label> <input type="number" id="sequence" name="sequence" required aria-label="工站序號"> <label for="work_order_id">工單 ID</label> <input type="text" id="work_order_id" name="work_order_id" required aria-label="工單 ID"> <button type="submit">提交</button> </form> {% endblock %}
CSS(plugs/qms_station/static/css/station.css)
form { display: flex; flex-direction: column; gap: 1rem; max-width: 400px; margin: 0 auto; } input, button { padding: 0.5rem; font-size: 1rem; } button { background-color: #2563eb; color: white; border: none; cursor: pointer; } button:hover { background-color: #1e40af; }
其他模組實現
以下模組遵循相同規範,僅概述關鍵結構:
- 出貨檢驗(OQC):
- 模型:OQCRecord(產品 ID、檢驗數值、客戶標準)。
- 路由:/oqc/qr_scan、/oqc/entry、/oqc/view。
- 模板:oqc_entry.html、qr_scan.html。
- 功能:QR Code 掃描產品 ID,與 ERP 串接客戶標準。
- 不良品管理(NG品處理):
- 模型:NGRecord(不良原因、處置方式)。
- 路由:/ng/entry、/ng/view。
- 模板:ng_entry.html。
- 功能:記錄不良品,與 ERP 成本系統連結。
- 來料異常處理(MRB):
- 模型:MRBRecord(物料 ID、責任歸屬)。
- 路由:/mrb/entry、/mrb/view。
- 模板:mrb_entry.html。
- 功能:生成退貨單,與 ERP 同步。
- 供應商品質管理(SPC + 稽核):
- 模型:Supplier、AuditRecord。
- 路由:/supplier/profile、/supplier/audit、/supplier/spc。
- 模板:supplier_profile.html、audit_schedule.html。
- 功能:SPC 分析,與 ERP 同步供應商資料。
- 量測儀器管理(含校正):
- 模型:Instrument、CalibrationRecord。
- 路由:/instrument/entry、/instrument/view。
- 模板:instrument_entry.html。
- 功能:管理校正週期,支援報告上傳。
- 品質異常與8D改善:
- 模型:8DRecord(異常來源、改善進度)。
- 路由:/8d/entry、/8d/view。
- 模板:8d_entry.html。
- 功能:執行8D流程,追蹤改善。
- 統計報表與分析:
- 模型:Report(報表資料)。
- 路由:/report/view。
- 模板:report_view.html。
- 功能:生成圖表,整合 ERP/MES 資料。
- 管理員插件:
- 路由:/admin/dashboard、/admin/user_management。
- 模板:dashboard.html、user_management.html。
- 功能:管理使用者、角色、API 配置。
依賴(requirements.txt)
Flask==2.0.1 Flask-SQLAlchemy==2.5.1 Flask-Login==0.5.0 Flask-Migrate==3.1.0 python-dotenv==0.19.0 gunicorn==20.1.0 matplotlib==3.5.1 numpy==1.21.5 requests==2.26.0
安裝與執行
- 複製 Pear Admin Flask 儲存庫:git clone https://gitee.com/pear-admin/pear-admin-flask.git。
- 進入專案目錄:cd qms-project。
- 安裝依賴:pip install -r requirements.txt。
- 下載 QR Code 掃描庫(jsQR):將 jsQR.min.js 放置於 static/js/(從 https://github.com/cozmo/jsQR 下載)。
- 初始化資料庫:flask db init && flask db migrate && flask db upgrade。
- 配置環境變數(在 application/config.py 中設定 MES_API_URL、ERP_API_URL、MES_API_KEY、ERP_API_KEY、UPLOAD_FOLDER)。
- 啟動應用:python run.py。
未來改進
- 支援多 ERP/MES 系統(如 Oracle NetSuite)。
- 新增即時品質監控儀表板。
- 實現多語言支援,符合全球合規性。