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)。
- 新增即時品質監控儀表板。
- 實現多語言支援,符合全球合規性。