BOM发料对比

This commit is contained in:
hjq
2026-06-22 16:48:54 +08:00
parent e751d53362
commit 5641188952
3 changed files with 517 additions and 290 deletions

View File

@@ -117,7 +117,7 @@ class MaterialReconciliationService:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
where_clause = "WHERE (inferred_production_order_no = '' OR inferred_production_order_no IS NULL)"
where_clause = "WHERE (production_order_no = '' OR production_order_no IS NULL) AND (inferred_production_order_no = '' OR inferred_production_order_no IS NULL)"
params = []
if start_date and end_date:
@@ -159,7 +159,7 @@ class MaterialReconciliationService:
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"
where_actual = "WHERE inferred_production_order_no != '' AND inferred_production_order_no IS NOT NULL AND (production_order_no = '' OR production_order_no IS NULL)"
sfc_field = "inferred_production_order_no"
params_actual = []
@@ -189,9 +189,12 @@ class MaterialReconciliationService:
work_orders_number as sfc,
product_code,
product_name,
order_date,
workshop,
material_code,
material_name,
theoretical_issue_qty as bom_qty
total_demand_qty as bom_qty,
warehouse_issue_qty as act_qty
FROM abnormal_report
{where_bom}
"""
@@ -211,22 +214,39 @@ class MaterialReconciliationService:
sfc_bom_dict[sfc] = {}
sfc_bom_dict[sfc][mat_code] = {
'bom_qty': r['bom_qty'] or 0,
'act_qty': r['act_qty'] or 0,
'material_name': r['material_name']
}
if r['product_code']:
sfc_to_product[sfc] = {
'product_code': r['product_code'],
'product_name': r['product_name']
'product_name': r['product_name'],
'order_date': r['order_date'],
'workshop': r['workshop']
}
sfc_actual_dict = {} # {sfc: {material_code: {qty, name}}}
# 【新增】针对从发料明细中获取但没有 BOM 基础数据的 SFC尝试从其发料明细中提取产品代码和产品名称
for r in actuals:
sfc = r['sfc']
if sfc not in sfc_to_product:
cursor.execute(f"SELECT product_material_code, product_material_name, execution_time, warehouse_name FROM issue_receipt_details WHERE {sfc_field} = ? LIMIT 1", (sfc,))
prod_row = cursor.fetchone()
if prod_row and prod_row['product_material_code']:
sfc_to_product[sfc] = {
'product_code': prod_row['product_material_code'],
'product_name': prod_row['product_material_name'],
'order_date': prod_row['execution_time'],
'workshop': prod_row['warehouse_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,
'actual_qty': float(r['total_actual_issue']) if r['total_actual_issue'] else 0.0,
'material_name': r['material_name']
}
@@ -236,16 +256,15 @@ class MaterialReconciliationService:
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']]
product_codes = list(set([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))
placeholders = ','.join(['?'] * len(product_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)
""", product_codes)
all_bom_children = cursor.fetchall()
from collections import defaultdict
@@ -262,6 +281,7 @@ class MaterialReconciliationService:
'material_name': row['node_material_name'],
'bom_level': row['bom_level'],
'parent_node_id': row['parent_node_id'],
'usage_qty': row['usage_qty'] or 1.0,
'bom_qty': 0,
'actual_qty': 0,
'diff_qty': 0,
@@ -293,9 +313,11 @@ class MaterialReconciliationService:
'sfc': sfc,
'material_code': p_code,
'material_name': f"【成品】{p_name}",
'bom_qty': '',
'actual_qty': '',
'diff_qty': '',
'order_date': prod_info.get('order_date', ''),
'workshop': prod_info.get('workshop', ''),
'bom_qty': 0.0,
'actual_qty': 0.0,
'diff_qty': 0.0,
'status': '',
'children': []
}
@@ -308,62 +330,109 @@ class MaterialReconciliationService:
if p_code in bom_trees:
tree_clone = copy.deepcopy(bom_trees[p_code])
def fill_tree_qty(nodes, sfc_id):
# 为了将 actuals_for_sfc 里的实际发料数量精确地匹配到树节点上,我们需要处理多层级物料
# 这里先把树结构展平,便于我们累加发料量
flat_nodes = {} # material_code -> list of nodes
def build_flat_nodes(nodes):
for node in nodes:
m_code = node['material_code']
if m_code not in flat_nodes:
flat_nodes[m_code] = []
flat_nodes[m_code].append(node)
build_flat_nodes(node['children'])
build_flat_nodes(tree_clone)
def fill_tree_qty(nodes, sfc_id, multiplier=1.0):
# 临时保存需要保留的节点
valid_nodes = []
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)
# 默认理论应发量 = 单台用量 * 理论产量
current_bom_qty = round(float(node.get('usage_qty', 1.0)) * multiplier, 4)
node['bom_qty'] = current_bom_qty
# 【核心修正】:如果 BOM 台账(abnormal_report)中给出了这个物料在该工单下的明确应发量,则直接覆盖!
if 'bom_qty' in bom_info and float(bom_info['bom_qty']) > 0:
node['bom_qty'] = float(bom_info['bom_qty'])
elif node['bom_qty'] > 0 and 'bom_qty' not in bom_info and m_code not in actuals_for_sfc:
# 这是一个纯理论计算出的子节点,但实际上并没有在这张工单的需求或者发料里出现
node['bom_qty'] = 0
# 由于这棵树是把所有的可能的 BOM 都列出来了,但是我们只查有实际领料记录或者在理论需求里的物料
node['actual_qty'] = 0.0
# 【核心修正】:实际发料量按照“仓库发放数量(act_qty)”来展示。如果BOM台账中有则优先用BOM台账的。
# 如果是完全的BOM外发料则回退使用发料单明细actuals_for_sfc中的数量。
if 'act_qty' in bom_info and float(bom_info['act_qty']) > 0:
if flat_nodes[m_code][0]['id'] == node['id']:
node['actual_qty'] = float(bom_info['act_qty'])
elif m_code in actuals_for_sfc and float(actuals_for_sfc[m_code].get('actual_qty', 0)) > 0:
if flat_nodes[m_code][0]['id'] == node['id']:
node['actual_qty'] = float(actuals_for_sfc[m_code]['actual_qty'])
node['diff_qty'] = round(node['actual_qty'] - node['bom_qty'], 4)
if node['diff_qty'] > 0 and node['bom_qty'] > 0:
if node['bom_qty'] > 0 and node['actual_qty'] == 0:
node['status'] = '未发料'
elif node['diff_qty'] > 0 and node['bom_qty'] > 0:
node['status'] = '超领发料'
elif node['diff_qty'] < 0:
elif node['diff_qty'] < 0 and node['bom_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)
# 递归子节点,将当前节点的 bom_qty 作为子节点的乘数基数
node['children'] = fill_tree_qty(node['children'], sfc_id, node['bom_qty'])
fill_tree_qty(tree_clone, sfc)
# 如果当前节点既没有理论需求也没有实际发料,且也没有有意义的子节点,则修剪掉
if node['bom_qty'] == 0 and node['actual_qty'] == 0 and len(node['children']) == 0:
continue
valid_nodes.append(node)
return valid_nodes
tree_clone = 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)
bom_q = float(bom_info.get('bom_qty', 0))
act_q = float(act_info.get('actual_qty', 0))
m_name = bom_info.get('material_name') or act_info.get('material_name') or '未知名称'
if bom_q == 0 and act_q == 0:
continue
diff_q = round(act_q - bom_q, 4)
status = ''
if diff_q > 0 and bom_q > 0:
if bom_q > 0 and act_q == 0:
status = '未发料'
elif diff_q > 0 and bom_q > 0:
status = '超领发料'
elif diff_q < 0:
elif diff_q < 0 and bom_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 = '发料'
else:
status = '发料任务'
sfc_node['children'].append({
'id': f"extra_{sfc}_{m_code}_{i}",
@@ -377,6 +446,22 @@ class MaterialReconciliationService:
'children': []
})
# 汇总根节点的发料数据
total_bom = 0.0
total_actual = 0.0
def sum_qty(nodes):
nonlocal total_bom, total_actual
for n in nodes:
total_bom += n.get('bom_qty', 0)
total_actual += n.get('actual_qty', 0)
sum_qty(n.get('children', []))
sum_qty(sfc_node['children'])
sfc_node['bom_qty'] = round(total_bom, 4)
sfc_node['actual_qty'] = round(total_actual, 4)
sfc_node['diff_qty'] = round(total_actual - total_bom, 4)
result.append(sfc_node)
# 根据工单号倒序排列