BOM发料对比
This commit is contained in:
@@ -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)
|
||||
|
||||
# 根据工单号倒序排列
|
||||
|
||||
Reference in New Issue
Block a user