CaiWuXiTong/modules/EcommerceFinance/Services/SupplierStatementMatcher.php
renshen 2ffb49ca92 供应商对账:条码匹配去掉厂家编号/版本后缀,表头不符提示缺列 (#366)
比对键先去掉厂家编号+版本后缀再匹配(实测雪翔家居文件),页面显示厂家原始条码;表头校验失败列出缺少/多出的列名。
2026-10-17 09:07:16 +08:00

133 lines
5.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Services;
/**
* 厂家对账单 vs 我方结算数据:
* - 行键 = 条码(去掉厂家编号/版本后缀,如 CJ019312N1097V01 → CJ019312)+ 单价
* (同一把钥匙的多行先合并:数量、金额相加);
* - 四类结果:一致 / 厂家多(不结算、退回)/ 金额不一致(按金额低的一方结算)/ 厂家少(按厂家数据结算,即不付款)。
*/
class SupplierStatementMatcher
{
public const TYPE_MATCH = '一致';
public const TYPE_SUPPLIER_EXTRA = '厂家多';
public const TYPE_MISMATCH = '金额不一致';
public const TYPE_SUPPLIER_MISSING = '厂家少';
/** 厂家条码后缀:供应商编号 + 版本,如 N1097V01、N1269V01。 */
private const SUPPLIER_SUFFIX = '/[A-Z]{1,3}\d{3,6}V\d{1,3}$/i';
/**
* @param list<array<string,mixed>> $mine 我方结算数据(货款对账:skuCode/unitPrice/qty/amount)
* @param list<array<string,mixed>> $theirs 厂家对账单(barcode/unitPrice/qty/amount)
* @return array{rows:list<array<string,mixed>>,summary:array<string,mixed>}
*/
public function compare(array $mine, array $theirs): array
{
$mineGrouped = $this->group($mine, 'skuCode');
$theirsGrouped = $this->group($theirs, 'barcode');
$rows = [];
$summary = ['match' => 0, 'supplierExtra' => 0, 'mismatch' => 0, 'supplierMissing' => 0,
'mineAmount' => 0.0, 'settleAmount' => 0.0];
foreach (array_unique(array_merge(array_keys($mineGrouped), array_keys($theirsGrouped))) as $key) {
$mineRow = $mineGrouped[$key] ?? null;
$theirsRow = $theirsGrouped[$key] ?? null;
$barcode = (string) ($mineRow['barcode'] ?? $theirsRow['barcode']);
$supplierBarcode = (string) ($theirsRow['barcode'] ?? '');
$unitPrice = (float) ($mineRow['unitPrice'] ?? $theirsRow['unitPrice']);
$mineQty = (float) ($mineRow['qty'] ?? 0);
$mineAmount = (float) ($mineRow['amount'] ?? 0);
$theirsQty = (float) ($theirsRow['qty'] ?? 0);
$theirsAmount = (float) ($theirsRow['amount'] ?? 0);
if ($mineRow === null) {
$type = self::TYPE_SUPPLIER_EXTRA;
$settle = 0.0;
$note = '我方没有这条数据:不结算,退回厂家修改账单';
$summary['supplierExtra']++;
} elseif ($theirsRow === null) {
$type = self::TYPE_SUPPLIER_MISSING;
$settle = 0.0;
$note = '厂家少报了这条数据:按厂家数据结算(不付款)';
$summary['supplierMissing']++;
} elseif (abs($mineAmount - $theirsAmount) < 0.005 && abs($mineQty - $theirsQty) < 0.005) {
$type = self::TYPE_MATCH;
$settle = $mineAmount;
$note = '正常结算';
$summary['match']++;
} else {
$type = self::TYPE_MISMATCH;
$settle = min($mineAmount, $theirsAmount);
$note = '金额/数量不一致:按金额低的一方结算';
$summary['mismatch']++;
}
$summary['mineAmount'] += $mineAmount;
$summary['settleAmount'] += $settle;
$rows[] = [
'barcode' => $barcode,
'supplierBarcode' => $supplierBarcode === $barcode ? '' : $supplierBarcode,
'unitPrice' => $unitPrice,
'mineQty' => $mineQty,
'mineAmount' => $mineAmount,
'theirsQty' => $theirsQty,
'theirsAmount' => $theirsAmount,
'type' => $type,
'settleAmount' => $settle,
'note' => $note,
];
}
usort($rows, static fn (array $a, array $b): int => [$a['type'] === self::TYPE_MATCH ? 1 : 0, $a['barcode']]
<=> [$b['type'] === self::TYPE_MATCH ? 1 : 0, $b['barcode']]);
$summary['mineAmount'] = round($summary['mineAmount'], 2);
$summary['settleAmount'] = round($summary['settleAmount'], 2);
$summary['differenceAmount'] = round($summary['mineAmount'] - $summary['settleAmount'], 2);
return ['rows' => $rows, 'summary' => $summary];
}
/**
* 按「条码 + 单价」归组,数量与金额相加。
*
* @param list<array<string,mixed>> $rows
* @return array<string,array{barcode:string,unitPrice:float,qty:float,amount:float}>
*/
private function group(array $rows, string $barcodeField): array
{
$grouped = [];
foreach ($rows as $row) {
$barcode = trim((string) ($row[$barcodeField] ?? ''));
if ($barcode === '') {
continue;
}
$unitPrice = (float) ($row['unitPrice'] ?? 0);
$key = $this->normalizeBarcode($barcode) . '|' . number_format($unitPrice, 4, '.', '');
$grouped[$key] ??= ['barcode' => $barcode, 'unitPrice' => $unitPrice, 'qty' => 0.0, 'amount' => 0.0];
$grouped[$key]['qty'] += (float) ($row['qty'] ?? 0);
$grouped[$key]['amount'] += (float) ($row['amount'] ?? 0);
}
return $grouped;
}
/** 去掉条码末尾的「厂家编号 + 版本」后缀,双方口径统一后再匹配。 */
private function normalizeBarcode(string $barcode): string
{
$normalized = preg_replace(self::SUPPLIER_SUFFIX, '', $barcode);
return $normalized === null || $normalized === '' ? $barcode : $normalized;
}
}