|
|
<template>
|
|
|
<div class="sales-order-scheduling">
|
|
|
<el-card class="mb-4">
|
|
|
<template #header>
|
|
|
<div class="flex justify-between items-center">
|
|
|
<span>销售订单排产</span>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<!-- 搜索区域 -->
|
|
|
<el-form :inline="true" :model="searchForm" class="mb-4">
|
|
|
<el-form-item label="订单编号">
|
|
|
<el-input v-model="searchForm.orderNo" placeholder="请输入订单编号" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="交货日期">
|
|
|
<el-date-picker
|
|
|
v-model="searchForm.deliveryDate"
|
|
|
type="daterange"
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始日期"
|
|
|
end-placeholder="结束日期"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="优先级">
|
|
|
<el-select v-model="searchForm.priority" placeholder="请选择优先级">
|
|
|
<el-option label="紧急" value="urgent" />
|
|
|
<el-option label="普通" value="normal" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="状态">
|
|
|
<el-select v-model="searchForm.status" placeholder="请选择状态">
|
|
|
<el-option label="待排产" value="pending" />
|
|
|
<el-option label="排产中" value="processing" />
|
|
|
<el-option label="已完成" value="completed" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
|
|
<el-button @click="resetSearch">重置</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
<!-- 订单列表 -->
|
|
|
<el-table
|
|
|
:data="sortedOrderList"
|
|
|
border
|
|
|
style="width: 100%"
|
|
|
@sort-change="handleSortChange"
|
|
|
>
|
|
|
<el-table-column prop="saleorder_code" label="订单编号" width="180" sortable="custom" />
|
|
|
<el-table-column prop="material_name" label="物料名称" width="180" />
|
|
|
<el-table-column prop="material_code" label="物料编码" width="120" />
|
|
|
<el-table-column prop="material_model" label="规格型号" width="120" />
|
|
|
<el-table-column prop="order_amount" label="订单数量" width="120" sortable="custom" />
|
|
|
<el-table-column prop="complete_amount" label="完成数量" width="120" />
|
|
|
<el-table-column prop="release_qty" label="下达数量" width="120" />
|
|
|
<el-table-column prop="plan_delivery_date" label="计划交货日期" width="180" sortable="custom" />
|
|
|
<el-table-column prop="priority" label="优先级" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="getPriorityType(row.priority)">
|
|
|
{{ getPriorityText(row.priority) }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="document_status" label="状态" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="getStatusType(row.document_status)">
|
|
|
{{ getStatusText(row.document_status) }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="create_time" label="创建时间" width="180" sortable="custom" />
|
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
<el-button type="primary" link @click="handleViewBom(row)">查看BOM</el-button>
|
|
|
<el-button
|
|
|
v-if="row.document_status === 'pending'"
|
|
|
type="primary"
|
|
|
link
|
|
|
@click="handleSchedule(row)"
|
|
|
>排产</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
<div class="flex justify-end mt-4">
|
|
|
<el-pagination
|
|
|
v-model:current-page="currentPage"
|
|
|
v-model:page-size="pageSize"
|
|
|
:total="total"
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
layout="total, sizes, prev, pager, next"
|
|
|
@size-change="handleSizeChange"
|
|
|
@current-change="handleCurrentChange"
|
|
|
/>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
|
|
|
<!-- BOM分解弹窗 -->
|
|
|
<el-dialog
|
|
|
v-model="bomDialogVisible"
|
|
|
title="BOM分解结果"
|
|
|
width="90%"
|
|
|
>
|
|
|
<div class="bom-container">
|
|
|
<!-- 订单信息 -->
|
|
|
<div class="bom-header mb-4">
|
|
|
<el-descriptions :column="4" border>
|
|
|
<el-descriptions-item label="订单编号">{{ currentOrder?.saleorder_code }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="物料名称">{{ currentOrder?.material_name }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="订单数量">{{ currentOrder?.order_amount }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="计划交货日期">{{ currentOrder?.plan_delivery_date }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
|
|
|
<!-- BOM树 -->
|
|
|
<el-table
|
|
|
:data="flattenedBomData"
|
|
|
border
|
|
|
style="width: 100%"
|
|
|
row-key="id"
|
|
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
|
|
>
|
|
|
<el-table-column prop="materialName" label="物料名称" min-width="200">
|
|
|
<template #default="{ row }">
|
|
|
<span :style="{ paddingLeft: row.level * 20 + 'px' }">
|
|
|
{{ row.level > 0 ? '└─ ' : '' }}{{ row.materialName }}
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="materialCode" label="物料编码" width="120" />
|
|
|
<el-table-column prop="unit" label="单位" width="80" />
|
|
|
<el-table-column prop="bomRatio" label="BOM用量系数" width="120">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.bomRatio }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="requiredAmount" label="需求数量" width="120">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.requiredAmount }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="inventoryAmount" label="当前库存" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.inventoryAmount >= row.requiredAmount ? 'success' : 'warning'">
|
|
|
{{ row.inventoryAmount }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="inTransitAmount" label="在途库存" width="120">
|
|
|
<template #default="{ row }">
|
|
|
{{ row.inTransitAmount }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="netRequiredAmount" label="净需求" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.netRequiredAmount > 0 ? 'danger' : 'success'">
|
|
|
{{ row.netRequiredAmount }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="status" label="状态" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="getBomStatusType(row)">
|
|
|
{{ getBomStatusText(row) }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 排产弹窗 -->
|
|
|
<el-dialog
|
|
|
v-model="scheduleDialogVisible"
|
|
|
title="生产计划排产"
|
|
|
width="90%"
|
|
|
>
|
|
|
<div class="schedule-container">
|
|
|
<!-- 订单信息 -->
|
|
|
<div class="schedule-header mb-4">
|
|
|
<el-descriptions :column="4" border>
|
|
|
<el-descriptions-item label="订单编号">{{ currentOrder?.saleorder_code }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="物料名称">{{ currentOrder?.material_name }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="订单数量">{{ currentOrder?.order_amount }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="计划交货日期">{{ currentOrder?.plan_delivery_date }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
</div>
|
|
|
|
|
|
<!-- 排产表单 -->
|
|
|
<el-form :model="scheduleForm" label-width="120px" class="mb-4">
|
|
|
<el-form-item label="排产类型">
|
|
|
<el-radio-group v-model="scheduleForm.scheduleType" @change="handleScheduleTypeChange">
|
|
|
<el-radio label="auto">自动排产</el-radio>
|
|
|
<el-radio label="manual">手动排产</el-radio>
|
|
|
</el-radio-group>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
<!-- BOM物料排产列表 -->
|
|
|
<div class="schedule-bom-list">
|
|
|
<h3 class="mb-2">BOM物料排产</h3>
|
|
|
<el-table :data="scheduleBomList" border style="width: 100%">
|
|
|
<el-table-column prop="materialName" label="物料名称" min-width="200">
|
|
|
<template #default="{ row }">
|
|
|
<span :style="{ paddingLeft: row.level * 20 + 'px' }">
|
|
|
{{ row.level > 0 ? '└─ ' : '' }}{{ row.materialName }}
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="materialCode" label="物料编码" width="120" />
|
|
|
<el-table-column prop="unit" label="单位" width="80" />
|
|
|
<el-table-column prop="bomRatio" label="BOM用量系数" width="120" />
|
|
|
<el-table-column prop="netRequiredAmount" label="净需求" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.netRequiredAmount > 0 ? 'danger' : 'success'">
|
|
|
{{ row.netRequiredAmount }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="计划生产数量" width="150">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.planAmount"
|
|
|
:min="0"
|
|
|
:max="row.netRequiredAmount"
|
|
|
:step="1"
|
|
|
@change="handlePlanAmountChange(row)"
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="计划开始时间" width="180">
|
|
|
<template #default="{ row }">
|
|
|
<el-date-picker
|
|
|
v-model="row.planStartDate"
|
|
|
type="datetime"
|
|
|
placeholder="选择开始时间"
|
|
|
@change="handleStartDateChange(row)"
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="计划结束时间" width="180">
|
|
|
<template #default="{ row }">
|
|
|
<el-date-picker
|
|
|
v-model="row.planEndDate"
|
|
|
type="datetime"
|
|
|
placeholder="选择结束时间"
|
|
|
:disabled-date="(time) => time < row.planStartDate"
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="生产车间" width="150">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="getWorkshopTagType(row.workshop)">
|
|
|
{{ getWorkshopName(row.workshop) }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
</div>
|
|
|
<template #footer>
|
|
|
<span class="dialog-footer">
|
|
|
<el-button @click="scheduleDialogVisible = false">取消</el-button>
|
|
|
<el-button type="primary" @click="handleConfirmSchedule">确认排产</el-button>
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="salesOrderScheduling" lang="ts">
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
// 搜索表单
|
|
|
const searchForm = ref({
|
|
|
orderNo: '',
|
|
|
deliveryDate: [],
|
|
|
priority: '',
|
|
|
status: ''
|
|
|
})
|
|
|
|
|
|
// 排序配置
|
|
|
const sortConfig = ref({
|
|
|
prop: 'planDeliveryDate',
|
|
|
order: 'ascending'
|
|
|
})
|
|
|
|
|
|
// BOM数据结构
|
|
|
interface BomItem {
|
|
|
id: string
|
|
|
materialName: string
|
|
|
materialCode: string
|
|
|
unit: string
|
|
|
bomRatio: number
|
|
|
requiredAmount: number
|
|
|
inventoryAmount: number
|
|
|
inTransitAmount: number
|
|
|
netRequiredAmount: number
|
|
|
level: number
|
|
|
children?: BomItem[]
|
|
|
planAmount?: number
|
|
|
workshop?: string
|
|
|
planStartDate?: string | Date
|
|
|
planEndDate?: string | Date
|
|
|
}
|
|
|
|
|
|
// 订单数据结构
|
|
|
interface SaleOrder {
|
|
|
sale_order_id: number
|
|
|
saleorder_code: string
|
|
|
material_id: number
|
|
|
material_code: string
|
|
|
material_name: string
|
|
|
material_model: string
|
|
|
order_amount: number
|
|
|
complete_amount: number
|
|
|
release_qty: number
|
|
|
plan_delivery_date: string
|
|
|
priority: number
|
|
|
document_status: string
|
|
|
create_time: string
|
|
|
remark: string
|
|
|
}
|
|
|
|
|
|
// 生产计划数据结构
|
|
|
interface ProductionPlan {
|
|
|
orderId: number
|
|
|
orderNo: string
|
|
|
materialId: number
|
|
|
materialName: string
|
|
|
orderAmount: number
|
|
|
planDeliveryDate: string
|
|
|
priority: number
|
|
|
bomItems: BomItem[]
|
|
|
}
|
|
|
|
|
|
// 订单列表数据
|
|
|
const orderList = ref<SaleOrder[]>([
|
|
|
{
|
|
|
sale_order_id: 1,
|
|
|
saleorder_code: 'SO20240301001',
|
|
|
material_id: 1001,
|
|
|
material_code: 'TYRE-A',
|
|
|
material_name: '轮胎成品A',
|
|
|
material_model: '205/55R16',
|
|
|
order_amount: 100,
|
|
|
complete_amount: 0,
|
|
|
release_qty: 0,
|
|
|
plan_delivery_date: '2024-03-15',
|
|
|
priority: 1,
|
|
|
document_status: 'pending',
|
|
|
create_time: '2024-03-01 10:00:00',
|
|
|
remark: '加急订单'
|
|
|
},
|
|
|
{
|
|
|
sale_order_id: 2,
|
|
|
saleorder_code: 'SO20240301002',
|
|
|
material_id: 1002,
|
|
|
material_code: 'TYRE-B',
|
|
|
material_name: '轮胎成品B',
|
|
|
material_model: '215/55R17',
|
|
|
order_amount: 200,
|
|
|
complete_amount: 50,
|
|
|
release_qty: 100,
|
|
|
plan_delivery_date: '2024-03-20',
|
|
|
priority: 2,
|
|
|
document_status: 'processing',
|
|
|
create_time: '2024-03-01 11:00:00',
|
|
|
remark: '常规订单'
|
|
|
},
|
|
|
{
|
|
|
sale_order_id: 3,
|
|
|
saleorder_code: 'SO20240301003',
|
|
|
material_id: 1003,
|
|
|
material_code: 'TYRE-C',
|
|
|
material_name: '轮胎成品C',
|
|
|
material_model: '225/55R18',
|
|
|
order_amount: 150,
|
|
|
complete_amount: 150,
|
|
|
release_qty: 150,
|
|
|
plan_delivery_date: '2024-03-25',
|
|
|
priority: 2,
|
|
|
document_status: 'completed',
|
|
|
create_time: '2024-03-01 12:00:00',
|
|
|
remark: '常规订单'
|
|
|
}
|
|
|
])
|
|
|
|
|
|
// 计算排序后的订单列表
|
|
|
const sortedOrderList = computed(() => {
|
|
|
let result = [...orderList.value]
|
|
|
|
|
|
// 优先级排序
|
|
|
result.sort((a, b) => {
|
|
|
return a.priority - b.priority
|
|
|
})
|
|
|
|
|
|
// 交货日期排序
|
|
|
if (sortConfig.value.prop === 'plan_delivery_date') {
|
|
|
result.sort((a, b) => {
|
|
|
const dateA = new Date(a.plan_delivery_date).getTime()
|
|
|
const dateB = new Date(b.plan_delivery_date).getTime()
|
|
|
return sortConfig.value.order === 'ascending'
|
|
|
? dateA - dateB
|
|
|
: dateB - dateA
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 订单数量排序
|
|
|
if (sortConfig.value.prop === 'order_amount') {
|
|
|
result.sort((a, b) => {
|
|
|
return sortConfig.value.order === 'ascending'
|
|
|
? a.order_amount - b.order_amount
|
|
|
: b.order_amount - a.order_amount
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 创建时间排序
|
|
|
if (sortConfig.value.prop === 'create_time') {
|
|
|
result.sort((a, b) => {
|
|
|
const dateA = new Date(a.create_time).getTime()
|
|
|
const dateB = new Date(b.create_time).getTime()
|
|
|
return sortConfig.value.order === 'ascending'
|
|
|
? dateA - dateB
|
|
|
: dateB - dateA
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
})
|
|
|
|
|
|
// 当前选中的订单
|
|
|
const currentOrder = ref<SaleOrder | null>(null)
|
|
|
|
|
|
// BOM弹窗数据
|
|
|
const bomDialogVisible = ref(false)
|
|
|
const bomTreeData = ref<BomItem[]>([
|
|
|
{
|
|
|
id: '1001',
|
|
|
materialName: '轮胎成品A',
|
|
|
materialCode: 'TYRE-A',
|
|
|
unit: '个',
|
|
|
bomRatio: 1,
|
|
|
requiredAmount: 100,
|
|
|
inventoryAmount: 20,
|
|
|
inTransitAmount: 10,
|
|
|
netRequiredAmount: 70,
|
|
|
level: 0,
|
|
|
children: [
|
|
|
{
|
|
|
id: '1001-1',
|
|
|
materialName: '胎面组件',
|
|
|
materialCode: 'TREAD-ASSY',
|
|
|
unit: '套',
|
|
|
bomRatio: 1,
|
|
|
requiredAmount: 100,
|
|
|
inventoryAmount: 30,
|
|
|
inTransitAmount: 20,
|
|
|
netRequiredAmount: 50,
|
|
|
level: 1,
|
|
|
children: [
|
|
|
{
|
|
|
id: '1001-1-1',
|
|
|
materialName: '胎面胶',
|
|
|
materialCode: 'TREAD-1',
|
|
|
unit: 'kg',
|
|
|
bomRatio: 2.5,
|
|
|
requiredAmount: 250,
|
|
|
inventoryAmount: 100,
|
|
|
inTransitAmount: 50,
|
|
|
netRequiredAmount: 100,
|
|
|
level: 2
|
|
|
},
|
|
|
{
|
|
|
id: '1001-1-2',
|
|
|
materialName: '钢丝帘布',
|
|
|
materialCode: 'STEEL-CORD',
|
|
|
unit: '米',
|
|
|
bomRatio: 3,
|
|
|
requiredAmount: 300,
|
|
|
inventoryAmount: 150,
|
|
|
inTransitAmount: 50,
|
|
|
netRequiredAmount: 100,
|
|
|
level: 2
|
|
|
}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
id: '1001-2',
|
|
|
materialName: '胎侧组件',
|
|
|
materialCode: 'SIDEWALL-ASSY',
|
|
|
unit: '套',
|
|
|
bomRatio: 2,
|
|
|
requiredAmount: 200,
|
|
|
inventoryAmount: 80,
|
|
|
inTransitAmount: 40,
|
|
|
netRequiredAmount: 80,
|
|
|
level: 1,
|
|
|
children: [
|
|
|
{
|
|
|
id: '1001-2-1',
|
|
|
materialName: '胎侧胶',
|
|
|
materialCode: 'SIDEWALL-1',
|
|
|
unit: 'kg',
|
|
|
bomRatio: 1.5,
|
|
|
requiredAmount: 300,
|
|
|
inventoryAmount: 100,
|
|
|
inTransitAmount: 50,
|
|
|
netRequiredAmount: 150,
|
|
|
level: 2
|
|
|
},
|
|
|
{
|
|
|
id: '1001-2-2',
|
|
|
materialName: '尼龙帘布',
|
|
|
materialCode: 'NYLON-CORD',
|
|
|
unit: '米',
|
|
|
bomRatio: 2,
|
|
|
requiredAmount: 400,
|
|
|
inventoryAmount: 200,
|
|
|
inTransitAmount: 100,
|
|
|
netRequiredAmount: 100,
|
|
|
level: 2
|
|
|
}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
id: '1001-3',
|
|
|
materialName: '胎圈组件',
|
|
|
materialCode: 'BEAD-ASSY',
|
|
|
unit: '套',
|
|
|
bomRatio: 2,
|
|
|
requiredAmount: 200,
|
|
|
inventoryAmount: 60,
|
|
|
inTransitAmount: 40,
|
|
|
netRequiredAmount: 100,
|
|
|
level: 1,
|
|
|
children: [
|
|
|
{
|
|
|
id: '1001-3-1',
|
|
|
materialName: '胎圈钢丝',
|
|
|
materialCode: 'BEAD-WIRE',
|
|
|
unit: 'kg',
|
|
|
bomRatio: 0.8,
|
|
|
requiredAmount: 160,
|
|
|
inventoryAmount: 50,
|
|
|
inTransitAmount: 30,
|
|
|
netRequiredAmount: 80,
|
|
|
level: 2
|
|
|
},
|
|
|
{
|
|
|
id: '1001-3-2',
|
|
|
materialName: '胎圈包布',
|
|
|
materialCode: 'BEAD-WRAP',
|
|
|
unit: '米',
|
|
|
bomRatio: 1.2,
|
|
|
requiredAmount: 240,
|
|
|
inventoryAmount: 100,
|
|
|
inTransitAmount: 50,
|
|
|
netRequiredAmount: 90,
|
|
|
level: 2
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
])
|
|
|
|
|
|
// 扁平化的BOM数据
|
|
|
const flattenedBomData = computed(() => {
|
|
|
const flatten = (items: BomItem[], level = 0): BomItem[] => {
|
|
|
return items.reduce((acc: BomItem[], item) => {
|
|
|
const flatItem = { ...item, level }
|
|
|
acc.push(flatItem)
|
|
|
if (item.children) {
|
|
|
acc.push(...flatten(item.children, level + 1))
|
|
|
}
|
|
|
return acc
|
|
|
}, [])
|
|
|
}
|
|
|
return flatten(bomTreeData.value)
|
|
|
})
|
|
|
|
|
|
// 获取BOM状态类型
|
|
|
const getBomStatusType = (row: BomItem) => {
|
|
|
if (row.netRequiredAmount <= 0) return 'success'
|
|
|
if (row.inventoryAmount + row.inTransitAmount > 0) return 'warning'
|
|
|
return 'danger'
|
|
|
}
|
|
|
|
|
|
// 获取BOM状态文本
|
|
|
const getBomStatusText = (row: BomItem) => {
|
|
|
if (row.netRequiredAmount <= 0) return '库存充足'
|
|
|
if (row.inventoryAmount + row.inTransitAmount > 0) return '部分库存'
|
|
|
return '需要生产'
|
|
|
}
|
|
|
|
|
|
// 排产弹窗
|
|
|
const scheduleDialogVisible = ref(false)
|
|
|
const scheduleForm = ref({
|
|
|
scheduleType: 'auto'
|
|
|
})
|
|
|
|
|
|
// 排产BOM列表
|
|
|
const scheduleBomList = ref<BomItem[]>([])
|
|
|
|
|
|
// 获取优先级类型
|
|
|
const getPriorityType = (priority) => {
|
|
|
const priorityMap = {
|
|
|
1: 'danger',
|
|
|
2: 'info'
|
|
|
}
|
|
|
return priorityMap[priority] || 'info'
|
|
|
}
|
|
|
|
|
|
// 获取优先级文本
|
|
|
const getPriorityText = (priority) => {
|
|
|
const priorityMap = {
|
|
|
1: '紧急',
|
|
|
2: '普通'
|
|
|
}
|
|
|
return priorityMap[priority] || '普通'
|
|
|
}
|
|
|
|
|
|
// 处理排序变化
|
|
|
const handleSortChange = ({ prop, order }) => {
|
|
|
sortConfig.value = { prop, order }
|
|
|
}
|
|
|
|
|
|
// 获取订单列表
|
|
|
const getOrderList = async () => {
|
|
|
try {
|
|
|
// TODO: 调用后端API获取订单列表
|
|
|
// const res = await getSaleOrders({
|
|
|
// page: currentPage.value,
|
|
|
// pageSize: pageSize.value,
|
|
|
// ...searchForm.value
|
|
|
// })
|
|
|
// orderList.value = res.data.list
|
|
|
// total.value = res.data.total
|
|
|
} catch (error) {
|
|
|
ElMessage.error('获取订单列表失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 查看BOM
|
|
|
const handleViewBom = async (row) => {
|
|
|
try {
|
|
|
currentOrder.value = row
|
|
|
// 模拟API调用延迟
|
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
// 根据订单ID筛选对应的BOM数据
|
|
|
const bomData = bomTreeData.value.find(item => item.id === row.material_id.toString())
|
|
|
if (bomData) {
|
|
|
bomTreeData.value = [bomData]
|
|
|
bomDialogVisible.value = true
|
|
|
} else {
|
|
|
ElMessage.warning('未找到对应的BOM数据')
|
|
|
}
|
|
|
} catch (error) {
|
|
|
ElMessage.error('获取BOM数据失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 排产
|
|
|
const handleSchedule = (row) => {
|
|
|
currentOrder.value = row
|
|
|
scheduleForm.value.scheduleType = 'auto' // 默认选择自动排产
|
|
|
|
|
|
// 根据BOM数据生成排产列表,自动关联车间和计算时间
|
|
|
scheduleBomList.value = flattenedBomData.value.map(item => {
|
|
|
const workshop = determineWorkshop(item.materialCode)
|
|
|
const planAmount = item.netRequiredAmount > 0 ? item.netRequiredAmount : 0
|
|
|
const planStartDate = calculatePlanStartDate(item.materialCode, row.plan_delivery_date)
|
|
|
const planEndDate = calculatePlanEndDate(item.materialCode, planAmount, planStartDate)
|
|
|
|
|
|
return {
|
|
|
...item,
|
|
|
planAmount,
|
|
|
workshop,
|
|
|
planStartDate,
|
|
|
planEndDate
|
|
|
}
|
|
|
})
|
|
|
scheduleDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
// 处理计划生产数量变化
|
|
|
const handlePlanAmountChange = (row: BomItem) => {
|
|
|
// 更新子件的计划生产数量
|
|
|
if (row.children) {
|
|
|
row.children.forEach(child => {
|
|
|
child.planAmount = row.planAmount * child.bomRatio
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理计划开始时间变化
|
|
|
const handleStartDateChange = (row: BomItem) => {
|
|
|
// 如果结束时间早于新的开始时间,清空结束时间
|
|
|
if (row.planEndDate && new Date(row.planEndDate) < new Date(row.planStartDate)) {
|
|
|
row.planEndDate = null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 确认排产
|
|
|
const handleConfirmSchedule = async () => {
|
|
|
try {
|
|
|
// 验证BOM排产数据
|
|
|
const invalidItems = scheduleBomList.value.filter(item =>
|
|
|
item.netRequiredAmount > 0 && (
|
|
|
item.planAmount <= 0 ||
|
|
|
!item.planStartDate ||
|
|
|
!item.planEndDate ||
|
|
|
new Date(item.planStartDate) > new Date(item.planEndDate)
|
|
|
)
|
|
|
)
|
|
|
if (invalidItems.length > 0) {
|
|
|
ElMessage.warning('请完善BOM物料的排产信息')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// TODO: 调用后端API保存排产计划
|
|
|
// await saveSchedulePlan({
|
|
|
// orderId: currentOrder.value.id,
|
|
|
// scheduleType: scheduleForm.value.scheduleType,
|
|
|
// bomScheduleList: scheduleBomList.value
|
|
|
// })
|
|
|
|
|
|
ElMessage.success('排产计划保存成功')
|
|
|
scheduleDialogVisible.value = false
|
|
|
} catch (error) {
|
|
|
ElMessage.error('排产计划保存失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 物料类型枚举
|
|
|
const MaterialType = {
|
|
|
FINISHED: 'TYRE', // 成品轮胎
|
|
|
SEMIFINISHED: { // 半成品
|
|
|
TREAD: 'TREAD', // 胎面
|
|
|
SIDEWALL: 'SIDEWALL',// 胎侧
|
|
|
BEAD: 'BEAD', // 胎圈
|
|
|
INNER: 'INNER' // 内胎
|
|
|
},
|
|
|
RAW: { // 原材料
|
|
|
RUBBER: 'RUBBER', // 橡胶
|
|
|
CORD: 'CORD', // 帘布
|
|
|
WIRE: 'WIRE', // 钢丝
|
|
|
OTHER: 'OTHER' // 其他
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 车间类型枚举
|
|
|
const WorkshopType = {
|
|
|
VULCANIZATION: 'vulcanization', // 硫化车间
|
|
|
FORMING: 'forming', // 成型车间
|
|
|
SEMIFINISHED: 'semiFinished' // 半成品车间
|
|
|
}
|
|
|
|
|
|
// 根据物料编码确定生产车间
|
|
|
const determineWorkshop = (materialCode: string): string => {
|
|
|
// 成品轮胎在硫化车间生产
|
|
|
if (materialCode.startsWith(MaterialType.FINISHED)) {
|
|
|
return WorkshopType.VULCANIZATION
|
|
|
}
|
|
|
|
|
|
// 胎面组件、胎侧组件、胎圈组件在成型车间组装
|
|
|
if (materialCode.includes('-ASSY')) {
|
|
|
return WorkshopType.FORMING
|
|
|
}
|
|
|
|
|
|
// 其他半成品在半成品车间生产
|
|
|
return WorkshopType.SEMIFINISHED
|
|
|
}
|
|
|
|
|
|
// 计算BOM生产计划
|
|
|
const calculateBomPlan = (bomItem: BomItem, orderAmount: number) => {
|
|
|
const plans: any[] = []
|
|
|
const workshop = determineWorkshop(bomItem.materialCode)
|
|
|
|
|
|
// 计算当前层级的计划
|
|
|
const requiredAmount = orderAmount * bomItem.bomRatio
|
|
|
const netRequiredAmount = requiredAmount - bomItem.inventoryAmount - bomItem.inTransitAmount
|
|
|
|
|
|
if (netRequiredAmount > 0) {
|
|
|
// 只为需要生产的物料创建计划
|
|
|
plans.push({
|
|
|
materialId: bomItem.id,
|
|
|
materialName: bomItem.materialName,
|
|
|
materialCode: bomItem.materialCode,
|
|
|
unit: bomItem.unit,
|
|
|
bomRatio: bomItem.bomRatio,
|
|
|
requiredAmount,
|
|
|
inventoryAmount: bomItem.inventoryAmount,
|
|
|
inTransitAmount: bomItem.inTransitAmount,
|
|
|
netRequiredAmount,
|
|
|
workshop,
|
|
|
planStartDate: calculatePlanStartDate(bomItem.materialCode, bomItem.plan_delivery_date),
|
|
|
planEndDate: calculatePlanEndDate(bomItem.materialCode, netRequiredAmount, calculatePlanStartDate(bomItem.materialCode, bomItem.plan_delivery_date))
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 递归计算子件的计划
|
|
|
if (bomItem.children) {
|
|
|
bomItem.children.forEach(child => {
|
|
|
plans.push(...calculateBomPlan(child, orderAmount))
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return plans
|
|
|
}
|
|
|
|
|
|
// 生成生产计划
|
|
|
const handleGeneratePlan = async () => {
|
|
|
try {
|
|
|
// 获取所有待排产的订单
|
|
|
const pendingOrders = orderList.value.filter(order => order.document_status === 'pending')
|
|
|
if (pendingOrders.length === 0) {
|
|
|
ElMessage.warning('没有待排产的订单')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 按优先级和交货日期排序
|
|
|
const sortedOrders = [...pendingOrders].sort((a, b) => {
|
|
|
// 优先级排序
|
|
|
const priorityDiff = a.priority - b.priority
|
|
|
if (priorityDiff !== 0) return priorityDiff
|
|
|
|
|
|
// 交货日期排序
|
|
|
const dateA = new Date(a.plan_delivery_date).getTime()
|
|
|
const dateB = new Date(b.plan_delivery_date).getTime()
|
|
|
return dateA - dateB
|
|
|
})
|
|
|
|
|
|
// 生成生产计划
|
|
|
const productionPlans: ProductionPlan[] = []
|
|
|
for (const order of sortedOrders) {
|
|
|
// 获取订单的BOM数据
|
|
|
const bomData = bomTreeData.value.find(item => item.id === order.material_id.toString())
|
|
|
if (!bomData) {
|
|
|
ElMessage.warning(`订单 ${order.saleorder_code} 未找到BOM数据`)
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
// 计算所有需要生产的物料计划
|
|
|
const allBomPlans = calculateBomPlan(bomData, order.order_amount)
|
|
|
|
|
|
// 按车间分组整理生产计划
|
|
|
const vulcanizationPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.VULCANIZATION)
|
|
|
const formingPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.FORMING)
|
|
|
const semiFinishedPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.SEMIFINISHED)
|
|
|
|
|
|
// 确保每个订单只生成一个硫化计划和一个成型计划
|
|
|
if (vulcanizationPlans.length > 1 || formingPlans.length > 1) {
|
|
|
ElMessage.warning(`订单 ${order.saleorder_code} 的成型或硫化计划数量异常`)
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
// 生成最终的生产计划
|
|
|
const plan: ProductionPlan = {
|
|
|
orderId: order.sale_order_id,
|
|
|
orderNo: order.saleorder_code,
|
|
|
materialId: order.material_id,
|
|
|
materialName: order.material_name,
|
|
|
orderAmount: order.order_amount,
|
|
|
planDeliveryDate: order.plan_delivery_date,
|
|
|
priority: order.priority,
|
|
|
vulcanizationPlan: vulcanizationPlans[0],
|
|
|
formingPlan: formingPlans[0],
|
|
|
semiFinishedPlans: semiFinishedPlans
|
|
|
}
|
|
|
|
|
|
productionPlans.push(plan)
|
|
|
}
|
|
|
|
|
|
// TODO: 调用后端API保存生产计划
|
|
|
// await saveProductionPlans(productionPlans)
|
|
|
|
|
|
ElMessage.success('生产计划生成成功')
|
|
|
// 刷新订单列表
|
|
|
getOrderList()
|
|
|
} catch (error) {
|
|
|
ElMessage.error('生产计划生成失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 计算计划开始时间
|
|
|
const calculatePlanStartDate = (materialCode: string, deliveryDate: string): Date => {
|
|
|
const delivery = new Date(deliveryDate)
|
|
|
const now = new Date()
|
|
|
|
|
|
// 根据物料类型和车间计算提前天数
|
|
|
let daysBeforeDelivery = 0
|
|
|
if (materialCode.startsWith(MaterialType.FINISHED)) {
|
|
|
// 成品轮胎在硫化车间,提前3天
|
|
|
daysBeforeDelivery = 3
|
|
|
} else if (materialCode.includes('-ASSY')) {
|
|
|
// 组件在成型车间,提前5天
|
|
|
daysBeforeDelivery = 5
|
|
|
} else {
|
|
|
// 半成品,提前7天
|
|
|
daysBeforeDelivery = 7
|
|
|
}
|
|
|
|
|
|
// 计算计划开始时间
|
|
|
const planStartDate = new Date(delivery)
|
|
|
planStartDate.setDate(planStartDate.getDate() - daysBeforeDelivery)
|
|
|
|
|
|
// 如果计算出的开始时间早于当前时间,则使用当前时间
|
|
|
return planStartDate < now ? now : planStartDate
|
|
|
}
|
|
|
|
|
|
// 计算计划结束时间
|
|
|
const calculatePlanEndDate = (materialCode: string, amount: number, startDate: Date): Date => {
|
|
|
// 根据物料类型和数量计算生产时间
|
|
|
let hoursPerUnit = 0
|
|
|
if (materialCode.startsWith(MaterialType.FINISHED)) {
|
|
|
// 成品轮胎,每个单位需要2小时
|
|
|
hoursPerUnit = 2
|
|
|
} else if (materialCode.includes('-ASSY')) {
|
|
|
// 组件,每个单位需要1.5小时
|
|
|
hoursPerUnit = 1.5
|
|
|
} else {
|
|
|
// 半成品,每个单位需要1小时
|
|
|
hoursPerUnit = 1
|
|
|
}
|
|
|
|
|
|
// 计算总生产时间(小时)
|
|
|
const totalHours = amount * hoursPerUnit
|
|
|
|
|
|
// 计算结束时间
|
|
|
return new Date(startDate.getTime() + totalHours * 60 * 60 * 1000)
|
|
|
}
|
|
|
|
|
|
// 获取状态类型
|
|
|
const getStatusType = (status) => {
|
|
|
const statusMap = {
|
|
|
pending: 'info',
|
|
|
processing: 'warning',
|
|
|
completed: 'success'
|
|
|
}
|
|
|
return statusMap[status] || 'info'
|
|
|
}
|
|
|
|
|
|
// 获取状态文本
|
|
|
const getStatusText = (status) => {
|
|
|
const statusMap = {
|
|
|
pending: '待排产',
|
|
|
processing: '排产中',
|
|
|
completed: '已完成'
|
|
|
}
|
|
|
return statusMap[status] || status
|
|
|
}
|
|
|
|
|
|
// 分页相关方法
|
|
|
const currentPage = ref(1)
|
|
|
const pageSize = ref(10)
|
|
|
const total = ref(0)
|
|
|
|
|
|
const handleSizeChange = (val) => {
|
|
|
pageSize.value = val
|
|
|
getOrderList()
|
|
|
}
|
|
|
|
|
|
const handleCurrentChange = (val) => {
|
|
|
currentPage.value = val
|
|
|
getOrderList()
|
|
|
}
|
|
|
|
|
|
// 搜索和重置
|
|
|
const handleSearch = () => {
|
|
|
currentPage.value = 1
|
|
|
getOrderList()
|
|
|
}
|
|
|
|
|
|
const resetSearch = () => {
|
|
|
searchForm.value = {
|
|
|
orderNo: '',
|
|
|
deliveryDate: [],
|
|
|
priority: '',
|
|
|
status: ''
|
|
|
}
|
|
|
handleSearch()
|
|
|
}
|
|
|
|
|
|
// 获取车间标签类型
|
|
|
const getWorkshopTagType = (workshop: string) => {
|
|
|
const typeMap = {
|
|
|
[WorkshopType.VULCANIZATION]: 'danger',
|
|
|
[WorkshopType.FORMING]: 'warning',
|
|
|
[WorkshopType.SEMIFINISHED]: 'info'
|
|
|
}
|
|
|
return typeMap[workshop] || 'info'
|
|
|
}
|
|
|
|
|
|
// 获取车间名称
|
|
|
const getWorkshopName = (workshop: string) => {
|
|
|
const nameMap = {
|
|
|
[WorkshopType.VULCANIZATION]: '硫化车间',
|
|
|
[WorkshopType.FORMING]: '成型车间',
|
|
|
[WorkshopType.SEMIFINISHED]: '半成品车间'
|
|
|
}
|
|
|
return nameMap[workshop] || workshop
|
|
|
}
|
|
|
|
|
|
// 处理排产类型变化
|
|
|
const handleScheduleTypeChange = (type: string) => {
|
|
|
if (type === 'auto' && currentOrder.value) {
|
|
|
// 自动排产时,重新计算所有物料的时间
|
|
|
scheduleBomList.value = scheduleBomList.value.map(item => {
|
|
|
const planStartDate = calculatePlanStartDate(item.materialCode, currentOrder.value.plan_delivery_date)
|
|
|
const planEndDate = calculatePlanEndDate(item.materialCode, item.planAmount, planStartDate)
|
|
|
return {
|
|
|
...item,
|
|
|
planStartDate,
|
|
|
planEndDate
|
|
|
}
|
|
|
})
|
|
|
} else {
|
|
|
// 手动排产时,清空时间
|
|
|
scheduleBomList.value = scheduleBomList.value.map(item => ({
|
|
|
...item,
|
|
|
planStartDate: null,
|
|
|
planEndDate: null
|
|
|
}))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
getOrderList()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.sales-order-scheduling {
|
|
|
padding: 20px;
|
|
|
}
|
|
|
|
|
|
.bom-container {
|
|
|
padding: 10px;
|
|
|
}
|
|
|
|
|
|
.bom-header {
|
|
|
background-color: #f5f7fa;
|
|
|
padding: 15px;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
:deep(.el-table .el-table__row) {
|
|
|
height: 50px;
|
|
|
}
|
|
|
|
|
|
:deep(.el-table .el-table__row:hover) {
|
|
|
background-color: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
.schedule-container {
|
|
|
padding: 10px;
|
|
|
}
|
|
|
|
|
|
.schedule-header {
|
|
|
background-color: #f5f7fa;
|
|
|
padding: 15px;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
.schedule-bom-list {
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
</style>
|