ForgeFlow-ERP/frontend/src/views/InventoryLedgerView.vue
2026-06-14 16:23:27 +08:00

7159 lines
302 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>