|
|
@@ -0,0 +1,1001 @@
|
|
|
+<template>
|
|
|
+ <div class="borderless" v-loading="pageLoading">
|
|
|
+ <div class="customer-head">
|
|
|
+ <div class="customer-back">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ style="border: none; background: none; color: red"
|
|
|
+ icon="el-icon-arrow-left"
|
|
|
+ @click="backToList"
|
|
|
+ >返回列表
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="add-customer-btn">
|
|
|
+ <el-button
|
|
|
+ v-if="!isView"
|
|
|
+ size="small"
|
|
|
+ :loading="saveLoading"
|
|
|
+ @click="saveDraft"
|
|
|
+ >保存
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="!isView"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="submitLoading"
|
|
|
+ @click="submitBill"
|
|
|
+ >提交
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="showRevoke"
|
|
|
+ type="warning"
|
|
|
+ plain
|
|
|
+ size="small"
|
|
|
+ :loading="revokeLoading"
|
|
|
+ @click="handleRevoke"
|
|
|
+ >撤销
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="inventory-check-print-wrap">
|
|
|
+ <trade-card title="基本信息" :show-btn="false">
|
|
|
+ <el-form
|
|
|
+ ref="headerForm"
|
|
|
+ :model="form"
|
|
|
+ :rules="headerRules"
|
|
|
+ label-width="100px"
|
|
|
+ size="small"
|
|
|
+ :disabled="isView"
|
|
|
+ >
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="盘点单号" prop="formNumber">
|
|
|
+ <el-input v-model="form.formNumber" placeholder="保存后生成" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="仓库" prop="warehouseId">
|
|
|
+ <el-select
|
|
|
+ v-model="form.warehouseId"
|
|
|
+ placeholder="请选择仓库(可选)"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ style="width: 100%"
|
|
|
+ :loading="warehouseLoading"
|
|
|
+ @change="onWarehouseChange"
|
|
|
+ @visible-change="onWarehouseSelectVisible"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="w in warehouseOptions"
|
|
|
+ :key="String(w.id)"
|
|
|
+ :label="warehouseOptionLabel(w)"
|
|
|
+ :value="w.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="盘点日期" prop="checkDate">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="form.checkDate"
|
|
|
+ type="date"
|
|
|
+ value-format="yyyy-MM-dd"
|
|
|
+ placeholder="盘点业务日期"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="制单人">
|
|
|
+ <el-input v-model="form.creator" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="差异数量合计">
|
|
|
+ <el-input :value="totalVarianceQtyDisplay" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-form-item label="差异金额合计">
|
|
|
+ <el-input :value="formatMoney(form.totalVarianceAmount)" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="备注" prop="remarks">
|
|
|
+ <el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="备注" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ </trade-card>
|
|
|
+
|
|
|
+ <trade-card title="盘点明细" :show-btn="false">
|
|
|
+ <div class="detail-toolbar">
|
|
|
+ <el-button
|
|
|
+ v-if="!isView"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ @click="openInventoryPickDialog"
|
|
|
+ >选择库存
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="!isView"
|
|
|
+ size="small"
|
|
|
+ :disabled="!detailSelection.length"
|
|
|
+ @click="removeSelectedDetails"
|
|
|
+ >移除所选
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="inventory-check-table-scroll">
|
|
|
+ <el-table
|
|
|
+ ref="detailTable"
|
|
|
+ :data="detailList"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ max-height="440"
|
|
|
+ @selection-change="onDetailSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column v-if="!isView" type="selection" width="48" align="center" />
|
|
|
+ <el-table-column type="index" label="#" width="46" align="center" />
|
|
|
+ <el-table-column prop="materialCode" label="物料编号" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="materialName" label="物料名称" min-width="140" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="specification" label="规格" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="warehouse" label="仓库" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="storageArea" label="库区" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="supplierName" label="供应商" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="unit" label="单位" width="72" align="center" />
|
|
|
+ <el-table-column label="库存单价" width="100" align="right" show-overflow-tooltip>
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span>{{ formatMoney(row.averageCostPrice) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="账面数量" width="108" align="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span>{{ formatQty(row.bookQuantity) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实盘数量" width="128" align="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-if="!isView"
|
|
|
+ v-model="row.actualQuantity"
|
|
|
+ class="inv-check-qty-input"
|
|
|
+ size="mini"
|
|
|
+ :precision="2"
|
|
|
+ :step="0.01"
|
|
|
+ :min="0"
|
|
|
+ :max="999999999"
|
|
|
+ :controls="false"
|
|
|
+ placeholder="正数,两位小数"
|
|
|
+ @change="() => onActualQuantityChange(row)"
|
|
|
+ />
|
|
|
+ <span v-else>{{ formatQty(row.actualQuantity) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="差异数量" width="100" align="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span>{{ formatQty(row.varianceQuantity) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实盘金额" width="100" align="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span>{{ formatMoney(row.actualAmount) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="note" label="备注" min-width="100">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-input v-if="!isView" v-model="row.note" size="mini" maxlength="200" />
|
|
|
+ <span v-else>{{ row.note }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </trade-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 选择库存(与库存查询分页一致) -->
|
|
|
+ <el-dialog
|
|
|
+ title="选择库存"
|
|
|
+ :visible.sync="pickDialogVisible"
|
|
|
+ width="960px"
|
|
|
+ append-to-body
|
|
|
+ destroy-on-close
|
|
|
+ @closed="onPickDialogClosed"
|
|
|
+ >
|
|
|
+ <el-form :inline="true" size="small" class="pick-search-form" @submit.native.prevent>
|
|
|
+ <el-form-item label="物料编号">
|
|
|
+ <el-input v-model="pickQuery.materialCode" clearable placeholder="模糊" style="width: 140px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="物料名称">
|
|
|
+ <el-input v-model="pickQuery.materialName" clearable placeholder="模糊" style="width: 160px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="loadPickPage(1)">查询</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <el-table
|
|
|
+ ref="pickTable"
|
|
|
+ v-loading="pickLoading"
|
|
|
+ :data="pickTableData"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ max-height="380"
|
|
|
+ @selection-change="onPickSelectionChange"
|
|
|
+ >
|
|
|
+ <el-table-column type="selection" width="48" align="center" :reserve-selection="true" />
|
|
|
+ <el-table-column type="index" label="#" width="46" align="center" />
|
|
|
+ <el-table-column prop="materialCode" label="物料编号" min-width="110" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="materialName" label="物料名称" min-width="140" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="warehouse" label="仓库" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="storageArea" label="库区" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="supplierName" label="供应商" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="currentStock" label="当前库存" width="92" align="right" />
|
|
|
+ <el-table-column prop="averageCostPrice" label="库存均价" width="92" align="right" />
|
|
|
+ <el-table-column prop="unit" label="单位" width="72" align="center" />
|
|
|
+ </el-table>
|
|
|
+ <el-pagination
|
|
|
+ class="pick-pagination"
|
|
|
+ :current-page="pickPage.currentPage"
|
|
|
+ :page-size="pickPage.pageSize"
|
|
|
+ :total="pickPage.total"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @size-change="onPickSizeChange"
|
|
|
+ @current-change="onPickCurrentChange"
|
|
|
+ />
|
|
|
+ <span slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="pickDialogVisible = false">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmPickInventory">确 定</el-button>
|
|
|
+ </span>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ getInventoryCheckDetail,
|
|
|
+ saveInventoryCheck,
|
|
|
+ submitInventoryCheck,
|
|
|
+ revokeInventoryCheck
|
|
|
+} from "@/api/exportTrade/stockInventoryCheck";
|
|
|
+import { getStockInventoryPage } from "@/api/exportTrade/stockBill";
|
|
|
+import { customerList as fetchStorageTypePage } from "@/api/basicData/basicStorageType";
|
|
|
+import _ from "lodash";
|
|
|
+
|
|
|
+const STATUS_SAVED = 0;
|
|
|
+const STATUS_SUBMITTED = 1;
|
|
|
+
|
|
|
+function todayStr() {
|
|
|
+ const d = new Date();
|
|
|
+ const p = n => String(n).padStart(2, "0");
|
|
|
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
|
+}
|
|
|
+
|
|
|
+/** 数量最多两位小数 */
|
|
|
+function normalizeQty2(v) {
|
|
|
+ if (v == null || v === "") {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ const n = Number(v);
|
|
|
+ if (!Number.isFinite(n)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return Math.round(n * 100) / 100;
|
|
|
+}
|
|
|
+
|
|
|
+function emptyDetailRow() {
|
|
|
+ return {
|
|
|
+ inventoryId: null,
|
|
|
+ materialId: null,
|
|
|
+ materialCode: "",
|
|
|
+ materialName: "",
|
|
|
+ materialTypeId: null,
|
|
|
+ materialEnglish: "",
|
|
|
+ materialType: "",
|
|
|
+ productDescription: "",
|
|
|
+ specification: "",
|
|
|
+ warehouseId: null,
|
|
|
+ warehouse: "",
|
|
|
+ storageAreaId: null,
|
|
|
+ storageArea: "",
|
|
|
+ supplierId: null,
|
|
|
+ supplierName: "",
|
|
|
+ unit: "",
|
|
|
+ bookQuantity: 0,
|
|
|
+ actualQuantity: 0,
|
|
|
+ varianceQuantity: 0,
|
|
|
+ bookAmount: null,
|
|
|
+ actualAmount: null,
|
|
|
+ varianceAmount: null,
|
|
|
+ averageCostPrice: null,
|
|
|
+ note: "",
|
|
|
+ $rowKey: `n-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function recalcRowStatic(row) {
|
|
|
+ const book = normalizeQty2(row.bookQuantity);
|
|
|
+ const act = normalizeQty2(row.actualQuantity);
|
|
|
+ const price = Number(row.averageCostPrice);
|
|
|
+ const avg = Number.isFinite(price) ? price : 0;
|
|
|
+ row.varianceQuantity = Math.round((act - book) * 100) / 100;
|
|
|
+ row.bookAmount = Math.round(book * avg * 100) / 100;
|
|
|
+ row.actualAmount = Math.round(act * avg * 100) / 100;
|
|
|
+ row.varianceAmount =
|
|
|
+ Math.round((row.actualAmount - row.bookAmount) * 100) / 100;
|
|
|
+}
|
|
|
+
|
|
|
+function mapInventoryRowToDetail(inv) {
|
|
|
+ const invId = inv.inventoryId != null && inv.inventoryId !== "" ? inv.inventoryId : inv.id;
|
|
|
+ const book = normalizeQty2(inv.currentStock);
|
|
|
+ const price = Number(inv.averageCostPrice);
|
|
|
+ const avg = Number.isFinite(price) ? price : 0;
|
|
|
+ const actualInit = book > 0 ? book : 0.01;
|
|
|
+ const row = {
|
|
|
+ ...emptyDetailRow(),
|
|
|
+ inventoryId: invId,
|
|
|
+ materialId: inv.materialId != null ? inv.materialId : null,
|
|
|
+ materialCode: inv.materialCode || "",
|
|
|
+ materialName: inv.materialName || "",
|
|
|
+ materialType: inv.materialType || "",
|
|
|
+ materialEnglish: inv.materialEnglish || "",
|
|
|
+ productDescription: inv.productDescription || "",
|
|
|
+ specification: inv.specification || "",
|
|
|
+ warehouseId: inv.warehouseId != null ? inv.warehouseId : null,
|
|
|
+ warehouse: inv.warehouse || "",
|
|
|
+ storageAreaId: inv.storageAreaId != null ? inv.storageAreaId : null,
|
|
|
+ storageArea: inv.storageArea || "",
|
|
|
+ supplierId: inv.supplierId != null ? inv.supplierId : null,
|
|
|
+ supplierName: inv.supplierName || "",
|
|
|
+ unit: inv.unit || "",
|
|
|
+ bookQuantity: book,
|
|
|
+ actualQuantity: actualInit,
|
|
|
+ averageCostPrice: avg,
|
|
|
+ note: ""
|
|
|
+ };
|
|
|
+ recalcRowStatic(row);
|
|
|
+ return row;
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "exportTradeStockInventoryCheckDetail",
|
|
|
+ props: {
|
|
|
+ detailData: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ pageLoading: false,
|
|
|
+ saveLoading: false,
|
|
|
+ submitLoading: false,
|
|
|
+ revokeLoading: false,
|
|
|
+ form: {
|
|
|
+ id: null,
|
|
|
+ version: null,
|
|
|
+ formNumber: "",
|
|
|
+ warehouseId: null,
|
|
|
+ warehouse: "",
|
|
|
+ checkDate: todayStr(),
|
|
|
+ status: STATUS_SAVED,
|
|
|
+ totalVarianceQty: null,
|
|
|
+ totalVarianceAmount: null,
|
|
|
+ remarks: "",
|
|
|
+ createdDate: todayStr(),
|
|
|
+ creator: ""
|
|
|
+ },
|
|
|
+ detailList: [],
|
|
|
+ detailSelection: [],
|
|
|
+ /** loadDetail 回填仓库时跳过「切换仓库清空明细」 */
|
|
|
+ suppressWarehouseDetailClear: false,
|
|
|
+ warehouseOptions: [],
|
|
|
+ warehouseLoading: false,
|
|
|
+ warehouseListLoaded: false,
|
|
|
+ pendingWarehouseLoad: null,
|
|
|
+ pickDialogVisible: false,
|
|
|
+ pickLoading: false,
|
|
|
+ pickTableData: [],
|
|
|
+ pickSelection: [],
|
|
|
+ pickQuery: {
|
|
|
+ materialCode: "",
|
|
|
+ materialName: ""
|
|
|
+ },
|
|
|
+ pickPage: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 0
|
|
|
+ },
|
|
|
+ headerRules: {
|
|
|
+ checkDate: [{ required: true, message: "请选择盘点日期", trigger: "change" }]
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ isView() {
|
|
|
+ if (this.detailData && this.detailData.status === 1) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return Number(this.form.status) === STATUS_SUBMITTED;
|
|
|
+ },
|
|
|
+ showRevoke() {
|
|
|
+ if (this.form.id == null || this.form.id === "") {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return Number(this.form.status) === STATUS_SUBMITTED;
|
|
|
+ },
|
|
|
+ totalVarianceQtyDisplay() {
|
|
|
+ const v = this.form.totalVarianceQty;
|
|
|
+ if (v == null || v === "") {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ const n = Number(v);
|
|
|
+ if (!Number.isFinite(n)) {
|
|
|
+ return String(v);
|
|
|
+ }
|
|
|
+ return String(parseFloat(n.toFixed(2)));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ const u = this.$store.getters.userInfo || {};
|
|
|
+ this.form.creator = u.user_name || u.name || "";
|
|
|
+ this.loadWarehouseOptions();
|
|
|
+ if (this.detailData && this.detailData.id) {
|
|
|
+ this.loadDetail(this.detailData.id);
|
|
|
+ } else {
|
|
|
+ this.detailList = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ warehouseOptionLabel(w) {
|
|
|
+ if (!w) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ if (w.code) {
|
|
|
+ return `${w.cname}(${w.code})`;
|
|
|
+ }
|
|
|
+ return w.cname || "";
|
|
|
+ },
|
|
|
+ loadWarehouseOptions() {
|
|
|
+ if (this.warehouseListLoaded) {
|
|
|
+ return Promise.resolve();
|
|
|
+ }
|
|
|
+ if (this.pendingWarehouseLoad) {
|
|
|
+ return this.pendingWarehouseLoad;
|
|
|
+ }
|
|
|
+ this.warehouseLoading = true;
|
|
|
+ const pageSize = 100;
|
|
|
+ const loadPage = (current, acc) => {
|
|
|
+ return fetchStorageTypePage({
|
|
|
+ parentId: 0,
|
|
|
+ current,
|
|
|
+ size: pageSize
|
|
|
+ }).then(res => {
|
|
|
+ const data = res.data.data || {};
|
|
|
+ const records = data.records || [];
|
|
|
+ const total = Number(data.total) || 0;
|
|
|
+ const merged = acc.concat(records);
|
|
|
+ if (
|
|
|
+ records.length < pageSize ||
|
|
|
+ merged.length >= total ||
|
|
|
+ !records.length
|
|
|
+ ) {
|
|
|
+ return merged;
|
|
|
+ }
|
|
|
+ return loadPage(current + 1, merged);
|
|
|
+ });
|
|
|
+ };
|
|
|
+ this.pendingWarehouseLoad = loadPage(1, [])
|
|
|
+ .then(list => {
|
|
|
+ this.warehouseOptions = list;
|
|
|
+ this.warehouseListLoaded = true;
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("仓库列表加载失败");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.warehouseLoading = false;
|
|
|
+ this.pendingWarehouseLoad = null;
|
|
|
+ });
|
|
|
+ return this.pendingWarehouseLoad;
|
|
|
+ },
|
|
|
+ onWarehouseSelectVisible(visible) {
|
|
|
+ if (visible && !this.warehouseListLoaded && !this.warehouseLoading) {
|
|
|
+ this.loadWarehouseOptions();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onWarehouseChange(val) {
|
|
|
+ if (this.suppressWarehouseDetailClear) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (val == null || val === "") {
|
|
|
+ this.form.warehouse = "";
|
|
|
+ } else {
|
|
|
+ const w = this.warehouseOptions.find(x => String(x.id) === String(val));
|
|
|
+ this.form.warehouse = w ? w.cname || "" : "";
|
|
|
+ }
|
|
|
+ this.clearDetailListAfterWarehouseChange();
|
|
|
+ },
|
|
|
+ /** 切换仓库后与明细行仓库/库存来源不一致,清空明细 */
|
|
|
+ clearDetailListAfterWarehouseChange() {
|
|
|
+ if (!this.detailList.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.detailList = [];
|
|
|
+ this.detailSelection = [];
|
|
|
+ if (this.$refs.detailTable) {
|
|
|
+ this.$refs.detailTable.clearSelection();
|
|
|
+ }
|
|
|
+ this.recalcTotals();
|
|
|
+ this.$message.info("已切换仓库,盘点明细已清空");
|
|
|
+ },
|
|
|
+ formatMoney(v) {
|
|
|
+ if (v === null || v === undefined || v === "") {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ const n = Number(v);
|
|
|
+ if (Number.isNaN(n)) {
|
|
|
+ return String(v);
|
|
|
+ }
|
|
|
+ return n.toFixed(2);
|
|
|
+ },
|
|
|
+ /** 数量展示:最多两位小数,去掉多余末尾 0 */
|
|
|
+ formatQty(v) {
|
|
|
+ if (v === null || v === undefined || v === "") {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ const n = Number(v);
|
|
|
+ if (!Number.isFinite(n)) {
|
|
|
+ return String(v);
|
|
|
+ }
|
|
|
+ return String(parseFloat(n.toFixed(2)));
|
|
|
+ },
|
|
|
+ onActualQuantityChange(row) {
|
|
|
+ const v = row.actualQuantity;
|
|
|
+ if (v == null || v === "") {
|
|
|
+ recalcRowStatic(row);
|
|
|
+ this.recalcTotals();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const n = Number(v);
|
|
|
+ if (!Number.isFinite(n) || n <= 0) {
|
|
|
+ this.$message.warning("实盘数量须为大于 0 的数字,最多两位小数");
|
|
|
+ this.$set(row, "actualQuantity", null);
|
|
|
+ } else {
|
|
|
+ this.$set(row, "actualQuantity", Math.round(n * 100) / 100);
|
|
|
+ }
|
|
|
+ recalcRowStatic(row);
|
|
|
+ this.recalcTotals();
|
|
|
+ },
|
|
|
+ backToList() {
|
|
|
+ this.$emit("goBack");
|
|
|
+ },
|
|
|
+ flattenDetail(raw) {
|
|
|
+ const list =
|
|
|
+ raw.detailList ||
|
|
|
+ raw.stockInventoryCheckDetailList ||
|
|
|
+ raw.businessStockInventoryCheckDetailList ||
|
|
|
+ [];
|
|
|
+ return Array.isArray(list) ? list : [];
|
|
|
+ },
|
|
|
+ loadDetail(id) {
|
|
|
+ if (id == null || id === "") {
|
|
|
+ return Promise.resolve();
|
|
|
+ }
|
|
|
+ this.pageLoading = true;
|
|
|
+ return getInventoryCheckDetail(id)
|
|
|
+ .then(res => {
|
|
|
+ const body = res.data || {};
|
|
|
+ if (body.code !== 200) {
|
|
|
+ this.$message.error(body.msg || "加载失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.suppressWarehouseDetailClear = true;
|
|
|
+ try {
|
|
|
+ const raw = body.data || {};
|
|
|
+ const main = raw.businessStockInventoryCheck || raw;
|
|
|
+ if (main.id != null) {
|
|
|
+ this.$set(this.form, "id", main.id);
|
|
|
+ }
|
|
|
+ if (main.version !== undefined) {
|
|
|
+ this.$set(this.form, "version", main.version);
|
|
|
+ }
|
|
|
+ [
|
|
|
+ "formNumber",
|
|
|
+ "warehouseId",
|
|
|
+ "warehouse",
|
|
|
+ "checkDate",
|
|
|
+ "status",
|
|
|
+ "totalVarianceQty",
|
|
|
+ "totalVarianceAmount",
|
|
|
+ "remarks",
|
|
|
+ "createdDate",
|
|
|
+ "creator"
|
|
|
+ ].forEach(k => {
|
|
|
+ if (main[k] !== undefined) {
|
|
|
+ this.$set(this.form, k, main[k]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const list = this.flattenDetail(raw);
|
|
|
+ this.detailList = list.length
|
|
|
+ ? list.map((r, idx) => {
|
|
|
+ const book = normalizeQty2(r.bookQuantity);
|
|
|
+ let act =
|
|
|
+ r.actualQuantity != null && r.actualQuantity !== ""
|
|
|
+ ? normalizeQty2(r.actualQuantity)
|
|
|
+ : book;
|
|
|
+ if (act <= 0) {
|
|
|
+ act = book > 0 ? book : 0.01;
|
|
|
+ }
|
|
|
+ const price = Number(r.averageCostPrice);
|
|
|
+ const avg = Number.isFinite(price) ? price : 0;
|
|
|
+ return {
|
|
|
+ ...r,
|
|
|
+ bookQuantity: book,
|
|
|
+ actualQuantity: act,
|
|
|
+ averageCostPrice: avg,
|
|
|
+ $rowKey: r.id
|
|
|
+ ? `id-${r.id}`
|
|
|
+ : `n-${idx}-${Date.now()}`
|
|
|
+ };
|
|
|
+ })
|
|
|
+ : [];
|
|
|
+ this.detailList.forEach(r => {
|
|
|
+ recalcRowStatic(r);
|
|
|
+ });
|
|
|
+ this.recalcTotals();
|
|
|
+ } finally {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.suppressWarehouseDetailClear = false;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("详情请求失败");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.pageLoading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ recalcRow(row) {
|
|
|
+ recalcRowStatic(row);
|
|
|
+ this.recalcTotals();
|
|
|
+ },
|
|
|
+ recalcTotals() {
|
|
|
+ let q = 0;
|
|
|
+ let a = 0;
|
|
|
+ this.detailList.forEach(r => {
|
|
|
+ q += Number(r.varianceQuantity) || 0;
|
|
|
+ a += Number(r.varianceAmount) || 0;
|
|
|
+ });
|
|
|
+ this.form.totalVarianceQty = Math.round(q * 100) / 100;
|
|
|
+ this.form.totalVarianceAmount = Math.round(a * 100) / 100;
|
|
|
+ },
|
|
|
+ onDetailSelectionChange(rows) {
|
|
|
+ this.detailSelection = rows;
|
|
|
+ },
|
|
|
+ removeSelectedDetails() {
|
|
|
+ if (!this.detailSelection.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const keys = new Set(this.detailSelection.map(r => r.$rowKey));
|
|
|
+ this.detailList = this.detailList.filter(r => !keys.has(r.$rowKey));
|
|
|
+ this.detailSelection = [];
|
|
|
+ this.recalcTotals();
|
|
|
+ },
|
|
|
+ openInventoryPickDialog() {
|
|
|
+ this.pickDialogVisible = true;
|
|
|
+ this.pickQuery = { materialCode: "", materialName: "" };
|
|
|
+ this.pickPage.currentPage = 1;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.pickTable) {
|
|
|
+ this.$refs.pickTable.clearSelection();
|
|
|
+ }
|
|
|
+ this.loadPickPage(1);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ onPickDialogClosed() {
|
|
|
+ this.pickTableData = [];
|
|
|
+ this.pickSelection = [];
|
|
|
+ },
|
|
|
+ onPickSelectionChange(rows) {
|
|
|
+ this.pickSelection = rows;
|
|
|
+ },
|
|
|
+ onPickSizeChange(val) {
|
|
|
+ this.pickPage.pageSize = val;
|
|
|
+ this.loadPickPage(1);
|
|
|
+ },
|
|
|
+ onPickCurrentChange(val) {
|
|
|
+ this.pickPage.currentPage = val;
|
|
|
+ this.loadPickPage(val);
|
|
|
+ },
|
|
|
+ loadPickPage(page) {
|
|
|
+ this.pickLoading = true;
|
|
|
+ const params = {
|
|
|
+ materialCode: this.pickQuery.materialCode || undefined,
|
|
|
+ materialName: this.pickQuery.materialName || undefined
|
|
|
+ };
|
|
|
+ if (this.form.warehouseId != null && this.form.warehouseId !== "") {
|
|
|
+ const w = this.warehouseOptions.find(
|
|
|
+ x => String(x.id) === String(this.form.warehouseId)
|
|
|
+ );
|
|
|
+ if (w) {
|
|
|
+ params.warehouseId = w.id;
|
|
|
+ params.warehouse = w.cname || "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ getStockInventoryPage(page, this.pickPage.pageSize, params)
|
|
|
+ .then(res => {
|
|
|
+ const body = res.data || {};
|
|
|
+ if (body.code !== 200) {
|
|
|
+ this.$message.error(body.msg || "库存列表加载失败");
|
|
|
+ this.pickTableData = [];
|
|
|
+ this.pickPage.total = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const data = body.data || {};
|
|
|
+ this.pickTableData = data.records || [];
|
|
|
+ this.pickPage.total = data.total || 0;
|
|
|
+ this.pickPage.currentPage = page;
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("库存列表请求失败");
|
|
|
+ this.pickTableData = [];
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.pickLoading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ confirmPickInventory() {
|
|
|
+ if (!this.pickSelection.length) {
|
|
|
+ this.$message.warning("请选择库存行");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const existing = new Set(
|
|
|
+ this.detailList.map(r =>
|
|
|
+ `${r.inventoryId || ""}_${r.storageAreaId || ""}_${r.materialId || ""}`
|
|
|
+ )
|
|
|
+ );
|
|
|
+ let added = 0;
|
|
|
+ this.pickSelection.forEach(inv => {
|
|
|
+ const row = mapInventoryRowToDetail(inv);
|
|
|
+ const key = `${row.inventoryId || ""}_${row.storageAreaId || ""}_${row.materialId || ""}`;
|
|
|
+ if (existing.has(key)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ existing.add(key);
|
|
|
+ this.detailList.push(row);
|
|
|
+ added += 1;
|
|
|
+ });
|
|
|
+ if (added) {
|
|
|
+ this.recalcTotals();
|
|
|
+ this.$message.success(`已添加 ${added} 条`);
|
|
|
+ } else {
|
|
|
+ this.$message.info("所选行已在明细中");
|
|
|
+ }
|
|
|
+ this.pickDialogVisible = false;
|
|
|
+ },
|
|
|
+ /** 已有单据 id 时保存/提交必须带 version(首次无 id 可不传) */
|
|
|
+ validateVersionForSave() {
|
|
|
+ if (this.form.id == null || this.form.id === "") {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ this.form.version === undefined ||
|
|
|
+ this.form.version === null ||
|
|
|
+ this.form.version === ""
|
|
|
+ ) {
|
|
|
+ this.$message.warning("单据缺少版本号,请刷新页面后重试");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ buildPayload(status) {
|
|
|
+ this.recalcTotals();
|
|
|
+ const detailList = this.detailList.map((r, i) => {
|
|
|
+ const o = _.omit(r, ["$rowKey"]);
|
|
|
+ o.sort = i + 1;
|
|
|
+ return o;
|
|
|
+ });
|
|
|
+ const payload = {
|
|
|
+ ...this.form,
|
|
|
+ status,
|
|
|
+ detailList
|
|
|
+ };
|
|
|
+ const hasId = payload.id != null && payload.id !== "";
|
|
|
+ if (!hasId) {
|
|
|
+ delete payload.version;
|
|
|
+ } else {
|
|
|
+ const vn = Number(payload.version);
|
|
|
+ payload.version = Number.isFinite(vn) ? vn : payload.version;
|
|
|
+ }
|
|
|
+ return payload;
|
|
|
+ },
|
|
|
+ /** 撤销:与保存/提交一致,有 id 时携带并规范化 version */
|
|
|
+ buildRevokePayload() {
|
|
|
+ const payload = { ...this.form };
|
|
|
+ const hasId = payload.id != null && payload.id !== "";
|
|
|
+ if (!hasId) {
|
|
|
+ delete payload.version;
|
|
|
+ return payload;
|
|
|
+ }
|
|
|
+ const vn = Number(payload.version);
|
|
|
+ payload.version = Number.isFinite(vn) ? vn : payload.version;
|
|
|
+ return payload;
|
|
|
+ },
|
|
|
+ applyVersionFromSaveResponse(body) {
|
|
|
+ const d = body.data;
|
|
|
+ if (d && typeof d === "object" && d.version !== undefined && d.version !== null) {
|
|
|
+ this.$set(this.form, "version", d.version);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ saveDraft() {
|
|
|
+ this.$refs.headerForm.validate(valid => {
|
|
|
+ if (!valid) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.validateVersionForSave()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.detailList.length) {
|
|
|
+ this.$message.warning("请至少添加一条盘点明细");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.saveLoading = true;
|
|
|
+ saveInventoryCheck(this.buildPayload(STATUS_SAVED))
|
|
|
+ .then(res => {
|
|
|
+ const body = res.data || {};
|
|
|
+ if (body.code !== 200) {
|
|
|
+ this.$message.error(body.msg || "保存失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.applyVersionFromSaveResponse(body);
|
|
|
+ this.$message.success("保存成功");
|
|
|
+ const d = body.data;
|
|
|
+ const reloadId =
|
|
|
+ d && typeof d === "object" && d.id != null
|
|
|
+ ? d.id
|
|
|
+ : typeof d === "number" || typeof d === "string"
|
|
|
+ ? d
|
|
|
+ : this.form.id;
|
|
|
+ if (reloadId != null && reloadId !== "") {
|
|
|
+ this.loadDetail(reloadId);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("保存请求失败");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.saveLoading = false;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ submitBill() {
|
|
|
+ this.$refs.headerForm.validate(valid => {
|
|
|
+ if (!valid) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.validateVersionForSave()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.detailList.length) {
|
|
|
+ this.$message.warning("请至少添加一条盘点明细");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.submitLoading = true;
|
|
|
+ submitInventoryCheck(this.buildPayload(STATUS_SUBMITTED))
|
|
|
+ .then(res => {
|
|
|
+ const body = res.data || {};
|
|
|
+ if (body.code !== 200) {
|
|
|
+ this.$message.error(body.msg || "提交失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.applyVersionFromSaveResponse(body);
|
|
|
+ this.$message.success("提交成功");
|
|
|
+ const d = body.data;
|
|
|
+ const reloadId =
|
|
|
+ d && typeof d === "object" && d.id != null
|
|
|
+ ? d.id
|
|
|
+ : typeof d === "number" || typeof d === "string"
|
|
|
+ ? d
|
|
|
+ : this.form.id;
|
|
|
+ if (reloadId != null && reloadId !== "") {
|
|
|
+ this.loadDetail(reloadId);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("提交请求失败");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.submitLoading = false;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleRevoke() {
|
|
|
+ if (!this.showRevoke) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const id = this.form.id;
|
|
|
+ if (id == null || id === "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.validateVersionForSave()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$confirm("确定撤销该盘点单?撤销后单据将回到可编辑状态。", "提示", {
|
|
|
+ type: "warning"
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.revokeLoading = true;
|
|
|
+ revokeInventoryCheck(this.buildRevokePayload())
|
|
|
+ .then(res => {
|
|
|
+ const body = res.data || {};
|
|
|
+ if (body.code !== 200) {
|
|
|
+ this.$message.error(body.msg || "撤销失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$message.success(body.msg || "撤销成功");
|
|
|
+ this.loadDetail(id);
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.$message.error("撤销请求失败");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ this.revokeLoading = false;
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+/* 与 variables.scss 中 fixed 顶栏配合:顶栏 top:40px + height:45px,预留足够间距避免遮挡「基本信息」等标题 */
|
|
|
+.customer-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 8px 0 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.inventory-check-print-wrap {
|
|
|
+ margin-top: 64px;
|
|
|
+ margin-bottom: 35px;
|
|
|
+ padding: 8px 12px 24px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-toolbar {
|
|
|
+ margin: 8px 0 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.inventory-check-table-scroll {
|
|
|
+ width: 100%;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.inv-check-qty-input {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.pick-search-form {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.pick-pagination {
|
|
|
+ margin-top: 12px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+</style>
|