fix(发货数据): 出库单实发明细与旧记录条码不同时以出库单为准覆盖 #835
@ -78,6 +78,12 @@ class ShippingReplicaSynchronizer
|
||||
/** 本轮因商品已更正(新旧条码完全不同)而覆盖旧记录的包裹数;旧金额属于错配商品,不再阻止更新。 */
|
||||
private int $packagesItemsCorrected = 0;
|
||||
|
||||
/** 当前正在写入的包裹,商品是否取自出库单实发明细(发货数据视图)。 */
|
||||
private bool $itemsFromView = false;
|
||||
|
||||
/** @var array<int,bool> 待替换包裹 => 商品取自出库单实发明细 */
|
||||
private array $replacementFromView = [];
|
||||
|
||||
/** 本轮按条码沿用旧金额的明细行数(商品行 ID 变化但条码相同)。 */
|
||||
private int $amountsMatchedByBarcode = 0;
|
||||
private int $staleMirrorPackages = 0;
|
||||
@ -340,6 +346,8 @@ class ShippingReplicaSynchronizer
|
||||
$this->replacementHasFreight = [];
|
||||
$this->replacementHasItems = [];
|
||||
$this->replacementSyntheticSkuIds = [];
|
||||
$this->replacementFromView = [];
|
||||
$this->itemsFromView = false;
|
||||
$this->replacementRowCount = 0;
|
||||
$this->skipFreight = false;
|
||||
$this->replaceWindow = false;
|
||||
@ -970,6 +978,7 @@ class ShippingReplicaSynchronizer
|
||||
}
|
||||
$virtualEmitted++;
|
||||
}
|
||||
$this->itemsFromView = true;
|
||||
$total += $this->emitPackage(self::OUTBOUND_OFFSET + (int) $io, $pkg, $viewItems[$io], $skus, $freight, $so, $tracking !== '' ? $tracking : null, $now, true);
|
||||
$this->markDone($io, $tracking);
|
||||
$this->markTrackSo($so, $tracking);
|
||||
@ -1077,6 +1086,7 @@ class ShippingReplicaSynchronizer
|
||||
'weight' => null,
|
||||
];
|
||||
$pkg = $this->withViewAddr($io, $pkg);
|
||||
$this->itemsFromView = true;
|
||||
$total += $this->emitPackage(self::PURCHASE_OUT_OFFSET + (int) $io, $pkg, $viewItems[$io], $skus, [], $so, $tracking, $now, true);
|
||||
$this->markDone($io, (string) $tracking);
|
||||
$emitted++;
|
||||
@ -1614,7 +1624,10 @@ class ShippingReplicaSynchronizer
|
||||
*/
|
||||
private function packageItems(string $io, array $viewItems, array $byOid, array $bySo, array $dws, string $oid, string $so, string $tracking): array
|
||||
{
|
||||
$this->itemsFromView = false;
|
||||
if ($io !== '' && ! empty($viewItems[$io])) {
|
||||
$this->itemsFromView = true;
|
||||
|
||||
return $viewItems[$io];
|
||||
}
|
||||
$tk = ltrim(trim($tracking), '@');
|
||||
@ -2304,6 +2317,8 @@ class ShippingReplicaSynchronizer
|
||||
/** @param list<array<string,mixed>> $rows */
|
||||
private function writePackage(int $sourceId, array $rows, bool $hasFreight, bool $hasItems, array $syntheticSkuIds): void
|
||||
{
|
||||
$fromView = $this->itemsFromView;
|
||||
$this->itemsFromView = false;
|
||||
if (! $this->replaceWindow) {
|
||||
foreach ($rows as $row) {
|
||||
$this->push($sourceId, (int) $row['sku_source_id'], $row);
|
||||
@ -2316,6 +2331,7 @@ class ShippingReplicaSynchronizer
|
||||
$this->replacementHasFreight[$sourceId] = $hasFreight;
|
||||
$this->replacementHasItems[$sourceId] = $hasItems;
|
||||
$this->replacementSyntheticSkuIds[$sourceId] = $syntheticSkuIds;
|
||||
$this->replacementFromView[$sourceId] = $fromView;
|
||||
$this->replacementRowCount += count($rows);
|
||||
if ($this->replacementRowCount >= self::FLUSH) {
|
||||
$this->flushReplacements();
|
||||
@ -2340,7 +2356,8 @@ class ShippingReplicaSynchronizer
|
||||
$hasFreight = $this->replacementHasFreight;
|
||||
$hasItems = $this->replacementHasItems;
|
||||
$syntheticSkuIds = $this->replacementSyntheticSkuIds;
|
||||
$this->db->transaction(function () use ($packages, $hasFreight, $hasItems, $syntheticSkuIds): void {
|
||||
$fromView = $this->replacementFromView;
|
||||
$this->db->transaction(function () use ($packages, $hasFreight, $hasItems, $syntheticSkuIds, $fromView): void {
|
||||
$ids = array_keys($packages);
|
||||
$previous = [];
|
||||
$previousItems = [];
|
||||
@ -2457,6 +2474,12 @@ class ShippingReplicaSynchronizer
|
||||
// 新旧商品完全不同:旧记录是错配商品(如把同订单其它包裹的商品挂到本单号),旧金额不属于本包裹,按新商品覆盖
|
||||
$unsafeAmount = false;
|
||||
$this->packagesItemsCorrected++;
|
||||
} elseif ($unsafeAmount && ($fromView[$id] ?? false) && $oldBarcodes !== [] && $newBarcodes !== []
|
||||
&& $oldBarcodes != $newBarcodes) {
|
||||
// 新商品取自出库单实发明细且条码集合与旧记录不同(旧记录多为订单商品兜底:整件/原商品/配件母件等):
|
||||
// 以出库单为准覆盖;条码相同的行已按条码沿用旧金额,其余旧金额属于错配商品,不再保留。
|
||||
$unsafeAmount = false;
|
||||
$this->packagesItemsCorrected++;
|
||||
}
|
||||
if ($unsafeAmount) {
|
||||
$this->packagesRetainedForUnmatchedAmount++;
|
||||
@ -2504,6 +2527,7 @@ class ShippingReplicaSynchronizer
|
||||
$this->replacementHasFreight = [];
|
||||
$this->replacementHasItems = [];
|
||||
$this->replacementSyntheticSkuIds = [];
|
||||
$this->replacementFromView = [];
|
||||
$this->replacementRowCount = 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1587,6 +1587,49 @@ class ShippingReplicaSynchronizerTest extends TestCase
|
||||
$this->assertSame(2, $db->table('jst_oms_shipping')->where('source_id', 9004)->count());
|
||||
}
|
||||
|
||||
public function test_outbound_view_items_replace_stale_order_fallback_items_when_barcodes_differ(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
// 旧记录是订单商品兜底(配件母件/原商品,数量0但带金额),与出库单实发明细部分重叠
|
||||
foreach ([
|
||||
['source_id' => 7000, 'sku_source_id' => 700001, 'ship_time' => '2026-09-02 12:00:00', 'express_no' => 'DPK7',
|
||||
'barcode' => 'XG017603N113V06-11', 'product_name' => '订单商品-11', 'qty' => 0, 'amount' => 12.50],
|
||||
['source_id' => 7000, 'sku_source_id' => 700002, 'ship_time' => '2026-09-02 12:00:00', 'express_no' => 'DPK7',
|
||||
'barcode' => 'XG017603N113V06-32', 'product_name' => '订单商品-32', 'qty' => 0, 'amount' => 3.00],
|
||||
] as $row) {
|
||||
$db->table('jst_oms_shipping')->insert($row);
|
||||
}
|
||||
$upstream = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$upstream->method('select')->willReturnCallback(function (string $sql): array {
|
||||
if (str_contains($sql, 'FROM rpt.jst_order_express_details e')) {
|
||||
return [['id' => 7000, 'inner_order_id' => 'order-7', 'logistic_id' => 'DPK7', 'send_time' => '2026-09-02 12:00:00']];
|
||||
}
|
||||
if (str_contains($sql, 'FROM rpt.jst_salesOutboundInfo WHERE io_date')) {
|
||||
return [['o_id' => 'order-7', 'io_id' => 'outbound-7', 'l_id' => 'DPK7']];
|
||||
}
|
||||
if (str_contains($sql, 'FROM rpt.Jst_SendData_View_extension')) {
|
||||
return [
|
||||
['io_id' => 'outbound-7', 'sku_ids' => 'XG017603N113V06-11', 'qty' => 1, 'type' => 0, 'sku_name' => '实发配件-11', 'volume' => null],
|
||||
['io_id' => 'outbound-7', 'sku_ids' => 'XG017603N113V06-40', 'qty' => 1, 'type' => 0, 'sku_name' => '实发配件-40', 'volume' => null],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->method('forSource')->with('operations_replica')->willReturn($upstream);
|
||||
|
||||
$result = (new ShippingReplicaSynchronizer($factory))->sync(Carbon::parse('2026-09-01'), Carbon::parse('2026-09-05'));
|
||||
|
||||
$this->assertSame(0, $result['packages_retained_for_unmatched_amount']);
|
||||
$this->assertSame(1, $result['packages_items_corrected']);
|
||||
$rows = $db->table('jst_oms_shipping')->where('source_id', 7000)->orderBy('barcode')->get();
|
||||
$this->assertSame(['XG017603N113V06-11', 'XG017603N113V06-40'], $rows->pluck('barcode')->all());
|
||||
$this->assertEquals(12.50, $rows[0]->amount);
|
||||
$this->assertNull($rows[1]->amount);
|
||||
$this->assertSame(['1/2'], $rows->pluck('pkg_count')->unique()->values()->all());
|
||||
}
|
||||
|
||||
private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void
|
||||
{
|
||||
$reflection = new \ReflectionProperty($synchronizer, $property);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user