7159 lines
302 KiB
Vue
7159 lines
302 KiB
Vue
<template>
|
||
<section class="page-grid">
|
||
<section class="panel inventory-unified-panel">
|
||
<div class="warehouse-switchboard" role="tablist" aria-label="嘉恒仓库库存类型">
|
||
<button
|
||
v-for="tab in inventoryTabs"
|
||
:key="tab.key"
|
||
class="warehouse-tab-card"
|
||
:class="[tab.iconClass, { 'warehouse-tab-card-active': activeInventoryTab === tab.key }]"
|
||
type="button"
|
||
role="tab"
|
||
:aria-selected="activeInventoryTab === tab.key"
|
||
@click="selectInventoryTab(tab.key)"
|
||
>
|
||
<span class="warehouse-tab-card-icon" aria-hidden="true">
|
||
<AppIcon :name="tab.icon" />
|
||
</span>
|
||
<span class="warehouse-tab-card-copy">
|
||
<strong>{{ tab.label }}</strong>
|
||
<span>{{ tab.count }} 条</span>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="inventory-section-divider"></div>
|
||
|
||
<div class="warehouse-operation-zone" aria-label="出入库业务操作">
|
||
<section class="warehouse-operation-group warehouse-operation-group-in" aria-label="入库">
|
||
<header>
|
||
<span class="warehouse-operation-kicker">入库区</span>
|
||
<strong>入库</strong>
|
||
</header>
|
||
<div class="warehouse-operation-actions">
|
||
<button
|
||
v-for="operation in activeInboundOperations"
|
||
:key="operation.key"
|
||
class="warehouse-operation-button warehouse-operation-button-in"
|
||
type="button"
|
||
:title="operation.title"
|
||
@click="openOperationDrawer(operation)"
|
||
>
|
||
<AppIcon name="plus" />
|
||
<strong>{{ operation.label }}</strong>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="warehouse-operation-group warehouse-operation-group-out" aria-label="出库">
|
||
<header>
|
||
<span class="warehouse-operation-kicker">出库区</span>
|
||
<strong>出库</strong>
|
||
</header>
|
||
<div class="warehouse-operation-actions">
|
||
<button
|
||
v-for="operation in activeOutboundOperations"
|
||
:key="operation.key"
|
||
class="warehouse-operation-button warehouse-operation-button-out"
|
||
type="button"
|
||
:title="operation.title"
|
||
@click="openOperationDrawer(operation)"
|
||
>
|
||
<AppIcon name="minus" />
|
||
<strong>{{ operation.label }}</strong>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="warehouse-operation-group warehouse-operation-group-tools" aria-label="工具">
|
||
<header>
|
||
<span class="warehouse-operation-kicker">辅助工具</span>
|
||
<strong>工具</strong>
|
||
</header>
|
||
<div class="warehouse-tool-actions">
|
||
<button
|
||
class="warehouse-tool-button no-auto-icon"
|
||
type="button"
|
||
title="查看当前仓库出入库流水"
|
||
@click="openWarehouseLedgerDrawer"
|
||
>
|
||
<AppIcon name="ledger" />
|
||
<strong>流水</strong>
|
||
</button>
|
||
<button
|
||
class="warehouse-tool-button no-auto-icon"
|
||
type="button"
|
||
title="对当前仓库执行盘库"
|
||
@click="openStocktakeDialog"
|
||
>
|
||
<AppIcon name="stocktake" />
|
||
<strong>盘库</strong>
|
||
</button>
|
||
<button
|
||
class="warehouse-tool-button warehouse-tool-button-primary no-auto-icon"
|
||
type="button"
|
||
title="按在生产的生产台账统一办理成品、余料、废料入库"
|
||
@click="openProductionWorkOrderInboundDrawer"
|
||
>
|
||
<AppIcon name="workOrder" />
|
||
<strong>生产台账入库</strong>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div v-if="!inventoryOverlayOpen && (feedbackMessage || errorMessage || lastWarehouseArchiveResult)" class="inventory-page-feedback">
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
<DocumentArchiveActions
|
||
v-if="lastWarehouseArchiveResult"
|
||
:status="lastWarehouseArchiveResult.archive_status || '未生成'"
|
||
@preview="previewWarehouseLedgerArchive(lastWarehouseArchiveResult)"
|
||
@download="downloadWarehouseLedgerArchive(lastWarehouseArchiveResult)"
|
||
@regenerate="regenerateWarehouseLedgerArchive(lastWarehouseArchiveResult)"
|
||
/>
|
||
</div>
|
||
|
||
<div class="inventory-tab-panel" role="tabpanel">
|
||
<TableControls
|
||
:key="`inventory-controls-${activeInventoryTab}`"
|
||
v-model:search="balanceControls.search.value"
|
||
v-model:sort-key="balanceControls.sortKey.value"
|
||
v-model:sort-direction="balanceControls.sortDirection.value"
|
||
:sort-options="balanceControls.sortOptions"
|
||
:placeholder="activeSearchPlaceholder"
|
||
/>
|
||
|
||
<div class="table-wrap">
|
||
<table :key="`inventory-main-table-${activeInventoryTab}`" class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ activeItemColumnLabel }}</th>
|
||
<th v-if="!activeWeightOnly">现存数量</th>
|
||
<th v-if="!activeWeightOnly">可用数量</th>
|
||
<th v-if="activeQuantityOnly">单位</th>
|
||
<th v-if="!activeQuantityOnly">现存重量(kg)</th>
|
||
<th v-if="!activeQuantityOnly">可用重量(kg)</th>
|
||
<th>仓库</th>
|
||
<th>库位</th>
|
||
<th>均价</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="balance in paginatedBalances" :key="balance.stock_balance_id">
|
||
<td>
|
||
<button class="link-button" type="button" :title="formatItemTypeLabel(balance.item_type)" @click="openStockDetail(balance)">
|
||
{{ formatInventoryBalanceLabel(balance) }}
|
||
</button>
|
||
</td>
|
||
<td v-if="!activeWeightOnly">{{ activeInventoryTab === "semi" ? formatSemiMeasureValue(balance, balance.qty_on_hand, "PIECE") : formatQty(balance.qty_on_hand) }}</td>
|
||
<td v-if="!activeWeightOnly">{{ activeInventoryTab === "semi" ? formatSemiMeasureValue(balance, balance.qty_available, "PIECE") : formatQty(balance.qty_available) }}</td>
|
||
<td v-if="activeQuantityOnly">{{ formatInventoryUnitLabel(balance) }}</td>
|
||
<td v-if="!activeQuantityOnly">{{ activeInventoryTab === "semi" ? formatSemiMeasureValue(balance, balance.weight_on_hand_kg, "WEIGHT") : formatWeight(balance.weight_on_hand_kg) }}</td>
|
||
<td v-if="!activeQuantityOnly">{{ activeInventoryTab === "semi" ? formatSemiMeasureValue(balance, balance.weight_available_kg, "WEIGHT") : formatWeight(balance.weight_available_kg) }}</td>
|
||
<td>{{ balance.warehouse_name }}</td>
|
||
<td>{{ balance.location_name || "-" }}</td>
|
||
<td>¥{{ formatAmount(balance.avg_unit_cost) }}</td>
|
||
</tr>
|
||
<tr v-if="!filteredBalances.length">
|
||
<td :colspan="activeWeightOnly ? 6 : activeQuantityOnly ? 7 : 8" class="empty-row">暂无{{ activeInventoryLabel }}库存</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<PaginationBar v-model:page="balancePage" v-model:page-size="balancePageSize" :total="filteredBalances.length" />
|
||
</div>
|
||
</section>
|
||
|
||
<Teleport to="body">
|
||
<div v-if="detailDrawerOpen" class="workflow-drawer-mask dual-drawer-mask inventory-ledger-detail-mask" @click.self="closeStockDetail">
|
||
<div class="dual-drawer-shell">
|
||
<aside class="workflow-drawer compact-side-drawer">
|
||
<header class="workflow-drawer-head">
|
||
<div>
|
||
<p class="eyebrow">{{ selectedDetailEntityLabel }}总览</p>
|
||
<h3>{{ selectedBalanceLabel }}</h3>
|
||
</div>
|
||
<button class="ghost-button" type="button" @click="closeStockDetail">关闭</button>
|
||
</header>
|
||
|
||
<div class="inventory-lot-summary-grid">
|
||
<article>
|
||
<span>{{ selectedPrimaryAvailableLabel }}</span>
|
||
<strong>{{ selectedPrimaryAvailableValue }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>{{ selectedPrimaryOnHandLabel }}</span>
|
||
<strong>{{ selectedPrimaryOnHandValue }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>{{ selectedAuxiliaryMetricLabel }}</span>
|
||
<strong>{{ selectedAuxiliaryMetricValue }}</strong>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="inventory-source-lots">
|
||
<span>来自库存批次号</span>
|
||
<div>
|
||
<b v-for="lotNo in selectedSourceLotNos" :key="lotNo">{{ lotNo }}</b>
|
||
<em v-if="!selectedSourceLotNos.length">{{ selectedSourceLotEmptyText }}</em>
|
||
</div>
|
||
</div>
|
||
|
||
<DrawerDataTable
|
||
:rows="selectedOverviewLots"
|
||
:columns="overviewLotDrawerColumns"
|
||
row-key="lot_id"
|
||
:empty-text="`暂无该${selectedDetailEntityLabel}库存批次`"
|
||
>
|
||
<template #cell-lot_no="{ row }">
|
||
<span class="drawer-cell-code" :title="formatSelectedDetailLotNo(row)">{{ formatSelectedDetailLotNo(row) }}</span>
|
||
</template>
|
||
<template #cell-status="{ row }">
|
||
<span class="status-pill" :class="lotStatusClass(row)">{{ formatStatusLabel(row.status) }}</span>
|
||
</template>
|
||
</DrawerDataTable>
|
||
|
||
<section class="drawer-subsection">
|
||
<div class="subsection-head">
|
||
<strong>库存批次明细</strong>
|
||
<span class="panel-tag">{{ filteredLots.length }} 条</span>
|
||
</div>
|
||
<TableControls
|
||
v-model:search="lotControls.search.value"
|
||
v-model:sort-key="lotControls.sortKey.value"
|
||
v-model:sort-direction="lotControls.sortDirection.value"
|
||
:sort-options="lotControls.sortOptions"
|
||
placeholder="搜索库存批次号、采购单号、入库单号、仓库、状态"
|
||
/>
|
||
|
||
<DrawerDataTable
|
||
v-model:expanded-rows="expandedLotRows"
|
||
:rows="paginatedLots"
|
||
:columns="lotDrawerColumns"
|
||
row-key="lot_id"
|
||
:empty-text="`暂无该${selectedDetailEntityLabel}库存批次`"
|
||
>
|
||
<template #cell-lot_no="{ row }">
|
||
<span class="drawer-cell-code" :title="formatSelectedDetailLotNo(row)">{{ formatSelectedDetailLotNo(row) }}</span>
|
||
</template>
|
||
<template #cell-status="{ row }">
|
||
<span class="status-pill" :class="lotStatusClass(row)">{{ formatStatusLabel(row.status) }}</span>
|
||
</template>
|
||
<template #detail="{ row }">
|
||
<div class="drawer-row-detail-grid">
|
||
<article><span>仓库</span><strong>{{ row.warehouse_name || "-" }}</strong></article>
|
||
<article><span>采购单号</span><strong>{{ row.source_purchase_order_no || "-" }}</strong></article>
|
||
<article><span>入库单号</span><strong>{{ row.source_receipt_no || "-" }}</strong></article>
|
||
<article><span>来源</span><strong>{{ formatSourceDocTypeLabel(row.source_doc_type) }}</strong></article>
|
||
<article><span>来源库存批次号</span><strong>{{ formatLotSourceLots(row) }}</strong></article>
|
||
<article><span>运单号</span><strong>{{ row.logistics_waybill_no || "-" }}</strong></article>
|
||
<article><span>运费</span><strong>{{ formatFreight(row.logistics_freight_amount) }}</strong></article>
|
||
<article>
|
||
<span>辅助照片</span>
|
||
<strong>
|
||
<button v-if="row.logistics_photo_url" class="link-button" type="button" @click="openLogisticsPhoto(row.logistics_photo_url)">查看</button>
|
||
<span v-else>-</span>
|
||
</strong>
|
||
</article>
|
||
<article><span>质检放行人</span><strong>{{ row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }}</strong></article>
|
||
</div>
|
||
</template>
|
||
</DrawerDataTable>
|
||
<PaginationBar v-model:page="lotPage" v-model:page-size="lotPageSize" :total="filteredLots.length" />
|
||
</section>
|
||
</aside>
|
||
|
||
<aside class="workflow-drawer compact-side-drawer">
|
||
<header class="workflow-drawer-head">
|
||
<div>
|
||
<p class="eyebrow">库存流水</p>
|
||
<h3>{{ selectedBalanceLabel }}</h3>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="drawer-subsection">
|
||
<div class="subsection-head">
|
||
<strong>库存流水</strong>
|
||
<span class="panel-tag">{{ filteredTransactions.length }} 条</span>
|
||
</div>
|
||
<TableControls
|
||
v-model:search="txnControls.search.value"
|
||
v-model:sort-key="txnControls.sortKey.value"
|
||
v-model:sort-direction="txnControls.sortDirection.value"
|
||
:sort-options="txnControls.sortOptions"
|
||
placeholder="搜索流水类型、库存批次号、来源、备注"
|
||
/>
|
||
<DrawerDataTable
|
||
v-model:expanded-rows="expandedTxnRows"
|
||
:rows="paginatedTransactions"
|
||
:columns="txnDrawerColumns"
|
||
row-key="inventory_txn_id"
|
||
:empty-text="`暂无该${selectedDetailEntityLabel}库存流水`"
|
||
>
|
||
<template #cell-lot_no="{ row }">
|
||
<span class="drawer-cell-code" :title="formatSelectedDetailLotNo(row)">{{ formatSelectedDetailLotNo(row) }}</span>
|
||
</template>
|
||
<template #cell-archive_actions="{ row }">
|
||
<DocumentArchiveActions
|
||
:status="row.archive_status || '未生成'"
|
||
@preview="previewWarehouseLedgerArchive(row)"
|
||
@download="downloadWarehouseLedgerArchive(row)"
|
||
@regenerate="regenerateWarehouseLedgerArchive(row)"
|
||
/>
|
||
</template>
|
||
<template #detail="{ row }">
|
||
<div class="drawer-row-detail-grid">
|
||
<article><span>来源</span><strong>{{ formatSourceDocTypeLabel(row.source_doc_type) }} #{{ row.source_doc_id || "-" }}</strong></article>
|
||
<article><span>备注</span><strong>{{ row.remark || "-" }}</strong></article>
|
||
</div>
|
||
</template>
|
||
</DrawerDataTable>
|
||
<PaginationBar v-model:page="txnPage" v-model:page-size="txnPageSize" :total="filteredTransactions.length" />
|
||
</section>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<FormDrawer
|
||
:open="genericDrawerOpen"
|
||
:eyebrow="activeOperation?.direction === 'out' ? '出库业务' : '入库业务'"
|
||
:title="activeOperation?.label || '出入库登记'"
|
||
:tag="activeInventoryLabel"
|
||
wide
|
||
@close="closeGenericDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
v-if="isCustomerSuppliedInbound"
|
||
class="warehouse-document-operation-form"
|
||
title="客料入库单"
|
||
:document-no="customerSuppliedLotNoPreview || '提交后生成'"
|
||
:business-date="new Date()"
|
||
@submit="submitGenericOperation"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="客料入库信息">
|
||
<div class="warehouse-document-meta-strip" aria-label="单据信息">
|
||
<span>入库业务</span>
|
||
<strong>{{ activeInventoryLabel }}</strong>
|
||
<span>成本按客供材料处理</span>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-grid" aria-label="客料入库填写项">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>{{ genericItemFieldLabel }}</span>
|
||
<select v-model="genericForm.item_option_key" required @change="handleGenericItemChange">
|
||
<option disabled value="">{{ genericItemPlaceholder }}</option>
|
||
<option v-for="item in genericItemOptions" :key="item.option_key || item.item_id" :value="item.option_key || String(item.item_id)">
|
||
{{ formatGenericItemOption(item) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>仓库</span>
|
||
<select v-model.number="genericForm.warehouse_id" required @change="handleGenericWarehouseChange">
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>库位</span>
|
||
<select v-model.number="genericForm.location_id">
|
||
<option value="">默认库位</option>
|
||
<option v-for="location in genericLocations" :key="location.id" :value="location.id">
|
||
{{ location.location_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>入库重量(kg)</span>
|
||
<input
|
||
v-model.number="genericForm.weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
required
|
||
/>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>{{ genericUnitCostLabel }}</span>
|
||
<input v-model.number="genericForm.unit_cost" type="number" min="0" step="0.0001" readonly />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>来源/提供方</span>
|
||
<input v-model.trim="genericForm.provider_name" type="text" placeholder="客户、项目或来源说明" />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>运单号</span>
|
||
<input v-model.trim="genericForm.waybill_no" type="text" placeholder="可填运单号,或上传辅助照片" />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>运费</span>
|
||
<input v-model.number="genericForm.freight_amount" type="number" min="0" step="0.01" required />
|
||
</label>
|
||
|
||
<div class="warehouse-paper-field warehouse-paper-upload-field">
|
||
<span>辅助照片</span>
|
||
<label
|
||
class="warehouse-paper-upload"
|
||
:class="{ 'is-uploaded': genericForm.order_photo_url, 'is-busy': logisticsPhotoUploading }"
|
||
>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
:disabled="logisticsPhotoUploading"
|
||
@change="handleLogisticsPhotoUpload(genericForm, $event)"
|
||
/>
|
||
<b aria-hidden="true">{{ genericForm.order_photo_url ? "✓" : "↑" }}</b>
|
||
<strong>{{ logisticsPhotoUploading ? "上传中..." : genericForm.order_photo_url ? "已上传辅助照片" : "上传辅助照片" }}</strong>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>库存批次号</span>
|
||
<p class="warehouse-paper-readonly-value">
|
||
{{ customerSuppliedLotNoPreview ? `提交后自动生成:${customerSuppliedLotNoPreview}` : "选择物料后自动生成库存批次号" }}
|
||
</p>
|
||
</div>
|
||
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="genericForm.remark" rows="4"></textarea>
|
||
</label>
|
||
</div>
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation || logisticsPhotoUploading">
|
||
{{ submittingOperation ? "处理中..." : "确认登记客料入库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
|
||
<WarehouseDocumentFormShell
|
||
v-else
|
||
class="warehouse-document-operation-form"
|
||
:title="genericOperationDocumentTitle"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
@submit="submitGenericOperation"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="出入库业务信息">
|
||
<div v-if="isProductionWorkOrderControlledInbound" class="warehouse-paper-control-grid warehouse-paper-control-grid-compact">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>{{ isReworkScrapInbound ? "返工工单" : "生产台账" }}</span>
|
||
<select v-model.number="genericForm.work_order_id" required @change="applyGenericWorkOrder">
|
||
<option disabled value="">{{ isReworkScrapInbound ? "请选择返工工单" : "请选择生产台账" }}</option>
|
||
<option v-for="workOrder in genericWorkOrderOptions" :key="workOrder.work_order_id" :value="workOrder.work_order_id">
|
||
{{ formatWorkOrderLedgerOption(workOrder, activeOperation?.bizType) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<template v-if="selectedGenericWorkOrder">
|
||
<article class="warehouse-paper-summary-card warehouse-paper-summary-card-wide">
|
||
<span>产品</span>
|
||
<strong>{{ selectedGenericWorkOrder.product_code }} · {{ selectedGenericWorkOrder.product_name }}</strong>
|
||
</article>
|
||
<article class="warehouse-paper-summary-card">
|
||
<span>材料库存批次号</span>
|
||
<strong>{{ selectedGenericWorkOrder.source_lot_summary || selectedGenericWorkOrder.material_lot_no || "-" }}</strong>
|
||
</article>
|
||
<article v-if="isProductionScrapInbound" class="warehouse-paper-summary-card">
|
||
<span>库外未闭环重量</span>
|
||
<strong>{{ formatWeight(selectedGenericWorkOrder.outside_weight_kg || selectedGenericWorkOrder.limits?.max_scrap_inbound_weight_kg) }}</strong>
|
||
</article>
|
||
<article v-if="isProductionSurplusInbound" class="warehouse-paper-summary-card">
|
||
<span>库外未闭环重量</span>
|
||
<strong>{{ formatWeight(selectedGenericWorkOrder.outside_weight_kg || selectedGenericWorkOrder.limits?.max_surplus_inbound_weight_kg) }}</strong>
|
||
</article>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>{{ genericItemFieldLabel }}</span>
|
||
<select v-model="genericForm.item_option_key" required :disabled="(isProductionScrapInbound || isReworkScrapInbound) && !!selectedGenericWorkOrder" @change="handleGenericItemChange">
|
||
<option disabled value="">{{ genericItemPlaceholder }}</option>
|
||
<option v-for="item in genericItemOptions" :key="item.option_key || item.item_id" :value="item.option_key || String(item.item_id)">
|
||
{{ formatGenericItemOption(item) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>仓库</span>
|
||
<select v-model.number="genericForm.warehouse_id" required @change="handleGenericWarehouseChange">
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>库位</span>
|
||
<select v-model.number="genericForm.location_id">
|
||
<option value="">默认库位</option>
|
||
<option v-for="location in genericLocations" :key="location.id" :value="location.id">
|
||
{{ location.location_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label v-if="isSemiSourceLotTraceInbound" class="warehouse-paper-field">
|
||
<span>入库类型</span>
|
||
<select v-model="genericForm.measure_basis" :disabled="isSemiMeasureBasisLocked" required @change="handleGenericMeasureBasisChange">
|
||
<option disabled value="">请选择入库类型</option>
|
||
<option value="PIECE">按件入库</option>
|
||
<option value="WEIGHT">按重入库</option>
|
||
</select>
|
||
</label>
|
||
<label v-if="showGenericWeightInput" class="warehouse-paper-field">
|
||
<span>{{ activeOperation?.direction === "out" ? "出库重量(kg)" : "入库重量(kg)" }}</span>
|
||
<input
|
||
v-model.number="genericForm.weight_kg"
|
||
type="number"
|
||
:min="isSemiQuantityWeightFlexibleOperation ? 0 : 0.001"
|
||
:max="genericWeightInputMax || undefined"
|
||
step="0.001"
|
||
:required="!isSemiQuantityWeightFlexibleOperation"
|
||
/>
|
||
</label>
|
||
<label v-if="showGenericQtyInput" class="warehouse-paper-field">
|
||
<span>{{ activeOperation?.direction === "out" ? "出库数量" : "入库数量" }}</span>
|
||
<input v-model.number="genericForm.qty" type="number" min="0" step="0.001" />
|
||
</label>
|
||
<label v-if="activeOperation?.direction !== 'out' && !isSourceLotReturnInbound && !isSemiSourceLotTraceInbound" class="warehouse-paper-field">
|
||
<span>{{ genericUnitCostLabel }}</span>
|
||
<input v-model.number="genericForm.unit_cost" type="number" min="0" step="0.0001" :readonly="activeOperation?.bizType === 'CUSTOMER_SUPPLIED'" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="activeOperation?.direction !== 'out' && !isCustomerSuppliedInbound && !isSourceLotReturnInbound && !isSemiSourceLotTraceInbound && !isFinishedOutsourcingInbound && !isProductionScrapInbound && !isReworkScrapInbound && !isOutsourcingScrapInbound" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>批次号</span>
|
||
<input v-model.trim="genericForm.lot_no" type="text" placeholder="不填则自动生成" />
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>来源/提供方</span>
|
||
<input v-model.trim="genericForm.provider_name" type="text" placeholder="客户、项目、委外厂或来源说明" />
|
||
</label>
|
||
</div>
|
||
<div v-else-if="isCustomerSuppliedInbound" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>来源/提供方</span>
|
||
<input v-model.trim="genericForm.provider_name" type="text" placeholder="客户、项目或来源说明" />
|
||
</label>
|
||
</div>
|
||
<div v-else-if="['OUTSOURCING_IN', 'OUTSOURCING_SURPLUS'].includes(activeOperation?.bizType)" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>来源/提供方</span>
|
||
<input v-model.trim="genericForm.provider_name" type="text" placeholder="委外厂或来源说明" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="activeOperation?.bizType === 'SCRAP_SALE_OUT'" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>购买方</span>
|
||
<input v-model.trim="genericForm.buyer_name" type="text" placeholder="填写废料购买方" />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>售卖单价(元/kg)</span>
|
||
<input v-model.number="genericForm.sale_unit_price" type="number" min="0.0001" step="0.0001" required />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>预计售卖金额</span>
|
||
<input :value="`¥${formatAmount(estimatedScrapSaleAmount)}`" type="text" disabled />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="genericRequiresLogistics" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>运单号</span>
|
||
<input v-model.trim="genericForm.waybill_no" type="text" placeholder="可填运单号,或上传辅助照片" />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>运费</span>
|
||
<input v-model.number="genericForm.freight_amount" type="number" min="0" step="0.01" required />
|
||
</label>
|
||
<div class="warehouse-paper-field warehouse-paper-upload-field">
|
||
<span>辅助照片</span>
|
||
<label
|
||
class="warehouse-paper-upload"
|
||
:class="{ 'is-uploaded': genericForm.order_photo_url, 'is-busy': logisticsPhotoUploading }"
|
||
>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
:disabled="logisticsPhotoUploading"
|
||
@change="handleLogisticsPhotoUpload(genericForm, $event)"
|
||
/>
|
||
<b aria-hidden="true">{{ genericForm.order_photo_url ? "✓" : "↑" }}</b>
|
||
<strong>{{ logisticsPhotoUploading ? "上传中..." : genericForm.order_photo_url ? "已上传辅助照片" : "上传辅助照片" }}</strong>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isCustomerSuppliedInbound" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>库存批次号</span>
|
||
<input :value="customerSuppliedLotNoPreview ? `提交后自动生成:${customerSuppliedLotNoPreview}` : '选择物料后自动生成库存批次号'" type="text" readonly />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-else-if="!usesGenericSourceLotSelector && !isSemiAutoSourceOutbound && !isReworkScrapInbound" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>来源库存批次号</span>
|
||
<input v-model.trim="genericForm.source_material_sub_batch_no" type="text" placeholder="有来源库存批次时填写" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-else-if="isSemiAutoSourceOutbound" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>来源库存批次号</span>
|
||
<input :value="semiAutoSourceOutboundSourceLotSummary || '选择半成品后自动带出库存批次号'" type="text" readonly />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="isSemiAutoSourceOutbound && semiAutoSourceOutboundSourceLots.length" class="table-wrap warehouse-paper-source-lot-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>来源库存批次号</th>
|
||
<th>半成品</th>
|
||
<th>可用数量</th>
|
||
<th>可用重量(kg)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="lot in semiAutoSourceOutboundSourceLots" :key="lot.lot_id">
|
||
<td>{{ lot.lot_no }}</td>
|
||
<td>{{ selectedGenericItemOption?.operation_label || selectedGenericItemOption?.source_material_summary || "-" }}</td>
|
||
<td>{{ formatQty(lot.remaining_qty) }}</td>
|
||
<td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="activeOperation?.bizType === 'OUTSOURCING_OUT'" class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>委外方</span>
|
||
<input v-model.trim="genericForm.outsourcing_party_name" type="text" placeholder="填写委外加工方" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="usesGenericSourceLotSelector" class="warehouse-paper-source-lot-panel">
|
||
<div class="warehouse-paper-source-lot-heading">
|
||
<span>来源库存批次号</span>
|
||
<strong>{{ genericSourceLotSelectPlaceholder }}</strong>
|
||
</div>
|
||
<StockLotTagSelect
|
||
v-model="selectedGenericSourceLots"
|
||
:options="genericSourceLotOptions"
|
||
:placeholder="genericSourceLotSelectPlaceholder"
|
||
:allow-unavailable="isSourceLotReturnInbound || isSemiSourceLotTraceInbound || isOutsourcingScrapInbound"
|
||
:display-weight-field="isProductionSurplusInbound ? 'issued_weight_kg' : 'remaining_weight_kg'"
|
||
:option-weight-label="isProductionSurplusInbound ? '领料' : '剩余'"
|
||
/>
|
||
</div>
|
||
|
||
<div v-if="isSourceLotReturnInbound && selectedGenericSourceLots.length" class="table-wrap warehouse-paper-source-lot-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>来源库存批次号</th>
|
||
<th>原材料</th>
|
||
<th>{{ isProductionSurplusInbound ? "领料重量" : "剩余重量" }}</th>
|
||
<th v-if="isProductionSurplusInbound">已归还重量</th>
|
||
<th v-if="isProductionSurplusInbound">建议归还重量</th>
|
||
<th>归还重量(kg)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="lot in selectedGenericSourceLots" :key="lot.lot_id">
|
||
<td>{{ lot.lot_no }}</td>
|
||
<td>{{ lot.item_code }} · {{ lot.item_name }}</td>
|
||
<td>{{ formatWeight(isProductionSurplusInbound ? lot.issued_weight_kg : lot.remaining_weight_kg) }}</td>
|
||
<td v-if="isProductionSurplusInbound">{{ formatWeight(lot.returned_weight_kg) }}</td>
|
||
<td v-if="isProductionSurplusInbound">{{ formatWeight(lot.max_return_weight_kg) }}</td>
|
||
<td>
|
||
<input
|
||
v-model.number="lot.return_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
:max="productionSurplusReturnInputMax(lot)"
|
||
step="0.001"
|
||
:disabled="productionSurplusReturnInputDisabled(lot)"
|
||
:placeholder="productionSurplusReturnInputDisabled(lot) ? '已归还满' : ''"
|
||
required
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else-if="isProductTraceOnlySourceLotInbound && selectedGenericSourceLots.length" class="table-wrap warehouse-paper-source-lot-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>来源库存批次号</th>
|
||
<th>原材料</th>
|
||
<th>剩余重量</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="lot in selectedGenericSourceLots" :key="lot.lot_id">
|
||
<td>{{ lot.lot_no }}</td>
|
||
<td>{{ lot.item_code }} · {{ lot.item_name }}</td>
|
||
<td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
|
||
<td><span class="status-pill" :class="lotStatusClass(lot)">{{ formatStatusLabel(lot.status) }}</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else-if="isSourceLotSelectedOutbound && selectedGenericSourceLots.length" class="table-wrap warehouse-paper-source-lot-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>来源库存批次号</th>
|
||
<th>{{ isScrapSaleOutbound ? "废料" : "原材料" }}</th>
|
||
<th>可用重量</th>
|
||
<th>本次出库重量(kg)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="lot in selectedGenericSourceLots" :key="lot.lot_id">
|
||
<td>{{ lot.lot_no }}</td>
|
||
<td>
|
||
{{ lot.item_code }} · {{ lot.item_name }}
|
||
<small v-if="isScrapSaleOutbound && lot.source_stock_lot_no">废料库存批次号:{{ lot.source_stock_lot_no }}</small>
|
||
</td>
|
||
<td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
|
||
<td>
|
||
<input v-model.number="lot.issued_weight_kg" type="number" min="0.001" :max="Number(lot.remaining_weight_kg || 0)" step="0.001" required />
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="usesGenericSourceLotSelector && !isProductTraceOnlySourceLotInbound" class="warehouse-paper-inline-summary">
|
||
{{ isSourceLotSelectedOutbound ? "本次出库总重量" : "本次归还总重量" }}:{{ formatWeight(genericSelectedSourceLotWeight) }}
|
||
</div>
|
||
<div v-else-if="isProductTraceOnlySourceLotInbound && selectedGenericSourceLots.length" class="warehouse-paper-inline-summary">
|
||
已选择来源库存批次号:{{ selectedGenericSourceLots.length }} 个,仅用于来源追溯,不扣减原材料库存
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>{{ activeWarehouseType === "AUX" && activeOperation?.bizType === "PRODUCTION_OUT" ? "用途" : "备注" }}</span>
|
||
<textarea
|
||
v-model.trim="genericForm.remark"
|
||
rows="3"
|
||
:required="activeWarehouseType === 'AUX' && activeOperation?.bizType === 'PRODUCTION_OUT'"
|
||
></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="isProductionSurplusInbound || isProductionScrapInbound" class="warehouse-paper-settle-panel">
|
||
<label class="warehouse-paper-settle-toggle">
|
||
<input v-model="genericForm.settle_work_order" type="checkbox" />
|
||
<span class="settle-dot" aria-hidden="true"></span>
|
||
<strong>{{ isProductionScrapInbound ? "该批材料废料结单" : "该批材料余料结单" }}</strong>
|
||
</label>
|
||
<div v-if="genericForm.settle_work_order" class="warehouse-paper-settle-grid">
|
||
<article>
|
||
<span>{{ isProductionScrapInbound ? "标准废料重量" : "标准归还重量" }}</span>
|
||
<strong>{{ formatWeight(isProductionScrapInbound ? productionScrapExpectedWeight : productionSurplusExpectedReturnWeight) }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>{{ isProductionScrapInbound ? "本次后入库废料" : "本次后归还重量" }}</span>
|
||
<strong>{{ formatWeight(isProductionScrapInbound ? productionScrapFinalWeight : productionSurplusFinalReturnWeight) }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>偏差</span>
|
||
<strong :class="{ 'settle-warning': isProductionScrapInbound ? productionScrapSettleNeedsRemark : productionSurplusSettleNeedsRemark }">
|
||
{{ formatPercent(isProductionScrapInbound ? productionScrapSettleDeviationRate : productionSurplusSettleDeviationRate) }}
|
||
</strong>
|
||
</article>
|
||
</div>
|
||
<label
|
||
v-if="genericForm.settle_work_order && (isProductionScrapInbound ? productionScrapSettleNeedsRemark : productionSurplusSettleNeedsRemark)"
|
||
class="warehouse-paper-field warehouse-paper-field-full warehouse-paper-settle-remark"
|
||
>
|
||
<span>{{ isProductionScrapInbound ? "废料结单说明" : "余料结单说明" }}</span>
|
||
<textarea
|
||
v-model.trim="genericForm.settle_remark"
|
||
rows="3"
|
||
required
|
||
:placeholder="isProductionScrapInbound ? '入库废料重量超出标准上下15%时填写现场说明' : '归还重量超出标准上下15%时填写现场说明'"
|
||
></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation || logisticsPhotoUploading">
|
||
{{ submittingOperation ? "处理中..." : "确认登记" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="specialAdjustmentDrawerOpen"
|
||
:eyebrow="`${activeInventoryLabel} · ${specialAdjustmentDirectionText}`"
|
||
:title="specialAdjustmentDirectionText"
|
||
tag="特殊调整"
|
||
wide
|
||
@close="closeSpecialAdjustmentDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
:title="`${activeInventoryLabel}${specialAdjustmentDirectionText}单`"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
:tone="specialAdjustmentForm.direction === 'OUT' ? 'red' : 'green'"
|
||
@submit="submitSpecialAdjustment"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="特殊调整信息">
|
||
<div class="document-inline-summary special-adjustment-paper-warning">
|
||
<strong>特殊调整会直接改变库存明细</strong>
|
||
<span>保存后写入库存流水和特殊调整记录,请仅在补录、纠错、线下已处理等特殊场景使用。</span>
|
||
</div>
|
||
|
||
<div class="document-control-grid special-adjustment-paper-grid">
|
||
<label class="document-paper-field document-paper-field-wide">
|
||
<span>调整说明</span>
|
||
<textarea
|
||
v-model.trim="specialAdjustmentForm.reason"
|
||
rows="3"
|
||
required
|
||
placeholder="必须填写现场原因、审批依据或补录说明"
|
||
></textarea>
|
||
</label>
|
||
<label class="document-paper-field">
|
||
<span>仓库</span>
|
||
<select v-model.number="specialAdjustmentForm.warehouse_id" required @change="handleSpecialAdjustmentWarehouseChange">
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-inline-summary special-adjustment-paper-toolbar">
|
||
<button class="ghost-button" type="button" :disabled="!specialAdjustmentForm.reason.trim()" @click="addSpecialAdjustmentLine">
|
||
添加调整明细
|
||
</button>
|
||
<span v-if="!specialAdjustmentForm.reason.trim()" class="special-adjustment-tip">请先填写调整说明</span>
|
||
<span v-else>每条明细都会写入库存流水,调整说明会同步进入变更记录。</span>
|
||
</div>
|
||
|
||
<div class="special-adjustment-paper-lines special-adjustment-lines" :class="{ 'is-empty': !specialAdjustmentForm.lines.length }">
|
||
<article
|
||
v-for="(line, index) in specialAdjustmentForm.lines"
|
||
:key="line.row_key"
|
||
class="special-adjustment-paper-line-card special-adjustment-line-card"
|
||
>
|
||
<header class="special-adjustment-line-head">
|
||
<div>
|
||
<span class="special-adjustment-line-index">明细 {{ index + 1 }}</span>
|
||
<strong>{{ specialAdjustmentItemColumnLabel }}</strong>
|
||
</div>
|
||
<button class="link-button danger" type="button" @click="removeSpecialAdjustmentLine(index)">删除明细</button>
|
||
</header>
|
||
|
||
<div class="special-adjustment-card-grid">
|
||
<label v-if="specialAdjustmentForm.direction === 'IN'" class="special-adjustment-card-field">
|
||
<span>入库方式</span>
|
||
<select v-model="line.inbound_mode" required @change="handleSpecialAdjustmentInboundModeChange(line)">
|
||
<option value="EXISTING">补增已有明细</option>
|
||
<option value="NEW">新增特殊明细</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label
|
||
v-if="specialAdjustmentLineUsesExistingLot(line)"
|
||
class="special-adjustment-card-field special-adjustment-card-field-wide"
|
||
>
|
||
<span>{{ specialAdjustmentForm.direction === "OUT" ? "库存明细" : "补增库存明细" }}</span>
|
||
<select
|
||
v-model.number="line.lot_id"
|
||
:required="specialAdjustmentForm.direction === 'OUT' || line.inbound_mode === 'EXISTING'"
|
||
@change="applySpecialAdjustmentLot(line)"
|
||
>
|
||
<option disabled value="">{{ specialAdjustmentForm.direction === "OUT" ? "请选择库存明细" : "请选择要补增的库存明细" }}</option>
|
||
<option v-for="lot in specialAdjustmentLotOptions" :key="lot.lot_id" :value="lot.lot_id">
|
||
{{ formatSpecialAdjustmentLotOption(lot) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label v-else class="special-adjustment-card-field special-adjustment-card-field-wide">
|
||
<span>物料</span>
|
||
<select v-model="line.item_option_key" required @change="applySpecialAdjustmentItem(line)">
|
||
<option disabled value="">请选择物料</option>
|
||
<option v-for="item in specialAdjustmentItemOptions" :key="item.option_key || item.item_id" :value="item.option_key || String(item.item_id)">
|
||
{{ formatSpecialAdjustmentItemOption(item) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<div v-if="line.item_name" class="special-adjustment-card-field special-adjustment-card-field-wide special-adjustment-selected-note">
|
||
<span>已带入</span>
|
||
<p>
|
||
{{ line.item_name }}<template v-if="line.lot_no"> · {{ line.lot_no }}</template> · 单价 ¥{{ formatAmount(line.unit_cost) }}
|
||
</p>
|
||
</div>
|
||
|
||
<label
|
||
v-if="specialAdjustmentForm.direction === 'IN' && line.inbound_mode === 'NEW'"
|
||
class="special-adjustment-card-field"
|
||
>
|
||
<span>单位成本</span>
|
||
<input v-model.number="line.unit_cost" type="number" min="0" step="0.0001" placeholder="可填写新明细单价" />
|
||
</label>
|
||
|
||
<label v-if="specialAdjustmentCanShowMeasureBasis" class="special-adjustment-card-field">
|
||
<span>调整口径</span>
|
||
<select
|
||
v-if="specialAdjustmentRawMeasureMode(line) === 'SEMI_SELECT'"
|
||
v-model="line.measure_mode"
|
||
required
|
||
@change="handleSpecialAdjustmentMeasureModeChange(line)"
|
||
>
|
||
<option value="QTY">按数量调整</option>
|
||
<option value="WEIGHT">按重量调整</option>
|
||
</select>
|
||
<p v-else class="special-adjustment-card-static">{{ formatSpecialAdjustmentMeasureMode(line) }}</p>
|
||
</label>
|
||
|
||
<label v-if="specialAdjustmentCanShowQty" class="special-adjustment-card-field">
|
||
<span>调整数量</span>
|
||
<input
|
||
v-if="specialAdjustmentShowQtyInput(line)"
|
||
v-model.number="line.adjust_qty"
|
||
type="number"
|
||
min="0"
|
||
step="0.001"
|
||
:max="specialAdjustmentForm.direction === 'OUT' ? Number(selectedSpecialAdjustmentLot(line)?.remaining_qty || 0) : undefined"
|
||
required
|
||
placeholder="请输入数量"
|
||
/>
|
||
<p v-else class="special-adjustment-card-static">-</p>
|
||
</label>
|
||
|
||
<label v-if="specialAdjustmentCanShowWeight" class="special-adjustment-card-field">
|
||
<span>调整重量</span>
|
||
<input
|
||
v-if="specialAdjustmentShowWeightInput(line)"
|
||
v-model.number="line.adjust_weight_kg"
|
||
type="number"
|
||
min="0"
|
||
step="0.001"
|
||
:max="specialAdjustmentForm.direction === 'OUT' ? Number(selectedSpecialAdjustmentLot(line)?.remaining_weight_kg || 0) : undefined"
|
||
:required="specialAdjustmentWeightRequired(line)"
|
||
placeholder="请输入重量"
|
||
/>
|
||
<p v-else class="special-adjustment-card-static">-</p>
|
||
</label>
|
||
|
||
<label class="special-adjustment-card-field special-adjustment-card-field-wide">
|
||
<span>明细说明</span>
|
||
<input v-model.trim="line.line_reason" type="text" placeholder="可填写该行补充说明" />
|
||
</label>
|
||
|
||
<div class="special-adjustment-card-preview">
|
||
<span>调整预览</span>
|
||
<strong>{{ specialAdjustmentLinePreview(line) }}</strong>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<div v-if="!specialAdjustmentForm.lines.length" class="special-adjustment-empty">
|
||
<strong>还没有调整明细</strong>
|
||
<span>先填写调整说明,再点击“添加调整明细”。每一条明细都会写入库存流水。</span>
|
||
</div>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="specialAdjustmentSubmitting">
|
||
{{ specialAdjustmentSubmitting ? "保存中..." : `确认${specialAdjustmentDirectionText}` }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="rawReturnDrawerOpen"
|
||
eyebrow="原材料库 · 退货出库"
|
||
title="原材料退货出库"
|
||
tag="采购退货"
|
||
wide
|
||
@close="closeRawReturnDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="原材料退货出库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
tone="red"
|
||
@submit="submitRawReturnOutbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="退货出库信息">
|
||
<div class="warehouse-paper-control-grid raw-return-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>仓库</span>
|
||
<select v-model.number="rawReturnForm.warehouse_id" required>
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>库存批次号</span>
|
||
<select v-model.number="rawReturnForm.lot_id" required @change="handleRawReturnLotChange">
|
||
<option disabled value="">请选择可退货批次</option>
|
||
<option v-for="lot in rawReturnLotOptions" :key="lot.lot_id" :value="lot.lot_id">
|
||
{{ lot.lot_no }} · {{ lot.item_code }} · {{ lot.item_name }} · 可退 {{ formatWeight(rawReturnLotAvailableWeight(lot)) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>采购来源</span>
|
||
<select
|
||
v-model.number="rawReturnForm.purchase_order_item_id"
|
||
required
|
||
:disabled="rawReturnLinkLoading || !rawReturnForm.lot_id"
|
||
@change="handleRawReturnLinkChange"
|
||
>
|
||
<option disabled value="">{{ rawReturnLinkLoading ? "采购来源加载中..." : "请选择采购来源" }}</option>
|
||
<option v-for="link in rawReturnPurchaseLinks" :key="link.purchase_order_item_id" :value="link.purchase_order_item_id">
|
||
{{ formatRawReturnLinkLabel(link) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>退货重量(kg)</span>
|
||
<input
|
||
v-model.number="rawReturnForm.return_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
required
|
||
:max="rawReturnMaxWeight"
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid raw-return-logistics-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>运单号</span>
|
||
<input v-model.trim="rawReturnForm.waybill_no" type="text" placeholder="可填运单号,或上传辅助照片" />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>运费</span>
|
||
<input v-model.number="rawReturnForm.freight_amount" type="number" min="0" step="0.01" placeholder="选填" />
|
||
</label>
|
||
<div class="warehouse-paper-field warehouse-paper-upload-field">
|
||
<span>辅助照片</span>
|
||
<label
|
||
class="warehouse-paper-upload"
|
||
:class="{ 'is-uploaded': rawReturnForm.order_photo_url, 'is-busy': logisticsPhotoUploading }"
|
||
>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
:disabled="logisticsPhotoUploading"
|
||
@change="handleLogisticsPhotoUpload(rawReturnForm, $event)"
|
||
/>
|
||
<b aria-hidden="true">{{ rawReturnForm.order_photo_url ? "✓" : "↑" }}</b>
|
||
<strong>{{ logisticsPhotoUploading ? "上传中..." : rawReturnForm.order_photo_url ? "已上传辅助照片" : "上传辅助照片" }}</strong>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid raw-return-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>退货原因</span>
|
||
<input v-model.trim="rawReturnForm.reason" type="text" placeholder="例如:质量或性能不合格" />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="rawReturnForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation || logisticsPhotoUploading || rawReturnLinkLoading">
|
||
{{ submittingOperation ? "出库中..." : "创建退货出库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="returnInboundDrawerOpen"
|
||
eyebrow="退货库 · 退货入库"
|
||
title="退货入库"
|
||
tag="待返工"
|
||
wide
|
||
@close="closeReturnInboundDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="退货入库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
@submit="submitReturnInbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="退货入库信息">
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>发货批次</span>
|
||
<select v-model.number="returnInboundForm.delivery_item_id" required @change="applyReturnInboundDeliveryLine">
|
||
<option disabled value="">请选择发货批次</option>
|
||
<option v-for="line in deliveryLineOptions" :key="line.delivery_item_id" :value="line.delivery_item_id">
|
||
{{ formatReturnDeliveryLineLabel(line) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>退货仓</span>
|
||
<select v-model.number="returnInboundForm.warehouse_id" required>
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-inline-summary return-trace-summary">
|
||
{{ formatReturnTraceSummary(selectedReturnInboundDeliveryLine) }}
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>退货数量</span>
|
||
<input
|
||
v-model.number="returnInboundForm.return_qty"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
:max="selectedReturnInboundDeliveryLine ? selectedReturnInboundDeliveryLine.returnable_qty : undefined"
|
||
required
|
||
/>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>退货重量(kg)</span>
|
||
<input
|
||
v-model.number="returnInboundForm.return_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
:max="selectedReturnInboundDeliveryLine ? selectedReturnInboundDeliveryLine.returnable_weight_kg : undefined"
|
||
required
|
||
/>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>处理人</span>
|
||
<select v-model.number="returnInboundForm.handler_employee_id">
|
||
<option value="">请选择</option>
|
||
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||
{{ employeeOptionLabel(employee) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>退货原因</span>
|
||
<input v-model.trim="returnInboundForm.return_reason" type="text" placeholder="例如:客户退回,可返工" />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="returnInboundForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation">
|
||
{{ submittingOperation ? "入库中..." : "创建退货入库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="returnScrapInboundDrawerOpen"
|
||
eyebrow="废料库 · 退货废料入库"
|
||
title="退货废料入库"
|
||
tag="客户退货报废"
|
||
wide
|
||
@close="closeReturnScrapInboundDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="退货废料入库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
@submit="submitReturnScrapInbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="退货废料入库信息">
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>发货批次</span>
|
||
<select v-model.number="returnScrapInboundForm.delivery_item_id" required @change="applyReturnScrapDeliveryLine">
|
||
<option disabled value="">请选择发货批次</option>
|
||
<option v-for="line in deliveryLineOptions" :key="line.delivery_item_id" :value="line.delivery_item_id">
|
||
{{ formatReturnDeliveryLineLabel(line) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>废料仓</span>
|
||
<select v-model.number="returnScrapInboundForm.warehouse_id" required>
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in activeWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-inline-summary return-trace-summary">
|
||
{{ formatReturnTraceSummary(selectedReturnScrapDeliveryLine) }}
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>报废数量</span>
|
||
<input
|
||
v-model.number="returnScrapInboundForm.scrap_qty"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
:max="selectedReturnScrapDeliveryLine ? selectedReturnScrapDeliveryLine.returnable_qty : undefined"
|
||
required
|
||
/>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>报废重量(kg)</span>
|
||
<input
|
||
v-model.number="returnScrapInboundForm.scrap_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
:max="selectedReturnScrapDeliveryLine ? selectedReturnScrapDeliveryLine.returnable_weight_kg : undefined"
|
||
required
|
||
/>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>废料单价(元/kg)</span>
|
||
<input v-model.number="returnScrapInboundForm.unit_cost" type="number" min="0" step="0.0001" placeholder="不填则按产品废料单价" />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>处理人</span>
|
||
<select v-model.number="returnScrapInboundForm.handler_employee_id">
|
||
<option value="">请选择</option>
|
||
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||
{{ employeeOptionLabel(employee) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>退货原因</span>
|
||
<input v-model.trim="returnScrapInboundForm.return_reason" type="text" placeholder="例如:客户退回,判定报废" />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="returnScrapInboundForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation">
|
||
{{ submittingOperation ? "入库中..." : "创建退货废料入库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="openingImportDrawerOpen"
|
||
eyebrow="嘉恒仓库 · 期初导入"
|
||
:title="`${activeInventoryLabel}期初库存导入`"
|
||
tag="库存初始化"
|
||
wide
|
||
@close="closeOpeningImportDrawer"
|
||
>
|
||
<div class="stack-form">
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<div class="import-layout">
|
||
<section class="import-card">
|
||
<strong>导出模版</strong>
|
||
<p>先下载当前仓库类型的期初导入模版,按实际清点库存填写后再导入。</p>
|
||
<button class="primary-button" type="button" :disabled="openingImportBusy" @click="exportOpeningTemplate">
|
||
{{ openingImportBusy ? "处理中..." : `导出${activeInventoryLabel}期初模版` }}
|
||
</button>
|
||
</section>
|
||
|
||
<section class="import-card">
|
||
<label class="import-dropzone">
|
||
<input ref="openingImportInputRef" class="hidden-file-input" type="file" accept=".xlsx,.xlsm" @change="importOpeningInventory" />
|
||
<span class="import-icon">XLSX</span>
|
||
<strong>{{ openingImportBusy ? "导入中..." : "上传期初库存 Excel" }}</strong>
|
||
<p>导入会生成期初批次、库存余额和库存流水;批次号重复会被拦截。</p>
|
||
</label>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="productionDrawerOpen"
|
||
eyebrow="原材料库 · 生产出库"
|
||
title="生产出库"
|
||
description="选择原材料库存批次出库,后续小程序报工按库存批次号推进。"
|
||
tag="生产出库"
|
||
wide
|
||
@close="productionDrawerOpen = false"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="生产出库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
tone="red"
|
||
novalidate
|
||
@submit="submitProductionOut"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
<WarehouseDocumentSection index="01" title="生产出库信息">
|
||
<div class="document-inline-summary production-out-paper-tip">
|
||
选择原材料库存批次出库,后续小程序报工按库存批次号推进。一次生产出库只能选择一个材料库存批次。
|
||
</div>
|
||
|
||
<div class="document-control-grid production-out-paper-grid">
|
||
<label class="document-paper-field document-paper-field-wide">
|
||
<span>产品</span>
|
||
<select v-model.number="productionForm.product_item_id" required>
|
||
<option disabled value="">请选择产品</option>
|
||
<option v-for="product in products" :key="product.item_id" :value="product.item_id">
|
||
{{ formatProductLabel(product) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-selector-panel production-out-source-lot-panel">
|
||
<div class="document-selector-heading">
|
||
<span>库存批次号</span>
|
||
<strong>搜索并选择一个材料库存批次号</strong>
|
||
</div>
|
||
<div class="document-selector-body">
|
||
<StockLotTagSelect
|
||
v-model="selectedProductionStockLots"
|
||
:options="productionStockLotOptions"
|
||
:max-selections="1"
|
||
placeholder="搜索并选择一个材料库存批次号"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="selectedProductionStockLots.length" class="table-wrap document-source-table production-out-source-lot-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ activeWarehouseType === "SEMI" ? "来源库存批次号" : "库存批次号" }}</th>
|
||
<th>原材料</th>
|
||
<th>剩余重量</th>
|
||
<th>单位成本</th>
|
||
<th>本次出库重量(kg)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="lot in selectedProductionStockLots" :key="lot.lot_id">
|
||
<td>{{ lot.lot_no }}</td>
|
||
<td>{{ lot.item_name }}</td>
|
||
<td>{{ formatWeight(lot.remaining_weight_kg) }}</td>
|
||
<td>¥{{ formatAmount(lot.unit_cost) }}/kg</td>
|
||
<td>
|
||
<input
|
||
v-model.number="lot.issued_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
:max="Number(lot.remaining_weight_kg || 0)"
|
||
required
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="document-control-grid production-out-paper-grid">
|
||
<label class="document-paper-field">
|
||
<span>本次生产出库重量(kg)</span>
|
||
<input
|
||
v-model.number="productionForm.issue_weight_kg"
|
||
type="number"
|
||
min="0.001"
|
||
step="0.001"
|
||
required
|
||
:readonly="selectedProductionStockLots.length > 0"
|
||
/>
|
||
</label>
|
||
<label class="document-paper-field">
|
||
<span>参考生产数量</span>
|
||
<input :value="formatQty(referenceProductionQty)" type="text" readonly />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="feedback-banner">
|
||
单件毛重:{{ selectedBomGrossWeight ? formatWeight(selectedBomGrossWeight) : "未配置" }};参考生产数量 = 本次生产出库重量 ÷ 单件毛重。
|
||
</div>
|
||
|
||
<div class="document-control-grid production-out-paper-grid production-out-paper-remark">
|
||
<label class="document-paper-field document-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="productionForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation">
|
||
{{ submittingOperation ? "出库中..." : "创建生产出库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="completionDrawerOpen"
|
||
:eyebrow="isReworkCompletionOperation ? '成品库 · 返工入库' : '成品库 · 生产入库'"
|
||
:title="isReworkCompletionOperation ? '新增返工入库' : '新增生产入库'"
|
||
:tag="isReworkCompletionOperation ? '返工入库' : '成品入库'"
|
||
wide
|
||
@close="completionDrawerOpen = false"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
:title="isReworkCompletionOperation ? '返工入库单' : '生产入库单'"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
@submit="submitCompletionInbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" :title="isReworkCompletionOperation ? '返工入库信息' : '生产入库信息'">
|
||
<div class="warehouse-paper-control-grid completion-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>{{ isReworkCompletionOperation ? "返工工单" : "生产工单" }}</span>
|
||
<select v-model.number="completionForm.work_order_id" required @change="applyCompletionWorkOrder">
|
||
<option disabled value="">{{ isReworkCompletionOperation ? "请选择返工工单" : "请选择生产工单" }}</option>
|
||
<option v-for="workOrder in completionWorkOrderOptions" :key="workOrder.work_order_id" :value="workOrder.work_order_id">
|
||
{{ workOrder.work_order_no }} · {{ workOrder.product_name }} · 可入库 {{ formatQty(workOrder.limits?.max_finished_inbound_qty) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>成品仓</span>
|
||
<select v-model.number="completionForm.warehouse_id" required>
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in finishedWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>接收人</span>
|
||
<select v-model.number="completionForm.receiver_employee_id">
|
||
<option value="">请选择</option>
|
||
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||
{{ employeeOptionLabel(employee) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>产品</span>
|
||
<input :value="completionProductLabel" type="text" disabled />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>库位</span>
|
||
<select v-model.number="completionForm.location_id">
|
||
<option value="">默认库位</option>
|
||
<option v-for="location in completionLocations" :key="location.id" :value="location.id">
|
||
{{ location.location_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid completion-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>计算最大可入库数量</span>
|
||
<input :value="formatQty(completionForm.max_receivable_qty)" type="text" disabled />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>入库数量</span>
|
||
<input
|
||
v-model.number="completionForm.receipt_qty"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
:max="completionForm.tolerance_receivable_qty || undefined"
|
||
required
|
||
@input="recalculateCompletionWeight"
|
||
/>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>入库重量(kg)</span>
|
||
<input v-model.number="completionForm.receipt_weight_kg" type="number" min="0" step="0.001" readonly />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid completion-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="completionForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-settle-panel completion-settle-panel">
|
||
<label
|
||
class="warehouse-paper-settle-toggle"
|
||
:class="{ 'is-disabled': !completionForm.work_order_id }"
|
||
:title="completionForm.work_order_id ? '勾选后保存时结单' : '选择生产工单后可勾选结单'"
|
||
>
|
||
<input v-model="completionForm.settle_work_order" type="checkbox" :disabled="!completionForm.work_order_id" />
|
||
<span class="settle-dot" aria-hidden="true"></span>
|
||
<strong>该工单结单</strong>
|
||
</label>
|
||
<div v-if="completionForm.settle_work_order" class="warehouse-paper-settle-grid">
|
||
<article>
|
||
<span>标准成品数量</span>
|
||
<strong>{{ formatQty(completionExpectedFinishedQty) }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>本次后入库成品</span>
|
||
<strong>{{ formatQty(completionFinalFinishedQty) }}</strong>
|
||
</article>
|
||
<article>
|
||
<span>偏差</span>
|
||
<strong :class="{ 'settle-warning': completionSettleNeedsRemark }">
|
||
{{ formatPercent(completionSettleDeviationRate) }}
|
||
</strong>
|
||
</article>
|
||
</div>
|
||
<label v-if="completionForm.settle_work_order && completionSettleNeedsRemark" class="warehouse-paper-field warehouse-paper-field-full warehouse-paper-settle-remark">
|
||
<span>结单说明</span>
|
||
<textarea
|
||
v-model.trim="completionForm.settle_remark"
|
||
rows="3"
|
||
required
|
||
placeholder="入库成品数量超出标准上下15%时填写现场说明"
|
||
></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation">
|
||
{{ submittingOperation ? "过账中..." : "创建生产入库单" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="deliveryDrawerOpen"
|
||
eyebrow="成品库 · 销售出库"
|
||
title="新增销售出库"
|
||
tag="销售订单发货"
|
||
wide
|
||
@close="deliveryDrawerOpen = false"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="销售出库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
tone="red"
|
||
@submit="submitSalesOut"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="销售出库信息">
|
||
<div class="workflow-mode-switch delivery-mode-switch sales-delivery-mode-panel" role="tablist" aria-label="销售出库方式">
|
||
<button
|
||
class="workflow-mode-option"
|
||
:class="{ active: isSalesOrderDelivery }"
|
||
type="button"
|
||
role="tab"
|
||
:aria-selected="isSalesOrderDelivery"
|
||
title="选择已有销售订单,发货后回写订单已发数量。"
|
||
@click='setDeliveryMode("SALES_ORDER")'
|
||
>
|
||
<span class="mode-option-dot" aria-hidden="true"></span>
|
||
<strong>销售订单发货</strong>
|
||
<small>回写订单</small>
|
||
</button>
|
||
<button
|
||
class="workflow-mode-option"
|
||
:class="{ active: isDirectCustomerDelivery }"
|
||
type="button"
|
||
role="tab"
|
||
:aria-selected="isDirectCustomerDelivery"
|
||
title="不绑定销售订单,直接按客户和产品从成品库出库。"
|
||
@click='setDeliveryMode("DIRECT_CUSTOMER")'
|
||
>
|
||
<span class="mode-option-dot" aria-hidden="true"></span>
|
||
<strong>直接客户发货</strong>
|
||
<small>直接出库</small>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="deliveryForm.delivery_mode === 'SALES_ORDER'" class="warehouse-paper-control-grid sales-delivery-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>关联销售订单</span>
|
||
<select v-model.number="deliveryForm.sales_order_id" required @change="applySalesOrderDefaults">
|
||
<option disabled value="">请选择销售订单</option>
|
||
<option v-for="order in openSalesOrders" :key="order.order_id" :value="order.order_id">
|
||
{{ order.order_no }} · {{ order.customer_name }} · 未发 {{ formatQty(orderPendingQty(order.order_id)) }} · {{ orderPendingProductSummary(order.order_id) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>销售订单明细</span>
|
||
<select v-model.number="deliveryForm.sales_order_item_id" required @change="applySalesOrderItemDefaults">
|
||
<option disabled value="">请选择订单明细</option>
|
||
<option v-for="item in selectableSalesOrderItems" :key="item.sales_order_item_id" :value="item.sales_order_item_id">
|
||
{{ item.product_code }} · {{ item.product_name }} · 未发 {{ formatQty(orderItemPendingQty(item)) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid sales-delivery-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>发货产品</span>
|
||
<select v-model.number="deliveryForm.product_item_id" required :disabled="isSalesOrderDelivery && Boolean(deliveryForm.sales_order_item_id)">
|
||
<option disabled value="">请选择产品</option>
|
||
<option v-for="product in products" :key="product.item_id" :value="product.item_id">
|
||
{{ product.item_code }} · {{ product.item_name }}{{ product.version_no ? ` · ${product.version_no}` : "" }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>发货仓</span>
|
||
<select v-model.number="deliveryForm.warehouse_id" required>
|
||
<option disabled value="">请选择仓库</option>
|
||
<option v-for="warehouse in finishedWarehouses" :key="warehouse.id" :value="warehouse.id">
|
||
{{ warehouse.warehouse_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>发货数量</span>
|
||
<input
|
||
v-model.number="deliveryForm.delivery_qty"
|
||
type="number"
|
||
min="1"
|
||
step="1"
|
||
required
|
||
:max="isSalesOrderDelivery && selectedSalesOrderItem ? orderItemPendingQty(selectedSalesOrderItem) : undefined"
|
||
/>
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>收货客户</span>
|
||
<select v-model.number="deliveryForm.customer_id" required :disabled="isSalesOrderDelivery" @change="applyDeliveryCustomerDefaults">
|
||
<option disabled value="">请选择客户</option>
|
||
<option v-for="customer in customers" :key="customer.id" :value="customer.id">
|
||
{{ customer.customer_name }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>发货人</span>
|
||
<select v-model.number="deliveryForm.shipper_employee_id">
|
||
<option value="">请选择</option>
|
||
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||
{{ employeeOptionLabel(employee) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>收货人</span>
|
||
<input v-model.trim="deliveryForm.consignee_name" type="text" />
|
||
</label>
|
||
|
||
<label class="warehouse-paper-field">
|
||
<span>联系电话</span>
|
||
<input v-model.trim="deliveryForm.consignee_phone" type="text" />
|
||
</label>
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>送货地址</span>
|
||
<input v-model.trim="deliveryForm.delivery_address" type="text" />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid sales-delivery-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>运单号</span>
|
||
<input v-model.trim="deliveryForm.waybill_no" type="text" placeholder="可填运单号,或上传辅助照片" />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>运费</span>
|
||
<input v-model.number="deliveryForm.freight_amount" type="number" min="0" step="0.01" required />
|
||
</label>
|
||
<div class="warehouse-paper-field warehouse-paper-upload-field">
|
||
<span>辅助照片</span>
|
||
<label
|
||
class="warehouse-paper-upload"
|
||
:class="{ 'is-uploaded': deliveryForm.order_photo_url, 'is-busy': logisticsPhotoUploading }"
|
||
>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
:disabled="logisticsPhotoUploading"
|
||
@change="handleLogisticsPhotoUpload(deliveryForm, $event)"
|
||
/>
|
||
<b aria-hidden="true">{{ deliveryForm.order_photo_url ? "✓" : "↑" }}</b>
|
||
<strong>{{ logisticsPhotoUploading ? "上传中..." : deliveryForm.order_photo_url ? "已上传辅助照片" : "上传辅助照片" }}</strong>
|
||
</label>
|
||
</div>
|
||
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>包装备注</span>
|
||
<input v-model.trim="deliveryForm.remark" type="text" placeholder="有无提供篮子等" />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-inline-summary sales-delivery-allocation-summary">
|
||
<strong>自动销售出库明细</strong>
|
||
<span class="panel-tag">可发 {{ formatQty(totalAvailableQty) }},本次拆分 {{ formatQty(totalAllocatedQty) }}</span>
|
||
</div>
|
||
<div class="table-wrap warehouse-paper-source-lot-table sales-delivery-allocation-table">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>成品库存批次号</th>
|
||
<th>来源原材料库存批次号</th>
|
||
<th>来源材料摘要</th>
|
||
<th>可发数量</th>
|
||
<th>本次发货</th>
|
||
<th>重量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in allocationRows" :key="item.lot_id">
|
||
<td>{{ item.lot_no }}</td>
|
||
<td>{{ item.source_material_sub_batch_no || item.source_material_lot_no || "-" }}</td>
|
||
<td :title="item.source_material_summary || ''">{{ item.source_material_summary || "-" }}</td>
|
||
<td>{{ formatQty(item.remaining_qty) }}</td>
|
||
<td>{{ formatQty(item.delivery_qty) }}</td>
|
||
<td>{{ formatWeight(item.delivery_weight_kg) }}</td>
|
||
</tr>
|
||
<tr v-if="!allocationRows.length">
|
||
<td colspan="6" class="empty-row">{{ isSalesOrderDelivery ? "选择销售订单和发货数量后显示拆分明细" : "选择客户、产品和发货数量后显示拆分明细" }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation || logisticsPhotoUploading">
|
||
{{ submittingOperation ? "出库中..." : "创建销售出库单" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="returnReworkDrawerOpen"
|
||
eyebrow="退货库 · 返工出库"
|
||
title="返工出库"
|
||
tag="退货返工"
|
||
wide
|
||
@close="closeReturnReworkDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="返工出库单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
tone="red"
|
||
@submit="submitReturnReworkOutbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<WarehouseDocumentSection index="01" title="返工出库信息">
|
||
<div class="warehouse-paper-control-grid return-rework-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-wide">
|
||
<span>待返工退货明细</span>
|
||
<select v-model.number="returnReworkForm.return_item_id" required>
|
||
<option disabled value="">请选择待返工退货明细</option>
|
||
<option v-for="item in pendingReturnItems" :key="item.return_item_id" :value="item.return_item_id">
|
||
{{ formatReturnReworkItemLabel(item) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="document-inline-summary return-rework-trace-summary">
|
||
{{ selectedReturnReworkItem ? formatReturnTraceSummary(selectedReturnReworkItem) : "请选择退货明细,系统会按退货库锁定批次办理返工出库。" }}
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-rework-paper-grid">
|
||
<label class="warehouse-paper-field">
|
||
<span>额外人工成本</span>
|
||
<input v-model.number="returnReworkForm.extra_cost_amount" type="number" min="0" step="0.01" />
|
||
</label>
|
||
<label class="warehouse-paper-field">
|
||
<span>返工重量(kg)</span>
|
||
<input :value="selectedReturnReworkItem ? formatWeight(selectedReturnReworkItem.return_weight_kg) : '-'" type="text" disabled />
|
||
</label>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-control-grid return-rework-paper-grid">
|
||
<label class="warehouse-paper-field warehouse-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="returnReworkForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation || !pendingReturnItems.length">
|
||
{{ submittingOperation ? "出库中..." : "确认返工出库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="warehouseLedgerDrawerOpen"
|
||
eyebrow="嘉恒仓库 · 库级流水"
|
||
:title="`${activeInventoryLabel} · 出入库流水`"
|
||
tag="库存流水"
|
||
wide
|
||
@close="closeWarehouseLedgerDrawer"
|
||
>
|
||
<div class="stack-form warehouse-ledger-drawer">
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
|
||
<div class="warehouse-ledger-summary">
|
||
<article class="warehouse-ledger-summary-card">
|
||
<span>入库笔数</span>
|
||
<strong>{{ warehouseLedgerSummary.in_count || 0 }}</strong>
|
||
</article>
|
||
<article class="warehouse-ledger-summary-card">
|
||
<span>出库笔数</span>
|
||
<strong>{{ warehouseLedgerSummary.out_count || 0 }}</strong>
|
||
</article>
|
||
<article class="warehouse-ledger-summary-card">
|
||
<span>{{ activeQuantityOnly ? "入库数量" : "入库重量" }}</span>
|
||
<strong>{{ activeQuantityOnly ? formatQty(warehouseLedgerSummary.in_qty || 0) : formatWeight(warehouseLedgerSummary.in_weight_kg || 0) }}</strong>
|
||
</article>
|
||
<article class="warehouse-ledger-summary-card">
|
||
<span>{{ activeQuantityOnly ? "出库数量" : "出库重量" }}</span>
|
||
<strong>{{ activeQuantityOnly ? formatQty(warehouseLedgerSummary.out_qty || 0) : formatWeight(warehouseLedgerSummary.out_weight_kg || 0) }}</strong>
|
||
</article>
|
||
<article class="warehouse-ledger-summary-card">
|
||
<span>最近流水</span>
|
||
<strong>{{ warehouseLedgerSummary.latest_biz_time ? formatDateTime(warehouseLedgerSummary.latest_biz_time) : "-" }}</strong>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="warehouse-ledger-filters">
|
||
<div class="warehouse-ledger-filter-fields">
|
||
<label class="form-field">
|
||
<span>搜索</span>
|
||
<input v-model.trim="warehouseLedgerFilters.keyword" type="text" placeholder="搜索物料、批次、流水类型、来源、运单号、备注" @keyup.enter="loadWarehouseLedger" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>方向</span>
|
||
<select v-model="warehouseLedgerFilters.direction" @change="loadWarehouseLedger">
|
||
<option value="ALL">全部</option>
|
||
<option value="IN">入库</option>
|
||
<option value="OUT">出库</option>
|
||
<option value="ADJUST">调整</option>
|
||
</select>
|
||
</label>
|
||
<label class="form-field">
|
||
<span>业务类型</span>
|
||
<input v-model.trim="warehouseLedgerFilters.txn_type" type="text" placeholder="输入流水类型中文名,如销售出库、退货入库" @keyup.enter="loadWarehouseLedger" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>开始日期</span>
|
||
<input v-model="warehouseLedgerFilters.start_date" type="date" @change="loadWarehouseLedger" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>结束日期</span>
|
||
<input v-model="warehouseLedgerFilters.end_date" type="date" @change="loadWarehouseLedger" />
|
||
</label>
|
||
</div>
|
||
<div class="warehouse-ledger-filter-actions">
|
||
<button class="ghost-button warehouse-ledger-reset-button" type="button" :disabled="warehouseLedgerLoading" @click="resetWarehouseLedgerFilters">
|
||
重置
|
||
</button>
|
||
<button class="primary-button warehouse-ledger-query-button" type="button" :disabled="warehouseLedgerLoading" @click="loadWarehouseLedger">
|
||
{{ warehouseLedgerLoading ? "查询中..." : "查询" }}
|
||
</button>
|
||
<button class="secondary-button warehouse-ledger-export-button" type="button" :disabled="warehouseLedgerLoading || warehouseLedgerExporting" @click="exportWarehouseLedgerExcel">
|
||
{{ warehouseLedgerExporting ? "导出中..." : "导出Excel" }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap warehouse-ledger-table-wrap">
|
||
<table ref="warehouseLedgerTableRef" class="data-table compact-table warehouse-ledger-table">
|
||
<colgroup>
|
||
<col class="ledger-col-time" />
|
||
<col class="ledger-col-direction" />
|
||
<col class="ledger-col-type" />
|
||
<col class="ledger-col-item" />
|
||
<col class="ledger-col-lot" />
|
||
<col v-if="!activeWeightOnly" class="ledger-col-qty" />
|
||
<col v-if="!activeQuantityOnly" class="ledger-col-weight" />
|
||
<col class="ledger-col-price" />
|
||
<col class="ledger-col-amount" />
|
||
<col class="ledger-col-source" />
|
||
<col class="ledger-col-operator" />
|
||
<col class="ledger-col-logistics" />
|
||
<col class="ledger-col-remark" />
|
||
<col class="ledger-col-archive" />
|
||
</colgroup>
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>方向</th>
|
||
<th>类型</th>
|
||
<th>物料</th>
|
||
<th>库存批次号</th>
|
||
<th v-if="!activeWeightOnly">变动数量</th>
|
||
<th v-if="!activeQuantityOnly">变动重量(kg)</th>
|
||
<th>单价</th>
|
||
<th>金额</th>
|
||
<th>来源</th>
|
||
<th>经办人</th>
|
||
<th>运单/照片</th>
|
||
<th>备注</th>
|
||
<th>单据留档</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in warehouseLedgerRows" :key="row.inventory_txn_id">
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatDateTime(row.biz_time))">{{ formatDateTime(row.biz_time) }}</td>
|
||
<td><span class="status-pill" :class="warehouseLedgerDirectionClass(row.direction)">{{ formatWarehouseLedgerDirection(row.direction) }}</span></td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatInventoryTxnTypeLabel(row.txn_type))">{{ formatInventoryTxnTypeLabel(row.txn_type) }}</td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatWarehouseLedgerItemLabel(row))">{{ formatWarehouseLedgerItemLabel(row) }}</td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatWarehouseLedgerLotNo(row))">{{ formatWarehouseLedgerLotNo(row) }}</td>
|
||
<td v-if="!activeWeightOnly">{{ formatQty(row.qty_change) }}</td>
|
||
<td v-if="!activeQuantityOnly">{{ formatWeight(row.weight_change_kg) }}</td>
|
||
<td>¥{{ formatAmount(row.unit_cost) }}</td>
|
||
<td>¥{{ formatAmount(row.amount) }}</td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatWarehouseLedgerSourceLabel(row))">{{ formatWarehouseLedgerSourceLabel(row) }}</td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(row.operator_name || '-')">{{ row.operator_name || "-" }}</td>
|
||
<td v-bind="warehouseLedgerTooltipAttrs(formatWarehouseLedgerLogisticsLabel(row))">
|
||
<span>{{ row.logistics_waybill_no || "-" }}</span>
|
||
<button v-if="row.logistics_photo_url" class="link-button" type="button" @click="openLogisticsPhoto(row.logistics_photo_url)">查看</button>
|
||
</td>
|
||
<td class="warehouse-ledger-remark-cell" v-bind="warehouseLedgerTooltipAttrs(row.remark || '-')">{{ row.remark || "-" }}</td>
|
||
<td class="warehouse-ledger-archive-cell">
|
||
<DocumentArchiveActions
|
||
:status="row.archive_status || '未生成'"
|
||
@preview="previewWarehouseLedgerArchive(row)"
|
||
@download="downloadWarehouseLedgerArchive(row)"
|
||
@regenerate="regenerateWarehouseLedgerArchive(row)"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!warehouseLedgerRows.length">
|
||
<td :colspan="warehouseLedgerEmptyColspan" class="empty-row">
|
||
{{ warehouseLedgerLoading ? "流水加载中..." : `暂无${activeInventoryLabel}出入库流水` }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<PaginationBar
|
||
v-model:page="warehouseLedgerPage"
|
||
v-model:page-size="warehouseLedgerPageSize"
|
||
:total="warehouseLedgerTotal"
|
||
/>
|
||
</div>
|
||
</FormDrawer>
|
||
|
||
<FormDrawer
|
||
:open="productionWorkOrderInboundDrawerOpen"
|
||
eyebrow="嘉恒仓库 · 生产台账入库"
|
||
title="生产台账入库"
|
||
description="按在生产的材料库存批次号统一办理成品入库、生产余料入库和生产废料入库。"
|
||
tag="材料批次闭环"
|
||
wide
|
||
@close="closeProductionWorkOrderInboundDrawer"
|
||
>
|
||
<WarehouseDocumentFormShell
|
||
class="warehouse-document-operation-form"
|
||
title="生产台账入库结算单"
|
||
document-no="保存后生成"
|
||
:business-date="new Date()"
|
||
@submit="submitProductionWorkOrderInbound"
|
||
>
|
||
<div v-if="feedbackMessage" class="feedback-banner">{{ feedbackMessage }}</div>
|
||
<div v-if="errorMessage" class="feedback-banner error-banner">{{ errorMessage }}</div>
|
||
<WarehouseDocumentSection index="01" title="生产台账入库结算信息">
|
||
<div class="document-inline-summary production-ledger-inbound-tip">
|
||
选择生产台账后,系统按领料重量、单件毛重和已入库成品数量填入理论余料、理论废料。理论值只是辅助参考,实际称重可修改;偏差超过上下15%时需要填写说明。
|
||
</div>
|
||
|
||
<div class="document-control-grid production-ledger-inbound-paper-grid">
|
||
<label class="document-paper-field document-paper-field-wide">
|
||
<span>生产台账</span>
|
||
<select v-model.number="selectedInboundWorkOrderId" required @change="loadProductionWorkOrderInboundPreview">
|
||
<option disabled value="">请选择在生产的生产台账</option>
|
||
<option v-for="workOrder in openProductionWorkOrders" :key="workOrder.work_order_id" :value="workOrder.work_order_id">
|
||
{{ formatProductionWorkOrderInboundOption(workOrder) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="productionWorkOrderInboundPreview" class="warehouse-paper-summary-grid production-ledger-inbound-summary">
|
||
<article class="warehouse-paper-summary-card warehouse-paper-summary-card-wide"><span>生产台账</span><strong>{{ productionWorkOrderInboundPreview.material_lot_no }} · {{ productionWorkOrderInboundPreview.product_name }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>产品</span><strong>{{ productionWorkOrderInboundPreview.product_name }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>材料库存批次号</span><strong>{{ productionWorkOrderInboundPreview.material_lot_no }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>累计领料重量</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.total_issued_weight_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>库外未闭环重量</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.outside_weight_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>参考成品数量</span><strong>{{ formatQty(productionWorkOrderInboundPreview.reference_finished_qty) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>单件毛重</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.gross_weight_per_piece_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>单件净重</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.net_weight_per_piece_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>已入库成品</span><strong>{{ formatQty(productionWorkOrderInboundPreview.finished_inbound_qty) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>已入库废料</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.scrap_inbound_weight_kg) }}</strong></article>
|
||
</div>
|
||
|
||
<div class="document-control-grid production-ledger-inbound-paper-grid">
|
||
<label class="document-paper-field">
|
||
<span>成品入库数量</span>
|
||
<input
|
||
v-model.number="productionWorkOrderInboundForm.finished_qty"
|
||
type="number"
|
||
min="0"
|
||
step="1"
|
||
@input="handleProductionWorkOrderInboundFinishedQtyInput"
|
||
/>
|
||
</label>
|
||
<label class="document-paper-field">
|
||
<span>余料入库重量(kg)</span>
|
||
<input v-model.number="productionWorkOrderInboundForm.surplus_weight_kg" type="number" min="0" step="0.001" />
|
||
</label>
|
||
<label class="document-paper-field">
|
||
<span>废料重量(kg)</span>
|
||
<input v-model.number="productionWorkOrderInboundForm.scrap_weight_kg" type="number" min="0" step="0.001" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="productionWorkOrderInboundPreview" class="warehouse-paper-summary-grid production-ledger-inbound-metrics">
|
||
<article class="warehouse-paper-summary-card"><span>理论余料</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.theoretical_surplus_weight_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>理论废料</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.theoretical_scrap_weight_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>已归还余料</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.surplus_inbound_weight_kg) }}</strong></article>
|
||
<article class="warehouse-paper-summary-card"><span>库外未闭环重量</span><strong>{{ formatWeight(productionWorkOrderInboundPreview.outside_weight_kg) }}</strong></article>
|
||
</div>
|
||
|
||
<div class="warehouse-paper-settle-panel production-ledger-inbound-settle-panel">
|
||
<label class="warehouse-paper-settle-toggle">
|
||
<input v-model="productionWorkOrderInboundForm.settle_work_order" type="checkbox" />
|
||
<span class="settle-dot" aria-hidden="true"></span>
|
||
<strong>本次入库后该批材料结单</strong>
|
||
</label>
|
||
</div>
|
||
<div class="document-control-grid production-ledger-inbound-paper-grid production-ledger-inbound-remark">
|
||
<label class="document-paper-field document-paper-field-full">
|
||
<span>备注</span>
|
||
<textarea v-model.trim="productionWorkOrderInboundForm.remark" rows="3"></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
</WarehouseDocumentSection>
|
||
|
||
<WarehouseDocumentActionBar>
|
||
<button class="primary-button" type="submit" :disabled="submittingOperation">
|
||
{{ submittingOperation ? "保存中..." : "保存生产台账入库" }}
|
||
</button>
|
||
</WarehouseDocumentActionBar>
|
||
</WarehouseDocumentFormShell>
|
||
</FormDrawer>
|
||
|
||
<StocktakeDialog
|
||
:open="stocktakeDialogOpen"
|
||
:warehouses="warehouses"
|
||
:default-warehouse-id="activeWarehouseId"
|
||
@close="closeStocktakeDialog"
|
||
@changed="loadAll"
|
||
@message="handleStocktakeMessage"
|
||
@error="handleStocktakeError"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||
|
||
import AppIcon from "../components/AppIcon.vue";
|
||
import DocumentArchiveActions from "../components/documentForms/DocumentArchiveActions.vue";
|
||
import WarehouseDocumentActionBar from "../components/documentForms/WarehouseDocumentActionBar.vue";
|
||
import WarehouseDocumentFormShell from "../components/documentForms/WarehouseDocumentFormShell.vue";
|
||
import WarehouseDocumentSection from "../components/documentForms/WarehouseDocumentSection.vue";
|
||
import DrawerDataTable from "../components/DrawerDataTable.vue";
|
||
import FormDrawer from "../components/FormDrawer.vue";
|
||
import PaginationBar from "../components/PaginationBar.vue";
|
||
import StockLotTagSelect from "../components/StockLotTagSelect.vue";
|
||
import StocktakeDialog from "../components/StocktakeDialog.vue";
|
||
import TableControls from "../components/TableControls.vue";
|
||
import { useBodyScrollLock } from "../composables/useBodyScrollLock";
|
||
import { downloadResource, fetchResource, openResource, postResource, requestJson, uploadResource } from "../services/api";
|
||
import { PERSONNEL_PERMISSION_CODES, employeeOptionLabel, fetchPermissionEmployeeOptions } from "../services/employeeOptions";
|
||
import {
|
||
formatInventoryTxnTypeLabel,
|
||
formatItemTypeLabel,
|
||
formatProductLabel,
|
||
formatQualityStatusLabel,
|
||
formatSourceDocTypeLabel,
|
||
formatStatusLabel,
|
||
formatWarehouseTypeLabel
|
||
} from "../utils/dictionaries";
|
||
import { formatAmount, formatDateTime, formatQty, formatWeight } from "../utils/formatters";
|
||
import { buildGenericItemOptions, buildProductOperationOptions } from "../utils/inventoryOperationOptions";
|
||
import { usePagination } from "../utils/pagination";
|
||
import { formatReturnDeliveryLineLabel, formatReturnReworkItemLabel, formatReturnTraceSummary } from "../utils/returnTraceFormatters";
|
||
import { specialAdjustmentDirectionLabel, specialAdjustmentMeasureMode } from "../utils/specialAdjustmentRules";
|
||
import { useTableControls } from "../utils/tableControls";
|
||
|
||
const WAREHOUSE_TYPE_BY_TAB = {
|
||
raw: "RAW",
|
||
semi: "SEMI",
|
||
finished: "FINISHED",
|
||
auxiliary: "AUX",
|
||
scrap: "SCRAP",
|
||
return: "RETURN"
|
||
};
|
||
|
||
const WAREHOUSE_LABEL_BY_TAB = {
|
||
raw: "原材料库",
|
||
semi: "半成品库",
|
||
finished: "成品库",
|
||
auxiliary: "辅料库",
|
||
scrap: "废料库",
|
||
return: "退货库"
|
||
};
|
||
|
||
const SEMI_MEASURE_BASIS_LABELS = {
|
||
PIECE: "按件",
|
||
WEIGHT: "按重",
|
||
MIXED: "历史混合"
|
||
};
|
||
|
||
const LOGISTICS_REQUIRED_BIZ_TYPES = new Set([
|
||
"CUSTOMER_SUPPLIED",
|
||
"OUTSOURCING_SURPLUS",
|
||
"OUTSOURCING_IN",
|
||
"OUTSOURCING_SCRAP_IN",
|
||
"OUTSOURCING_OUT",
|
||
"SCRAP_SALE_OUT"
|
||
]);
|
||
const SOURCE_LOT_RETURN_INBOUND_BIZ_TYPES = new Set(["PRODUCTION_SURPLUS", "OUTSOURCING_SURPLUS"]);
|
||
const SOURCE_LOT_SELECTED_OUTBOUND_BIZ_TYPES = new Set(["OUTSOURCING_OUT", "SCRAP_OUT", "SCRAP_SALE_OUT"]);
|
||
const WAREHOUSE_OPERATION_DOCUMENT_TITLES = {
|
||
OPENING: "期初入库单",
|
||
CUSTOMER_SUPPLIED: "客料入库单",
|
||
PRODUCTION_SURPLUS: "生产余料入库单",
|
||
OUTSOURCING_SURPLUS: "委外余料入库单",
|
||
PRODUCTION_OUT: "生产出库单",
|
||
OUTSOURCING_OUT: "委外出库单",
|
||
PURCHASE_RETURN_OUT: "退货出库单",
|
||
SCRAP_OUT: "报废出库单",
|
||
WIP_IN: "产中入库单",
|
||
WIP_OUT: "产中出库单",
|
||
OUTSOURCING_IN: "委外入库单",
|
||
PRODUCTION_FINISHED: "生产入库单",
|
||
FG_IN: "生产入库单",
|
||
REWORK_FINISHED_IN: "返工入库单",
|
||
SALES_OUT: "销售出库单",
|
||
PRODUCTION_SCRAP_IN: "生产废料入库单",
|
||
REWORK_SCRAP_IN: "返工废料入库单",
|
||
OUTSOURCING_SCRAP_IN: "委外废料入库单",
|
||
RETURN_SCRAP_IN: "退货废料入库单",
|
||
SCRAP_SALE_OUT: "售卖出库单",
|
||
RETURN_IN: "退货入库单",
|
||
RETURN_OUT: "返工出库单",
|
||
SPECIAL_IN: "特殊入库单",
|
||
SPECIAL_OUT: "特殊出库单"
|
||
};
|
||
|
||
const lots = ref([]);
|
||
const balances = ref([]);
|
||
const transactions = ref([]);
|
||
const materials = ref([]);
|
||
const products = ref([]);
|
||
const boms = ref([]);
|
||
const processRoutes = ref([]);
|
||
const workOrders = ref([]);
|
||
const workOrderLedgerRows = ref([]);
|
||
const productionLedgerRows = ref([]);
|
||
const productionStockLotOptions = ref([]);
|
||
const selectedProductionStockLots = ref([]);
|
||
const genericSourceLotOptions = ref([]);
|
||
const selectedGenericSourceLots = ref([]);
|
||
const rawReturnPurchaseLinks = ref([]);
|
||
const warehouses = ref([]);
|
||
const locations = ref([]);
|
||
const employees = ref([]);
|
||
const customers = ref([]);
|
||
const salesOrders = ref([]);
|
||
const salesOrderItems = ref([]);
|
||
const deliveries = ref([]);
|
||
const deliveryItems = ref([]);
|
||
const returnItems = ref([]);
|
||
const selectedItemId = ref(null);
|
||
const selectedBalance = ref(null);
|
||
const activeInventoryTab = ref("raw");
|
||
const activeOperation = ref(null);
|
||
const detailDrawerOpen = ref(false);
|
||
const expandedLotRows = ref([]);
|
||
const expandedTxnRows = ref([]);
|
||
const genericDrawerOpen = ref(false);
|
||
const specialAdjustmentDrawerOpen = ref(false);
|
||
const rawReturnDrawerOpen = ref(false);
|
||
const productionDrawerOpen = ref(false);
|
||
const completionDrawerOpen = ref(false);
|
||
const deliveryDrawerOpen = ref(false);
|
||
const returnInboundDrawerOpen = ref(false);
|
||
const returnScrapInboundDrawerOpen = ref(false);
|
||
const returnReworkDrawerOpen = ref(false);
|
||
const stocktakeDialogOpen = ref(false);
|
||
const openingImportDrawerOpen = ref(false);
|
||
const warehouseLedgerDrawerOpen = ref(false);
|
||
const productionWorkOrderInboundDrawerOpen = ref(false);
|
||
const warehouseLedgerLoading = ref(false);
|
||
const warehouseLedgerExporting = ref(false);
|
||
const warehouseLedgerRows = ref([]);
|
||
const warehouseLedgerTotal = ref(0);
|
||
const warehouseLedgerPage = ref(1);
|
||
const warehouseLedgerPageSize = ref(10);
|
||
const warehouseLedgerTableRef = ref(null);
|
||
const openingImportBusy = ref(false);
|
||
const openingImportInputRef = ref(null);
|
||
const logisticsPhotoUploading = ref(false);
|
||
const rawReturnLinkLoading = ref(false);
|
||
const feedbackMessage = ref("");
|
||
const errorMessage = ref("");
|
||
const lastWarehouseArchiveResult = ref(null);
|
||
const submittingOperation = ref(false);
|
||
const specialAdjustmentSubmitting = ref(false);
|
||
let productionStockLotOptionsRequestSeq = 0;
|
||
let genericSourceLotOptionsRequestSeq = 0;
|
||
let rawReturnLinksRequestSeq = 0;
|
||
let productionWorkOrderInboundPreviewRequestSeq = 0;
|
||
let warehouseLedgerTooltipTable = null;
|
||
let warehouseLedgerTooltipElement = null;
|
||
let warehouseLedgerTooltipCleanupHandlers = [];
|
||
|
||
const genericForm = reactive(buildEmptyGenericForm());
|
||
const specialAdjustmentForm = reactive(buildEmptySpecialAdjustmentForm());
|
||
const rawReturnForm = reactive(buildEmptyRawReturnForm());
|
||
const productionForm = reactive(buildEmptyProductionForm());
|
||
const productionWorkOrderInboundForm = reactive(buildEmptyProductionWorkOrderInboundForm());
|
||
const completionForm = reactive(buildEmptyCompletionForm());
|
||
const deliveryForm = reactive(buildEmptyDeliveryForm());
|
||
const returnInboundForm = reactive(buildEmptyReturnInboundForm());
|
||
const returnScrapInboundForm = reactive(buildEmptyReturnScrapInboundForm());
|
||
const returnReworkForm = reactive(buildEmptyReturnReworkForm());
|
||
const warehouseLedgerSummary = reactive(buildEmptyWarehouseLedgerSummary());
|
||
const warehouseLedgerFilters = reactive(buildEmptyWarehouseLedgerFilters());
|
||
const selectedInboundWorkOrderId = ref("");
|
||
const productionWorkOrderInboundPreview = ref(null);
|
||
|
||
const rawBalances = computed(() =>
|
||
balances.value.filter((item) => normalizeWarehouseType(item) === "RAW" || (!normalizeWarehouseType(item) && item.item_type === "RAW_MATERIAL"))
|
||
);
|
||
const auxiliaryBalances = computed(() => balances.value.filter((item) => normalizeWarehouseType(item) === "AUX"));
|
||
const scrapBalances = computed(() => balances.value.filter((item) => normalizeWarehouseType(item) === "SCRAP"));
|
||
const returnBalances = computed(() => aggregateItemBalances(balances.value.filter((item) => normalizeWarehouseType(item) === "RETURN"), "RETURN"));
|
||
const semiFinishedBalances = computed(() => aggregateSemiOperationBalances(lots.value.filter((item) => normalizeWarehouseType(item) === "SEMI")));
|
||
const finishedBalances = computed(() =>
|
||
aggregateItemBalances(
|
||
balances.value.filter((item) => normalizeWarehouseType(item) === "FINISHED" || (!normalizeWarehouseType(item) && item.item_type === "FINISHED_GOOD")),
|
||
"FINISHED"
|
||
)
|
||
);
|
||
|
||
const inventoryTabs = computed(() => [
|
||
{
|
||
key: "raw",
|
||
label: "原材料库",
|
||
count: filteredRawCount.value,
|
||
icon: "warehouseRaw",
|
||
iconClass: "inventory-tab-icon-raw",
|
||
iconPaths: ["M4 8.5 12 4l8 4.5-8 4.5-8-4.5Z", "m4 12 8 4.5 8-4.5", "m4 15.5 8 4.5 8-4.5"]
|
||
},
|
||
{
|
||
key: "semi",
|
||
label: "半成品库",
|
||
count: filteredSemiCount.value,
|
||
icon: "warehouseSemi",
|
||
iconClass: "inventory-tab-icon-semi",
|
||
iconPaths: ["M6 7h7.5a3.5 3.5 0 0 1 0 7H11", "M6 7v10h5", "M16 15.5 19 18l-3 2.5", "M19 18h-7"]
|
||
},
|
||
{
|
||
key: "finished",
|
||
label: "成品库",
|
||
count: filteredFinishedCount.value,
|
||
icon: "warehouseFinished",
|
||
iconClass: "inventory-tab-icon-finished",
|
||
iconPaths: ["M4 8.5 12 4l8 4.5v8.8L12 21l-8-3.7V8.5Z", "M4 8.5 12 13l8-4.5", "M12 13v8"]
|
||
},
|
||
{
|
||
key: "auxiliary",
|
||
label: "辅料库",
|
||
count: filteredAuxiliaryCount.value,
|
||
icon: "warehouseAuxiliary",
|
||
iconClass: "inventory-tab-icon-auxiliary",
|
||
iconPaths: ["M5 18.5 14.5 9", "m13 7 4-4 4 4-4 4-4-4Z", "M4 20h7", "M7.5 14.5 10 17", "M15.5 14.5h4", "M17.5 12.5v4"]
|
||
},
|
||
{
|
||
key: "scrap",
|
||
label: "废料库",
|
||
count: filteredScrapCount.value,
|
||
icon: "warehouseScrap",
|
||
iconClass: "inventory-tab-icon-scrap",
|
||
iconPaths: ["M6 7h12l-1 13H7L6 7Z", "M9 7V4h6v3", "M4 7h16", "M9.5 11v5", "M14.5 11v5"]
|
||
},
|
||
{
|
||
key: "return",
|
||
label: "退货库",
|
||
count: filteredReturnCount.value,
|
||
icon: "warehouseReturn",
|
||
iconClass: "inventory-tab-icon-return",
|
||
iconPaths: ["M7 7h10l2 4H5l2-4Z", "M5 11v7h14v-7", "M9 15h6", "M10 13l-2 2 2 2", "M8 15h8"]
|
||
}
|
||
]);
|
||
|
||
const activeInventoryTitle = computed(() => {
|
||
const titleMap = {
|
||
raw: "原材料库存台账与批次",
|
||
semi: "半成品暂存与委外流转",
|
||
finished: "成品库存与销售出库",
|
||
auxiliary: "辅料库存与生产消耗",
|
||
scrap: "废料库存与售卖出库",
|
||
return: "退货待返工库存"
|
||
};
|
||
return titleMap[activeInventoryTab.value] || titleMap.raw;
|
||
});
|
||
const activeInventoryTag = computed(() => {
|
||
return formatWarehouseTypeLabel(activeWarehouseType.value);
|
||
});
|
||
const activeInventoryLabel = computed(() => WAREHOUSE_LABEL_BY_TAB[activeInventoryTab.value] || "仓库");
|
||
const activeWarehouseType = computed(() => WAREHOUSE_TYPE_BY_TAB[activeInventoryTab.value] || "RAW");
|
||
const activeWeightOnly = computed(() => ["raw", "scrap"].includes(activeInventoryTab.value));
|
||
const activeQuantityOnly = computed(() => activeInventoryTab.value === "auxiliary");
|
||
const warehouseLedgerEmptyColspan = computed(() => (activeWeightOnly.value || activeQuantityOnly.value ? 13 : 14));
|
||
const selectedWeightOnly = computed(
|
||
() => ["RAW", "SCRAP"].includes(selectedWarehouseType.value)
|
||
);
|
||
const selectedQuantityOnly = computed(() => selectedWarehouseType.value === "AUX");
|
||
const activeItemColumnLabel = computed(() => {
|
||
if (activeInventoryTab.value === "semi") {
|
||
return "半成品";
|
||
}
|
||
if (["finished", "return"].includes(activeInventoryTab.value)) {
|
||
return "产品";
|
||
}
|
||
return "物料";
|
||
});
|
||
const activeSearchPlaceholder = computed(() => `搜索${activeInventoryLabel.value}${activeItemColumnLabel.value}编码、名称、规格、单位、仓库、库位`);
|
||
const activeWarehouses = computed(() => warehouses.value.filter((item) => normalizeWarehouseType(item) === activeWarehouseType.value));
|
||
const activeWarehouseId = computed(() => activeWarehouses.value[0]?.id || 0);
|
||
const finishedWarehouses = computed(() => warehouses.value.filter((item) => normalizeWarehouseType(item) === "FINISHED"));
|
||
const activeBalances = computed(() => {
|
||
const rowsByTab = {
|
||
raw: rawBalances.value,
|
||
semi: semiFinishedBalances.value,
|
||
finished: finishedBalances.value,
|
||
auxiliary: auxiliaryBalances.value,
|
||
scrap: scrapBalances.value,
|
||
return: returnBalances.value
|
||
};
|
||
return rowsByTab[activeInventoryTab.value] || rowsByTab.raw;
|
||
});
|
||
|
||
function normalizeSemiMeasureBasis(value) {
|
||
const normalized = String(value || "").toUpperCase();
|
||
return ["PIECE", "WEIGHT", "MIXED"].includes(normalized) ? normalized : "";
|
||
}
|
||
|
||
function semiMeasureBasisLabel(value) {
|
||
return SEMI_MEASURE_BASIS_LABELS[normalizeSemiMeasureBasis(value)] || "未确定";
|
||
}
|
||
|
||
function deriveSemiLotMeasureBasis(lot) {
|
||
const hasQty =
|
||
Number(lot?.inbound_qty || 0) > 0 ||
|
||
Number(lot?.remaining_qty || 0) > 0 ||
|
||
Number(lot?.locked_qty || 0) > 0 ||
|
||
Number(lot?.qty_on_hand || 0) > 0 ||
|
||
Number(lot?.qty_available || 0) > 0;
|
||
const hasWeight =
|
||
Number(lot?.inbound_weight_kg || 0) > 0 ||
|
||
Number(lot?.remaining_weight_kg || 0) > 0 ||
|
||
Number(lot?.locked_weight_kg || 0) > 0 ||
|
||
Number(lot?.weight_on_hand_kg || 0) > 0 ||
|
||
Number(lot?.weight_available_kg || 0) > 0;
|
||
if (hasQty && hasWeight) {
|
||
return "MIXED";
|
||
}
|
||
if (hasQty) {
|
||
return "PIECE";
|
||
}
|
||
if (hasWeight) {
|
||
return "WEIGHT";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function mergeSemiMeasureBasis(current, next) {
|
||
const normalizedCurrent = normalizeSemiMeasureBasis(current);
|
||
const normalizedNext = normalizeSemiMeasureBasis(next);
|
||
if (!normalizedCurrent) {
|
||
return normalizedNext;
|
||
}
|
||
if (!normalizedNext || normalizedCurrent === normalizedNext) {
|
||
return normalizedCurrent;
|
||
}
|
||
return "MIXED";
|
||
}
|
||
|
||
function deriveSemiMeasureBasisFromRows(rows) {
|
||
return rows.reduce((basis, row) => mergeSemiMeasureBasis(basis, row.measure_basis || deriveSemiLotMeasureBasis(row)), "");
|
||
}
|
||
|
||
function formatSemiMeasureValue(row, value, measureType) {
|
||
if (normalizeWarehouseType(row) !== "SEMI") {
|
||
return measureType === "PIECE" ? formatQty(value) : formatWeight(value);
|
||
}
|
||
const basis = normalizeSemiMeasureBasis(row?.measure_basis || deriveSemiLotMeasureBasis(row));
|
||
if (basis === "PIECE" && measureType === "WEIGHT") {
|
||
return "-";
|
||
}
|
||
if (basis === "WEIGHT" && measureType === "PIECE") {
|
||
return "-";
|
||
}
|
||
if (measureType === "PIECE") {
|
||
return formatQty(value);
|
||
}
|
||
return formatWeight(value);
|
||
}
|
||
|
||
function formatSemiAvailability(item) {
|
||
const basis = normalizeSemiMeasureBasis(item?.measure_basis);
|
||
if (basis === "PIECE") {
|
||
return `可用 ${formatQty(item?.qty_available)} 件`;
|
||
}
|
||
if (basis === "WEIGHT") {
|
||
return `可用 ${formatWeight(item?.weight_available_kg)}`;
|
||
}
|
||
if (basis === "MIXED") {
|
||
return `历史混合 · 可用 ${formatQty(item?.qty_available)} 件 / ${formatWeight(item?.weight_available_kg)}`;
|
||
}
|
||
return `可用 ${formatQty(item?.qty_available)} 件 / ${formatWeight(item?.weight_available_kg)}`;
|
||
}
|
||
|
||
const operationMatrix = computed(() => ({
|
||
raw: {
|
||
inbound: [
|
||
makeOperation("raw-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的初始材料。"),
|
||
makeOperation("raw-customer-in", "in", "CUSTOMER_SUPPLIED", "客料入库", "客户提供的原材料直接入原材料库,成本按0处理。"),
|
||
makeOperation("raw-production-surplus-in", "in", "PRODUCTION_SURPLUS", "生产余料入库", "生产结束后剩余原材料重新入库。"),
|
||
makeOperation("raw-outsourcing-surplus-in", "in", "OUTSOURCING_SURPLUS", "委外余料入库", "委外加工结束剩余退回的原材料重新入库。"),
|
||
makeOperation("raw-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整原材料库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("raw-production-out", "out", "PRODUCTION_OUT", "生产出库", "选择原材料库存批次出库,后续小程序报工按库存批次号推进。", "production"),
|
||
makeOperation("raw-outsourcing-out", "out", "OUTSOURCING_OUT", "委外出库", "原材料出库给第三方加工。"),
|
||
makeOperation("raw-return-out", "out", "PURCHASE_RETURN_OUT", "退货出库", "入仓后发现质量或性能不合格的原材料退回供应商。", "rawReturn"),
|
||
makeOperation("raw-scrap-out", "out", "SCRAP_OUT", "报废出库", "原材料报废出库。"),
|
||
makeOperation("raw-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减原材料库库存明细。", "specialAdjustment")
|
||
]
|
||
},
|
||
semi: {
|
||
inbound: [
|
||
makeOperation("semi-wip-in", "in", "WIP_IN", "产中入库", "生产过程中需要暂时入库的半成品。"),
|
||
makeOperation("semi-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的初始半成品。"),
|
||
makeOperation("semi-outsourcing-in", "in", "OUTSOURCING_IN", "委外入库", "委外某道工序完成后入库,等待后续继续加工。"),
|
||
makeOperation("semi-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整半成品库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("semi-wip-out", "out", "WIP_OUT", "产中出库", "暂存半成品出库,继续完成后续工序。"),
|
||
makeOperation("semi-outsourcing-out", "out", "OUTSOURCING_OUT", "委外出库", "半成品出库委外加工后续工序。"),
|
||
makeOperation("semi-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减半成品库库存明细。", "specialAdjustment")
|
||
]
|
||
},
|
||
finished: {
|
||
inbound: [
|
||
makeOperation("finished-production-in", "in", "PRODUCTION_FINISHED", "生产入库", "按生产台账办理生产完工成品入库,并可同步办理余料、废料入库。", "productionLedgerInbound"),
|
||
makeOperation("finished-rework-in", "in", "REWORK_FINISHED_IN", "返工入库", "退货返工完成后的成品回库。", "reworkCompletion"),
|
||
makeOperation("finished-outsourcing-in", "in", "OUTSOURCING_IN", "委外入库", "委外加工完成后的成品入库。"),
|
||
makeOperation("finished-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的初始成品。"),
|
||
makeOperation("finished-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整成品库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("finished-sales-out", "out", "SALES_OUT", "销售出库", "按销售订单进行成品发货出库。", "delivery"),
|
||
makeOperation("finished-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减成品库库存明细。", "specialAdjustment")
|
||
]
|
||
},
|
||
auxiliary: {
|
||
inbound: [
|
||
makeOperation("aux-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的初始辅料。"),
|
||
makeOperation("aux-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整辅料库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("aux-production-out", "out", "PRODUCTION_OUT", "生产出库", "辅料用于生产消耗的出库。"),
|
||
makeOperation("aux-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减辅料库库存明细。", "specialAdjustment")
|
||
]
|
||
},
|
||
scrap: {
|
||
inbound: [
|
||
makeOperation("scrap-production-in", "in", "PRODUCTION_SCRAP_IN", "生产废料入库", "生产过程产生的废料入库。"),
|
||
makeOperation("scrap-rework-in", "in", "REWORK_SCRAP_IN", "返工废料入库", "退货返工失败产生的废品入废料库。", "reworkScrapInbound"),
|
||
makeOperation("scrap-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的初始废料。"),
|
||
makeOperation("scrap-outsourcing-in", "in", "OUTSOURCING_SCRAP_IN", "委外废料入库", "委外加工产生的废料入库。"),
|
||
makeOperation("scrap-return-in", "in", "RETURN_SCRAP_IN", "退货废料入库", "客户退回后判定报废的产品直接进入废料库。", "returnScrapInbound"),
|
||
makeOperation("scrap-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整废料库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("scrap-sale-out", "out", "SCRAP_SALE_OUT", "售卖出库", "废料称重售卖出库。"),
|
||
makeOperation("scrap-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减废料库库存明细。", "specialAdjustment")
|
||
]
|
||
},
|
||
return: {
|
||
inbound: [
|
||
makeOperation("return-opening-in", "in", "OPENING", "期初入库", "仓库初始化时导入的历史退货待返工品。"),
|
||
makeOperation("return-customer-in", "in", "RETURN_IN", "退货入库", "客户退回且判定可返工的产品入退货库。", "returnInbound"),
|
||
makeOperation("return-special-in", "in", "SPECIAL_IN", "特殊入库", "强制填写说明后,直接调整退货库库存明细。", "specialAdjustment")
|
||
],
|
||
outbound: [
|
||
makeOperation("return-rework-out", "out", "RETURN_OUT", "返工出库", "退货库待返工品出库并生成返工处置。", "returnRework"),
|
||
makeOperation("return-special-out", "out", "SPECIAL_OUT", "特殊出库", "强制填写说明后,直接扣减退货库库存明细。", "specialAdjustment")
|
||
]
|
||
}
|
||
}));
|
||
const activeInboundOperations = computed(() => operationMatrix.value[activeInventoryTab.value]?.inbound || []);
|
||
const activeOutboundOperations = computed(() => operationMatrix.value[activeInventoryTab.value]?.outbound || []);
|
||
const productOperationOptions = computed(() => buildProductOperationOptions(processRoutes.value));
|
||
const isSemiWarehouseOperation = computed(() => activeWarehouseType.value === "SEMI");
|
||
const genericItemFieldLabel = computed(() => {
|
||
if (isSemiWarehouseOperation.value) {
|
||
return "半成品";
|
||
}
|
||
if (isScrapSaleOutbound.value) {
|
||
return "废料";
|
||
}
|
||
if (isOutsourcingScrapInbound.value || isReworkScrapInbound.value || ["finished", "return"].includes(activeInventoryTab.value)) {
|
||
return "产品";
|
||
}
|
||
return "物料";
|
||
});
|
||
const genericItemPlaceholder = computed(() => `请选择${genericItemFieldLabel.value}`);
|
||
|
||
const baseGenericItemOptions = computed(() => {
|
||
if (isOutsourcingScrapInbound.value || isReworkScrapInbound.value) {
|
||
return products.value;
|
||
}
|
||
return buildGenericItemOptions({
|
||
direction: activeOperation.value?.direction || "in",
|
||
activeInventoryTab: activeInventoryTab.value,
|
||
activeWarehouseType: activeWarehouseType.value,
|
||
warehouseId: genericForm.warehouse_id,
|
||
balances: activeInventoryTab.value === "semi" ? semiFinishedBalances.value : balances.value,
|
||
materials: materials.value,
|
||
products: products.value,
|
||
productOperations: productOperationOptions.value
|
||
});
|
||
});
|
||
const genericItemOptions = computed(() => {
|
||
if (!isProductionWorkOrderControlledInbound.value || !selectedGenericWorkOrder.value) {
|
||
return baseGenericItemOptions.value;
|
||
}
|
||
if (isReworkScrapInbound.value) {
|
||
const product = products.value.find((item) => Number(item.item_id) === Number(selectedGenericWorkOrder.value.product_item_id));
|
||
return product
|
||
? [{ ...product, option_key: String(product.item_id), item_id: Number(product.item_id) }]
|
||
: [{
|
||
option_key: String(selectedGenericWorkOrder.value.product_item_id),
|
||
item_id: Number(selectedGenericWorkOrder.value.product_item_id),
|
||
item_code: selectedGenericWorkOrder.value.product_code || "",
|
||
item_name: selectedGenericWorkOrder.value.product_name || ""
|
||
}];
|
||
}
|
||
const optionsByMaterialId = new Map(
|
||
baseGenericItemOptions.value.map((item) => [Number(item.item_id || 0), item])
|
||
);
|
||
const seen = new Set();
|
||
return (selectedGenericWorkOrder.value.materials || [])
|
||
.map((material) => {
|
||
const materialItemId = Number(material.material_item_id || 0);
|
||
if (!materialItemId || seen.has(materialItemId)) {
|
||
return null;
|
||
}
|
||
seen.add(materialItemId);
|
||
return {
|
||
...(optionsByMaterialId.get(materialItemId) || {}),
|
||
option_key: String(materialItemId),
|
||
item_id: materialItemId,
|
||
item_code: material.material_code || optionsByMaterialId.get(materialItemId)?.item_code || "",
|
||
item_name: material.material_name || optionsByMaterialId.get(materialItemId)?.item_name || "",
|
||
unit_cost: Number(material.unit_cost || 0)
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
});
|
||
const selectedGenericItemOption = computed(() =>
|
||
genericItemOptions.value.find((item) => String(item.option_key || item.item_id) === String(genericForm.item_option_key || "")) || null
|
||
);
|
||
const selectedSemiExistingMeasureBasis = computed(() => {
|
||
if (activeWarehouseType.value !== "SEMI") {
|
||
return "";
|
||
}
|
||
const itemId = Number(genericForm.item_id || 0);
|
||
const warehouseId = Number(genericForm.warehouse_id || 0);
|
||
const sourceLineId = Number(genericForm.source_line_id || 0);
|
||
if (!itemId || !warehouseId || !sourceLineId) {
|
||
return "";
|
||
}
|
||
return deriveSemiMeasureBasisFromRows(
|
||
lots.value.filter(
|
||
(lot) =>
|
||
Number(lot.item_id || 0) === itemId &&
|
||
Number(lot.warehouse_id || 0) === warehouseId &&
|
||
Number(lot.source_line_id || 0) === sourceLineId &&
|
||
(Number(lot.remaining_qty || 0) > 0 || Number(lot.remaining_weight_kg || 0) > 0)
|
||
)
|
||
);
|
||
});
|
||
const selectedSemiMeasureBasis = computed(() => {
|
||
if (isSemiSourceLotTraceInbound.value) {
|
||
return normalizeSemiMeasureBasis(genericForm.measure_basis || selectedSemiExistingMeasureBasis.value);
|
||
}
|
||
if (isSemiAutoSourceOutbound.value) {
|
||
return normalizeSemiMeasureBasis(selectedGenericItemOption.value?.measure_basis || selectedSemiExistingMeasureBasis.value);
|
||
}
|
||
return "";
|
||
});
|
||
const isSemiMeasureBasisLocked = computed(() =>
|
||
isSemiSourceLotTraceInbound.value && ["PIECE", "WEIGHT"].includes(normalizeSemiMeasureBasis(selectedSemiExistingMeasureBasis.value))
|
||
);
|
||
const showGenericWeightInput = computed(() => {
|
||
if (activeWarehouseType.value === "AUX") {
|
||
return false;
|
||
}
|
||
if (isOutsourcingScrapInbound.value) {
|
||
return true;
|
||
}
|
||
if (isReworkScrapInbound.value) {
|
||
return true;
|
||
}
|
||
if (!isSemiQuantityWeightFlexibleOperation.value) {
|
||
return !usesGenericSourceLotSelector.value;
|
||
}
|
||
return selectedSemiMeasureBasis.value === "WEIGHT";
|
||
});
|
||
const showGenericQtyInput = computed(() =>
|
||
activeWarehouseType.value === "AUX" ||
|
||
isReworkScrapInbound.value ||
|
||
isFinishedOutsourcingInbound.value ||
|
||
(isSemiQuantityWeightFlexibleOperation.value && selectedSemiMeasureBasis.value === "PIECE")
|
||
);
|
||
const genericSourceLotMaterialIds = computed(() => {
|
||
if (isScrapSaleOutbound.value) {
|
||
const scrapItemId = Number(genericForm.item_id || 0);
|
||
return scrapItemId ? [scrapItemId] : [];
|
||
}
|
||
if (isProductionSurplusInbound.value && selectedGenericWorkOrder.value) {
|
||
return (selectedGenericWorkOrder.value.materials || []).map((item) => item.material_item_id);
|
||
}
|
||
if (isProductTraceOnlySourceLotInbound.value || isOutsourcingScrapInbound.value) {
|
||
return getProductBomMaterialItemIds(genericForm.item_id);
|
||
}
|
||
const materialItemId = Number(genericForm.item_id || 0);
|
||
return materialItemId ? [materialItemId] : [];
|
||
});
|
||
const semiAutoSourceOutboundSourceLots = computed(() => {
|
||
if (!isSemiAutoSourceOutbound.value) {
|
||
return [];
|
||
}
|
||
const itemId = Number(genericForm.item_id || 0);
|
||
const warehouseId = Number(genericForm.warehouse_id || 0);
|
||
const sourceLineId = Number(genericForm.source_line_id || 0);
|
||
if (!itemId || !warehouseId || !sourceLineId) {
|
||
return [];
|
||
}
|
||
return lots.value
|
||
.filter(
|
||
(lot) =>
|
||
Number(lot.item_id || 0) === itemId &&
|
||
Number(lot.warehouse_id || 0) === warehouseId &&
|
||
Number(lot.source_line_id || 0) === sourceLineId &&
|
||
String(lot.status || "").toUpperCase() === "AVAILABLE" &&
|
||
(Number(lot.remaining_qty || 0) > 0 || Number(lot.remaining_weight_kg || 0) > 0)
|
||
)
|
||
.sort((left, right) => String(left.lot_no || "").localeCompare(String(right.lot_no || ""), "zh-CN", { numeric: true }));
|
||
});
|
||
const semiAutoSourceOutboundSourceLotSummary = computed(() =>
|
||
semiAutoSourceOutboundSourceLots.value.map((lot) => lot.lot_no).filter(Boolean).join("、")
|
||
);
|
||
const semiAutoSourceOutboundAvailableQty = computed(() =>
|
||
semiAutoSourceOutboundSourceLots.value.reduce((total, lot) => total + Number(lot.remaining_qty || 0), 0)
|
||
);
|
||
const semiAutoSourceOutboundAvailableWeight = computed(() =>
|
||
semiAutoSourceOutboundSourceLots.value.reduce((total, lot) => total + Number(lot.remaining_weight_kg || 0), 0)
|
||
);
|
||
const genericLocations = computed(() => locations.value.filter((item) => item.warehouse_id === Number(genericForm.warehouse_id)));
|
||
const completionLocations = computed(() => locations.value.filter((item) => item.warehouse_id === Number(completionForm.warehouse_id)));
|
||
const specialAdjustmentDirectionText = computed(() => specialAdjustmentDirectionLabel(specialAdjustmentForm.direction));
|
||
const specialAdjustmentItemColumnLabel = computed(() => (specialAdjustmentForm.direction === "IN" ? "入库方式 / 明细" : "库存明细"));
|
||
const specialAdjustmentCanShowMeasureBasis = computed(() => activeWarehouseType.value === "SEMI");
|
||
const specialAdjustmentCanShowQty = computed(() => !["RAW", "SCRAP"].includes(activeWarehouseType.value));
|
||
const specialAdjustmentCanShowWeight = computed(() => activeWarehouseType.value !== "AUX");
|
||
const specialAdjustmentLotOptions = computed(() => {
|
||
const warehouseId = Number(specialAdjustmentForm.warehouse_id || 0);
|
||
if (!warehouseId) {
|
||
return [];
|
||
}
|
||
return lots.value
|
||
.filter((lot) => Number(lot.warehouse_id || 0) === warehouseId && normalizeWarehouseType(lot) === activeWarehouseType.value)
|
||
.sort(compareInventoryLots);
|
||
});
|
||
const specialAdjustmentItemOptions = computed(() => {
|
||
if (activeWarehouseType.value === "SEMI") {
|
||
return productOperationOptions.value.length ? productOperationOptions.value : products.value;
|
||
}
|
||
return buildGenericItemOptions({
|
||
direction: "in",
|
||
activeInventoryTab: activeInventoryTab.value,
|
||
activeWarehouseType: activeWarehouseType.value,
|
||
warehouseId: specialAdjustmentForm.warehouse_id,
|
||
balances: activeInventoryTab.value === "semi" ? semiFinishedBalances.value : balances.value,
|
||
materials: materials.value,
|
||
products: products.value,
|
||
productOperations: productOperationOptions.value
|
||
});
|
||
});
|
||
const rawReturnLotOptions = computed(() =>
|
||
lots.value
|
||
.filter(
|
||
(item) =>
|
||
(normalizeWarehouseType(item) === "RAW" || item.item_type === "RAW_MATERIAL") &&
|
||
item.status === "AVAILABLE" &&
|
||
item.quality_status === "PASS" &&
|
||
rawReturnLotAvailableWeight(item) > 0
|
||
)
|
||
.sort((left, right) => {
|
||
const leftKey = left.lot_no || "";
|
||
const rightKey = right.lot_no || "";
|
||
return leftKey.localeCompare(rightKey, "zh-CN", { numeric: true }) || Number(left.lot_id) - Number(right.lot_id);
|
||
})
|
||
);
|
||
const selectedRawReturnLot = computed(() =>
|
||
rawReturnLotOptions.value.find((item) => Number(item.lot_id) === Number(rawReturnForm.lot_id)) || null
|
||
);
|
||
const selectedRawReturnLink = computed(() =>
|
||
rawReturnPurchaseLinks.value.find((item) => Number(item.purchase_order_item_id) === Number(rawReturnForm.purchase_order_item_id)) || null
|
||
);
|
||
const selectedRawReturnLotAvailableWeight = computed(() => rawReturnLotAvailableWeight(selectedRawReturnLot.value));
|
||
const rawReturnMaxWeight = computed(() => {
|
||
const lotAvailableWeight = selectedRawReturnLotAvailableWeight.value;
|
||
if (!selectedRawReturnLink.value) {
|
||
return lotAvailableWeight;
|
||
}
|
||
return Math.min(lotAvailableWeight, Math.max(Number(selectedRawReturnLink.value.returnable_weight_kg || 0), 0));
|
||
});
|
||
const deliveryById = computed(() => new Map(deliveries.value.map((item) => [Number(item.delivery_id), item])));
|
||
const returnedTotalsByDeliveryItem = computed(() => {
|
||
const map = new Map();
|
||
returnItems.value.forEach((item) => {
|
||
const deliveryItemId = Number(item.delivery_item_id || 0);
|
||
if (!deliveryItemId) {
|
||
return;
|
||
}
|
||
const current = map.get(deliveryItemId) || { qty: 0, weight: 0 };
|
||
current.qty += Number(item.return_qty || 0);
|
||
current.weight += Number(item.return_weight_kg || 0);
|
||
map.set(deliveryItemId, current);
|
||
});
|
||
return map;
|
||
});
|
||
const deliveryLineOptions = computed(() =>
|
||
deliveryItems.value
|
||
.map((item) => {
|
||
const delivery = deliveryById.value.get(Number(item.delivery_id)) || {};
|
||
const returned = returnedTotalsByDeliveryItem.value.get(Number(item.delivery_item_id)) || { qty: 0, weight: 0 };
|
||
return {
|
||
...item,
|
||
customer_id: delivery.customer_id,
|
||
customer_name: delivery.customer_name,
|
||
returnable_qty: Math.max(Number(item.delivery_qty || 0) - returned.qty, 0),
|
||
returnable_weight_kg: Math.max(Number(item.delivery_weight_kg || 0) - returned.weight, 0)
|
||
};
|
||
})
|
||
.filter((item) => String(item.status || "").toUpperCase() === "POSTED" && item.returnable_qty > 0)
|
||
);
|
||
const selectedReturnInboundDeliveryLine = computed(() =>
|
||
deliveryLineOptions.value.find((item) => Number(item.delivery_item_id) === Number(returnInboundForm.delivery_item_id)) || null
|
||
);
|
||
const selectedReturnScrapDeliveryLine = computed(() =>
|
||
deliveryLineOptions.value.find((item) => Number(item.delivery_item_id) === Number(returnScrapInboundForm.delivery_item_id)) || null
|
||
);
|
||
const pendingReturnItems = computed(() =>
|
||
returnItems.value.filter((item) => String(item.disposition_status || "").toUpperCase() === "PENDING")
|
||
);
|
||
const selectedReturnReworkItem = computed(() =>
|
||
pendingReturnItems.value.find((item) => Number(item.return_item_id) === Number(returnReworkForm.return_item_id)) || null
|
||
);
|
||
|
||
function getProductBomMaterialItemIds(productItemId) {
|
||
const id = Number(productItemId || 0);
|
||
if (!id) {
|
||
return [];
|
||
}
|
||
const activeBoms = boms.value
|
||
.filter((item) => Number(item.product_item_id || 0) === id && String(item.status || "ACTIVE").toUpperCase() === "ACTIVE")
|
||
.sort((left, right) => Number(right.is_default || 0) - Number(left.is_default || 0) || Number(right.bom_id || 0) - Number(left.bom_id || 0));
|
||
const selectedProductBom = activeBoms.find((item) => Number(item.is_default || 0) === 1) || activeBoms[0] || null;
|
||
const seen = new Set();
|
||
return (selectedProductBom?.items || [])
|
||
.map((item) => Number(item.material_item_id || 0))
|
||
.filter((materialItemId) => {
|
||
if (!materialItemId || seen.has(materialItemId)) {
|
||
return false;
|
||
}
|
||
seen.add(materialItemId);
|
||
return true;
|
||
});
|
||
}
|
||
|
||
const selectedBom = computed(() =>
|
||
boms.value.find((item) => item.product_item_id === Number(productionForm.product_item_id) && item.is_default) ||
|
||
boms.value.find((item) => item.product_item_id === Number(productionForm.product_item_id)) ||
|
||
null
|
||
);
|
||
const selectedProductionBomMaterialRows = computed(() => {
|
||
const seen = new Set();
|
||
return (selectedBom.value?.items || [])
|
||
.filter((item) => Number(item.material_item_id || 0) > 0)
|
||
.filter((item) => {
|
||
const materialItemId = Number(item.material_item_id);
|
||
if (seen.has(materialItemId)) {
|
||
return false;
|
||
}
|
||
seen.add(materialItemId);
|
||
return true;
|
||
});
|
||
});
|
||
const selectedProductionBomMaterialRow = computed(() => {
|
||
const selectedMaterialId = Number(selectedProductionStockLots.value[0]?.item_id || 0);
|
||
if (selectedMaterialId) {
|
||
return selectedProductionBomMaterialRows.value.find((item) => Number(item.material_item_id || 0) === selectedMaterialId) || null;
|
||
}
|
||
return selectedProductionBomMaterialRows.value[0] || null;
|
||
});
|
||
const selectedBomGrossWeight = computed(() => Number(selectedProductionBomMaterialRow.value?.gross_weight_per_piece_kg || 0));
|
||
const selectedProductionIssueWeight = computed(() =>
|
||
selectedProductionStockLots.value.reduce((total, lot) => {
|
||
const issuedWeight = Number(lot.issued_weight_kg);
|
||
return total + (Number.isFinite(issuedWeight) ? issuedWeight : 0);
|
||
}, 0)
|
||
);
|
||
const referenceProductionQty = computed(() => {
|
||
const grossWeight = selectedBomGrossWeight.value;
|
||
const issueWeight = Number(selectedProductionIssueWeight.value || 0);
|
||
if (grossWeight <= 0 || issueWeight <= 0) {
|
||
return 0;
|
||
}
|
||
return Math.floor(issueWeight / grossWeight);
|
||
});
|
||
function hasWorkOrderSettleMarker(workOrder, marker) {
|
||
return String(workOrder?.remark || "").includes(marker);
|
||
}
|
||
|
||
function isWorkOrderSettled(workOrder, bizType = "") {
|
||
if (String(workOrder?.status || "") === "锁单") {
|
||
return true;
|
||
}
|
||
if (bizType === "PRODUCTION_SCRAP_IN" && hasWorkOrderSettleMarker(workOrder, "生产废料已结单")) {
|
||
return true;
|
||
}
|
||
if (bizType === "PRODUCTION_SURPLUS" && hasWorkOrderSettleMarker(workOrder, "生产余料已结单")) {
|
||
return true;
|
||
}
|
||
const status = String(workOrder?.status || "").toUpperCase();
|
||
const label = String(formatStatusLabel(workOrder?.status || "") || "");
|
||
const statusSettled = status === "SETTLED" || label === "已结单";
|
||
if (!statusSettled) {
|
||
return false;
|
||
}
|
||
if (bizType === "PRODUCTION_SCRAP_IN") {
|
||
return Number(workOrder?.limits?.tolerance_scrap_inbound_weight_kg || workOrder?.max_scrap_inbound_weight_kg || 0) <= 0;
|
||
}
|
||
if (bizType === "PRODUCTION_SURPLUS") {
|
||
return Number(workOrder?.limits?.tolerance_surplus_inbound_weight_kg || workOrder?.max_surplus_inbound_weight_kg || 0) <= 0;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const receivableWorkOrders = computed(() =>
|
||
workOrderLedgerRows.value.filter(
|
||
(item) =>
|
||
String(item.work_order_type || "").toUpperCase() !== "REWORK" &&
|
||
!isWorkOrderSettled(item) &&
|
||
Number(item.limits?.tolerance_finished_inbound_qty || item.max_finished_inbound_qty || 0) > 0
|
||
)
|
||
);
|
||
const reworkReceivableWorkOrders = computed(() =>
|
||
workOrderLedgerRows.value.filter(
|
||
(item) =>
|
||
String(item.work_order_type || "").toUpperCase() === "REWORK" &&
|
||
!isWorkOrderSettled(item) &&
|
||
Number(item.limits?.max_finished_inbound_qty || item.max_finished_inbound_qty || 0) > 0
|
||
)
|
||
);
|
||
const isReworkCompletionOperation = computed(() => activeOperation.value?.drawerType === "reworkCompletion");
|
||
const completionWorkOrderOptions = computed(() =>
|
||
isReworkCompletionOperation.value ? reworkReceivableWorkOrders.value : receivableWorkOrders.value
|
||
);
|
||
const selectedCompletionWorkOrder = computed(() =>
|
||
workOrderLedgerRows.value.find((item) => Number(item.work_order_id) === Number(completionForm.work_order_id)) || null
|
||
);
|
||
const completionProduct = computed(() => products.value.find((item) => item.item_id === Number(completionForm.product_item_id)) || null);
|
||
const completionProductLabel = computed(() => formatProductLabel(completionProduct.value));
|
||
const completionFinishedBeforeQty = computed(() =>
|
||
Number(selectedCompletionWorkOrder.value?.limits?.finished_received_qty || selectedCompletionWorkOrder.value?.finished_received_qty || 0)
|
||
);
|
||
const completionExpectedFinishedQty = computed(() =>
|
||
Number((completionFinishedBeforeQty.value + Number(completionForm.max_receivable_qty || 0)).toFixed(3))
|
||
);
|
||
const completionFinalFinishedQty = computed(() =>
|
||
Number((completionFinishedBeforeQty.value + Number(completionForm.receipt_qty || 0)).toFixed(3))
|
||
);
|
||
const completionSettleDeviationRate = computed(() => {
|
||
const expected = completionExpectedFinishedQty.value;
|
||
if (expected <= 0) {
|
||
return 0;
|
||
}
|
||
return (completionFinalFinishedQty.value - expected) / expected;
|
||
});
|
||
const completionSettleNeedsRemark = computed(() => {
|
||
if (!completionForm.settle_work_order) {
|
||
return false;
|
||
}
|
||
const expected = completionExpectedFinishedQty.value;
|
||
if (expected <= 0) {
|
||
return completionFinalFinishedQty.value > 0;
|
||
}
|
||
const finalQty = completionFinalFinishedQty.value;
|
||
return finalQty < expected * 0.85 || finalQty > expected * 1.15;
|
||
});
|
||
const selectedSalesOrder = computed(() =>
|
||
salesOrders.value.find((item) => Number(item.order_id) === Number(deliveryForm.sales_order_id)) || null
|
||
);
|
||
const isSalesOrderDelivery = computed(() => deliveryForm.delivery_mode === "SALES_ORDER");
|
||
const isDirectCustomerDelivery = computed(() => deliveryForm.delivery_mode === "DIRECT_CUSTOMER");
|
||
const selectedSalesOrderItem = computed(() =>
|
||
salesOrderItems.value.find((item) => Number(item.sales_order_item_id) === Number(deliveryForm.sales_order_item_id)) || null
|
||
);
|
||
const openSalesOrders = computed(() =>
|
||
salesOrders.value.filter((order) => order.status !== "CLOSED" && orderPendingQty(order.order_id) > 0)
|
||
);
|
||
const selectableSalesOrderItems = computed(() => {
|
||
if (!isSalesOrderDelivery.value) {
|
||
return [];
|
||
}
|
||
return salesOrderItems.value.filter(
|
||
(item) => Number(item.sales_order_id) === Number(deliveryForm.sales_order_id) && orderItemPendingQty(item) > 0
|
||
);
|
||
});
|
||
const availableFinishedLots = computed(() =>
|
||
lots.value
|
||
.filter(
|
||
(item) =>
|
||
item.item_id === Number(deliveryForm.product_item_id) &&
|
||
item.warehouse_id === Number(deliveryForm.warehouse_id) &&
|
||
item.status === "AVAILABLE" &&
|
||
Number(item.remaining_qty || 0) > 0
|
||
)
|
||
.sort((left, right) => {
|
||
const leftKey = left.source_material_sub_batch_no || left.lot_no || "";
|
||
const rightKey = right.source_material_sub_batch_no || right.lot_no || "";
|
||
return leftKey.localeCompare(rightKey, "zh-CN", { numeric: true }) || Number(left.lot_id) - Number(right.lot_id);
|
||
})
|
||
);
|
||
const totalAvailableQty = computed(() =>
|
||
availableFinishedLots.value.reduce((total, lot) => total + Number(lot.remaining_qty || 0), 0)
|
||
);
|
||
const allocationRows = computed(() => {
|
||
let remaining = Number(deliveryForm.delivery_qty || 0);
|
||
const rows = [];
|
||
for (const lot of availableFinishedLots.value) {
|
||
if (remaining <= 0) {
|
||
break;
|
||
}
|
||
const availableQty = Number(lot.remaining_qty || 0);
|
||
const deliveryQty = Math.min(availableQty, remaining);
|
||
const ratio = availableQty > 0 ? deliveryQty / availableQty : 0;
|
||
rows.push({
|
||
...lot,
|
||
delivery_qty: deliveryQty,
|
||
delivery_weight_kg: Number(lot.remaining_weight_kg || 0) * ratio
|
||
});
|
||
remaining -= deliveryQty;
|
||
}
|
||
return rows;
|
||
});
|
||
const totalAllocatedQty = computed(() =>
|
||
allocationRows.value.reduce((total, item) => total + Number(item.delivery_qty || 0), 0)
|
||
);
|
||
const estimatedScrapSaleAmount = computed(() =>
|
||
Number(genericForm.weight_kg || 0) * Number(genericForm.sale_unit_price || 0)
|
||
);
|
||
const isProductionScrapInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" && activeOperation.value?.bizType === "PRODUCTION_SCRAP_IN"
|
||
);
|
||
const isReworkScrapInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" && activeOperation.value?.bizType === "REWORK_SCRAP_IN"
|
||
);
|
||
const isProductionSurplusInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" && activeOperation.value?.bizType === "PRODUCTION_SURPLUS"
|
||
);
|
||
const isProductionWorkOrderControlledInbound = computed(() => isProductionScrapInbound.value || isProductionSurplusInbound.value || isReworkScrapInbound.value);
|
||
const productionLedgerOptionRows = computed(() =>
|
||
productionLedgerRows.value.map((row) => ({
|
||
...row,
|
||
work_order_id: Number(row.production_ledger_id || row.work_order_id || 0),
|
||
work_order_no: row.material_lot_no || "-",
|
||
source_lot_summary: row.material_lot_no || row.source_lot_summary || "",
|
||
issued_weight_kg: Number(row.total_issued_weight_kg || row.issued_weight_kg || 0),
|
||
returned_material_weight_kg: Number(row.surplus_inbound_weight_kg || row.returned_material_weight_kg || 0),
|
||
materials: [
|
||
{
|
||
source_lot_id: Number(row.material_lot_id || 0),
|
||
source_lot_no: row.material_lot_no || "",
|
||
material_item_id: Number(row.material_item_id || 0),
|
||
material_code: row.material_code || "",
|
||
material_name: row.material_name || "",
|
||
issued_weight_kg: Number(row.total_issued_weight_kg || 0),
|
||
returned_weight_kg: Number(row.surplus_inbound_weight_kg || 0),
|
||
unit_cost: Number(row.unit_cost || 0)
|
||
}
|
||
],
|
||
limits: {
|
||
issued_weight_kg: Number(row.total_issued_weight_kg || 0),
|
||
returned_material_weight_kg: Number(row.surplus_inbound_weight_kg || 0),
|
||
max_surplus_inbound_weight_kg: Number(row.outside_weight_kg || 0),
|
||
tolerance_surplus_inbound_weight_kg: Number(row.outside_weight_kg || 0) * 1.15,
|
||
scrap_inbound_weight_kg: Number(row.scrap_inbound_weight_kg || 0),
|
||
max_scrap_inbound_weight_kg: Number(row.outside_weight_kg || 0),
|
||
tolerance_scrap_inbound_weight_kg: Number(row.outside_weight_kg || 0) * 1.15
|
||
}
|
||
}))
|
||
);
|
||
const selectedGenericWorkOrder = computed(() =>
|
||
(isProductionScrapInbound.value || isProductionSurplusInbound.value
|
||
? productionLedgerOptionRows.value
|
||
: workOrderLedgerRows.value
|
||
).find((item) => Number(item.work_order_id) === Number(genericForm.work_order_id)) || null
|
||
);
|
||
const genericWorkOrderOptions = computed(() => {
|
||
if (isProductionScrapInbound.value) {
|
||
return productionLedgerOptionRows.value.filter(
|
||
(item) => !isWorkOrderSettled(item, "PRODUCTION_SCRAP_IN") && Number(item.limits?.tolerance_scrap_inbound_weight_kg || item.max_scrap_inbound_weight_kg || 0) > 0
|
||
);
|
||
}
|
||
if (isReworkScrapInbound.value) {
|
||
return workOrderLedgerRows.value.filter(
|
||
(item) => String(item.work_order_type || "").toUpperCase() === "REWORK" && !isWorkOrderSettled(item)
|
||
);
|
||
}
|
||
if (isProductionSurplusInbound.value) {
|
||
return productionLedgerOptionRows.value.filter(
|
||
(item) => !isWorkOrderSettled(item, "PRODUCTION_SURPLUS") && Number(item.limits?.tolerance_surplus_inbound_weight_kg || item.max_surplus_inbound_weight_kg || 0) > 0
|
||
);
|
||
}
|
||
return workOrderLedgerRows.value.filter((item) => !isWorkOrderSettled(item));
|
||
});
|
||
const openProductionWorkOrders = computed(() =>
|
||
productionLedgerOptionRows.value.filter(
|
||
(item) =>
|
||
!isWorkOrderSettled(item) &&
|
||
Number(item.work_order_id || 0) > 0
|
||
)
|
||
);
|
||
const genericWeightInputMax = computed(() => {
|
||
if (isProductionScrapInbound.value) {
|
||
if (genericForm.settle_work_order) {
|
||
return 0;
|
||
}
|
||
return Number(productionScrapToleranceRemainingWeight.value || 0);
|
||
}
|
||
return 0;
|
||
});
|
||
const genericUnitCostLabel = computed(() => {
|
||
if (isProductionScrapInbound.value) {
|
||
return "原材料成本单价(元/kg)";
|
||
}
|
||
return formatGenericUnitCostLabel(activeWarehouseType.value);
|
||
});
|
||
function formatGenericUnitCostLabel(activeWarehouseType) {
|
||
if (activeWarehouseType === "FINISHED") {
|
||
return "单价(元/件)";
|
||
}
|
||
return activeWarehouseType === "AUX" ? "单价(元/件)" : "单价(元/kg)";
|
||
}
|
||
const genericRequiresLogistics = computed(() =>
|
||
LOGISTICS_REQUIRED_BIZ_TYPES.has(activeOperation.value?.bizType)
|
||
);
|
||
const isSourceLotReturnInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" && SOURCE_LOT_RETURN_INBOUND_BIZ_TYPES.has(activeOperation.value?.bizType)
|
||
);
|
||
const isCustomerSuppliedInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" && activeOperation.value?.bizType === "CUSTOMER_SUPPLIED"
|
||
);
|
||
const isSemiSourceLotTraceInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" &&
|
||
activeWarehouseType.value === "SEMI" &&
|
||
["WIP_IN", "OUTSOURCING_IN"].includes(activeOperation.value?.bizType)
|
||
);
|
||
const isFinishedOutsourcingInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" &&
|
||
activeWarehouseType.value === "FINISHED" &&
|
||
activeOperation.value?.bizType === "OUTSOURCING_IN"
|
||
);
|
||
const isOutsourcingScrapInbound = computed(() =>
|
||
activeOperation.value?.direction === "in" &&
|
||
activeWarehouseType.value === "SCRAP" &&
|
||
activeOperation.value?.bizType === "OUTSOURCING_SCRAP_IN"
|
||
);
|
||
const isScrapSaleOutbound = computed(() =>
|
||
activeOperation.value?.direction === "out" &&
|
||
activeWarehouseType.value === "SCRAP" &&
|
||
activeOperation.value?.bizType === "SCRAP_SALE_OUT"
|
||
);
|
||
const isProductTraceOnlySourceLotInbound = computed(() =>
|
||
isSemiSourceLotTraceInbound.value || isFinishedOutsourcingInbound.value
|
||
);
|
||
const isSemiAutoSourceOutbound = computed(() =>
|
||
activeOperation.value?.direction === "out" &&
|
||
activeWarehouseType.value === "SEMI" &&
|
||
["WIP_OUT", "OUTSOURCING_OUT"].includes(activeOperation.value?.bizType)
|
||
);
|
||
const isSemiQuantityWeightFlexibleOperation = computed(() => isSemiSourceLotTraceInbound.value || isSemiAutoSourceOutbound.value);
|
||
const isSourceLotSelectedOutbound = computed(() =>
|
||
activeOperation.value?.direction === "out" &&
|
||
(activeWarehouseType.value === "RAW" || isScrapSaleOutbound.value) &&
|
||
SOURCE_LOT_SELECTED_OUTBOUND_BIZ_TYPES.has(activeOperation.value?.bizType)
|
||
);
|
||
const genericOperationDocumentTitle = computed(
|
||
() => WAREHOUSE_OPERATION_DOCUMENT_TITLES[activeOperation.value?.bizType] || `${activeOperation.value?.label || "仓库出入库"}单`
|
||
);
|
||
const usesGenericSourceLotSelector = computed(() =>
|
||
isSourceLotReturnInbound.value ||
|
||
isSourceLotSelectedOutbound.value ||
|
||
isProductTraceOnlySourceLotInbound.value ||
|
||
isOutsourcingScrapInbound.value ||
|
||
isScrapSaleOutbound.value
|
||
);
|
||
const genericSourceLotReturnWeight = computed(() =>
|
||
selectedGenericSourceLots.value.reduce((total, lot) => total + Number(lot.return_weight_kg || 0), 0)
|
||
);
|
||
const genericSourceLotOutboundWeight = computed(() =>
|
||
selectedGenericSourceLots.value.reduce((total, lot) => total + Number(lot.issued_weight_kg || 0), 0)
|
||
);
|
||
const genericSelectedSourceLotWeight = computed(() =>
|
||
isSourceLotSelectedOutbound.value ? genericSourceLotOutboundWeight.value : genericSourceLotReturnWeight.value
|
||
);
|
||
const productionSurplusReturnedBeforeWeight = computed(() =>
|
||
Number(selectedGenericWorkOrder.value?.limits?.returned_material_weight_kg || selectedGenericWorkOrder.value?.returned_material_weight_kg || 0)
|
||
);
|
||
const productionSurplusExpectedReturnWeight = computed(() => {
|
||
const issuedWeight = Number(selectedGenericWorkOrder.value?.limits?.issued_weight_kg || selectedGenericWorkOrder.value?.issued_weight_kg || 0);
|
||
const usedWeight = Number(selectedGenericWorkOrder.value?.limits?.used_material_weight_kg || selectedGenericWorkOrder.value?.used_material_weight_kg || 0);
|
||
const remainingLimit = Number(selectedGenericWorkOrder.value?.limits?.max_surplus_inbound_weight_kg || selectedGenericWorkOrder.value?.max_surplus_inbound_weight_kg || 0);
|
||
const calculated = issuedWeight > 0 ? issuedWeight - usedWeight : productionSurplusReturnedBeforeWeight.value + remainingLimit;
|
||
return Math.max(Number(calculated.toFixed(3)), 0);
|
||
});
|
||
const productionSurplusFinalReturnWeight = computed(() =>
|
||
Number((productionSurplusReturnedBeforeWeight.value + genericSourceLotReturnWeight.value).toFixed(3))
|
||
);
|
||
const productionSurplusToleranceRemainingWeight = computed(() => {
|
||
const apiLimit = Number(selectedGenericWorkOrder.value?.limits?.tolerance_surplus_inbound_weight_kg || 0);
|
||
if (apiLimit > 0) {
|
||
return Number(apiLimit.toFixed(3));
|
||
}
|
||
const calculated = productionSurplusExpectedReturnWeight.value * 1.15 - productionSurplusReturnedBeforeWeight.value;
|
||
return Math.max(Number(calculated.toFixed(3)), 0);
|
||
});
|
||
const productionSurplusSettleDeviationRate = computed(() => {
|
||
const expected = productionSurplusExpectedReturnWeight.value;
|
||
if (expected <= 0) {
|
||
return 0;
|
||
}
|
||
return (productionSurplusFinalReturnWeight.value - expected) / expected;
|
||
});
|
||
const productionSurplusSettleNeedsRemark = computed(() => {
|
||
if (!isProductionSurplusInbound.value || !genericForm.settle_work_order || !selectedGenericSourceLots.value.length) {
|
||
return false;
|
||
}
|
||
const expected = productionSurplusExpectedReturnWeight.value;
|
||
if (expected <= 0) {
|
||
return productionSurplusFinalReturnWeight.value > 0;
|
||
}
|
||
const finalWeight = productionSurplusFinalReturnWeight.value;
|
||
return finalWeight < expected * 0.85 || finalWeight > expected * 1.15;
|
||
});
|
||
const productionScrapInboundBeforeWeight = computed(() =>
|
||
Number(selectedGenericWorkOrder.value?.limits?.scrap_inbound_weight_kg || selectedGenericWorkOrder.value?.scrap_inbound_weight_kg || 0)
|
||
);
|
||
const productionScrapExpectedWeight = computed(() => {
|
||
const standardWeight = Number(selectedGenericWorkOrder.value?.limits?.standard_scrap_weight_kg || 0);
|
||
if (standardWeight > 0) {
|
||
return Math.max(Number(standardWeight.toFixed(3)), 0);
|
||
}
|
||
const remainingStandardWeight = Number(
|
||
selectedGenericWorkOrder.value?.limits?.max_scrap_inbound_weight_kg || selectedGenericWorkOrder.value?.max_scrap_inbound_weight_kg || 0
|
||
);
|
||
const calculated = productionScrapInboundBeforeWeight.value + remainingStandardWeight;
|
||
return Math.max(Number(calculated.toFixed(3)), 0);
|
||
});
|
||
const productionScrapFinalWeight = computed(() =>
|
||
Number((productionScrapInboundBeforeWeight.value + Number(genericForm.weight_kg || 0)).toFixed(3))
|
||
);
|
||
const productionScrapToleranceRemainingWeight = computed(() => {
|
||
const apiLimit = Number(selectedGenericWorkOrder.value?.limits?.tolerance_scrap_inbound_weight_kg || 0);
|
||
if (apiLimit > 0) {
|
||
return Number(apiLimit.toFixed(3));
|
||
}
|
||
const calculated = productionScrapExpectedWeight.value * 1.15 - productionScrapInboundBeforeWeight.value;
|
||
return Math.max(Number(calculated.toFixed(3)), 0);
|
||
});
|
||
const productionScrapSettleDeviationRate = computed(() => {
|
||
const expected = productionScrapExpectedWeight.value;
|
||
if (expected <= 0) {
|
||
return 0;
|
||
}
|
||
return (productionScrapFinalWeight.value - expected) / expected;
|
||
});
|
||
const productionScrapSettleNeedsRemark = computed(() => {
|
||
if (!isProductionScrapInbound.value || !genericForm.settle_work_order) {
|
||
return false;
|
||
}
|
||
const expected = productionScrapExpectedWeight.value;
|
||
if (expected <= 0) {
|
||
return productionScrapFinalWeight.value > 0;
|
||
}
|
||
const finalWeight = productionScrapFinalWeight.value;
|
||
return finalWeight < expected * 0.85 || finalWeight > expected * 1.15;
|
||
});
|
||
const genericSourceLotSelectPlaceholder = computed(() =>
|
||
isFinishedOutsourcingInbound.value
|
||
? "搜索并选择该产品用料的可用来源库存批次号,可多选;委外厂自供原料可不选"
|
||
: isScrapSaleOutbound.value
|
||
? "搜索并选择该废料对应的原材料来源库存批次号,可多选"
|
||
: isOutsourcingScrapInbound.value
|
||
? "搜索并选择该产品用料的来源库存批次号,可多选"
|
||
: isSemiSourceLotTraceInbound.value
|
||
? "搜索并选择该产品用料的来源库存批次号,可多选"
|
||
: isSourceLotSelectedOutbound.value
|
||
? "搜索并选择出库来源库存批次号,可多选"
|
||
: "搜索并选择归还到的来源库存批次号,可多选"
|
||
);
|
||
const customerSuppliedLotNoPreview = computed(() => {
|
||
if (!isCustomerSuppliedInbound.value) {
|
||
return "";
|
||
}
|
||
return buildInventoryLotNoPreview(selectedGenericItemOption.value, lots.value);
|
||
});
|
||
|
||
const selectedBalanceLabel = computed(() =>
|
||
selectedBalance.value ? formatSelectedBalanceTitle(selectedBalance.value) : "库存明细"
|
||
);
|
||
const selectedWarehouseType = computed(() => normalizeWarehouseType(selectedBalance.value));
|
||
const selectedLots = computed(() => {
|
||
const rows = lots.value.filter(
|
||
(item) =>
|
||
item.item_id === selectedItemId.value &&
|
||
(!selectedWarehouseType.value || normalizeWarehouseType(item) === selectedWarehouseType.value)
|
||
);
|
||
if (selectedWarehouseType.value === "SEMI" && selectedBalance.value && Object.hasOwn(selectedBalance.value, "source_line_id")) {
|
||
const selectedOperationId = Number(selectedBalance.value.source_line_id || 0);
|
||
return rows.filter((lot) => Number(lot.source_line_id || 0) === selectedOperationId);
|
||
}
|
||
if (selectedBalance.value?.item_type !== "FINISHED_GOOD" || selectedWarehouseType.value !== "FINISHED") {
|
||
return rows;
|
||
}
|
||
const map = new Map();
|
||
rows.forEach((lot) => {
|
||
const key = lot.source_material_sub_batch_no || lot.lot_no;
|
||
if (!map.has(key)) {
|
||
map.set(key, {
|
||
...lot,
|
||
lot_id: `FG-SRC-${key}`,
|
||
lot_no: key,
|
||
warehouse_name: "",
|
||
inbound_qty: 0,
|
||
inbound_weight_kg: 0,
|
||
remaining_qty: 0,
|
||
remaining_weight_kg: 0,
|
||
locked_qty: 0,
|
||
locked_weight_kg: 0,
|
||
source_material_sub_batch_no: key,
|
||
source_material_summary: lot.source_material_summary,
|
||
status: "AVAILABLE",
|
||
_warehouses: new Set()
|
||
});
|
||
}
|
||
const row = map.get(key);
|
||
row.inbound_qty += Number(lot.inbound_qty || 0);
|
||
row.inbound_weight_kg += Number(lot.inbound_weight_kg || 0);
|
||
row.remaining_qty += Number(lot.remaining_qty || 0);
|
||
row.remaining_weight_kg += Number(lot.remaining_weight_kg || 0);
|
||
row.locked_qty += Number(lot.locked_qty || 0);
|
||
row.locked_weight_kg += Number(lot.locked_weight_kg || 0);
|
||
row._warehouses.add(lot.warehouse_name);
|
||
if (row.remaining_qty <= 0 && row.remaining_weight_kg <= 0) {
|
||
row.status = "DEPLETED";
|
||
}
|
||
});
|
||
return Array.from(map.values()).map((item) => ({
|
||
...item,
|
||
warehouse_name: Array.from(item._warehouses).join(" / ") || "-"
|
||
}));
|
||
});
|
||
|
||
function lotAvailableWeight(lot) {
|
||
return Math.max(Number(lot?.remaining_weight_kg || 0) - Number(lot?.locked_weight_kg || 0), 0);
|
||
}
|
||
|
||
function lotAvailableQty(lot) {
|
||
return Math.max(Number(lot?.remaining_qty || 0) - Number(lot?.locked_qty || 0), 0);
|
||
}
|
||
|
||
function isInventoryLotUsable(lot) {
|
||
if (String(lot?.status || "").toUpperCase() !== "AVAILABLE") {
|
||
return false;
|
||
}
|
||
if (normalizeWarehouseType(lot) === "AUX") {
|
||
return lotAvailableQty(lot) > 0;
|
||
}
|
||
if (normalizeWarehouseType(lot) === "SEMI") {
|
||
const basis = normalizeSemiMeasureBasis(lot?.measure_basis || deriveSemiLotMeasureBasis(lot));
|
||
if (basis === "PIECE") {
|
||
return lotAvailableQty(lot) > 0;
|
||
}
|
||
if (basis === "WEIGHT") {
|
||
return lotAvailableWeight(lot) > 0;
|
||
}
|
||
return lotAvailableQty(lot) > 0 || lotAvailableWeight(lot) > 0;
|
||
}
|
||
return lotAvailableWeight(lot) > 0;
|
||
}
|
||
|
||
function compareInventoryLots(left, right) {
|
||
const leftRank = isInventoryLotUsable(left) ? 0 : 1;
|
||
const rightRank = isInventoryLotUsable(right) ? 0 : 1;
|
||
if (leftRank !== rightRank) {
|
||
return leftRank - rightRank;
|
||
}
|
||
return String(left.lot_no || "").localeCompare(String(right.lot_no || ""), "zh-CN", { numeric: true }) || Number(left.lot_id) - Number(right.lot_id);
|
||
}
|
||
|
||
const sortedSelectedLots = computed(() => [...selectedLots.value].sort(compareInventoryLots));
|
||
const selectedOverviewLots = computed(() => sortedSelectedLots.value.slice(0, 8));
|
||
const selectedSemiDetailMeasureBasis = computed(() =>
|
||
selectedWarehouseType.value === "SEMI" ? normalizeSemiMeasureBasis(selectedBalance.value?.measure_basis || deriveSemiMeasureBasisFromRows(selectedLots.value)) : ""
|
||
);
|
||
const isSelectedFinishedWarehouse = computed(() => selectedWarehouseType.value === "FINISHED");
|
||
const selectedLotNoColumnLabel = computed(() =>
|
||
selectedWarehouseType.value === "SEMI" ? "来源库存批次号" : "库存批次号"
|
||
);
|
||
const selectedDetailEntityLabel = computed(() => {
|
||
if (selectedWarehouseType.value === "SEMI") {
|
||
return "半成品";
|
||
}
|
||
if (isSelectedFinishedWarehouse.value || selectedWarehouseType.value === "RETURN") {
|
||
return "产品";
|
||
}
|
||
return "物料";
|
||
});
|
||
const selectedSourceLotNos = computed(() =>
|
||
Array.from(
|
||
new Set(
|
||
sortedSelectedLots.value
|
||
.filter((lot) => shouldDisplaySourceMaterialLotNo(selectedWarehouseType.value) || isInventoryLotUsable(lot))
|
||
.map((lot) => (shouldDisplaySourceMaterialLotNo(selectedWarehouseType.value) ? formatLotSourceLots(lot) : lot.lot_no))
|
||
.filter((lotNo) => lotNo && lotNo !== "-")
|
||
)
|
||
)
|
||
);
|
||
const selectedSourceLotEmptyText = computed(() =>
|
||
shouldDisplaySourceMaterialLotNo(selectedWarehouseType.value) ? "暂无来源库存批次号" : "暂无可用库存批次"
|
||
);
|
||
const selectedSemiOperationLabel = computed(() => formatSemiCompletedOperation(selectedBalance.value));
|
||
const selectedLotSummary = computed(() => {
|
||
const totalCount = selectedLots.value.length;
|
||
const availableRows = selectedLots.value.filter((lot) => isInventoryLotUsable(lot));
|
||
return {
|
||
totalCount,
|
||
availableCount: availableRows.length,
|
||
remainingQty: selectedLots.value.reduce((total, lot) => total + Number(lot.remaining_qty || 0), 0),
|
||
availableQty: availableRows.reduce((total, lot) => total + lotAvailableQty(lot), 0),
|
||
remainingWeight: selectedLots.value.reduce((total, lot) => total + Number(lot.remaining_weight_kg || 0), 0),
|
||
availableWeight: availableRows.reduce((total, lot) => total + lotAvailableWeight(lot), 0)
|
||
};
|
||
});
|
||
const selectedPrimaryAvailableLabel = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value || selectedQuantityOnly.value || (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE")) {
|
||
return "可用数量";
|
||
}
|
||
return "可用重量";
|
||
});
|
||
const selectedPrimaryAvailableValue = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value || selectedQuantityOnly.value || (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE")) {
|
||
return formatQty(selectedLotSummary.value.availableQty);
|
||
}
|
||
return formatWeight(selectedLotSummary.value.availableWeight);
|
||
});
|
||
const selectedPrimaryOnHandLabel = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value || selectedQuantityOnly.value || (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE")) {
|
||
return "现存数量";
|
||
}
|
||
return "现存重量";
|
||
});
|
||
const selectedPrimaryOnHandValue = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value || selectedQuantityOnly.value || (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE")) {
|
||
return formatQty(selectedLotSummary.value.remainingQty);
|
||
}
|
||
return formatWeight(selectedLotSummary.value.remainingWeight);
|
||
});
|
||
const selectedAuxiliaryMetricLabel = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value) {
|
||
return "辅助重量";
|
||
}
|
||
if (selectedWarehouseType.value === "SEMI") {
|
||
return "已完成工序";
|
||
}
|
||
return "可用批次";
|
||
});
|
||
const selectedAuxiliaryMetricValue = computed(() =>
|
||
isSelectedFinishedWarehouse.value
|
||
? formatWeight(selectedLotSummary.value.remainingWeight)
|
||
: selectedWarehouseType.value === "SEMI"
|
||
? selectedSemiOperationLabel.value
|
||
: `${selectedLotSummary.value.availableCount} / ${selectedLotSummary.value.totalCount}`
|
||
);
|
||
const selectedOverviewAvailableLabel = computed(() => {
|
||
if (isSelectedFinishedWarehouse.value || selectedWarehouseType.value === "AUX" || (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE")) {
|
||
return "可用数量";
|
||
}
|
||
return "可用重量(kg)";
|
||
});
|
||
const selectedUnitCostSuffix = computed(() => {
|
||
if (selectedWarehouseType.value === "FINISHED" || selectedWarehouseType.value === "AUX") {
|
||
return "/件";
|
||
}
|
||
if (selectedWarehouseType.value === "SEMI" && selectedSemiDetailMeasureBasis.value === "PIECE") {
|
||
return "/件";
|
||
}
|
||
return "/kg";
|
||
});
|
||
const selectedUnitCostColumnLabel = computed(() => {
|
||
if (selectedWarehouseType.value === "FINISHED") {
|
||
return "入库材料成本(毛重;元/件)";
|
||
}
|
||
return selectedUnitCostSuffix.value === "/件" ? "入库单价(元/件)" : "入库单价(元/kg)";
|
||
});
|
||
const overviewLotDrawerColumns = computed(() => [
|
||
{ key: "lot_no", label: selectedLotNoColumnLabel.value, compactLabel: selectedLotNoColumnLabel.value, type: "code", width: "220px" },
|
||
{ key: "available", label: selectedOverviewAvailableLabel.value, compactLabel: isSelectedFinishedWarehouse.value || selectedQuantityOnly.value ? "可用数" : "可用重量", type: "number", width: "108px", format: (_value, row) => formatLotAvailableByBasis(row) },
|
||
...(isSelectedFinishedWarehouse.value
|
||
? [{ key: "auxiliary_weight", label: "辅助重量(kg)", compactLabel: "辅助重量", type: "number", width: "110px", format: (_value, row) => formatWeight(lotAvailableWeight(row)) }]
|
||
: []),
|
||
{ key: "unit_cost", label: selectedUnitCostColumnLabel.value, compactLabel: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
|
||
{ key: "status", label: "状态", type: "status", width: "88px" }
|
||
]);
|
||
const lotDrawerColumns = computed(() => [
|
||
{ key: "lot_no", label: selectedLotNoColumnLabel.value, compactLabel: selectedLotNoColumnLabel.value, type: "code", width: "220px" },
|
||
...(isSelectedFinishedWarehouse.value || selectedQuantityOnly.value ? [{ key: "inbound_qty", label: "入库数量", compactLabel: "入库数", type: "number", width: "96px", format: (_value, row) => formatQty(row.inbound_qty) }] : []),
|
||
...(selectedWeightOnly.value ? [] : [{ key: "remaining_qty", label: isSelectedFinishedWarehouse.value || selectedQuantityOnly.value ? "剩余数量" : "变动数量", compactLabel: isSelectedFinishedWarehouse.value || selectedQuantityOnly.value ? "剩余数" : "变动数", type: "number", width: "96px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_qty, "PIECE") : formatQty(row.remaining_qty) }]),
|
||
...(!selectedQuantityOnly.value ? [{ key: "inbound_weight_kg", label: "入库重量(kg)", compactLabel: "入库重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.inbound_weight_kg, "WEIGHT") : formatWeight(row.inbound_weight_kg) }] : []),
|
||
...(!selectedQuantityOnly.value ? [{ key: "remaining_weight_kg", label: "剩余重量(kg)", compactLabel: "剩余重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_weight_kg, "WEIGHT") : formatWeight(row.remaining_weight_kg) }] : []),
|
||
{ key: "unit_cost", label: selectedUnitCostColumnLabel.value, compactLabel: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
|
||
{ key: "status", label: "状态", type: "status", width: "88px" },
|
||
{ key: "warehouse_name", label: "仓库", detailOnly: true },
|
||
{ key: "source_purchase_order_no", label: "采购单号", detailOnly: true },
|
||
{ key: "source_receipt_no", label: "入库单号", detailOnly: true },
|
||
{ key: "source_material_summary", label: "来源库存批次", detailOnly: true },
|
||
{ key: "source_doc_type", label: "来源", detailOnly: true, format: (_value, row) => formatSourceDocTypeLabel(row.source_doc_type) },
|
||
{ key: "logistics_waybill_no", label: "运单号", detailOnly: true },
|
||
{ key: "logistics_freight_amount", label: "运费", detailOnly: true, format: (_value, row) => formatFreight(row.logistics_freight_amount) },
|
||
{ key: "quality_release", label: "质检放行人", detailOnly: true, format: (_value, row) => row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }
|
||
]);
|
||
const txnDrawerColumns = computed(() => [
|
||
{ key: "biz_time", label: "时间", type: "longText", width: "128px", format: (_value, row) => formatDateTime(row.biz_time) },
|
||
{ key: "txn_type", label: "类型", width: "94px", format: (_value, row) => formatInventoryTxnTypeLabel(row.txn_type) },
|
||
{ key: "lot_no", label: selectedLotNoColumnLabel.value, compactLabel: selectedLotNoColumnLabel.value, type: "code", width: "220px" },
|
||
...(selectedWeightOnly.value ? [] : [{ key: "qty_change", label: "变动数量", compactLabel: "变动数", type: "number", width: "96px", format: (_value, row) => formatQty(row.qty_change) }]),
|
||
...(!selectedQuantityOnly.value ? [{ key: "weight_change_kg", label: "变动重量(kg)", compactLabel: "变动重量", type: "number", width: "110px", format: (_value, row) => formatWeight(row.weight_change_kg) }] : []),
|
||
{ key: "unit_cost", label: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
|
||
{ key: "amount", label: "金额", type: "number", width: "104px", format: (_value, row) => `¥${formatAmount(row.amount)}` },
|
||
{ key: "archive_actions", label: "单据留档", width: "210px" },
|
||
{ key: "source", label: "来源", detailOnly: true, format: (_value, row) => `${formatSourceDocTypeLabel(row.source_doc_type)} #${row.source_doc_id || "-"}` },
|
||
{ key: "remark", label: "备注", detailOnly: true }
|
||
]);
|
||
const selectedLotTableColumnCount = computed(() => {
|
||
if (selectedWeightOnly.value) {
|
||
return 13;
|
||
}
|
||
return isSelectedFinishedWarehouse.value ? 16 : 15;
|
||
});
|
||
|
||
function formatLotAvailableByBasis(lot) {
|
||
if (normalizeWarehouseType(lot) === "FINISHED") {
|
||
return formatQty(lotAvailableQty(lot));
|
||
}
|
||
if (normalizeWarehouseType(lot) === "AUX") {
|
||
return formatQty(lotAvailableQty(lot));
|
||
}
|
||
if (normalizeWarehouseType(lot) === "SEMI") {
|
||
const basis = normalizeSemiMeasureBasis(lot?.measure_basis || selectedSemiDetailMeasureBasis.value || deriveSemiLotMeasureBasis(lot));
|
||
if (basis === "PIECE") {
|
||
return formatQty(lotAvailableQty(lot));
|
||
}
|
||
if (basis === "WEIGHT") {
|
||
return formatWeight(lotAvailableWeight(lot));
|
||
}
|
||
}
|
||
return formatWeight(lotAvailableWeight(lot));
|
||
}
|
||
|
||
function formatSelectedUnitCost(value) {
|
||
return `¥${formatAmount(value)}${selectedUnitCostSuffix.value}`;
|
||
}
|
||
|
||
function formatSelectedBalanceTitle(balance) {
|
||
if (normalizeWarehouseType(balance) === "SEMI") {
|
||
return balance?.item_name || balance?.item_code || "半成品明细";
|
||
}
|
||
return formatInventoryBalanceLabel(balance);
|
||
}
|
||
|
||
function extractSummaryPart(summary, label) {
|
||
const escapedLabel = String(label).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
const match = String(summary || "").match(new RegExp(`${escapedLabel}[::]\\s*([^;;]+)`));
|
||
return match?.[1]?.trim() || "";
|
||
}
|
||
|
||
function formatSemiCompletedOperation(row) {
|
||
const summary = row?.operation_label || row?.source_material_summary || "";
|
||
const operation = extractSummaryPart(summary, "已完成工序");
|
||
if (operation) {
|
||
return operation;
|
||
}
|
||
const fallback = String(summary || "")
|
||
.split(/[;;]/)
|
||
.map((part) => part.trim())
|
||
.find((part) => part.includes("工序") && !part.includes("来源库存批次号"));
|
||
return fallback || "未绑定工序";
|
||
}
|
||
|
||
function formatLotSourceLots(lot) {
|
||
const explicitLotNo = lot?.source_material_lot_no || lot?.source_material_sub_batch_no || lot?.material_sub_batch_no;
|
||
if (explicitLotNo) {
|
||
return explicitLotNo;
|
||
}
|
||
return extractSummaryPart(lot?.source_material_summary, "来源库存批次号") || "-";
|
||
}
|
||
|
||
function shouldDisplaySourceMaterialLotNo(warehouseType) {
|
||
return ["SEMI", "FINISHED", "SCRAP", "RETURN"].includes(String(warehouseType || "").toUpperCase());
|
||
}
|
||
|
||
function formatSelectedDetailLotNo(row) {
|
||
if (shouldDisplaySourceMaterialLotNo(selectedWarehouseType.value)) {
|
||
return formatLotSourceLots(row);
|
||
}
|
||
return row?.lot_no || "-";
|
||
}
|
||
|
||
function formatWarehouseLedgerLotNo(row) {
|
||
if (shouldDisplaySourceMaterialLotNo(activeWarehouseType.value)) {
|
||
return formatLotSourceLots(row);
|
||
}
|
||
return row?.lot_no || "-";
|
||
}
|
||
|
||
function formatWarehouseLedgerItemLabel(row) {
|
||
const code = String(row?.item_code || "").trim();
|
||
const name = String(row?.item_name || "").trim();
|
||
if (code && name) {
|
||
return `${code} · ${name}`;
|
||
}
|
||
return code || name || "-";
|
||
}
|
||
|
||
function formatWarehouseLedgerSourceLabel(row) {
|
||
const sourceLabel = formatSourceDocTypeLabel(row?.source_doc_type);
|
||
const sourceId = row?.source_doc_id ? ` #${row.source_doc_id}` : "";
|
||
return `${sourceLabel || ""}${sourceId}`.trim() || "-";
|
||
}
|
||
|
||
function formatWarehouseLedgerLogisticsLabel(row) {
|
||
const waybillNo = String(row?.logistics_waybill_no || "").trim();
|
||
if (waybillNo) {
|
||
return waybillNo;
|
||
}
|
||
return row?.logistics_photo_url ? "有辅助照片" : "-";
|
||
}
|
||
|
||
function normalizeWarehouseLedgerTooltipValue(value) {
|
||
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
||
return text && text !== "-" ? text : "";
|
||
}
|
||
|
||
function warehouseLedgerTooltipAttrs(value) {
|
||
const fullValue = normalizeWarehouseLedgerTooltipValue(value);
|
||
if (!fullValue) {
|
||
return {};
|
||
}
|
||
return {
|
||
"data-table-tooltip": "auto",
|
||
"data-table-full-value": fullValue,
|
||
"aria-label": fullValue
|
||
};
|
||
}
|
||
|
||
function lotStatusClass(lot) {
|
||
if (isInventoryLotUsable(lot)) {
|
||
return "status-success";
|
||
}
|
||
const status = String(lot?.status || "").toUpperCase();
|
||
if (status === "LOCKED" || status === "PENDING_QC") {
|
||
return "status-warning";
|
||
}
|
||
return "status-muted";
|
||
}
|
||
|
||
const selectedTransactions = computed(() =>
|
||
transactions.value.filter((item) => {
|
||
if (item.item_id !== selectedItemId.value) {
|
||
return false;
|
||
}
|
||
if (selectedWarehouseType.value && normalizeWarehouseType(item) !== selectedWarehouseType.value) {
|
||
return false;
|
||
}
|
||
if (selectedWarehouseType.value === "SEMI" && selectedBalance.value && Object.hasOwn(selectedBalance.value, "source_line_id")) {
|
||
return Number(item.source_line_id || 0) === Number(selectedBalance.value.source_line_id || 0);
|
||
}
|
||
return true;
|
||
})
|
||
);
|
||
|
||
const balanceControls = useTableControls(activeBalances, {
|
||
searchFields: ["item_code", "item_name", "specification", "stock_unit_name", "purchase_unit_name", "sales_unit_name", "warehouse_name", "location_name", "operation_label", "source_material_summary"],
|
||
sortOptions: [
|
||
{ key: "item_code", label: "编码" },
|
||
{ key: "item_name", label: "名称" },
|
||
{ key: "operation_label", label: "半成品" },
|
||
{ key: "warehouse_name", label: "仓库" },
|
||
{ key: "weight_available_kg", label: "可用重量" },
|
||
{ key: "qty_available", label: "可用数量" },
|
||
{ key: "avg_unit_cost", label: "均价" }
|
||
],
|
||
defaultSortKey: "item_code",
|
||
defaultSortDirection: "asc"
|
||
});
|
||
const lotControls = useTableControls(sortedSelectedLots, {
|
||
searchFields: [
|
||
"lot_no",
|
||
"warehouse_name",
|
||
"status",
|
||
"quality_user_name",
|
||
"source_purchase_order_no",
|
||
"source_receipt_no",
|
||
"source_material_lot_no",
|
||
"source_material_sub_batch_no",
|
||
"source_material_summary",
|
||
(row) => formatLotSourceLots(row),
|
||
"logistics_waybill_no",
|
||
(row) => formatStatusLabel(row.status),
|
||
(row) => formatQualityStatusLabel(row.quality_status),
|
||
(row) => formatItemTypeLabel(row.item_type)
|
||
],
|
||
sortOptions: [
|
||
{ key: "availability", label: "可用优先", field: (row) => (isInventoryLotUsable(row) ? 0 : 1) },
|
||
{ key: "lot_no", label: selectedLotNoColumnLabel.value, field: (row) => formatSelectedDetailLotNo(row) },
|
||
{ key: "remaining_weight_kg", label: "剩余重量(kg)" },
|
||
{ key: "status", label: "状态" }
|
||
],
|
||
defaultSortKey: "availability",
|
||
defaultSortDirection: "asc"
|
||
});
|
||
const txnControls = useTableControls(selectedTransactions, {
|
||
searchFields: [
|
||
"txn_type",
|
||
"lot_no",
|
||
"source_material_lot_no",
|
||
"source_material_sub_batch_no",
|
||
"source_material_summary",
|
||
"source_doc_type",
|
||
"logistics_waybill_no",
|
||
(row) => formatLotSourceLots(row),
|
||
(row) => formatInventoryTxnTypeLabel(row.txn_type),
|
||
(row) => formatSourceDocTypeLabel(row.source_doc_type)
|
||
],
|
||
sortOptions: [
|
||
{ key: "biz_time", label: "时间" },
|
||
{ key: "txn_type", label: "类型" },
|
||
{ key: "lot_no", label: selectedLotNoColumnLabel.value, field: (row) => formatSelectedDetailLotNo(row) },
|
||
{ key: "weight_change_kg", label: "变动重量(kg)" }
|
||
],
|
||
defaultSortKey: "biz_time",
|
||
defaultSortDirection: "desc"
|
||
});
|
||
const filteredBalances = balanceControls.rows;
|
||
const filteredLots = lotControls.rows;
|
||
const filteredTransactions = txnControls.rows;
|
||
const filteredRawCount = computed(() => rawBalances.value.length);
|
||
const filteredSemiCount = computed(() => semiFinishedBalances.value.length);
|
||
const filteredFinishedCount = computed(() => finishedBalances.value.length);
|
||
const filteredAuxiliaryCount = computed(() => auxiliaryBalances.value.length);
|
||
const filteredScrapCount = computed(() => scrapBalances.value.length);
|
||
const filteredReturnCount = computed(() => returnBalances.value.length);
|
||
|
||
const {
|
||
page: balancePage,
|
||
pageSize: balancePageSize,
|
||
rows: paginatedBalances
|
||
} = usePagination(filteredBalances);
|
||
const {
|
||
page: lotPage,
|
||
pageSize: lotPageSize,
|
||
rows: paginatedLots
|
||
} = usePagination(filteredLots);
|
||
const {
|
||
page: txnPage,
|
||
pageSize: txnPageSize,
|
||
rows: paginatedTransactions
|
||
} = usePagination(filteredTransactions);
|
||
|
||
const inventoryOverlayOpen = computed(
|
||
() =>
|
||
detailDrawerOpen.value ||
|
||
genericDrawerOpen.value ||
|
||
specialAdjustmentDrawerOpen.value ||
|
||
rawReturnDrawerOpen.value ||
|
||
productionDrawerOpen.value ||
|
||
completionDrawerOpen.value ||
|
||
deliveryDrawerOpen.value ||
|
||
returnInboundDrawerOpen.value ||
|
||
returnScrapInboundDrawerOpen.value ||
|
||
returnReworkDrawerOpen.value ||
|
||
warehouseLedgerDrawerOpen.value ||
|
||
productionWorkOrderInboundDrawerOpen.value ||
|
||
openingImportDrawerOpen.value ||
|
||
stocktakeDialogOpen.value
|
||
);
|
||
|
||
useBodyScrollLock(inventoryOverlayOpen);
|
||
|
||
function makeOperation(key, direction, bizType, label, title, drawerType = "generic") {
|
||
return { key, direction, bizType, label, title, drawerType };
|
||
}
|
||
|
||
function buildEmptyWarehouseLedgerSummary() {
|
||
return {
|
||
in_count: 0,
|
||
out_count: 0,
|
||
adjust_count: 0,
|
||
in_qty: 0,
|
||
out_qty: 0,
|
||
in_weight_kg: 0,
|
||
out_weight_kg: 0,
|
||
latest_biz_time: null
|
||
};
|
||
}
|
||
|
||
function buildEmptyWarehouseLedgerFilters() {
|
||
return {
|
||
keyword: "",
|
||
direction: "ALL",
|
||
txn_type: "",
|
||
start_date: "",
|
||
end_date: "",
|
||
sort_key: "biz_time",
|
||
sort_direction: "desc"
|
||
};
|
||
}
|
||
|
||
function buildEmptyGenericForm() {
|
||
return {
|
||
work_order_id: "",
|
||
item_option_key: "",
|
||
item_id: "",
|
||
warehouse_id: "",
|
||
location_id: "",
|
||
weight_kg: 0,
|
||
qty: 0,
|
||
measure_basis: "",
|
||
unit_cost: 0,
|
||
sale_unit_price: 0,
|
||
buyer_name: "",
|
||
lot_no: "",
|
||
provider_name: "",
|
||
source_doc_id: null,
|
||
source_line_id: null,
|
||
source_material_sub_batch_no: "",
|
||
sale_remark: "",
|
||
waybill_no: "",
|
||
order_photo_url: "",
|
||
freight_amount: "",
|
||
outsourcing_party_name: "",
|
||
remark: "",
|
||
settle_work_order: false,
|
||
settle_remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptySpecialAdjustmentForm() {
|
||
return {
|
||
direction: "IN",
|
||
warehouse_id: "",
|
||
reason: "",
|
||
lines: []
|
||
};
|
||
}
|
||
|
||
function buildEmptyRawReturnForm() {
|
||
return {
|
||
warehouse_id: "",
|
||
lot_id: "",
|
||
purchase_order_item_id: "",
|
||
return_weight_kg: 0,
|
||
waybill_no: "",
|
||
order_photo_url: "",
|
||
freight_amount: "",
|
||
reason: "",
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyProductionForm() {
|
||
return {
|
||
product_item_id: "",
|
||
issue_weight_kg: 0,
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyProductionWorkOrderInboundForm() {
|
||
return {
|
||
finished_qty: 0,
|
||
surplus_weight_kg: 0,
|
||
scrap_weight_kg: 0,
|
||
deviation_remark: "",
|
||
remark: "",
|
||
settle_work_order: false
|
||
};
|
||
}
|
||
|
||
function buildEmptyCompletionForm() {
|
||
return {
|
||
work_order_id: "",
|
||
warehouse_id: "",
|
||
receiver_employee_id: "",
|
||
product_item_id: "",
|
||
location_id: "",
|
||
max_receivable_qty: 0,
|
||
tolerance_receivable_qty: 0,
|
||
unit_weight_kg: 0,
|
||
receipt_qty: 0,
|
||
receipt_weight_kg: 0,
|
||
remark: "",
|
||
settle_work_order: false,
|
||
settle_remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyDeliveryForm() {
|
||
return {
|
||
delivery_mode: "SALES_ORDER",
|
||
sales_order_id: "",
|
||
sales_order_item_id: "",
|
||
product_item_id: "",
|
||
warehouse_id: "",
|
||
customer_id: "",
|
||
shipper_employee_id: "",
|
||
consignee_name: "",
|
||
consignee_phone: "",
|
||
delivery_address: "",
|
||
delivery_qty: 0,
|
||
waybill_no: "",
|
||
order_photo_url: "",
|
||
freight_amount: "",
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyReturnInboundForm() {
|
||
return {
|
||
warehouse_id: "",
|
||
delivery_item_id: "",
|
||
return_qty: 0,
|
||
return_weight_kg: 0,
|
||
handler_employee_id: "",
|
||
return_reason: "",
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyReturnScrapInboundForm() {
|
||
return {
|
||
warehouse_id: "",
|
||
delivery_item_id: "",
|
||
scrap_qty: 0,
|
||
scrap_weight_kg: 0,
|
||
unit_cost: "",
|
||
handler_employee_id: "",
|
||
return_reason: "",
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function buildEmptyReturnReworkForm() {
|
||
return {
|
||
return_item_id: "",
|
||
extra_cost_amount: 0,
|
||
remark: ""
|
||
};
|
||
}
|
||
|
||
function isFreightBlank(value) {
|
||
return value === "" || value === null || value === undefined || Number.isNaN(Number(value));
|
||
}
|
||
|
||
function validateLogisticsForm(form) {
|
||
if (!String(form.waybill_no || "").trim() && !String(form.order_photo_url || "").trim()) {
|
||
return "请填写运单号或上传辅助照片";
|
||
}
|
||
if (isFreightBlank(form.freight_amount)) {
|
||
return "请填写运费";
|
||
}
|
||
if (Number(form.freight_amount) < 0) {
|
||
return "运费不能小于0";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
async function handleLogisticsPhotoUpload(form, event) {
|
||
const file = event.target.files?.[0];
|
||
if (!file) {
|
||
return;
|
||
}
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
logisticsPhotoUploading.value = true;
|
||
try {
|
||
const result = await uploadResource("/inventory/logistics-photos", formData);
|
||
form.order_photo_url = result.url;
|
||
feedbackMessage.value = "辅助照片已上传";
|
||
errorMessage.value = "";
|
||
} catch (error) {
|
||
form.order_photo_url = "";
|
||
errorMessage.value = error.message || "辅助照片上传失败";
|
||
} finally {
|
||
logisticsPhotoUploading.value = false;
|
||
event.target.value = "";
|
||
}
|
||
}
|
||
|
||
async function openLogisticsPhoto(url) {
|
||
if (!url) {
|
||
return;
|
||
}
|
||
try {
|
||
await openResource(url);
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "辅助照片打开失败";
|
||
}
|
||
}
|
||
|
||
function formatFreight(value) {
|
||
if (value === null || value === undefined || value === "") {
|
||
return "-";
|
||
}
|
||
return `¥${formatAmount(value)}`;
|
||
}
|
||
|
||
function formatPercent(value) {
|
||
const numeric = Number(value || 0);
|
||
if (!Number.isFinite(numeric)) {
|
||
return "-";
|
||
}
|
||
return `${(numeric * 100).toFixed(2)}%`;
|
||
}
|
||
|
||
function resetFeedback() {
|
||
feedbackMessage.value = "";
|
||
errorMessage.value = "";
|
||
lastWarehouseArchiveResult.value = null;
|
||
}
|
||
|
||
function formatArchiveFeedback(result) {
|
||
if (!result?.archive_status) {
|
||
return "";
|
||
}
|
||
if (result.archive_status === "已归档") {
|
||
return ",PDF归档已生成";
|
||
}
|
||
if (result.archive_status === "归档失败") {
|
||
return `,PDF归档失败${result.archive_error_message ? `:${result.archive_error_message}` : ""}`;
|
||
}
|
||
return `,PDF归档状态:${result.archive_status}`;
|
||
}
|
||
|
||
function rememberWarehouseArchiveResult(result, fallbackLabel = "仓库出入库单") {
|
||
if (!result?.archive_business_id) {
|
||
lastWarehouseArchiveResult.value = null;
|
||
return;
|
||
}
|
||
lastWarehouseArchiveResult.value = {
|
||
archive_status: result.archive_status || "未生成",
|
||
archive_business_id: result.archive_business_id,
|
||
archive_document_type: result.archive_document_type || "仓库出入库单",
|
||
archive_error_message: result.archive_error_message || "",
|
||
txn_no: result.lot_no || result.material_lot_no || fallbackLabel
|
||
};
|
||
}
|
||
|
||
function resetGenericForm() {
|
||
Object.assign(genericForm, buildEmptyGenericForm());
|
||
genericForm.warehouse_id = activeWarehouses.value[0]?.id || "";
|
||
genericForm.unit_cost = activeOperation.value?.bizType === "CUSTOMER_SUPPLIED" ? 0 : 0;
|
||
genericSourceLotOptionsRequestSeq += 1;
|
||
selectedGenericSourceLots.value = [];
|
||
genericSourceLotOptions.value = [];
|
||
}
|
||
|
||
function resetSpecialAdjustmentForm() {
|
||
Object.assign(specialAdjustmentForm, buildEmptySpecialAdjustmentForm());
|
||
specialAdjustmentForm.direction = activeOperation.value?.direction === "out" ? "OUT" : "IN";
|
||
specialAdjustmentForm.warehouse_id = activeWarehouses.value[0]?.id || "";
|
||
}
|
||
|
||
function resetRawReturnForm() {
|
||
rawReturnLinksRequestSeq += 1;
|
||
Object.assign(rawReturnForm, buildEmptyRawReturnForm());
|
||
rawReturnForm.warehouse_id = activeWarehouses.value[0]?.id || "";
|
||
rawReturnPurchaseLinks.value = [];
|
||
rawReturnLinkLoading.value = false;
|
||
}
|
||
|
||
function buildInventoryLotNoPreview(material, lotRows) {
|
||
const materialItemId = Number(material?.item_id || material?.id || 0);
|
||
if (!materialItemId) {
|
||
return "";
|
||
}
|
||
const materialCode = String(material?.item_code || "");
|
||
const digits = materialCode.replace(/\D/g, "");
|
||
const codeTail = digits ? digits.slice(-5).padStart(5, "0") : String(materialItemId % 100000).padStart(5, "0");
|
||
const rawName = String(material?.item_name || "物料");
|
||
const materialName = rawName.replace(/\s+/g, "").trim() || "物料";
|
||
const maxNameLength = 50 - "JH_".length - codeTail.length - "_0001".length;
|
||
const prefix = `JH_${materialName.slice(0, maxNameLength)}${codeTail}_`;
|
||
const maxSequence = lotRows
|
||
.filter((lot) => Number(lot.item_id || 0) === materialItemId)
|
||
.reduce((currentMax, lot) => {
|
||
const match = String(lot.lot_no || "").match(/_(\d{4})$/);
|
||
return match ? Math.max(currentMax, Number(match[1])) : currentMax;
|
||
}, 0);
|
||
return `${prefix}${String(maxSequence + 1).padStart(4, "0")}`;
|
||
}
|
||
|
||
function formatGenericItemOption(item) {
|
||
if (activeWarehouseType.value === "SEMI") {
|
||
const operationLabel = item.operation_label || item.source_material_summary || "";
|
||
const specificationLabel = item.specification ? ` · ${item.specification}` : "";
|
||
const unitLabel = formatInventoryUnitLabel(item);
|
||
const baseLabel = `${item.item_code} · ${item.item_name}${specificationLabel}${unitLabel !== "-" ? ` · ${unitLabel}` : ""}${item.version_no ? ` · ${item.version_no}` : ""}`;
|
||
const stockLabel = activeOperation.value?.direction === "out" ? ` · ${formatSemiAvailability(item)}` : "";
|
||
return `${baseLabel}${operationLabel ? ` · ${operationLabel}` : ""}${stockLabel}`;
|
||
}
|
||
const specificationLabel = item.specification ? ` · ${item.specification}` : "";
|
||
const unitLabel = formatInventoryUnitLabel(item);
|
||
const baseLabel = `${item.item_code} · ${item.item_name}${specificationLabel}${unitLabel !== "-" ? ` · ${unitLabel}` : ""}${item.version_no ? ` · ${item.version_no}` : ""}`;
|
||
if (activeOperation.value?.direction !== "out") {
|
||
return baseLabel;
|
||
}
|
||
if (activeWarehouseType.value === "AUX") {
|
||
return `${baseLabel} · 可用 ${formatQty(item.qty_available)}`;
|
||
}
|
||
if (activeWeightOnly.value) {
|
||
return `${baseLabel} · 可用 ${formatWeight(item.weight_available_kg)}`;
|
||
}
|
||
return `${baseLabel} · 可用 ${formatQty(item.qty_available)} / ${formatWeight(item.weight_available_kg)}`;
|
||
}
|
||
|
||
function formatWorkOrderLedgerOption(workOrder, bizType) {
|
||
const baseLabel = `${workOrder.work_order_no || "-"} · ${workOrder.product_name || "-"}`;
|
||
if (bizType === "PRODUCTION_SCRAP_IN") {
|
||
return `${baseLabel} · 库外未闭环 ${formatWeight(workOrder.outside_weight_kg || workOrder.limits?.max_scrap_inbound_weight_kg)}`;
|
||
}
|
||
if (bizType === "REWORK_SCRAP_IN") {
|
||
return `${baseLabel} · 返工废料入库`;
|
||
}
|
||
if (bizType === "PRODUCTION_SURPLUS") {
|
||
return `${baseLabel} · 可归还余料 ${formatWeight(workOrder.outside_weight_kg || workOrder.limits?.max_surplus_inbound_weight_kg)}`;
|
||
}
|
||
return `${baseLabel} · 可入库 ${formatQty(workOrder.limits?.max_finished_inbound_qty)}`;
|
||
}
|
||
|
||
function formatProductionWorkOrderInboundOption(workOrder) {
|
||
return [
|
||
workOrder.material_lot_no || workOrder.source_lot_summary || workOrder.work_order_no || "-",
|
||
workOrder.product_name || "-",
|
||
`累计领料 ${formatWeight(workOrder.total_issued_weight_kg || workOrder.issued_weight_kg)}`,
|
||
`库外未闭环 ${formatWeight(workOrder.outside_weight_kg)}`
|
||
].join(" · ");
|
||
}
|
||
|
||
function buildWorkOrderSourceLotOptions(workOrder) {
|
||
if (!workOrder) {
|
||
return [];
|
||
}
|
||
const stockLotById = new Map(lots.value.map((lot) => [Number(lot.lot_id || lot.id || 0), lot]));
|
||
return (workOrder.materials || [])
|
||
.map((material) => {
|
||
const sourceLotId = Number(material.source_lot_id || 0);
|
||
const stockLot = stockLotById.get(sourceLotId) || {};
|
||
const issuedWeight = Number(material.issued_weight_kg || 0);
|
||
const returnedWeight = Number(material.returned_weight_kg || 0);
|
||
const returnLimit = computeWorkOrderSourceLotReturnLimit(workOrder, material);
|
||
return {
|
||
...stockLot,
|
||
lot_id: sourceLotId,
|
||
lot_no: material.source_lot_no || stockLot.lot_no || "",
|
||
item_id: Number(material.material_item_id || stockLot.item_id || 0),
|
||
item_code: material.material_code || stockLot.item_code || "",
|
||
item_name: material.material_name || stockLot.item_name || "",
|
||
warehouse_id: stockLot.warehouse_id || genericForm.warehouse_id,
|
||
warehouse_type: stockLot.warehouse_type || "RAW",
|
||
remaining_weight_kg: Number(stockLot.remaining_weight_kg || 0),
|
||
issued_weight_kg: issuedWeight,
|
||
returned_weight_kg: returnedWeight,
|
||
source_remaining_weight_kg: Math.max(Number((issuedWeight - returnedWeight).toFixed(3)), 0),
|
||
max_return_weight_kg: returnLimit.normal,
|
||
tolerance_return_weight_kg: returnLimit.tolerance,
|
||
unit_cost: Number(material.unit_cost ?? stockLot.unit_cost ?? 0)
|
||
};
|
||
})
|
||
.filter((lot) => lot.lot_id && lot.item_id);
|
||
}
|
||
|
||
function buildScrapSaleSourceLotOptions(scrapItemId, warehouseId) {
|
||
const itemId = Number(scrapItemId || 0);
|
||
const selectedWarehouseId = Number(warehouseId || 0);
|
||
if (!itemId || !selectedWarehouseId) {
|
||
return [];
|
||
}
|
||
return lots.value
|
||
.filter(
|
||
(lot) =>
|
||
Number(lot.item_id || 0) === itemId &&
|
||
Number(lot.warehouse_id || 0) === selectedWarehouseId &&
|
||
normalizeWarehouseType(lot) === "SCRAP"
|
||
)
|
||
.map((lot) => {
|
||
const sourceLotNo =
|
||
lot.source_material_lot_no ||
|
||
lot.source_material_sub_batch_no ||
|
||
extractSummaryPart(lot.source_material_summary, "来源库存批次号") ||
|
||
lot.lot_no ||
|
||
"";
|
||
const availableWeight = Math.max(Number(lot.remaining_weight_kg || 0) - Number(lot.locked_weight_kg || 0), 0);
|
||
const isAvailable =
|
||
String(lot.status || "").toUpperCase() === "AVAILABLE" &&
|
||
String(lot.quality_status || "").toUpperCase() === "PASS" &&
|
||
availableWeight > 0;
|
||
return {
|
||
...lot,
|
||
lot_id: Number(lot.lot_id || lot.id || 0),
|
||
lot_no: sourceLotNo,
|
||
source_stock_lot_no: lot.lot_no,
|
||
source_material_sub_batch_no: sourceLotNo,
|
||
remaining_weight_kg: Number(availableWeight.toFixed(3)),
|
||
is_available_for_issue: isAvailable,
|
||
disabled_reason: isAvailable ? null : "该废料批次不可用或无剩余重量"
|
||
};
|
||
})
|
||
.filter((lot) => lot.lot_id && lot.lot_no)
|
||
.sort((left, right) => {
|
||
const leftRank = left.is_available_for_issue ? 0 : 1;
|
||
const rightRank = right.is_available_for_issue ? 0 : 1;
|
||
if (leftRank !== rightRank) {
|
||
return leftRank - rightRank;
|
||
}
|
||
return String(left.lot_no || "").localeCompare(String(right.lot_no || ""), "zh-CN", { numeric: true }) || Number(left.lot_id) - Number(right.lot_id);
|
||
});
|
||
}
|
||
|
||
function computeWorkOrderSourceLotReturnLimit(workOrder, material) {
|
||
const issuedWeight = Number(material?.issued_weight_kg || 0);
|
||
const returnedWeight = Number(material?.returned_weight_kg || 0);
|
||
const issuedWorkOrderWeight = Number(workOrder?.limits?.issued_weight_kg || workOrder?.issued_weight_kg || 0);
|
||
const usedWorkOrderWeight = Number(workOrder?.limits?.used_material_weight_kg || workOrder?.used_material_weight_kg || 0);
|
||
const sourceUsedWeight = issuedWorkOrderWeight > 0 ? (usedWorkOrderWeight * issuedWeight) / issuedWorkOrderWeight : 0;
|
||
const sourceExpectedReturnWeight = Math.max(issuedWeight - sourceUsedWeight, 0);
|
||
const sourceLotRemaining = Math.max(sourceExpectedReturnWeight - returnedWeight, 0);
|
||
const sourceIssuedRemaining = Math.max(issuedWeight - returnedWeight, 0);
|
||
const normalWorkOrderLimit = Number(workOrder?.limits?.max_surplus_inbound_weight_kg || workOrder?.max_surplus_inbound_weight_kg || 0);
|
||
const workOrderReturnedWeight = Number(workOrder?.limits?.returned_material_weight_kg || workOrder?.returned_material_weight_kg || 0);
|
||
const expectedWorkOrderReturnWeight = issuedWorkOrderWeight > 0 ? issuedWorkOrderWeight - usedWorkOrderWeight : workOrderReturnedWeight + normalWorkOrderLimit;
|
||
const toleranceWorkOrderLimit = Number(
|
||
workOrder?.limits?.tolerance_surplus_inbound_weight_kg ??
|
||
Math.max(expectedWorkOrderReturnWeight * 1.15 - workOrderReturnedWeight, 0)
|
||
);
|
||
const sourceToleranceLimit = Math.max(sourceExpectedReturnWeight * 1.15 - returnedWeight, 0);
|
||
return {
|
||
normal: Number(Math.min(sourceLotRemaining, Math.max(normalWorkOrderLimit, 0)).toFixed(3)),
|
||
tolerance: Number(Math.min(sourceIssuedRemaining, sourceToleranceLimit, Math.max(toleranceWorkOrderLimit, 0)).toFixed(3))
|
||
};
|
||
}
|
||
|
||
function productionSurplusReturnInputMax(lot) {
|
||
if (!isProductionSurplusInbound.value) {
|
||
return lot?.tolerance_return_weight_kg ?? lot?.max_return_weight_kg ?? undefined;
|
||
}
|
||
return lot?.source_remaining_weight_kg ?? undefined;
|
||
}
|
||
|
||
function productionSurplusReturnInputDisabled(lot) {
|
||
return isProductionSurplusInbound.value && Number(lot?.source_remaining_weight_kg || 0) <= 0;
|
||
}
|
||
|
||
function applyGenericWorkOrder() {
|
||
selectedGenericSourceLots.value = [];
|
||
genericSourceLotOptions.value = [];
|
||
genericForm.settle_work_order = false;
|
||
genericForm.settle_remark = "";
|
||
const workOrder = selectedGenericWorkOrder.value;
|
||
if (!workOrder) {
|
||
genericForm.item_option_key = "";
|
||
genericForm.item_id = "";
|
||
genericForm.source_doc_id = null;
|
||
genericForm.source_line_id = null;
|
||
genericForm.source_material_sub_batch_no = "";
|
||
genericForm.weight_kg = 0;
|
||
return;
|
||
}
|
||
const firstMaterial = (workOrder.materials || [])[0] || null;
|
||
if (isReworkScrapInbound.value) {
|
||
genericForm.item_option_key = String(workOrder.product_item_id || "");
|
||
genericForm.item_id = Number(workOrder.product_item_id || 0);
|
||
genericForm.unit_cost = 0;
|
||
} else if (firstMaterial) {
|
||
genericForm.item_option_key = String(firstMaterial.material_item_id);
|
||
genericForm.item_id = Number(firstMaterial.material_item_id);
|
||
genericForm.unit_cost = Number(firstMaterial.unit_cost || 0);
|
||
}
|
||
genericForm.source_doc_id = Number(workOrder.work_order_id);
|
||
genericForm.source_line_id = null;
|
||
genericForm.source_material_sub_batch_no = workOrder.source_lot_summary || "";
|
||
genericForm.provider_name = workOrder.work_order_no || "";
|
||
if (isProductionScrapInbound.value) {
|
||
genericForm.weight_kg = Number(workOrder.limits?.max_scrap_inbound_weight_kg || 0);
|
||
genericForm.qty = 0;
|
||
} else if (isReworkScrapInbound.value) {
|
||
genericForm.weight_kg = 0;
|
||
genericForm.qty = Number(workOrder.limits?.max_rework_scrap_inbound_qty || workOrder.max_rework_scrap_inbound_qty || 0);
|
||
} else if (isProductionSurplusInbound.value) {
|
||
genericForm.weight_kg = 0;
|
||
}
|
||
if (isProductionSurplusInbound.value) {
|
||
loadGenericSourceLotOptions(genericSourceLotMaterialIds.value);
|
||
}
|
||
}
|
||
|
||
function handleGenericMeasureBasisChange() {
|
||
if (genericForm.measure_basis === "PIECE") {
|
||
genericForm.weight_kg = 0;
|
||
} else if (genericForm.measure_basis === "WEIGHT") {
|
||
genericForm.qty = 0;
|
||
}
|
||
}
|
||
|
||
function syncGenericSemiMeasureBasis() {
|
||
if (isSemiSourceLotTraceInbound.value) {
|
||
const existingBasis = normalizeSemiMeasureBasis(selectedSemiExistingMeasureBasis.value);
|
||
if (["PIECE", "WEIGHT"].includes(existingBasis)) {
|
||
genericForm.measure_basis = existingBasis;
|
||
} else if (!["PIECE", "WEIGHT"].includes(normalizeSemiMeasureBasis(genericForm.measure_basis))) {
|
||
genericForm.measure_basis = "";
|
||
}
|
||
handleGenericMeasureBasisChange();
|
||
return;
|
||
}
|
||
if (isSemiAutoSourceOutbound.value) {
|
||
genericForm.measure_basis = normalizeSemiMeasureBasis(selectedGenericItemOption.value?.measure_basis || selectedSemiExistingMeasureBasis.value);
|
||
handleGenericMeasureBasisChange();
|
||
}
|
||
}
|
||
|
||
function handleGenericItemChange() {
|
||
const selectedOption = selectedGenericItemOption.value;
|
||
genericForm.item_id = selectedOption?.item_id || "";
|
||
genericForm.source_doc_id = selectedOption?.route_id || selectedOption?.source_doc_id || null;
|
||
genericForm.source_line_id = selectedOption?.route_operation_id || selectedOption?.source_line_id || null;
|
||
syncGenericSemiMeasureBasis();
|
||
}
|
||
|
||
function formatLotQualityRelease(lot) {
|
||
const qualityStatus = String(lot?.quality_status || "").toUpperCase();
|
||
if (qualityStatus === "PENDING_QC") {
|
||
return "待放行";
|
||
}
|
||
return lot?.quality_user_name || formatQualityStatusLabel(qualityStatus);
|
||
}
|
||
|
||
function handleGenericWarehouseChange() {
|
||
genericForm.location_id = "";
|
||
if (!genericItemOptions.value.some((item) => String(item.option_key || item.item_id) === String(genericForm.item_option_key || ""))) {
|
||
genericForm.item_option_key = "";
|
||
genericForm.item_id = "";
|
||
genericForm.source_doc_id = null;
|
||
genericForm.source_line_id = null;
|
||
genericForm.measure_basis = "";
|
||
genericForm.qty = 0;
|
||
genericForm.weight_kg = 0;
|
||
}
|
||
syncGenericSemiMeasureBasis();
|
||
}
|
||
|
||
function buildSpecialAdjustmentEmptyLine() {
|
||
return {
|
||
row_key: `special-adjustment-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||
inbound_mode: "EXISTING",
|
||
lot_id: "",
|
||
item_option_key: "",
|
||
item_id: "",
|
||
item_name: "",
|
||
lot_no: "",
|
||
location_id: "",
|
||
measure_mode: activeWarehouseType.value === "SEMI" ? "QTY" : specialAdjustmentMeasureMode(activeWarehouseType.value),
|
||
adjust_qty: "",
|
||
adjust_weight_kg: "",
|
||
unit_cost: "",
|
||
line_reason: ""
|
||
};
|
||
}
|
||
|
||
function resetSpecialAdjustmentLineSelection(line, inboundMode = line?.inbound_mode || "EXISTING") {
|
||
const rowKey = line?.row_key || `special-adjustment-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||
Object.assign(line, {
|
||
...buildSpecialAdjustmentEmptyLine(),
|
||
row_key: rowKey,
|
||
inbound_mode: inboundMode,
|
||
measure_mode: activeWarehouseType.value === "SEMI" ? "QTY" : specialAdjustmentMeasureMode(activeWarehouseType.value)
|
||
});
|
||
}
|
||
|
||
function handleSpecialAdjustmentWarehouseChange() {
|
||
specialAdjustmentForm.lines = [];
|
||
}
|
||
|
||
function addSpecialAdjustmentLine() {
|
||
resetFeedback();
|
||
if (!String(specialAdjustmentForm.reason || "").trim()) {
|
||
errorMessage.value = `${specialAdjustmentDirectionText.value}必须先填写调整说明`;
|
||
return;
|
||
}
|
||
specialAdjustmentForm.lines.push(buildSpecialAdjustmentEmptyLine());
|
||
}
|
||
|
||
function removeSpecialAdjustmentLine(index) {
|
||
specialAdjustmentForm.lines.splice(index, 1);
|
||
}
|
||
|
||
function selectedSpecialAdjustmentLot(line) {
|
||
return specialAdjustmentLotOptions.value.find((lot) => Number(lot.lot_id || 0) === Number(line?.lot_id || 0)) || null;
|
||
}
|
||
|
||
function selectedSpecialAdjustmentItem(line) {
|
||
return specialAdjustmentItemOptions.value.find((item) => String(item.option_key || item.item_id) === String(line?.item_option_key || "")) || null;
|
||
}
|
||
|
||
function specialAdjustmentLineUsesExistingLot(line) {
|
||
return specialAdjustmentForm.direction === "OUT" || line?.inbound_mode !== "NEW";
|
||
}
|
||
|
||
function specialAdjustmentRawMeasureMode(line) {
|
||
return specialAdjustmentMeasureMode(activeWarehouseType.value, selectedSpecialAdjustmentLot(line) || {});
|
||
}
|
||
|
||
function specialAdjustmentEffectiveMeasureMode(line) {
|
||
const rawMode = specialAdjustmentRawMeasureMode(line);
|
||
if (rawMode === "SEMI_SELECT") {
|
||
return line.measure_mode === "WEIGHT" ? "WEIGHT" : "QTY";
|
||
}
|
||
return rawMode;
|
||
}
|
||
|
||
function handleSpecialAdjustmentMeasureModeChange(line) {
|
||
if (line.measure_mode === "WEIGHT") {
|
||
line.adjust_qty = "";
|
||
return;
|
||
}
|
||
if (line.measure_mode === "QTY") {
|
||
line.adjust_weight_kg = "";
|
||
}
|
||
}
|
||
|
||
function handleSpecialAdjustmentInboundModeChange(line) {
|
||
resetSpecialAdjustmentLineSelection(line, line.inbound_mode === "NEW" ? "NEW" : "EXISTING");
|
||
}
|
||
|
||
function applySpecialAdjustmentLot(line) {
|
||
const lot = selectedSpecialAdjustmentLot(line);
|
||
if (!lot) {
|
||
resetSpecialAdjustmentLineSelection(line, line.inbound_mode || "EXISTING");
|
||
return;
|
||
}
|
||
line.inbound_mode = "EXISTING";
|
||
line.item_option_key = "";
|
||
line.item_id = lot.item_id;
|
||
line.item_name = lot.item_name || lot.item_code || "";
|
||
line.lot_no = lot.lot_no || "";
|
||
line.location_id = lot.location_id || "";
|
||
line.unit_cost = Number(lot.unit_cost || 0);
|
||
const measureMode = specialAdjustmentRawMeasureMode(line);
|
||
line.measure_mode = measureMode === "SEMI_SELECT" ? "QTY" : measureMode;
|
||
handleSpecialAdjustmentMeasureModeChange(line);
|
||
}
|
||
|
||
function applySpecialAdjustmentItem(line) {
|
||
const item = selectedSpecialAdjustmentItem(line);
|
||
if (!item) {
|
||
resetSpecialAdjustmentLineSelection(line, "NEW");
|
||
return;
|
||
}
|
||
line.inbound_mode = "NEW";
|
||
line.lot_id = "";
|
||
line.item_id = Number(item.item_id || 0);
|
||
line.item_name = [item.item_code, item.item_name].filter(Boolean).join(" · ") || item.item_name || item.item_code || "";
|
||
line.lot_no = "";
|
||
line.location_id = "";
|
||
line.unit_cost = Number(item.unit_cost ?? item.avg_unit_cost ?? 0);
|
||
const measureMode = specialAdjustmentMeasureMode(activeWarehouseType.value, item || {});
|
||
line.measure_mode = measureMode === "SEMI_SELECT" ? "QTY" : measureMode;
|
||
handleSpecialAdjustmentMeasureModeChange(line);
|
||
}
|
||
|
||
function formatSpecialAdjustmentMeasureMode(line) {
|
||
const mode = specialAdjustmentEffectiveMeasureMode(line);
|
||
if (mode === "WEIGHT") {
|
||
return "按重量调整";
|
||
}
|
||
if (mode === "QTY") {
|
||
return "按数量调整";
|
||
}
|
||
return "按数量并记录辅助重量";
|
||
}
|
||
|
||
function specialAdjustmentShowQtyInput(line) {
|
||
return ["QTY", "QTY_WITH_WEIGHT"].includes(specialAdjustmentEffectiveMeasureMode(line));
|
||
}
|
||
|
||
function specialAdjustmentShowWeightInput(line) {
|
||
return ["WEIGHT", "QTY_WITH_WEIGHT"].includes(specialAdjustmentEffectiveMeasureMode(line));
|
||
}
|
||
|
||
function specialAdjustmentWeightRequired(line) {
|
||
return specialAdjustmentEffectiveMeasureMode(line) === "WEIGHT";
|
||
}
|
||
|
||
function formatSpecialAdjustmentLotOption(lot) {
|
||
const itemLabel = [lot.item_code, lot.item_name].filter(Boolean).join(" · ") || "库存物料";
|
||
const lotLabel = lot.lot_no || "-";
|
||
const qtyLabel = formatQty(lot.remaining_qty);
|
||
const weightLabel = formatWeight(lot.remaining_weight_kg);
|
||
return `${itemLabel} · ${lotLabel} · 当前数量 ${qtyLabel} · 当前重量 ${weightLabel}`;
|
||
}
|
||
|
||
function formatSpecialAdjustmentItemOption(item) {
|
||
return formatGenericItemOption(item);
|
||
}
|
||
|
||
function specialAdjustmentLinePreview(line) {
|
||
const lot = selectedSpecialAdjustmentLot(line);
|
||
if (!lot) {
|
||
if (specialAdjustmentForm.direction === "IN" && line?.inbound_mode === "NEW") {
|
||
const itemLabel = line.item_name || "请选择物料";
|
||
const qtyChange = specialAdjustmentShowQtyInput(line) ? Number(line.adjust_qty || 0) : 0;
|
||
const weightChange = specialAdjustmentShowWeightInput(line) ? Number(line.adjust_weight_kg || 0) : 0;
|
||
if (specialAdjustmentEffectiveMeasureMode(line) === "WEIGHT") {
|
||
return `新增特殊明细:${itemLabel};入库后重量 ${formatWeight(weightChange)}`;
|
||
}
|
||
if (specialAdjustmentEffectiveMeasureMode(line) === "QTY") {
|
||
return `新增特殊明细:${itemLabel};入库后数量 ${formatQty(qtyChange)}`;
|
||
}
|
||
return `新增特殊明细:${itemLabel};入库后数量 ${formatQty(qtyChange)} / 重量 ${formatWeight(weightChange)}`;
|
||
}
|
||
return "请选择库存明细";
|
||
}
|
||
const multiplier = specialAdjustmentForm.direction === "OUT" ? -1 : 1;
|
||
const qtyChange = specialAdjustmentShowQtyInput(line) ? Number(line.adjust_qty || 0) * multiplier : 0;
|
||
const weightChange = specialAdjustmentShowWeightInput(line) ? Number(line.adjust_weight_kg || 0) * multiplier : 0;
|
||
const beforeQty = Number(lot.remaining_qty || 0);
|
||
const beforeWeight = Number(lot.remaining_weight_kg || 0);
|
||
const afterQty = Math.max(beforeQty + qtyChange, 0);
|
||
const afterWeight = Math.max(beforeWeight + weightChange, 0);
|
||
if (specialAdjustmentEffectiveMeasureMode(line) === "WEIGHT") {
|
||
return `调整前:${formatWeight(beforeWeight)};调整后:${formatWeight(afterWeight)}`;
|
||
}
|
||
if (specialAdjustmentEffectiveMeasureMode(line) === "QTY") {
|
||
return `调整前:${formatQty(beforeQty)};调整后:${formatQty(afterQty)}`;
|
||
}
|
||
return `调整前:${formatQty(beforeQty)} / ${formatWeight(beforeWeight)};调整后:${formatQty(afterQty)} / ${formatWeight(afterWeight)}`;
|
||
}
|
||
|
||
function resetProductionForm() {
|
||
Object.assign(productionForm, buildEmptyProductionForm());
|
||
productionStockLotOptionsRequestSeq += 1;
|
||
selectedProductionStockLots.value = [];
|
||
productionStockLotOptions.value = [];
|
||
}
|
||
|
||
function resetProductionWorkOrderInboundForm() {
|
||
selectedInboundWorkOrderId.value = "";
|
||
productionWorkOrderInboundPreview.value = null;
|
||
productionWorkOrderInboundPreviewRequestSeq += 1;
|
||
Object.assign(productionWorkOrderInboundForm, buildEmptyProductionWorkOrderInboundForm());
|
||
}
|
||
|
||
function resetCompletionForm() {
|
||
Object.assign(completionForm, buildEmptyCompletionForm());
|
||
completionForm.warehouse_id = finishedWarehouses.value[0]?.id || "";
|
||
}
|
||
|
||
function resetDeliveryForm() {
|
||
Object.assign(deliveryForm, buildEmptyDeliveryForm());
|
||
deliveryForm.warehouse_id = finishedWarehouses.value[0]?.id || "";
|
||
}
|
||
|
||
function setDeliveryMode(mode) {
|
||
deliveryForm.delivery_mode = mode === "DIRECT_CUSTOMER" ? "DIRECT_CUSTOMER" : "SALES_ORDER";
|
||
deliveryForm.sales_order_id = "";
|
||
deliveryForm.sales_order_item_id = "";
|
||
deliveryForm.product_item_id = "";
|
||
deliveryForm.customer_id = "";
|
||
deliveryForm.consignee_name = "";
|
||
deliveryForm.consignee_phone = "";
|
||
deliveryForm.delivery_address = "";
|
||
deliveryForm.delivery_qty = 0;
|
||
}
|
||
|
||
function resetReturnInboundForm() {
|
||
Object.assign(returnInboundForm, buildEmptyReturnInboundForm());
|
||
returnInboundForm.warehouse_id = activeWarehouses.value[0]?.id || "";
|
||
}
|
||
|
||
function resetReturnScrapInboundForm() {
|
||
Object.assign(returnScrapInboundForm, buildEmptyReturnScrapInboundForm());
|
||
returnScrapInboundForm.warehouse_id = activeWarehouses.value[0]?.id || "";
|
||
}
|
||
|
||
function resetReturnReworkForm() {
|
||
Object.assign(returnReworkForm, buildEmptyReturnReworkForm());
|
||
}
|
||
|
||
function selectInventoryTab(tabKey) {
|
||
activeInventoryTab.value = tabKey;
|
||
balancePage.value = 1;
|
||
if (tabKey !== "semi" && balanceControls.sortKey.value === "operation_label") {
|
||
balanceControls.sortKey.value = "item_code";
|
||
balanceControls.sortDirection.value = "asc";
|
||
}
|
||
}
|
||
|
||
function openWarehouseLedgerDrawer() {
|
||
resetFeedback();
|
||
warehouseLedgerDrawerOpen.value = true;
|
||
warehouseLedgerPage.value = 1;
|
||
Object.assign(warehouseLedgerFilters, buildEmptyWarehouseLedgerFilters());
|
||
loadWarehouseLedger();
|
||
}
|
||
|
||
function openProductionWorkOrderInboundDrawer() {
|
||
resetFeedback();
|
||
resetProductionWorkOrderInboundForm();
|
||
productionWorkOrderInboundDrawerOpen.value = true;
|
||
}
|
||
|
||
function closeProductionWorkOrderInboundDrawer() {
|
||
productionWorkOrderInboundDrawerOpen.value = false;
|
||
}
|
||
|
||
function closeWarehouseLedgerDrawer() {
|
||
warehouseLedgerDrawerOpen.value = false;
|
||
clearWarehouseLedgerTooltipListeners();
|
||
}
|
||
|
||
async function loadProductionWorkOrderInboundPreview(useCurrentFinishedQty = false) {
|
||
const requestSeq = ++productionWorkOrderInboundPreviewRequestSeq;
|
||
if (!Number(selectedInboundWorkOrderId.value || 0)) {
|
||
productionWorkOrderInboundPreview.value = null;
|
||
return;
|
||
}
|
||
try {
|
||
const params = new URLSearchParams({
|
||
production_ledger_id: String(selectedInboundWorkOrderId.value)
|
||
});
|
||
if (useCurrentFinishedQty) {
|
||
params.set("finished_qty", String(Number(productionWorkOrderInboundForm.finished_qty || 0)));
|
||
}
|
||
let result = await requestJson(`/production/production-ledger-inbounds/preview?${params.toString()}`, { method: "GET" });
|
||
if (requestSeq !== productionWorkOrderInboundPreviewRequestSeq) {
|
||
return;
|
||
}
|
||
if (!result?.production_ledger_id || !result?.material_lot_no) {
|
||
throw new Error("生产台账入库预览数据不完整,请刷新后重试");
|
||
}
|
||
if (!useCurrentFinishedQty) {
|
||
const defaultFinishedQty = Math.max(Number(result.reference_finished_qty || 0) - Number(result.finished_inbound_qty || 0), 0);
|
||
productionWorkOrderInboundForm.finished_qty = Number(defaultFinishedQty.toFixed(3));
|
||
if (defaultFinishedQty > 0) {
|
||
params.set("finished_qty", String(productionWorkOrderInboundForm.finished_qty));
|
||
result = await requestJson(`/production/production-ledger-inbounds/preview?${params.toString()}`, { method: "GET" });
|
||
if (requestSeq !== productionWorkOrderInboundPreviewRequestSeq) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
productionWorkOrderInboundPreview.value = result;
|
||
productionWorkOrderInboundForm.surplus_weight_kg = Number(result.theoretical_surplus_weight_kg || 0);
|
||
productionWorkOrderInboundForm.scrap_weight_kg = Number(result.theoretical_scrap_weight_kg || 0);
|
||
errorMessage.value = "";
|
||
} catch (error) {
|
||
if (requestSeq !== productionWorkOrderInboundPreviewRequestSeq) {
|
||
return;
|
||
}
|
||
productionWorkOrderInboundPreview.value = null;
|
||
errorMessage.value = error.message || "生产台账入库预览加载失败";
|
||
}
|
||
}
|
||
|
||
function handleProductionWorkOrderInboundFinishedQtyInput(event) {
|
||
const value = Number(event?.target?.value || 0);
|
||
productionWorkOrderInboundForm.finished_qty = Number.isFinite(value) ? value : 0;
|
||
loadProductionWorkOrderInboundPreview(true);
|
||
}
|
||
|
||
function resetWarehouseLedgerFilters() {
|
||
Object.assign(warehouseLedgerFilters, buildEmptyWarehouseLedgerFilters());
|
||
warehouseLedgerPage.value = 1;
|
||
loadWarehouseLedger();
|
||
}
|
||
|
||
function formatWarehouseLedgerDirection(direction) {
|
||
return {
|
||
IN: "入库",
|
||
OUT: "出库",
|
||
ADJUST: "调整"
|
||
}[String(direction || "").toUpperCase()] || "-";
|
||
}
|
||
|
||
function warehouseLedgerDirectionClass(direction) {
|
||
return {
|
||
IN: "status-success",
|
||
OUT: "status-warning",
|
||
ADJUST: "status-info"
|
||
}[String(direction || "").toUpperCase()] || "status-muted";
|
||
}
|
||
|
||
function hideWarehouseLedgerCellTooltip() {
|
||
warehouseLedgerTooltipElement?.classList.remove("table-cell-hover-tooltip-visible");
|
||
}
|
||
|
||
function ensureWarehouseLedgerTooltipElement() {
|
||
if (typeof document === "undefined") {
|
||
return null;
|
||
}
|
||
|
||
warehouseLedgerTooltipElement = document.querySelector(".table-cell-hover-tooltip");
|
||
if (!warehouseLedgerTooltipElement) {
|
||
warehouseLedgerTooltipElement = document.createElement("div");
|
||
warehouseLedgerTooltipElement.className = "table-cell-hover-tooltip";
|
||
warehouseLedgerTooltipElement.setAttribute("role", "tooltip");
|
||
document.body.appendChild(warehouseLedgerTooltipElement);
|
||
}
|
||
|
||
return warehouseLedgerTooltipElement;
|
||
}
|
||
|
||
function positionWarehouseLedgerCellTooltip(event) {
|
||
if (!warehouseLedgerTooltipElement || typeof window === "undefined") {
|
||
return;
|
||
}
|
||
|
||
const padding = 14;
|
||
const offset = 16;
|
||
const rect = warehouseLedgerTooltipElement.getBoundingClientRect();
|
||
const nextLeft = Math.min(event.clientX + offset, window.innerWidth - rect.width - padding);
|
||
const nextTop = Math.min(event.clientY + offset, window.innerHeight - rect.height - padding);
|
||
|
||
warehouseLedgerTooltipElement.style.left = `${Math.max(padding, nextLeft)}px`;
|
||
warehouseLedgerTooltipElement.style.top = `${Math.max(padding, nextTop)}px`;
|
||
}
|
||
|
||
function getWarehouseLedgerTooltipCell(event) {
|
||
const cell = event.target?.closest?.('td[data-table-tooltip="auto"]');
|
||
const table = warehouseLedgerTableRef.value;
|
||
if (!cell || !table?.contains(cell)) {
|
||
return null;
|
||
}
|
||
return cell;
|
||
}
|
||
|
||
function showWarehouseLedgerCellTooltip(cell, event) {
|
||
const fullValue = cell?.dataset?.tableFullValue || "";
|
||
if (!fullValue) {
|
||
hideWarehouseLedgerCellTooltip();
|
||
return;
|
||
}
|
||
|
||
const tooltip = ensureWarehouseLedgerTooltipElement();
|
||
if (!tooltip) {
|
||
return;
|
||
}
|
||
|
||
tooltip.textContent = fullValue;
|
||
tooltip.classList.add("table-cell-hover-tooltip-visible");
|
||
positionWarehouseLedgerCellTooltip(event);
|
||
}
|
||
|
||
function clearWarehouseLedgerTooltipListeners() {
|
||
warehouseLedgerTooltipCleanupHandlers.forEach((cleanup) => cleanup());
|
||
warehouseLedgerTooltipCleanupHandlers = [];
|
||
if (warehouseLedgerTooltipTable) {
|
||
delete warehouseLedgerTooltipTable.dataset.warehouseLedgerTooltipBound;
|
||
warehouseLedgerTooltipTable = null;
|
||
}
|
||
hideWarehouseLedgerCellTooltip();
|
||
}
|
||
|
||
function bindWarehouseLedgerCellTooltip() {
|
||
const table = warehouseLedgerTableRef.value;
|
||
if (!table || warehouseLedgerTooltipTable === table) {
|
||
return;
|
||
}
|
||
|
||
clearWarehouseLedgerTooltipListeners();
|
||
warehouseLedgerTooltipTable = table;
|
||
table.dataset.warehouseLedgerTooltipBound = "true";
|
||
|
||
const handleMouseOver = (event) => {
|
||
const cell = getWarehouseLedgerTooltipCell(event);
|
||
if (cell) {
|
||
showWarehouseLedgerCellTooltip(cell, event);
|
||
}
|
||
};
|
||
const handleMouseMove = (event) => {
|
||
const cell = getWarehouseLedgerTooltipCell(event);
|
||
if (cell) {
|
||
showWarehouseLedgerCellTooltip(cell, event);
|
||
}
|
||
};
|
||
const handleMouseOut = (event) => {
|
||
const cell = getWarehouseLedgerTooltipCell(event);
|
||
if (cell && !cell.contains(event.relatedTarget)) {
|
||
hideWarehouseLedgerCellTooltip();
|
||
}
|
||
};
|
||
const handleDocumentMouseMove = (event) => {
|
||
if (!getWarehouseLedgerTooltipCell(event)) {
|
||
hideWarehouseLedgerCellTooltip();
|
||
}
|
||
};
|
||
|
||
table.addEventListener("mouseover", handleMouseOver);
|
||
table.addEventListener("mousemove", handleMouseMove);
|
||
table.addEventListener("mouseout", handleMouseOut);
|
||
document.addEventListener("mousemove", handleDocumentMouseMove, true);
|
||
window.addEventListener("scroll", hideWarehouseLedgerCellTooltip, true);
|
||
window.addEventListener("blur", hideWarehouseLedgerCellTooltip);
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => table.removeEventListener("mouseover", handleMouseOver));
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => table.removeEventListener("mousemove", handleMouseMove));
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => table.removeEventListener("mouseout", handleMouseOut));
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => document.removeEventListener("mousemove", handleDocumentMouseMove, true));
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => window.removeEventListener("scroll", hideWarehouseLedgerCellTooltip, true));
|
||
warehouseLedgerTooltipCleanupHandlers.push(() => window.removeEventListener("blur", hideWarehouseLedgerCellTooltip));
|
||
}
|
||
|
||
function buildWarehouseLedgerParams({ includePagination = true } = {}) {
|
||
const params = new URLSearchParams({
|
||
warehouse_type: activeWarehouseType.value,
|
||
direction: warehouseLedgerFilters.direction || "ALL",
|
||
sort_key: warehouseLedgerFilters.sort_key || "biz_time",
|
||
sort_direction: warehouseLedgerFilters.sort_direction || "desc"
|
||
});
|
||
if (includePagination) {
|
||
params.set("page", String(warehouseLedgerPage.value));
|
||
params.set("page_size", String(warehouseLedgerPageSize.value));
|
||
}
|
||
if (warehouseLedgerFilters.keyword) {
|
||
params.set("keyword", warehouseLedgerFilters.keyword);
|
||
}
|
||
if (warehouseLedgerFilters.txn_type) {
|
||
params.set("txn_type", warehouseLedgerFilters.txn_type);
|
||
}
|
||
if (warehouseLedgerFilters.start_date) {
|
||
params.set("start_date", warehouseLedgerFilters.start_date);
|
||
}
|
||
if (warehouseLedgerFilters.end_date) {
|
||
params.set("end_date", warehouseLedgerFilters.end_date);
|
||
}
|
||
return params;
|
||
}
|
||
|
||
function warehouseLedgerArchiveTypeKey(row) {
|
||
const documentType = String(row?.archive_document_type || "");
|
||
if (documentType === "生产领料出库单") {
|
||
return "production-material-out";
|
||
}
|
||
if (documentType === "生产入库结算单") {
|
||
return "production-inbound-settlement";
|
||
}
|
||
return "warehouse-operation";
|
||
}
|
||
|
||
function warehouseLedgerArchiveBusinessId(row) {
|
||
return Number(row?.archive_business_id || row?.inventory_txn_id || 0);
|
||
}
|
||
|
||
async function previewWarehouseLedgerArchive(row) {
|
||
feedbackMessage.value = "";
|
||
errorMessage.value = "";
|
||
const businessId = warehouseLedgerArchiveBusinessId(row);
|
||
if (!businessId) {
|
||
errorMessage.value = "该流水缺少归档业务ID,无法预览PDF";
|
||
return;
|
||
}
|
||
try {
|
||
await openResource(`/document-archives/${warehouseLedgerArchiveTypeKey(row)}/${businessId}/latest/preview`);
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "PDF归档预览失败";
|
||
}
|
||
}
|
||
|
||
async function downloadWarehouseLedgerArchive(row) {
|
||
feedbackMessage.value = "";
|
||
errorMessage.value = "";
|
||
const businessId = warehouseLedgerArchiveBusinessId(row);
|
||
if (!businessId) {
|
||
errorMessage.value = "该流水缺少归档业务ID,无法下载PDF";
|
||
return;
|
||
}
|
||
try {
|
||
const filename = `${row?.txn_no || row?.archive_document_type || "仓库出入库单"}归档.pdf`;
|
||
await downloadResource(`/document-archives/${warehouseLedgerArchiveTypeKey(row)}/${businessId}/latest/download`, filename);
|
||
feedbackMessage.value = "PDF归档已下载";
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "PDF归档下载失败";
|
||
}
|
||
}
|
||
|
||
async function regenerateWarehouseLedgerArchive(row) {
|
||
feedbackMessage.value = "";
|
||
errorMessage.value = "";
|
||
const businessId = warehouseLedgerArchiveBusinessId(row);
|
||
if (!businessId) {
|
||
errorMessage.value = "该流水缺少归档业务ID,无法重新生成PDF";
|
||
return;
|
||
}
|
||
try {
|
||
await postResource(`/document-archives/${warehouseLedgerArchiveTypeKey(row)}/${businessId}/generate`, {});
|
||
feedbackMessage.value = "PDF归档已重新生成";
|
||
await loadWarehouseLedger();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "PDF归档重新生成失败";
|
||
}
|
||
}
|
||
|
||
function warehouseLedgerExportFilename() {
|
||
const start = warehouseLedgerFilters.start_date || "全部";
|
||
const end = warehouseLedgerFilters.end_date || "全部";
|
||
return `${activeInventoryLabel.value}出入库流水_${start}_${end}.xlsx`;
|
||
}
|
||
|
||
async function exportWarehouseLedgerExcel() {
|
||
if (!warehouseLedgerFilters.start_date || !warehouseLedgerFilters.end_date) {
|
||
feedbackMessage.value = "当前未选择完整时间范围,将导出全部匹配流水";
|
||
}
|
||
warehouseLedgerExporting.value = true;
|
||
try {
|
||
const params = buildWarehouseLedgerParams({ includePagination: false });
|
||
await downloadResource(`/inventory/transaction-ledger/export?${params.toString()}`, warehouseLedgerExportFilename());
|
||
if (warehouseLedgerFilters.start_date && warehouseLedgerFilters.end_date) {
|
||
feedbackMessage.value = "流水Excel已导出";
|
||
}
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "流水Excel导出失败";
|
||
} finally {
|
||
warehouseLedgerExporting.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadWarehouseLedger() {
|
||
warehouseLedgerLoading.value = true;
|
||
try {
|
||
const params = buildWarehouseLedgerParams();
|
||
const result = await fetchResource(`/inventory/transaction-ledger?${params.toString()}`, {
|
||
items: [],
|
||
total: 0,
|
||
summary: buildEmptyWarehouseLedgerSummary()
|
||
});
|
||
warehouseLedgerRows.value = Array.isArray(result.items) ? result.items : [];
|
||
warehouseLedgerTotal.value = Number(result.total || 0);
|
||
Object.assign(warehouseLedgerSummary, buildEmptyWarehouseLedgerSummary(), result.summary || {});
|
||
errorMessage.value = "";
|
||
} catch (error) {
|
||
warehouseLedgerRows.value = [];
|
||
warehouseLedgerTotal.value = 0;
|
||
Object.assign(warehouseLedgerSummary, buildEmptyWarehouseLedgerSummary());
|
||
errorMessage.value = error.message || "仓库出入库流水加载失败";
|
||
} finally {
|
||
warehouseLedgerLoading.value = false;
|
||
await nextTick();
|
||
bindWarehouseLedgerCellTooltip();
|
||
}
|
||
}
|
||
|
||
function openOperationDrawer(operation) {
|
||
activeOperation.value = operation;
|
||
resetFeedback();
|
||
if (operation.bizType === "OPENING" && operation.direction === "in") {
|
||
openingImportDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "production") {
|
||
resetProductionForm();
|
||
productionDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "rawReturn") {
|
||
resetRawReturnForm();
|
||
rawReturnDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "completion" || operation.drawerType === "reworkCompletion") {
|
||
resetCompletionForm();
|
||
completionDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "productionLedgerInbound") {
|
||
openProductionWorkOrderInboundDrawer();
|
||
return;
|
||
}
|
||
if (operation.drawerType === "delivery") {
|
||
resetDeliveryForm();
|
||
deliveryDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "returnInbound") {
|
||
resetReturnInboundForm();
|
||
returnInboundDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "returnScrapInbound") {
|
||
resetReturnScrapInboundForm();
|
||
returnScrapInboundDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "returnRework") {
|
||
resetReturnReworkForm();
|
||
returnReworkDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
if (operation.drawerType === "specialAdjustment") {
|
||
resetSpecialAdjustmentForm();
|
||
specialAdjustmentDrawerOpen.value = true;
|
||
return;
|
||
}
|
||
resetGenericForm();
|
||
genericDrawerOpen.value = true;
|
||
}
|
||
|
||
function closeGenericDrawer() {
|
||
genericDrawerOpen.value = false;
|
||
}
|
||
|
||
function closeSpecialAdjustmentDrawer() {
|
||
specialAdjustmentDrawerOpen.value = false;
|
||
}
|
||
|
||
function closeRawReturnDrawer() {
|
||
rawReturnDrawerOpen.value = false;
|
||
resetRawReturnForm();
|
||
}
|
||
|
||
function closeReturnInboundDrawer() {
|
||
returnInboundDrawerOpen.value = false;
|
||
resetReturnInboundForm();
|
||
}
|
||
|
||
function closeReturnScrapInboundDrawer() {
|
||
returnScrapInboundDrawerOpen.value = false;
|
||
resetReturnScrapInboundForm();
|
||
}
|
||
|
||
function closeReturnReworkDrawer() {
|
||
returnReworkDrawerOpen.value = false;
|
||
resetReturnReworkForm();
|
||
}
|
||
|
||
function closeOpeningImportDrawer() {
|
||
openingImportDrawerOpen.value = false;
|
||
if (openingImportInputRef.value) {
|
||
openingImportInputRef.value.value = "";
|
||
}
|
||
}
|
||
|
||
function openStockDetail(balance) {
|
||
selectedItemId.value = balance.item_id;
|
||
selectedBalance.value = balance;
|
||
lotPage.value = 1;
|
||
txnPage.value = 1;
|
||
expandedLotRows.value = [];
|
||
expandedTxnRows.value = [];
|
||
lotControls.search.value = "";
|
||
txnControls.search.value = "";
|
||
detailDrawerOpen.value = true;
|
||
}
|
||
|
||
function closeStockDetail() {
|
||
detailDrawerOpen.value = false;
|
||
}
|
||
|
||
function openStocktakeDialog() {
|
||
resetFeedback();
|
||
stocktakeDialogOpen.value = true;
|
||
}
|
||
|
||
function closeStocktakeDialog() {
|
||
stocktakeDialogOpen.value = false;
|
||
}
|
||
|
||
function handleStocktakeMessage(message) {
|
||
feedbackMessage.value = message;
|
||
errorMessage.value = "";
|
||
}
|
||
|
||
function handleStocktakeError(message) {
|
||
errorMessage.value = message;
|
||
feedbackMessage.value = "";
|
||
}
|
||
|
||
function normalizeWarehouseType(item) {
|
||
return String(item?.warehouse_type || "").toUpperCase();
|
||
}
|
||
|
||
function aggregateItemBalances(rows, warehouseType) {
|
||
const map = new Map();
|
||
rows.forEach((item) => {
|
||
const key = Number(item.item_id);
|
||
if (!map.has(key)) {
|
||
map.set(key, {
|
||
...item,
|
||
stock_balance_id: `${warehouseType}-${key}`,
|
||
warehouse_type: warehouseType,
|
||
warehouse_name: "",
|
||
location_name: "",
|
||
qty_on_hand: 0,
|
||
weight_on_hand_kg: 0,
|
||
qty_available: 0,
|
||
weight_available_kg: 0,
|
||
qty_allocated: 0,
|
||
weight_allocated_kg: 0,
|
||
measure_basis: "",
|
||
avg_unit_cost: 0,
|
||
_cost_amount: 0,
|
||
_weight_for_cost: 0,
|
||
_warehouses: new Set(),
|
||
_locations: new Set()
|
||
});
|
||
}
|
||
const row = map.get(key);
|
||
row.qty_on_hand += Number(item.qty_on_hand || 0);
|
||
row.weight_on_hand_kg += Number(item.weight_on_hand_kg || 0);
|
||
row.qty_available += Number(item.qty_available || 0);
|
||
row.weight_available_kg += Number(item.weight_available_kg || 0);
|
||
row.qty_allocated += Number(item.qty_allocated || 0);
|
||
row.weight_allocated_kg += Number(item.weight_allocated_kg || 0);
|
||
row._cost_amount += Number(item.avg_unit_cost || 0) * Number(item.weight_on_hand_kg || 0);
|
||
row._weight_for_cost += Number(item.weight_on_hand_kg || 0);
|
||
row._warehouses.add(item.warehouse_name);
|
||
if (item.location_name) {
|
||
row._locations.add(item.location_name);
|
||
}
|
||
});
|
||
return Array.from(map.values()).map((item) => ({
|
||
...item,
|
||
warehouse_name: Array.from(item._warehouses).join(" / ") || "-",
|
||
location_name: Array.from(item._locations).join(" / ") || "-",
|
||
avg_unit_cost: item._weight_for_cost > 0 ? item._cost_amount / item._weight_for_cost : 0
|
||
}));
|
||
}
|
||
|
||
function aggregateSemiOperationBalances(rows) {
|
||
const map = new Map();
|
||
rows.forEach((lot) => {
|
||
const itemId = Number(lot.item_id || 0);
|
||
if (!itemId) {
|
||
return;
|
||
}
|
||
const sourceLineId = Number(lot.source_line_id || 0);
|
||
const key = `${itemId}::${sourceLineId}`;
|
||
if (!map.has(key)) {
|
||
map.set(key, {
|
||
...lot,
|
||
stock_balance_id: `SEMI-${key}`,
|
||
option_key: key,
|
||
source_line_id: sourceLineId || null,
|
||
route_operation_id: sourceLineId || null,
|
||
operation_label: sourceLineId ? lot.source_material_summary || "半成品未说明" : "未绑定半成品",
|
||
qty_on_hand: 0,
|
||
weight_on_hand_kg: 0,
|
||
qty_available: 0,
|
||
weight_available_kg: 0,
|
||
qty_allocated: 0,
|
||
weight_allocated_kg: 0,
|
||
avg_unit_cost: 0,
|
||
_cost_amount: 0,
|
||
_weight_for_cost: 0,
|
||
_warehouses: new Set(),
|
||
_locations: new Set()
|
||
});
|
||
}
|
||
const row = map.get(key);
|
||
const remainingWeight = Number(lot.remaining_weight_kg || 0);
|
||
const lockedWeight = Number(lot.locked_weight_kg || 0);
|
||
const availableWeight = String(lot.status || "").toUpperCase() === "AVAILABLE" ? Math.max(remainingWeight - lockedWeight, 0) : 0;
|
||
const remainingQty = Number(lot.remaining_qty || 0);
|
||
const lockedQty = Number(lot.locked_qty || 0);
|
||
const availableQty = String(lot.status || "").toUpperCase() === "AVAILABLE" ? Math.max(remainingQty - lockedQty, 0) : 0;
|
||
row.measure_basis = mergeSemiMeasureBasis(row.measure_basis, deriveSemiLotMeasureBasis(lot));
|
||
row.qty_on_hand += remainingQty;
|
||
row.qty_available += availableQty;
|
||
row.qty_allocated += lockedQty;
|
||
row.weight_on_hand_kg += remainingWeight;
|
||
row.weight_available_kg += availableWeight;
|
||
row.weight_allocated_kg += lockedWeight;
|
||
row._cost_amount += Number(lot.unit_cost || 0) * remainingWeight;
|
||
row._weight_for_cost += remainingWeight;
|
||
row._warehouses.add(lot.warehouse_name);
|
||
if (lot.location_name) {
|
||
row._locations.add(lot.location_name);
|
||
}
|
||
});
|
||
return Array.from(map.values()).map((item) => ({
|
||
...item,
|
||
warehouse_name: Array.from(item._warehouses).join(" / ") || "-",
|
||
location_name: Array.from(item._locations).join(" / ") || "-",
|
||
avg_unit_cost: item._weight_for_cost > 0 ? item._cost_amount / item._weight_for_cost : 0
|
||
}));
|
||
}
|
||
|
||
function formatInventoryBalanceLabel(balance) {
|
||
const specificationLabel = balance.specification ? ` · ${balance.specification}` : "";
|
||
const baseLabel = `${balance.item_code} · ${balance.item_name}${specificationLabel}`;
|
||
if (normalizeWarehouseType(balance) !== "SEMI") {
|
||
return baseLabel;
|
||
}
|
||
return `${baseLabel} · ${balance.operation_label || balance.source_material_summary || "未绑定工序"}`;
|
||
}
|
||
|
||
function formatInventoryUnitLabel(item) {
|
||
return item?.stock_unit_name || item?.purchase_unit_name || item?.sales_unit_name || extractUnitFromRemark(item?.item_remark) || "-";
|
||
}
|
||
|
||
function extractUnitFromRemark(remark) {
|
||
const match = String(remark || "").match(/计量单位:([^;;]+)/);
|
||
return match?.[1]?.trim() || "";
|
||
}
|
||
|
||
function orderItemPendingQty(item) {
|
||
return Math.max(Number(item.order_qty || 0) - Number(item.delivered_qty || 0), 0);
|
||
}
|
||
|
||
function orderPendingQty(orderId) {
|
||
return salesOrderItems.value
|
||
.filter((item) => Number(item.sales_order_id) === Number(orderId))
|
||
.reduce((total, item) => total + orderItemPendingQty(item), 0);
|
||
}
|
||
|
||
function orderPendingProductSummary(orderId) {
|
||
const rows = salesOrderItems.value
|
||
.filter((item) => Number(item.sales_order_id) === Number(orderId) && orderItemPendingQty(item) > 0)
|
||
.map((item) => {
|
||
const productName = item.product_name || item.product_code || "产品";
|
||
return `${productName} ${formatQty(orderItemPendingQty(item))}`;
|
||
});
|
||
return rows.length ? rows.join(";") : "无未发产品";
|
||
}
|
||
|
||
function deliveryStockShortageMessage(requestedQty, availableQty) {
|
||
const requested = Math.max(Number(requestedQty || 0), 0);
|
||
const available = Math.max(Number(availableQty || 0), 0);
|
||
const shortage = Math.max(requested - available, 0);
|
||
return `成品库存不足:本次要发 ${formatQty(requested)},可发 ${formatQty(available)},缺 ${formatQty(shortage)}`;
|
||
}
|
||
|
||
function applyCompletionWorkOrder() {
|
||
const workOrder = selectedCompletionWorkOrder.value;
|
||
if (!workOrder) {
|
||
return;
|
||
}
|
||
completionForm.settle_work_order = false;
|
||
completionForm.settle_remark = "";
|
||
const product = products.value.find((item) => item.item_id === Number(workOrder.product_item_id));
|
||
completionForm.product_item_id = workOrder.product_item_id;
|
||
completionForm.max_receivable_qty = Number(workOrder.limits?.max_finished_inbound_qty || workOrder.max_finished_inbound_qty || 0);
|
||
completionForm.tolerance_receivable_qty = Number(workOrder.limits?.tolerance_finished_inbound_qty || completionForm.max_receivable_qty || 0);
|
||
completionForm.unit_weight_kg = Number(product?.unit_weight_kg || 0);
|
||
completionForm.receipt_qty = Number(completionForm.max_receivable_qty || 0);
|
||
recalculateCompletionWeight();
|
||
}
|
||
|
||
function recalculateCompletionWeight() {
|
||
const toleranceMax = Number(completionForm.tolerance_receivable_qty || completionForm.max_receivable_qty || 0);
|
||
if (Number(completionForm.receipt_qty || 0) > toleranceMax) {
|
||
completionForm.receipt_qty = toleranceMax;
|
||
}
|
||
completionForm.receipt_weight_kg = Number((Number(completionForm.receipt_qty || 0) * Number(completionForm.unit_weight_kg || 0)).toFixed(3));
|
||
}
|
||
|
||
function applySalesOrderDefaults() {
|
||
deliveryForm.sales_order_item_id = "";
|
||
if (!isSalesOrderDelivery.value) {
|
||
return;
|
||
}
|
||
const order = selectedSalesOrder.value;
|
||
if (!order) {
|
||
return;
|
||
}
|
||
deliveryForm.customer_id = order.customer_id;
|
||
deliveryForm.delivery_address = order.delivery_address || "";
|
||
const customer = customers.value.find((item) => item.id === Number(order.customer_id));
|
||
deliveryForm.consignee_name = customer?.contact_name || "";
|
||
deliveryForm.consignee_phone = customer?.contact_phone || "";
|
||
deliveryForm.delivery_address = order.delivery_address || customer?.address || "";
|
||
const firstOpenItem = selectableSalesOrderItems.value[0];
|
||
if (firstOpenItem) {
|
||
deliveryForm.sales_order_item_id = firstOpenItem.sales_order_item_id;
|
||
applySalesOrderItemDefaults();
|
||
}
|
||
}
|
||
|
||
function applySalesOrderItemDefaults() {
|
||
const item = selectedSalesOrderItem.value;
|
||
if (!item || !isSalesOrderDelivery.value) {
|
||
return;
|
||
}
|
||
deliveryForm.product_item_id = item.product_item_id;
|
||
deliveryForm.delivery_qty = orderItemPendingQty(item);
|
||
}
|
||
|
||
function applyDeliveryCustomerDefaults() {
|
||
if (isSalesOrderDelivery.value) {
|
||
return;
|
||
}
|
||
const customer = customers.value.find((item) => item.id === Number(deliveryForm.customer_id));
|
||
deliveryForm.consignee_name = customer?.contact_name || "";
|
||
deliveryForm.consignee_phone = customer?.contact_phone || "";
|
||
deliveryForm.delivery_address = customer?.address || "";
|
||
}
|
||
|
||
function formatRawReturnLinkLabel(link) {
|
||
const orderNo = link.purchase_order_no || link.order_no || "采购单";
|
||
const supplierName = link.supplier_name || "供应商";
|
||
const returnableWeight = formatWeight(link.returnable_weight_kg);
|
||
return `${orderNo} · ${supplierName} · 可退 ${returnableWeight}`;
|
||
}
|
||
|
||
function rawReturnLotAvailableWeight(lot) {
|
||
if (!lot) {
|
||
return 0;
|
||
}
|
||
const remainingWeight = Number(lot.remaining_weight_kg || 0);
|
||
const lockedWeight = Number(lot.locked_weight_kg || 0);
|
||
return Math.max(remainingWeight - lockedWeight, 0);
|
||
}
|
||
|
||
function applyRawReturnLinkWeight() {
|
||
const linkReturnableWeight = Number(selectedRawReturnLink.value?.returnable_weight_kg || 0);
|
||
const maxReturnWeight = selectedRawReturnLink.value
|
||
? Math.min(Math.max(linkReturnableWeight, 0), selectedRawReturnLotAvailableWeight.value)
|
||
: selectedRawReturnLotAvailableWeight.value;
|
||
rawReturnForm.return_weight_kg = Number(maxReturnWeight.toFixed(3));
|
||
}
|
||
|
||
function handleRawReturnLinkChange() {
|
||
if (!selectedRawReturnLink.value) {
|
||
rawReturnForm.return_weight_kg = 0;
|
||
return;
|
||
}
|
||
applyRawReturnLinkWeight();
|
||
}
|
||
|
||
function applyReturnInboundDeliveryLine() {
|
||
const line = selectedReturnInboundDeliveryLine.value;
|
||
if (!line) {
|
||
returnInboundForm.return_qty = 0;
|
||
returnInboundForm.return_weight_kg = 0;
|
||
return;
|
||
}
|
||
returnInboundForm.return_qty = Number(line.returnable_qty || 0);
|
||
returnInboundForm.return_weight_kg = Number(Number(line.returnable_weight_kg || 0).toFixed(3));
|
||
}
|
||
|
||
function applyReturnScrapDeliveryLine() {
|
||
const line = selectedReturnScrapDeliveryLine.value;
|
||
if (!line) {
|
||
returnScrapInboundForm.scrap_qty = 0;
|
||
returnScrapInboundForm.scrap_weight_kg = 0;
|
||
returnScrapInboundForm.unit_cost = "";
|
||
return;
|
||
}
|
||
const product = products.value.find((item) => Number(item.item_id) === Number(line.product_item_id));
|
||
returnScrapInboundForm.scrap_qty = Number(line.returnable_qty || 0);
|
||
returnScrapInboundForm.scrap_weight_kg = Number(Number(line.returnable_weight_kg || 0).toFixed(3));
|
||
returnScrapInboundForm.unit_cost = product?.scrap_sale_price ?? "";
|
||
}
|
||
|
||
async function handleRawReturnLotChange() {
|
||
rawReturnLinksRequestSeq += 1;
|
||
const requestSeq = rawReturnLinksRequestSeq;
|
||
const lotId = Number(rawReturnForm.lot_id);
|
||
rawReturnForm.purchase_order_item_id = "";
|
||
rawReturnForm.return_weight_kg = 0;
|
||
rawReturnForm.waybill_no = "";
|
||
rawReturnForm.order_photo_url = "";
|
||
rawReturnForm.freight_amount = "";
|
||
rawReturnPurchaseLinks.value = [];
|
||
const lot = selectedRawReturnLot.value;
|
||
if (!lot) {
|
||
return;
|
||
}
|
||
rawReturnForm.warehouse_id = lot.warehouse_id || rawReturnForm.warehouse_id;
|
||
rawReturnLinkLoading.value = true;
|
||
try {
|
||
const result = await fetchResource(`/inventory/stock-lots/${lotId}/purchase-links`, []);
|
||
if (requestSeq !== rawReturnLinksRequestSeq || Number(rawReturnForm.lot_id) !== lotId) {
|
||
return;
|
||
}
|
||
rawReturnPurchaseLinks.value = Array.isArray(result) ? result : result?.items || result?.links || [];
|
||
if (rawReturnPurchaseLinks.value.length === 1) {
|
||
rawReturnForm.purchase_order_item_id = rawReturnPurchaseLinks.value[0].purchase_order_item_id;
|
||
applyRawReturnLinkWeight();
|
||
}
|
||
errorMessage.value = "";
|
||
} catch (error) {
|
||
if (requestSeq !== rawReturnLinksRequestSeq || Number(rawReturnForm.lot_id) !== lotId) {
|
||
return;
|
||
}
|
||
errorMessage.value = error.message || "采购来源加载失败";
|
||
} finally {
|
||
if (requestSeq === rawReturnLinksRequestSeq && Number(rawReturnForm.lot_id) === lotId) {
|
||
rawReturnLinkLoading.value = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
function validateRawReturnForm() {
|
||
if (!Number(rawReturnForm.lot_id)) {
|
||
return "请选择库存批次号";
|
||
}
|
||
if (!Number(rawReturnForm.purchase_order_item_id)) {
|
||
return "请选择采购来源";
|
||
}
|
||
const returnWeight = Number(rawReturnForm.return_weight_kg || 0);
|
||
if (!Number.isFinite(returnWeight) || returnWeight <= 0) {
|
||
return "退货重量必须大于0";
|
||
}
|
||
const lotAvailableWeight = selectedRawReturnLotAvailableWeight.value;
|
||
if (lotAvailableWeight > 0 && returnWeight > lotAvailableWeight) {
|
||
return `退货重量不能超过批次可用重量 ${formatWeight(lotAvailableWeight)}`;
|
||
}
|
||
const linkReturnableWeight = Number(selectedRawReturnLink.value?.returnable_weight_kg || 0);
|
||
if (selectedRawReturnLink.value && returnWeight > Math.max(linkReturnableWeight, 0)) {
|
||
return `退货重量不能超过采购来源可退重量 ${formatWeight(linkReturnableWeight)}`;
|
||
}
|
||
if (!String(rawReturnForm.waybill_no || "").trim() && !String(rawReturnForm.order_photo_url || "").trim()) {
|
||
return "请填写运单号或上传辅助照片";
|
||
}
|
||
if (!isFreightBlank(rawReturnForm.freight_amount) && Number(rawReturnForm.freight_amount) < 0) {
|
||
return "运费不能小于0";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function validateReturnQuantityWeight(line, qty, weight, qtyLabel, weightLabel) {
|
||
if (!line) {
|
||
return "请选择发货批次";
|
||
}
|
||
if (!Number.isFinite(qty) || qty <= 0) {
|
||
return `${qtyLabel}必须大于0`;
|
||
}
|
||
if (!Number.isFinite(weight) || weight <= 0) {
|
||
return `${weightLabel}必须大于0`;
|
||
}
|
||
if (qty > Number(line.returnable_qty || 0)) {
|
||
return `${qtyLabel}不能超过发货批次可退数量 ${formatQty(line.returnable_qty)}`;
|
||
}
|
||
if (weight > Number(line.returnable_weight_kg || 0)) {
|
||
return `${weightLabel}不能超过发货批次可退重量 ${formatWeight(line.returnable_weight_kg)}`;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function validateReturnInboundForm() {
|
||
return validateReturnQuantityWeight(
|
||
selectedReturnInboundDeliveryLine.value,
|
||
Number(returnInboundForm.return_qty || 0),
|
||
Number(returnInboundForm.return_weight_kg || 0),
|
||
"退货数量",
|
||
"退货重量"
|
||
);
|
||
}
|
||
|
||
function validateReturnScrapInboundForm() {
|
||
const unitCost = Number(returnScrapInboundForm.unit_cost || 0);
|
||
if (returnScrapInboundForm.unit_cost !== "" && (!Number.isFinite(unitCost) || unitCost < 0)) {
|
||
return "废料单价不能小于0";
|
||
}
|
||
return validateReturnQuantityWeight(
|
||
selectedReturnScrapDeliveryLine.value,
|
||
Number(returnScrapInboundForm.scrap_qty || 0),
|
||
Number(returnScrapInboundForm.scrap_weight_kg || 0),
|
||
"报废数量",
|
||
"报废重量"
|
||
);
|
||
}
|
||
|
||
function validateReturnReworkForm() {
|
||
if (!selectedReturnReworkItem.value) {
|
||
return "请选择待返工退货明细";
|
||
}
|
||
if (Number(returnReworkForm.extra_cost_amount || 0) < 0) {
|
||
return "额外人工成本不能小于0";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function validateGenericSourceLots() {
|
||
if (!usesGenericSourceLotSelector.value) {
|
||
return "";
|
||
}
|
||
if (!selectedGenericSourceLots.value.length) {
|
||
if (isFinishedOutsourcingInbound.value) {
|
||
return "";
|
||
}
|
||
return "请选择至少一个来源库存批次号";
|
||
}
|
||
if (isProductTraceOnlySourceLotInbound.value) {
|
||
return "";
|
||
}
|
||
if (isProductionSurplusInbound.value) {
|
||
const hasPositiveReturnWeight = selectedGenericSourceLots.value.some((lot) => Number(lot.return_weight_kg || 0) > 0);
|
||
if (!hasPositiveReturnWeight) {
|
||
return "请至少填写一个来源库存批次号的归还重量";
|
||
}
|
||
}
|
||
for (const lot of selectedGenericSourceLots.value) {
|
||
const weight = Number(isSourceLotSelectedOutbound.value ? lot.issued_weight_kg : lot.return_weight_kg || 0);
|
||
const sourceRemainingWeight = Number(lot.source_remaining_weight_kg ?? 0);
|
||
if (isProductionSurplusInbound.value && sourceRemainingWeight <= 0 && weight <= 0) {
|
||
continue;
|
||
}
|
||
if (!Number.isFinite(weight) || weight <= 0) {
|
||
return `来源库存批次号 ${lot.lot_no || "-"} 的${isSourceLotSelectedOutbound.value ? "出库" : "归还"}重量必须大于0`;
|
||
}
|
||
if (
|
||
isProductionSurplusInbound.value &&
|
||
Number.isFinite(sourceRemainingWeight) &&
|
||
weight > sourceRemainingWeight
|
||
) {
|
||
return `来源库存批次号 ${lot.lot_no || "-"} 的归还重量不能超过该批次原领料未归还重量 ${formatWeight(sourceRemainingWeight)}`;
|
||
}
|
||
if (isSourceLotSelectedOutbound.value && weight > Number(lot.remaining_weight_kg || 0)) {
|
||
return `来源库存批次号 ${lot.lot_no || "-"} 的出库重量不能超过可用重量 ${formatWeight(lot.remaining_weight_kg)}`;
|
||
}
|
||
}
|
||
if (isProductionSurplusInbound.value) {
|
||
const toleranceMax = Number(productionSurplusToleranceRemainingWeight.value || 0);
|
||
if (!genericForm.settle_work_order && toleranceMax > 0 && genericSourceLotReturnWeight.value > toleranceMax) {
|
||
return "本次归还总重量超过系统允许范围,请调整后重试";
|
||
}
|
||
if (genericForm.settle_work_order && productionSurplusSettleNeedsRemark.value && !String(genericForm.settle_remark || "").trim()) {
|
||
return "该批材料余料结单归还重量超出标准归还重量上下15%,请填写余料结单说明";
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function specialAdjustmentSubmitQty(line) {
|
||
return specialAdjustmentShowQtyInput(line) ? Number(line.adjust_qty || 0) : 0;
|
||
}
|
||
|
||
function specialAdjustmentSubmitWeight(line) {
|
||
return specialAdjustmentShowWeightInput(line) ? Number(line.adjust_weight_kg || 0) : 0;
|
||
}
|
||
|
||
function validateSpecialAdjustmentBeforeSubmit() {
|
||
const reason = String(specialAdjustmentForm.reason || "").trim();
|
||
if (!reason) {
|
||
return `${specialAdjustmentDirectionText.value}必须填写调整说明`;
|
||
}
|
||
if (reason.length < 4) {
|
||
return "调整说明请至少填写4个字";
|
||
}
|
||
if (!Number(specialAdjustmentForm.warehouse_id || 0)) {
|
||
return "请选择仓库";
|
||
}
|
||
if (!specialAdjustmentForm.lines.length) {
|
||
return `${specialAdjustmentDirectionText.value}必须至少添加一条明细`;
|
||
}
|
||
for (const [index, line] of specialAdjustmentForm.lines.entries()) {
|
||
const lineNo = index + 1;
|
||
const lot = selectedSpecialAdjustmentLot(line);
|
||
const lotRequired = specialAdjustmentForm.direction === "OUT" || specialAdjustmentLineUsesExistingLot(line);
|
||
if (specialAdjustmentForm.direction === "OUT" && (!Number(line.lot_id || 0) || !lot)) {
|
||
return `第${lineNo}行特殊出库必须选择库存明细`;
|
||
}
|
||
if (specialAdjustmentForm.direction === "IN" && lotRequired && (!Number(line.lot_id || 0) || !lot)) {
|
||
return `第${lineNo}行请选择要补增的库存明细,或切换为新增特殊明细`;
|
||
}
|
||
if (!Number(line.item_id || 0)) {
|
||
return specialAdjustmentForm.direction === "IN" && !lotRequired
|
||
? `第${lineNo}行新增特殊明细必须选择物料`
|
||
: `第${lineNo}行库存明细未带入物料`;
|
||
}
|
||
const qty = specialAdjustmentSubmitQty(line);
|
||
const weight = specialAdjustmentSubmitWeight(line);
|
||
if (specialAdjustmentShowQtyInput(line) && (!Number.isFinite(qty) || qty <= 0)) {
|
||
return `第${lineNo}行必须填写调整数量`;
|
||
}
|
||
if (specialAdjustmentWeightRequired(line) && (!Number.isFinite(weight) || weight <= 0)) {
|
||
return `第${lineNo}行必须填写调整重量`;
|
||
}
|
||
if (specialAdjustmentShowWeightInput(line) && (!Number.isFinite(weight) || weight < 0)) {
|
||
return `第${lineNo}行调整重量不能小于0`;
|
||
}
|
||
if (specialAdjustmentEffectiveMeasureMode(line) === "SEMI_SELECT") {
|
||
return `第${lineNo}行必须选择调整口径`;
|
||
}
|
||
if (specialAdjustmentForm.direction === "OUT") {
|
||
const remainingQty = Number(lot.remaining_qty || 0);
|
||
const remainingWeight = Number(lot.remaining_weight_kg || 0);
|
||
if (qty > remainingQty) {
|
||
return `第${lineNo}行调整数量不能超过当前剩余数量 ${formatQty(remainingQty)}`;
|
||
}
|
||
if (weight > remainingWeight) {
|
||
return `第${lineNo}行调整重量不能超过当前剩余重量 ${formatWeight(remainingWeight)}`;
|
||
}
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function specialAdjustmentLineLotId(line) {
|
||
const lotId = Number(line.lot_id || 0);
|
||
return lotId > 0 ? lotId : null;
|
||
}
|
||
|
||
function buildSpecialAdjustmentPayloadLine(line) {
|
||
const lotId = specialAdjustmentLineLotId(line);
|
||
if (!lotId) {
|
||
return {
|
||
item_id: Number(line.item_id),
|
||
lot_id: null,
|
||
location_id: line.location_id ? Number(line.location_id) : null,
|
||
adjust_qty: specialAdjustmentSubmitQty(line),
|
||
adjust_weight_kg: specialAdjustmentSubmitWeight(line),
|
||
unit_cost: Number(line.unit_cost || 0),
|
||
line_reason: line.line_reason || null
|
||
};
|
||
}
|
||
return {
|
||
item_id: Number(line.item_id),
|
||
lot_id: lotId,
|
||
location_id: line.location_id ? Number(line.location_id) : null,
|
||
adjust_qty: specialAdjustmentSubmitQty(line),
|
||
adjust_weight_kg: specialAdjustmentSubmitWeight(line),
|
||
unit_cost: Number(line.unit_cost || 0),
|
||
line_reason: line.line_reason || null
|
||
};
|
||
}
|
||
|
||
async function submitSpecialAdjustment() {
|
||
resetFeedback();
|
||
const validationError = validateSpecialAdjustmentBeforeSubmit();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
return;
|
||
}
|
||
const confirmed = window.confirm(`确认执行${specialAdjustmentDirectionText.value}?保存后会直接改变库存明细,并写入库存流水和特殊调整记录。`);
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
specialAdjustmentSubmitting.value = true;
|
||
try {
|
||
const payload = {
|
||
warehouse_id: Number(specialAdjustmentForm.warehouse_id),
|
||
direction: specialAdjustmentForm.direction,
|
||
reason: String(specialAdjustmentForm.reason || "").trim(),
|
||
lines: specialAdjustmentForm.lines.map((line) => buildSpecialAdjustmentPayloadLine(line))
|
||
};
|
||
const result = await postResource("/inventory/special-adjustments", payload);
|
||
const savedNo = result.adjustment_no || result.id || "";
|
||
rememberWarehouseArchiveResult(result, specialAdjustmentDirectionText.value);
|
||
feedbackMessage.value = `${specialAdjustmentDirectionText.value} ${savedNo} 已保存${formatArchiveFeedback(result)}`;
|
||
closeSpecialAdjustmentDrawer();
|
||
await loadAll();
|
||
resetSpecialAdjustmentForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || `${specialAdjustmentDirectionText.value}保存失败`;
|
||
} finally {
|
||
specialAdjustmentSubmitting.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitGenericOperation() {
|
||
resetFeedback();
|
||
if (activeOperation.value?.bizType === "SCRAP_SALE_OUT" && Number(genericForm.sale_unit_price || 0) <= 0) {
|
||
errorMessage.value = "售卖单价必须大于0";
|
||
return;
|
||
}
|
||
if (genericRequiresLogistics.value) {
|
||
const logisticsError = validateLogisticsForm(genericForm);
|
||
if (logisticsError) {
|
||
errorMessage.value = logisticsError;
|
||
return;
|
||
}
|
||
}
|
||
if (isProductionWorkOrderControlledInbound.value && !selectedGenericWorkOrder.value) {
|
||
errorMessage.value = `${activeOperation.value?.label || "生产入库"}必须先选择${isReworkScrapInbound.value ? "返工工单" : "生产台账"}`;
|
||
return;
|
||
}
|
||
if (isProductionScrapInbound.value) {
|
||
const inboundWeight = Number(genericForm.weight_kg || 0);
|
||
const scrapToleranceMax = Number(productionScrapToleranceRemainingWeight.value || 0);
|
||
if (!genericForm.settle_work_order && scrapToleranceMax > 0 && inboundWeight > scrapToleranceMax) {
|
||
errorMessage.value = "生产废料入库重量超过系统允许范围,请调整后重试";
|
||
return;
|
||
}
|
||
if (genericForm.settle_work_order && productionScrapSettleNeedsRemark.value && !String(genericForm.settle_remark || "").trim()) {
|
||
errorMessage.value = "该批材料废料结单入库废料重量超出标准废料重量上下15%,请填写废料结单说明";
|
||
return;
|
||
}
|
||
}
|
||
if (isReworkScrapInbound.value) {
|
||
const inboundQty = Number(genericForm.qty || 0);
|
||
const inboundWeight = Number(genericForm.weight_kg || 0);
|
||
if (!Number.isFinite(inboundQty) || inboundQty <= 0) {
|
||
errorMessage.value = "返工废料入库数量必须大于0";
|
||
return;
|
||
}
|
||
if (!Number.isFinite(inboundWeight) || inboundWeight <= 0) {
|
||
errorMessage.value = "返工废料入库重量必须大于0";
|
||
return;
|
||
}
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const selectedOption = selectedGenericItemOption.value;
|
||
const selectedItemId = Number(selectedOption?.item_id || genericForm.item_id || 0);
|
||
const selectedSourceDocId = selectedOption?.route_id || selectedOption?.source_doc_id || genericForm.source_doc_id || null;
|
||
const selectedSourceLineId = selectedOption?.route_operation_id || selectedOption?.source_line_id || genericForm.source_line_id || null;
|
||
const selectedSourceSummary =
|
||
selectedOption?.source_material_summary ||
|
||
selectedOption?.operation_label ||
|
||
genericForm.provider_name ||
|
||
genericForm.source_material_sub_batch_no ||
|
||
null;
|
||
if (!selectedItemId) {
|
||
errorMessage.value = `请选择${genericItemFieldLabel.value}`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (activeWarehouseType.value === "SEMI" && !selectedSourceLineId) {
|
||
errorMessage.value = "半成品入出库必须选择半成品";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (activeOperation.value?.direction === "out") {
|
||
if (activeWarehouseType.value === "AUX") {
|
||
const outboundQty = Number(genericForm.qty || 0);
|
||
if (!Number.isFinite(outboundQty) || outboundQty <= 0) {
|
||
errorMessage.value = "辅料生产出库数量必须大于0";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (!String(genericForm.remark || "").trim()) {
|
||
errorMessage.value = "辅料生产出库必须填写用途";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
if (isSemiAutoSourceOutbound.value) {
|
||
const measureBasis = selectedSemiMeasureBasis.value;
|
||
const outboundWeight = measureBasis === "WEIGHT" ? Number(genericForm.weight_kg || 0) : 0;
|
||
const outboundQty = measureBasis === "PIECE" ? Number(genericForm.qty || 0) : 0;
|
||
const operationLabel = activeOperation.value?.label || "半成品出库";
|
||
if (measureBasis === "MIXED") {
|
||
errorMessage.value = "该半成品已有历史混合库存,请先清理历史库存后再出库";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (!["PIECE", "WEIGHT"].includes(measureBasis)) {
|
||
errorMessage.value = "请选择有可用库存的半成品";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if ((!Number.isFinite(outboundWeight) || outboundWeight < 0) || (!Number.isFinite(outboundQty) || outboundQty < 0)) {
|
||
errorMessage.value = measureBasis === "PIECE" ? "出库数量不能小于0" : "出库重量不能小于0";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (measureBasis === "PIECE" && outboundQty <= 0) {
|
||
errorMessage.value = `${operationLabel}的出库数量必须大于0`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (measureBasis === "WEIGHT" && outboundWeight <= 0) {
|
||
errorMessage.value = `${operationLabel}的出库重量必须大于0`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (outboundQty > 0 && outboundQty > semiAutoSourceOutboundAvailableQty.value) {
|
||
errorMessage.value = `${operationLabel}数量不能超过该半成品可用数量 ${formatQty(semiAutoSourceOutboundAvailableQty.value)}`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (outboundWeight > 0 && outboundWeight > semiAutoSourceOutboundAvailableWeight.value) {
|
||
errorMessage.value = `${operationLabel}重量不能超过该半成品可用重量 ${formatWeight(semiAutoSourceOutboundAvailableWeight.value)}`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
if (isSourceLotSelectedOutbound.value) {
|
||
const validationError = validateGenericSourceLots();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
const selectedOutboundSourceLots = isSourceLotSelectedOutbound.value
|
||
? selectedGenericSourceLots.value.map((lot) => ({
|
||
source_lot_id: Number(lot.lot_id),
|
||
outbound_weight_kg: Number(lot.issued_weight_kg || 0)
|
||
}))
|
||
: [];
|
||
const result = await postResource("/inventory/outbound", {
|
||
biz_type: activeOperation.value.bizType,
|
||
item_id: selectedItemId,
|
||
warehouse_id: Number(genericForm.warehouse_id),
|
||
location_id: genericForm.location_id ? Number(genericForm.location_id) : null,
|
||
outbound_weight_kg: activeWarehouseType.value === "AUX"
|
||
? 0
|
||
: isSourceLotSelectedOutbound.value
|
||
? Number(genericSourceLotOutboundWeight.value.toFixed(3))
|
||
: isSemiAutoSourceOutbound.value && selectedSemiMeasureBasis.value === "PIECE"
|
||
? 0
|
||
: Number(genericForm.weight_kg || 0),
|
||
outbound_qty: activeWarehouseType.value === "AUX"
|
||
? Number(genericForm.qty || 0)
|
||
: isSemiAutoSourceOutbound.value && selectedSemiMeasureBasis.value === "PIECE"
|
||
? Number(genericForm.qty || 0)
|
||
: 0,
|
||
target_doc_type: activeWarehouseType.value === "SEMI" ? "PRODUCT_ROUTE_OPERATION" : null,
|
||
target_doc_id: activeWarehouseType.value === "SEMI" ? Number(selectedSourceDocId || 0) : null,
|
||
target_doc_line_id: activeWarehouseType.value === "SEMI" ? Number(selectedSourceLineId || 0) : null,
|
||
target_material_sub_batch_no: isSourceLotSelectedOutbound.value
|
||
? selectedGenericSourceLots.value.map((lot) => lot.lot_no).filter(Boolean).join("、") || null
|
||
: activeWarehouseType.value === "SEMI"
|
||
? isSemiAutoSourceOutbound.value
|
||
? semiAutoSourceOutboundSourceLotSummary.value || selectedOption?.operation_label || selectedSourceSummary
|
||
: selectedOption?.operation_label || selectedSourceSummary
|
||
: genericForm.source_material_sub_batch_no || null,
|
||
sale_unit_price: activeOperation.value.bizType === "SCRAP_SALE_OUT" ? Number(genericForm.sale_unit_price || 0) : null,
|
||
buyer_name: activeOperation.value.bizType === "SCRAP_SALE_OUT" ? genericForm.buyer_name || null : null,
|
||
sale_remark: activeOperation.value.bizType === "SCRAP_SALE_OUT" ? genericForm.sale_remark || genericForm.remark || null : null,
|
||
waybill_no: genericRequiresLogistics.value ? genericForm.waybill_no || null : null,
|
||
order_photo_url: genericRequiresLogistics.value ? genericForm.order_photo_url || null : null,
|
||
freight_amount: genericRequiresLogistics.value ? Number(genericForm.freight_amount) : null,
|
||
outsourcing_party_name: activeOperation.value.bizType === "OUTSOURCING_OUT" ? genericForm.outsourcing_party_name || null : null,
|
||
remark: genericForm.remark || null,
|
||
source_lots: selectedOutboundSourceLots
|
||
});
|
||
rememberWarehouseArchiveResult(result, activeOperation.value.label);
|
||
feedbackMessage.value = `${activeOperation.value.label}已登记${formatArchiveFeedback(result)}`;
|
||
} else {
|
||
if (activeWarehouseType.value === "AUX") {
|
||
const inboundQty = Number(genericForm.qty || 0);
|
||
if (!Number.isFinite(inboundQty) || inboundQty <= 0) {
|
||
errorMessage.value = "辅料入库数量必须大于0";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
if (isFinishedOutsourcingInbound.value) {
|
||
const inboundQty = Number(genericForm.qty || 0);
|
||
if (!Number.isFinite(inboundQty) || inboundQty <= 0) {
|
||
errorMessage.value = "成品委外入库数量必须大于0";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
if (isSemiSourceLotTraceInbound.value) {
|
||
const measureBasis = selectedSemiMeasureBasis.value;
|
||
const inboundWeight = measureBasis === "WEIGHT" ? Number(genericForm.weight_kg || 0) : 0;
|
||
const inboundQty = measureBasis === "PIECE" ? Number(genericForm.qty || 0) : 0;
|
||
if (selectedSemiExistingMeasureBasis.value === "MIXED") {
|
||
errorMessage.value = "该半成品已有历史混合库存,请先清理历史库存后再入库";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (!["PIECE", "WEIGHT"].includes(measureBasis)) {
|
||
errorMessage.value = "请选择入库类型";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if ((!Number.isFinite(inboundWeight) || inboundWeight < 0) || (!Number.isFinite(inboundQty) || inboundQty < 0)) {
|
||
errorMessage.value = measureBasis === "PIECE" ? "入库数量不能小于0" : "入库重量不能小于0";
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (measureBasis === "PIECE" && inboundQty <= 0) {
|
||
errorMessage.value = `${activeOperation.value?.label || "半成品入库"}的入库数量必须大于0`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
if (measureBasis === "WEIGHT" && inboundWeight <= 0) {
|
||
errorMessage.value = `${activeOperation.value?.label || "半成品入库"}的入库重量必须大于0`;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
const validationError = validateGenericSourceLots();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
}
|
||
if (isSourceLotReturnInbound.value) {
|
||
const validationError = validateGenericSourceLots();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
submittingOperation.value = false;
|
||
return;
|
||
}
|
||
const sourceLotsToSubmit = selectedGenericSourceLots.value.filter((lot) => !isProductionSurplusInbound.value || Number(lot.return_weight_kg || 0) > 0);
|
||
let lastResult = null;
|
||
for (const [sourceLotIndex, lot] of sourceLotsToSubmit.entries()) {
|
||
const isLastSourceLotReturn = sourceLotIndex === sourceLotsToSubmit.length - 1;
|
||
lastResult = await postResource("/inventory/inbound", {
|
||
biz_type: activeOperation.value.bizType,
|
||
item_id: isProductionSurplusInbound.value ? Number(lot.item_id || selectedItemId) : selectedItemId,
|
||
warehouse_id: Number(genericForm.warehouse_id),
|
||
location_id: genericForm.location_id ? Number(genericForm.location_id) : null,
|
||
source_lot_id: Number(lot.lot_id),
|
||
inbound_weight_kg: Number(lot.return_weight_kg || 0),
|
||
unit_cost: Number(lot.unit_cost ?? genericForm.unit_cost ?? 0),
|
||
lot_no: null,
|
||
source_doc_type: isProductionSurplusInbound.value ? "PRODUCTION_LEDGER" : null,
|
||
source_doc_id: isProductionSurplusInbound.value ? Number(selectedGenericWorkOrder.value.production_ledger_id || selectedGenericWorkOrder.value.work_order_id) : null,
|
||
source_material_sub_batch_no: lot.lot_no || null,
|
||
source_material_summary: isProductionSurplusInbound.value
|
||
? `${selectedGenericWorkOrder.value.work_order_no || "生产台账"} · ${lot.lot_no || ""}`
|
||
: lot.lot_no || null,
|
||
provider_name: genericForm.provider_name || null,
|
||
waybill_no: genericRequiresLogistics.value ? genericForm.waybill_no || null : null,
|
||
order_photo_url: genericRequiresLogistics.value ? genericForm.order_photo_url || null : null,
|
||
freight_amount: genericRequiresLogistics.value ? Number(genericForm.freight_amount) : null,
|
||
remark: genericForm.remark || null,
|
||
settle_work_order: isProductionSurplusInbound.value && isLastSourceLotReturn && Boolean(genericForm.settle_work_order),
|
||
settle_remark: isProductionSurplusInbound.value && isLastSourceLotReturn ? genericForm.settle_remark || null : null
|
||
});
|
||
}
|
||
rememberWarehouseArchiveResult(lastResult, activeOperation.value.label);
|
||
feedbackMessage.value = `${activeOperation.value.label}已登记${formatArchiveFeedback(lastResult)}`;
|
||
genericDrawerOpen.value = false;
|
||
await loadAll();
|
||
return;
|
||
}
|
||
const inboundWeightKg = activeWarehouseType.value === "AUX" || isFinishedOutsourcingInbound.value
|
||
? 0
|
||
: isSemiSourceLotTraceInbound.value && selectedSemiMeasureBasis.value === "PIECE"
|
||
? 0
|
||
: Number(genericForm.weight_kg || 0);
|
||
const inboundQty = activeWarehouseType.value === "AUX" || isFinishedOutsourcingInbound.value
|
||
? Number(genericForm.qty || 0)
|
||
: isReworkScrapInbound.value
|
||
? Number(genericForm.qty || 0)
|
||
: isSemiSourceLotTraceInbound.value && selectedSemiMeasureBasis.value === "PIECE"
|
||
? Number(genericForm.qty || 0)
|
||
: 0;
|
||
const selectedTraceSourceLotIds = isProductTraceOnlySourceLotInbound.value || isOutsourcingScrapInbound.value
|
||
? selectedGenericSourceLots.value.map((lot) => Number(lot.lot_id)).filter(Boolean)
|
||
: [];
|
||
const selectedTraceSourceLotNos = isProductTraceOnlySourceLotInbound.value || isOutsourcingScrapInbound.value
|
||
? selectedGenericSourceLots.value.map((lot) => lot.lot_no).filter(Boolean).join("、") || null
|
||
: null;
|
||
const result = await postResource("/inventory/inbound", {
|
||
biz_type: activeOperation.value.bizType,
|
||
item_id: selectedItemId,
|
||
warehouse_id: Number(genericForm.warehouse_id),
|
||
location_id: genericForm.location_id ? Number(genericForm.location_id) : null,
|
||
inbound_weight_kg: inboundWeightKg,
|
||
inbound_qty: inboundQty,
|
||
unit_cost: isSemiSourceLotTraceInbound.value ? null : activeOperation.value.bizType === "CUSTOMER_SUPPLIED" ? 0 : Number(genericForm.unit_cost || 0),
|
||
lot_no: isCustomerSuppliedInbound.value ? null : isProductionScrapInbound.value ? null : isReworkScrapInbound.value ? null : isProductTraceOnlySourceLotInbound.value ? null : isOutsourcingScrapInbound.value ? null : genericForm.lot_no || null,
|
||
source_doc_type: isProductionScrapInbound.value ? "PRODUCTION_LEDGER" : isReworkScrapInbound.value ? "WORK_ORDER" : activeWarehouseType.value === "SEMI" ? "PRODUCT_ROUTE_OPERATION" : null,
|
||
source_doc_id: isProductionScrapInbound.value
|
||
? Number(selectedGenericWorkOrder.value.production_ledger_id || selectedGenericWorkOrder.value.work_order_id)
|
||
: isReworkScrapInbound.value
|
||
? Number(selectedGenericWorkOrder.value.work_order_id)
|
||
: activeWarehouseType.value === "SEMI"
|
||
? Number(selectedSourceDocId || 0)
|
||
: null,
|
||
source_line_id: activeWarehouseType.value === "SEMI" ? Number(selectedSourceLineId || 0) : null,
|
||
source_lot_ids: selectedTraceSourceLotIds,
|
||
source_material_sub_batch_no: isCustomerSuppliedInbound.value
|
||
? null
|
||
: isProductTraceOnlySourceLotInbound.value || isOutsourcingScrapInbound.value
|
||
? selectedTraceSourceLotNos
|
||
: genericForm.source_material_sub_batch_no || null,
|
||
source_material_summary: isProductionScrapInbound.value
|
||
? selectedGenericWorkOrder.value.source_lot_summary || selectedGenericWorkOrder.value.work_order_no || null
|
||
: isReworkScrapInbound.value
|
||
? selectedGenericWorkOrder.value.work_order_no || null
|
||
: activeWarehouseType.value === "SEMI"
|
||
? selectedSourceSummary
|
||
: isFinishedOutsourcingInbound.value
|
||
? genericForm.provider_name || null
|
||
: isOutsourcingScrapInbound.value
|
||
? genericForm.provider_name || null
|
||
: genericForm.provider_name || genericForm.source_material_sub_batch_no || null,
|
||
provider_name: genericForm.provider_name || null,
|
||
waybill_no: genericRequiresLogistics.value ? genericForm.waybill_no || null : null,
|
||
order_photo_url: genericRequiresLogistics.value ? genericForm.order_photo_url || null : null,
|
||
freight_amount: genericRequiresLogistics.value ? Number(genericForm.freight_amount) : null,
|
||
remark: genericForm.remark || null,
|
||
settle_work_order: (isProductionScrapInbound.value || isReworkScrapInbound.value) && Boolean(genericForm.settle_work_order),
|
||
settle_remark: isProductionScrapInbound.value || isReworkScrapInbound.value ? genericForm.settle_remark || null : null
|
||
});
|
||
rememberWarehouseArchiveResult(result, activeOperation.value.label);
|
||
feedbackMessage.value = `${activeOperation.value.label}已登记${formatArchiveFeedback(result)}`;
|
||
}
|
||
genericDrawerOpen.value = false;
|
||
await loadAll();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || `${activeOperation.value?.label || "出入库"}登记失败`;
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function exportOpeningTemplate() {
|
||
resetFeedback();
|
||
openingImportBusy.value = true;
|
||
try {
|
||
await downloadResource(
|
||
`/inventory/opening/template?warehouse_type=${encodeURIComponent(activeWarehouseType.value)}`,
|
||
`${activeInventoryLabel.value}期初导入模版.xlsx`
|
||
);
|
||
feedbackMessage.value = `${activeInventoryLabel.value}期初导入模版已导出`;
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "期初导入模版导出失败";
|
||
} finally {
|
||
openingImportBusy.value = false;
|
||
}
|
||
}
|
||
|
||
async function importOpeningInventory(event) {
|
||
const file = event.target.files?.[0];
|
||
if (!file) {
|
||
errorMessage.value = "请选择期初库存 Excel 文件";
|
||
return;
|
||
}
|
||
resetFeedback();
|
||
openingImportBusy.value = true;
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
const result = await uploadResource(`/inventory/opening/import?warehouse_type=${encodeURIComponent(activeWarehouseType.value)}`, formData);
|
||
feedbackMessage.value = result.message || `${activeInventoryLabel.value}期初库存导入完成`;
|
||
openingImportDrawerOpen.value = false;
|
||
await loadAll();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "期初库存导入失败";
|
||
} finally {
|
||
openingImportBusy.value = false;
|
||
if (openingImportInputRef.value) {
|
||
openingImportInputRef.value.value = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
async function submitRawReturnOutbound() {
|
||
resetFeedback();
|
||
const validationError = validateRawReturnForm();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/inventory/raw-material-return-outbound", {
|
||
warehouse_id: Number(rawReturnForm.warehouse_id),
|
||
waybill_no: rawReturnForm.waybill_no || null,
|
||
order_photo_url: rawReturnForm.order_photo_url || null,
|
||
freight_amount: isFreightBlank(rawReturnForm.freight_amount) ? null : Number(rawReturnForm.freight_amount),
|
||
reason: rawReturnForm.reason || null,
|
||
remark: rawReturnForm.remark || null,
|
||
items: [
|
||
{
|
||
lot_id: Number(rawReturnForm.lot_id),
|
||
purchase_order_item_id: Number(rawReturnForm.purchase_order_item_id),
|
||
return_weight_kg: Number(rawReturnForm.return_weight_kg || 0),
|
||
reason: rawReturnForm.reason || null,
|
||
remark: rawReturnForm.remark || null
|
||
}
|
||
]
|
||
});
|
||
rememberWarehouseArchiveResult(result, "退货出库");
|
||
feedbackMessage.value = `退货出库 ${result.return_no} 已登记${formatArchiveFeedback(result)}`;
|
||
rawReturnDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetRawReturnForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "退货出库登记失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitReturnInbound() {
|
||
resetFeedback();
|
||
const validationError = validateReturnInboundForm();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
return;
|
||
}
|
||
const line = selectedReturnInboundDeliveryLine.value;
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/returns/orders", {
|
||
customer_id: Number(line.customer_id),
|
||
sales_order_id: line.sales_order_id ? Number(line.sales_order_id) : null,
|
||
delivery_id: Number(line.delivery_id),
|
||
warehouse_id: Number(returnInboundForm.warehouse_id),
|
||
handler_employee_id: returnInboundForm.handler_employee_id ? Number(returnInboundForm.handler_employee_id) : null,
|
||
return_reason: returnInboundForm.return_reason || null,
|
||
remark: returnInboundForm.remark || null,
|
||
items: [
|
||
{
|
||
delivery_item_id: Number(line.delivery_item_id),
|
||
sales_order_item_id: line.sales_order_item_id ? Number(line.sales_order_item_id) : null,
|
||
product_item_id: Number(line.product_item_id),
|
||
source_lot_id: line.lot_id ? Number(line.lot_id) : null,
|
||
return_qty: Number(returnInboundForm.return_qty || 0),
|
||
return_weight_kg: Number(returnInboundForm.return_weight_kg || 0),
|
||
remark: returnInboundForm.remark || null
|
||
}
|
||
]
|
||
});
|
||
rememberWarehouseArchiveResult(result, "退货入库");
|
||
feedbackMessage.value = `退货入库 ${result.return_no} 已创建${formatArchiveFeedback(result)}`;
|
||
returnInboundDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetReturnInboundForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "退货入库失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitReturnScrapInbound() {
|
||
resetFeedback();
|
||
const validationError = validateReturnScrapInboundForm();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
return;
|
||
}
|
||
const line = selectedReturnScrapDeliveryLine.value;
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/returns/scrap-inbound", {
|
||
customer_id: Number(line.customer_id),
|
||
delivery_id: Number(line.delivery_id),
|
||
warehouse_id: Number(returnScrapInboundForm.warehouse_id),
|
||
handler_employee_id: returnScrapInboundForm.handler_employee_id ? Number(returnScrapInboundForm.handler_employee_id) : null,
|
||
return_reason: returnScrapInboundForm.return_reason || null,
|
||
remark: returnScrapInboundForm.remark || null,
|
||
items: [
|
||
{
|
||
delivery_item_id: Number(line.delivery_item_id),
|
||
product_item_id: Number(line.product_item_id),
|
||
source_lot_id: line.lot_id ? Number(line.lot_id) : null,
|
||
scrap_qty: Number(returnScrapInboundForm.scrap_qty || 0),
|
||
scrap_weight_kg: Number(returnScrapInboundForm.scrap_weight_kg || 0),
|
||
unit_cost: returnScrapInboundForm.unit_cost === "" ? null : Number(returnScrapInboundForm.unit_cost || 0),
|
||
remark: returnScrapInboundForm.remark || null
|
||
}
|
||
]
|
||
});
|
||
rememberWarehouseArchiveResult(result, "退货废料入库");
|
||
feedbackMessage.value = `退货废料入库 ${result.return_no} 已创建${formatArchiveFeedback(result)}`;
|
||
returnScrapInboundDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetReturnScrapInboundForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "退货废料入库失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitReturnReworkOutbound() {
|
||
resetFeedback();
|
||
const validationError = validateReturnReworkForm();
|
||
if (validationError) {
|
||
errorMessage.value = validationError;
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/returns/rework-outbound", {
|
||
return_item_id: Number(returnReworkForm.return_item_id),
|
||
disposition_type: "REWORK",
|
||
extra_cost_amount: Number(returnReworkForm.extra_cost_amount || 0),
|
||
remark: returnReworkForm.remark || null
|
||
});
|
||
rememberWarehouseArchiveResult(result, "返工出库");
|
||
feedbackMessage.value = `返工出库 ${result.disposition_no} 已完成${formatArchiveFeedback(result)}`;
|
||
returnReworkDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetReturnReworkForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "返工出库失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitProductionOut() {
|
||
resetFeedback();
|
||
if (!Number(productionForm.product_item_id)) {
|
||
errorMessage.value = "请选择产品";
|
||
return;
|
||
}
|
||
const lotValidationError = validateSelectedProductionStockLots(selectedProductionStockLots.value);
|
||
if (lotValidationError) {
|
||
errorMessage.value = lotValidationError;
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const selectedStockLotPayload = selectedProductionStockLots.value.map((lot) => ({
|
||
source_lot_id: Number(lot.lot_id),
|
||
material_item_id: Number(lot.item_id),
|
||
issued_weight_kg: Number(lot.issued_weight_kg)
|
||
}));
|
||
const result = await postResource("/production/work-orders", {
|
||
source_sales_order_item_id: null,
|
||
product_item_id: Number(productionForm.product_item_id),
|
||
planned_qty: Number(referenceProductionQty.value || 0),
|
||
issue_weight_kg: Number(productionForm.issue_weight_kg || 0),
|
||
planned_start_time: null,
|
||
planned_end_time: null,
|
||
priority_level: 3,
|
||
auto_issue_materials: false,
|
||
selected_stock_lots: selectedStockLotPayload,
|
||
remark: productionForm.remark || null
|
||
});
|
||
feedbackMessage.value = `生产出库 ${result.work_order_no} 已创建`;
|
||
productionDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetProductionForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "生产出库失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
function productionWorkOrderInboundDeviationExceeded(actual, theoretical) {
|
||
const actualValue = Number(actual || 0);
|
||
const theoreticalValue = Number(theoretical || 0);
|
||
if (!Number.isFinite(actualValue) || !Number.isFinite(theoreticalValue)) {
|
||
return false;
|
||
}
|
||
if (theoreticalValue === 0) {
|
||
return actualValue !== 0;
|
||
}
|
||
return Math.abs(actualValue - theoreticalValue) / Math.abs(theoreticalValue) > 0.15;
|
||
}
|
||
|
||
function productionWorkOrderInboundFinishedQtyExceedsReference() {
|
||
const preview = productionWorkOrderInboundPreview.value;
|
||
if (!preview) {
|
||
return false;
|
||
}
|
||
const totalFinishedQty = Number(preview.finished_inbound_qty || 0) + Number(productionWorkOrderInboundForm.finished_qty || 0);
|
||
const referenceFinishedQty = Number(preview.reference_finished_qty || 0);
|
||
if (!Number.isFinite(totalFinishedQty) || !Number.isFinite(referenceFinishedQty)) {
|
||
return false;
|
||
}
|
||
if (referenceFinishedQty <= 0) {
|
||
return totalFinishedQty > 0;
|
||
}
|
||
return totalFinishedQty > referenceFinishedQty * 1.15;
|
||
}
|
||
|
||
function productionWorkOrderInboundNeedsDeviationRemark() {
|
||
const preview = productionWorkOrderInboundPreview.value;
|
||
if (!preview) {
|
||
return false;
|
||
}
|
||
return (
|
||
productionWorkOrderInboundFinishedQtyExceedsReference() ||
|
||
productionWorkOrderInboundDeviationExceeded(
|
||
productionWorkOrderInboundForm.surplus_weight_kg,
|
||
preview.theoretical_surplus_weight_kg
|
||
) ||
|
||
productionWorkOrderInboundDeviationExceeded(
|
||
productionWorkOrderInboundForm.scrap_weight_kg,
|
||
preview.theoretical_scrap_weight_kg
|
||
)
|
||
);
|
||
}
|
||
|
||
function buildProductionWorkOrderInboundDeviationPrompt() {
|
||
const preview = productionWorkOrderInboundPreview.value || {};
|
||
const reasons = [];
|
||
if (productionWorkOrderInboundFinishedQtyExceedsReference()) {
|
||
reasons.push(
|
||
`成品入库数量超过本生产台账参考生产数量上限15%:参考 ${formatQty(preview.reference_finished_qty)} 件,已入库 ${formatQty(preview.finished_inbound_qty)} 件,本次 ${formatQty(productionWorkOrderInboundForm.finished_qty)} 件`
|
||
);
|
||
}
|
||
if (productionWorkOrderInboundDeviationExceeded(productionWorkOrderInboundForm.surplus_weight_kg, preview.theoretical_surplus_weight_kg)) {
|
||
reasons.push(`余料入库重量与理论值偏差超过上下15%:理论 ${formatWeight(preview.theoretical_surplus_weight_kg)},本次 ${formatWeight(productionWorkOrderInboundForm.surplus_weight_kg)}`);
|
||
}
|
||
if (productionWorkOrderInboundDeviationExceeded(productionWorkOrderInboundForm.scrap_weight_kg, preview.theoretical_scrap_weight_kg)) {
|
||
reasons.push(`废料重量与理论值偏差超过上下15%:理论 ${formatWeight(preview.theoretical_scrap_weight_kg)},本次 ${formatWeight(productionWorkOrderInboundForm.scrap_weight_kg)}`);
|
||
}
|
||
return [
|
||
"以下指标超过15%导致异常,请填写偏差说明:",
|
||
"",
|
||
...reasons.map((reason, index) => `${index + 1}. ${reason}`)
|
||
].join("\n");
|
||
}
|
||
|
||
function ensureProductionWorkOrderInboundDeviationRemark() {
|
||
if (!productionWorkOrderInboundNeedsDeviationRemark() || String(productionWorkOrderInboundForm.deviation_remark || "").trim()) {
|
||
return true;
|
||
}
|
||
const remark = window.prompt(buildProductionWorkOrderInboundDeviationPrompt());
|
||
if (!String(remark || "").trim()) {
|
||
errorMessage.value = "偏差超过上下15%时必须填写偏差说明";
|
||
return false;
|
||
}
|
||
productionWorkOrderInboundForm.deviation_remark = String(remark).trim();
|
||
return true;
|
||
}
|
||
|
||
function buildProductionWorkOrderInboundConfirmMessage(finishedQty, surplusWeight, scrapWeight) {
|
||
const preview = productionWorkOrderInboundPreview.value || {};
|
||
const lines = [
|
||
"请确认本次生产台账入库",
|
||
"",
|
||
`生产台账:${preview.material_lot_no || ""} · ${preview.product_name || ""}`,
|
||
`产品:${preview.product_name || ""}`,
|
||
`材料库存批次号:${preview.material_lot_no || ""}`,
|
||
`参考成品数量:${formatQty(preview.reference_finished_qty)} 件`,
|
||
`已入库成品:${formatQty(preview.finished_inbound_qty)} 件`,
|
||
`成品入库:${formatQty(finishedQty)} 件`,
|
||
`余料入库:${formatWeight(surplusWeight)}`,
|
||
`废料入库:${formatWeight(scrapWeight)}`,
|
||
`理论余料:${formatWeight(preview.theoretical_surplus_weight_kg)}`,
|
||
`理论废料:${formatWeight(preview.theoretical_scrap_weight_kg)}`
|
||
];
|
||
if (productionWorkOrderInboundForm.settle_work_order) {
|
||
lines.push(
|
||
"",
|
||
"本次勾选了该批材料结单。确认后该生产台账将变为锁单,小程序不再显示该材料库存批次号,后续不能再对该批材料继续生产入库。"
|
||
);
|
||
}
|
||
return lines.join("\n");
|
||
}
|
||
|
||
async function submitProductionWorkOrderInbound() {
|
||
resetFeedback();
|
||
if (!Number(selectedInboundWorkOrderId.value || 0)) {
|
||
errorMessage.value = "请选择生产台账";
|
||
return;
|
||
}
|
||
if (!productionWorkOrderInboundPreview.value) {
|
||
await loadProductionWorkOrderInboundPreview(true);
|
||
}
|
||
const finishedQty = Number(productionWorkOrderInboundForm.finished_qty || 0);
|
||
const surplusWeight = Number(productionWorkOrderInboundForm.surplus_weight_kg || 0);
|
||
const scrapWeight = Number(productionWorkOrderInboundForm.scrap_weight_kg || 0);
|
||
if (finishedQty < 0 || surplusWeight < 0 || scrapWeight < 0) {
|
||
errorMessage.value = "入库数量或重量不能小于0";
|
||
return;
|
||
}
|
||
if (finishedQty <= 0 && surplusWeight <= 0 && scrapWeight <= 0) {
|
||
errorMessage.value = "生产台账入库至少需要填写成品、余料或废料中的一项";
|
||
return;
|
||
}
|
||
const confirmed = window.confirm(buildProductionWorkOrderInboundConfirmMessage(finishedQty, surplusWeight, scrapWeight));
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
if (!ensureProductionWorkOrderInboundDeviationRemark()) {
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/production/production-ledger-inbounds", {
|
||
production_ledger_id: Number(selectedInboundWorkOrderId.value),
|
||
finished_qty: finishedQty,
|
||
surplus_weight_kg: surplusWeight,
|
||
scrap_weight_kg: scrapWeight,
|
||
deviation_remark: productionWorkOrderInboundForm.deviation_remark || null,
|
||
remark: productionWorkOrderInboundForm.remark || null,
|
||
lock_material_batch: Boolean(productionWorkOrderInboundForm.settle_work_order),
|
||
lock_confirmed: Boolean(productionWorkOrderInboundForm.settle_work_order)
|
||
});
|
||
rememberWarehouseArchiveResult(result, "生产台账入库");
|
||
feedbackMessage.value = `生产台账入库 ${result.material_lot_no || ""} 已保存${formatArchiveFeedback(result)}`;
|
||
productionWorkOrderInboundDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetProductionWorkOrderInboundForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "生产台账入库保存失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitCompletionInbound() {
|
||
resetFeedback();
|
||
if (!selectedCompletionWorkOrder.value) {
|
||
errorMessage.value = `${isReworkCompletionOperation.value ? "返工入库" : "生产入库"}必须先选择${isReworkCompletionOperation.value ? "返工工单" : "生产工单"}`;
|
||
return;
|
||
}
|
||
const receiptQty = Number(completionForm.receipt_qty || 0);
|
||
const toleranceMax = Number(completionForm.tolerance_receivable_qty || completionForm.max_receivable_qty || 0);
|
||
if (!Number.isFinite(receiptQty) || receiptQty <= 0) {
|
||
errorMessage.value = "入库数量必须大于0";
|
||
return;
|
||
}
|
||
if (toleranceMax > 0 && receiptQty > toleranceMax) {
|
||
errorMessage.value = "入库数量超过系统允许范围,请调整后重试";
|
||
return;
|
||
}
|
||
if (completionForm.settle_work_order && completionSettleNeedsRemark.value && !String(completionForm.settle_remark || "").trim()) {
|
||
errorMessage.value = "该工单结单入库成品数量超出标准成品数量上下15%,请填写结单说明";
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/production/completion-receipts", {
|
||
work_order_id: Number(completionForm.work_order_id),
|
||
warehouse_id: Number(completionForm.warehouse_id),
|
||
receiver_employee_id: completionForm.receiver_employee_id ? Number(completionForm.receiver_employee_id) : null,
|
||
remark: completionForm.remark || null,
|
||
settle_work_order: Boolean(completionForm.settle_work_order),
|
||
settle_remark: completionForm.settle_work_order ? completionForm.settle_remark || null : null,
|
||
items: [
|
||
{
|
||
product_item_id: Number(completionForm.product_item_id),
|
||
location_id: completionForm.location_id ? Number(completionForm.location_id) : null,
|
||
receipt_qty: Number(completionForm.receipt_qty || 0),
|
||
receipt_weight_kg: Number(completionForm.receipt_weight_kg || 0)
|
||
}
|
||
]
|
||
});
|
||
feedbackMessage.value = `${isReworkCompletionOperation.value ? "返工入库单" : "生产入库单"} ${result.receipt_no} 已创建`;
|
||
completionDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetCompletionForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || `${isReworkCompletionOperation.value ? "返工入库" : "生产入库"}失败`;
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function submitSalesOut() {
|
||
resetFeedback();
|
||
if (isSalesOrderDelivery.value && !Number(deliveryForm.sales_order_id)) {
|
||
errorMessage.value = "请选择销售订单";
|
||
return;
|
||
}
|
||
if (isSalesOrderDelivery.value && !Number(deliveryForm.sales_order_item_id)) {
|
||
errorMessage.value = "请选择销售订单明细";
|
||
return;
|
||
}
|
||
if (!Number(deliveryForm.customer_id)) {
|
||
errorMessage.value = "请选择收货客户";
|
||
return;
|
||
}
|
||
if (!Number(deliveryForm.product_item_id)) {
|
||
errorMessage.value = "请选择发货产品";
|
||
return;
|
||
}
|
||
if (!Number(deliveryForm.warehouse_id)) {
|
||
errorMessage.value = "请选择发货仓";
|
||
return;
|
||
}
|
||
const requestedQty = Number(deliveryForm.delivery_qty || 0);
|
||
if (!Number.isFinite(requestedQty) || requestedQty <= 0) {
|
||
errorMessage.value = "发货数量必须大于0";
|
||
return;
|
||
}
|
||
if (totalAllocatedQty.value < requestedQty) {
|
||
errorMessage.value = deliveryStockShortageMessage(requestedQty, totalAvailableQty.value);
|
||
return;
|
||
}
|
||
const logisticsError = validateLogisticsForm(deliveryForm);
|
||
if (logisticsError) {
|
||
errorMessage.value = logisticsError;
|
||
return;
|
||
}
|
||
submittingOperation.value = true;
|
||
try {
|
||
const result = await postResource("/sales/deliveries", {
|
||
sales_order_id: isSalesOrderDelivery.value ? Number(deliveryForm.sales_order_id) : null,
|
||
customer_id: Number(deliveryForm.customer_id),
|
||
warehouse_id: Number(deliveryForm.warehouse_id),
|
||
shipper_employee_id: deliveryForm.shipper_employee_id ? Number(deliveryForm.shipper_employee_id) : null,
|
||
consignee_name: deliveryForm.consignee_name || null,
|
||
consignee_phone: deliveryForm.consignee_phone || null,
|
||
delivery_address: deliveryForm.delivery_address || null,
|
||
waybill_no: deliveryForm.waybill_no || null,
|
||
order_photo_url: deliveryForm.order_photo_url || null,
|
||
freight_amount: Number(deliveryForm.freight_amount),
|
||
remark: deliveryForm.remark || null,
|
||
items: allocationRows.value.map((item) => ({
|
||
sales_order_item_id: isSalesOrderDelivery.value ? Number(deliveryForm.sales_order_item_id) : null,
|
||
product_item_id: Number(deliveryForm.product_item_id),
|
||
lot_id: Number(item.lot_id),
|
||
delivery_qty: Number(item.delivery_qty || 0),
|
||
delivery_weight_kg: Number(item.delivery_weight_kg || 0),
|
||
unit_price: Number(selectedSalesOrderItem.value?.unit_price || 0),
|
||
remark: deliveryForm.remark || null
|
||
}))
|
||
});
|
||
rememberWarehouseArchiveResult(result, "销售出库单");
|
||
feedbackMessage.value = `销售出库单 ${result.delivery_no} 已创建${formatArchiveFeedback(result)}`;
|
||
deliveryDrawerOpen.value = false;
|
||
await loadAll();
|
||
resetDeliveryForm();
|
||
} catch (error) {
|
||
errorMessage.value = error.message || "销售出库失败";
|
||
} finally {
|
||
submittingOperation.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadProductionStockLotOptions(materialItemIds) {
|
||
const requestSeq = ++productionStockLotOptionsRequestSeq;
|
||
const ids = Array.from(new Set((materialItemIds || []).map((id) => Number(id)).filter(Boolean)));
|
||
if (!ids.length) {
|
||
productionStockLotOptions.value = [];
|
||
return;
|
||
}
|
||
const resultSets = await Promise.all(
|
||
ids.map((materialItemId) => fetchResource(`/inventory/stock-lot-options?material_item_id=${materialItemId}&limit=1000`, []))
|
||
);
|
||
if (requestSeq !== productionStockLotOptionsRequestSeq) {
|
||
return;
|
||
}
|
||
const optionRows = resultSets.flatMap((rows) => (Array.isArray(rows) ? rows : []));
|
||
productionStockLotOptions.value = optionRows;
|
||
const optionMap = new Map(optionRows.map((lot) => [Number(lot.lot_id), lot]));
|
||
selectedProductionStockLots.value = selectedProductionStockLots.value
|
||
.filter((lot) => optionMap.has(Number(lot.lot_id)))
|
||
.map((lot) => ({
|
||
...optionMap.get(Number(lot.lot_id)),
|
||
issued_weight_kg: normalizeIssuedWeight(lot.issued_weight_kg)
|
||
}));
|
||
}
|
||
|
||
async function loadGenericSourceLotOptions(materialItemIds) {
|
||
const requestSeq = ++genericSourceLotOptionsRequestSeq;
|
||
const ids = [...new Set((Array.isArray(materialItemIds) ? materialItemIds : [materialItemIds]).map((item) => Number(item || 0)).filter(Boolean))];
|
||
if (!ids.length || !usesGenericSourceLotSelector.value) {
|
||
genericSourceLotOptions.value = [];
|
||
selectedGenericSourceLots.value = [];
|
||
return;
|
||
}
|
||
try {
|
||
if (isScrapSaleOutbound.value) {
|
||
const optionRows = buildScrapSaleSourceLotOptions(genericForm.item_id, genericForm.warehouse_id);
|
||
if (requestSeq !== genericSourceLotOptionsRequestSeq) {
|
||
return;
|
||
}
|
||
genericSourceLotOptions.value = optionRows;
|
||
const optionMap = new Map(optionRows.map((lot) => [Number(lot.lot_id), lot]));
|
||
selectedGenericSourceLots.value = selectedGenericSourceLots.value
|
||
.filter((lot) => optionMap.has(Number(lot.lot_id)))
|
||
.map((lot) => ({
|
||
...optionMap.get(Number(lot.lot_id)),
|
||
issued_weight_kg: normalizeIssuedWeight(lot.issued_weight_kg)
|
||
}));
|
||
return;
|
||
}
|
||
if (isProductionSurplusInbound.value && selectedGenericWorkOrder.value) {
|
||
const optionRows = buildWorkOrderSourceLotOptions(selectedGenericWorkOrder.value);
|
||
if (requestSeq !== genericSourceLotOptionsRequestSeq) {
|
||
return;
|
||
}
|
||
genericSourceLotOptions.value = optionRows;
|
||
const optionMap = new Map(optionRows.map((lot) => [Number(lot.lot_id), lot]));
|
||
selectedGenericSourceLots.value = selectedGenericSourceLots.value
|
||
.filter((lot) => optionMap.has(Number(lot.lot_id)))
|
||
.map((lot) => {
|
||
const latestOption = optionMap.get(Number(lot.lot_id)) || {};
|
||
return {
|
||
...latestOption,
|
||
return_weight_kg: Number(lot.return_weight_kg || 0),
|
||
issued_weight_kg: Number(latestOption.issued_weight_kg || 0)
|
||
};
|
||
});
|
||
return;
|
||
}
|
||
const resultSets = await Promise.all(
|
||
ids.map((materialItemId) => fetchResource(`/inventory/stock-lot-options?material_item_id=${materialItemId}&limit=1000`, []))
|
||
);
|
||
if (requestSeq !== genericSourceLotOptionsRequestSeq) {
|
||
return;
|
||
}
|
||
const rowsByLotId = new Map();
|
||
resultSets.flatMap((rows) => (Array.isArray(rows) ? rows : [])).forEach((lot) => rowsByLotId.set(Number(lot.lot_id), lot));
|
||
genericSourceLotOptions.value = Array.from(rowsByLotId.values());
|
||
const optionMap = new Map(genericSourceLotOptions.value.map((lot) => [Number(lot.lot_id), lot]));
|
||
selectedGenericSourceLots.value = selectedGenericSourceLots.value
|
||
.filter((lot) => optionMap.has(Number(lot.lot_id)))
|
||
.map((lot) => ({
|
||
...optionMap.get(Number(lot.lot_id)),
|
||
return_weight_kg: Number(lot.return_weight_kg || 0),
|
||
issued_weight_kg: normalizeIssuedWeight(lot.issued_weight_kg)
|
||
}));
|
||
} catch (error) {
|
||
if (requestSeq === genericSourceLotOptionsRequestSeq) {
|
||
genericSourceLotOptions.value = [];
|
||
selectedGenericSourceLots.value = [];
|
||
errorMessage.value = error.message || "来源库存批次号加载失败";
|
||
}
|
||
}
|
||
}
|
||
|
||
function normalizeIssuedWeight(value) {
|
||
const issuedWeight = Number(value);
|
||
return Number.isFinite(issuedWeight) ? issuedWeight : 0;
|
||
}
|
||
|
||
function validateSelectedProductionStockLots(lots) {
|
||
if (!lots.length) {
|
||
return "请选择一个材料库存批次号";
|
||
}
|
||
if (lots.length > 1) {
|
||
return "一次生产出库只能选择一个材料库存批次号";
|
||
}
|
||
const allowedMaterialIds = new Set(selectedProductionBomMaterialRows.value.map((item) => Number(item.material_item_id || 0)).filter(Boolean));
|
||
const lot = lots[0];
|
||
if (allowedMaterialIds.size && !allowedMaterialIds.has(Number(lot.item_id || 0))) {
|
||
return `材料库存批次号 ${lot.lot_no || "未命名批次"} 不是该产品需用清单中的默认用料`;
|
||
}
|
||
const issuedWeight = Number(lot.issued_weight_kg);
|
||
const remainingWeight = Number(lot.remaining_weight_kg || 0);
|
||
const lotNo = lot.lot_no || "未命名批次";
|
||
if (!Number.isFinite(issuedWeight) || issuedWeight <= 0) {
|
||
return `材料库存批次号 ${lotNo} 的本次出库重量必须大于0`;
|
||
}
|
||
if (issuedWeight > remainingWeight) {
|
||
return `材料库存批次号 ${lotNo} 的本次出库重量不能超过剩余重量 ${formatWeight(remainingWeight)}`;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
async function loadAll() {
|
||
const [
|
||
stockLots,
|
||
stockBalances,
|
||
inventoryTransactions,
|
||
materialRows,
|
||
productRows,
|
||
bomRows,
|
||
processRouteRows,
|
||
workOrderRows,
|
||
workOrderLedgerRowData,
|
||
productionLedgerRowData,
|
||
warehouseRows,
|
||
locationRows,
|
||
employeeRows,
|
||
customerRows,
|
||
salesOrderRows,
|
||
salesOrderItemRows,
|
||
deliveryRows,
|
||
deliveryItemRows,
|
||
returnItemRows
|
||
] = await Promise.all([
|
||
fetchResource("/inventory/stock-lots?limit=500", []),
|
||
fetchResource("/inventory/stock-balances?limit=500", []),
|
||
fetchResource("/inventory/transactions?limit=800", []),
|
||
fetchResource("/master-data/materials?limit=200", []),
|
||
fetchResource("/master-data/products?limit=200", []),
|
||
fetchResource("/master-data/boms?limit=500", []),
|
||
fetchResource("/master-data/process-routes?limit=500", []),
|
||
fetchResource("/production/work-orders?limit=500", []),
|
||
fetchResource("/production/work-order-ledger?limit=500", []),
|
||
fetchResource("/production/production-ledger?limit=500", []),
|
||
fetchResource("/master-data/warehouses", []),
|
||
fetchResource("/master-data/locations", []),
|
||
fetchPermissionEmployeeOptions(PERSONNEL_PERMISSION_CODES.INVENTORY_LEDGER, 500),
|
||
fetchResource("/sales/customers?limit=200", []),
|
||
fetchResource("/sales/orders?limit=500", []),
|
||
fetchResource("/sales/order-items?limit=1000", []),
|
||
fetchResource("/sales/deliveries?limit=500", []),
|
||
fetchResource("/sales/delivery-items?limit=500", []),
|
||
fetchResource("/returns/items?limit=500", [])
|
||
]);
|
||
lots.value = stockLots;
|
||
balances.value = stockBalances;
|
||
transactions.value = inventoryTransactions;
|
||
materials.value = materialRows;
|
||
products.value = productRows;
|
||
boms.value = bomRows;
|
||
processRoutes.value = processRouteRows;
|
||
workOrders.value = workOrderRows;
|
||
workOrderLedgerRows.value = workOrderLedgerRowData;
|
||
productionLedgerRows.value = productionLedgerRowData;
|
||
warehouses.value = warehouseRows;
|
||
locations.value = locationRows;
|
||
employees.value = employeeRows;
|
||
customers.value = customerRows;
|
||
salesOrders.value = salesOrderRows;
|
||
salesOrderItems.value = salesOrderItemRows;
|
||
deliveries.value = deliveryRows;
|
||
deliveryItems.value = deliveryItemRows;
|
||
returnItems.value = returnItemRows;
|
||
}
|
||
|
||
watch([warehouseLedgerPage, warehouseLedgerPageSize], () => {
|
||
if (warehouseLedgerDrawerOpen.value) {
|
||
loadWarehouseLedger();
|
||
}
|
||
});
|
||
|
||
watch(activeInventoryTab, () => {
|
||
if (warehouseLedgerDrawerOpen.value) {
|
||
warehouseLedgerPage.value = 1;
|
||
Object.assign(warehouseLedgerFilters, buildEmptyWarehouseLedgerFilters());
|
||
loadWarehouseLedger();
|
||
}
|
||
});
|
||
|
||
watch(
|
||
() => [
|
||
genericForm.item_id,
|
||
genericForm.source_line_id,
|
||
genericForm.work_order_id,
|
||
genericForm.warehouse_id,
|
||
activeWarehouseType.value,
|
||
activeOperation.value?.bizType,
|
||
genericDrawerOpen.value,
|
||
genericSourceLotMaterialIds.value.join(",")
|
||
],
|
||
async () => {
|
||
if (!genericDrawerOpen.value || !usesGenericSourceLotSelector.value) {
|
||
return;
|
||
}
|
||
selectedGenericSourceLots.value = [];
|
||
await loadGenericSourceLotOptions(genericSourceLotMaterialIds.value);
|
||
}
|
||
);
|
||
|
||
watch(
|
||
selectedGenericSourceLots,
|
||
() => {
|
||
if (usesGenericSourceLotSelector.value && !isProductTraceOnlySourceLotInbound.value) {
|
||
genericForm.weight_kg = Number(genericSelectedSourceLotWeight.value.toFixed(3));
|
||
}
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
watch(() => productionForm.product_item_id, async () => {
|
||
if (!productionDrawerOpen.value) {
|
||
return;
|
||
}
|
||
selectedProductionStockLots.value = [];
|
||
productionForm.issue_weight_kg = 0;
|
||
await loadProductionStockLotOptions(selectedProductionBomMaterialRows.value.map((item) => item.material_item_id));
|
||
});
|
||
|
||
watch(
|
||
selectedProductionStockLots,
|
||
() => {
|
||
if (selectedProductionStockLots.value.length) {
|
||
productionForm.issue_weight_kg = Number(selectedProductionIssueWeight.value.toFixed(3));
|
||
} else {
|
||
productionForm.issue_weight_kg = 0;
|
||
}
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
onMounted(async () => {
|
||
await loadAll();
|
||
});
|
||
|
||
onBeforeUnmount(clearWarehouseLedgerTooltipListeners);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.work-order-settle-panel {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 14px;
|
||
border: 1px solid rgba(37, 99, 235, 0.14);
|
||
border-radius: 18px;
|
||
background: linear-gradient(135deg, rgba(239, 246, 255, 0.92), rgba(255, 255, 255, 0.96));
|
||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.08);
|
||
}
|
||
|
||
.work-order-settle-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: fit-content;
|
||
cursor: pointer;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.work-order-settle-toggle.is-disabled {
|
||
cursor: not-allowed;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.work-order-settle-toggle input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.settle-dot {
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 2px solid #94a3b8;
|
||
border-radius: 999px;
|
||
background: #fff;
|
||
box-shadow: inset 0 0 0 4px #fff;
|
||
transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
|
||
}
|
||
|
||
.work-order-settle-toggle input:checked + .settle-dot {
|
||
border-color: #2563eb;
|
||
background: #2563eb;
|
||
box-shadow: inset 0 0 0 4px #fff, 0 0 0 4px rgba(37, 99, 235, 0.12);
|
||
}
|
||
|
||
.work-order-settle-toggle input:disabled + .settle-dot {
|
||
border-color: #cbd5e1;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.inventory-live-summary {
|
||
margin-top: 14px;
|
||
margin-bottom: 8px;
|
||
padding: 12px 16px;
|
||
border: 1px solid #d9ecff;
|
||
border-radius: 4px;
|
||
background: #ecf5ff;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.special-adjustment-warning {
|
||
display: grid;
|
||
gap: 6px;
|
||
padding: 14px 16px;
|
||
border: 1px solid rgba(180, 83, 9, 0.24);
|
||
border-radius: 16px;
|
||
background: linear-gradient(135deg, rgba(255, 247, 237, 0.96), rgba(254, 243, 199, 0.72));
|
||
color: #78350f;
|
||
}
|
||
|
||
.special-adjustment-warning strong {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.special-adjustment-warning span,
|
||
.special-adjustment-tip,
|
||
.special-adjustment-selected-note p {
|
||
color: #92400e;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.special-adjustment-lines {
|
||
display: grid;
|
||
grid-column: 1 / -1;
|
||
gap: 14px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.special-adjustment-line-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
border: 1.5px solid rgba(20, 14, 6, 0.72);
|
||
background:
|
||
linear-gradient(90deg, rgba(31, 122, 74, 0.06), transparent 44%),
|
||
rgba(255, 250, 240, 0.72);
|
||
box-shadow:
|
||
inset 0 0 0 1px rgba(255, 255, 255, 0.5),
|
||
0 8px 18px rgba(52, 39, 15, 0.08);
|
||
}
|
||
|
||
.special-adjustment-line-card::before {
|
||
position: absolute;
|
||
inset: 5px;
|
||
content: "";
|
||
border: 1px dashed rgba(20, 14, 6, 0.16);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.special-adjustment-line-head {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
min-height: 44px;
|
||
padding: 8px 10px 8px 12px;
|
||
border-bottom: 1.5px solid rgba(20, 14, 6, 0.72);
|
||
background:
|
||
linear-gradient(180deg, rgba(255, 255, 255, 0.26), transparent),
|
||
rgba(227, 207, 161, 0.62);
|
||
}
|
||
|
||
.special-adjustment-line-head div {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.special-adjustment-line-head strong {
|
||
color: #17120a;
|
||
font-family: var(--ui-font-display);
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.special-adjustment-line-index {
|
||
display: inline-grid;
|
||
place-items: center;
|
||
min-width: 58px;
|
||
height: 26px;
|
||
padding: 0 8px;
|
||
color: var(--document-paper-stamp, #1f7a4a);
|
||
border: 1.5px solid currentColor;
|
||
background: rgba(232, 246, 232, 0.58);
|
||
font-family: var(--ui-font-display);
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.special-adjustment-card-grid {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 0;
|
||
border-left: 1.5px solid rgba(20, 14, 6, 0.72);
|
||
}
|
||
|
||
.special-adjustment-card-field,
|
||
.special-adjustment-card-preview {
|
||
display: grid;
|
||
grid-template-rows: auto minmax(48px, auto);
|
||
min-width: 0;
|
||
margin: 0;
|
||
border-right: 1.5px solid rgba(20, 14, 6, 0.72);
|
||
border-bottom: 1.5px solid rgba(20, 14, 6, 0.72);
|
||
background: rgba(255, 250, 240, 0.42);
|
||
}
|
||
|
||
.special-adjustment-card-field-wide,
|
||
.special-adjustment-card-preview {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.special-adjustment-card-field > span,
|
||
.special-adjustment-card-preview > span {
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: 30px;
|
||
padding: 7px 10px;
|
||
color: rgba(23, 18, 10, 0.74);
|
||
border-bottom: 1px solid rgba(20, 14, 6, 0.48);
|
||
background:
|
||
linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent),
|
||
rgba(227, 207, 161, 0.5);
|
||
font-family: var(--ui-font-display);
|
||
font-size: 12px;
|
||
font-weight: 900;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.special-adjustment-card-field input,
|
||
.special-adjustment-card-field select,
|
||
.special-adjustment-card-static,
|
||
.special-adjustment-selected-note p,
|
||
.special-adjustment-card-preview strong {
|
||
width: 100%;
|
||
min-width: 0;
|
||
min-height: 48px;
|
||
margin: 0;
|
||
padding: 10px 11px;
|
||
color: #17120a;
|
||
border: 0;
|
||
border-radius: 0;
|
||
outline: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
font-family: "Songti SC", "SimSun", var(--ui-font-body);
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.special-adjustment-card-field input:focus,
|
||
.special-adjustment-card-field select:focus {
|
||
background:
|
||
linear-gradient(90deg, rgba(31, 122, 74, 0.12), transparent 42%),
|
||
rgba(255, 255, 255, 0.48);
|
||
box-shadow: inset 0 0 0 2px rgba(31, 122, 74, 0.42);
|
||
}
|
||
|
||
.special-adjustment-card-field select {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.special-adjustment-card-static {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #6a5631;
|
||
}
|
||
|
||
.special-adjustment-selected-note {
|
||
background:
|
||
repeating-linear-gradient(-45deg, rgba(31, 122, 74, 0.05) 0 5px, transparent 5px 10px),
|
||
rgba(232, 246, 232, 0.34);
|
||
}
|
||
|
||
.special-adjustment-selected-note p {
|
||
display: flex;
|
||
align-items: center;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.special-adjustment-card-preview {
|
||
background:
|
||
linear-gradient(90deg, rgba(31, 122, 74, 0.1), transparent 56%),
|
||
rgba(255, 250, 240, 0.62);
|
||
}
|
||
|
||
.special-adjustment-card-preview strong {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #145c39;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.special-adjustment-empty {
|
||
display: grid;
|
||
gap: 6px;
|
||
min-height: 92px;
|
||
padding: 18px;
|
||
color: #6a5631;
|
||
border: 1.5px dashed rgba(106, 86, 49, 0.42);
|
||
background: rgba(255, 250, 240, 0.58);
|
||
}
|
||
|
||
.special-adjustment-empty strong {
|
||
color: #17120a;
|
||
font-family: var(--ui-font-display);
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.special-adjustment-empty span {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.special-adjustment-card-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 620px) {
|
||
.special-adjustment-line-head {
|
||
align-items: stretch;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.special-adjustment-card-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.special-adjustment-card-field-wide,
|
||
.special-adjustment-card-preview {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.settle-summary-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.settle-summary-grid article {
|
||
padding: 10px 12px;
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.82);
|
||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||
}
|
||
|
||
.settle-summary-grid span {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.settle-summary-grid strong {
|
||
color: #0f172a;
|
||
}
|
||
|
||
.settle-summary-grid .settle-warning {
|
||
color: #b45309;
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.settle-summary-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|