133 lines
5.5 KiB
PHP
133 lines
5.5 KiB
PHP
<?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;
|
||
}
|
||
}
|