Merge pull request 'fix(wle): 转仓代发(转x00005)不再计入原仓发货量' (#85) from yanhui-wle-transfer-denominator-20261024 into main
This commit is contained in:
commit
a839d78d56
@ -251,20 +251,16 @@ function buildNotifyText(data = {}, options = {}) {
|
||||
|
||||
const excludedCount = (Number(exclusions.carrierPackages) || 0)
|
||||
+ (Number(exclusions.voidedPackages) || 0)
|
||||
+ (Number(exclusions.nonRealPackages) || 0);
|
||||
+ (Number(exclusions.nonRealPackages) || 0)
|
||||
+ (Number(exclusions.transferPackages) || 0);
|
||||
if (excludedCount > 0) {
|
||||
head.push(`已剔除 ${excludedCount} 个包裹(承运商 / 已终止 / 刷单·信誉单,不计考核)`);
|
||||
head.push(`已剔除 ${excludedCount} 个包裹(承运商 / 已终止 / 刷单·信誉单 / 转仓代发,不计考核)`);
|
||||
}
|
||||
// 现场拦截(售后已签收):货被快递当场拉回、售后仓已签收,不算异常;仍算发货分母
|
||||
const interceptCount = Number(exclusions.interceptPackages) || 0;
|
||||
if (interceptCount > 0) {
|
||||
head.push(`另有 ${interceptCount} 个包裹属现场拦截(售后已签收),不计异常`);
|
||||
}
|
||||
// 转仓代发(转x00005):记账仓下单、目标仓实际发出,本仓永远停在「未揽收」,不算异常
|
||||
const transferCount = Number(exclusions.transferPackages) || 0;
|
||||
if (transferCount > 0) {
|
||||
head.push(`另有 ${transferCount} 个包裹属转仓代发(转x00005),不计异常`);
|
||||
}
|
||||
|
||||
const centerLines = totalPackages > 0 ? buildCenterLines(data) : [];
|
||||
// 明细按仓聚集 + 每仓保底 1 条(避免大仓吃满额度),组间按异常数降序、组内保持时长降序
|
||||
|
||||
@ -158,7 +158,7 @@ const EXCLUDED_ORDER_TYPE_LABELS = Object.freeze([
|
||||
{ keyword: '转空包', label: '信誉单' },
|
||||
]);
|
||||
|
||||
// ── 转仓代发(业务 2026-10-23 指定:不用核对异常)──────────────────────────
|
||||
// ── 转仓代发(业务 2026-10-23 指定不核对异常;2026-10-24 补充不计原仓发货量)──
|
||||
// 聚水潭「转仓」操作会在 `order_labels` 留一条 `转x00005` 形式的标签
|
||||
// (x + 目标仓编码)。这类单**记账仓是原仓**(如崔桥工厂仓),货却由目标仓
|
||||
// (x00005 = 曹县)实际发出 —— 承运账号也跟着变成曹县韵达小件。
|
||||
@ -169,8 +169,9 @@ const EXCLUDED_ORDER_TYPE_LABELS = Object.freeze([
|
||||
// 快递分布:曹县韵达小件 133、邮政快递包裹POSTB 124、睢宁中通 41 ……
|
||||
// 目前只出现过 `转x00005` 一种编码,但标签是机器生成的(后续可能出现 x00006),
|
||||
// 所以用正则匹配「转x<3位以上数字>」,不写死单个编码。
|
||||
// ⚠️ 与「转空包」区分:转空包是发空包(信誉单,假发货);转仓代发是**真发货**,
|
||||
// 只是不从记账仓发 —— 所以保留发货分母,只从异常清单摘掉(同「现场拦截」)。
|
||||
// ⚠️ 与「转空包」区分:转空包是发空包(信誉单,假发货);转仓代发是**真发货**。
|
||||
// 🔥 口径:**整单不计** —— 货不是从原仓走的,既不算原仓异常,也不进原仓发货分母
|
||||
// (记进分母会把分母做大、异常率被稀释)。这与「现场拦截」相反,后者货是本仓发的、留分母。
|
||||
const TRANSFER_WAREHOUSE_LABEL_PATTERN = /转\s*[xX]\s*\d{3,}/;
|
||||
|
||||
// ── 现场拦截(货已被售后仓签收)不算转运异常(业务 2026-10-23 指定)────────
|
||||
|
||||
@ -408,6 +408,18 @@ async function getLogisticsExceptions(options = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ④ 转仓代发(`转x00005` 这类标签):记账仓是原仓,货由目标仓实际发出。
|
||||
// 业务 2026-10-24 定版:**原仓的发货量也不该算** —— 货不是从这个仓走的,
|
||||
// 记在原仓只会把发货分母做大、异常率稀释。所以按「整单不计」处理
|
||||
// (既不算异常、也不进发货分母),与承运商/刷单那几类同级,不是「只摘异常」。
|
||||
const transferReason = registry.matchTransferWarehouse(row.order_labels);
|
||||
if (transferReason) {
|
||||
skippedByTransfer.packages += 1;
|
||||
skippedByTransfer.orders.add(oId);
|
||||
skippedByTransfer.reasons[transferReason] = (skippedByTransfer.reasons[transferReason] || 0) + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = centerStat.get(center.key);
|
||||
stat.shippedOrders.add(oId);
|
||||
stat.packages += 1;
|
||||
@ -419,21 +431,10 @@ async function getLogisticsExceptions(options = {}) {
|
||||
|
||||
const typeMeta = registry.TYPE_MAP.get(type) || { label: type, tone: 'default' };
|
||||
|
||||
// ④ 转仓代发(`转x00005` 这类标签):记账仓是原仓,货由目标仓发出,
|
||||
// 原仓物流状态必然停在「未揽收 / 无物流单」→ 全是假异常(业务 2026-10-23 指定不核对)。
|
||||
// ⚠️ 与 a/b/c 三类不同:货**确实发了**,所以保留发货分母,只从异常清单摘掉。
|
||||
const transferReason = registry.matchTransferWarehouse(row.order_labels);
|
||||
if (transferReason) {
|
||||
skippedByTransfer.packages += 1;
|
||||
skippedByTransfer.orders.add(oId);
|
||||
skippedByTransfer.reasons[transferReason] = (skippedByTransfer.reasons[transferReason] || 0) + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⑤ 现场拦截:售后单有签收单号 + 实退数量 > 0 —— 货已被售后仓签收,
|
||||
// 物流环节其实结束了(派送时被拦下、原车拉回),不算「卡在网点发不出去」。
|
||||
// ⚠️ 同④:**保留发货分母**(货确实发出去了),只从异常清单里摘掉,
|
||||
// 异常率才不会被错误地放大。
|
||||
// ⚠️ 与上面几类不同:这里**保留发货分母**(货确实从这个仓发出去了),
|
||||
// 只从异常清单里摘掉,异常率才不会被错误地放大。
|
||||
if (interceptedOrders.has(oId)) {
|
||||
skippedByIntercept.packages += 1;
|
||||
skippedByIntercept.orders.add(oId);
|
||||
@ -546,7 +547,7 @@ async function getLogisticsExceptions(options = {}) {
|
||||
interceptPackages: skippedByIntercept.packages,
|
||||
interceptOrders: skippedByIntercept.orders.size,
|
||||
interceptReasons: skippedByIntercept.reasons,
|
||||
// 转仓代发(转x00005)—— 同上,只摘异常、保留发货分母
|
||||
// 转仓代发(转x00005)—— 整单不计:既不算异常,也不进原仓发货分母
|
||||
transferPackages: skippedByTransfer.packages,
|
||||
transferOrders: skippedByTransfer.orders.size,
|
||||
transferReasons: skippedByTransfer.reasons,
|
||||
|
||||
@ -555,9 +555,9 @@ test('转仓标签识别:转x00005(含大小写/空格)都认,转空包
|
||||
assert.equal(registry.matchTransferWarehouse(null), null);
|
||||
});
|
||||
|
||||
test('转仓代发不算异常,但仍计入发货分母', async () => {
|
||||
test('转仓代发整单不计:不算异常,也不进原仓发货分母', async () => {
|
||||
const mixed = [
|
||||
// 30:转x00005 · 未揽收 → 崔桥下单、曹县代发,本仓永远「未揽收」,必须不报异常
|
||||
// 30:转x00005 · 未揽收 → 崔桥下单、曹县代发,货不是从原仓走的,整单不计
|
||||
mk({ inner_order_id: 30, so_id: 'SO-30', logistic_status_name: '未揽收', logistic_time: null,
|
||||
logistic_company_name: '曹县韵达小件', order_labels: '["京喜","转x00005"]' }),
|
||||
// 31:同样「未揽收」但没转仓标签 → 必须留下
|
||||
@ -571,7 +571,13 @@ test('转仓代发不算异常,但仍计入发货分母', async () => {
|
||||
assert.equal(data.exclusions.transferPackages, 1);
|
||||
assert.equal(data.exclusions.transferReasons['转x00005'], 1);
|
||||
assert.equal(data.summary.exceptionPackages, 1, '转仓单不计异常');
|
||||
assert.equal(data.summary.packageCount, 2, '转仓单仍计入发货分母(货确实发了)');
|
||||
// 🔥 业务 2026-10-24:货不从原仓走,原仓发货量也不该算
|
||||
assert.equal(data.summary.packageCount, 1, '转仓单不进原仓发货分母');
|
||||
assert.equal(data.summary.shippedOrders, 1, '转仓单不算原仓发货单');
|
||||
// 仓维度:崔桥仓发货单数也只剩 31 那单(30 是转仓、不计)
|
||||
const center = data.centers.find((c) => c.key === 'CQ');
|
||||
assert.equal(center.shippedOrders, 1, '崔桥仓发货单数不应包含转仓单');
|
||||
assert.equal(center.exceptionOrders, 1, '崔桥仓异常只算 31');
|
||||
}, mixed);
|
||||
});
|
||||
|
||||
|
||||
@ -780,14 +780,15 @@ function WarehouseLogisticsException() {
|
||||
: null}
|
||||
;<b>{exclusions?.orderTypeLabels?.length ? exclusions.orderTypeLabels.join(' / ') : '刷单 / 信誉单'}</b>
|
||||
(按订单标签「刷单」「转空包」识别)—— 都不是真实发货,不用跟物流;
|
||||
<b>现场拦截</b>(售后单有签收单号且实退数量 > 0,货已被售后仓签收)—— 物流环节已结束,不算异常。
|
||||
<b>现场拦截</b>(售后单有签收单号且实退数量 > 0,货已被售后仓签收)—— 物流环节已结束,不算异常;
|
||||
<b>转仓代发</b>(标签如「转x00005」,货由目标仓发出)—— 不算本仓异常,也不算本仓发货量。
|
||||
{exclusions && (exclusions.carrierOrders > 0 || exclusions.voidedOrders > 0
|
||||
|| exclusions.nonRealOrders > 0) ? (
|
||||
|| exclusions.nonRealOrders > 0 || exclusions.transferOrders > 0) ? (
|
||||
<>
|
||||
{' '}本批已剔除 <b>{(exclusions.carrierOrders || 0) + (exclusions.voidedOrders || 0)
|
||||
+ (exclusions.nonRealOrders || 0)}</b> 单
|
||||
+ (exclusions.nonRealOrders || 0) + (exclusions.transferOrders || 0)}</b> 单
|
||||
(承运商 {exclusions.carrierOrders || 0} 单、已终止 {exclusions.voidedOrders || 0} 单、
|
||||
刷单/信誉单 {exclusions.nonRealOrders || 0} 单),
|
||||
刷单/信誉单 {exclusions.nonRealOrders || 0} 单、转仓代发 {exclusions.transferOrders || 0} 单),
|
||||
{exclusions.carrierReasons && Object.keys(exclusions.carrierReasons).length ? (
|
||||
<>
|
||||
其中承运商剔除:<b>
|
||||
@ -815,22 +816,6 @@ function WarehouseLogisticsException() {
|
||||
—— 货是被快递当场拦下原车拉回、由售后仓签收的,<b>仍计入发货分母、只是不再报异常</b>。
|
||||
</>
|
||||
) : null}
|
||||
{exclusions?.transferOrders > 0 ? (
|
||||
<>
|
||||
{' '}另有 <b>{exclusions.transferOrders}</b> 单属<b>转仓代发</b>
|
||||
{exclusions.transferReasons && Object.keys(exclusions.transferReasons).length ? (
|
||||
<>(
|
||||
{Object.entries(exclusions.transferReasons)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([k, v]) => `标签${k} ${v} 单`)
|
||||
.join(' / ')}
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
—— 记账仓下单、实际由目标仓发出,本仓物流状态必然停在「未揽收」,
|
||||
<b>仍计入发货分母、只是不再报异常</b>。
|
||||
</>
|
||||
) : null}
|
||||
<br />
|
||||
页面按<b>当前物流快照</b>计算:核验时刻之后才更新的轨迹(比如 9 点后才揽收、才签收)会让对应异常自动消失,
|
||||
所以这里的数字是「到此刻为止还异常的」—— 想留档 09:00 那一刻的结论,需要每天 9 点落一份快照。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user