Dockerfile 部署
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user