diff --git a/app/Http/Controllers/Business/SupplierReconciliation.php b/app/Http/Controllers/Business/SupplierReconciliation.php index 2e634abe..cbae4cf7 100644 --- a/app/Http/Controllers/Business/SupplierReconciliation.php +++ b/app/Http/Controllers/Business/SupplierReconciliation.php @@ -130,7 +130,7 @@ class SupplierReconciliation extends Controller $request->validate([ 'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'], 'reconciler' => ['nullable', 'string', 'max:191'], - 'comparison_filter' => ['nullable', 'in:,supplier_over,supplier_under,mismatch,match'], + 'comparison_filter' => ['nullable', 'in:,unpriced,supplier_over,supplier_under,mismatch,match'], ]); [$supplier, $companyId] = $this->returnTarget($request); $detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->query()); @@ -232,7 +232,7 @@ class SupplierReconciliation extends Controller 'date' => ['nullable', 'string', 'max:32'], 'barcode' => ['nullable', 'string', 'max:191'], 'difference_type' => ['nullable', 'string', 'max:32'], - 'comparison_filter' => ['nullable', 'in:,supplier_over,supplier_under,mismatch,match'], + 'comparison_filter' => ['nullable', 'in:,unpriced,supplier_over,supplier_under,mismatch,match'], 'selected_rows' => ['nullable', 'array', 'max:600'], 'selected_rows.*' => ['string', 'max:2048'], ]); @@ -245,7 +245,10 @@ class SupplierReconciliation extends Controller } $differences = array_values(array_filter( $detail['rows'], - static fn (array $row): bool => ($row['type'] ?? '') !== SupplierStatementMatcher::TYPE_MATCH, + static fn (array $row): bool => ! in_array($row['type'] ?? '', [ + SupplierStatementMatcher::TYPE_MATCH, + SupplierStatementMatcher::TYPE_UNPRICED, + ], true), )); if ($request->input('scope') === 'row') { diff --git a/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php b/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php index 06258fe7..1a8298b0 100644 --- a/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php +++ b/modules/EcommerceFinance/Services/SupplierComparisonCsvExporter.php @@ -10,7 +10,7 @@ class SupplierComparisonCsvExporter { private const HEADERS = [ '采购单号', '日期', '我方商品编码', '厂家条码', '比对结果', - '我方数量', '我方单价', '我方金额', + '我方数量', '我方单价', '我方金额', '我方预估单价', '我方预估金额', '厂家数量', '厂家单价', '厂家金额', '数量差异', '金额差异', '结算口径', '结算金额', ]; @@ -48,6 +48,8 @@ class SupplierComparisonCsvExporter '厂家多', (int) ($summary['supplierExtra'] ?? 0), '数量/金额不一致', (int) ($summary['mismatch'] ?? 0), '厂家少', (int) ($summary['supplierMissing'] ?? 0), + '未定价', (int) ($summary['unpriced'] ?? 0), + '未定价预估金额', $this->money($summary['unpricedGuessAmount'] ?? 0), ]); $this->write($csv, [ '我方金额合计', $this->money($summary['mineAmount'] ?? 0), @@ -69,11 +71,15 @@ class SupplierComparisonCsvExporter $this->number($row['mineQty'] ?? 0), $this->money($row['mineUnitPrice'] ?? 0), $this->money($row['mineAmount'] ?? 0), + $this->money($row['mineGuessUnitPrice'] ?? 0), + $this->money($row['mineGuessAmount'] ?? 0), $this->number($row['theirsQty'] ?? 0), $this->money($row['theirsUnitPrice'] ?? 0), $this->money($row['theirsAmount'] ?? 0), $this->quantityDifference($row['qtyDifference'] ?? 0), - $this->amountDifference($row['amountDifference'] ?? 0), + ($row['type'] ?? '') === SupplierStatementMatcher::TYPE_UNPRICED + ? '未定价' + : $this->amountDifference($row['amountDifference'] ?? 0), $row['note'] ?? '', $this->money($row['settleAmount'] ?? 0), ]); diff --git a/modules/EcommerceFinance/Services/SupplierStatementMatcher.php b/modules/EcommerceFinance/Services/SupplierStatementMatcher.php index 17b34d09..520a6c67 100644 --- a/modules/EcommerceFinance/Services/SupplierStatementMatcher.php +++ b/modules/EcommerceFinance/Services/SupplierStatementMatcher.php @@ -25,6 +25,8 @@ class SupplierStatementMatcher public const TYPE_SUPPLIER_MISSING = '厂家少'; + public const TYPE_UNPRICED = '未定价'; + /** 厂家条码后缀:供应商编号 + 版本,如 N1097V01、N1269V01。 */ private const SUPPLIER_SUFFIX = '/[A-Z]{1,3}\d{3,6}V\d{1,3}$/i'; @@ -170,6 +172,8 @@ class SupplierStatementMatcher $date = $this->normalizeDate($row[$mineSide ? 'date' : 'createdAt'] ?? ''); $expressNo = trim((string) $this->firstValue($row, ['expressNo', 'express_no'])); $unitPrice = (float) ($row['unitPrice'] ?? 0); + $guessUnitPrice = (float) ($row['guessUnitPrice'] ?? $row['guess_unit_price'] ?? 0); + $guessAmount = (float) ($row['guessAmount'] ?? $row['guess_amount'] ?? 0); $grouped[$key] ??= [ 'purchaseNo' => '', @@ -185,6 +189,10 @@ class SupplierStatementMatcher 'expressNos' => [], 'unitPrice' => 0.0, 'unitPrices' => [], + 'guessUnitPrice' => 0.0, + 'guessUnitPrices' => [], + 'guessAmount' => 0.0, + 'unpricedQty' => 0.0, 'qty' => 0.0, 'amount' => 0.0, 'sourceRows' => 0, @@ -202,6 +210,9 @@ class SupplierStatementMatcher if ($unitPrice > 0) { $grouped[$key]['unitPrices'][number_format($unitPrice, 4, '.', '')] = true; } + if ($guessUnitPrice > 0) { + $grouped[$key]['guessUnitPrices'][number_format($guessUnitPrice, 4, '.', '')] = true; + } if ($grouped[$key]['barcode'] === '' && $barcode !== '') { $grouped[$key]['barcode'] = $barcode; } @@ -211,6 +222,10 @@ class SupplierStatementMatcher $grouped[$key]['qty'] += (float) ($row['qty'] ?? 0); $grouped[$key]['amount'] += (float) ($row['amount'] ?? 0); + $grouped[$key]['guessAmount'] += $guessAmount; + if ($mineSide && $unitPrice <= 0 && (float) ($row['amount'] ?? 0) <= 0) { + $grouped[$key]['unpricedQty'] += (float) ($row['qty'] ?? 0); + } $grouped[$key]['sourceRows']++; } @@ -294,6 +309,9 @@ class SupplierStatementMatcher 'supplierExtra' => 0, 'mismatch' => 0, 'supplierMissing' => 0, + 'unpriced' => 0, + 'unpricedQty' => 0.0, + 'unpricedGuessAmount' => 0.0, 'mineAmount' => 0.0, 'theirsAmount' => 0.0, 'supplierOverQty' => 0.0, @@ -313,6 +331,8 @@ class SupplierStatementMatcher $summary['theirsAmount'] = round((float) $summary['theirsAmount'], 2); $summary['supplierOverQty'] = round((float) $summary['supplierOverQty'], 4); $summary['supplierOverAmount'] = round((float) $summary['supplierOverAmount'], 2); + $summary['unpricedQty'] = round((float) $summary['unpricedQty'], 4); + $summary['unpricedGuessAmount'] = round((float) $summary['unpricedGuessAmount'], 2); $summary['settleQty'] = round((float) $summary['settleQty'], 4); $summary['settleAmount'] = round((float) $summary['settleAmount'], 2); $summary['differenceAmount'] = round( @@ -370,11 +390,19 @@ class SupplierStatementMatcher $unitPrice = $mineUnitPrice > 0 ? $mineUnitPrice : $theirsUnitPrice; $mineQty = (float) ($mineRow['qty'] ?? 0); $mineAmount = (float) ($mineRow['amount'] ?? 0); + $mineGuessUnitPrice = (float) ($mineRow['guessUnitPrice'] ?? 0); + $mineGuessAmount = (float) ($mineRow['guessAmount'] ?? 0); + $mineUnpricedQty = (float) ($mineRow['unpricedQty'] ?? 0); $theirsQty = (float) ($theirsRow['qty'] ?? 0); $theirsAmount = (float) ($theirsRow['amount'] ?? 0); $qtyDifference = $theirsQty - $mineQty; $amountDifference = $theirsAmount - $mineAmount; $quantityDifference = $this->describeDifference($qtyDifference, $amountDifference); + $mineIsUnpriced = $mineRow !== null + && $mineUnpricedQty >= $mineQty - 0.005 + && $mineUnitPrice <= 0 + && $mineAmount <= 0; + $theirsIsPriced = $theirsUnitPrice > 0 || $theirsAmount > 0; $dateDifferenceDays = $this->dateDifferenceDays($date, $theirsDate); $matchNote = $matchedLayer === null ? '' @@ -401,6 +429,20 @@ class SupplierStatementMatcher . ',金额 ¥' . number_format($mineAmount, 2) . ';四层匹配后仍未找到厂家对应记录,按厂家数据结算(不付款)'; $summary['supplierMissing']++; + } elseif ($mineIsUnpriced && $theirsIsPriced && abs($qtyDifference) < 0.005) { + $type = self::TYPE_UNPRICED; + $settleQty = $mineQty; + $settle = 0.0; + $note = $matchNote + . '采购单号、商品编码、数量一致,我方尚未定价' + . ($mineGuessUnitPrice > 0 + ? '(预估单价 ¥' . number_format($mineGuessUnitPrice, 2) + . ',预估金额 ¥' . number_format($mineGuessAmount, 2) . ')' + : '') + . ';不计厂家多算,待定价后结算'; + $summary['unpriced']++; + $summary['unpricedQty'] += $mineQty; + $summary['unpricedGuessAmount'] += $mineGuessAmount; } elseif (abs($mineAmount - $theirsAmount) < 0.005 && abs($mineQty - $theirsQty) < 0.005) { $type = self::TYPE_MATCH; $settleQty = $mineQty; @@ -422,8 +464,10 @@ class SupplierStatementMatcher $summary['mineAmount'] += $mineAmount; $summary['theirsAmount'] += $theirsAmount; - $summary['supplierOverQty'] += max($qtyDifference, 0); - $summary['supplierOverAmount'] += max($amountDifference, 0); + if ($type !== self::TYPE_UNPRICED) { + $summary['supplierOverQty'] += max($qtyDifference, 0); + $summary['supplierOverAmount'] += max($amountDifference, 0); + } $summary['settleQty'] += $settleQty; $summary['settleAmount'] += $settle; @@ -443,6 +487,8 @@ class SupplierStatementMatcher 'expressNo' => $expressNo, 'unitPrice' => $unitPrice, 'mineUnitPrice' => $mineUnitPrice, + 'mineGuessUnitPrice' => $mineGuessUnitPrice, + 'mineGuessAmount' => $mineGuessAmount, 'theirsUnitPrice' => $theirsUnitPrice, 'mineQty' => $mineQty, 'mineAmount' => $mineAmount, @@ -451,7 +497,9 @@ class SupplierStatementMatcher 'qtyDifference' => round($qtyDifference, 4), 'amountDifference' => round($amountDifference, 2), 'supplierOverQty' => round(max($qtyDifference, 0), 4), - 'supplierOverAmount' => round(max($amountDifference, 0), 2), + 'supplierOverAmount' => $type === self::TYPE_UNPRICED + ? 0.0 + : round(max($amountDifference, 0), 2), 'type' => $type, 'settleQty' => $settleQty, 'settleAmount' => $settle, @@ -527,6 +575,13 @@ class SupplierStatementMatcher $group['unitPrice'] = round((float) $group['amount'] / (float) $group['qty'], 4); } + $guessPrices = array_keys((array) ($group['guessUnitPrices'] ?? [])); + if (count($guessPrices) === 1) { + $group['guessUnitPrice'] = (float) $guessPrices[0]; + } elseif ((float) $group['qty'] != 0.0) { + $group['guessUnitPrice'] = round((float) $group['guessAmount'] / (float) $group['qty'], 4); + } + return $group; } diff --git a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php index c0c9c42d..f19ea20b 100644 --- a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php +++ b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php @@ -13,7 +13,7 @@ use Modules\EcommerceFinance\Services\SupplierStatementStore; /** * 「供应商对账」页面数据: - * - 财务端数据:按供应商汇总 + 行级差异(采购单号 + 日期 + 条码匹配,四类结果带结算口径) + * - 财务端数据:按供应商与商品编码汇总,区分一致、未定价、厂家多、厂家少和数量/金额差异 * - 厂家端数据:厂家上传的对账单(实时)与只读的退货明细 */ class SupplierReconciliationPageData @@ -21,6 +21,7 @@ class SupplierReconciliationPageData /** 财务端比对结果筛选。 */ private const COMPARISON_FILTERS = [ '', + 'unpriced', 'supplier_over', 'supplier_under', 'mismatch', @@ -599,6 +600,9 @@ class SupplierReconciliationPageData 'theirsAmount' => 0.0, 'supplierOverQty' => 0.0, 'supplierOverAmount' => 0.0, + 'unpriced' => 0, + 'unpricedQty' => 0.0, + 'unpricedGuessAmount' => 0.0, 'settleQty' => 0.0, 'settleAmount' => 0.0, 'differenceAmount' => 0.0, @@ -650,6 +654,7 @@ class SupplierReconciliationPageData $allRows = array_values((array) ($detail['rows'] ?? [])); $counts = [ 'all' => count($allRows), + 'unpriced' => 0, 'supplier_over' => 0, 'supplier_under' => 0, 'mismatch' => 0, @@ -658,10 +663,13 @@ class SupplierReconciliationPageData $differenceCount = 0; foreach ($allRows as $row) { - if ((string) ($row['type'] ?? '') !== SupplierStatementMatcher::TYPE_MATCH) { + if (! in_array((string) ($row['type'] ?? ''), [ + SupplierStatementMatcher::TYPE_MATCH, + SupplierStatementMatcher::TYPE_UNPRICED, + ], true)) { $differenceCount++; } - foreach (['supplier_over', 'supplier_under', 'mismatch', 'match'] as $filter) { + foreach (['unpriced', 'supplier_over', 'supplier_under', 'mismatch', 'match'] as $filter) { if ($this->comparisonRowMatchesFilter($row, $filter)) { $counts[$filter]++; } @@ -674,7 +682,10 @@ class SupplierReconciliationPageData )); $filteredDifferenceCount = count(array_filter( $rows, - static fn (array $row): bool => (string) ($row['type'] ?? '') !== SupplierStatementMatcher::TYPE_MATCH, + static fn (array $row): bool => ! in_array((string) ($row['type'] ?? ''), [ + SupplierStatementMatcher::TYPE_MATCH, + SupplierStatementMatcher::TYPE_UNPRICED, + ], true), )); $detail['all_total_rows'] = count($allRows); @@ -695,10 +706,17 @@ class SupplierReconciliationPageData $amountDifference = (float) ($row['amountDifference'] ?? 0); return match ($comparisonFilter) { + 'unpriced' => $type === SupplierStatementMatcher::TYPE_UNPRICED, 'supplier_over' => $type === SupplierStatementMatcher::TYPE_SUPPLIER_EXTRA - || ($type !== SupplierStatementMatcher::TYPE_MATCH && $amountDifference > 0.005), + || ( + ! in_array($type, [SupplierStatementMatcher::TYPE_MATCH, SupplierStatementMatcher::TYPE_UNPRICED], true) + && $amountDifference > 0.005 + ), 'supplier_under' => $type === SupplierStatementMatcher::TYPE_SUPPLIER_MISSING - || ($type !== SupplierStatementMatcher::TYPE_MATCH && $amountDifference < -0.005), + || ( + ! in_array($type, [SupplierStatementMatcher::TYPE_MATCH, SupplierStatementMatcher::TYPE_UNPRICED], true) + && $amountDifference < -0.005 + ), 'mismatch' => $type === SupplierStatementMatcher::TYPE_MISMATCH, 'match' => $type === SupplierStatementMatcher::TYPE_MATCH, default => true, @@ -717,7 +735,7 @@ class SupplierReconciliationPageData } /** - * @param array{all:int,supplier_over:int,supplier_under:int,mismatch:int,match:int} $counts + * @param array{all:int,unpriced:int,supplier_over:int,supplier_under:int,mismatch:int,match:int} $counts * @return list */ private function comparisonFilterOptions(array $counts): array @@ -727,7 +745,13 @@ class SupplierReconciliationPageData 'value' => '', 'label' => '全部结果', 'count' => (int) ($counts['all'] ?? 0), - 'description' => '显示双方一致和全部差异行', + 'description' => '显示双方一致、未定价和全部差异行', + ], + [ + 'value' => 'unpriced', + 'label' => '未定价', + 'count' => (int) ($counts['unpriced'] ?? 0), + 'description' => '采购单号、商品编码、数量一致,但我方尚无实际单价和金额', ], [ 'value' => 'supplier_over', 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 e2c4a23f..8abc4d5a 100644 --- a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php @@ -45,10 +45,12 @@ .supplier-reconciliation-page .sr-tag.is-extra { background: #FDF1E2; color: #B4650A; } .supplier-reconciliation-page .sr-tag.is-mismatch { background: #FBEBE9; color: #A6392F; } .supplier-reconciliation-page .sr-tag.is-missing { background: #EAF2F9; color: #2F6FA8; } + .supplier-reconciliation-page .sr-tag.is-unpriced { background: #FFF4D8; color: #866211; } .supplier-reconciliation-page .sr-tag.is-logistics { background: #F7F0DE; color: #856632; } .supplier-reconciliation-page .sr-tag.is-no-bookkeeping { background: #EFEAE0; color: #595959; } .supplier-reconciliation-page tr.is-extra td, - .supplier-reconciliation-page tr.is-mismatch td { background: #FFFDF7; } + .supplier-reconciliation-page tr.is-mismatch td, + .supplier-reconciliation-page tr.is-unpriced td { background: #FFFDF7; } .supplier-reconciliation-page tr.is-logistics td { background: #FFFDF7; } .supplier-reconciliation-page .sr-comparison-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; padding: 9px 10px; border: 1px solid #E4DCCB; border-radius: 8px; background: var(--vip-content-accent-soft, #F7F0DE); } .supplier-reconciliation-page .sr-filter-tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } @@ -74,6 +76,8 @@ .supplier-reconciliation-page .sr-push-preview-head span { color: var(--vip-content-muted, #595959); font-size: .75rem; } .supplier-reconciliation-page .sr-message-preview { min-height: 42px; display: flex; align-items: center; padding: 9px 12px; border: 1px solid #DED3BD; border-left: 4px solid var(--vip-content-accent-dark, #856632); border-radius: 7px; background: #fff; color: var(--vip-content-ink, #171717); font-size: .875rem; font-weight: 600; line-height: 1.6; font-variant-numeric: tabular-nums; } .supplier-reconciliation-page .sr-push-scope { color: var(--vip-content-muted, #595959); font-size: .75rem; line-height: 1.55; } + .supplier-reconciliation-page .sr-unpriced-note { color: #866211; font-size: .75rem; line-height: 1.55; } + .supplier-reconciliation-page .sr-estimated { color: #866211; font-size: .7rem; white-space: nowrap; } .supplier-reconciliation-page .sr-push-controls { min-width: 0; display: grid; gap: 8px; align-content: center; } .supplier-reconciliation-page .sr-push-basis-field { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 9px; } .supplier-reconciliation-page .sr-push-basis-field > span { color: #5B5343; font-size: .75rem; font-weight: 600; white-space: nowrap; } @@ -191,7 +195,9 @@ 页面“双方原始合计”直接按筛选后的原始行汇总,不受匹配分组影响;匹配过程不会删除或改写我方终版货款对账原始行。
数量、金额一致 → 正常结算;厂家多(我方没有)→ 不结算、退回修改; 数量或金额不一致 → 明确列出双方数量、单价、金额及厂家多算部分,取金额低的一方结算; - 厂家少(我方有)→ 按厂家数据结算(不付款)。厂家未填采购单号时,仅在同日期+商品编码能唯一匹配我方一条数据时兜底。
+ 厂家少(我方有)→ 按厂家数据结算(不付款); + 采购单号、商品编码、数量一致但仅有我方预估价 → 标记“未定价”,不计厂家多算,待定价后结算。 + 厂家未填采购单号时,仅在同日期+商品编码能唯一匹配我方一条数据时兜底。
厂家登录:在「厂家登录账号管理」新增供应商并勾选可登录,供应商名称必须与对账数据中的厂家名称完全一致; 厂家登录后只能查看自己的对账单上传记录、全部退货明细及导出数据。 @@ -488,7 +494,7 @@ @endphp
账期:{{ $displayMonthLabel }},件数{{ $formatPushQty($pushPreviewQty) }}件,货款{{ number_format($pushPreviewAmount, 2, '.', '') }}元
-
汇总范围:当前账期 + 当前对账人;下方“差异筛选”只改变表格显示,不改变整期推送范围。
+
汇总范围:当前账期 + 当前对账人;下方“结果筛选”只改变表格显示,不改变整期推送范围。
+ @if ($unpricedCount > 0) +
+ 另有 {{ number_format($unpricedCount) }} 条未定价,共 {{ $formatPushQty($unpricedQty) }} 件,预估金额 {{ number_format($unpricedGuessAmount, 2) }} 元;未计价金额不计入推送货款,待定价后结算。 +
+ @endif
@@ -633,12 +648,13 @@ '厂家多' => 'is-extra', '数量/金额不一致' => 'is-mismatch', '厂家少' => 'is-missing', + '未定价' => 'is-unpriced', default => 'is-match', }; @endphp - @if ($row['type'] !== '一致') + @if (! in_array($row['type'], ['一致', '未定价'], true)) {{ $row['mineQty'] !== 0.0 ? number_format($row['mineQty'], 2) : '—' }} - {{ $row['mineUnitPrice'] !== 0.0 ? number_format($row['mineUnitPrice'], 2) : '—' }} - {{ $row['mineAmount'] !== 0.0 ? number_format($row['mineAmount'], 2) : '—' }} + + @if ($row['type'] === '未定价' && (float) ($row['mineGuessUnitPrice'] ?? 0) > 0) + 预估 {{ number_format($row['mineGuessUnitPrice'], 2) }} + @else + {{ $row['mineUnitPrice'] !== 0.0 ? number_format($row['mineUnitPrice'], 2) : '—' }} + @endif + + + @if ($row['type'] === '未定价' && (float) ($row['mineGuessAmount'] ?? 0) > 0) + 预估 {{ number_format($row['mineGuessAmount'], 2) }} + @else + {{ $row['mineAmount'] !== 0.0 ? number_format($row['mineAmount'], 2) : '—' }} + @endif + {{ $row['theirsQty'] !== 0.0 ? number_format($row['theirsQty'], 2) : '—' }} {{ $row['theirsUnitPrice'] !== 0.0 ? number_format($row['theirsUnitPrice'], 2) : '—' }} {{ $row['theirsAmount'] !== 0.0 ? number_format($row['theirsAmount'], 2) : '—' }} @@ -685,7 +713,9 @@ @endif - @if (abs($row['amountDifference']) < 0.005) + @if ($row['type'] === '未定价') + 未定价 + @elseif (abs($row['amountDifference']) < 0.005) 一致 @else 厂家{{ $row['amountDifference'] > 0 ? '多' : '少' }} ¥{{ number_format(abs($row['amountDifference']), 2) }} @@ -694,9 +724,11 @@ {{ $row['note'] }} - {{ number_format($row['settleAmount'], 2) }} + {{ $row['type'] === '未定价' ? '待定价' : number_format($row['settleAmount'], 2) }} - @if ($row['type'] !== '一致') + @if ($row['type'] === '未定价') + 待定价 + @elseif ($row['type'] !== '一致')
diff --git a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php index cab53abf..833ecd29 100644 --- a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php +++ b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php @@ -195,6 +195,53 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase }); } + public function test_unpriced_rows_are_not_pushed_as_supplier_differences(): void + { + app(SupplierDingTalkNotifier::class)->save('未定价厂家', self::WEBHOOK, 'SEC-test-123', false); + app(SupplierPayableOverrideStore::class)->save( + 'final:supplier:payment-reconciliation', + 'mine:unpriced', + [ + 'summary_month' => '2026-10', + 'purchaseNo' => '948180', + 'date' => '2026-10-14', + 'supplier' => '未定价厂家', + 'skuCode' => 'SZ014916', + 'productCode' => 'SZ014916N1109V01', + 'qty' => '20', + 'unitPrice' => null, + 'amount' => null, + 'guessUnitPrice' => '195', + 'guessAmount' => '3900', + 'reconciler' => '陈亲园', + ], + (int) $this->company->id, + null, + ); + app(SupplierStatementStore::class)->save('未定价厂家', [ + array_merge($this->statementRow('SZ014916N1109V01', '186.00', '20', '3720.00'), [ + 'purchaseNo' => '948180', + 'createdAt' => '2026-10-08', + 'supplier' => '未定价厂家', + ]), + ], 'unpriced.csv', (int) $this->company->id, null); + Http::fake(); + + $this->loginAs() + ->post(route('business.supplier-reconciliation.push-differences'), [ + 'supplier' => '未定价厂家', + 'month' => '2026-10', + 'reconciler' => '陈亲园', + 'scope' => 'all', + ]) + ->assertStatus(302) + ->assertSessionHasErrors([ + 'dingtalk_push' => '当前筛选没有可推送的差异。', + ]); + + Http::assertNothingSent(); + } + public function test_push_reconciliation_summary_rejects_unknown_basis_without_http_call(): void { $this->withExceptionHandling(); diff --git a/tests/Feature/Business/SupplierReconciliationTest.php b/tests/Feature/Business/SupplierReconciliationTest.php index 4876232f..6b3a052a 100644 --- a/tests/Feature/Business/SupplierReconciliationTest.php +++ b/tests/Feature/Business/SupplierReconciliationTest.php @@ -466,6 +466,110 @@ class SupplierReconciliationTest extends FeatureTestCase $this->assertSame(-30.0, $rows['NET-B-N1065V01']['amountDifference']); } + public function test_finance_comparison_marks_missing_mine_price_as_unpriced(): void + { + $companyId = (int) $this->company->id; + $store = app(SupplierPayableOverrideStore::class); + $store->save( + 'final:supplier:payment-reconciliation', + 'unpriced:948180', + [ + 'summary_month' => '2026-10', + 'purchaseNo' => '948180', + 'date' => '2026-10-14', + 'supplier' => '木缘居', + 'skuCode' => 'SZ014916', + 'productCode' => 'SZ014916N1109V01', + 'qty' => '20', + 'unitPrice' => null, + 'amount' => null, + 'guessUnitPrice' => '195', + 'guessAmount' => '3900', + 'reconciler' => '陈亲园', + ], + $companyId, + null, + ); + $store->save( + 'final:supplier:payment-reconciliation', + 'priced:948180', + [ + 'summary_month' => '2026-10', + 'purchaseNo' => '948181', + 'date' => '2026-10-14', + 'supplier' => '木缘居', + 'skuCode' => 'SZ014917', + 'productCode' => 'SZ014917N1109V01', + 'qty' => '10', + 'unitPrice' => '100', + 'amount' => '1000', + 'guessUnitPrice' => '100', + 'guessAmount' => '1000', + 'reconciler' => '陈亲园', + ], + $companyId, + null, + ); + $this->seedStatement('木缘居', [ + array_merge($this->statementRow('SZ014916N1109V01', '186.00', '20', '3720.00'), [ + 'purchaseNo' => '948180', + 'createdAt' => '2026-10-08', + 'supplier' => '木缘居', + ]), + array_merge($this->statementRow('SZ014917N1109V01', '100.00', '10', '1000.00'), [ + 'purchaseNo' => '948181', + 'createdAt' => '2026-10-14', + 'supplier' => '木缘居', + ]), + ]); + + $detail = app(SupplierReconciliationPageData::class)->financeDetailForFilters( + '木缘居', + $companyId, + ['month' => '2026-10', 'reconciler' => '陈亲园'], + ); + $unpriced = collect($detail['rows'])->firstWhere('type', SupplierStatementMatcher::TYPE_UNPRICED); + + $this->assertSame(1, $detail['summary']['match']); + $this->assertSame(1, $detail['summary']['unpriced']); + $this->assertSame(0, $detail['summary']['supplierExtra']); + $this->assertSame(0, $detail['summary']['supplierMissing']); + $this->assertSame(0, $detail['summary']['mismatch']); + $this->assertSame(0.0, $detail['summary']['supplierOverAmount']); + $this->assertSame(0.0, $detail['summary']['differenceAmount']); + $this->assertSame(1, $detail['comparison_counts']['unpriced']); + $this->assertSame(0, $detail['difference_count']); + $this->assertSame(195.0, $unpriced['mineGuessUnitPrice']); + $this->assertSame(3900.0, $unpriced['mineGuessAmount']); + + $filtered = app(SupplierReconciliationPageData::class)->financeDetailForFilters( + '木缘居', + $companyId, + [ + 'month' => '2026-10', + 'reconciler' => '陈亲园', + 'comparison_filter' => 'unpriced', + ], + ); + $this->assertSame(1, $filtered['total_rows']); + $this->assertSame(1, $filtered['comparison_counts']['unpriced']); + $this->assertSame(0, $filtered['difference_count']); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'supplier-reconciliation', + 'page' => 'finance-data', + 'supplier' => '木缘居', + 'month' => '2026-10', + 'reconciler' => '陈亲园', + ])) + ->assertOk() + ->assertSeeText('未定价') + ->assertSeeText('预估 195.00') + ->assertSeeText('预估 3,900.00') + ->assertSeeText('待定价') + ->assertSeeText('未计价金额不计入推送货款,待定价后结算'); + } + public function test_finance_comparison_matches_crossed_purchase_numbers_by_barcode_and_quantity(): void { $companyId = (int) $this->company->id; diff --git a/tests/Unit/EcommerceFinance/SupplierStatementMatcherTest.php b/tests/Unit/EcommerceFinance/SupplierStatementMatcherTest.php index 8e0de5d8..0f257ac2 100644 --- a/tests/Unit/EcommerceFinance/SupplierStatementMatcherTest.php +++ b/tests/Unit/EcommerceFinance/SupplierStatementMatcherTest.php @@ -125,6 +125,46 @@ class SupplierStatementMatcherTest extends TestCase $this->assertStringContainsString('采购单号+商品编码匹配', $result['rows'][0]['note']); } + public function test_matching_quantity_with_missing_actual_mine_price_is_unpriced(): void + { + $mine = [[ + 'purchaseNo' => '948180', + 'date' => '2026-10-14', + 'productCode' => 'SZ014916N1109V01', + 'unitPrice' => null, + 'qty' => '20', + 'amount' => null, + 'guessUnitPrice' => '195', + 'guessAmount' => '3900', + ]]; + $theirs = [[ + 'purchaseNo' => '948180', + 'createdAt' => '2026-10-08', + 'barcode' => 'SZ014916N1109V01', + 'unitPrice' => '186', + 'qty' => '20', + 'amount' => '3720', + ]]; + + $result = app(SupplierStatementMatcher::class)->netByBarcode($mine, $theirs); + $row = $result['rows'][0]; + + $this->assertSame(SupplierStatementMatcher::TYPE_UNPRICED, $row['type']); + $this->assertSame(20.0, $row['mineQty']); + $this->assertSame(20.0, $row['theirsQty']); + $this->assertSame(0.0, $row['mineAmount']); + $this->assertSame(3720.0, $row['theirsAmount']); + $this->assertSame(195.0, $row['mineGuessUnitPrice']); + $this->assertSame(3900.0, $row['mineGuessAmount']); + $this->assertSame(1, $result['summary']['unpriced']); + $this->assertSame(20.0, $result['summary']['unpricedQty']); + $this->assertSame(3900.0, $result['summary']['unpricedGuessAmount']); + $this->assertSame(0.0, $result['summary']['supplierOverAmount']); + $this->assertSame(20.0, $result['summary']['settleQty']); + $this->assertSame(0.0, $result['summary']['settleAmount']); + $this->assertSame(0.0, $result['summary']['differenceAmount']); + } + public function test_purchase_and_barcode_match_with_unrecognized_supplier_date(): void { $mine = [[