|
|
@@ -0,0 +1,870 @@
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ :title="dialogTitle"
|
|
|
+ :visible.sync="visible"
|
|
|
+ width="1050px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ @close="reset"
|
|
|
+ >
|
|
|
+ <template v-if="mode === 'records'">
|
|
|
+ <ele-pro-table
|
|
|
+ ref="recordTable"
|
|
|
+ :columns="recordColumns"
|
|
|
+ :datasource="recordSource"
|
|
|
+ height="450px"
|
|
|
+ :pageSize="10"
|
|
|
+ >
|
|
|
+ <template v-slot:status="{ row }">
|
|
|
+ <el-tag :type="statusType(row.verifyStatus)">
|
|
|
+ {{ statusText(row.verifyStatus) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-slot:action="{ row }">
|
|
|
+ <el-button type="text" @click="showDetail(row)">详情</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="row.verifyStatus === 0"
|
|
|
+ type="text"
|
|
|
+ :loading="recordActionId === row.id"
|
|
|
+ @click="submitRecord(row)"
|
|
|
+ >提交</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ v-if="row.verifyStatus === 1"
|
|
|
+ type="text"
|
|
|
+ :loading="recordActionId === row.id"
|
|
|
+ @click="withdrawRecord(row)"
|
|
|
+ >撤回</el-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </ele-pro-table>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-else v-loading="sourceLoading">
|
|
|
+ <el-alert
|
|
|
+ v-if="mode === 'merge'"
|
|
|
+ title="仅允许同仓库、批次号、物品属性、供应商和价格完全一致的库存合批,审批通过后生成新系统批次。"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-else
|
|
|
+ title="包装明细必须整行分配;锁定明细按锁定单分配,关联包装时自动跟随包装目标。"
|
|
|
+ type="info"
|
|
|
+ :closable="false"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-if="pendingApproveId"
|
|
|
+ title="申请已保存,当前仅续提审批,不会重复生成审核代码。"
|
|
|
+ type="warning"
|
|
|
+ :closable="false"
|
|
|
+ class="saved-alert"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="section-title">来源库存</div>
|
|
|
+ <el-table :data="sources" size="mini" border>
|
|
|
+ <el-table-column
|
|
|
+ prop="systemBatchNo"
|
|
|
+ label="来源系统批次"
|
|
|
+ min-width="150"
|
|
|
+ />
|
|
|
+ <el-table-column prop="batchNo" label="业务批次" />
|
|
|
+ <el-table-column prop="warehouseName" label="仓库" />
|
|
|
+ <el-table-column prop="categoryCode" label="物品编码" />
|
|
|
+ <el-table-column prop="stockCountBase" label="计量数量" />
|
|
|
+ <el-table-column prop="packingQuantity" label="包装数量" />
|
|
|
+ <el-table-column prop="weight" label="重量" />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <template v-if="mode === 'split'">
|
|
|
+ <div class="section-title">目标子批次</div>
|
|
|
+ <el-table :data="targetRows" size="mini" border>
|
|
|
+ <el-table-column label="目标" width="90">
|
|
|
+ <template slot-scope="{ row }"
|
|
|
+ >子批次 {{ row.targetDetailId }}</template
|
|
|
+ >
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="系统批次号" width="130">
|
|
|
+ <template>审批后生成</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="计量数量">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-model="row.target.measureQuantity"
|
|
|
+ :min="hasPacking ? Number(row.packingMeasureQuantity) : 0"
|
|
|
+ :precision="6"
|
|
|
+ :step="0.000001"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ size="mini"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="包装数量">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span v-if="hasPacking">{{ row.packageQuantity }}</span>
|
|
|
+ <el-input-number
|
|
|
+ v-else
|
|
|
+ v-model="row.target.packageQuantity"
|
|
|
+ :min="0"
|
|
|
+ :precision="6"
|
|
|
+ :step="0.000001"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ size="mini"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="重量">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-model="row.target.weight"
|
|
|
+ :min="hasPacking ? Number(row.packingWeight) : 0"
|
|
|
+ :precision="6"
|
|
|
+ :step="0.000001"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ size="mini"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="lockQuantity" label="锁库数量" />
|
|
|
+ <el-table-column width="70">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ @click="removeTarget(row.targetDetailId)"
|
|
|
+ >删除</el-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ @click="addTarget"
|
|
|
+ >新增子批次</el-button
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ <div v-else class="merge-summary">
|
|
|
+ 合并后计量数量:{{ total('stockCountBase') }},包装数量:{{
|
|
|
+ total('packingQuantity')
|
|
|
+ }},重量:{{ total('weight') }};存货周期取来源最长值。
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-if="packingRows.length">
|
|
|
+ <div class="section-title">整包装分配</div>
|
|
|
+ <el-table :data="packingRows" size="mini" border max-height="260">
|
|
|
+ <el-table-column
|
|
|
+ prop="systemBatchNo"
|
|
|
+ label="来源系统批次"
|
|
|
+ min-width="140"
|
|
|
+ />
|
|
|
+ <el-table-column prop="packageNo" label="包装编码" min-width="130" />
|
|
|
+ <el-table-column prop="measureQuantity" label="计量数量" />
|
|
|
+ <el-table-column prop="packageQuantity" label="包装数量" />
|
|
|
+ <el-table-column prop="weight" label="重量" />
|
|
|
+ <el-table-column prop="lockCountBase" label="锁库数量" />
|
|
|
+ <el-table-column label="目标子批次" min-width="150">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-select
|
|
|
+ v-model="row.targetDetailId"
|
|
|
+ size="mini"
|
|
|
+ :disabled="mode === 'merge' || Boolean(pendingApproveId)"
|
|
|
+ placeholder="请选择"
|
|
|
+ @change="syncLockTargets"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="target in targets"
|
|
|
+ :key="target.targetDetailId"
|
|
|
+ :label="targetLabel(target.targetDetailId)"
|
|
|
+ :value="target.targetDetailId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="lockRows.length">
|
|
|
+ <div class="section-title">锁定单分配</div>
|
|
|
+ <el-table :data="lockRows" size="mini" border max-height="260">
|
|
|
+ <el-table-column prop="orderNo" label="锁定单号" min-width="140" />
|
|
|
+ <el-table-column
|
|
|
+ prop="lockOrderDetailId"
|
|
|
+ label="锁定明细ID"
|
|
|
+ min-width="110"
|
|
|
+ />
|
|
|
+ <el-table-column prop="quantity" label="锁定数量" />
|
|
|
+ <el-table-column label="关联包装" min-width="180">
|
|
|
+ <template slot-scope="{ row }">{{ packingNames(row) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="目标子批次" min-width="150">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-select
|
|
|
+ v-model="row.targetDetailId"
|
|
|
+ size="mini"
|
|
|
+ :disabled="
|
|
|
+ mode === 'merge' ||
|
|
|
+ row.lockedPackingStockIds.length > 0 ||
|
|
|
+ Boolean(pendingApproveId)
|
|
|
+ "
|
|
|
+ placeholder="请选择"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="target in targets"
|
|
|
+ :key="target.targetDetailId"
|
|
|
+ :label="targetLabel(target.targetDetailId)"
|
|
|
+ :value="target.targetDetailId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <div v-if="row.targetConflict" class="error-text"
|
|
|
+ >关联包装不能跨目标</div
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-input
|
|
|
+ v-model="remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ :disabled="Boolean(pendingApproveId)"
|
|
|
+ placeholder="申请说明(可选)"
|
|
|
+ class="remark"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="visible = false">取 消</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="mode !== 'records'"
|
|
|
+ type="primary"
|
|
|
+ :loading="saving"
|
|
|
+ :disabled="sourceLoading"
|
|
|
+ @click="submitApply"
|
|
|
+ >{{ pendingApproveId ? '继续提交审批' : '提交审批' }}</el-button
|
|
|
+ >
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ title="批次变更申请详情"
|
|
|
+ :visible.sync="detailVisible"
|
|
|
+ width="850px"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <el-descriptions
|
|
|
+ v-loading="detailLoading"
|
|
|
+ :column="3"
|
|
|
+ border
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ <el-descriptions-item label="审核代码">{{
|
|
|
+ detailData.changeNo || '-'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="变更类型">{{
|
|
|
+ detailData.changeType === 1 ? '拆批' : '合批'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="审批状态">{{
|
|
|
+ statusText(detailData.verifyStatus)
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="仓库">{{
|
|
|
+ detailData.warehouseName || '-'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="申请时间">{{
|
|
|
+ detailData.createTime || '-'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="说明">{{
|
|
|
+ detailData.remark || '-'
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <el-table
|
|
|
+ :data="detailData.details || []"
|
|
|
+ size="mini"
|
|
|
+ border
|
|
|
+ class="detail-table"
|
|
|
+ >
|
|
|
+ <el-table-column prop="sourceStockBatchId" label="来源批次ID" />
|
|
|
+ <el-table-column
|
|
|
+ prop="systemBatchNo"
|
|
|
+ label="来源系统批次"
|
|
|
+ min-width="140"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="targetSystemBatchNo"
|
|
|
+ label="目标系统批次"
|
|
|
+ min-width="140"
|
|
|
+ />
|
|
|
+ <el-table-column prop="measureQuantity" label="计量数量" />
|
|
|
+ <el-table-column prop="packageQuantity" label="包装数量" />
|
|
|
+ <el-table-column prop="weight" label="重量" />
|
|
|
+ <el-table-column prop="lockQuantity" label="锁库数量" />
|
|
|
+ <el-table-column
|
|
|
+ prop="packingStockIds"
|
|
|
+ label="包装库存分配"
|
|
|
+ min-width="180"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="lockAllocations"
|
|
|
+ label="锁定单分配"
|
|
|
+ min-width="220"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ </el-table>
|
|
|
+ </el-dialog>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import BigNumber from 'bignumber.js';
|
|
|
+ import stockBatchChange from '@/api/warehouseManagement/stockBatchChange';
|
|
|
+
|
|
|
+ function decimal(value) {
|
|
|
+ const result = new BigNumber(value == null || value === '' ? 0 : value);
|
|
|
+ return result.isFinite() ? result : new BigNumber(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ function decimalValue(value) {
|
|
|
+ return decimal(value).decimalPlaces(6).toFixed(6);
|
|
|
+ }
|
|
|
+
|
|
|
+ export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ visible: false,
|
|
|
+ mode: 'split',
|
|
|
+ sources: [],
|
|
|
+ targets: [],
|
|
|
+ remark: '',
|
|
|
+ saving: false,
|
|
|
+ sourceLoading: false,
|
|
|
+ detailVisible: false,
|
|
|
+ detailLoading: false,
|
|
|
+ detailData: {},
|
|
|
+ pendingApproveId: null,
|
|
|
+ recordActionId: null
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ dialogTitle() {
|
|
|
+ if (this.mode === 'records') return '批次变更申请记录';
|
|
|
+ return this.mode === 'split' ? '库存拆批申请' : '库存合批申请';
|
|
|
+ },
|
|
|
+ packingRows() {
|
|
|
+ return this.sources.reduce(
|
|
|
+ (rows, source) => rows.concat(source.packingDetails || []),
|
|
|
+ []
|
|
|
+ );
|
|
|
+ },
|
|
|
+ lockRows() {
|
|
|
+ return this.sources.reduce(
|
|
|
+ (rows, source) => rows.concat(source.lockDetails || []),
|
|
|
+ []
|
|
|
+ );
|
|
|
+ },
|
|
|
+ hasPacking() {
|
|
|
+ return this.packingRows.length > 0;
|
|
|
+ },
|
|
|
+ targetRows() {
|
|
|
+ return this.targets.map((target) => {
|
|
|
+ const packages = this.packingRows.filter(
|
|
|
+ (row) => row.targetDetailId === target.targetDetailId
|
|
|
+ );
|
|
|
+ const locks = this.lockRows.filter(
|
|
|
+ (row) => row.targetDetailId === target.targetDetailId
|
|
|
+ );
|
|
|
+ const packingMeasureQuantity = this.sum(packages, 'measureQuantity');
|
|
|
+ const packingWeight = this.sum(packages, 'weight');
|
|
|
+ return {
|
|
|
+ target,
|
|
|
+ targetDetailId: target.targetDetailId,
|
|
|
+ packingMeasureQuantity,
|
|
|
+ packingWeight,
|
|
|
+ measureQuantity: target.measureQuantity,
|
|
|
+ packageQuantity: this.hasPacking
|
|
|
+ ? this.sum(packages, 'packageQuantity')
|
|
|
+ : target.packageQuantity,
|
|
|
+ weight: target.weight,
|
|
|
+ lockQuantity: this.sum(locks, 'quantity')
|
|
|
+ };
|
|
|
+ });
|
|
|
+ },
|
|
|
+ recordColumns() {
|
|
|
+ return [
|
|
|
+ { prop: 'changeNo', label: '审核代码' },
|
|
|
+ {
|
|
|
+ prop: 'changeType',
|
|
|
+ label: '类型',
|
|
|
+ formatter: (row) => (row.changeType === 1 ? '拆批' : '合批')
|
|
|
+ },
|
|
|
+ { prop: 'verifyStatus', slot: 'status', label: '审批状态' },
|
|
|
+ { prop: 'createTime', label: '申请时间' },
|
|
|
+ {
|
|
|
+ columnKey: 'action',
|
|
|
+ slot: 'action',
|
|
|
+ label: '操作',
|
|
|
+ width: 140,
|
|
|
+ fixed: 'right'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async open(mode, rows) {
|
|
|
+ this.mode = mode;
|
|
|
+ this.pendingApproveId = null;
|
|
|
+ this.visible = true;
|
|
|
+ if (mode === 'records') return;
|
|
|
+ this.sourceLoading = true;
|
|
|
+ try {
|
|
|
+ const selectedRows = rows || [];
|
|
|
+ const ids = selectedRows.map((row) => row.stockBatchId || row.id);
|
|
|
+ const result = await stockBatchChange.sourceDetails(ids);
|
|
|
+ const sourceList = Array.isArray(result)
|
|
|
+ ? result
|
|
|
+ : result.sources || [];
|
|
|
+ this.sources = sourceList.map((source) => {
|
|
|
+ const selected =
|
|
|
+ selectedRows.find(
|
|
|
+ (row) =>
|
|
|
+ String(row.stockBatchId || row.id) ===
|
|
|
+ String(source.stockBatchId)
|
|
|
+ ) || {};
|
|
|
+ return this.normalizeSource({ ...selected, ...source });
|
|
|
+ });
|
|
|
+ this.targets =
|
|
|
+ mode === 'merge'
|
|
|
+ ? [this.createTarget(1)]
|
|
|
+ : [this.createTarget(1), this.createTarget(2)];
|
|
|
+ if (mode === 'merge') this.assignMergeTarget();
|
|
|
+ } catch (error) {
|
|
|
+ this.visible = false;
|
|
|
+ this.$message.error(error.message || '批次来源详情加载失败');
|
|
|
+ } finally {
|
|
|
+ this.sourceLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ normalizeSource(source) {
|
|
|
+ const packingDetails = (source.packingDetails || []).map((row) => ({
|
|
|
+ ...row,
|
|
|
+ sourceStockBatchId: source.stockBatchId,
|
|
|
+ systemBatchNo: source.systemBatchNo,
|
|
|
+ targetDetailId: null
|
|
|
+ }));
|
|
|
+ const lockDetails = (source.lockDetails || []).map((row) => ({
|
|
|
+ ...row,
|
|
|
+ sourceStockBatchId: source.stockBatchId,
|
|
|
+ lockedPackingStockIds: this.parseIdList(row.lockedPackingStockIds),
|
|
|
+ targetDetailId: null,
|
|
|
+ targetConflict: false
|
|
|
+ }));
|
|
|
+ return {
|
|
|
+ ...source,
|
|
|
+ stockCountBase:
|
|
|
+ source.stockCountBase == null
|
|
|
+ ? source.measureQuantity
|
|
|
+ : source.stockCountBase,
|
|
|
+ packingQuantity:
|
|
|
+ source.packingQuantity == null
|
|
|
+ ? source.packageQuantity
|
|
|
+ : source.packingQuantity,
|
|
|
+ packingDetails,
|
|
|
+ lockDetails
|
|
|
+ };
|
|
|
+ },
|
|
|
+ parseIdList(value) {
|
|
|
+ if (Array.isArray(value)) return value.map(String);
|
|
|
+ if (!value) return [];
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(value);
|
|
|
+ return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
|
+ } catch (error) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ createTarget(targetDetailId) {
|
|
|
+ return {
|
|
|
+ targetDetailId,
|
|
|
+ measureQuantity: 0,
|
|
|
+ packageQuantity: 0,
|
|
|
+ weight: 0
|
|
|
+ };
|
|
|
+ },
|
|
|
+ reset() {
|
|
|
+ this.sources = [];
|
|
|
+ this.targets = [];
|
|
|
+ this.remark = '';
|
|
|
+ this.detailVisible = false;
|
|
|
+ this.detailData = {};
|
|
|
+ this.pendingApproveId = null;
|
|
|
+ },
|
|
|
+ addTarget() {
|
|
|
+ const maxId = this.targets.reduce(
|
|
|
+ (max, target) => Math.max(max, target.targetDetailId),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+ this.targets.push(this.createTarget(maxId + 1));
|
|
|
+ },
|
|
|
+ removeTarget(targetDetailId) {
|
|
|
+ if (this.targets.length <= 2)
|
|
|
+ return this.$message.warning('拆批至少保留两个目标子批次');
|
|
|
+ if (
|
|
|
+ this.packingRows.some(
|
|
|
+ (row) => row.targetDetailId === targetDetailId
|
|
|
+ ) ||
|
|
|
+ this.lockRows.some((row) => row.targetDetailId === targetDetailId)
|
|
|
+ ) {
|
|
|
+ return this.$message.warning('该目标已有包装或锁定单分配,不能删除');
|
|
|
+ }
|
|
|
+ this.targets = this.targets.filter(
|
|
|
+ (target) => target.targetDetailId !== targetDetailId
|
|
|
+ );
|
|
|
+ },
|
|
|
+ assignMergeTarget() {
|
|
|
+ this.packingRows.forEach((row) => {
|
|
|
+ row.targetDetailId = 1;
|
|
|
+ });
|
|
|
+ this.lockRows.forEach((row) => {
|
|
|
+ row.targetDetailId = 1;
|
|
|
+ });
|
|
|
+ this.syncTargetPackingMinimums();
|
|
|
+ },
|
|
|
+ /** 包装目标变化后,关联包装的锁定明细必须自动落到同一目标。 */
|
|
|
+ syncLockTargets() {
|
|
|
+ const targetByPackingId = this.packingRows.reduce((map, row) => {
|
|
|
+ map[String(row.packingStockId)] = row.targetDetailId;
|
|
|
+ return map;
|
|
|
+ }, {});
|
|
|
+ this.lockRows.forEach((lock) => {
|
|
|
+ if (!lock.lockedPackingStockIds.length) return;
|
|
|
+ const targetIds = lock.lockedPackingStockIds
|
|
|
+ .map((id) => targetByPackingId[String(id)])
|
|
|
+ .filter(Boolean);
|
|
|
+ const distinct = Array.from(new Set(targetIds));
|
|
|
+ lock.targetConflict = distinct.length > 1;
|
|
|
+ lock.targetDetailId =
|
|
|
+ !lock.targetConflict &&
|
|
|
+ targetIds.length === lock.lockedPackingStockIds.length
|
|
|
+ ? distinct[0]
|
|
|
+ : null;
|
|
|
+ });
|
|
|
+ this.syncTargetPackingMinimums();
|
|
|
+ },
|
|
|
+ /** 包装变更后同步目标下限,超出包装汇总的部分作为散量。 */
|
|
|
+ syncTargetPackingMinimums() {
|
|
|
+ this.targets.forEach((target) => {
|
|
|
+ const packages = this.packingRows.filter(
|
|
|
+ (row) => row.targetDetailId === target.targetDetailId
|
|
|
+ );
|
|
|
+ const measureMinimum = decimal(this.sum(packages, 'measureQuantity'));
|
|
|
+ const weightMinimum = decimal(this.sum(packages, 'weight'));
|
|
|
+ if (decimal(target.measureQuantity).lt(measureMinimum)) {
|
|
|
+ target.measureQuantity = Number(measureMinimum.toFixed(6));
|
|
|
+ }
|
|
|
+ if (decimal(target.weight).lt(weightMinimum)) {
|
|
|
+ target.weight = Number(weightMinimum.toFixed(6));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ targetLabel(id) {
|
|
|
+ return this.mode === 'merge' ? '合并目标' : `子批次 ${id}`;
|
|
|
+ },
|
|
|
+ packingNames(lock) {
|
|
|
+ if (!lock.lockedPackingStockIds.length) return '-';
|
|
|
+ return lock.lockedPackingStockIds
|
|
|
+ .map((id) => {
|
|
|
+ const packing = this.packingRows.find(
|
|
|
+ (row) => String(row.packingStockId) === String(id)
|
|
|
+ );
|
|
|
+ return packing ? packing.packageNo : id;
|
|
|
+ })
|
|
|
+ .join('、');
|
|
|
+ },
|
|
|
+ sum(rows, key) {
|
|
|
+ return rows
|
|
|
+ .reduce((sum, row) => sum.plus(decimal(row[key])), new BigNumber(0))
|
|
|
+ .toFixed(6);
|
|
|
+ },
|
|
|
+ total(key) {
|
|
|
+ return this.sum(this.sources, key);
|
|
|
+ },
|
|
|
+ statusText(value) {
|
|
|
+ return (
|
|
|
+ { 0: '未提交', 1: '审批中', 2: '已通过', 3: '已驳回', 4: '已撤回' }[
|
|
|
+ value
|
|
|
+ ] || '-'
|
|
|
+ );
|
|
|
+ },
|
|
|
+ statusType(value) {
|
|
|
+ return value === 2 ? 'success' : value === 3 ? 'danger' : 'info';
|
|
|
+ },
|
|
|
+ recordSource({ page, limit, where }) {
|
|
|
+ return stockBatchChange.page({ ...where, pageNum: page, size: limit });
|
|
|
+ },
|
|
|
+ async showDetail(row) {
|
|
|
+ this.detailVisible = true;
|
|
|
+ this.detailLoading = true;
|
|
|
+ this.detailData = {};
|
|
|
+ try {
|
|
|
+ this.detailData = (await stockBatchChange.detail(row.id)) || {};
|
|
|
+ } catch (error) {
|
|
|
+ this.detailVisible = false;
|
|
|
+ this.$message.error(error.message || '申请详情加载失败');
|
|
|
+ } finally {
|
|
|
+ this.detailLoading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async submitRecord(row) {
|
|
|
+ this.recordActionId = row.id;
|
|
|
+ try {
|
|
|
+ await stockBatchChange.submit({ id: row.id });
|
|
|
+ this.$message.success('申请已提交审批');
|
|
|
+ this.reloadRecords();
|
|
|
+ } finally {
|
|
|
+ this.recordActionId = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async withdrawRecord(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认撤回该批次变更申请?', '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.recordActionId = row.id;
|
|
|
+ try {
|
|
|
+ await stockBatchChange.withdraw({ id: row.id });
|
|
|
+ this.$message.success('申请已撤回');
|
|
|
+ this.reloadRecords();
|
|
|
+ } finally {
|
|
|
+ this.recordActionId = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ reloadRecords() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.recordTable) this.$refs.recordTable.reload();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getBatchAttributeDifferences() {
|
|
|
+ const attributes = [
|
|
|
+ { key: 'warehouseId', label: '仓库', displayKey: 'warehouseName' },
|
|
|
+ { key: 'batchNo', label: '业务批次号' },
|
|
|
+ { key: 'categoryCode', label: '物品编码' },
|
|
|
+ { key: 'categoryName', label: '物品名称' },
|
|
|
+ { key: 'specification', label: '规格' },
|
|
|
+ { key: 'categoryModel', label: '型号' },
|
|
|
+ { key: 'brandNum', label: '牌号' },
|
|
|
+ { key: 'colorKey', label: '颜色' },
|
|
|
+ { key: 'modelKey', label: '机型' },
|
|
|
+ { key: 'unitPrice', label: '单价' },
|
|
|
+ { key: 'supplierId', label: '供应商', displayKey: 'supplierName' }
|
|
|
+ ];
|
|
|
+ return attributes.reduce((differences, attribute) => {
|
|
|
+ const values = this.sources.map((row) =>
|
|
|
+ String(row[attribute.key] == null ? '' : row[attribute.key])
|
|
|
+ );
|
|
|
+ if (new Set(values).size < 2) return differences;
|
|
|
+ const displayValues = Array.from(
|
|
|
+ new Set(
|
|
|
+ this.sources.map((row) => {
|
|
|
+ const displayValue = attribute.displayKey
|
|
|
+ ? row[attribute.displayKey]
|
|
|
+ : null;
|
|
|
+ const value =
|
|
|
+ displayValue == null || displayValue === ''
|
|
|
+ ? row[attribute.key]
|
|
|
+ : displayValue;
|
|
|
+ return value == null || value === '' ? '未填写' : value;
|
|
|
+ })
|
|
|
+ )
|
|
|
+ );
|
|
|
+ differences.push(`${attribute.label}(${displayValues.join('、')})`);
|
|
|
+ return differences;
|
|
|
+ }, []);
|
|
|
+ },
|
|
|
+ validateAllocation() {
|
|
|
+ if (this.mode === 'split' && this.targets.length < 2)
|
|
|
+ return '拆批至少需要两个目标子批次';
|
|
|
+ if (this.packingRows.some((row) => !row.targetDetailId))
|
|
|
+ return '请为每条包装明细选择目标子批次';
|
|
|
+ if (
|
|
|
+ this.lockRows.some((row) => row.targetConflict || !row.targetDetailId)
|
|
|
+ )
|
|
|
+ return '请完成锁定单分配,关联包装必须属于同一目标';
|
|
|
+ if (this.targetRows.some((row) => decimal(row.measureQuantity).lte(0)))
|
|
|
+ return '各目标子批次计量数量必须大于零';
|
|
|
+ if (
|
|
|
+ this.targetRows.some((row) =>
|
|
|
+ decimal(row.lockQuantity).gt(decimal(row.measureQuantity))
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return '目标子批次锁库数量不能大于计量数量';
|
|
|
+ if (
|
|
|
+ this.hasPacking &&
|
|
|
+ this.targetRows.some(
|
|
|
+ (row) =>
|
|
|
+ decimal(row.measureQuantity).lt(
|
|
|
+ decimal(row.packingMeasureQuantity)
|
|
|
+ ) || decimal(row.weight).lt(decimal(row.packingWeight))
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return '目标计量数量和重量不能小于所选整包装汇总';
|
|
|
+ const source = this.sources[0];
|
|
|
+ if (this.mode === 'split') {
|
|
|
+ if (
|
|
|
+ !decimal(this.sum(this.targetRows, 'measureQuantity')).eq(
|
|
|
+ decimal(source.stockCountBase)
|
|
|
+ ) ||
|
|
|
+ !decimal(this.sum(this.targetRows, 'packageQuantity')).eq(
|
|
|
+ decimal(source.packingQuantity)
|
|
|
+ ) ||
|
|
|
+ !decimal(this.sum(this.targetRows, 'weight')).eq(
|
|
|
+ decimal(source.weight)
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ return '目标计量数量、包装数量和重量合计必须等于来源库存';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+ buildLockAllocations(sourceStockBatchId, targetDetailId) {
|
|
|
+ return JSON.stringify(
|
|
|
+ this.lockRows
|
|
|
+ .filter(
|
|
|
+ (row) =>
|
|
|
+ row.sourceStockBatchId === sourceStockBatchId &&
|
|
|
+ row.targetDetailId === targetDetailId
|
|
|
+ )
|
|
|
+ .map((row) => ({
|
|
|
+ lockOrderDetailId: row.lockOrderDetailId,
|
|
|
+ quantity: decimalValue(row.quantity)
|
|
|
+ }))
|
|
|
+ );
|
|
|
+ },
|
|
|
+ buildPackingIds(sourceStockBatchId, targetDetailId) {
|
|
|
+ return JSON.stringify(
|
|
|
+ this.packingRows
|
|
|
+ .filter(
|
|
|
+ (row) =>
|
|
|
+ row.sourceStockBatchId === sourceStockBatchId &&
|
|
|
+ row.targetDetailId === targetDetailId
|
|
|
+ )
|
|
|
+ .map((row) => row.packingStockId)
|
|
|
+ );
|
|
|
+ },
|
|
|
+ buildSplitDetails() {
|
|
|
+ const source = this.sources[0];
|
|
|
+ return this.targetRows.map((row) => ({
|
|
|
+ detailType: 2,
|
|
|
+ sourceStockBatchId: source.stockBatchId,
|
|
|
+ targetDetailId: row.targetDetailId,
|
|
|
+ batchNo: source.batchNo,
|
|
|
+ systemBatchNo: source.systemBatchNo,
|
|
|
+ measureQuantity: decimalValue(row.measureQuantity),
|
|
|
+ packageQuantity: decimalValue(row.packageQuantity),
|
|
|
+ weight: decimalValue(row.weight),
|
|
|
+ lockQuantity: decimalValue(row.lockQuantity),
|
|
|
+ packingStockIds: this.buildPackingIds(
|
|
|
+ source.stockBatchId,
|
|
|
+ row.targetDetailId
|
|
|
+ ),
|
|
|
+ lockAllocations: this.buildLockAllocations(
|
|
|
+ source.stockBatchId,
|
|
|
+ row.targetDetailId
|
|
|
+ )
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ buildMergeDetails() {
|
|
|
+ return this.sources.map((source) => ({
|
|
|
+ detailType: 1,
|
|
|
+ sourceStockBatchId: source.stockBatchId,
|
|
|
+ targetDetailId: 1,
|
|
|
+ batchNo: source.batchNo,
|
|
|
+ systemBatchNo: source.systemBatchNo,
|
|
|
+ measureQuantity: decimalValue(source.stockCountBase),
|
|
|
+ packageQuantity: decimalValue(source.packingQuantity),
|
|
|
+ weight: decimalValue(source.weight),
|
|
|
+ lockQuantity: this.sum(source.lockDetails || [], 'quantity'),
|
|
|
+ packingStockIds: this.buildPackingIds(source.stockBatchId, 1),
|
|
|
+ lockAllocations: this.buildLockAllocations(source.stockBatchId, 1)
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ async submitApply() {
|
|
|
+ if (this.pendingApproveId) return this.continueSubmit();
|
|
|
+ if (!this.sources.length)
|
|
|
+ return this.$message.warning('请选择库存批次');
|
|
|
+ if (this.mode === 'merge' && this.sources.length < 2)
|
|
|
+ return this.$message.warning('合批至少选择两条库存');
|
|
|
+ const attributeDifferences = this.getBatchAttributeDifferences();
|
|
|
+ if (this.mode === 'merge' && attributeDifferences.length)
|
|
|
+ return this.$message.warning(
|
|
|
+ `所选库存无法合批,以下信息不一致:${attributeDifferences.join(
|
|
|
+ ';'
|
|
|
+ )}。`
|
|
|
+ );
|
|
|
+ const allocationError = this.validateAllocation();
|
|
|
+ if (allocationError) return this.$message.warning(allocationError);
|
|
|
+ this.saving = true;
|
|
|
+ try {
|
|
|
+ const source = this.sources[0];
|
|
|
+ const saved = await stockBatchChange.save({
|
|
|
+ changeType: this.mode === 'split' ? 1 : 2,
|
|
|
+ warehouseId: source.warehouseId,
|
|
|
+ warehouseName: source.warehouseName,
|
|
|
+ remark: this.remark,
|
|
|
+ details:
|
|
|
+ this.mode === 'split'
|
|
|
+ ? this.buildSplitDetails()
|
|
|
+ : this.buildMergeDetails()
|
|
|
+ });
|
|
|
+ this.pendingApproveId = saved.id || saved;
|
|
|
+ await stockBatchChange.submit({ id: this.pendingApproveId });
|
|
|
+ this.pendingApproveId = null;
|
|
|
+ this.$message.success('申请已提交,审批通过后生效');
|
|
|
+ this.visible = false;
|
|
|
+ this.$emit('success');
|
|
|
+ } finally {
|
|
|
+ this.saving = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async continueSubmit() {
|
|
|
+ this.saving = true;
|
|
|
+ try {
|
|
|
+ await stockBatchChange.submit({ id: this.pendingApproveId });
|
|
|
+ this.pendingApproveId = null;
|
|
|
+ this.$message.success('申请已提交,审批通过后生效');
|
|
|
+ this.visible = false;
|
|
|
+ this.$emit('success');
|
|
|
+ } finally {
|
|
|
+ this.saving = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+ .section-title {
|
|
|
+ margin: 14px 0 8px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .merge-summary {
|
|
|
+ padding: 14px 0 0;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ .remark {
|
|
|
+ margin-top: 14px;
|
|
|
+ }
|
|
|
+ .saved-alert {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+ .error-text {
|
|
|
+ margin-top: 4px;
|
|
|
+ color: #f56c6c;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ .detail-table {
|
|
|
+ margin-top: 14px;
|
|
|
+ }
|
|
|
+</style>
|