feat: model mixed-SKU dropship cartons
This commit is contained in:
@@ -113,6 +113,7 @@ export interface FirstLegOrderCargoPayload {
|
||||
shipmentId?: string
|
||||
referenceId?: string
|
||||
productImageFileId?: number | string
|
||||
packingGroupNo?: string
|
||||
}
|
||||
|
||||
export interface ProductImageUploadResult {
|
||||
|
||||
@@ -90,6 +90,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust
|
||||
customsHsCode: item.customsHsCode || undefined,
|
||||
packageType: item.packageType,
|
||||
packageCount: item.packageCount,
|
||||
packingGroupNo: draft.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? item.packingGroupNo.trim() || undefined : undefined,
|
||||
grossWeightKg: item.grossWeightKg,
|
||||
volumeCbm: item.volumeCbm,
|
||||
lengthCm: item.lengthCm || undefined,
|
||||
@@ -296,7 +297,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
|
||||
productImageName: '', productImagePreviewUrl: '', productImageUploading: false, productImageError: '',
|
||||
customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0,
|
||||
heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1,
|
||||
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg,
|
||||
pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, packingGroupNo: item.packingGroupNo ?? '', grossWeightKg: item.grossWeightKg,
|
||||
volumeCbm: item.volumeCbm, remark: item.cargoRemark ?? '',
|
||||
packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0,
|
||||
currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false,
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface FirstLegCargoItem {
|
||||
unitsPerPackage: number
|
||||
pieces: number
|
||||
packageCount: number
|
||||
packingGroupNo: string
|
||||
grossWeightKg: number
|
||||
volumeCbm: number
|
||||
netWeightKg: number
|
||||
|
||||
@@ -54,7 +54,7 @@ function validDraft() {
|
||||
destinationType: 'FBA',
|
||||
amazonWarehouseCode: 'ONT8',
|
||||
shipmentId: 'FBA-001',
|
||||
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }],
|
||||
items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', unitsPerPackage: 100, pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,17 @@ test('validates the confirmed minimum submission fields', () => {
|
||||
assert.ok(errors.includes('第 1 条货物毛重必须大于 0'))
|
||||
})
|
||||
|
||||
test('rejects a total quantity that does not equal units per package multiplied by package count', () => {
|
||||
const draft = validDraft()
|
||||
draft.items[0].unitsPerPackage = 24
|
||||
draft.items[0].packageCount = 5
|
||||
draft.items[0].pieces = 119
|
||||
|
||||
assert.ok(validateFirstLegOrderDraft(draft).includes('第 1 条货物总数量应等于每箱数量 × 箱数(120)'))
|
||||
draft.items[0].pieces = 120
|
||||
assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('总数量应等于')))
|
||||
})
|
||||
|
||||
test('only requires sender for pickup and receiver for a private destination', () => {
|
||||
const publicDraft = validDraft()
|
||||
publicDraft.shipperPartySnapshot = null
|
||||
@@ -169,3 +180,15 @@ test('summarizes cargo values from line items', () => {
|
||||
volumeCbm: 4.75,
|
||||
})
|
||||
})
|
||||
|
||||
test('counts one physical carton series for SKU lines in the same mixed-carton group', () => {
|
||||
const first = { ...validDraft().items[0], packingGroupNo: 'A', packageCount: 10, pieces: 20, unitsPerPackage: 2 }
|
||||
const second = { ...first, id: 'cargo-2', sku: 'SKU-002', pieces: 30, unitsPerPackage: 3 }
|
||||
|
||||
assert.equal(summarizeFirstLegCargo([first, second]).packageCount, 10)
|
||||
const draft = { ...validDraft(), fulfillmentType: 'ONE_CLICK_DROPSHIP', items: [first, second] }
|
||||
assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('混箱组合')))
|
||||
|
||||
second.packageCount = 9
|
||||
assert.ok(validateFirstLegOrderDraft(draft).includes('混箱组合 A 的箱数、包装类型和箱规必须一致'))
|
||||
})
|
||||
|
||||
@@ -65,6 +65,7 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI
|
||||
unitsPerPackage: 1,
|
||||
pieces: 1,
|
||||
packageCount: 1,
|
||||
packingGroupNo: '',
|
||||
grossWeightKg: 0,
|
||||
volumeCbm: 0,
|
||||
netWeightKg: 0,
|
||||
@@ -130,22 +131,48 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) {
|
||||
if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`)
|
||||
if (!Number.isInteger(item.packageCount) || item.packageCount <= 0) errors.push(`${line}箱数必须为正整数`)
|
||||
if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 0) errors.push(`${line}每箱数量必须为正整数`)
|
||||
if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0
|
||||
&& Number.isInteger(item.packageCount) && item.packageCount > 0
|
||||
&& Number.isInteger(item.pieces) && item.pieces > 0
|
||||
&& item.pieces !== item.unitsPerPackage * item.packageCount) {
|
||||
errors.push(`${line}总数量应等于每箱数量 × 箱数(${item.unitsPerPackage * item.packageCount})`)
|
||||
}
|
||||
if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`)
|
||||
if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`)
|
||||
if (item.woodenProduct && draft.customsInspection !== true) errors.push(`${line}为木制品,“是否商检”必须选择“是”`)
|
||||
})
|
||||
|
||||
if (draft.fulfillmentType === 'ONE_CLICK_DROPSHIP') {
|
||||
const groups = new Map<string, FirstLegCargoItem>()
|
||||
draft.items.forEach((item) => {
|
||||
const group = item.packingGroupNo.trim()
|
||||
if (!group) return
|
||||
const first = groups.get(group)
|
||||
if (!first) groups.set(group, item)
|
||||
else if (first.packageCount !== item.packageCount || first.packageType !== item.packageType
|
||||
|| first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm) {
|
||||
errors.push(`混箱组合 ${group} 的箱数、包装类型和箱规必须一致`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) {
|
||||
const countedGroups = new Set<string>()
|
||||
return items.reduce(
|
||||
(summary, item) => ({
|
||||
(summary, item) => {
|
||||
const group = item.packingGroupNo.trim()
|
||||
const packageCount = group && countedGroups.has(group) ? 0 : item.packageCount
|
||||
if (group) countedGroups.add(group)
|
||||
return ({
|
||||
pieces: summary.pieces + item.pieces,
|
||||
packageCount: summary.packageCount + item.packageCount,
|
||||
packageCount: summary.packageCount + packageCount,
|
||||
grossWeightKg: summary.grossWeightKg + item.grossWeightKg,
|
||||
volumeCbm: summary.volumeCbm + item.volumeCbm,
|
||||
}),
|
||||
})
|
||||
},
|
||||
{ pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -430,6 +430,8 @@ test('shows field-level validation and concise guidance for required order input
|
||||
assert.match(source, /prop="consigneePartySnapshot\.address" required/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/)
|
||||
assert.match(source, /<span class="required-field-mark" aria-hidden="true">\*<\/span>总毛重/)
|
||||
assert.match(source, /<span class="required-field-mark" aria-hidden="true">\*<\/span>总体积/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.upc`">\s*<el-input v-model="item\.upc"[^>]*placeholder="UPC NO\.(选填)"/)
|
||||
assert.doesNotMatch(source, /:prop="`items\.\$\{index\}\.upc`"[^>]*(?:required|:rules)/)
|
||||
assert.match(source, /UPC NO\. 选填/)
|
||||
|
||||
@@ -1973,13 +1973,14 @@ async function searchOrders(model: QueryModel) {
|
||||
<table class="cargo-edit-table">
|
||||
<thead><tr>
|
||||
<th class="cargo-check-cell"><el-checkbox :model-value="allCargoSelected" :indeterminate="someCargoSelected" aria-label="全选货物" @change="toggleAllCargoItems" /></th><th class="cargo-index-cell">#</th>
|
||||
<th>SKU NO.<small>SKU必填</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class="cargo-switch-column">木制品<small>勾选后须商检</small></th>
|
||||
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column">总毛重</th><th class="cargo-number-column">总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
|
||||
<th>SKU NO.<small>SKU必填</small></th><th v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">混箱组合<small>同号为同一批箱</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class="cargo-switch-column">木制品<small>勾选后须商检</small></th>
|
||||
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总毛重</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
|
||||
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>货件编号<small>货件编号必填</small></th><th>跟踪编号<small>跟踪编号必填</small></th><th>UPC NO.</th><th>备注</th><th class="cargo-operation-column">操作</th>
|
||||
</tr></thead>
|
||||
<tbody><tr v-for="(item, index) in form.items" :key="item.id">
|
||||
<td class="cargo-check-cell"><el-checkbox :model-value="selectedCargoIds.includes(item.id)" :aria-label="`选择货物 ${index + 1}`" @change="toggleCargoItem(item.id, $event)" /></td><td class="cargo-index-cell">{{ index + 1 }}</td>
|
||||
<td><el-form-item :prop="`items.${index}.sku`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入 SKU NO.') : undefined"><el-input v-model="item.sku" maxlength="64" placeholder="SKU编号" /></el-form-item></td>
|
||||
<td><el-form-item :prop="`items.${index}.sku`" :required="form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" :rules="form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? requiredTextRules('请输入 SKU NO.') : undefined"><el-input v-model="item.sku" maxlength="64" placeholder="SKU编号" /></el-form-item></td>
|
||||
<td v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'"><el-input v-model="item.packingGroupNo" maxlength="32" placeholder="如 A(选填)" /></td>
|
||||
<td><el-form-item :prop="`items.${index}.cargoNameCn`" required :rules="requiredTextRules('请输入中文品名')"><el-input v-model="item.cargoNameCn" maxlength="128" placeholder="中文品名" /></el-form-item></td>
|
||||
<td class="cargo-image-column"><div class="cargo-table-image"><img v-if="item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-upload v-else :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button text :loading="item.productImageUploading" aria-label="上传图片"><ImagePlus :size="17" /></el-button></el-upload><el-button v-if="item.productImagePreviewUrl" class="cargo-image-remove" text type="danger" aria-label="移除图片" @click="removeCargoProductImage(item)"><Trash2 :size="12" /></el-button></div></td>
|
||||
<td><el-input v-model="item.cargoNameEn" maxlength="128" placeholder="English Name" /></td><td><el-input v-model="item.hsCode" maxlength="20" placeholder="HS Code" /></td><td><el-input v-model="item.customsHsCode" maxlength="20" placeholder="HS Code" /></td>
|
||||
@@ -1993,7 +1994,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cargo-table-hint"><span>SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填</span></div>
|
||||
<div class="cargo-table-hint"><span v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">相同混箱组合号表示相同一批物理箱;组内箱数、包装类型和箱规必须一致,物理箱数只计算一次。</span><span v-else>SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填</span></div>
|
||||
</section>
|
||||
|
||||
<section class="order-form-section prototype-section cost-estimate-section" aria-label="费用预估">
|
||||
@@ -2138,6 +2139,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-table :data="pagedDetailCargoItems" border stripe class="cargo-detail-table" row-key="id">
|
||||
<el-table-column type="index" label="#" width="54" align="center" :index="(index: number) => (detailCargoPage - 1) * detailCargoPageSize + index + 1" />
|
||||
<el-table-column label="SKU NO." width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.sku || '-' }}</span></template></el-table-column>
|
||||
<el-table-column v-if="detailOrder.fulfillmentType === 'ONE_CLICK_DROPSHIP'" prop="packingGroupNo" label="混箱组合" width="100" align="center"><template #default="{ row }">{{ row.packingGroupNo || '-' }}</template></el-table-column>
|
||||
<el-table-column label="中文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameCn || '-' }}</template></el-table-column>
|
||||
<el-table-column label="图片" width="66" align="center"><template #default="{ row }"><div class="cargo-detail-thumbnail"><img v-if="row.productImagePreviewUrl" :src="row.productImagePreviewUrl" :alt="`${row.cargoNameCn || '货物'}产品图片`" /><span v-else>-</span></div></template></el-table-column>
|
||||
<el-table-column label="英文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameEn || '-' }}</template></el-table-column>
|
||||
@@ -2425,6 +2427,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.cargo-check-cell, .cargo-index-cell { width: 38px !important; text-align: center !important; vertical-align: middle !important; }
|
||||
.cargo-image-column { width: 62px !important; text-align: center !important; }
|
||||
.cargo-number-column { width: 92px !important; }
|
||||
.required-field-mark { margin-right: 2px; color: var(--el-color-danger); }
|
||||
.cargo-dimensions-column { width: 240px !important; }
|
||||
.cargo-operation-column { width: 76px !important; text-align: center !important; }
|
||||
.cargo-switch-column { width: 112px !important; text-align: center !important; vertical-align: middle !important; }
|
||||
|
||||
Reference in New Issue
Block a user