From e751d53362f94252606accdbb52e6b63d26dba4e Mon Sep 17 00:00:00 2001 From: hjq <770690987@qq.com> Date: Mon, 22 Jun 2026 14:34:23 +0800 Subject: [PATCH] =?UTF-8?q?Dockerfile=20=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser_login/analysis_service.py | 226 ++++++++++++++++++++++----- update_html.py | 57 +++++++ web_ui/app.py | 8 +- web_ui/templates/reconciliation.html | 81 ++++++---- 4 files changed, 299 insertions(+), 73 deletions(-) create mode 100644 update_html.py diff --git a/browser_login/analysis_service.py b/browser_login/analysis_service.py index 29238a6..aaaa455 100644 --- a/browser_login/analysis_service.py +++ b/browser_login/analysis_service.py @@ -146,31 +146,36 @@ class MaterialReconciliationService: conn.close() return [dict(r) for r in rows] - def step3_bom_reconciliation(self, start_date=None, end_date=None): + def step3_bom_reconciliation(self, start_date=None, end_date=None, match_type='official'): """ 第三步: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" + if match_type == 'official': + where_actual = "WHERE production_order_no != '' AND production_order_no IS NOT NULL" + sfc_field = "production_order_no" + else: + where_actual = "WHERE (production_order_no = '' OR production_order_no IS NULL) AND inferred_production_order_no != '' AND inferred_production_order_no IS NOT NULL" + sfc_field = "inferred_production_order_no" + 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, + {sfc_field} 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 + GROUP BY {sfc_field}, material_code """ where_bom = "" @@ -182,6 +187,8 @@ class MaterialReconciliationService: sql_bom = f""" SELECT work_orders_number as sfc, + product_code, + product_name, material_code, material_name, theoretical_issue_qty as bom_qty @@ -195,45 +202,186 @@ class MaterialReconciliationService: cursor.execute(sql_bom, params_bom) boms = cursor.fetchall() - conn.close() - - reconciliation = {} - + sfc_to_product = {} + sfc_bom_dict = {} # {sfc: {material_code: {qty, name}}} 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'], + sfc = r['sfc'] + mat_code = r['material_code'] + if sfc not in sfc_bom_dict: + sfc_bom_dict[sfc] = {} + sfc_bom_dict[sfc][mat_code] = { 'bom_qty': r['bom_qty'] or 0, - 'actual_qty': 0, - 'diff_qty': 0 - (r['bom_qty'] or 0), - 'status': '未发料' + 'material_name': r['material_name'] + } + if r['product_code']: + sfc_to_product[sfc] = { + 'product_code': r['product_code'], + 'product_name': r['product_name'] + } + + sfc_actual_dict = {} # {sfc: {material_code: {qty, name}}} + for r in actuals: + sfc = r['sfc'] + mat_code = r['material_code'] + if sfc not in sfc_actual_dict: + sfc_actual_dict[sfc] = {} + sfc_actual_dict[sfc][mat_code] = { + 'actual_qty': r['total_actual_issue'] or 0, + 'material_name': r['material_name'] } - 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 match_type == 'official': + all_sfcs = set(sfc_bom_dict.keys()).union(set(sfc_actual_dict.keys())) + else: + all_sfcs = set(sfc_actual_dict.keys()) + + # 预先获取所有的 product_code 的 bom_child 树结构 + product_codes = [p['product_code'] for p in sfc_to_product.values() if p['product_code']] + bom_trees = {} # {product_code: list of root children} + if product_codes: + unique_p_codes = list(set(product_codes)) + placeholders = ','.join(['?'] * len(unique_p_codes)) + cursor.execute(f""" + SELECT id, parent_material_code, node_material_code, node_material_name, bom_level, parent_node_id, usage_qty + FROM bom_child + WHERE parent_material_code IN ({placeholders}) + """, unique_p_codes) + all_bom_children = cursor.fetchall() - 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'] = '发料正常' + from collections import defaultdict + child_by_product = defaultdict(list) + for row in all_bom_children: + child_by_product[row['parent_material_code']].append(dict(row)) - return list(reconciliation.values()) + for p_code, children in child_by_product.items(): + nodes_map = {} + for row in children: + nodes_map[row['id']] = { + 'id': f"node_{row['id']}", + 'material_code': row['node_material_code'], + 'material_name': row['node_material_name'], + 'bom_level': row['bom_level'], + 'parent_node_id': row['parent_node_id'], + 'bom_qty': 0, + 'actual_qty': 0, + 'diff_qty': 0, + 'status': '', + 'children': [] + } + root_children = [] + for row in children: + node_id = row['id'] + parent_id = row['parent_node_id'] + if parent_id is None: + root_children.append(nodes_map[node_id]) + else: + if parent_id in nodes_map: + nodes_map[parent_id]['children'].append(nodes_map[node_id]) + bom_trees[p_code] = root_children + + conn.close() + + result = [] + + for sfc in all_sfcs: + prod_info = sfc_to_product.get(sfc, {}) + p_code = prod_info.get('product_code', '') + p_name = prod_info.get('product_name', '无工单关联产品') + + sfc_node = { + 'id': f"sfc_{sfc}", + 'sfc': sfc, + 'material_code': p_code, + 'material_name': f"【成品】{p_name}", + 'bom_qty': '', + 'actual_qty': '', + 'diff_qty': '', + 'status': '', + 'children': [] + } + + boms_for_sfc = sfc_bom_dict.get(sfc, {}) + actuals_for_sfc = sfc_actual_dict.get(sfc, {}) + processed_materials = set() + + import copy + if p_code in bom_trees: + tree_clone = copy.deepcopy(bom_trees[p_code]) + + def fill_tree_qty(nodes, sfc_id): + for idx, node in enumerate(nodes): + node['id'] = f"{sfc_id}_{node['id']}_{idx}" # 确保前端树节点的唯一性 + m_code = node['material_code'] + + bom_info = boms_for_sfc.get(m_code, {}) + act_info = actuals_for_sfc.get(m_code, {}) + + node['bom_qty'] = bom_info.get('bom_qty', 0) + node['actual_qty'] = act_info.get('actual_qty', 0) + + node['diff_qty'] = round(node['actual_qty'] - node['bom_qty'], 4) + if node['diff_qty'] > 0 and node['bom_qty'] > 0: + node['status'] = '超领发料' + elif node['diff_qty'] < 0: + node['status'] = '少领发料' + elif node['diff_qty'] == 0 and node['bom_qty'] > 0: + node['status'] = '发料正常' + elif node['bom_qty'] == 0 and node['actual_qty'] > 0: + node['status'] = 'BOM外发料' + elif node['bom_qty'] > 0 and node['actual_qty'] == 0: + node['status'] = '未发料' + else: + node['status'] = '无发料任务' + + processed_materials.add(m_code) + fill_tree_qty(node['children'], sfc_id) + + fill_tree_qty(tree_clone, sfc) + sfc_node['children'].extend(tree_clone) + + all_m_codes_for_sfc = set(boms_for_sfc.keys()).union(set(actuals_for_sfc.keys())) + extra_materials = all_m_codes_for_sfc - processed_materials + + # 将多出的(或者没有树结构的)物料直接作为工单的子节点 + for i, m_code in enumerate(extra_materials): + bom_info = boms_for_sfc.get(m_code, {}) + act_info = actuals_for_sfc.get(m_code, {}) + + bom_q = bom_info.get('bom_qty', 0) + act_q = act_info.get('actual_qty', 0) + m_name = bom_info.get('material_name') or act_info.get('material_name') or '未知名称' + + diff_q = round(act_q - bom_q, 4) + + status = '' + if diff_q > 0 and bom_q > 0: + status = '超领发料' + elif diff_q < 0: + status = '少领发料' + elif diff_q == 0 and bom_q > 0: + status = '发料正常' + elif bom_q == 0 and act_q > 0: + status = 'BOM外发料' + elif bom_q > 0 and act_q == 0: + status = '未发料' + + sfc_node['children'].append({ + 'id': f"extra_{sfc}_{m_code}_{i}", + 'sfc': '', + 'material_code': m_code, + 'material_name': m_name, + 'bom_qty': bom_q, + 'actual_qty': act_q, + 'diff_qty': diff_q, + 'status': status, + 'children': [] + }) + + result.append(sfc_node) + + # 根据工单号倒序排列 + result.sort(key=lambda x: x['sfc'], reverse=True) + return result if __name__ == '__main__': service = MaterialReconciliationService() diff --git a/update_html.py b/update_html.py new file mode 100644 index 0000000..b4c45fd --- /dev/null +++ b/update_html.py @@ -0,0 +1,57 @@ +import re + +with open('web_ui/templates/reconciliation.html', 'r', encoding='utf-8') as f: + content = f.read() + +# Replace activeTab +content = content.replace("activeTab: 'unmatched'", "activeTab: 'official'") + +# Replace the HTML for Tabs +tabs_start = '' +tabs_end = '' + +start_idx = content.find(tabs_start) +end_idx = content.find(tabs_end, start_idx) + len(tabs_end) + +if start_idx != -1 and end_idx != -1: + new_tabs = """ + + + 工单发料明细 + +
+ ' + +start_idx = content.find(tabs_start) +end_idx = content.find(tabs_ti +start_idx = content.find(tabs +
+ 导出 Excel
- - + @@ -132,17 +131,17 @@