商品成本:拆单订单有子单时只取子单,避免主单与子单重复计

聚水潭拆单后主单保留原始下单行、子单才是实际发货行;之前按「商品+数量+金额+仓库」去重,
主单 10 件与子单各 1 件键不同,会重复累加。改为:有子单时只取子单(按 o_id+oi_id 去重),
整单没有子单才用主单;再按「发货仓库 + 商品编码」汇总数量。

回归样例 5122123095012119232:31202C 计 10 件(10 个子单各 1 件)、DNZ032916N887V01 计 10 件。
This commit is contained in:
chenxi 2026-10-19 16:31:13 +08:00
parent c44d267902
commit e0abc6732b

View File

@ -166,7 +166,7 @@ class SyncStoreReceiptProductCost extends Command
}
$this->info('订单侧候选单号:' . count($candidates));
/** @var array<string,list<array{sku:string,qty:float,amount:float,warehouse:string}>> $lines */
/** @var array<string,list<array{sku:string,qty:float,amount:float,warehouse:string,is_main:bool,row_key:string}>> $lines */
$lines = [];
foreach (self::SOURCES as $source) {
try {
@ -174,6 +174,8 @@ class SyncStoreReceiptProductCost extends Command
"SELECT o.so_id as so_id, "
. $source['warehouse'] . " as warehouse_code, "
. " COALESCE(wh.name, '') as warehouse_name, "
. " CASE WHEN o.link_o_id IS NULL OR o.link_o_id = 0 THEN 1 ELSE 0 END as is_main, "
. " CONCAT(o.o_id, '-', i.oi_id) as row_key, "
. " i.sku_id as sku_id, i.qty as qty, i.amount as amount
FROM tmp_cost_order_nos t
STRAIGHT_JOIN {$source['orders']} o ON o.so_id = t.no
@ -221,6 +223,9 @@ class SyncStoreReceiptProductCost extends Command
// 组合装明细金额对应整包,拆到组件后不再参与「数量×单价 vs 明细金额」校验。
'amount' => $isComponent ? 0.0 : $amount,
'warehouse' => $warehouse,
// 拆单后子单(link_o_id 非空)才是实际发货行;主单只是原始下单记录。
'is_main' => (int) $row->is_main === 1,
'row_key' => (string) $row->row_key,
];
}
}
@ -348,11 +353,15 @@ class SyncStoreReceiptProductCost extends Command
}
/**
* 拆单去重:聚水潭的拆单子单会把主单的商品行再列一遍,同一批商品只算一次;
* 同一商品在同一订单里既有真实发货仓库行、又有无仓库的占位行时,丢掉占位行,
* 这样「同一单分多仓发货」会按子单的实际仓库分行。
* 拆单取数:聚水潭拆单后,主单(link_o_id 为空)保留的是原始下单行,
* 子单(link_o_id 指向主单)才是实际发货行。一个订单只要出现子单,就只取子单,
* 否则主单和子单会被各算一遍(主单的原始商品 + 子单的实际发货商品)。
* 没有子单的订单才用主单行。
*
* @param list<array{sku:string,qty:float,amount:float,warehouse:string}> $rows
* 同一订单的多条子单(不同仓库 / 分批发货)按「发货仓库 + 商品编码」汇总,数量相加,
* 这样「一个订单有几个发货仓库、商品编码、数量」会分开列。
*
* @param list<array{sku:string,qty:float,amount:float,warehouse:string,is_main:bool,row_key:string}> $rows
* @return list<array{sku:string,qty:float,amount:float,warehouse:string}>
*/
private function costLines(array $rows): array
@ -361,25 +370,23 @@ class SyncStoreReceiptProductCost extends Command
return [];
}
// 1) 完全相同的行(商品 + 数量 + 金额 + 仓库)只保留一条。
// 1) 先按订单明细主键去掉上游的完全重复行(同一 o_id + oi_id 只算一次)。
$unique = [];
foreach ($rows as $row) {
$key = $row['sku'] . "\0" . $row['qty'] . "\0" . $row['amount'] . "\0" . $row['warehouse'];
$key = $row['row_key'] . "\0" . $row['sku'];
$unique[$key] = $row;
}
// 2) 有真实仓库的同一(商品 + 数量 + 金额),优先于无仓库的占位行。
$withWarehouse = [];
// 2) 有子单就只取子单(实际发货行);整单没有子单才用主单行。
$hasSubOrder = false;
foreach ($unique as $row) {
if ($row['warehouse'] !== '') {
$withWarehouse[$row['sku'] . "\0" . $row['qty'] . "\0" . $row['amount']] = true;
if (! $row['is_main']) {
$hasSubOrder = true;
break;
}
}
foreach ($unique as $key => $row) {
if ($row['warehouse'] === ''
&& isset($withWarehouse[$row['sku'] . "\0" . $row['qty'] . "\0" . $row['amount']])) {
unset($unique[$key]);
}
if ($hasSubOrder) {
$unique = array_filter($unique, static fn (array $row): bool => ! $row['is_main']);
}
// 3) 汇总到「发货仓库 + 商品编码」。