parent
c78ce5e00c
commit
9a9e01461c
@ -180,7 +180,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
$this->info('订单侧候选单号:' . count($candidates));
|
||||
|
||||
/** @var array<string,list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,properties_value:string,src_combine_sku_id:string}>> $lines */
|
||||
/** @var array<string,list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,outer_oi_id:string,properties_value:string,src_combine_sku_id:string}>> $lines */
|
||||
$lines = [];
|
||||
$excludedSkuSql = "'" . implode("','", self::EXCLUDED_SKUS) . "'";
|
||||
foreach (self::SOURCES as $source) {
|
||||
@ -194,6 +194,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
. " CONCAT(o.o_id, '-', i.oi_id) as row_key, "
|
||||
. " i.sku_id as sku_id, i.qty as qty, i.amount as amount, o.status as status, "
|
||||
. " i.shop_i_id as shop_i_id, i.shop_sku_id as shop_sku_id, "
|
||||
. " i.outer_oi_id as outer_oi_id, "
|
||||
. " i.properties_value as properties_value, i.src_combine_sku_id as src_combine_sku_id
|
||||
FROM tmp_cost_order_nos t
|
||||
STRAIGHT_JOIN {$source['orders']} o ON o.so_id = t.no
|
||||
@ -257,6 +258,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
'row_key' => (string) $row->row_key,
|
||||
'shop_i_id' => trim((string) ($row->shop_i_id ?? '')),
|
||||
'shop_sku_id' => trim((string) ($row->shop_sku_id ?? '')),
|
||||
'outer_oi_id' => trim((string) ($row->outer_oi_id ?? '')),
|
||||
'properties_value' => trim((string) ($row->properties_value ?? '')),
|
||||
'src_combine_sku_id' => mb_strtoupper(trim((string) ($row->src_combine_sku_id ?? ''))),
|
||||
];
|
||||
@ -294,6 +296,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
$matchedPriceRows = 0;
|
||||
$placeholderExcludedRows = 0;
|
||||
$bundleParentExcludedRows = 0;
|
||||
$plusParentExcludedRows = 0;
|
||||
$autoReplacedRows = 0;
|
||||
$mappingAmbiguousRows = 0;
|
||||
$mappingMissingRows = 0;
|
||||
@ -307,13 +310,16 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
$excludedPlaceholders = 0;
|
||||
$excludedBundleParents = 0;
|
||||
$excludedPlusParents = 0;
|
||||
$costLines = $this->costLines(
|
||||
$lines[$core] ?? [],
|
||||
$excludedPlaceholders,
|
||||
$excludedBundleParents,
|
||||
$excludedPlusParents,
|
||||
);
|
||||
$placeholderExcludedRows += $excludedPlaceholders;
|
||||
$bundleParentExcludedRows += $excludedBundleParents;
|
||||
$plusParentExcludedRows += $excludedPlusParents;
|
||||
$mapped = $this->mapPlatformSkus($costLines, $platformSkuMapper);
|
||||
$costLines = $mapped['lines'];
|
||||
$autoReplacedRows += $mapped['auto_replaced_rows'];
|
||||
@ -427,10 +433,11 @@ class SyncStoreReceiptProductCost extends Command
|
||||
));
|
||||
$pendingCostQuantity = array_sum(array_column($pendingCostRows, 'product_quantity'));
|
||||
$this->info(sprintf(
|
||||
' 正常匹配单价:%d 行;排除平台占位行:%d 行;排除组合装主行:%d 行;待核验成本:%d 行 / %s 件',
|
||||
' 正常匹配单价:%d 行;排除平台占位行:%d 行;排除组合装主行:%d 行;排除加号组合父行:%d 行;待核验成本:%d 行 / %s 件',
|
||||
$matchedPriceRows,
|
||||
$placeholderExcludedRows,
|
||||
$bundleParentExcludedRows,
|
||||
$plusParentExcludedRows,
|
||||
$missingPriceRows,
|
||||
number_format((float) $pendingCostQuantity, 2),
|
||||
));
|
||||
@ -452,7 +459,8 @@ class SyncStoreReceiptProductCost extends Command
|
||||
'normal_matched_rows' => $matchedPriceRows,
|
||||
'placeholder_excluded_rows' => $placeholderExcludedRows,
|
||||
'bundle_parent_excluded_rows' => $bundleParentExcludedRows,
|
||||
'excluded_rows' => $placeholderExcludedRows + $bundleParentExcludedRows,
|
||||
'plus_parent_excluded_rows' => $plusParentExcludedRows,
|
||||
'excluded_rows' => $placeholderExcludedRows + $bundleParentExcludedRows + $plusParentExcludedRows,
|
||||
'auto_replaced_platform_sku_rows' => $autoReplacedRows,
|
||||
'platform_sku_mapping_ambiguous_rows' => $mappingAmbiguousRows,
|
||||
'platform_sku_mapping_missing_rows' => $mappingMissingRows,
|
||||
@ -664,6 +672,21 @@ class SyncStoreReceiptProductCost extends Command
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $row */
|
||||
private function plusCombinationGroupKey(array $row): ?string
|
||||
{
|
||||
$outerItemId = trim((string) ($row['outer_oi_id'] ?? ''));
|
||||
$shopSkuId = trim((string) ($row['shop_sku_id'] ?? ''));
|
||||
if ($outerItemId === '' || $shopSkuId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $row['source']
|
||||
. "\0" . (string) $row['o_id']
|
||||
. "\0" . $outerItemId
|
||||
. "\0" . $shopSkuId;
|
||||
}
|
||||
|
||||
private function pendingCodeType(string $code): string
|
||||
{
|
||||
$code = mb_strtoupper(trim($code));
|
||||
@ -698,17 +721,19 @@ class SyncStoreReceiptProductCost extends Command
|
||||
* 如果带仓库的行全部是 Cancelled,则整单不参与商品成本;
|
||||
* 4) 排除 Cancelled 行,同一订单的多条末级行按「发货仓库 + 商品编码」汇总数量。
|
||||
*
|
||||
* @param list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,properties_value:string,src_combine_sku_id:string}> $rows
|
||||
* @param list<array{source:string,sku:string,qty:float,amount:float,warehouse:string,o_id:string,parent_o_id:string,row_key:string,status:string,shop_i_id:string,shop_sku_id:string,outer_oi_id:string,properties_value:string,src_combine_sku_id:string}> $rows
|
||||
* @return list<array{sku:string,qty:float,amount:float,warehouse:string,shop_i_id:string,shop_sku_id:string,properties_value:string}>
|
||||
*/
|
||||
private function costLines(
|
||||
array $rows,
|
||||
?int &$placeholderExcludedRows = null,
|
||||
?int &$bundleParentExcludedRows = null,
|
||||
?int &$plusParentExcludedRows = null,
|
||||
): array
|
||||
{
|
||||
$placeholderExcludedRows = 0;
|
||||
$bundleParentExcludedRows = 0;
|
||||
$plusParentExcludedRows = 0;
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
@ -720,7 +745,37 @@ class SyncStoreReceiptProductCost extends Command
|
||||
$unique[$key] = $row;
|
||||
}
|
||||
|
||||
// 2) 组合装拆分后的组件会记录 src_combine_sku_id。源订单上的组合装主行
|
||||
// 2) 上游有些平台组合会同时返回「+ 拼接父行」和多条组件行。
|
||||
// 只有同一订单、outer_oi_id、shop_sku_id 下至少有两条组件,且组件金额
|
||||
// 合计与父行一致时,父行才是可确认的汇总占位,才允许排除。
|
||||
$plusParents = [];
|
||||
$plusComponents = [];
|
||||
foreach ($unique as $key => $row) {
|
||||
$groupKey = $this->plusCombinationGroupKey($row);
|
||||
if ($groupKey === null) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains((string) $row['sku'], '+')) {
|
||||
$plusParents[$key] = $groupKey;
|
||||
continue;
|
||||
}
|
||||
$plusComponents[$groupKey]['rows'] = ($plusComponents[$groupKey]['rows'] ?? 0) + 1;
|
||||
$plusComponents[$groupKey]['amount'] = ($plusComponents[$groupKey]['amount'] ?? 0.0)
|
||||
+ (float) $row['amount'];
|
||||
}
|
||||
foreach ($plusParents as $key => $groupKey) {
|
||||
$component = $plusComponents[$groupKey] ?? null;
|
||||
if (
|
||||
$component !== null
|
||||
&& $component['rows'] >= 2
|
||||
&& abs(round((float) $component['amount'], 2) - round((float) $unique[$key]['amount'], 2)) <= 0.01
|
||||
) {
|
||||
$plusParentExcludedRows++;
|
||||
unset($unique[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 组合装拆分后的组件会记录 src_combine_sku_id。源订单上的组合装主行
|
||||
// 只是汇总占位,不能与组件同时计入成本。
|
||||
$bundleParents = [];
|
||||
foreach ($unique as $row) {
|
||||
@ -738,7 +793,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 平台原始商品编码通常是一长串数字。同一订单、同一 shop_i_id
|
||||
// 4) 平台原始商品编码通常是一长串数字。同一订单、同一 shop_i_id
|
||||
// 如果已经有内部商品编码,则数字行只是平台占位,不参与成本。
|
||||
$internalSiblings = [];
|
||||
foreach ($unique as $row) {
|
||||
@ -761,7 +816,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 组合装条码(ZH-/MKH-条码-数量-条码-数量)按子订单拆成组件行:
|
||||
// 5) 组合装条码(ZH-/MKH-条码-数量-条码-数量)按子订单拆成组件行:
|
||||
// 一个订单有几个发货仓库、商品编码、数量就分开列,组件数量 = 明细数量 × 组件倍数。
|
||||
// 解析不了的组合条码(条码被截断等)保持原样,避免丢行。
|
||||
$expanded = [];
|
||||
@ -780,7 +835,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
return [];
|
||||
}
|
||||
|
||||
// 5) 只保留叶子行:o_id 没有被同组其他行的 link_o_id 引用的,才是实际发货行。
|
||||
// 6) 只保留叶子行:o_id 没有被同组其他行的 link_o_id 引用的,才是实际发货行。
|
||||
$parentIds = [];
|
||||
foreach ($unique as $row) {
|
||||
$parent = trim((string) ($row['parent_o_id'] ?? ''));
|
||||
@ -799,7 +854,7 @@ class SyncStoreReceiptProductCost extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// 6) 组内只要出现带仓库的行,空仓库行就是汇总/占位行,直接丢弃。
|
||||
// 7) 组内只要出现带仓库的行,空仓库行就是汇总/占位行,直接丢弃。
|
||||
$hasWarehouse = false;
|
||||
foreach ($unique as $row) {
|
||||
if (trim((string) $row['warehouse']) !== '') {
|
||||
@ -814,13 +869,13 @@ class SyncStoreReceiptProductCost extends Command
|
||||
);
|
||||
}
|
||||
|
||||
// 7) 排除已取消行;若带仓库行全部取消,上面已把空仓库汇总行丢掉,这里结果为空 → 整单不计。
|
||||
// 8) 排除已取消行;若带仓库行全部取消,上面已把空仓库汇总行丢掉,这里结果为空 → 整单不计。
|
||||
$unique = array_filter(
|
||||
$unique,
|
||||
static fn (array $row): bool => strtoupper(trim((string) ($row['status'] ?? ''))) !== 'CANCELLED'
|
||||
);
|
||||
|
||||
// 8) 汇总到「发货仓库 + 商品编码」。
|
||||
// 9) 汇总到「发货仓库 + 商品编码」。
|
||||
$aggregated = [];
|
||||
foreach ($unique as $row) {
|
||||
$isPlatformSku = preg_match('/^\d+$/', (string) $row['sku']) === 1;
|
||||
|
||||
@ -117,6 +117,8 @@
|
||||
拆单只取最末级订单行(没有任何下级的叶子行),主单和中间汇总层不参与、避免重复计数;
|
||||
同一单分多仓发货的按末级实际仓库分行;虚拟条码(X0000x)不计入;
|
||||
ZH-、MKH- 开头的组合装按「组件条码 + 数量」拆分,无法解析的组合装保留原编码;
|
||||
商品编码含「+」的组合父行,在同一订单、outer_oi_id、shop_sku_id 下有多条组件且金额合计一致时剔除,仅保留组件行;
|
||||
组件缺失或金额合计不一致时保留父行待核验。
|
||||
套餐/服务、运费及指定配件、组合装占位编码 TCXJ、FUWU、AZF、DF-PJZY0001、
|
||||
JIAXIANGJIANCAIJIAJUZIZHUANG964A、YUNFEI、BCJ 全局不取数(组合装里拆出的这些编码同样剔除)。
|
||||
发货仓库默认排除「常州简尚家居有限公司」「简尚-苏州福利品仓」「简尚-自动发货仓」,
|
||||
|
||||
@ -42,6 +42,34 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
$this->assertSame(2.0, $lines[0]['qty']);
|
||||
}
|
||||
|
||||
public function test_plus_parent_is_removed_when_component_amounts_match(): void
|
||||
{
|
||||
[$lines, $placeholders, $bundles, $plusParents] = $this->costLines([
|
||||
$this->row('plus-parent', 'GBGJ-A+V01+GBGJ-B+V01', '993830851777', 1, 648.06),
|
||||
$this->row('plus-child-a', 'GBGJ-AV01', '993830851777', 1, 450.03),
|
||||
$this->row('plus-child-b', 'GBGJ-BV01', '993830851777', 1, 198.03),
|
||||
]);
|
||||
|
||||
$this->assertSame(0, $placeholders);
|
||||
$this->assertSame(0, $bundles);
|
||||
$this->assertSame(1, $plusParents);
|
||||
$this->assertSame(['GBGJ-AV01', 'GBGJ-BV01'], array_column($lines, 'sku'));
|
||||
}
|
||||
|
||||
public function test_plus_parent_remains_when_component_amounts_do_not_match(): void
|
||||
{
|
||||
[$lines, $placeholders, $bundles, $plusParents] = $this->costLines([
|
||||
$this->row('plus-parent', 'GBGJ-A+V01+GBGJ-B+V01', '993830851777', 1, 1142),
|
||||
$this->row('plus-child-a', 'GBGJ-AV01', '993830851777', 1, 255.20),
|
||||
$this->row('plus-child-b', 'GBGJ-BV01', '993830851777', 1, 328.56),
|
||||
]);
|
||||
|
||||
$this->assertSame(0, $placeholders);
|
||||
$this->assertSame(0, $bundles);
|
||||
$this->assertSame(0, $plusParents);
|
||||
$this->assertContains('GBGJ-A+V01+GBGJ-B+V01', array_column($lines, 'sku'));
|
||||
}
|
||||
|
||||
public function test_unique_platform_sku_is_replaced_after_line_filtering(): void
|
||||
{
|
||||
[$lines] = $this->costLines([
|
||||
@ -106,7 +134,7 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
$this->assertSame(552392.0, $result['cost']);
|
||||
}
|
||||
|
||||
/** @return array{0:list<array<string,mixed>>,1:int,2:int} */
|
||||
/** @return array{0:list<array<string,mixed>>,1:int,2:int,3:int} */
|
||||
private function costLines(array $rows): array
|
||||
{
|
||||
$command = new SyncStoreReceiptProductCost();
|
||||
@ -114,9 +142,10 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
$method->setAccessible(true);
|
||||
$placeholders = 0;
|
||||
$bundles = 0;
|
||||
$lines = $method->invokeArgs($command, [$rows, &$placeholders, &$bundles]);
|
||||
$plusParents = 0;
|
||||
$lines = $method->invokeArgs($command, [$rows, &$placeholders, &$bundles, &$plusParents]);
|
||||
|
||||
return [$lines, $placeholders, $bundles];
|
||||
return [$lines, $placeholders, $bundles, $plusParents];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
@ -140,6 +169,7 @@ class StoreReceiptProductCostLineTest extends TestCase
|
||||
'status' => 'Sent',
|
||||
'shop_i_id' => $shopItemId,
|
||||
'shop_sku_id' => '3533103677164802',
|
||||
'outer_oi_id' => 'OUTER-TEST-1',
|
||||
'properties_value' => '颜色分类:六格-90*30*75cm-木纹胡桃色?赠送后背板',
|
||||
'src_combine_sku_id' => $bundleParent,
|
||||
];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user