diff --git a/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php b/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php index cad1c903..06258fe7 100644 --- a/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php +++ b/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php @@ -9,7 +9,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class SupplierComparisonCsvExporter { private const HEADERS = [ - '采购单号', '日期', '条码', '厂家条码', '比对结果', + '采购单号', '日期', '我方商品编码', '厂家条码', '比对结果', '我方数量', '我方单价', '我方金额', '厂家数量', '厂家单价', '厂家金额', '数量差异', '金额差异', '结算口径', '结算金额', diff --git a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php index 2cdad982..d0da1b14 100644 --- a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php +++ b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php @@ -187,7 +187,7 @@ class SupplierDingTalkNotifier $supplierBarcode = trim((string) ($row['supplierBarcode'] ?? '')); $lines[] = sprintf( - '%d. **%s**|采购单号:%s|日期:%s|条码:%s%s', + '%d. **%s**|采购单号:%s|日期:%s|商品编码:%s%s', $rowOffset + $index + 1, (string) ($row['type'] ?? '差异'), $purchaseNo !== '' ? $purchaseNo : '—', diff --git a/modules/EcommerceFinance/Services/SupplierStatementMatcher.php b/modules/EcommerceFinance/Services/SupplierStatementMatcher.php index 8bdb4652..ee0c8596 100644 --- a/modules/EcommerceFinance/Services/SupplierStatementMatcher.php +++ b/modules/EcommerceFinance/Services/SupplierStatementMatcher.php @@ -4,9 +4,9 @@ namespace Modules\EcommerceFinance\Services; /** * 厂家对账单 vs 我方结算数据: - * - 行键 = 采购单号 + 日期 + 条码(条码去掉厂家编号/版本后缀); + * - 行键 = 采购单号 + 日期 + 我方商品编码(对应厂家条码去掉厂家编号/版本后缀); * - 同一行键的多行先合并数量与金额,再比较双方数量和金额; - * - 厂家未填采购单号时,仅在同日期+条码能唯一对应我方一条数据时兜底匹配。 + * - 厂家未填采购单号时,仅在同日期+商品编码能唯一对应我方一条数据时兜底匹配。 */ class SupplierStatementMatcher { @@ -22,16 +22,16 @@ class SupplierStatementMatcher private const SUPPLIER_SUFFIX = '/[A-Z]{1,3}\d{3,6}V\d{1,3}$/i'; /** - * @param list> $mine 我方数据(purchaseNo/date/skuCode/unitPrice/qty/amount) + * @param list> $mine 我方数据(purchaseNo/date/productCode/unitPrice/qty/amount;productCode 为空才回退货号) * @param list> $theirs 厂家数据(purchaseNo/createdAt/barcode/unitPrice/qty/amount) * @return array{rows:list>,summary:array} */ public function compare(array $mine, array $theirs): array { - $mineGrouped = $this->group($mine, 'skuCode', 'purchaseNo', 'date'); - $theirsGrouped = $this->group($theirs, 'barcode', 'purchaseNo', 'createdAt'); + $mineGrouped = $this->group($mine, ['productCode', 'skuCode'], 'purchaseNo', 'date'); + $theirsGrouped = $this->group($theirs, ['barcode'], 'purchaseNo', 'createdAt'); - // 已有采购单号时严格按三元组匹配;缺失采购单号时,仅在日期+条码唯一时兜底。 + // 已有采购单号时严格按三元组匹配;缺失采购单号时,仅在日期+商品编码唯一时兜底。 $fallbackIndex = []; foreach ($mineGrouped as $key => $row) { $fallbackIndex[$row['date'] . "\0" . $row['barcodeKey']][] = $key; @@ -71,15 +71,15 @@ class SupplierStatementMatcher $quantityDifference = $this->describeDifference($qtyDifference, $amountDifference); $fallbackNote = ''; if (($theirsRow['matchedByFallback'] ?? false) === true) { - $fallbackNote = '厂家未填采购单号,已按日期+条码唯一匹配;'; + $fallbackNote = '厂家未填采购单号,已按日期+商品编码唯一匹配;'; } elseif (($theirsRow['fallbackAmbiguous'] ?? false) === true) { - $fallbackNote = '厂家未填采购单号,且同日期+条码有多条我方数据,无法唯一匹配;'; + $fallbackNote = '厂家未填采购单号,且同日期+商品编码有多条我方数据,无法唯一匹配;'; } if ($mineRow === null) { $type = self::TYPE_SUPPLIER_EXTRA; $settle = 0.0; - $note = $fallbackNote . '厂家整条多报:' . $quantityDifference . ';我方没有该采购单号+日期+条码,不结算'; + $note = $fallbackNote . '厂家整条多报:' . $quantityDifference . ';我方没有该采购单号+日期+商品编码,不结算'; $summary['supplierExtra']++; } elseif ($theirsRow === null) { $type = self::TYPE_SUPPLIER_MISSING; @@ -92,7 +92,7 @@ class SupplierStatementMatcher } elseif (abs($mineAmount - $theirsAmount) < 0.005 && abs($mineQty - $theirsQty) < 0.005) { $type = self::TYPE_MATCH; $settle = $mineAmount; - $note = $fallbackNote . '采购单号、日期、条码、数量和金额一致,正常结算'; + $note = $fallbackNote . '采购单号、日期、商品编码、数量和金额一致,正常结算'; $summary['match']++; } else { $type = self::TYPE_MISMATCH; @@ -152,16 +152,17 @@ class SupplierStatementMatcher } /** - * 按「采购单号 + 日期 + 条码」归组,数量与金额相加。 + * 按「采购单号 + 日期 + 商品编码」归组,数量与金额相加。 * * @param list> $rows + * @param list $barcodeFields * @return array> */ - private function group(array $rows, string $barcodeField, string $purchaseField, string $dateField): array + private function group(array $rows, array $barcodeFields, string $purchaseField, string $dateField): array { $grouped = []; foreach ($rows as $row) { - $barcode = trim((string) ($row[$barcodeField] ?? '')); + $barcode = $this->barcodeValue($row, $barcodeFields); if ($barcode === '') { continue; } @@ -175,9 +176,9 @@ class SupplierStatementMatcher $grouped[$key] ??= [ 'purchaseNo' => $purchaseNo, 'date' => $date, - 'barcode' => $barcodeKey, + 'barcode' => $barcode, 'barcodeKey' => $barcodeKey, - 'supplierBarcode' => $barcodeField === 'barcode' ? $barcode : '', + 'supplierBarcode' => in_array('barcode', $barcodeFields, true) ? $barcode : '', 'unitPrice' => $unitPrice, 'unitPrices' => [], 'qty' => 0.0, @@ -200,6 +201,19 @@ class SupplierStatementMatcher return $grouped; } + /** @param array $row @param list $fields */ + private function barcodeValue(array $row, array $fields): string + { + foreach ($fields as $field) { + $value = trim((string) ($row[$field] ?? '')); + if ($value !== '') { + return $value; + } + } + + return ''; + } + /** * @param array> $groups * @param array> $fallbackIndex diff --git a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php index f4d34be1..46a07812 100644 --- a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php @@ -111,10 +111,10 @@ @endif

- 比对口径:按「采购单号 + 日期 + 条码」核对,同一采购单号、日期和条码的多行先合并数量与金额。 + 比对口径:按「采购单号 + 日期 + 商品编码」核对,以我方货款对账的“商品编码”匹配厂家条码;同一采购单号、日期和商品编码的多行先合并数量与金额。 数量、金额一致 → 正常结算;厂家多(我方没有)→ 不结算、退回修改; 数量或金额不一致 → 明确列出双方数量、单价、金额及厂家多算部分,取金额低的一方结算; - 厂家少(我方有)→ 按厂家数据结算(不付款)。厂家未填采购单号时,仅在同日期+条码能唯一匹配我方一条数据时兜底。
+ 厂家少(我方有)→ 按厂家数据结算(不付款)。厂家未填采购单号时,仅在同日期+商品编码能唯一匹配我方一条数据时兜底。
厂家登录:在「厂家登录账号管理」新增供应商并勾选可登录,供应商名称必须与对账数据中的厂家名称完全一致; 厂家登录后只能查看自己的对账单上传记录、全部退货明细及导出数据。

@@ -280,7 +280,7 @@ 比对结果 采购单号 日期 - 条码 + 我方商品编码 / 厂家条码 我方数量 我方单价 我方金额 diff --git a/tests/Feature/Business/SupplierReconciliationTest.php b/tests/Feature/Business/SupplierReconciliationTest.php index db469863..efacfe85 100644 --- a/tests/Feature/Business/SupplierReconciliationTest.php +++ b/tests/Feature/Business/SupplierReconciliationTest.php @@ -46,7 +46,7 @@ class SupplierReconciliationTest extends FeatureTestCase ->assertSeeText('厂家端数据') ->assertSeeText('厂家多') ->assertSeeText('取金额低的一方结算') - ->assertSeeText('采购单号 + 日期 + 条码') + ->assertSeeText('采购单号 + 日期 + 商品编码') ->assertSee('list="finance-supplier-options"', false) ->assertSeeText('对账人') ->assertSeeText('下载对账单模板'); @@ -251,7 +251,7 @@ class SupplierReconciliationTest extends FeatureTestCase '厂家多,1', '数量/金额不一致,1', '厂家少,0', - '采购单号,日期,条码,厂家条码,比对结果', + '采购单号,日期,我方商品编码,厂家条码,比对结果', 'PO-1', 'PO-2', 'PO-D', @@ -1027,10 +1027,10 @@ class SupplierReconciliationTest extends FeatureTestCase public function test_matcher_normalizes_supplier_barcode_suffix(): void { - // 真实场景:我方货号不带厂家后缀,厂家条码带「厂家编号 + 版本」(CJ019312 对 CJ019312N1097V01) + // 真实场景:我方商品编码与厂家条码的厂家编号/版本后缀不同,去掉后缀后应一致。 $mine = [ - ['purchaseNo' => 'PO-CJ-1', 'date' => '2026-09-30', 'skuCode' => 'CJ019312', 'unitPrice' => '182.5', 'qty' => '130', 'amount' => '23725'], - ['purchaseNo' => 'PO-DNZ-1', 'date' => '2026-09-30', 'skuCode' => 'DNZ037602', 'unitPrice' => '27', 'qty' => '38', 'amount' => '1026'], + ['purchaseNo' => 'PO-CJ-1', 'date' => '2026-09-30', 'skuCode' => 'CJ019312', 'productCode' => 'CJ019312N1178V01', 'unitPrice' => '182.5', 'qty' => '130', 'amount' => '23725'], + ['purchaseNo' => 'PO-DNZ-1', 'date' => '2026-09-30', 'skuCode' => 'DNZ037602', 'productCode' => 'DNZ037602N1178V01', 'unitPrice' => '27', 'qty' => '38', 'amount' => '1026'], ]; $theirs = [ ['purchaseNo' => 'PO-CJ-1', 'createdAt' => '2026-09-30', 'barcode' => 'CJ019312N1097V01', 'unitPrice' => '182.5', 'qty' => '130', 'amount' => '23725'], @@ -1047,6 +1047,33 @@ class SupplierReconciliationTest extends FeatureTestCase $this->assertSame('CJ019312N1097V01', $result['rows'][0]['supplierBarcode'] ?? null); } + public function test_matcher_uses_product_code_instead_of_sku_code(): void + { + $mine = [[ + 'purchaseNo' => 'PO-CODE-1', + 'date' => '2026-09-30', + 'skuCode' => 'SKU-ONLY', + 'productCode' => 'CJ019312N1178V01', + 'unitPrice' => '182.5', + 'qty' => '2', + 'amount' => '365', + ]]; + $theirs = [[ + 'purchaseNo' => 'PO-CODE-1', + 'createdAt' => '2026-09-30', + 'barcode' => 'CJ019312N1097V01', + 'unitPrice' => '182.5', + 'qty' => '2', + 'amount' => '365', + ]]; + + $result = app(SupplierStatementMatcher::class)->compare($mine, $theirs); + + $this->assertSame(1, $result['summary']['match']); + $this->assertSame('CJ019312N1178V01', $result['rows'][0]['barcode']); + $this->assertSame('CJ019312N1097V01', $result['rows'][0]['supplierBarcode']); + } + public function test_matcher_uses_purchase_number_date_and_barcode_as_the_unique_key(): void { $mine = [ @@ -1085,7 +1112,7 @@ class SupplierReconciliationTest extends FeatureTestCase $this->assertSame(1, $result['summary']['match']); $this->assertSame('PO-1', $result['rows'][0]['purchaseNo']); - $this->assertStringContainsString('按日期+条码唯一匹配', $result['rows'][0]['note']); + $this->assertStringContainsString('按日期+商品编码唯一匹配', $result['rows'][0]['note']); } public function test_matcher_does_not_guess_when_missing_purchase_number_is_ambiguous(): void