在采购订单查看页面新增"生成到货通知单"按钮,已审核的采购订单可一键生成到货通知单, 自动携带供应商信息和未到货物料明细行。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
965 lines
38 KiB
Vue
965 lines
38 KiB
Vue
<template>
|
||
<div class="form-container">
|
||
<el-card shadow="never">
|
||
<!-- 页面标题和操作按钮 -->
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="page-title">采购订单</span>
|
||
<div class="header-actions">
|
||
<!-- 新增模式 -->
|
||
<template v-if="mode === 'add'">
|
||
<el-button type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
|
||
<el-button @click="handleCancel">取消</el-button>
|
||
<el-button type="success" @click="handleApprove" :disabled="!form.orderId">审核</el-button>
|
||
<el-button type="warning" @click="handleUnapprove" :disabled="true">反审核</el-button>
|
||
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
|
||
</template>
|
||
<!-- 编辑模式 -->
|
||
<template v-else-if="mode === 'edit'">
|
||
<el-button type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
|
||
<el-button @click="handleRevert">撤回</el-button>
|
||
<el-button type="success" @click="handleApprove" :disabled="form.status !== 'PREPARE'">审核</el-button>
|
||
<el-button type="warning" @click="handleUnapprove" :disabled="form.status !== 'APPROVED'">反审核</el-button>
|
||
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
|
||
</template>
|
||
<!-- 查看模式 -->
|
||
<template v-else>
|
||
<el-button type="primary" @click="handleNewFromView">新增</el-button>
|
||
<el-button @click="handleSwitchToEdit">编辑</el-button>
|
||
<el-button type="success" @click="handleApprove" :disabled="form.status !== 'PREPARE'">审核</el-button>
|
||
<el-button type="warning" @click="handleUnapprove" :disabled="form.status !== 'APPROVED'">反审核</el-button>
|
||
<el-button type="info" @click="handleGenArrivalNotice" :disabled="form.status !== 'APPROVED'">生成到货通知单</el-button>
|
||
<el-button @click="handlePrint">打印</el-button>
|
||
<el-button @click="toggleHeaderCollapse">{{ headerCollapsed ? '展开' : '收起' }}</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============ 表头表单区域(可折叠) ============ -->
|
||
<el-form
|
||
v-show="!headerCollapsed"
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="formRules"
|
||
:disabled="mode === 'view'"
|
||
label-width="80px"
|
||
class="header-form"
|
||
>
|
||
<!-- 行1 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="5">
|
||
<el-form-item label="单据编码" prop="orderCode">
|
||
<el-input v-model="form.orderCode" placeholder="自动生成" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="业务类型" prop="businessType">
|
||
<el-select v-model="form.businessType" placeholder="请选择" style="width: 100%">
|
||
<el-option v-for="opt in businessTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="操作员">
|
||
<el-input v-model="form.operatorName" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="供方">
|
||
<el-input v-model="form.supplierName" placeholder="点击选择" readonly @click="openSupplierDialog">
|
||
<template #append>
|
||
<el-button :icon="Search" @click="openSupplierDialog" :disabled="mode === 'view'" />
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="4">
|
||
<el-form-item label="备注信息">
|
||
<el-input v-model="form.remark" type="textarea" :rows="1" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 行2 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="5">
|
||
<el-form-item label="单据日期" prop="orderDate">
|
||
<el-date-picker v-model="form.orderDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="单据类型">
|
||
<el-input v-model="form.orderType" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="审核员">
|
||
<el-input v-model="form.approverName" placeholder="-" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="到货日期">
|
||
<el-date-picker v-model="form.deliveryDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="4" />
|
||
</el-row>
|
||
|
||
<!-- 行3 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="5">
|
||
<el-form-item label="单据状态">
|
||
<el-tag :type="statusTagType" size="default">{{ statusLabel }}</el-tag>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="采购部门">
|
||
<el-select v-model="form.deptId" placeholder="请选择" clearable style="width: 100%">
|
||
<el-option label="采购部" :value="1" />
|
||
<el-option label="生产部" :value="2" />
|
||
<el-option label="仓储部" :value="3" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="审核日期">
|
||
<el-date-picker v-model="form.approveDate" type="date" placeholder="-" disabled value-format="YYYY-MM-DD" style="width: 100%" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="采购合同">
|
||
<el-input v-model="form.contractFile" placeholder="合同附件" disabled />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="4" />
|
||
</el-row>
|
||
|
||
<!-- 行4 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="5">
|
||
<el-form-item label="业务状态" prop="businessStatus">
|
||
<el-select v-model="form.businessStatus" placeholder="请选择" style="width: 100%">
|
||
<el-option v-for="opt in businessStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="采购人员">
|
||
<el-select v-model="form.userId" placeholder="请选择" clearable style="width: 100%">
|
||
<el-option label="张三" :value="1" />
|
||
<el-option label="李四" :value="2" />
|
||
<el-option label="王五" :value="3" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="用料需求">
|
||
<el-select v-model="form.materialNeed" disabled style="width: 100%">
|
||
<el-option label="订单用料" value="订单用料" />
|
||
<el-option label="备库用料" value="备库用料" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="5">
|
||
<el-form-item label="合同号">
|
||
<el-input v-model="form.contractNo" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="4" />
|
||
</el-row>
|
||
</el-form>
|
||
|
||
<!-- ============ 物料明细子表 ============ -->
|
||
<div class="material-section">
|
||
<div class="material-header">
|
||
<span class="section-title">物料信息</span>
|
||
<div v-if="isEditMode" class="material-actions">
|
||
<el-button type="warning" size="small" @click="showImportDialog = true">
|
||
<el-icon><Upload /></el-icon>引入
|
||
</el-button>
|
||
<el-button type="primary" size="small" @click="handleAddLine">
|
||
<el-icon><Plus /></el-icon>新增物料
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="form.lines" stripe border style="width: 100%" show-summary :summary-method="getSummaries">
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column prop="trackCode" label="跟单编号" width="120">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditMode" v-model="row.trackCode" size="small" placeholder="手工单据" />
|
||
<span v-else>{{ row.trackCode || '手工单据' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="planCode" label="计划单号" width="120">
|
||
<template #default="{ row }">{{ row.planCode || '手工单据' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="物料编码" width="150">
|
||
<template #default="{ row, $index }">
|
||
<div v-if="isEditMode" class="inline-select">
|
||
<span class="mono">{{ row.itemCode || '请选择' }}</span>
|
||
<el-button type="primary" link @click="openItemDialog($index)">选择</el-button>
|
||
</div>
|
||
<span v-else>{{ row.itemCode || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="itemName" label="物料名称" min-width="200" show-overflow-tooltip />
|
||
<el-table-column prop="specification" label="型号规格" width="150" />
|
||
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
|
||
<el-table-column prop="needDate" label="需求日期" width="130">
|
||
<template #default="{ row }">
|
||
<el-date-picker v-if="isEditMode" v-model="row.needDate" type="date" placeholder="选择" value-format="YYYY-MM-DD" size="small" style="width: 100%" />
|
||
<span v-else>{{ row.needDate || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="quantity" label="数量" width="100">
|
||
<template #default="{ row }">
|
||
<el-input-number v-if="isEditMode" v-model="row.quantity" :min="0" :precision="0" size="small" style="width: 90px" @change="calcLineAmount(row)" />
|
||
<span v-else>{{ row.quantity }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="unitPrice" label="单价" width="100">
|
||
<template #default="{ row }">
|
||
<el-input-number v-if="isEditMode" v-model="row.unitPrice" :min="0" :precision="2" size="small" style="width: 90px" @change="calcLineAmount(row)" />
|
||
<span v-else>{{ row.unitPrice ?? '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="amount" label="金额" width="100" align="right">
|
||
<template #default="{ row }">{{ row.amount != null ? Number(row.amount).toFixed(2) : '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="arrivedQuantity" label="到货数量" width="90" align="right">
|
||
<template #default="{ row }">{{ row.arrivedQuantity ?? 0 }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="采购说明" min-width="120">
|
||
<template #default="{ row }">
|
||
<el-input v-if="isEditMode" v-model="row.remark" size="small" />
|
||
<span v-else>{{ row.remark || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="isEditMode" label="操作" width="80" fixed="right">
|
||
<template #default="{ $index }">
|
||
<el-button type="danger" link @click="handleDeleteLine($index)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 上一条/下一条 -->
|
||
<div v-if="mode !== 'add'" class="nav-links">
|
||
<el-link type="primary" :disabled="!hasPrev" @click="navigatePrev">上一条</el-link>
|
||
<el-link type="primary" :disabled="!hasNext" @click="navigateNext">下一条</el-link>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- ============ 打印对话框 ============ -->
|
||
<PrintDialog
|
||
v-model:visible="showPrintDialog"
|
||
:config="printConfig"
|
||
/>
|
||
|
||
<!-- ============ 选择供应商弹窗 ============ -->
|
||
<el-dialog v-model="showSupplierDialog" title="选择供应商" width="900px" destroy-on-close>
|
||
<div class="dialog-search">
|
||
<el-input v-model="supplierSearch.supplierName" placeholder="供应商名称" clearable style="width: 200px; margin-right: 8px" />
|
||
<el-input v-model="supplierSearch.contact1" placeholder="业务联系人" clearable style="width: 160px; margin-right: 8px" />
|
||
<el-button type="primary" @click="loadSuppliers">搜索</el-button>
|
||
<el-button type="success" @click="ElMessage.info('快速添加功能开发中')">快速添加</el-button>
|
||
<el-button @click="resetSupplierSearch">查询所有</el-button>
|
||
</div>
|
||
<el-table :data="supplierList" stripe border max-height="400" highlight-current-row>
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column prop="supplierName" label="供应商名称" min-width="200" show-overflow-tooltip />
|
||
<el-table-column prop="tel" label="电话" width="130" />
|
||
<el-table-column prop="contact1" label="业务联系人" width="120" />
|
||
<el-table-column prop="contact1Tel" label="手机号" width="130" />
|
||
<el-table-column prop="enableFlag" label="状态" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.enableFlag === 'Y' ? 'success' : 'info'" size="small">
|
||
{{ row.enableFlag === 'Y' ? '启用' : '停用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="handleSelectSupplier(row)">选择</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<!-- ============ 引入采购计划单明细弹窗 ============ -->
|
||
<el-dialog v-model="showImportDialog" title="采购计划单明细" width="1100px" destroy-on-close>
|
||
<div class="dialog-search">
|
||
<el-input v-model="importSearch.salesOrderCode" placeholder="跟单编号" clearable style="width: 140px; margin-right: 8px" />
|
||
<el-input v-model="importSearch.businessType" placeholder="料品大类" clearable style="width: 120px; margin-right: 8px" />
|
||
<el-input v-model="importSearch.itemCode" placeholder="物料编码" clearable style="width: 140px; margin-right: 8px" />
|
||
<el-input v-model="importSearch.itemName" placeholder="物料名称" clearable style="width: 140px; margin-right: 8px" />
|
||
<el-button type="primary" @click="loadImportData">搜索</el-button>
|
||
<el-button @click="handleSelectAllImport">全选</el-button>
|
||
</div>
|
||
<el-table ref="importTableRef" :data="importList" stripe border max-height="400" @selection-change="handleImportSelectionChange">
|
||
<el-table-column type="selection" width="50" />
|
||
<el-table-column prop="purchaseCode" label="计划单号" width="130" />
|
||
<el-table-column prop="salesUserName" label="销售员" width="80" />
|
||
<el-table-column prop="salesOrderCode" label="跟单编号" width="130" />
|
||
<el-table-column prop="deliveryDate" label="订单交期" width="110" />
|
||
<el-table-column prop="itemCode" label="物料编码" width="120" />
|
||
<el-table-column prop="itemName" label="物料名称" min-width="150" show-overflow-tooltip />
|
||
<el-table-column prop="demandDate" label="需求日期" width="110" />
|
||
<el-table-column prop="purchaseQty" label="需求数量" width="100" align="right" />
|
||
<el-table-column prop="orderedQty" label="已采数量" width="100" align="right" />
|
||
<el-table-column label="未采数量" width="100" align="right">
|
||
<template #default="{ row }">{{ (row.purchaseQty ?? 0) - (row.orderedQty ?? 0) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button @click="showImportDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="handleConfirmImport">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============ 选择物料弹窗(手工新增物料) ============ -->
|
||
<el-dialog v-model="showItemDialog" title="选择物料" width="960px" destroy-on-close @open="handleItemDialogOpen">
|
||
<div class="item-dialog-body">
|
||
<!-- 左侧:分类树 -->
|
||
<div class="category-tree">
|
||
<el-input v-model="itemTypeFilter" placeholder="请输入分类名称" clearable size="small" style="margin-bottom: 8px" />
|
||
<el-scrollbar height="360px">
|
||
<el-tree
|
||
ref="itemTreeRef"
|
||
:data="itemTypeTree"
|
||
:props="{ label: 'label', children: 'children' }"
|
||
node-key="id"
|
||
highlight-current
|
||
default-expand-all
|
||
:filter-node-method="filterTreeNode"
|
||
@node-click="handleTreeNodeClick"
|
||
/>
|
||
</el-scrollbar>
|
||
</div>
|
||
<!-- 右侧:搜索 + 表格 -->
|
||
<div class="item-table-area">
|
||
<div class="item-search-area">
|
||
<el-input v-model="itemSearch.itemCode" placeholder="物料编码" clearable style="width: 140px" @keyup.enter="loadItemTableData" />
|
||
<el-input v-model="itemSearch.itemName" placeholder="物料名称" clearable style="width: 140px" @keyup.enter="loadItemTableData" />
|
||
<el-button type="primary" @click="loadItemTableData">搜索</el-button>
|
||
<el-button @click="resetItemSearch">重置</el-button>
|
||
</div>
|
||
<div class="item-toolbar">
|
||
<el-button size="small" @click="loadAllItems">查询所有</el-button>
|
||
<el-button size="small" :type="continuousSelect ? 'success' : 'default'" @click="continuousSelect = !continuousSelect">连选</el-button>
|
||
</div>
|
||
<el-table :data="itemTableData" v-loading="itemTableLoading" stripe border max-height="340" highlight-current-row>
|
||
<el-table-column prop="itemCode" label="物料编码" width="130" />
|
||
<el-table-column prop="itemName" label="物料名称" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="specification" label="型号规格" width="150" show-overflow-tooltip />
|
||
<el-table-column prop="unitName" label="主计量" width="80" align="center" />
|
||
<el-table-column label="操作" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="handleSelectItem(row)">选择</el-button>
|
||
<el-button type="info" link @click="handleViewItem(row)">查看</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Search, Upload, Plus } from '@element-plus/icons-vue'
|
||
import {
|
||
getPurchaseOrderDetail,
|
||
createPurchaseOrder,
|
||
updatePurchaseOrder,
|
||
approvePurchaseOrder,
|
||
unapprovePurchaseOrder,
|
||
genPurchaseOrderCode,
|
||
genArrivalNotice,
|
||
getPoSupplierList,
|
||
getLeadInList,
|
||
STATUS_MAP,
|
||
BUSINESS_TYPE_OPTIONS,
|
||
BUSINESS_STATUS_OPTIONS,
|
||
type PurchaseOrder,
|
||
type PurchaseOrderLine,
|
||
type PoSupplier,
|
||
type LeadInRow
|
||
} from '@/api/purchaseOrder'
|
||
import PrintDialog from '@/components/print/PrintDialog.vue'
|
||
import type { PrintConfig } from '@/components/print/types'
|
||
import { listMdItem, type MdItem } from '@/api/masterdata/item'
|
||
import { listItemType, handleTree, type ItemType } from '@/api/masterdata/itemtype'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// ============ Mode ============
|
||
const mode = computed(() => {
|
||
if (route.path.includes('/new')) return 'add'
|
||
if (route.path.includes('/edit')) return 'edit'
|
||
return 'view'
|
||
})
|
||
const isEditMode = computed(() => mode.value === 'add' || mode.value === 'edit')
|
||
|
||
// ============ Header collapse ============
|
||
const headerCollapsed = ref(false)
|
||
function toggleHeaderCollapse() { headerCollapsed.value = !headerCollapsed.value }
|
||
|
||
// ============ Form State ============
|
||
const formRef = ref()
|
||
const saveLoading = ref(false)
|
||
const businessTypeOptions = BUSINESS_TYPE_OPTIONS
|
||
const businessStatusOptions = BUSINESS_STATUS_OPTIONS
|
||
|
||
const form = reactive<Partial<PurchaseOrder>>({
|
||
orderId: undefined,
|
||
orderCode: '',
|
||
orderDate: new Date().toISOString().split('T')[0],
|
||
status: 'PREPARE',
|
||
businessType: '原材料',
|
||
businessStatus: 'NORMAL',
|
||
orderType: '采购订单',
|
||
materialNeed: '订单用料',
|
||
supplierId: undefined,
|
||
supplierCode: '',
|
||
supplierName: '',
|
||
deptId: undefined,
|
||
deptName: '',
|
||
userId: undefined,
|
||
userName: '',
|
||
deliveryDate: '',
|
||
contractNo: '',
|
||
contractFile: '',
|
||
totalQuantity: 0,
|
||
totalAmount: 0,
|
||
arrivedQuantity: 0,
|
||
remark: '',
|
||
operatorName: 'admin',
|
||
approverName: '',
|
||
approveDate: '',
|
||
lines: []
|
||
})
|
||
|
||
const formRules = {
|
||
orderDate: [{ required: true, message: '请选择单据日期', trigger: 'change' }],
|
||
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }]
|
||
}
|
||
|
||
// ============ Status helpers ============
|
||
const statusTagType = computed(() => STATUS_MAP[form.status || 'PREPARE']?.type ?? 'info')
|
||
const statusLabel = computed(() => STATUS_MAP[form.status || 'PREPARE']?.label ?? form.status)
|
||
|
||
// ============ Load data ============
|
||
async function loadFormData() {
|
||
const id = route.params.id as string
|
||
if (id && mode.value !== 'add') {
|
||
try {
|
||
const data = await getPurchaseOrderDetail(Number(id))
|
||
Object.assign(form, data)
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error)
|
||
ElMessage.error('加载采购订单数据失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
async function initNewForm() {
|
||
try {
|
||
const code = await genPurchaseOrderCode()
|
||
if (code) form.orderCode = code
|
||
} catch (error) {
|
||
console.error('生成编码失败:', error)
|
||
}
|
||
}
|
||
|
||
// ============ Line operations ============
|
||
function calcLineAmount(row: PurchaseOrderLine) {
|
||
row.amount = (row.quantity || 0) * (row.unitPrice || 0)
|
||
}
|
||
|
||
function computeTotals() {
|
||
const lines = form.lines || []
|
||
form.totalQuantity = lines.reduce((sum, l) => sum + (l.quantity || 0), 0)
|
||
form.totalAmount = lines.reduce((sum, l) => sum + (l.amount || 0), 0)
|
||
}
|
||
|
||
function getSummaries({ columns, data }: any) {
|
||
const sums: string[] = []
|
||
columns.forEach((col: any, index: number) => {
|
||
if (index === 0) { sums[index] = '合计'; return }
|
||
const prop = col.property
|
||
if (prop === 'quantity' || prop === 'amount' || prop === 'arrivedQuantity') {
|
||
const val = data.reduce((sum: number, row: any) => sum + (Number(row[prop]) || 0), 0)
|
||
sums[index] = prop === 'amount' ? val.toFixed(2) : String(val)
|
||
} else {
|
||
sums[index] = ''
|
||
}
|
||
})
|
||
return sums
|
||
}
|
||
|
||
function handleAddLine() {
|
||
form.lines = form.lines || []
|
||
form.lines.push({
|
||
lineId: 0,
|
||
orderId: 0,
|
||
orderCode: '',
|
||
lineNo: form.lines.length + 1,
|
||
trackCode: '',
|
||
planCode: '',
|
||
itemId: 0,
|
||
itemCode: '',
|
||
itemName: '',
|
||
specification: '',
|
||
unitName: '',
|
||
needDate: '',
|
||
quantity: 1,
|
||
unitPrice: 0,
|
||
amount: 0,
|
||
arrivedQuantity: 0,
|
||
remark: ''
|
||
})
|
||
}
|
||
|
||
function handleDeleteLine(index: number) {
|
||
form.lines?.splice(index, 1)
|
||
form.lines?.forEach((l, i) => { l.lineNo = i + 1 })
|
||
}
|
||
|
||
// ============ Supplier dialog ============
|
||
const showSupplierDialog = ref(false)
|
||
const supplierSearch = reactive({ supplierName: '', contact1: '' })
|
||
const supplierList = ref<PoSupplier[]>([])
|
||
|
||
function openSupplierDialog() {
|
||
if (mode.value === 'view') return
|
||
showSupplierDialog.value = true
|
||
loadSuppliers()
|
||
}
|
||
|
||
async function loadSuppliers() {
|
||
const res = await getPoSupplierList({ supplierName: supplierSearch.supplierName, contact1: supplierSearch.contact1 })
|
||
supplierList.value = res.rows
|
||
}
|
||
|
||
function resetSupplierSearch() {
|
||
supplierSearch.supplierName = ''
|
||
supplierSearch.contact1 = ''
|
||
loadSuppliers()
|
||
}
|
||
|
||
function handleSelectSupplier(row: PoSupplier) {
|
||
form.supplierId = row.supplierId
|
||
form.supplierCode = row.supplierCode
|
||
form.supplierName = row.supplierName
|
||
showSupplierDialog.value = false
|
||
}
|
||
|
||
// ============ Import dialog ============
|
||
const showImportDialog = ref(false)
|
||
const importSearch = reactive({ salesOrderCode: '', businessType: '', itemCode: '', itemName: '' })
|
||
const importList = ref<LeadInRow[]>([])
|
||
const selectedImportRows = ref<LeadInRow[]>([])
|
||
const importTableRef = ref()
|
||
|
||
async function loadImportData() {
|
||
const res = await getLeadInList({
|
||
salesOrderCode: importSearch.salesOrderCode || undefined,
|
||
itemCode: importSearch.itemCode || undefined,
|
||
itemName: importSearch.itemName || undefined,
|
||
businessType: importSearch.businessType || undefined
|
||
})
|
||
importList.value = res.rows
|
||
}
|
||
|
||
function handleImportSelectionChange(rows: LeadInRow[]) {
|
||
selectedImportRows.value = rows
|
||
}
|
||
|
||
function handleSelectAllImport() {
|
||
importTableRef.value?.toggleAllSelection()
|
||
}
|
||
|
||
function handleConfirmImport() {
|
||
if (selectedImportRows.value.length === 0) {
|
||
ElMessage.warning('请至少选择一条记录')
|
||
return
|
||
}
|
||
const existingCodes = new Set((form.lines ?? []).map(l => `${l.itemCode}_${l.planCode}`))
|
||
const newLines: PurchaseOrderLine[] = selectedImportRows.value
|
||
.filter(item => !existingCodes.has(`${item.itemCode}_${item.purchaseCode}`))
|
||
.map((item, idx) => ({
|
||
lineId: 0,
|
||
orderId: 0,
|
||
orderCode: '',
|
||
lineNo: (form.lines?.length || 0) + idx + 1,
|
||
trackCode: item.salesOrderCode || '',
|
||
planCode: item.purchaseCode || '',
|
||
itemId: item.itemId,
|
||
itemCode: item.itemCode,
|
||
itemName: item.itemName,
|
||
specification: item.specification || '',
|
||
unitName: item.unitName || '',
|
||
needDate: item.demandDate || '',
|
||
quantity: (item.purchaseQty ?? 0) - (item.orderedQty ?? 0),
|
||
unitPrice: 0,
|
||
amount: 0,
|
||
arrivedQuantity: 0,
|
||
remark: ''
|
||
}))
|
||
form.lines = [...(form.lines || []), ...newLines]
|
||
form.lines.forEach((l, i) => { l.lineNo = i + 1 })
|
||
showImportDialog.value = false
|
||
ElMessage.success(`成功引入 ${newLines.length} 条物料`)
|
||
}
|
||
|
||
// ============ 选择物料弹窗(手工新增物料) ============
|
||
const showItemDialog = ref(false)
|
||
const itemTableLoading = ref(false)
|
||
const itemTableData = ref<MdItem[]>([])
|
||
const editingLineIndex = ref(-1)
|
||
const continuousSelect = ref(false)
|
||
const itemTreeRef = ref()
|
||
const itemTypeTree = ref<any[]>([])
|
||
const itemTypeFilter = ref('')
|
||
const itemSearch = reactive<{ itemCode: string; itemName: string; itemTypeId: number | undefined }>({
|
||
itemCode: '',
|
||
itemName: '',
|
||
itemTypeId: undefined
|
||
})
|
||
|
||
// Watch tree filter text to filter tree nodes
|
||
watch(itemTypeFilter, (val) => {
|
||
itemTreeRef.value?.filter(val)
|
||
})
|
||
|
||
function filterTreeNode(value: string, data: any) {
|
||
if (!value) return true
|
||
return (data.label || '').includes(value)
|
||
}
|
||
|
||
function openItemDialog(idx: number) {
|
||
editingLineIndex.value = idx
|
||
showItemDialog.value = true
|
||
}
|
||
|
||
async function handleItemDialogOpen() {
|
||
// Reset search state each time dialog opens
|
||
itemSearch.itemCode = ''
|
||
itemSearch.itemName = ''
|
||
itemSearch.itemTypeId = undefined
|
||
itemTypeFilter.value = ''
|
||
continuousSelect.value = false
|
||
await Promise.all([loadItemTypeTree(), loadItemTableData()])
|
||
}
|
||
|
||
async function loadItemTypeTree() {
|
||
try {
|
||
const res = await listItemType()
|
||
const flat = (res as any).data || res || []
|
||
const list = Array.isArray(flat) ? flat : []
|
||
const tree = handleTree(list)
|
||
// Convert to el-tree format: { id, label, children }
|
||
function toTreeNodes(items: ItemType[]): any[] {
|
||
return items.map(item => ({
|
||
id: item.itemTypeId,
|
||
label: item.itemTypeName || '',
|
||
raw: item,
|
||
children: item.children && item.children.length > 0 ? toTreeNodes(item.children) : undefined
|
||
}))
|
||
}
|
||
itemTypeTree.value = toTreeNodes(tree)
|
||
} catch (e) {
|
||
console.error('加载物料分类树失败:', e)
|
||
itemTypeTree.value = []
|
||
}
|
||
}
|
||
|
||
async function loadItemTableData() {
|
||
itemTableLoading.value = true
|
||
try {
|
||
const params: any = { enableFlag: 'Y', pageNum: 1, pageSize: 100 }
|
||
if (itemSearch.itemCode) params.itemCode = itemSearch.itemCode
|
||
if (itemSearch.itemName) params.itemName = itemSearch.itemName
|
||
if (itemSearch.itemTypeId) params.itemTypeId = itemSearch.itemTypeId
|
||
const res = await listMdItem(params)
|
||
itemTableData.value = res.rows || []
|
||
} catch (e) {
|
||
console.error('加载物料列表失败:', e)
|
||
itemTableData.value = []
|
||
} finally {
|
||
itemTableLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleTreeNodeClick(node: any) {
|
||
itemSearch.itemTypeId = node.id
|
||
loadItemTableData()
|
||
}
|
||
|
||
function resetItemSearch() {
|
||
itemSearch.itemCode = ''
|
||
itemSearch.itemName = ''
|
||
itemSearch.itemTypeId = undefined
|
||
itemTreeRef.value?.setCurrentKey(null)
|
||
loadItemTableData()
|
||
}
|
||
|
||
function loadAllItems() {
|
||
itemSearch.itemCode = ''
|
||
itemSearch.itemName = ''
|
||
itemSearch.itemTypeId = undefined
|
||
itemTypeFilter.value = ''
|
||
itemTreeRef.value?.setCurrentKey(null)
|
||
loadItemTableData()
|
||
}
|
||
|
||
function handleSelectItem(selected: MdItem) {
|
||
const idx = editingLineIndex.value
|
||
if (idx < 0 || !form.lines) return
|
||
const row = form.lines[idx]
|
||
if (!row) return
|
||
row.itemId = selected.itemId ?? 0
|
||
row.itemCode = selected.itemCode ?? ''
|
||
row.itemName = selected.itemName ?? ''
|
||
row.specification = selected.specification ?? ''
|
||
row.unitName = selected.unitName ?? selected.unitOfMeasure ?? ''
|
||
calcLineAmount(row)
|
||
if (!continuousSelect.value) {
|
||
showItemDialog.value = false
|
||
} else {
|
||
ElMessage.success(`已选择: ${selected.itemCode} - ${selected.itemName}`)
|
||
}
|
||
}
|
||
|
||
function handleViewItem(row: MdItem) {
|
||
ElMessage.info(`物料详情: ${row.itemCode} - ${row.itemName}(${row.specification || '无规格'})`)
|
||
}
|
||
|
||
// ============ Save ============
|
||
async function handleSave() {
|
||
try {
|
||
await formRef.value?.validate()
|
||
} catch {
|
||
ElMessage.warning('请完善必填信息')
|
||
return
|
||
}
|
||
if (!form.lines || form.lines.length === 0) {
|
||
ElMessage.warning('请至少添加一条物料明细')
|
||
return
|
||
}
|
||
const invalidLines = form.lines.filter(l => !l.itemCode || !l.quantity || l.quantity <= 0)
|
||
if (invalidLines.length > 0) {
|
||
ElMessage.warning('请检查物料信息:物料编码不能为空,数量须大于0')
|
||
return
|
||
}
|
||
|
||
computeTotals()
|
||
saveLoading.value = true
|
||
try {
|
||
if (mode.value === 'add') {
|
||
await createPurchaseOrder(form)
|
||
ElMessage.success('保存成功')
|
||
router.push('/purchasing/order')
|
||
} else {
|
||
await updatePurchaseOrder(form)
|
||
ElMessage.success('保存成功')
|
||
await loadFormData()
|
||
}
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
} finally {
|
||
saveLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ============ Cancel / Revert ============
|
||
function handleCancel() { router.push('/purchasing/order') }
|
||
|
||
async function handleRevert() {
|
||
try {
|
||
await ElMessageBox.confirm('确认撤回未保存的修改吗?', '提示', { type: 'warning' })
|
||
await loadFormData()
|
||
ElMessage.success('已撤回')
|
||
} catch { /* cancelled */ }
|
||
}
|
||
|
||
// ============ Approve / Unapprove ============
|
||
async function handleApprove() {
|
||
if (!form.orderId) {
|
||
ElMessage.warning('请先保存后再审核')
|
||
return
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm('确认审核该采购订单吗?', '审核确认', { type: 'info' })
|
||
await approvePurchaseOrder(String(form.orderId))
|
||
ElMessage.success('审核成功')
|
||
await loadFormData()
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') console.error('审核失败:', error)
|
||
}
|
||
}
|
||
|
||
async function handleUnapprove() {
|
||
if (!form.orderId) return
|
||
try {
|
||
await ElMessageBox.confirm('确认反审核该采购订单吗?', '反审核确认', { type: 'warning' })
|
||
await unapprovePurchaseOrder(String(form.orderId))
|
||
ElMessage.success('反审核成功')
|
||
router.replace(`/purchasing/order/edit/${form.orderId}`)
|
||
await loadFormData()
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') console.error('反审核失败:', error)
|
||
}
|
||
}
|
||
|
||
// ============ Generate arrival notice ============
|
||
async function handleGenArrivalNotice() {
|
||
if (!form.orderId) return
|
||
try {
|
||
await ElMessageBox.confirm('确认根据当前采购订单生成到货通知单吗?', '生成到货通知单', { type: 'info' })
|
||
const noticeId = await genArrivalNotice(form.orderId)
|
||
ElMessage.success('到货通知单生成成功')
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') console.error('生成到货通知单失败:', error)
|
||
}
|
||
}
|
||
|
||
// ============ View-mode actions ============
|
||
function handleNewFromView() { router.push('/purchasing/order/new') }
|
||
function handleSwitchToEdit() {
|
||
if (form.orderId) router.replace(`/purchasing/order/edit/${form.orderId}`)
|
||
}
|
||
const showPrintDialog = ref(false)
|
||
|
||
const printConfig = computed<PrintConfig>(() => ({
|
||
title: '采购订单',
|
||
qrCodeValue: form.orderCode || undefined,
|
||
headerFields: [
|
||
{ label: '单据编码', value: String(form.orderCode || '') },
|
||
{ label: '单据日期', value: String(form.orderDate || '') },
|
||
{ label: '单据状态', value: String(statusLabel.value || '') },
|
||
{ label: '业务类型', value: String(businessTypeOptions.find(o => o.value === form.businessType)?.label || form.businessType || '') },
|
||
{ label: '业务状态', value: String(businessStatusOptions.find(o => o.value === form.businessStatus)?.label || form.businessStatus || '') },
|
||
{ label: '供方', value: String(form.supplierName || '') },
|
||
{ label: '采购部门', value: String(form.deptName || '') },
|
||
{ label: '采购人员', value: String(form.userName || '') },
|
||
{ label: '到货日期', value: String(form.deliveryDate || '') },
|
||
{ label: '合同号', value: String(form.contractNo || '') },
|
||
{ label: '用料需求', value: String(form.materialNeed || '') },
|
||
{ label: '备注', value: String(form.remark || '') }
|
||
],
|
||
columns: [
|
||
{ prop: '_index', label: '序号', type: 'index', width: '50px', align: 'center' },
|
||
{ prop: 'trackCode', label: '跟单编号', width: '100px' },
|
||
{ prop: 'planCode', label: '计划单号', width: '100px' },
|
||
{ prop: 'itemCode', label: '物料编码', width: '120px' },
|
||
{ prop: 'itemName', label: '物料名称', width: '150px' },
|
||
{ prop: 'specification', label: '型号规格', width: '120px' },
|
||
{ prop: 'unitName', label: '主计量', width: '70px', align: 'center' },
|
||
{ prop: 'quantity', label: '数量', width: '80px', align: 'right' },
|
||
{ prop: 'unitPrice', label: '单价', width: '90px', align: 'right', type: 'amount' },
|
||
{ prop: 'amount', label: '金额', width: '100px', align: 'right', type: 'amount' },
|
||
{ prop: 'remark', label: '采购说明' }
|
||
],
|
||
data: form.lines || [],
|
||
summaries: [
|
||
{ label: '总数量', value: String(form.totalQuantity ?? 0) },
|
||
{ label: '总金额', value: Number(form.totalAmount ?? 0).toFixed(2) }
|
||
],
|
||
footer: {
|
||
creator: String(form.operatorName || ''),
|
||
approver: String(form.approverName || '')
|
||
}
|
||
}))
|
||
|
||
function handlePrint() { showPrintDialog.value = true }
|
||
|
||
// ============ Prev / Next navigation ============
|
||
const hasPrev = ref(false)
|
||
const hasNext = ref(false)
|
||
const siblingIds = ref<number[]>([])
|
||
const currentIdxInList = computed(() => {
|
||
const id = form.orderId
|
||
return id ? siblingIds.value.indexOf(id) : -1
|
||
})
|
||
|
||
function navigatePrev() {
|
||
const idx = currentIdxInList.value
|
||
if (idx > 0) {
|
||
const prevId = siblingIds.value[idx - 1]
|
||
router.replace(`/purchasing/order/${mode.value === 'edit' ? 'edit' : 'view'}/${prevId}`)
|
||
}
|
||
}
|
||
function navigateNext() {
|
||
const idx = currentIdxInList.value
|
||
if (idx >= 0 && idx < siblingIds.value.length - 1) {
|
||
const nextId = siblingIds.value[idx + 1]
|
||
router.replace(`/purchasing/order/${mode.value === 'edit' ? 'edit' : 'view'}/${nextId}`)
|
||
}
|
||
}
|
||
|
||
// ============ Lifecycle ============
|
||
onMounted(async () => {
|
||
if (mode.value === 'add') {
|
||
await initNewForm()
|
||
} else {
|
||
await loadFormData()
|
||
}
|
||
// Load import data for dialog when opened
|
||
nextTick(() => {
|
||
if (showImportDialog.value) loadImportData()
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.form-container { padding: 4px; }
|
||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||
.page-title { font-size: 16px; font-weight: 600; }
|
||
.header-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.header-form { margin-bottom: 8px; }
|
||
.material-section { margin-top: 8px; }
|
||
.material-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||
.section-title { font-size: 15px; font-weight: 600; color: #303133; }
|
||
.material-actions { display: flex; gap: 8px; }
|
||
.nav-links { display: flex; justify-content: flex-end; gap: 16px; margin-top: 12px; padding: 8px 0; }
|
||
.dialog-search { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-bottom: 16px; }
|
||
.inline-select { display: flex; align-items: center; gap: 4px; }
|
||
.inline-select .mono { font-family: monospace; color: #606266; }
|
||
|
||
/* 选择物料弹窗 */
|
||
.item-dialog-body { display: flex; gap: 12px; min-height: 420px; }
|
||
.category-tree {
|
||
width: 180px;
|
||
min-width: 180px;
|
||
border-right: 1px solid #e4e7ed;
|
||
padding-right: 12px;
|
||
}
|
||
.category-tree :deep(.el-tree-node__label) { font-size: 13px; }
|
||
.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||
background-color: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
.item-table-area { flex: 1; overflow: hidden; }
|
||
.item-search-area {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.item-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
</style>
|