feat: 工艺路线支持子图逻辑
This commit is contained in:
parent
a31faa0cda
commit
c002017a84
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.12 on 2025-03-27 04:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mtm', '0054_process_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='routepack',
|
||||
name='gjson',
|
||||
field=models.JSONField(blank=True, default=dict, verbose_name='子图结构'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='routepack',
|
||||
name='material',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='产品'),
|
||||
),
|
||||
]
|
|
@ -214,11 +214,154 @@ class RoutePack(CommonADModel):
|
|||
state = models.PositiveSmallIntegerField('状态', default=10, choices=RP_STATE)
|
||||
name = models.CharField('名称', max_length=100)
|
||||
material = models.ForeignKey(
|
||||
Material, verbose_name='产品', on_delete=models.CASCADE)
|
||||
Material, verbose_name='产品', on_delete=models.CASCADE, null=True, blank=True)
|
||||
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.SET_NULL, related_name='routepack_ticket', null=True, blank=True, db_constraint=False)
|
||||
document = models.ForeignKey("system.file", verbose_name='工艺文件', on_delete=models.SET_NULL, null=True, blank=True, db_constraint=False)
|
||||
gjson = models.JSONField('子图结构', default=dict, blank=True)
|
||||
|
||||
def validate_and_return_gjson(self, raise_exception=True):
|
||||
"""
|
||||
校验所有子图,返回结构化数据:
|
||||
{
|
||||
final_material_out_id1: {
|
||||
"routes": [1, 2, ...], # 子图关联的Route ID
|
||||
"meta": {
|
||||
"is_valid": True,
|
||||
"errors": [],
|
||||
"levels": {material_id: 层级深度}
|
||||
}
|
||||
},
|
||||
final_material_out_id2: { ... }
|
||||
}
|
||||
"""
|
||||
all_routes = Route.objects.filter(routepack=self).order_by('sort', 'process__sort', 'create_time')
|
||||
result = {}
|
||||
|
||||
# 1. 找出所有最终产品(未被任何material_in引用的out_id)
|
||||
all_out_ids = {route.material_out.id for route in all_routes}
|
||||
all_in_ids = {route.material_in.id for route in all_routes}
|
||||
final_ids = all_out_ids - all_in_ids
|
||||
|
||||
if not final_ids:
|
||||
raise ParseError("未找到任何最终产品")
|
||||
|
||||
# 4. 为每个最终产品构建子图
|
||||
for final_id in final_ids:
|
||||
graph = defaultdict(list)
|
||||
# BFS收集子图所有material_id
|
||||
visited = set()
|
||||
queue = deque([final_id])
|
||||
subgraph_materials = set()
|
||||
while queue:
|
||||
current_id = queue.popleft()
|
||||
if current_id in visited:
|
||||
continue
|
||||
visited.add(current_id)
|
||||
subgraph_materials.add(current_id)
|
||||
queue.extend(graph.get(current_id, []))
|
||||
|
||||
# 收集子图关联的Route ID
|
||||
subgraph_route_ids = []
|
||||
for route in all_routes:
|
||||
if route.material_out.id in visited:
|
||||
subgraph_route_ids.append(route.id)
|
||||
|
||||
# 校验子图是否有效(无循环、路径可达)
|
||||
is_valid, errors, levels = self._validate_subgraph(subgraph_materials, graph, final_id)
|
||||
if errors and raise_exception:
|
||||
raise ParseError(errors)
|
||||
|
||||
# 记录子图信息
|
||||
result[final_id] = {
|
||||
"routes": subgraph_route_ids,
|
||||
"meta": {
|
||||
"is_valid": is_valid,
|
||||
"errors": errors,
|
||||
"levels": levels # 层级结构,用于生产排程
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
def _validate_subgraph(materials, graph, final_id):
|
||||
"""校验单个子图的有效性并返回层级结构"""
|
||||
errors = []
|
||||
levels = {}
|
||||
|
||||
# 层级计算(从最终物料逆向遍历)
|
||||
queue = deque([(final_id, 0)])
|
||||
while queue:
|
||||
current_id, level = queue.popleft()
|
||||
if current_id in levels:
|
||||
if level > levels[current_id]:
|
||||
levels[current_id] = level
|
||||
continue
|
||||
levels[current_id] = level
|
||||
for in_id in graph.get(current_id, []):
|
||||
queue.append((in_id, level + 1))
|
||||
|
||||
# 检查循环依赖
|
||||
visited = set()
|
||||
path = set()
|
||||
|
||||
def detect_cycle(m_id):
|
||||
if m_id in path:
|
||||
return True
|
||||
if m_id in visited:
|
||||
return False
|
||||
visited.add(m_id)
|
||||
path.add(m_id)
|
||||
for out_id in graph.get(m_id, []):
|
||||
if detect_cycle(out_id):
|
||||
return True
|
||||
path.remove(m_id)
|
||||
return False
|
||||
|
||||
for m_id in materials:
|
||||
if detect_cycle(m_id):
|
||||
errors.append(f"物料(ID:{m_id})存在循环依赖")
|
||||
return False, errors, {}
|
||||
|
||||
# 检查孤立节点(所有物料必须能到达最终产品)
|
||||
unreachable = materials - set(levels.keys())
|
||||
if unreachable:
|
||||
errors.append(f"存在孤立物料: {unreachable}")
|
||||
return False, errors, {}
|
||||
|
||||
return True, [], levels
|
||||
|
||||
def get_gjson(self, need_update=False, final_material_id=None):
|
||||
if not self.gjson or need_update:
|
||||
self.gjson = self.validate_and_return_gjson()
|
||||
self.save()
|
||||
return self.gjson.get(final_material_id, {}) if final_material_id else self.gjson
|
||||
|
||||
def get_dags(self):
|
||||
gjson = self.get_gjson()
|
||||
finalMaterialIdList = []
|
||||
routeIdList = []
|
||||
for final_material_id, data in gjson.items():
|
||||
finalMaterialIdList.append(final_material_id)
|
||||
routeIdList.extend(data['routes'])
|
||||
mat_qs = Material.objects.filter(id__in=finalMaterialIdList).order_by("process__sort", "create_time")
|
||||
mat_dict = {mat.id: {"id": mat.id, "label": str(mat), "shape": "rect"}for mat in mat_qs}
|
||||
route_qs = Route.objects.filter(id__in=routeIdList).select_related("material_in", "material_out", "process")
|
||||
route_dict = {r.id: {"label": r.process.name, "source": r.material_in.id, "target": r.material_out.id} for r in route_qs}
|
||||
res = {}
|
||||
for final_material_id, data in gjson.items():
|
||||
item = {"name": mat_dict[final_material_id]}
|
||||
edges = []
|
||||
nodes_set = set()
|
||||
for route_id in data['routes']:
|
||||
edges.append(route_dict[route_id])
|
||||
nodes_set.update([route_dict[route_id]['source'], route_dict[route_id]['target']])
|
||||
item['edges'] = edges
|
||||
item['nodes'] = [mat_dict[node_id] for node_id in nodes_set]
|
||||
res[final_material_id] = item
|
||||
return res
|
||||
|
||||
def get_final_material_ids(self):
|
||||
return list(self.get_gjson().keys())
|
||||
class Route(CommonADModel):
|
||||
"""
|
||||
加工路线
|
||||
|
@ -246,7 +389,7 @@ class Route(CommonADModel):
|
|||
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
@staticmethod
|
||||
def get_routes(material: Material=None, routepack:RoutePack=None):
|
||||
def get_routes(material: Material=None, routepack:RoutePack=None, routeIds=None):
|
||||
"""
|
||||
返回工艺路线带车间(不关联工艺包)
|
||||
"""
|
||||
|
@ -262,6 +405,8 @@ class Route(CommonADModel):
|
|||
# raise ParseError('未指定统计步骤')
|
||||
elif routepack:
|
||||
rqs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
|
||||
elif routeIds:
|
||||
rqs = Route.objects.filter(id__in=routeIds).order_by('sort', 'process__sort', 'create_time')
|
||||
return rqs
|
||||
|
||||
@classmethod
|
||||
|
@ -363,17 +508,9 @@ class Route(CommonADModel):
|
|||
'target': target,
|
||||
'label': rq.process.name,
|
||||
})
|
||||
|
||||
# 去重边
|
||||
unique_edges = {}
|
||||
for edge in edges:
|
||||
key = (edge['source'], edge['target'])
|
||||
if key not in unique_edges:
|
||||
unique_edges[key] = edge
|
||||
# 将批次号排序
|
||||
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")
|
||||
|
||||
# batch_to_id = {batch: idx for idx, batch in enumerate(nodes_list)}
|
||||
# 构建节点数据,默认使用'rect'形状
|
||||
nodes = [{
|
||||
'id': item.id,
|
||||
|
@ -381,14 +518,7 @@ class Route(CommonADModel):
|
|||
'shape': 'rect' # 可根据业务需求调整形状
|
||||
} for item in nodes_qs]
|
||||
|
||||
# 构建边数据
|
||||
edges_converted = [{
|
||||
'source': edge['source'],
|
||||
'target': edge['target'],
|
||||
'label': edge['label']
|
||||
} for edge in unique_edges.values()]
|
||||
|
||||
return {'nodes': nodes, 'edges': edges_converted}
|
||||
return {'nodes': nodes, 'edges': edges}
|
||||
class RouteMat(BaseModel):
|
||||
route = models.ForeignKey(Route, verbose_name='关联路线', on_delete=models.CASCADE, related_name="routemat_route")
|
||||
material = models.ForeignKey(Material, verbose_name='辅助物料', on_delete=models.CASCADE)
|
|
@ -146,6 +146,7 @@ def mgroup_run_change(mgroup: Mgroup, new_run: bool, last_timex: datetime, note:
|
|||
|
||||
def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
|
||||
routepack = RoutePack.objects.get(id=new_ticket_data['t_id'])
|
||||
routepack.get_gjson(need_update=True)
|
||||
if routepack.ticket and routepack.ticket.id!=ticket.id:
|
||||
raise ParseError('重复创建工单')
|
||||
if not Route.objects.filter(routepack=routepack).exists():
|
||||
|
|
|
@ -275,6 +275,7 @@ class RoutePackViewSet(CustomModelViewSet):
|
|||
ins.state = RoutePack.RP_S_CREATE
|
||||
elif ins.state == RoutePack.RP_S_CREATE:
|
||||
if ins.ticket is not None:
|
||||
ins.get_gjson(need_update=True)
|
||||
ins.state = RoutePack.RP_S_CONFIRM
|
||||
else:
|
||||
raise ParseError("该路线未提交审核")
|
||||
|
@ -285,7 +286,31 @@ class RoutePackViewSet(CustomModelViewSet):
|
|||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def dag(self, request, *args, **kwargs):
|
||||
"""获取总图
|
||||
|
||||
获取总图
|
||||
"""
|
||||
return Response(Route.get_dag(rqs=Route.objects.filter(routepack=self.get_object())))
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def dags(self, request, *args, **kwargs):
|
||||
"""获取所有子图
|
||||
|
||||
获取所有子图
|
||||
"""
|
||||
ins = self.get_object()
|
||||
return Response(ins.get_dags())
|
||||
|
||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||
def final_materials(self, request, *args, **kwargs):
|
||||
"""获取最终产品
|
||||
|
||||
获取最终产品"""
|
||||
ins:RoutePack = self.get_object()
|
||||
matIds = ins.get_final_material_ids()
|
||||
qs = Material.objects.filter(id__in=matIds)
|
||||
res = [{"id": x.id, "name": str(x)} for x in qs]
|
||||
return Response(res)
|
||||
class RouteViewSet(CustomModelViewSet):
|
||||
queryset = Route.objects.all()
|
||||
serializer_class = RouteSerializer
|
||||
|
|
|
@ -9,6 +9,8 @@ from apps.utils.serializers import CustomModelSerializer
|
|||
from apps.system.models import Dept
|
||||
from apps.wpm.models import Mlog
|
||||
from apps.utils.constants import EXCLUDE_FIELDS_BASE
|
||||
from apps.mtm.models import RoutePack, Material
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class UtaskSerializer(CustomModelSerializer):
|
||||
|
@ -28,9 +30,16 @@ class UtaskSerializer(CustomModelSerializer):
|
|||
'number': {"required": False, "allow_blank": True}
|
||||
}
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
if not validated_data.get('number', None):
|
||||
validated_data["number"] = Utask.get_a_number()
|
||||
routepack:RoutePack = validated_data.get("routepack", None)
|
||||
material:Material = validated_data.get("material", None)
|
||||
if routepack:
|
||||
finMatIds = routepack.get_final_material_ids()
|
||||
if (material and material.id not in finMatIds) or not material:
|
||||
raise ParseError('请指定正确的产品')
|
||||
return super().create(validated_data)
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -43,8 +52,6 @@ class UtaskSerializer(CustomModelSerializer):
|
|||
attrs['count_day'] = math.ceil(attrs['count']/rela_days)
|
||||
except Exception:
|
||||
raise ParseError('日均任务数计划失败')
|
||||
if attrs.get('routepack', None):
|
||||
attrs['material'] = attrs['routepack'].material
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
|
|
@ -49,6 +49,7 @@ class PmService:
|
|||
@classmethod
|
||||
def cal_real_task_count(cls, count: int, rate_list):
|
||||
"""
|
||||
废弃
|
||||
计算实际任务数
|
||||
"""
|
||||
r_list = []
|
||||
|
@ -167,15 +168,18 @@ class PmService:
|
|||
else:
|
||||
# 获取产品的加工路线
|
||||
if utask.routepack: # 指定工艺路线
|
||||
rqs = Route.get_routes(routepack=utask.routepack)
|
||||
gjson_item = utask.routepack.get_gjson(final_material_id=product.id)
|
||||
if not gjson_item:
|
||||
raise ParseError("缺少该产品的生产子图")
|
||||
rqs = Route.get_routes(gjson_item["routes"])
|
||||
else:
|
||||
rqs = Route.get_routes(material=product)
|
||||
if not rqs.exists():
|
||||
raise ParseError('未配置工艺路线')
|
||||
|
||||
# 通过出材率校正任务数, out_rate 默认为 100
|
||||
Route.validate_dag(utask.material, rqs)
|
||||
r_count_dict = cls.cal_x_task_count(utask.material.id, count, rqs)
|
||||
Route.validate_dag(product, rqs)
|
||||
r_count_dict = cls.cal_x_task_count(product.id, count, rqs)
|
||||
|
||||
# 创建小任务
|
||||
for ind, (routeId, count) in enumerate(r_count_dict.items()):
|
||||
|
|
|
@ -917,6 +917,10 @@ def get_batch_dag(batch_number: str):
|
|||
edges = []
|
||||
prev_size = 0
|
||||
|
||||
r_dict = {
|
||||
"split": "分",
|
||||
"merge": "合"
|
||||
}
|
||||
while len(nodes_set) > prev_size:
|
||||
prev_size = len(nodes_set)
|
||||
# 查询所有与当前批次相关的记录(作为source或target)
|
||||
|
@ -931,15 +935,16 @@ def get_batch_dag(batch_number: str):
|
|||
edges.append({
|
||||
'source': source,
|
||||
'target': target,
|
||||
'label': log.relation_type, # 使用relation_type作为边的标签
|
||||
'label': r_dict.get(log.relation_type, ""), # 使用relation_type作为边的标签
|
||||
})
|
||||
|
||||
# 去重边
|
||||
unique_edges = {}
|
||||
for edge in edges:
|
||||
key = (edge['source'], edge['target'])
|
||||
if key not in unique_edges:
|
||||
unique_edges[key] = edge
|
||||
# unique_edges = {}
|
||||
# for edge in edges:
|
||||
# key = (edge['source'], edge['target'])
|
||||
# if key not in unique_edges:
|
||||
# unique_edges[key] = edge
|
||||
|
||||
# 将批次号排序
|
||||
nodes_qs = BatchSt.objects.filter(id__in=nodes_set).order_by('id')
|
||||
|
||||
|
@ -952,11 +957,11 @@ def get_batch_dag(batch_number: str):
|
|||
} for item in nodes_qs]
|
||||
|
||||
# 构建边数据
|
||||
edges_converted = [{
|
||||
'source': edge['source'],
|
||||
'target': edge['target'],
|
||||
'label': edge['label']
|
||||
} for edge in unique_edges.values()]
|
||||
# edges_converted = [{
|
||||
# 'source': edge['source'],
|
||||
# 'target': edge['target'],
|
||||
# 'label': edge['label']
|
||||
# } for edge in unique_edges.values()]
|
||||
|
||||
return {'nodes': nodes, 'edges': edges_converted}
|
||||
return {'nodes': nodes, 'edges': edges}
|
||||
|
||||
|
|
Loading…
Reference in New Issue