diff --git a/AGENTS.md b/AGENTS.md index 31693af..ea0a386 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ | 中英西语言设置 | `src/i18n/index.js`、`src/i18n/locales/`、`src/pages/profile/profile.vue` | 个人中心可选择并持久化 `zh-CN`、`en`、`es`;当前业务翻译资源仍为空,页面业务文案继续使用中文,不得把语言选择入口误报为全量翻译完成 | | 柜号签收、照片与入仓真实作业 | `src/pages/operation/operation.vue`、`src/components/ReceivingSessionSummary.vue`、`src/api/inbound.js`、`src/api/receiving-adapter.js`、`src/api/overseas.js`、`src/stores/receiving.js` | 签收前展开显示国内空仓、半仓、满仓、锁仓和带锁码五类整柜只读照片,收货中折叠查看,单图加载失败只允许重试且不阻断拆柜;海外三个固定照片角色各一张,支持现场拍照、鉴权预览及更换失败保留原图;多计划同柜直接展示服务端 `assigned/mergeAllowed/urgent/mergeHint`,合托明确为现场可选而非完成门禁;扫码实收确认计划生成阶段已预留的真实托盘和库位,校验卡展示预留库位编码及 `A0` 共享溢出位提示,不在设备端重新分配。Backend `lastleg-service/inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,公网前缀 `/api/pda/v1/last-leg/receiving-containers/**` | | 退货入库签收与合箱 | `src/pages/return-receipt/return-receipt.vue`、`src/pages/return-packing/return-packing.vue`、`src/api/return-receipt.js`、`src/api/return-packing.js`、`src/stores/warehouse-work.js` | 签收优先扫描退货单号,也可扫描 SKU 后选择候选退货单,按本包实际 SKU 数量生成退货箱与计费批次;合箱只选择同客户已完成退货单的可用库存并录入新箱箱规;公网前缀 `/api/pda/v1/last-leg/return-receipts/**`、`/api/pda/v1/last-leg/return-inventory/**` | +| 退货箱内清点 | `src/pages/return-counting/return-counting.vue`、`src/api/return-counting.js` | 扫描箱号或托盘号定位清点任务;各箱 SKU 汇总与签收数量核平,支持恢复当前任务;页面注册见 `src/pages.json` | | 仓储转运测量、打托与标签 | `src/pages/warehouse-transfer/warehouse-transfer.vue`、`src/api/warehouse-transfers.js` | 工作台键 `warehouseTransfer`;候选→详情→逐托测量草稿→生成新托盘号→扫码绑定→照片与完成打托。API 模块路径 `/pda/v1/last-leg/warehouse-transfers` 经统一海外请求层处理;不是 Admin 转运审核或排车转派送。回归 `tests/warehouse-transfer-api.test.mjs` | | 托盘管理真实作业 | `src/pages/pallet/pallet.vue`、`src/api/pallet.js` | 扫码查询托盘内容,按服务端返回的托盘/库存版本执行组托、拆托、换托和解绑;公网前缀 `/api/pda/v1/last-leg/pallet-operations/**` | | 库存盘点(入口隐藏) | `src/pages/stocktake/stocktake.vue`、`src/api/warehouse-operations.js`、`src/stores/warehouse-work.js` | 底层创建、恢复、计数与提交实现仍保留;当前 `src/data/overseas-workbench.js` 不注册库存盘点,不向现场开放入口 | diff --git a/output/gray-h5-scan/gray-release-candidate.json b/output/gray-h5-scan/gray-release-candidate.json new file mode 100644 index 0000000..d2c4f50 --- /dev/null +++ b/output/gray-h5-scan/gray-release-candidate.json @@ -0,0 +1,16 @@ +{ + "base": "3133aac01953a2b7034517171cb098148407cd6e", + "commit": "a7388914cdd1ae654efa08867edb51fc4e0b6e5c", + "files": [ + "src/pages/dispatch/dispatch.vue", + "src/utils/dispatch-error.js", + "tests/fulfillment-workflows.test.mjs" + ], + "supersedes": "cf984ab4812d64b33e1ad2c98bc2804a272c6c77", + "deployed": false, + "checks": { + "tests": 215, + "h5Build": "PASS", + "browser": "local corrected H5 with gray API" + } +} diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js index e558403..82cb71a 100644 --- a/src/api/fulfillment.js +++ b/src/api/fulfillment.js @@ -1,4 +1,4 @@ -import { overseasRequest } from './overseas.js' +import { overseasRequest, overseasUpload, overseasDownload } from './overseas.js' const DISPATCH_BASE = '/pda/v1/last-leg/dispatches' const OUTBOUND_BASE = '/pda/v1/last-leg/outbound-tasks' @@ -43,6 +43,7 @@ export function normalizeDispatchTask(value) { warehouseRouteLabel: String(item?.warehouseRouteLabel || ''), scheduledPackages: count(item?.scheduledPackages), pickedPackages: count(item?.pickedPackages), + returnCartons: asList(item?.returnCartons).map((carton) => ({ ...carton, id: String(carton.id || ''), cartonMark: String(carton.cartonMark || ''), productQuantity: count(carton.productQuantity) })), positions: asList(item?.positions).map((position) => ({ ...position, id: String(position?.id || ''), @@ -137,6 +138,7 @@ export function outboundPickPayload(item, position, packages, scannedCode = '') outboundPalletItemId: item.outboundPalletItemId, inventoryPositionId: position.inventoryPositionId || null, packages: Number(packages), + ...(item.returnSkuQuantity != null ? { productQuantity: Number(item.returnSkuQuantity) } : {}), cartonMarks: skuScan ? [] : position.cartonMark ? [position.cartonMark] : [], ...(skuScan ? { skuCode: item.skuCode } : {}) } @@ -173,6 +175,11 @@ export async function resolveCargoDiscardPallet(palletNo) { return normalizeCargoDiscardTask(await overseasRequest('POST', `${CARGO_DISCARD_BASE}/resolve`, { palletNo })) } -export async function confirmCargoDiscard(discardNo, palletNo, lines) { - return normalizeCargoDiscardTask(await overseasRequest('POST', `${CARGO_DISCARD_BASE}/${encoded(discardNo)}/pallets/${encoded(palletNo)}/confirm`, { lines })) +export async function confirmCargoDiscard(discardNo, palletNo, lines, photoFileIds) { + return normalizeCargoDiscardTask(await overseasRequest('POST', `${CARGO_DISCARD_BASE}/${encoded(discardNo)}/pallets/${encoded(palletNo)}/confirm`, { lines, photoFileIds })) } + +const discardPhotoPath = (discardNo, palletNo) => `${CARGO_DISCARD_BASE}/${encoded(discardNo)}/pallets/${encoded(palletNo)}/photos/files` +export const listCargoDiscardPhotos = (discardNo, palletNo) => overseasRequest('GET', discardPhotoPath(discardNo, palletNo)) +export const uploadCargoDiscardPhoto = (discardNo, palletNo, filePath) => overseasUpload(discardPhotoPath(discardNo, palletNo), filePath) +export const downloadCargoDiscardPhoto = (discardNo, palletNo, fileId) => overseasDownload(`${discardPhotoPath(discardNo, palletNo)}/${encoded(fileId)}/download`, { expectedContentTypePrefix: 'image/' }) diff --git a/src/api/pallet.js b/src/api/pallet.js index d90081f..1677fd4 100644 --- a/src/api/pallet.js +++ b/src/api/pallet.js @@ -4,6 +4,8 @@ const BASE = '/pda/v1/last-leg/pallet-operations' export const findRelabelSources = (relabelNo) => overseasRequest('POST', `${BASE}/relabel-sources`, { relabelNo }) export const createRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-pallet-label`, payload) export const bindRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-disassemble`, payload) +export const listRelabelPalletMeasurements = (relabelNo) => overseasRequest('POST', `${BASE}/relabel-pallet-measurements`, { relabelNo }) +export const measureRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-pallet-measure`, payload) export function resolvePallet(code) { return overseasRequest('POST', `${BASE}/pallets/resolve`, { code: String(code || '').trim() }) diff --git a/src/api/relabel.js b/src/api/relabel.js index a04e836..117342b 100644 --- a/src/api/relabel.js +++ b/src/api/relabel.js @@ -4,6 +4,7 @@ const BASE = '/pda/v1/last-leg/relabel-tasks' const encoded = (value) => encodeURIComponent(String(value || '').trim()) export const scanRelabelSku = async (no, payload) => normalizeRelabel(await overseasRequest('POST', `${BASE}/${encoded(no)}/skus/scan`, payload)) export const completeReturnRelabelSku = async (no, payload) => normalizeRelabel(await overseasRequest('POST', `${BASE}/${encoded(no)}/return-skus/complete`, payload)) +export const packReturnRelabelCartons = async (no, payload) => normalizeRelabel(await overseasRequest('POST', `${BASE}/${encoded(no)}/return-cartons/pack`, payload)) export const completeReturnRelabelSkuTotal = async (no, payload) => normalizeRelabel(await overseasRequest('POST', `${BASE}/${encoded(no)}/return-skus/complete-total`, payload)) function asList(value) { return Array.isArray(value) ? value : [] } @@ -11,6 +12,7 @@ function asList(value) { return Array.isArray(value) ? value : [] } export function normalizeRelabel(value = {}) { const items = asList(value.items).map((item) => ({ ...item, itemId: String(item.itemId || ''), skuCode: String(item.skuCode || ''), + returnCartonId: item.returnCartonId == null ? null : String(item.returnCartonId), returnQuantity: Number(item.returnQuantity || 0), newSkuCode: String(item.newSkuCode || ''), operationType: String(item.operationType || 'CARTON_LABEL'), expectedPackages: Number(item.expectedPackages || 0), relabelledPackages: Number(item.relabelledPackages || 0), skippedPackages: Number(item.skippedPackages || 0), exceptionPackages: Number(item.exceptionPackages || 0) diff --git a/src/api/return-counting.js b/src/api/return-counting.js new file mode 100644 index 0000000..93705e4 --- /dev/null +++ b/src/api/return-counting.js @@ -0,0 +1,20 @@ +import { overseasRequest } from './overseas.js' +const BASE = '/pda/v1/last-leg/return-inventory/counting' +export function getCurrentReturnCountTask() { + return overseasRequest('GET', `${BASE}/current`) +} +export function beginReturnCountTask(receiptNo) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}/begin`) +} +export function scanReturnCountTasks(code) { + return overseasRequest('GET', `${BASE}/scan/${encodeURIComponent(String(code).trim())}`) +} +export function listReturnCountTasks({ page = 1, size = 10, keyword = '' } = {}) { + return overseasRequest('GET', `${BASE}/tasks?page=${page}&size=${size}&keyword=${encodeURIComponent(keyword.trim())}`) +} +export function scanReturnCountCarton(mark) { + return overseasRequest('GET', `${BASE}/cartons/${encodeURIComponent(String(mark).trim())}`) +} +export function confirmReturnCount(receiptNo, request) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}`, request) +} diff --git a/src/api/return-packing.js b/src/api/return-packing.js index 9b43e69..40197a4 100644 --- a/src/api/return-packing.js +++ b/src/api/return-packing.js @@ -17,7 +17,17 @@ function normalizeItems(items) { } export function normalizePackingPreview(value = {}) { - return { + value = value && typeof value === 'object' ? value : {} + const normalizeCustomer = (customer = {}) => ({ + customerId: customer.customerId == null ? '' : String(customer.customerId), + orders: Array.isArray(customer.orders) ? customer.orders.map((item) => ({ + returnNo: String(item.returnNo || ''), customerReferenceNo: String(item.customerReferenceNo || ''), status: String(item.status || '') + })) : [], + skus: Array.isArray(customer.skus) ? customer.skus.map((item) => ({ + skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), availableQuantity: count(item.availableQuantity) + })) : [] + }) + const result = { customerKey: value.customerId == null ? '' : String(value.customerId), orders: Array.isArray(value.orders) ? value.orders.map((item) => ({ returnNo: String(item.returnNo || ''), customerReferenceNo: String(item.customerReferenceNo || ''), status: String(item.status || '') @@ -26,6 +36,13 @@ export function normalizePackingPreview(value = {}) { skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), availableQuantity: count(item.availableQuantity) })) : [] } + if (Array.isArray(value.customers)) result.customers = value.customers.map(normalizeCustomer).filter((item) => item.customerId) + else if (result.customerKey && result.orders.length) { + // Older service versions returned the single-customer shape. Keep the + // PDA behavior consistent by exposing it through the grouped contract. + result.customers = [{ customerId: result.customerKey, orders: result.orders, skus: result.skus }] + } + return result } export function normalizeReturnLabel(value = {}) { @@ -37,16 +54,53 @@ export function normalizeReturnLabel(value = {}) { } } -export async function findPackingOrders(returnNo = '') { +export async function findPackingOrders(returnNo = '', mode = 'RETURN_NO') { const query = String(returnNo || '').trim() try { - return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`)) + return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?${mode === 'RETURN_NO' ? 'returnNo' : 'skuCode'}=${encoded(query)}` : ''}`)) } catch (error) { if (isNotFound(error)) return normalizePackingPreview() throw error } } +export async function listPackingPlans() { + const value = await overseasRequest('GET', `${BASE}/packing/plans`) + return Array.isArray(value) ? value.map(normalizePackingPlan) : [] +} + +export async function completePackingPlan(planNo, request) { + const value = await overseasRequest('POST', `${BASE}/packing/plans/${encoded(planNo)}/complete`, request) + return { planNo: String(value?.planNo || planNo), labels: Array.isArray(value?.labels) ? value.labels.map(normalizeReturnLabel) : [] } +} + +export function normalizePackingPlan(value = {}) { + value = { ...value, completionCartons: Array.isArray(value.completionCartons) ? value.completionCartons.map(normalizeReturnLabel) : [], transfers: Array.isArray(value.transfers) ? value.transfers : [], completedAt: value.completedAt || null } + return { completionCartons: value.completionCartons, transfers: value.transfers, completedAt: value.completedAt, planNo: String(value.planNo || ''), customerId: String(value.customerId || ''), returnNos: Array.isArray(value.returnNos) ? value.returnNos.map(String) : [], sourceCartonMarks: Array.isArray(value.sourceCartonMarks) ? value.sourceCartonMarks.map(String) : [], sources: Array.isArray(value.sources) ? value.sources : [], mode: String(value.mode || ''), status: String(value.status || ''), boxes: Array.isArray(value.boxes) ? value.boxes : [], remark: String(value.remark || '') } +} + +export async function getPackingPlan(planNo) { + return normalizePackingPlan(await overseasRequest('GET', `${BASE}/packing/plans/${encoded(planNo)}`)) +} + +export async function scanReturnCarton(mark) { + return overseasRequest('GET', `${BASE}/cartons/${encoded(mark)}`) +} + +export async function mergeReturnBoxes(request) { + const value = await overseasRequest('POST', `${BASE}/cartons/merge-boxes`, request) + return { labels: [normalizeReturnLabel(value?.label)] } +} + +export async function packReturnSkus(request) { + const value = await overseasRequest('POST', `${BASE}/cartons/pack`, request) + return { labels: Array.isArray(value?.labels) ? value.labels.map(normalizeReturnLabel) : [] } +} + +export async function putawayReturnCarton(mark, request) { + return overseasRequest('POST', `${BASE}/cartons/${encoded(mark)}/putaway`, request) +} + export async function mergeReturnCartons(request) { const value = await overseasRequest('POST', `${BASE}/cartons/merge`, request) return { cartonMark: String(value?.cartonMark || ''), skuCode: String(value?.skuCode || ''), totalQuantity: count(value?.totalQuantity), label: normalizeReturnLabel(value?.label) } @@ -55,3 +109,7 @@ export async function mergeReturnCartons(request) { export async function getReturnCartonLabel(cartonMark) { return normalizeReturnLabel(await overseasRequest('GET', `${BASE}/cartons/${encoded(cartonMark)}/label`)) } + +export async function confirmReturnLabel(cartonMark) { + return normalizeReturnLabel(await overseasRequest('POST', `${BASE}/cartons/${encoded(cartonMark)}/confirm-label`)) +} diff --git a/src/components/RelabelPalletWork.vue b/src/components/RelabelPalletWork.vue index bd24a41..9661914 100644 --- a/src/components/RelabelPalletWork.vue +++ b/src/components/RelabelPalletWork.vue @@ -6,20 +6,30 @@ 已绑定箱唛:选择后扫描新目标托盘可改绑(已排车或出库不可直接改绑)。 - + + + {{ pallet.palletCode }} · {{ pallet.totalPackages }} 箱 · 体积 {{ pallet.volumeCbm == null ? '待补测' : pallet.volumeCbm + ' m³' }} · 重量 {{ pallet.grossWeightKg == null ? '待补测' : pallet.grossWeightKg + ' kg' }} + + {{ pallet.palletCode }} · {{ pallet.packages }} 箱 + {{ pallet.inherited ? '原托盘货物未变,沿用原测量;包装变化请重新测量。' : '拆托、合托后请确认本托最终尺寸和毛重,原托盘剩余货物也需补测。' }} + 已确认:{{ pallet.volumeCbm }} m³ · {{ pallet.grossWeightKg }} kg + + + + 正在改绑:{{ rebindSelection.mark }},当前托盘:{{ rebindSelection.pallet }} - 已记录组托:{{ entry.cartonMarks.join('、') }} → {{ entry.targetPallets.join('、') }}(含历史轮次) + + {{ message }}{{ error }} diff --git a/src/components/ReturnLabelConfirmation.vue b/src/components/ReturnLabelConfirmation.vue new file mode 100644 index 0000000..e41609e --- /dev/null +++ b/src/components/ReturnLabelConfirmation.vue @@ -0,0 +1,31 @@ + + diff --git a/src/components/ReturnRelabelPackWork.vue b/src/components/ReturnRelabelPackWork.vue new file mode 100644 index 0000000..1ecb4f2 --- /dev/null +++ b/src/components/ReturnRelabelPackWork.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index d99c613..ec649be 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -4,7 +4,8 @@ export const MODULES = [ { key: 'warehouseTransferList', name: '仓储转运测量列表', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' }, { key: 'warehouseTransferWork', name: '转运测量作业', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' }, { key: 'returnReceipt', name: '退货入库', nameKey: 'modules.returnReceipt.name', icon: '退', iconType: 'undo', description: '扫描退货单或 SKU,按本包实收数量生成退货库存', descriptionKey: 'modules.returnReceipt.description', available: true, route: '/pages/return-receipt/return-receipt' }, - { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' } + { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' }, + { key: 'returnCounting', name: '退货箱内清点', icon: '清', iconType: 'list', description: '逐箱清点托盘签收货物,核对 SKU 和数量', available: true, route: '/pages/return-counting/return-counting' } ] }, { key: 'outbound', name: '出库履约', nameKey: 'modules.outbound.name', icon: '出', description: '排车拣货、出库任务与发车闭环', descriptionKey: 'modules.outbound.description', operations: [ { key: 'dispatchPicking', name: '排车拣货', nameKey: 'modules.dispatchPicking.name', icon: '车', iconType: 'paperplane', description: '按排车任务扫描托盘拣货,完成门禁后执行发车', descriptionKey: 'modules.dispatchPicking.description', available: true, route: '/pages/dispatch/dispatch' }, diff --git a/src/pages.json b/src/pages.json index 31803c6..efd3a9a 100644 --- a/src/pages.json +++ b/src/pages.json @@ -6,6 +6,7 @@ { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柜号入库与现场作业", "backgroundColor": "#F5F7FA" } }, { "path": "pages/warehouse-transfer/warehouse-transfer", "style": { "navigationStyle": "custom", "navigationBarTitleText": "仓储转运测量", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-receipt/return-receipt", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货入库", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/return-counting/return-counting", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货箱内清点", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-packing/return-packing", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货合箱", "backgroundColor": "#F5F7FA" } }, { "path": "pages/pallet/pallet", "style": { "navigationStyle": "custom", "navigationBarTitleText": "托盘管理", "backgroundColor": "#F5F7FA" } }, { "path": "pages/stocktake/stocktake", "style": { "navigationStyle": "custom", "navigationBarTitleText": "库存盘点", "backgroundColor": "#F5F7FA" } }, diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue index ba9b125..723da3c 100644 --- a/src/pages/dispatch/dispatch.vue +++ b/src/pages/dispatch/dispatch.vue @@ -5,14 +5,14 @@ 排车拣货与发车 - 客户与当前海外仓由登录令牌和服务端确定;扫描排车单号或任务中的托盘码开始作业 + 扫描排车单号打开任务,再扫描任务中的托盘码进行拣货 当前任务排车拣货详情 - + - 网络失败不会清空草稿,再次提交会沿用同一幂等键。 + 网络中断会保留当前作业,恢复后可重试。 正在加载排车任务… @@ -21,7 +21,7 @@ 错误码:{{ error.code }} requestId:{{ error.requestId }} traceId:{{ error.traceId }} - 网络连接失败,未完成作业与幂等键已保留。 + 网络连接失败,当前作业已保留,请恢复网络后重试。 @@ -47,9 +47,10 @@ - + {{ item.palletNo || '未关联托盘码' }}{{ item.pickedPackages }}/{{ item.scheduledPackages }} 箱 {{ routeLabel(item) }} + {{ carton.cartonMark }} · {{ carton.status === 'OUTBOUNDED' ? '已拣货' : '待扫描' }} @@ -57,7 +58,8 @@ 本次拣货:{{ selectedItem.palletNo }} - 拣货箱数最多 {{ selectedRemaining }} 箱 + {{ selectedReturnCarton ? `已扫描 ${selectedReturnCarton.cartonMark}` : '请扫描此托盘上的来源箱唛,每次拣货一箱' }} + 拣货箱数最多 {{ selectedRemaining }} 箱 @@ -105,16 +107,17 @@ computed: { recentTaskNo() { return fulfillment.dispatch.currentNo || '' }, packages() { return fulfillment.dispatch.packages }, - selectedItem() { return this.current?.items?.find((item) => item.palletNo === fulfillment.dispatch.palletCode) || null }, + selectedItem() { return this.current?.items?.find((item) => fulfillment.dispatch.dispatchItemId ? item.dispatchItemId === fulfillment.dispatch.dispatchItemId : item.palletNo === fulfillment.dispatch.palletCode) || null }, + selectedReturnCarton() { return this.selectedItem?.returnCartons?.find((carton) => carton.cartonMark === fulfillment.dispatch.cartonCode && carton.status === 'OUTBOUND_PENDING') || null }, selectedPosition() { return this.selectedItem?.positions?.find((item) => item.availablePackages > 0) || this.selectedItem?.positions?.[0] || null }, - selectedRemaining() { return Math.max(0, Math.min(this.selectedItem?.scheduledPackages - this.selectedItem?.pickedPackages || 0, this.selectedPosition?.availablePackages || 0)) }, + selectedRemaining() { const remaining = this.selectedItem?.scheduledPackages - this.selectedItem?.pickedPackages || 0; return Math.max(0, this.selectedItem?.returnCartons?.length ? remaining : Math.min(remaining, this.selectedPosition?.availablePackages || 0)) }, plannedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.scheduledPackages, 0) }, pickedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.pickedPackages, 0) }, remainingPackages() { return Math.max(0, this.plannedPackages - this.pickedPackages) }, allPicked() { return this.plannedPackages > 0 && this.remainingPackages === 0 }, pickingCompleted() { return finishedStatuses.includes(this.current?.status) }, departed() { return this.current?.status === 'DEPARTED' }, - canPick() { return this.current?.canPick === true && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining && !this.pickingCompleted }, + canPick() { return this.current?.canPick === true && (this.selectedItem?.returnCartons?.length ? Boolean(this.selectedReturnCarton) : Boolean(this.selectedPosition) && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining) && !this.pickingCompleted }, canCompleteStage() { return Boolean(this.current) && !this.pickingCompleted }, canComplete() { return this.current?.canPick === true && this.plannedPackages > 0 && this.remainingPackages === 0 }, canDepart() { return this.pickingCompleted && this.allPicked && this.current?.canDepart === true && !this.departed } @@ -144,26 +147,36 @@ async loadTasks() { await this.run('加载排车任务', async () => { this.tasks = await listDispatchTasks(); this.current = null }, true) }, async openTask(dispatchNo) { const code = String(dispatchNo || '').trim(); if (!code) return - await this.run('恢复排车任务', async () => { const previousNo = fulfillment.dispatch.currentNo; this.current = await getDispatchTask(code); updateFulfillmentDraft('dispatch', { currentNo: this.current.dispatchNo, palletCode: previousNo === this.current.dispatchNo ? fulfillment.dispatch.palletCode : '' }); this.scanCode = '' }, true) + await this.run('恢复排车任务', async () => { const previousNo = fulfillment.dispatch.currentNo; this.current = await getDispatchTask(code); const same = previousNo === this.current.dispatchNo; updateFulfillmentDraft('dispatch', { currentNo: this.current.dispatchNo, palletCode: same ? fulfillment.dispatch.palletCode : '', dispatchItemId: same ? fulfillment.dispatch.dispatchItemId : '', cartonCode: same ? fulfillment.dispatch.cartonCode : '' }); this.scanCode = '' }, true) }, - closeTask() { this.current = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', packages: 1 }); this.loadTasks() }, + closeTask() { this.current = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); this.loadTasks() }, handleScan(raw) { const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return console.info('[Overseas PDA dispatch] scan', { code, task: this.current?.dispatchNo || '' }) this.scanCode = code + this.error = null; this.successMessage = '' + updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }) if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code) - const item = this.current.items.find((value) => [value.palletNo, value.fbaWarehouseCode].includes(code)) - if (!item) return this.setError(new Error('扫码未匹配当前排车任务的托盘或稳定业务码'), '扫码未匹配') + const cartonItem = this.current.items.find((item) => item.returnCartons?.some((carton) => carton.cartonMark === code)) + if (cartonItem) { + if (cartonItem.returnCartons.find((carton) => carton.cartonMark === code)?.status !== 'OUTBOUND_PENDING') return this.setError(new Error('该箱已拣货或不可出库'), '扫码未匹配') + this.selectPallet(cartonItem); updateFulfillmentDraft('dispatch', { cartonCode: code, packages: 1 }); this.scanCode = ''; playSuccessSound(); return + } + const matches = this.current.items.filter((value) => value.palletNo === code && value.scheduledPackages > value.pickedPackages) + if (matches.length > 1) return this.setError(new Error('此托盘包含多条待拣明细,请选择对应货物卡片'), '请选择拣货明细') + const item = matches[0] + if (!item) return this.setError(new Error(this.current.items.some((value) => value.palletNo === code) ? '该托盘已完成拣货,请勿重复操作' : '扫码未匹配当前任务的托盘码或来源箱唛'), '扫码未匹配') this.selectPallet(item); this.scanCode = ''; playSuccessSound() }, - selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, + selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, async submitPick() { if (!this.canPick) return - const payload = { dispatchNo: this.current.dispatchNo, palletNo: this.selectedItem.palletNo, positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 } + const pick = this.selectedReturnCarton ? { positionId: null, cartonMark: this.selectedReturnCarton.cartonMark, packages: 1, productQuantity: this.selectedReturnCarton.productQuantity } : { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 } + const payload = { dispatchNo: this.current.dispatchNo, dispatchItemId: this.selectedItem.dispatchItemId, ...pick } const key = fulfillmentIdempotencyKey('dispatch', 'pick', payload) await this.run('提交排车拣货', async () => { - await pickDispatchItem(this.current.dispatchNo, this.selectedItem.dispatchItemId, { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0, idempotencyKey: key.value }) - this.current = await getDispatchTask(this.current.dispatchNo); releaseFulfillmentKey('dispatch', key.signature); updateFulfillmentDraft('dispatch', { palletCode: '', packages: 1 }); this.successMessage = '本次排车拣货已记录'; playSuccessSound() + await pickDispatchItem(this.current.dispatchNo, this.selectedItem.dispatchItemId, { ...pick, idempotencyKey: key.value }) + this.current = await getDispatchTask(this.current.dispatchNo); releaseFulfillmentKey('dispatch', key.signature); updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); this.successMessage = '本次排车拣货已记录'; playSuccessSound() }) }, async completePicking() { diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index ee1cf5c..60097e7 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -6,7 +6,7 @@ 出库任务整托扫描托盘码,散箱扫描箱唛;扫描排车业务号会转入“排车拣货” 当前任务出库作业详情 - + 任务号直接恢复;退货整托出库扫托盘码,散箱出库扫箱唛。 @@ -27,18 +27,18 @@ {{ current.taskNo || '—' }}{{ statusLabel(current.status) }}出库单 {{ current.outboundNo || '—' }} · {{ taskType(current) }}{{ plannedPackages }}计划箱数{{ pickedPackages }}已拣箱数{{ remainingPackages }}待拣箱数 - + {{ item.palletNo || '散件出库' }}{{ item.pickedPackages }}/{{ item.plannedPackages }} 箱 {{ lineBusinessCode(position) }}{{ position.inboundCode || '无入仓码' }} · 已拣 {{ position.pickedPackages }} 箱 · 可拣 {{ position.availablePackages }} 箱 - 整托出库请扫描托盘码 {{ item.palletNo }} + 整托出库请扫描托盘号 {{ item.palletNo }} 当前明细缺少可扫码库存业务码,已禁止拣货;请刷新或联系平台确认任务下发。 任务详情尚无可拣货明细请刷新;若持续为空,请联系平台确认出库货物是否已下发。 - 本次拣货:{{ lineBusinessCode(selectedPosition) }}拣货箱数最多 {{ selectedRemaining }} 箱 + 本次拣货:{{ lineBusinessCode(selectedPosition) }}本来源计划出库 {{ selectedItem.returnSkuQuantity }} 件,请清点这些件数后确认;余数保留原箱。拣货箱数最多 {{ selectedRemaining }} 箱 完成出库任务尚有 {{ remainingPackages }} 箱未拣,不能完成出库。出库任务已完成,不可重复提交。全部计划货物已扫描核平,服务端将再次校验库存门禁。 @@ -46,7 +46,7 @@ {{ successMessage }} @@ -58,7 +58,7 @@ - + diff --git a/src/pages/return-counting/return-counting.vue b/src/pages/return-counting/return-counting.vue new file mode 100644 index 0000000..335375f --- /dev/null +++ b/src/pages/return-counting/return-counting.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/pages/return-packing/return-packing.vue b/src/pages/return-packing/return-packing.vue index 835c67b..4a84e7f 100644 --- a/src/pages/return-packing/return-packing.vue +++ b/src/pages/return-packing/return-packing.vue @@ -2,18 +2,37 @@ - 添加退货单 + + + 仓库现场直接合箱扫描 SKU 或箱号开始;系统会自动锁定客户和当前仓,跨客户货物不能合箱。 + 平台合箱计划审核通过后推送按计划锁定的来源箱和数量作业,后续合箱规则不变。 - 没有匹配的已完成退货单或可用库存 + 选择客户该 SKU 在多个客户有可用退货库存,请先选择客户 + 没有匹配的已完成退货单或可用库存 -