抓取生产工单,抓取发料异常
This commit is contained in:
249
browser_login/analysis_service.py
Normal file
249
browser_login/analysis_service.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import sqlite3
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# 把当前目录加入 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from config import OUTPUT_DIR
|
||||
|
||||
DB_PATH = OUTPUT_DIR / 'erp_data.db'
|
||||
|
||||
class MaterialReconciliationService:
|
||||
def __init__(self):
|
||||
self.db_path = DB_PATH
|
||||
|
||||
def get_conn(self):
|
||||
return sqlite3.connect(self.db_path)
|
||||
|
||||
def get_data_period(self):
|
||||
"""获取当前数据库中发料记录的时间周期,用于前端展示对账月份"""
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
now = datetime.datetime.now()
|
||||
first_day = datetime.date(now.year, now.month, 1).strftime('%Y-%m-%d')
|
||||
last_day = datetime.date(now.year, now.month, calendar.monthrange(now.year, now.month)[1]).strftime('%Y-%m-%d')
|
||||
|
||||
return {"start": first_day, "end": last_day}
|
||||
|
||||
def step1_extract_and_match_work_orders(self):
|
||||
"""
|
||||
第一步:匹配生产工单。
|
||||
对于有生产工单的,直接赋值。
|
||||
对于没有生产工单的,从备注中通过正则提取潜在的工单号(如 SFC... 或 连续数字)。
|
||||
并将其更新到 inferred_production_order_no 字段中。
|
||||
"""
|
||||
print("[开始] 正在连接数据库并准备清洗工单数据...")
|
||||
conn = self.get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 确保表有 inferred_production_order_no 字段
|
||||
try:
|
||||
cursor.execute("ALTER TABLE issue_receipt_details ADD COLUMN inferred_production_order_no TEXT;")
|
||||
print("[信息] 已在数据库中新建 inferred_production_order_no 字段。")
|
||||
except sqlite3.OperationalError:
|
||||
pass # 已经存在
|
||||
|
||||
print("[执行] 正在读取所有发料单明细...")
|
||||
# 获取所有发料单
|
||||
cursor.execute("SELECT id, production_order_no, work_orders_remark, detailed_remark, production_order_remark FROM issue_receipt_details")
|
||||
rows = cursor.fetchall()
|
||||
print(f"[执行] 共读取到 {len(rows)} 条发料记录,开始进行正则比对...")
|
||||
|
||||
# 预先加载 abnormal_report 中的有效工单号作为验证库
|
||||
cursor.execute("SELECT DISTINCT work_orders_number FROM abnormal_report")
|
||||
valid_sfcs = {r[0].upper() for r in cursor.fetchall() if r[0]}
|
||||
|
||||
updates = []
|
||||
matched_count = 0
|
||||
|
||||
for row in rows:
|
||||
row_id = row[0]
|
||||
prod_no = row[1]
|
||||
work_remark = row[2] or ''
|
||||
detail_remark = row[3] or ''
|
||||
prod_remark = row[4] or ''
|
||||
|
||||
inferred_sfc = ''
|
||||
|
||||
# 如果系统本身已经有工单号
|
||||
if prod_no and prod_no.strip():
|
||||
inferred_sfc = prod_no.strip()
|
||||
else:
|
||||
# 合并所有备注信息
|
||||
combined_remarks = f"{work_remark} {detail_remark} {prod_remark}"
|
||||
if combined_remarks.strip():
|
||||
# 优先匹配完整的 SFC 格式 (例如 SFC018845)
|
||||
sfc_match = re.search(r'(SFC\d+)', combined_remarks, re.IGNORECASE)
|
||||
if sfc_match:
|
||||
inferred_sfc = sfc_match.group(1).upper()
|
||||
else:
|
||||
# 尝试匹配纯数字格式 (长度大于等于4位的连续数字)
|
||||
num_match = re.search(r'(\d{4,})', combined_remarks)
|
||||
if num_match:
|
||||
candidate_num = num_match.group(1)
|
||||
# 尝试拼凑 SFC 前缀并校验是否存在于有效库中
|
||||
candidate_sfc = f"SFC0{candidate_num}"
|
||||
if candidate_sfc in valid_sfcs:
|
||||
inferred_sfc = candidate_sfc
|
||||
else:
|
||||
candidate_sfc2 = f"SFC{candidate_num}"
|
||||
if candidate_sfc2 in valid_sfcs:
|
||||
inferred_sfc = candidate_sfc2
|
||||
else:
|
||||
inferred_sfc = candidate_sfc # 哪怕没在库里也提取出来,作为参考
|
||||
|
||||
if inferred_sfc:
|
||||
matched_count += 1
|
||||
updates.append((inferred_sfc, row_id))
|
||||
|
||||
print(f"[信息] 匹配完成。有 {matched_count} 条数据成功匹配到工单号。正在将结果写回数据库...")
|
||||
# 批量更新数据库
|
||||
cursor.executemany(
|
||||
"UPDATE issue_receipt_details SET inferred_production_order_no = ? WHERE id = ?",
|
||||
updates
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"[完成] 数据库更新完毕!本次清洗共处理了 {len(updates)} 条数据。")
|
||||
return len(updates)
|
||||
|
||||
def step2_get_unmatched_materials(self, start_date=None, end_date=None):
|
||||
"""
|
||||
第二步:整理出没有生产工单的物料明细表
|
||||
"""
|
||||
conn = self.get_conn()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
where_clause = "WHERE (inferred_production_order_no = '' OR inferred_production_order_no IS NULL)"
|
||||
params = []
|
||||
|
||||
if start_date and end_date:
|
||||
where_clause += " AND execution_time >= ? AND execution_time <= ?"
|
||||
params.extend([f"{start_date} 00:00:00", f"{end_date} 23:59:59"])
|
||||
|
||||
sql = f"""
|
||||
SELECT
|
||||
work_orders_number as issue_receipt_no,
|
||||
material_code,
|
||||
material_name,
|
||||
material_specification,
|
||||
issue_number,
|
||||
work_orders_remark,
|
||||
detailed_remark,
|
||||
production_order_remark,
|
||||
execution_time,
|
||||
executor_user_name,
|
||||
warehouse_name
|
||||
FROM issue_receipt_details
|
||||
{where_clause}
|
||||
ORDER BY execution_time DESC
|
||||
"""
|
||||
cursor.execute(sql, params)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def step3_bom_reconciliation(self, start_date=None, end_date=None):
|
||||
"""
|
||||
第三步:BOM 校准物料,比对发料与BOM差异
|
||||
返回按工单分组的差异列表。
|
||||
"""
|
||||
conn = self.get_conn()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
where_actual = "WHERE inferred_production_order_no != '' AND inferred_production_order_no IS NOT NULL"
|
||||
params_actual = []
|
||||
|
||||
if start_date and end_date:
|
||||
where_actual += " AND execution_time >= ? AND execution_time <= ?"
|
||||
params_actual.extend([f"{start_date} 00:00:00", f"{end_date} 23:59:59"])
|
||||
|
||||
sql_actual = f"""
|
||||
SELECT
|
||||
inferred_production_order_no as sfc,
|
||||
material_code,
|
||||
material_name,
|
||||
SUM(issue_number) as total_actual_issue
|
||||
FROM issue_receipt_details
|
||||
{where_actual}
|
||||
GROUP BY inferred_production_order_no, material_code
|
||||
"""
|
||||
|
||||
where_bom = ""
|
||||
params_bom = []
|
||||
if start_date and end_date:
|
||||
where_bom = "WHERE order_date >= ? AND order_date <= ?"
|
||||
params_bom.extend([f"{start_date} 00:00:00", f"{end_date} 23:59:59"])
|
||||
|
||||
sql_bom = f"""
|
||||
SELECT
|
||||
work_orders_number as sfc,
|
||||
material_code,
|
||||
material_name,
|
||||
theoretical_issue_qty as bom_qty
|
||||
FROM abnormal_report
|
||||
{where_bom}
|
||||
"""
|
||||
|
||||
cursor.execute(sql_actual, params_actual)
|
||||
actuals = cursor.fetchall()
|
||||
|
||||
cursor.execute(sql_bom, params_bom)
|
||||
boms = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
reconciliation = {}
|
||||
|
||||
for r in boms:
|
||||
key = (r['sfc'], r['material_code'])
|
||||
reconciliation[key] = {
|
||||
'sfc': r['sfc'],
|
||||
'material_code': r['material_code'],
|
||||
'material_name': r['material_name'],
|
||||
'bom_qty': r['bom_qty'] or 0,
|
||||
'actual_qty': 0,
|
||||
'diff_qty': 0 - (r['bom_qty'] or 0),
|
||||
'status': '未发料'
|
||||
}
|
||||
|
||||
for r in actuals:
|
||||
key = (r['sfc'], r['material_code'])
|
||||
if key not in reconciliation:
|
||||
reconciliation[key] = {
|
||||
'sfc': r['sfc'],
|
||||
'material_code': r['material_code'],
|
||||
'material_name': r['material_name'],
|
||||
'bom_qty': 0,
|
||||
'actual_qty': 0,
|
||||
'diff_qty': 0,
|
||||
'status': 'BOM外发料'
|
||||
}
|
||||
reconciliation[key]['actual_qty'] += r['total_actual_issue']
|
||||
reconciliation[key]['diff_qty'] = round(reconciliation[key]['actual_qty'] - reconciliation[key]['bom_qty'], 4)
|
||||
|
||||
if reconciliation[key]['diff_qty'] > 0 and reconciliation[key]['bom_qty'] > 0:
|
||||
reconciliation[key]['status'] = '超领发料'
|
||||
elif reconciliation[key]['diff_qty'] < 0:
|
||||
reconciliation[key]['status'] = '少领发料'
|
||||
elif reconciliation[key]['diff_qty'] == 0 and reconciliation[key]['bom_qty'] > 0:
|
||||
reconciliation[key]['status'] = '发料正常'
|
||||
|
||||
return list(reconciliation.values())
|
||||
|
||||
if __name__ == '__main__':
|
||||
service = MaterialReconciliationService()
|
||||
print("Executing Step 1: Matching work orders...")
|
||||
updated = service.step1_extract_and_match_work_orders()
|
||||
print(f"Updated {updated} issue receipts with inferred production orders.")
|
||||
|
||||
unmatched = service.step2_get_unmatched_materials()
|
||||
print(f"Found {len(unmatched)} unmatched materials.")
|
||||
|
||||
recon = service.step3_bom_reconciliation()
|
||||
abnormal = [r for r in recon if r['status'] in ['超领发料', 'BOM外发料']]
|
||||
print(f"Found {len(abnormal)} abnormal BOM issues.")
|
||||
Reference in New Issue
Block a user