优化
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
| 海外采购 | `src/modules/cross-border/routes.ts`、`views/ProcurementView.vue`、`components/ProcurementDetailPanel.vue`、`api/procurement.ts` | 独立列表、新建及详情路由;客户范围只取 Token,创建的 `customerName` 只取认证会话企业名称。七状态简化流程展示分批收货、分批出库与费用,待客户确认时提供当前客户确认动作;不开放内部文件或平台操作。Backend `lastleg-service/procurement/`;规则见 `baiyue-firstleg/lastleg-service/docs/procurement-workflow.md`(工作区路径) |
|
||||
| 美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 两个运输业务共用列表/表单组件,客户范围只取 Token;不消费采购字段或状态。Backend `lastleg-service/` 的 `trucking/`、`looseboard/` 客户接口 |
|
||||
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
|
||||
| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
|
||||
| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层提供统一贴标明细模板,使用“贴标类型”列区分只换箱唛、只换 SKU 标签和 SKU+箱唛三种模式;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,同一份文件不得混用多种类型,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
|
||||
|
||||
## Client 全量验收快速索引
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ test('truck requires both contacts vehicle and minute times without inheriting l
|
||||
truck.form.pickupAt = '2026-10-20T09:30:00'
|
||||
let failure
|
||||
truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error })
|
||||
assert.equal(failure.message, 'C点送达时间不能早于B点取货时间')
|
||||
assert.equal(failure.message, '送达时间不能早于取货时间')
|
||||
})
|
||||
|
||||
test('truck retry submits the complete new contract with identical local times and idempotency key', async () => {
|
||||
@@ -176,8 +176,8 @@ test('truck details render saved minute times and unknown pool demand without ti
|
||||
current.detail.value = { summary: { vehicleType: null, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null }
|
||||
const fields = new Map(current.detailFields.value)
|
||||
assert.equal(fields.get('车辆类型'), '—')
|
||||
assert.equal(fields.get('B点取货时间'), '2026-10-20 09:30')
|
||||
assert.equal(fields.get('C点送达时间'), '—')
|
||||
assert.equal(fields.get('取货时间'), '2026-10-20 09:30')
|
||||
assert.equal(fields.get('送达时间'), '—')
|
||||
assert.equal(fields.has('柜型'), false)
|
||||
assert.equal(fields.has('托盘数'), false)
|
||||
})
|
||||
|
||||
@@ -50,10 +50,10 @@ const configuration = computed(() => ({
|
||||
})[props.kind])
|
||||
|
||||
function transportColumns(looseboard: boolean): ColumnDefinition[] {
|
||||
const originLabel = looseboard ? '起运地' : 'B点取货地'
|
||||
const destinationLabel = looseboard ? '目的地' : 'C点送达地'
|
||||
const pickupLabel = looseboard ? '取货时间' : 'B点取货时间'
|
||||
const deliveryLabel = looseboard ? '送货时间' : 'C点送达时间'
|
||||
const originLabel = looseboard ? '起运地' : '取货地'
|
||||
const destinationLabel = looseboard ? '目的地' : '送达地'
|
||||
const pickupLabel = looseboard ? '取货时间' : '取货时间'
|
||||
const deliveryLabel = looseboard ? '送货时间' : '送达时间'
|
||||
return [
|
||||
{ key: 'orderNo', label: '订单编号', width: 180 },
|
||||
...(!looseboard ? [{ key: 'vehicleType', label: '车辆类型', width: 110 }] : []),
|
||||
@@ -127,13 +127,13 @@ const detailFields = computed(() => {
|
||||
...(props.kind === 'looseboard' ? [
|
||||
['商品名称', summary.productName], ['预计托数', summary.estimatedPalletCount],
|
||||
] : [['车辆类型', vehicleTypeLabel(summary.vehicleType)]]),
|
||||
[props.kind === 'trucking' ? 'B点取货地' : '起运地', read(summary, 'origin')], [props.kind === 'trucking' ? 'B点取货地址' : '起运地址', current.originAddress],
|
||||
[props.kind === 'trucking' ? 'C点送达地' : '目的地', read(summary, 'destination')], [props.kind === 'trucking' ? 'C点送达地址' : '目的地址', current.destinationAddress],
|
||||
[props.kind === 'trucking' ? '取货地' : '起运地', read(summary, 'origin')], [props.kind === 'trucking' ? '取货地址' : '起运地址', current.originAddress],
|
||||
[props.kind === 'trucking' ? '送达地' : '目的地', read(summary, 'destination')], [props.kind === 'trucking' ? '送达地址' : '目的地址', current.destinationAddress],
|
||||
['件数', read(summary, 'packageCount')], ['总重量(kg)', read(summary, 'totalWeightKg')],
|
||||
['总体积(m³)', read(summary, 'totalVolumeCbm')], ['预算总价', money(read(summary, 'budgetAmount'), read(summary, 'currency'))],
|
||||
[props.kind === 'trucking' ? 'B点联系人' : '起运地联系人', summary.originContactName], [props.kind === 'trucking' ? 'B点联系电话' : '起运地联系电话', summary.originContactPhone],
|
||||
[props.kind === 'trucking' ? 'C点联系人' : '目的地联系人', summary.destinationContactName], [props.kind === 'trucking' ? 'C点联系电话' : '目的地联系电话', summary.destinationContactPhone],
|
||||
[props.kind === 'trucking' ? 'B点取货时间' : '取货时间', localMinute(summary.pickupAt)], [props.kind === 'trucking' ? 'C点送达时间' : '送货时间', localMinute(summary.deliveryAt)], ['备注', current.remark],
|
||||
[props.kind === 'trucking' ? '取货联系人' : '起运地联系人', summary.originContactName], [props.kind === 'trucking' ? '取货联系电话' : '起运地联系电话', summary.originContactPhone],
|
||||
[props.kind === 'trucking' ? '送达联系人' : '目的地联系人', summary.destinationContactName], [props.kind === 'trucking' ? '送达联系电话' : '目的地联系电话', summary.destinationContactPhone],
|
||||
[props.kind === 'trucking' ? '取货时间' : '取货时间', localMinute(summary.pickupAt)], [props.kind === 'trucking' ? '送达时间' : '送货时间', localMinute(summary.deliveryAt)], ['备注', current.remark],
|
||||
]
|
||||
})
|
||||
|
||||
@@ -216,7 +216,7 @@ function getRowActions(candidate: object): TableAction[] {
|
||||
}
|
||||
|
||||
function detailFieldClass(label: unknown) {
|
||||
return ['备注', props.kind === 'trucking' ? 'B点取货地址' : '起运地址', props.kind === 'trucking' ? 'C点送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : ''
|
||||
return ['备注', props.kind === 'trucking' ? '取货地址' : '起运地址', props.kind === 'trucking' ? '送达地址' : '目的地址'].includes(String(label)) ? 'is-wide' : ''
|
||||
}
|
||||
|
||||
const listRouteName = computed(() => ({
|
||||
|
||||
@@ -29,14 +29,14 @@ defineExpose({
|
||||
<el-form-item v-if="kind === 'truck'" class="transport-demand-wide" label="车辆类型" prop="vehicleType">
|
||||
<el-select v-model="model.vehicleType" placeholder=""><el-option label="15拖" value="PALLET_15" /><el-option label="30拖" value="PALLET_30" /></el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'B点取货地' : '起运地'" prop="origin"><el-input v-model.trim="model.origin" maxlength="255" placeholder="城市 / 港口 / 州" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'C点送达地' : '目的地'" prop="destination"><el-input v-model.trim="model.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'B点联系人' : '起运地联系人'" prop="originContactName"><el-input v-model.trim="model.originContactName" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'C点联系人' : '目的地联系人'" prop="destinationContactName"><el-input v-model.trim="model.destinationContactName" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'B点联系电话' : '起运地联系电话'" prop="originContactPhone"><el-input v-model.trim="model.originContactPhone" type="tel" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'C点联系电话' : '目的地联系电话'" prop="destinationContactPhone"><el-input v-model.trim="model.destinationContactPhone" type="tel" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'B点取货地址' : '起运详细地址'"><el-input v-model.trim="model.originAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'C点送达地址' : '目的详细地址'"><el-input v-model.trim="model.destinationAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '取货地' : '起运地'" prop="origin"><el-input v-model.trim="model.origin" maxlength="255" placeholder="城市 / 港口 / 州" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '送达地' : '目的地'" prop="destination"><el-input v-model.trim="model.destination" maxlength="255" placeholder="城市 / 州" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '取货联系人' : '起运地联系人'" prop="originContactName"><el-input v-model.trim="model.originContactName" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '送达联系人' : '目的地联系人'" prop="destinationContactName"><el-input v-model.trim="model.destinationContactName" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '取货联系电话' : '起运地联系电话'" prop="originContactPhone"><el-input v-model.trim="model.originContactPhone" type="tel" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '送达联系电话' : '目的地联系电话'" prop="destinationContactPhone"><el-input v-model.trim="model.destinationContactPhone" type="tel" maxlength="128" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '取货地址' : '起运详细地址'"><el-input v-model.trim="model.originAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '送达地址' : '目的详细地址'"><el-input v-model.trim="model.destinationAddress" maxlength="500" placeholder="街道 / 门牌等补充信息(选填)" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
<section class="transport-demand-section">
|
||||
@@ -58,8 +58,8 @@ defineExpose({
|
||||
<el-select v-model="model.currency" aria-label="预算币种" placeholder=""><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /></el-select>
|
||||
</div></el-form-item>
|
||||
<div />
|
||||
<el-form-item :label="kind === 'truck' ? 'B点取货时间' : '取货时间'" prop="pickupAt"><el-date-picker v-model="model.pickupAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" @change="formRef?.validateField('deliveryAt').catch(() => undefined)" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? 'C点送达时间' : '送货时间'" prop="deliveryAt"><el-date-picker v-model="model.deliveryAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '取货时间' : '取货时间'" prop="pickupAt"><el-date-picker v-model="model.pickupAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" @change="formRef?.validateField('deliveryAt').catch(() => undefined)" /></el-form-item>
|
||||
<el-form-item :label="kind === 'truck' ? '送达时间' : '送货时间'" prop="deliveryAt"><el-date-picker v-model="model.deliveryAt" type="datetime" format="YYYY-MM-DD HH:mm" time-format="HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="" /></el-form-item>
|
||||
<el-form-item class="transport-demand-wide" label="备注"><el-input v-model="model.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="" /></el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -33,12 +33,12 @@ export function createTransportDemandForm(): TransportDemandForm {
|
||||
}
|
||||
|
||||
const requiredText = (kind: TransportDemandKind) => ({
|
||||
origin: kind === 'truck' ? 'B点取货地' : '起运地',
|
||||
destination: kind === 'truck' ? 'C点送达地' : '目的地',
|
||||
originContactName: kind === 'truck' ? 'B点联系人' : '起运地联系人',
|
||||
originContactPhone: kind === 'truck' ? 'B点联系电话' : '起运地联系电话',
|
||||
destinationContactName: kind === 'truck' ? 'C点联系人' : '目的地联系人',
|
||||
destinationContactPhone: kind === 'truck' ? 'C点联系电话' : '目的地联系电话',
|
||||
origin: kind === 'truck' ? '取货地' : '起运地',
|
||||
destination: kind === 'truck' ? '送达地' : '目的地',
|
||||
originContactName: kind === 'truck' ? '取货联系人' : '起运地联系人',
|
||||
originContactPhone: kind === 'truck' ? '取货联系电话' : '起运地联系电话',
|
||||
destinationContactName: kind === 'truck' ? '送达联系人' : '目的地联系人',
|
||||
destinationContactPhone: kind === 'truck' ? '送达联系电话' : '目的地联系电话',
|
||||
} as const)
|
||||
|
||||
export function localMinuteInput(value: unknown) {
|
||||
@@ -57,7 +57,7 @@ export function transportDemandRules(kind: TransportDemandKind, form: TransportD
|
||||
rules[field] = [{ required: true, type: 'number', min: 0, message: `${label}须为不小于 0 的数字`, trigger: 'change' }]
|
||||
}
|
||||
rules.budgetAmount = [{ type: 'number', min: 0, message: '预算总价不能小于 0', trigger: 'change' }]
|
||||
const pickupLabel = kind === 'truck' ? 'B点取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? 'C点送达时间' : '送货时间'
|
||||
const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间'
|
||||
rules.pickupAt = [{ required: true, message: `请选择${pickupLabel}`, trigger: 'change' }]
|
||||
rules.deliveryAt = [{ required: true, message: `请选择${deliveryLabel}`, trigger: 'change' }, {
|
||||
validator: (_rule, value, callback) => callback(value && form.pickupAt && localMinuteInput(value) < localMinuteInput(form.pickupAt) ? new Error(`${deliveryLabel}不能早于${pickupLabel}`) : undefined), trigger: 'change',
|
||||
@@ -71,7 +71,7 @@ export function transportDemandError(kind: TransportDemandKind, form: TransportD
|
||||
for (const [field, label] of Object.entries(labels)) {
|
||||
if (!String(form[field as keyof typeof labels] || '').trim()) return `请输入${label}`
|
||||
}
|
||||
const pickupLabel = kind === 'truck' ? 'B点取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? 'C点送达时间' : '送货时间'
|
||||
const pickupLabel = kind === 'truck' ? '取货时间' : '取货时间', deliveryLabel = kind === 'truck' ? '送达时间' : '送货时间'
|
||||
for (const [field, label] of [['pickupAt', pickupLabel], ['deliveryAt', deliveryLabel]] as const) {
|
||||
if (!form[field]) return `请选择${label}`
|
||||
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/.test(form[field])) return `请选择有效的${label}`
|
||||
|
||||
@@ -428,9 +428,7 @@ function relabelOptionLabel(option: typeof relabelInboundOptions.value[number])
|
||||
}
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const cartonTemplateInput = ref<HTMLInputElement | null>(null)
|
||||
const skuTemplateInput = ref<HTMLInputElement | null>(null)
|
||||
const mixedTemplateInput = ref<HTMLInputElement | null>(null)
|
||||
const unifiedTemplateInput = ref<HTMLInputElement | null>(null)
|
||||
const transferCartonMarkInput = ref<HTMLInputElement | null>(null)
|
||||
const relabelFileInput = ref<HTMLInputElement | null>(null)
|
||||
const relabelFileScope = ref<'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM'>('CARTON_TOTAL')
|
||||
@@ -827,110 +825,62 @@ async function importTransferCartonMarks(event: Event) {
|
||||
for (const row of cargoRows.value) row.customerCartonMarks = imported.filter((item) => item.SKU.trim().toUpperCase() === row.sku.trim().toUpperCase()).map((item) => item['客户箱唛号'].trim()).join('\n')
|
||||
ElMessage.success(`已导入 ${imported.length} 个客户箱唛`)
|
||||
}
|
||||
function downloadRelabelTemplate(templateType: RelabelTemplateType) {
|
||||
function relabelTemplateTypeForMode(mode: LabelingMode): RelabelTemplateType {
|
||||
return mode === 'carton' ? 'carton' : mode === 'sku' ? 'sku' : 'mixed'
|
||||
}
|
||||
function relabelTemplateTypeLabel(type: RelabelTemplateType) {
|
||||
return type === 'carton' ? '只换箱唛' : type === 'sku' ? '只换 SKU 标签' : 'SKU 和箱唛都换'
|
||||
}
|
||||
function parseRelabelTemplateType(value: string): RelabelTemplateType | null {
|
||||
const normalized = value.trim().toUpperCase().replaceAll(' ', '')
|
||||
if (['只换箱唛', '箱号关联', '箱唛换标', 'CARTON', 'CARTON_LABEL'].includes(normalized)) return 'carton'
|
||||
if (['只换SKU标签', 'SKU关联', 'SKU换标', 'SKU', 'SKU_LABEL'].includes(normalized)) return 'sku'
|
||||
if (['SKU和箱唛都换', 'SKU混箱', 'SKU混箱换标', '混箱', 'MIXED', 'CARTON_AND_SKU'].includes(normalized)) return 'mixed'
|
||||
return null
|
||||
}
|
||||
function downloadRelabelTemplate() {
|
||||
if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
|
||||
const cartonTemplate = templateType === 'carton'
|
||||
const mixedTemplate = templateType === 'mixed'
|
||||
const includeDestination = false
|
||||
const templateType = relabelTemplateTypeForMode(labelingMode.value)
|
||||
const cartonMarkHeader = props.service === 'transfer' ? '客户箱唛号' : '仓库唛头'
|
||||
const header = cartonTemplate
|
||||
? ['明细行标识', cartonMarkHeader, '新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号', '备注']
|
||||
: ['明细行标识', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)',
|
||||
...(mixedTemplate ? ['新FBA箱唛', ...(includeDestination ? ['FBA目的仓'] : []), 'FBA货件编号'] : []), '备注']
|
||||
const rows = cargoRows.value.map((item) => cartonTemplate
|
||||
? [item.id, item.cartonMark, item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo, item.remark]
|
||||
: [item.id, item.cartonMark, item.sku, '', 1,
|
||||
...(mixedTemplate ? [item.fbaCartonMark, ...(includeDestination ? [item.destinationWarehouseId] : []), item.shipmentNo] : []), item.remark])
|
||||
const header = ['明细行标识', '贴标类型', cartonMarkHeader, '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注']
|
||||
const rows = cargoRows.value.map((item) => [item.id, relabelTemplateTypeLabel(templateType), props.service === 'transfer' ? item.customerCartonMarks : item.cartonMark, item.sku, '', 1, '', item.shipmentNo, item.remark])
|
||||
const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = cartonTemplate ? '贴标申请-箱号关联模板.csv'
|
||||
: mixedTemplate ? '贴标申请-SKU混箱换标模板.csv' : '贴标申请-SKU关联模板.csv'
|
||||
anchor.click(); URL.revokeObjectURL(url)
|
||||
const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = '贴标申请-统一明细关联模板.csv'; anchor.click(); URL.revokeObjectURL(url)
|
||||
}
|
||||
function parseCsvLine(line: string) {
|
||||
const values: string[] = []; let value = ''; let quoted = false
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index]
|
||||
if (char === '"') { if (quoted && line[index + 1] === '"') { value += '"'; index += 1 } else quoted = !quoted }
|
||||
else if (char === ',' && !quoted) { values.push(value.trim()); value = '' }
|
||||
else value += char
|
||||
}
|
||||
for (let index = 0; index < line.length; index += 1) { const char = line[index]; if (char === '"') { if (quoted && line[index + 1] === '"') { value += '"'; index += 1 } else quoted = !quoted } else if (char === ',' && !quoted) { values.push(value.trim()); value = '' } else value += char }
|
||||
values.push(value.trim()); return values
|
||||
}
|
||||
async function importRelabelTemplate(event: Event, templateType: RelabelTemplateType) {
|
||||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''
|
||||
if (!file) return
|
||||
async function importRelabelTemplate(event: Event) {
|
||||
const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = ''; if (!file) return
|
||||
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
|
||||
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
|
||||
// Accept both the current display name and legacy template header while keeping the internal key stable.
|
||||
const headers = parseCsvLine(lines[0]).map((name) => name === '仓库唛头' || name === '客户箱唛号' || name === '旧唛头' ? '旧箱唛' : name)
|
||||
if (templateType !== 'carton') {
|
||||
const mixedTemplate = templateType === 'mixed'
|
||||
const required = ['明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)',
|
||||
...(mixedTemplate ? ['新FBA箱唛', 'FBA货件编号'] : [])]
|
||||
if (required.some((name) => !headers.includes(name))) {
|
||||
ElMessage.error('SKU 关联模板格式不正确,请重新下载 SKU 关联模板'); return
|
||||
}
|
||||
const imported = lines.slice(1).map(parseCsvLine)
|
||||
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
|
||||
.filter((item) => item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛'])
|
||||
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
|
||||
ElMessage.error('仓库唛头、旧 SKU 标签、新 SKU 标签和换标数量必须填写完整'); return
|
||||
}
|
||||
const rows: ImportedRelabelBinding[] = []
|
||||
for (const item of imported) {
|
||||
const quantity = Number(item['换标数量(件)'])
|
||||
if (!Number.isInteger(quantity) || quantity < 1) {
|
||||
ElMessage.error(`仓库唛头 ${item['旧箱唛']} 的换标数量必须是大于 0 的整数`); return
|
||||
}
|
||||
const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识'])
|
||||
if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return }
|
||||
rows.push({
|
||||
id: ++skuRelabelSequence,
|
||||
sourceRowId: source.id,
|
||||
cartonMark: item['旧箱唛'].trim(),
|
||||
originalSku: item['旧SKU标签'].trim(),
|
||||
newSku: item['新SKU标签'].trim(),
|
||||
quantity,
|
||||
fbaCartonMark: mixedTemplate ? item['新FBA箱唛'].trim() : '',
|
||||
destinationWarehouseId: form.destination,
|
||||
remark: item['备注'] || '',
|
||||
templateType,
|
||||
selected: true,
|
||||
shipmentNo: mixedTemplate ? item['FBA货件编号'].trim() : '',
|
||||
})
|
||||
}
|
||||
importedRelabelBindings.value = rows
|
||||
ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`)
|
||||
return
|
||||
const requiredHeaders = ['明细行标识', '贴标类型', '旧箱唛', '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注']
|
||||
if (requiredHeaders.some((name) => !headers.includes(name))) { ElMessage.error('统一明细关联模板格式不正确,请重新下载模板'); return }
|
||||
const imported = lines.slice(1).map(parseCsvLine).map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))).filter((item) => item['明细行标识'] || item['贴标类型'] || item['旧箱唛'] || item['新SKU标签'] || item['新FBA箱唛'])
|
||||
if (!imported.length) { ElMessage.error('模板中没有可导入的明细'); return }
|
||||
const rows: ImportedRelabelBinding[] = []; let detectedType: RelabelTemplateType | null = null
|
||||
for (const item of imported) {
|
||||
const templateType = parseRelabelTemplateType(item['贴标类型']); if (!templateType) { ElMessage.error(`明细行 ${item['明细行标识']} 的贴标类型无效`); return }
|
||||
if (detectedType && detectedType !== templateType) { ElMessage.error('同一份明细文件只能包含一种贴标类型,请拆分后分别提交换标单'); return }
|
||||
detectedType = templateType
|
||||
const source = cargoRows.value.find((row) => String(row.id) === item['明细行标识']); if (!source) { ElMessage.error(`明细行 ${item['明细行标识']} 不属于当前库存,请重新下载模板`); return }
|
||||
const quantity = Number(item['换标数量(件)']); const requiresSku = templateType !== 'carton'; const requiresCarton = templateType !== 'sku'
|
||||
if (!item['旧箱唛'] || (requiresSku && (!item['旧SKU标签'] || !item['新SKU标签'])) || !Number.isInteger(quantity) || quantity < 1 || (requiresCarton && (!item['新FBA箱唛'] || !item['FBA货件编号']))) { ElMessage.error(`明细行 ${item['明细行标识']} 未按“${relabelTemplateTypeLabel(templateType)}”填写完整`); return }
|
||||
rows.push({ id: ++skuRelabelSequence, sourceRowId: source.id, cartonMark: item['旧箱唛'].trim(), originalSku: (item['旧SKU标签'] || source.sku).trim(), newSku: item['新SKU标签'].trim(), quantity: templateType === 'carton' ? 1 : quantity, fbaCartonMark: item['新FBA箱唛'].trim(), destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType, selected: true, shipmentNo: item['FBA货件编号'].trim() })
|
||||
}
|
||||
const required = ['明细行标识', '旧箱唛', '新FBA箱唛', 'FBA货件编号']
|
||||
if (required.some((name) => !headers.includes(name))) { ElMessage.error('箱号关联模板格式不正确,请重新下载箱号关联模板'); return }
|
||||
const imported = lines.slice(1).map(parseCsvLine)
|
||||
.map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
|
||||
.filter((item) => item['旧箱唛'] || item['新FBA箱唛'])
|
||||
if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
|
||||
ElMessage.error('模板中的明细行、仓库唛头、新 FBA 箱唛和货件编号必须填写完整'); return
|
||||
const keys = rows.map((row) => detectedType === 'carton' ? String(row.sourceRowId) : `${row.cartonMark.toUpperCase()}|${row.originalSku.toUpperCase()}`)
|
||||
if (new Set(keys).size !== keys.length) { ElMessage.error('存在重复箱明细或重复的箱内 SKU,请合并后重新上传'); return }
|
||||
const cartonTargets = new Map<string, string>()
|
||||
for (const row of rows) {
|
||||
const key = row.cartonMark.toUpperCase()
|
||||
const target = `${row.fbaCartonMark}|${row.shipmentNo}`
|
||||
if (detectedType === 'mixed' && cartonTargets.has(key) && cartonTargets.get(key) !== target) { ElMessage.error('同一箱内的 SKU 必须填写相同的新 FBA 箱唛和货件编号'); return }
|
||||
cartonTargets.set(key, target)
|
||||
}
|
||||
if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) {
|
||||
ElMessage.error('模板中的明细行标识不能重复'); return
|
||||
}
|
||||
const importedByRow = new Map(imported.map((item) => [item['明细行标识'], item]))
|
||||
if (importedByRow.size !== imported.length || [...importedByRow.keys()].some((id) => !cargoRows.value.some((row) => String(row.id) === id))) {
|
||||
ElMessage.error('模板存在重复或不属于当前库存的明细行,请重新下载模板'); return
|
||||
}
|
||||
const bindings: ImportedRelabelBinding[] = []
|
||||
for (const [rowId, item] of importedByRow) {
|
||||
const row = cargoRows.value.find((candidate) => String(candidate.id) === rowId)!
|
||||
bindings.push({ id: ++skuRelabelSequence, sourceRowId: row.id, cartonMark: item['旧箱唛'],
|
||||
originalSku: row.sku, newSku: '', quantity: 1, fbaCartonMark: item['新FBA箱唛'],
|
||||
destinationWarehouseId: form.destination, remark: item['备注'] || '', templateType,
|
||||
selected: true, shipmentNo: item['FBA货件编号'] })
|
||||
}
|
||||
importedRelabelBindings.value = bindings
|
||||
ElMessage.success(`已读取 ${bindings.length} 条箱号关联,请选择后导入到明细`)
|
||||
importedRelabelBindings.value = rows; ElMessage.success(`已读取 ${rows.length} 条绑定关系,请选择后导入到明细`)
|
||||
}
|
||||
function selectAllImportedBindings(value: boolean) {
|
||||
for (const item of importedRelabelBindings.value) item.selected = value
|
||||
@@ -1795,13 +1745,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<section class="warehouse-file-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>2</span>明细关联模板</strong></div><em>按需使用</em></div>
|
||||
<div class="warehouse-template-list">
|
||||
<div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>统一贴标明细</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate()"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="unifiedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">␍ <div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||||
<input ref="unifiedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">␍ <div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||||
<el-input v-model="returnCartonQuery" clearable placeholder="" />␍ <template v-if="returnLooseCandidates.length">
|
||||
<div class="warehouse-section-heading"><h3>可用包裹</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableSkusCollapsed" aria-controls="return-available-skus" @click="availableSkusCollapsed = !availableSkusCollapsed">{{ availableSkusCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
|
||||
<table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</th><th>SKU 换标结果</th><th>剩余可换(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ source.cartonMark }}</td><td>{{ source.inboundOrderNo }}</td><td>{{ sku.skuCode }}</td><td><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="labelingMode === 'sku' ? addReturnSkuBinding(source, sku.skuCode) : addReturnBoxBinding(source)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="6">暂无可用包裹库存</td></tr></tbody></table>
|
||||
@@ -1871,8 +1819,8 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
</section>
|
||||
<section class="warehouse-file-group warehouse-transfer-template-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class="warehouse-template-list"><div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class="warehouse-template-list"><div><span><strong>统一贴标明细</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate()"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="unifiedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||||
<input ref="unifiedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate">
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
|
||||
|
||||
@@ -57,19 +57,16 @@ test('客户贴标表单不展示客户单号、托盘和内部库存来源列',
|
||||
assert.doesNotMatch(view, /已选托盘组成/)
|
||||
})
|
||||
|
||||
test('贴标申请保留关联模板下载和上传入口', () => {
|
||||
test('贴标申请使用统一明细模板下载和上传入口', () => {
|
||||
assert.match(view, /downloadRelabelTemplate/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, /<strong>统一贴标明细<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /skuTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /cartonTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /unifiedTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /importRelabelTemplate/)
|
||||
assert.match(view, /明细行标识/)
|
||||
assert.match(view, /'明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签'/)
|
||||
assert.match(view, /const includeDestination = false/)
|
||||
assert.match(view, /'明细行标识', '贴标类型'/)
|
||||
assert.doesNotMatch(view.slice(view.indexOf('function downloadRelabelTemplate'), view.indexOf('function parseCsvLine')), /FBA目的仓/)
|
||||
assert.doesNotMatch(view, /const includeDestination = props\.service !== 'transfer'/)
|
||||
assert.match(view, /<strong>绑定明细<\/strong>/)
|
||||
assert.match(view, /<th>仓库唛头 \*<\/th>/)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
|
||||
const csvCell = source.slice(source.indexOf('function csvCell('), source.indexOf('function transferCustomerMarks'))
|
||||
const buildItems = source.slice(source.indexOf('function buildSkuRelabelItems('), source.indexOf('function addRelabelCargoRow('))
|
||||
const submitStart = source.indexOf('const sourceInboundOrders =', source.indexOf('async function submit('))
|
||||
const submitBody = source.slice(submitStart, source.indexOf(' try {', submitStart))
|
||||
const code = ts.transpileModule(csvCell + buildItems + source.slice(source.indexOf('function relabelTemplateTypeForMode'), source.indexOf('function selectDiscardInbound')) + '\nasync function submitImported() { const idempotencyKey = "test-key"; ' + submitBody + '\nreturn created; }', { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
function harness(service = 'labeling') {
|
||||
const state = { props: { service }, cargoRows: { value: [{ id: 1, sku: 'OLD', name: '货物', included: false, cartonMark: 'BOX-1', customerCartonMarks: 'CUSTOMER-1', shipmentNo: '', remark: '' }] }, labelingMode: { value: 'carton' }, importedRelabelBindings: { value: [] }, skuRelabelRows: { value: [] }, transferRelabelRows: { value: [] }, form: { destination: 'ONT8' }, ElMessage: { error: message => errors.push(message), warning: message => errors.push(message), success: () => {} }, createCargoRow: () => ({}), transferCustomerMarks: row => row.customerCartonMarks.split(',') }
|
||||
const requests = []
|
||||
Object.assign(state, { isReturnRelabelSource: { value: false }, selectedReturnCandidates: { value: [] }, cartonDetailRows: { get value() { return state.cargoRows.value.filter(row => row.included) } }, createRelabel: async request => { requests.push(request); return { application: { relabelNo: 'TEST-RELABEL' } } } })
|
||||
Object.assign(state.cargoRows.value[0], { availableQuantity: 1, sourceInventoryBalanceId: '101', sourceInventoryPositionId: '102', sourceInboundOrderId: '103' })
|
||||
const errors = [], downloads = []
|
||||
const URL = { createObjectURL: blob => { downloads.push(blob); return 'blob:test' }, revokeObjectURL: () => {} }
|
||||
const document = { createElement: () => ({ click() {} }) }
|
||||
const result = new Function(...Object.keys(state), 'URL', 'document', 'let skuRelabelSequence = 0;\n' + code + '\nreturn { downloadRelabelTemplate, importRelabelTemplate, importSelectedBindingsToDetails, buildSkuRelabelItems, submitImported };')(...Object.values(state), URL, document)
|
||||
async function upload(rows) {
|
||||
const csv = [['明细行标识', '贴标类型', '仓库唛头', '旧SKU标签', '新SKU标签', '换标数量(件)', '新FBA箱唛', 'FBA货件编号', '备注'], ...rows].map(row => row.join(',')).join('\r\n')
|
||||
await result.importRelabelTemplate({ target: { files: [new File([csv], '统一模板.csv')], value: 'file' } })
|
||||
}
|
||||
return { ...state, ...result, upload, errors, downloads, requests }
|
||||
}
|
||||
|
||||
for (const [type, mode, expected] of [['只换箱唛', 'carton', 'carton'], ['只换 SKU 标签', 'sku', 'sku'], ['SKU 和箱唛都换', 'carton_and_sku', 'mixed']]) {
|
||||
test(`${type}:统一模板导入并生成对应类型明细`, async () => {
|
||||
const state = harness()
|
||||
await state.upload([[1, type, 'BOX-1', 'OLD', mode === 'carton' ? '' : 'NEW', 3, mode === 'sku' ? '' : 'FBA-1', mode === 'sku' ? '' : 'SHIP-1', '']])
|
||||
assert.deepEqual(state.errors, [])
|
||||
assert.equal(state.importedRelabelBindings.value[0].templateType, expected)
|
||||
state.importSelectedBindingsToDetails()
|
||||
assert.equal(state.labelingMode.value, mode)
|
||||
if (mode === 'carton') { assert.equal(state.cargoRows.value[0].included, true); assert.equal(state.cargoRows.value[0].quantity, 1) }
|
||||
else {
|
||||
assert.equal(state.skuRelabelRows.value[0].newSku, 'NEW')
|
||||
const [item] = state.buildSkuRelabelItems()
|
||||
assert.equal(item.operationType, mode === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU')
|
||||
assert.equal(item.totalPackageCount, 1)
|
||||
assert.equal(item.skuItems[0].quantity, 3)
|
||||
assert.equal(item.destinationFbaWarehouseCode, mode === 'sku' ? undefined : 'ONT8')
|
||||
}
|
||||
state.form.deliveryMethod = 'TRUCK'
|
||||
await state.submitImported()
|
||||
assert.equal(state.requests.length, 1)
|
||||
assert.equal(state.requests[0].items[0].operationType, mode === 'carton' ? 'CARTON_LABEL' : mode === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU')
|
||||
assert.equal(state.requests[0].deliveryMethod, mode === 'sku' ? undefined : 'TRUCK')
|
||||
})
|
||||
}
|
||||
|
||||
test('三种类型下载同一列结构,并带出当前换标类型', async () => {
|
||||
const state = harness()
|
||||
const headers = []
|
||||
for (const mode of ['carton', 'sku', 'carton_and_sku']) {
|
||||
state.labelingMode.value = mode
|
||||
state.downloadRelabelTemplate()
|
||||
const csv = await state.downloads.at(-1).text()
|
||||
const lines = csv.split('\r\n')
|
||||
assert.equal(lines.length, 3)
|
||||
headers.push(lines[0])
|
||||
assert.match(lines[1], /只换箱唛|只换 SKU 标签|SKU 和箱唛都换/)
|
||||
}
|
||||
assert.equal(new Set(headers).size, 1)
|
||||
})
|
||||
|
||||
test('组合换标同箱多 SKU 生成一条箱任务并保留全部 SKU 子项', async () => {
|
||||
const state = harness()
|
||||
await state.upload([[1, 'SKU 和箱唛都换', 'BOX-1', 'OLD', 'NEW', 3, 'FBA-1', 'SHIP-1', ''], [1, 'SKU 和箱唛都换', 'BOX-1', 'OLD2', 'NEW2', 2, 'FBA-1', 'SHIP-1', '']])
|
||||
assert.deepEqual(state.errors, [])
|
||||
state.importSelectedBindingsToDetails()
|
||||
await state.submitImported()
|
||||
assert.equal(state.requests[0].items.length, 1)
|
||||
assert.equal(state.requests[0].items[0].totalPackageCount, 1)
|
||||
assert.deepEqual(state.requests[0].items[0].skuItems.map(item => item.quantity), [3, 2])
|
||||
})
|
||||
|
||||
test('混合类型、缺字段、无效数量、重复关系和未知库存均不导入', async () => {
|
||||
const valid = [1, 'SKU 和箱唛都换', 'BOX-1', 'OLD', 'NEW', 3, 'FBA-1', 'SHIP-1', '']
|
||||
for (const rows of [[valid, [1, '只换箱唛', 'BOX-2', '', '', 1, 'FBA-2', 'SHIP-2', '']], [[...valid.slice(0, 4), '', ...valid.slice(5)]], [[...valid.slice(0, 5), 0, ...valid.slice(6)]], [valid, valid], [[999, ...valid.slice(1)]], [valid, [1, 'SKU 和箱唛都换', 'BOX-1', 'OTHER', 'NEW2', 1, 'FBA-OTHER', 'SHIP-1', '']]]) {
|
||||
const state = harness()
|
||||
await state.upload(rows)
|
||||
assert.equal(state.errors.length, 1)
|
||||
assert.equal(state.importedRelabelBindings.value.length, 0)
|
||||
}
|
||||
})
|
||||
@@ -69,7 +69,7 @@ test('仓储转运换标整票只允许一个目的仓', () => {
|
||||
assert.match(view, /props\.service === 'transfer' \? '客户箱唛号' : '仓库唛头'/)
|
||||
assert.match(view, /已导入 \$\{importedRows\.length\} 条仓储转运换标明细/)
|
||||
assert.doesNotMatch(transferRelabelSection, /新 FBA 箱唛<\/th><th>FBA 目的仓<\/th>/)
|
||||
assert.match(view, /const includeDestination = false/)
|
||||
assert.doesNotMatch(view.slice(view.indexOf('function downloadRelabelTemplate'), view.indexOf('function parseCsvLine')), /FBA目的仓/)
|
||||
})
|
||||
|
||||
test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换标时填写客户箱唛', () => {
|
||||
@@ -81,9 +81,7 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.match(view, /仓库唛头 \*/)
|
||||
assert.match(view, /downloadTransferCartonMarkTemplate/)
|
||||
assert.match(view, /importTransferCartonMarks/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, /<strong>统一贴标明细<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /换标类型/)
|
||||
|
||||
Reference in New Issue
Block a user