Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test
This commit is contained in:
@@ -26,6 +26,11 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { lastMileStatusLabel, packageDifferenceText, toLastMileOrderListItem } from '../lastMilePresentation'
|
||||
import type { LastMileOrderListItem } from '../types'
|
||||
import { reworkRelabelApplications } from '../relabelRework'
|
||||
const reworkOpen = ref(false)
|
||||
const reworkSaving = ref(false)
|
||||
const reworkOptions = ref<Array<{ relabelNo: string; status: string }>>([])
|
||||
const reworkForm = reactive({ relabelNo: '', remark: '' })
|
||||
const rows = ref<LastMileOrderListItem[]>([])
|
||||
const submitExport = useExportTask()
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -955,7 +960,7 @@ async function reviewDirectRelabel(source: object) {
|
||||
}
|
||||
}
|
||||
function canConfirmRelabel(row: { relabelStatusCode?: string }) { return row.relabelStatusCode === 'CUSTOMER_CONFIRM_PENDING' }
|
||||
function canReworkRelabel(row: { relabelStatusCode?: string }) { return ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(row.relabelStatusCode ?? '') }
|
||||
function canReworkRelabel(row: LastMileOrderListItem) { return reworkRelabelApplications(row).length > 0 }
|
||||
function hasRelabelActions(row: LastMileOrderListItem) {
|
||||
return access.can(permission.relabelOrderStatus)
|
||||
&& Boolean(row.relabelOrderNo)
|
||||
@@ -986,7 +991,7 @@ function lastMileRowActions(row: LastMileOrderListItem): RowActionItem[] {
|
||||
{ key: 'hold', label: row.detained ? '解除暂扣' : '暂扣订单', priority: 300, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && (row.detained || row.currentStatusCode === 'INBOUND_COMPLETED') },
|
||||
{ key: 'manual-nodes', label: '人工修改节点', priority: 200, visible: access.can(permission.lastMileOrderTransfer) },
|
||||
{ key: 'confirm-relabel', label: '换标完成确认', priority: 150, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canConfirmRelabel(row) },
|
||||
{ key: 'rework-relabel', label: '重新换标', priority: 100, tone: 'danger', divided: true, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canReworkRelabel(row) },
|
||||
{ key: 'rework-relabel', label: '换标退回重做', priority: 100, tone: 'danger', divided: true, visible: access.can(permission.relabelOrderStatus) && canReworkRelabel(row) },
|
||||
]
|
||||
}
|
||||
function handleLastMileRowAction(command: string, row: LastMileOrderListItem) {
|
||||
@@ -1011,16 +1016,26 @@ async function confirmRelabel(row: { relabelOrderNo?: string }) {
|
||||
await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标完成确认失败').message) }
|
||||
}
|
||||
async function reworkRelabel(row: { relabelOrderNo?: string }) {
|
||||
const relabelNo = row.relabelOrderNo
|
||||
if (!relabelNo) return
|
||||
function reworkRelabel(row: LastMileOrderListItem) {
|
||||
reworkOptions.value = reworkRelabelApplications(row)
|
||||
if (!reworkOptions.value.length) return
|
||||
reworkForm.relabelNo = reworkOptions.value[0].relabelNo
|
||||
reworkForm.remark = ''
|
||||
reworkOpen.value = true
|
||||
}
|
||||
async function submitReworkRelabel() {
|
||||
if (reworkSaving.value || !reworkForm.relabelNo || !reworkForm.remark.trim()) return
|
||||
const relabelNo = reworkForm.relabelNo
|
||||
reworkSaving.value = true
|
||||
try {
|
||||
const task = await getRelabelReviewTask(relabelNo)
|
||||
const { value } = await ElMessageBox.prompt('填写退回原因,仓库将重新执行换标并上传照片。', '重新换标', { customClass: 'admin-density-confirm', inputType: 'textarea', inputValidator: (value) => Boolean(value?.trim()) && value.trim().length <= 1000 || '请填写 1 至 1000 字原因', confirmButtonText: '确认重新换标', cancelButtonText: '取消' })
|
||||
await returnRelabelForRework(relabelNo, task.version, value.trim(), crypto.randomUUID())
|
||||
if (task.taskStatus !== 'WAIT_REVIEW') { ElMessage.warning('仓库尚未提交完工复核,暂不能退回重做'); return }
|
||||
await returnRelabelForRework(relabelNo, task.version, reworkForm.remark.trim(), crypto.randomUUID())
|
||||
ElMessage.success('已退回重新换标')
|
||||
reworkOpen.value = false
|
||||
await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '重新换标失败').message) }
|
||||
} catch (error) { ElMessage.error(getApiErrorDetails(error, '重新换标失败').message) }
|
||||
finally { reworkSaving.value = false }
|
||||
}
|
||||
function openDetail(source: object) {
|
||||
const row = source as LastMileOrderListItem
|
||||
@@ -1255,6 +1270,12 @@ onMounted(() => { void load() })
|
||||
<el-alert title="保存后该订单只显示人工节点,不再跟随柜级节点;请至少填写一个节点时间。" type="warning" :closable="false" show-icon />
|
||||
<div class="manual-node-table"><div class="manual-node-table__head"><span>节点</span><span>节点时间</span></div><div v-for="node in manualNodeDefinitions" :key="node.code" class="manual-node-table__row"><span>{{ node.label }}</span><el-date-picker v-model="manualNodeValues[node.code]" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="年/月/日 --:--" /></div></div>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="reworkOpen" title="换标退回重做" width="min(620px, calc(100vw - 28px))" confirm-text="确认退回重做" :confirm-loading="reworkSaving" :confirm-disabled="!reworkForm.relabelNo || !reworkForm.remark.trim()" @confirm="submitReworkRelabel">
|
||||
<el-form class="density-form" label-position="top">
|
||||
<el-form-item label="换标单" required><el-select v-model="reworkForm.relabelNo" :disabled="reworkSaving" placeholder="请选择需要退回的换标单"><el-option v-for="application in reworkOptions" :key="application.relabelNo" :label="application.relabelNo" :value="application.relabelNo" /></el-select></el-form-item>
|
||||
<el-form-item label="退回原因" required><el-input v-model="reworkForm.remark" :disabled="reworkSaving" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="填写退回原因,仓库将重新执行换标并上传照片" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -64,7 +64,7 @@ function transferCustomerName(row: WarehouseTransferOutboundCandidate) {
|
||||
const customer = tenantOptions.value.find(item => String(item.id) === String(row.customerId))
|
||||
return customer?.legalName?.trim() || customer?.tenantName?.trim() || `客户 ${row.customerId}`
|
||||
}
|
||||
const selectedReturnStock = computed(() => returnStockRows.value.filter((item) => Number(item.availableQuantity || 0) > 0))
|
||||
const selectedReturnStock = computed(() => returnStockRows.value.filter((item) => Number(returnPickMode.value === 'SKU' ? item.looseAvailableQuantity || 0 : item.availableQuantity || 0) > 0))
|
||||
const selectedReturnCartons = computed(() => returnCartonRows.value.filter((item) => item.status === 'AVAILABLE'))
|
||||
const returnOrderOptions = computed(() => [...new Set([
|
||||
...returnCandidates.value.flatMap(item => (item.returnNo || '').split(',')),
|
||||
@@ -79,7 +79,7 @@ const filteredReturnCartons = computed(() => selectedReturnCartons.value.filter(
|
||||
if (selectedReturnNo.value && !(item.sourceReturnNo || returnCandidateByMark.value[item.cartonMark]?.returnNo || '').split(',').includes(selectedReturnNo.value)) return false
|
||||
if (returnPickMode.value === 'SKU') {
|
||||
const detail = returnCartonDetails.value[item.cartonMark]
|
||||
if (!selectedReturnSkus.value.length || returnCandidateByMark.value[item.cartonMark]?.palletCode
|
||||
if (!selectedReturnSkus.value.length || item.sourceUnitType !== 'PARCEL' || !returnCandidateByMark.value[item.cartonMark]
|
||||
|| detail?.items?.length !== 1 || detail.items[0]?.skuCode !== selectedReturnSkus.value[0]) return false
|
||||
}
|
||||
const detailSkus = returnCartonDetails.value[item.cartonMark]?.items?.map((detail) => detail.skuCode).join(',') || ''
|
||||
@@ -200,8 +200,15 @@ async function submit() {
|
||||
await bindWarehouseTransferDestination({ transfers: selected.map(item => ({ customerId: item.customerId, transferNo: item.transferNo })), destinationType: destinationType.value, destinationWarehouseId: destination?.value, destinationWarehouseCode: destination?.code, destinationWarehouseName: destination?.name, privateDestinationAddress: deliveryAddress.value.trim() || undefined, privateDestinationPostalCode: postalCode.value.trim() || undefined, privateDestinationContactName: contactName.value.trim() || undefined, privateDestinationContactPhone: contactPhone.value.trim() || undefined, idempotencyKey: `web-transfer-destination-${Date.now()}` })
|
||||
ElMessage.success(`已为 ${selected.length} 个转运单绑定目的地,可进入智能排车`)
|
||||
} else if (outboundType.value === 'RETURN_OUTBOUND') {
|
||||
if (!recipient.value.trim() || !selectedReturnNo.value || !selectedReturnMarks.value.length) { ElMessage.warning('请填写收件信息并选择退货单和箱唛'); return }
|
||||
if (!recipient.value.trim()) { ElMessage.warning('请填写收件信息'); return }
|
||||
if (!selectedReturnNo.value) { ElMessage.warning('请选择退货单'); return }
|
||||
if (returnPickMode.value === 'SKU' && selectedReturnSkus.value.length !== 1) { ElMessage.warning('请选择一个 SKU'); return }
|
||||
if (!selectedReturnMarks.value.length) {
|
||||
ElMessage.warning(returnPickMode.value === 'SKU'
|
||||
? filteredReturnCartons.value.length ? '请在右侧选择该 SKU 的散件来源记录' : '该退货单没有此 SKU 可出库的散件库存;已装箱货物请切换箱/托出库'
|
||||
: '请选择出库箱号或托盘')
|
||||
return
|
||||
}
|
||||
await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, skuCode: returnPickMode.value === 'SKU' ? selectedReturnSkus.value[0] : undefined, unitType: returnPickMode.value === 'SKU' ? 'BOX' : returnUnitType.value, remark: remark.value.trim() || undefined, idempotencyKey: `web-return-${Date.now()}` })
|
||||
ElMessage.success('退货出库单和待派送单已创建,作业已推送 PDA')
|
||||
} else {
|
||||
@@ -329,7 +336,7 @@ onMounted(load)
|
||||
<section class="return-list-card">
|
||||
<div class="list-title"><span>SKU库存</span><small>按 SKU 查询</small></div>
|
||||
<div class="candidate-query"><el-input v-model="returnSkuQueryInput" clearable placeholder="如:SKU001,SKU002" /><el-button type="primary" @click="searchReturnSkus">查询</el-button></div>
|
||||
<el-table :data="pagedReturnStocks" border stripe class="return-result-table" max-height="400"><el-table-column v-if="returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" show-overflow-tooltip /><el-table-column prop="productNameZh" label="品名" min-width="150" show-overflow-tooltip /><el-table-column prop="availableQuantity" label="可用数量" width="100" align="right" /></el-table>
|
||||
<el-table :data="pagedReturnStocks" border stripe class="return-result-table" max-height="400"><el-table-column v-if="returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" show-overflow-tooltip /><el-table-column prop="productNameZh" label="品名" min-width="150" show-overflow-tooltip /><el-table-column :label="returnPickMode === 'SKU' ? '散件可用数量' : '可用数量'" width="110" align="right"><template #default="{ row }">{{ returnPickMode === 'SKU' ? row.looseAvailableQuantity : row.availableQuantity }}</template></el-table-column></el-table>
|
||||
<div class="return-pagination"><span>{{ returnPickMode === 'SKU' ? `已选择 ${selectedReturnSkus.length} 个 SKU` : 'SKU 库存' }}</span><el-pagination v-model:current-page="returnSkuPage" v-model:page-size="returnSkuPageSize" :page-sizes="[5, 10, 20, 50]" :total="filteredReturnStocks.length" layout="total, sizes, prev, pager, next" @size-change="returnSkuPage = 1" /></div>
|
||||
</section>
|
||||
<section class="return-list-card">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { reworkRelabelApplications } from './relabelRework'
|
||||
|
||||
describe('tail-order relabel rework candidates', () => {
|
||||
it('uses each return relabel application instead of the aggregate completed status', () => {
|
||||
const applications = [
|
||||
{ relabelNo: 'RL-1', status: 'CUSTOMER_CONFIRM_PENDING', quantity: 1, createdAt: '', statusLabel: '' },
|
||||
{ relabelNo: 'RL-2', status: 'COMPLETED', quantity: 1, createdAt: '', statusLabel: '' },
|
||||
{ relabelNo: 'RL-3', status: 'PROCESSING', quantity: 1, createdAt: '', statusLabel: '' },
|
||||
]
|
||||
expect(reworkRelabelApplications({ relabelOrderNo: '', relabelApplications: applications, relabelStatusCode: 'COMPLETED' }).map(item => item.relabelNo)).toEqual(['RL-1', 'RL-3'])
|
||||
})
|
||||
it('preserves the existing single-application entry and excludes confirmed orders', () => {
|
||||
expect(reworkRelabelApplications({ relabelOrderNo: 'RL-1', relabelStatusCode: 'CUSTOMER_CONFIRM_PENDING' })).toHaveLength(1)
|
||||
expect(reworkRelabelApplications({ relabelOrderNo: 'RL-1', relabelStatusCode: 'READY_FOR_OUTBOUND' })).toEqual([])
|
||||
expect(reworkRelabelApplications({ relabelOrderNo: '', relabelStatusCode: 'PROCESSING' })).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { LastMileOrderListItem } from './types'
|
||||
|
||||
export function reworkRelabelApplications(row: Pick<LastMileOrderListItem, 'relabelApplications' | 'relabelOrderNo' | 'relabelStatusCode'>) {
|
||||
const applications = row.relabelApplications?.length
|
||||
? row.relabelApplications
|
||||
: row.relabelOrderNo ? [{ relabelNo: row.relabelOrderNo, status: row.relabelStatusCode }] : []
|
||||
return applications.filter(application => ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(application.status))
|
||||
}
|
||||
Reference in New Issue
Block a user