diff --git a/modules/EcommerceFinance/Services/SupplierReturnAccounting.php b/modules/EcommerceFinance/Services/SupplierReturnAccounting.php new file mode 100644 index 00000000..a0749be6 --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierReturnAccounting.php @@ -0,0 +1,133 @@ + $row */ + public function enrich(array $row): array + { + $remark = trim((string) ($row['remark'] ?? '')); + $title = trim((string) ($row['title'] ?? '')); + $isLogistics = preg_match('/记账物流|物流记账|物流.*不记账|不记账.*物流/u', $remark) === 1; + $isNoBookkeeping = ! $isLogistics && str_contains($remark, '不记账'); + $target = $isLogistics ? '物流' : ($isNoBookkeeping ? '不记账' : '厂家'); + + $logisticsQty = 0.0; + $originalQty = 0.0; + if ($isLogistics) { + if (preg_match('/\*\s*(\d+(?:\.\d+)?)\s*记账物流/u', $remark, $matches) === 1 + || preg_match('/记账物流\s*[×xX*]?\s*(\d+(?:\.\d+)?)/u', $remark, $matches) === 1) { + $logisticsQty = (float) $matches[1]; + } + if (preg_match('/原数量\s*(\d+(?:\.\d+)?)/u', $remark, $matches) === 1) { + $originalQty = (float) $matches[1]; + } + if ($logisticsQty === 0.0 && $originalQty > 0) { + $logisticsQty = max(0.0, $originalQty - $this->number($row['qty'] ?? 0)); + } + } + + $qty = $this->number($row['qty'] ?? 0); + $unitPrice = $this->number($row['unit_price'] ?? $row['guess_unit_price'] ?? 0); + $amount = $this->number($row['amount'] ?? $row['guess_amount'] ?? 0); + if ($amount === 0.0 && $qty !== 0.0) { + $amount = $qty * $unitPrice; + } + if ($unitPrice === 0.0 && $qty !== 0.0) { + $unitPrice = $amount / $qty; + } + + $supplierQty = $qty; + $supplierAmount = $amount; + $amountContainsLogistics = $originalQty > 0 + && $logisticsQty > 0 + && abs($amount - ($originalQty * $unitPrice)) < 0.01; + if ($amountContainsLogistics) { + $supplierQty = max(0.0, $originalQty - $logisticsQty); + $supplierAmount = max(0.0, $amount - ($logisticsQty * $unitPrice)); + } + + $row['_accounting_target'] = $target; + $row['_accounting_label'] = $isLogistics ? '转物流记账' : ($isNoBookkeeping ? '不记账' : '厂家扣减'); + $row['_accounting_note'] = $isLogistics + ? '不扣本厂家,转' . ($title !== '' ? $title : '物流') . '记账' + : ($isNoBookkeeping ? '本厂家不记账' : '按厂家退货扣减'); + $row['_logistics_qty'] = $isLogistics ? $logisticsQty : 0.0; + $row['_logistics_amount'] = $isLogistics + ? round($logisticsQty * $unitPrice, 2) + : 0.0; + $row['_logistics_company'] = $isLogistics ? $title : ''; + $row['_supplier_qty'] = round($supplierQty, 2); + $row['_supplier_amount'] = round($supplierAmount, 2); + + return $row; + } + + /** + * @param list> $rows + * @return array{count:int,supplier_rows:int,supplier_qty:float,supplier_amount:float,logistics_rows:int,logistics_qty:float,logistics_amount:float,logistics_companies:list,no_bookkeeping_rows:int} + */ + public function summarize(array $rows): array + { + $summary = [ + 'count' => count($rows), + 'supplier_rows' => 0, + 'supplier_qty' => 0.0, + 'supplier_amount' => 0.0, + 'logistics_rows' => 0, + 'logistics_qty' => 0.0, + 'logistics_amount' => 0.0, + 'logistics_companies' => [], + 'no_bookkeeping_rows' => 0, + ]; + $companies = []; + + foreach ($rows as $row) { + $row = $this->enrich($row); + $target = (string) $row['_accounting_target']; + + if ($target === '不记账') { + $summary['no_bookkeeping_rows']++; + continue; + } + + $summary['supplier_rows']++; + $summary['supplier_qty'] += (float) $row['_supplier_qty']; + $summary['supplier_amount'] += (float) $row['_supplier_amount']; + + if ($target !== '物流') { + continue; + } + + $summary['logistics_rows']++; + $summary['logistics_qty'] += (float) $row['_logistics_qty']; + $summary['logistics_amount'] += (float) $row['_logistics_amount']; + $company = trim((string) $row['_logistics_company']); + if ($company !== '') { + $companies[$company] = true; + } + } + + $summary['supplier_qty'] = round($summary['supplier_qty'], 2); + $summary['supplier_amount'] = round($summary['supplier_amount'], 2); + $summary['logistics_qty'] = round($summary['logistics_qty'], 2); + $summary['logistics_amount'] = round($summary['logistics_amount'], 2); + $summary['logistics_companies'] = array_keys($companies); + + return $summary; + } + + private function number(mixed $value): float + { + $raw = trim((string) $value); + + return is_numeric(str_replace(',', '', $raw)) ? (float) str_replace(',', '', $raw) : 0.0; + } +} diff --git a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php index 33c42f08..b1396544 100644 --- a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php +++ b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php @@ -5,6 +5,7 @@ namespace Modules\EcommerceFinance\View; use App\Http\Controllers\Business\PayablesFinalActions; use Modules\EcommerceFinance\Services\SupplierDingTalkNotifier; use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore; +use Modules\EcommerceFinance\Services\SupplierReturnAccounting; use Modules\EcommerceFinance\Services\SupplierReturnConfirmation; use Modules\EcommerceFinance\Services\SupplierStatementMatcher; use Modules\EcommerceFinance\Services\SupplierStatementStore; @@ -25,6 +26,7 @@ class SupplierReconciliationPageData private readonly SupplierStatementMatcher $matcher, private readonly SupplierReturnConfirmation $returnConfirmations, private readonly SupplierDingTalkNotifier $dingTalkNotifier, + private readonly SupplierReturnAccounting $returnAccounting, ) { } @@ -85,6 +87,15 @@ class SupplierReconciliationPageData foreach ($this->statements->uploadedSuppliers($companyId) as $name) { $suppliers[$name] = true; } + foreach ($this->store->listRows( + PayablesFinalActions::DATASET_PREFIX . 'supplier:return-reconciliation', + $companyId, + ) as $row) { + $name = trim((string) ($row['supplier'] ?? '')); + if ($name !== '') { + $suppliers[$name] = true; + } + } $supplierNames = array_keys($suppliers); sort($supplierNames); @@ -113,6 +124,9 @@ class SupplierReconciliationPageData 'reconciler' => $reconciler, 'selectedSupplier' => $selected, 'detail' => $detail, + 'financeReturnPanel' => $detail !== null + ? $this->financeReturnPanel($selected, $companyId, $month, $reconciler) + : null, 'robot' => $selected !== '' ? $this->dingTalkNotifier->configuration($selected) : null, 'listUrl' => route('business.pages.show', array_filter([ 'section' => 'supplier-reconciliation', @@ -144,6 +158,15 @@ class SupplierReconciliationPageData foreach ($this->statements->uploadedSuppliers($companyId) as $name) { $suppliers[$name] = true; } + foreach ($this->store->listRows( + PayablesFinalActions::DATASET_PREFIX . 'supplier:return-reconciliation', + $companyId, + ) as $row) { + $name = trim((string) ($row['supplier'] ?? '')); + if ($name !== '') { + $suppliers[$name] = true; + } + } $supplierNames = array_keys($suppliers); sort($supplierNames); @@ -231,6 +254,46 @@ class SupplierReconciliationPageData return $this->detail($mine, $supplier, $companyId); } + /** + * 财务端比对页的退货面板:与厂家端使用同一终版数据集,并保留月份/对账人筛选。 + * + * @return array + */ + private function financeReturnPanel( + string $supplier, + int $companyId, + string $month, + string $reconciler, + ): array { + $allRows = $this->returnRows($supplier, $companyId); + $statuses = $this->returnConfirmations->statuses($supplier, $companyId); + $rows = []; + + foreach ($allRows as $row) { + $rowMonth = substr(trim((string) ($row['send_time'] ?? '')), 0, 7); + if ($rowMonth === '') { + $rowMonth = substr(trim((string) ($row['summary_month'] ?? '')), 0, 7); + } + if ($month !== '' && $rowMonth !== $month) { + continue; + } + if ($reconciler !== '' && trim((string) ($row['reconciler'] ?? '')) !== $reconciler) { + continue; + } + + $row['_status'] = $statuses[(string) ($row['_action_key'] ?? '')] ?? ''; + $rows[] = $row; + } + + return $this->returnConfirmationPanel( + $supplier, + $rows, + $this->returnFilters([]), + count($allRows), + 'finance_return_page', + ); + } + /** * @param array $input * @return array{returnPanel:array,returnWarehouses:list} @@ -268,7 +331,13 @@ class SupplierReconciliationPageData * @param array{keyword:string,date_from:string,date_to:string,warehouse:string,status:string} $filters * @return array */ - private function returnConfirmationPanel(string $supplier, array $returnRows, array $filters, int $allRows): array + private function returnConfirmationPanel( + string $supplier, + array $returnRows, + array $filters, + int $allRows, + string $pageQueryKey = 'return_page', + ): array { $total = 0.0; $confirmedAmount = 0.0; @@ -278,8 +347,11 @@ class SupplierReconciliationPageData $rows = []; foreach ($returnRows as $row) { + $row = $this->returnAccounting->enrich($row); $key = (string) ($row['_action_key'] ?? ''); - $deduction = $this->returnDeduction($row); + $deduction = $row['_accounting_target'] === '不记账' + ? 0.0 + : (float) $row['_supplier_amount']; $status = (string) ($row['_status'] ?? ''); $total += $deduction; @@ -297,7 +369,7 @@ class SupplierReconciliationPageData // 全部退货行按导入顺序分页展示(每页 15 行),不做金额排序。 $perPage = self::PER_PAGE; $pages = max((int) ceil(count($rows) / $perPage), 1); - $page = min(max((int) request()->query('return_page', 1), 1), $pages); + $page = min(max((int) request()->query($pageQueryKey, 1), 1), $pages); $filterQuery = $this->returnFilterQuery($filters); @@ -314,25 +386,13 @@ class SupplierReconciliationPageData 'confirmed_amount' => round($confirmedAmount, 2), 'disputed_count' => $disputedCount, 'disputed_amount' => round($disputedAmount, 2), + 'accounting_summary' => $this->returnAccounting->summarize($returnRows), 'filters' => $filters, 'filter_query' => $filterQuery, 'is_filtered' => $filterQuery !== [], ]; } - /** 扣减金额:金额为空时用预估金额(与采购方记账口径一致)。 */ - private function returnDeduction(array $row): float - { - $amount = trim((string) ($row['amount'] ?? '')); - if ($amount !== '' && is_numeric(str_replace(',', '', $amount))) { - return (float) str_replace(',', '', $amount); - } - - $guess = trim((string) ($row['guess_amount'] ?? '')); - - return is_numeric(str_replace(',', '', $guess)) ? (float) str_replace(',', '', $guess) : 0.0; - } - /** @return array */ private function detail(array $mine, string $supplier, int $companyId): array { 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 07820ac3..0a24a693 100644 --- a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php @@ -27,8 +27,11 @@ .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-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-logistics td { background: #FFFDF7; } .supplier-reconciliation-page .sr-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; } .supplier-reconciliation-page .sr-summary-cell { padding: 10px 12px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; display: grid; gap: 4px; } .supplier-reconciliation-page .sr-summary-cell b { font-size: 1.125rem; font-weight: 600; } @@ -333,6 +336,110 @@ + + @php $returnAccounting = $financeReturnPanel['accounting_summary']; @endphp +
+
+

退货扣减 · 财务与厂家同步

+ + 直接读取「应付款终版 → 退货对账」同一数据集;修改退货数量、备注、标题后,财务端和厂家门户同时更新。 + +
+
+
退货行数{{ number_format($returnAccounting['count']) }}
+
厂家净扣减¥{{ number_format($returnAccounting['supplier_amount'], 2) }}
+
转物流记账{{ number_format($returnAccounting['logistics_qty'], 2) }} 件
+
转物流金额¥{{ number_format($returnAccounting['logistics_amount'], 2) }}
+
不记账行数{{ number_format($returnAccounting['no_bookkeeping_rows']) }}
+
+ @if ($returnAccounting['logistics_rows'] > 0) +

+ 已识别 {{ number_format($returnAccounting['logistics_rows']) }} 行“不扣本厂家、转物流记账” + @if (! empty($returnAccounting['logistics_companies'])) + ,物流公司:{{ implode('、', $returnAccounting['logistics_companies']) }} + @endif + 。厂家端显示相同备注和“转物流记账”标记,厂家扣减金额按调整后的净数量/金额计算。 +

+ @endif +
+ + + + + + + + + + + @forelse ($financeReturnPanel['rows'] as $row) + $row['_accounting_target'] === '物流'])> + + + + + + + + + + + + + + + @empty + + @endforelse + +
发货时间条形码商品名称厂家扣减数量厂家扣减金额记账归属物流记账数量物流记账金额物流公司备注快递单号对账人确认状态
{{ $row['send_time'] ?? '—' }}{{ $row['barcode'] ?? '—' }}{{ $row['sku_name'] ?? '—' }}{{ $row['_accounting_target'] === '不记账' ? '—' : number_format($row['_supplier_qty'], 2) }}{{ $row['_accounting_target'] === '不记账' ? '—' : number_format($row['_supplier_amount'], 2) }} + + {{ $row['_accounting_label'] }} + + @if ($row['_accounting_note'] !== '按厂家退货扣减') +
{{ $row['_accounting_note'] }}
+ @endif +
{{ $row['_logistics_qty'] > 0 ? number_format($row['_logistics_qty'], 2) : '—' }}{{ $row['_logistics_amount'] > 0 ? number_format($row['_logistics_amount'], 2) : '—' }}{{ $row['_logistics_company'] !== '' ? $row['_logistics_company'] : '—' }}{{ $row['remark'] ?? '—' }}{{ $row['express_no'] ?? '—' }}{{ $row['reconciler'] ?? '—' }} + @if ($row['status'] === 'confirmed') + 已确认 + @elseif ($row['status'] === 'disputed') + 异议中 + @else + 待确认 + @endif +
当前月份和对账人筛选下没有退货数据。
+
+ @if ($financeReturnPanel['pages'] > 1) + @php + $returnPageCount = $financeReturnPanel['pages']; + $returnPageCurrent = $financeReturnPanel['page']; + $returnPageStart = max(1, min($returnPageCurrent - 4, $returnPageCount - 8)); + $returnPageEnd = min($returnPageCount, $returnPageStart + 8); + $returnPageUrl = fn (int $number): string => route('business.pages.show', array_filter([ + 'section' => $section['slug'], + 'page' => $page['slug'], + 'supplier' => $selectedSupplier, + 'month' => $month, + 'reconciler' => $reconciler, + 'finance_return_page' => $number, + ], static fn (mixed $value): bool => $value !== null && $value !== '')); + @endphp +
+ 第 {{ $returnPageCurrent }} / {{ $returnPageCount }} 页(每页 {{ $financeReturnPanel['per_page'] }} 行) + @if ($returnPageCurrent > 1) + 上一页 + @endif + @for ($number = $returnPageStart; $number <= $returnPageEnd; $number++) + {{ $number }} + @endfor + @if ($returnPageCurrent < $returnPageCount) + 下一页 + @endif +
+ @endif +
@endif diff --git a/resources/views/business/pages/supplier-reconciliation/supplier-data.blade.php b/resources/views/business/pages/supplier-reconciliation/supplier-data.blade.php index f3d3f32c..690cd2c3 100644 --- a/resources/views/business/pages/supplier-reconciliation/supplier-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/supplier-data.blade.php @@ -46,6 +46,9 @@ .supplier-reconciliation-page .sr-tag { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 4px; font-size: .75rem; white-space: nowrap; } .supplier-reconciliation-page .sr-tag.is-match { background: #ECF4EC; color: #3F7A46; } .supplier-reconciliation-page .sr-tag.is-mismatch { background: #FBEBE9; color: #A6392F; } + .supplier-reconciliation-page .sr-tag.is-logistics { margin-left: 4px; background: #F7F0DE; color: #856632; } + .supplier-reconciliation-page .sr-tag.is-no-bookkeeping { margin-left: 4px; background: #EFEAE0; color: #595959; } + .supplier-reconciliation-page tr.is-logistics td { background: #FFFDF7; } .supplier-reconciliation-page .sr-inline-form { display: inline-flex; } .supplier-reconciliation-page .sr-return-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; } .supplier-reconciliation-page .sr-return-panel > .sr-return-actions { gap: 12px; } @@ -195,6 +198,9 @@ 已确认 {{ $returnPanel['confirmed_count'] }} 行(¥{{ number_format($returnPanel['confirmed_amount'], 2) }}) · 异议 {{ $returnPanel['disputed_count'] }} 行(暂不扣减 ¥{{ number_format($returnPanel['disputed_amount'], 2) }}) · 待确认 {{ max($returnPanel['total_rows'] - $returnPanel['confirmed_count'] - $returnPanel['disputed_count'], 0) }} 行 + @if ($returnPanel['accounting_summary']['logistics_rows'] > 0) + · 转物流记账 {{ $returnPanel['accounting_summary']['logistics_rows'] }} 行,{{ number_format($returnPanel['accounting_summary']['logistics_qty'], 2) }} 件(¥{{ number_format($returnPanel['accounting_summary']['logistics_amount'], 2) }}) + @endif
@forelse ($returnPanel['rows'] as $row) - + $row['_accounting_target'] === '物流'])> {{ $row['send_time'] ?? '—' }} {{ $row['supplier'] ?? '—' }} {{ $row['barcode'] ?? '—' }} @@ -261,7 +267,15 @@ {{ $row['unit_price'] ?? '—' }} {{ $row['amount'] ?? '—' }} {{ $row['supplier_barcode'] ?? '—' }} - {{ $row['remark'] ?? '—' }} + + {{ $row['remark'] ?? '—' }} + @if ($row['_accounting_target'] !== '厂家') + {{ $row['_accounting_label'] }} + @endif + @if ($row['_accounting_note'] !== '按厂家退货扣减') +
{{ $row['_accounting_note'] }}
+ @endif + {{ $row['title'] ?? '—' }} {{ $row['wms_pick_no'] ?? '—' }} {{ $row['warehouse'] ?? '—' }} diff --git a/resources/views/portal/supplier-reconciliation/index.blade.php b/resources/views/portal/supplier-reconciliation/index.blade.php index feb62f83..79d4b6a2 100644 --- a/resources/views/portal/supplier-reconciliation/index.blade.php +++ b/resources/views/portal/supplier-reconciliation/index.blade.php @@ -35,6 +35,9 @@ .supplier-portal-page .sp-tag { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 4px; background: #EFEAE0; color: #595959; font-size: .75rem; white-space: nowrap; } .supplier-portal-page .sp-tag.is-match { background: #ECF4EC; color: #3F7A46; } .supplier-portal-page .sp-tag.is-mismatch { background: #FBEBE9; color: #A6392F; } + .supplier-portal-page .sp-tag.is-logistics { margin-left: 4px; background: #F7F0DE; color: #856632; } + .supplier-portal-page .sp-tag.is-no-bookkeeping { margin-left: 4px; background: #EFEAE0; color: #595959; } + .supplier-portal-page tr.is-logistics td { background: #FFFDF7; } .supplier-portal-page .sp-inline-form { display: inline-flex; } .supplier-portal-page .sp-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; } @media (max-width: 960px) { @@ -95,6 +98,9 @@ 已确认 {{ $returnPanel['confirmed_count'] }} 行(¥{{ number_format($returnPanel['confirmed_amount'], 2) }}) 异议 {{ $returnPanel['disputed_count'] }} 行(暂不扣减 ¥{{ number_format($returnPanel['disputed_amount'], 2) }}) 待确认 {{ max($returnPanel['total_rows'] - $returnPanel['confirmed_count'] - $returnPanel['disputed_count'], 0) }} 行 + @if ($returnPanel['accounting_summary']['logistics_rows'] > 0) + 转物流记账 {{ $returnPanel['accounting_summary']['logistics_rows'] }} 行,{{ number_format($returnPanel['accounting_summary']['logistics_qty'], 2) }} 件(¥{{ number_format($returnPanel['accounting_summary']['logistics_amount'], 2) }}) + @endif @forelse ($returnPanel['rows'] as $row) - + $row['_accounting_target'] === '物流'])> {{ $row['send_time'] ?? '—' }} {{ $row['supplier'] ?? '—' }} {{ $row['barcode'] ?? '—' }} @@ -161,7 +167,15 @@ {{ $row['unit_price'] ?? '—' }} {{ $row['amount'] ?? '—' }} {{ $row['supplier_barcode'] ?? '—' }} - {{ $row['remark'] ?? '—' }} + + {{ $row['remark'] ?? '—' }} + @if ($row['_accounting_target'] !== '厂家') + {{ $row['_accounting_label'] }} + @endif + @if ($row['_accounting_note'] !== '按厂家退货扣减') +
{{ $row['_accounting_note'] }}
+ @endif + {{ $row['title'] ?? '—' }} {{ $row['wms_pick_no'] ?? '—' }} {{ $row['warehouse'] ?? '—' }} diff --git a/tests/Feature/Business/SupplierReconciliationTest.php b/tests/Feature/Business/SupplierReconciliationTest.php index 87ef3760..1b34d507 100644 --- a/tests/Feature/Business/SupplierReconciliationTest.php +++ b/tests/Feature/Business/SupplierReconciliationTest.php @@ -283,6 +283,79 @@ class SupplierReconciliationTest extends FeatureTestCase $this->assertCount(0, app(SupplierStatementStore::class)->rows('鸣飞家具', (int) $this->company->id)); } + public function test_return_bookkeeping_note_is_identical_on_finance_and_supplier_views(): void + { + $companyId = (int) $this->company->id; + app(SupplierPayableOverrideStore::class)->save( + 'final:supplier:payment-reconciliation', + 'mine:logistics:1', + [ + 'summary_month' => '2026-09', + 'supplier' => '曹县宇诺', + 'reconciler' => '陈亲园', + 'skuCode' => 'CZ008407', + 'qty' => '1', + 'unitPrice' => '259', + 'amount' => '259', + ], + $companyId, + null, + ); + app(SupplierPayableOverrideStore::class)->save( + 'final:supplier:return-reconciliation', + 'return:logistics:1', + [ + 'summary_month' => '2026-09', + 'send_time' => '2026-09-27', + 'supplier' => '曹县宇诺', + 'barcode' => 'CZ008407N1178V01', + 'sku_name' => '款式二 160/80/75 黑胡桃色', + 'qty' => '1', + 'unit_price' => '259', + 'amount' => '259', + 'remark' => '原数量2件,CZ008407N1178V01*1记账物流', + 'title' => '常州松宇货运', + 'express_no' => '202609270079', + 'reconciler' => '陈亲园', + ], + $companyId, + null, + ); + + $finance = $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'supplier-reconciliation', + 'page' => 'finance-data', + 'supplier' => '曹县宇诺', + 'month' => '2026-09', + 'reconciler' => '陈亲园', + ])); + $finance->assertOk() + ->assertSeeText('退货扣减 · 财务与厂家同步') + ->assertSeeText('转物流记账') + ->assertSeeText('常州松宇货运') + ->assertSeeText('原数量2件,CZ008407N1178V01*1记账物流') + ->assertSeeText('259.00'); + + $supplier = $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'supplier-reconciliation', + 'page' => 'supplier-data', + 'supplier' => '曹县宇诺', + ])); + $supplier->assertOk() + ->assertSeeText('转物流记账') + ->assertSeeText('不扣本厂家,转常州松宇货运记账') + ->assertSeeText('原数量2件,CZ008407N1178V01*1记账物流'); + + $vendor = $this->createVendorPortalUser('曹县宇诺'); + $this->loginAs($vendor)->get(route('portal.supplier-reconciliation.index', [ + 'company_id' => $companyId, + ])) + ->assertOk() + ->assertSeeText('转物流记账') + ->assertSeeText('不扣本厂家,转常州松宇货运记账') + ->assertSeeText('原数量2件,CZ008407N1178V01*1记账物流'); + } + public function test_statement_rows_are_paginated_fifteen_per_page(): void { $rows = []; diff --git a/tests/Unit/EcommerceFinance/SupplierReturnAccountingTest.php b/tests/Unit/EcommerceFinance/SupplierReturnAccountingTest.php new file mode 100644 index 00000000..ff175c38 --- /dev/null +++ b/tests/Unit/EcommerceFinance/SupplierReturnAccountingTest.php @@ -0,0 +1,81 @@ +enrich([ + 'qty' => '1', + 'unit_price' => '259', + 'amount' => '259', + 'remark' => '原数量2件,CZ008407N1178V01*1记账物流', + 'title' => '常州松宇货运', + ]); + + $this->assertSame('物流', $row['_accounting_target']); + $this->assertSame('转物流记账', $row['_accounting_label']); + $this->assertSame('不扣本厂家,转常州松宇货运记账', $row['_accounting_note']); + $this->assertSame(1.0, $row['_logistics_qty']); + $this->assertSame(259.0, $row['_logistics_amount']); + $this->assertSame(1.0, $row['_supplier_qty']); + $this->assertSame(259.0, $row['_supplier_amount']); + } + + public function test_summary_matches_finance_and_supplier_views(): void + { + $summary = (new SupplierReturnAccounting())->summarize([ + [ + 'qty' => '1', + 'unit_price' => '259', + 'amount' => '259', + 'remark' => '原数量2件,CZ008407N1178V01*1记账物流', + 'title' => '常州松宇货运', + ], + [ + 'qty' => '3', + 'unit_price' => '250', + 'amount' => '750', + 'remark' => '原数量4件,CZ008412N1178V01*1记账物流', + 'title' => '常州松宇货运', + ], + [ + 'qty' => '2', + 'unit_price' => '30', + 'amount' => '60', + 'remark' => '不记账厂家', + 'title' => '', + ], + ]); + + $this->assertSame(3, $summary['count']); + $this->assertSame(2, $summary['supplier_rows']); + $this->assertSame(4.0, $summary['supplier_qty']); + $this->assertSame(1009.0, $summary['supplier_amount']); + $this->assertSame(2, $summary['logistics_rows']); + $this->assertSame(2.0, $summary['logistics_qty']); + $this->assertSame(509.0, $summary['logistics_amount']); + $this->assertSame(['常州松宇货运'], $summary['logistics_companies']); + $this->assertSame(1, $summary['no_bookkeeping_rows']); + } + + public function test_full_amount_row_is_reduced_when_quantity_was_not_pre_split(): void + { + $row = (new SupplierReturnAccounting())->enrich([ + 'qty' => '4', + 'unit_price' => '250', + 'amount' => '1000', + 'remark' => '原数量4件,CZ008412N1178V01*1记账物流', + 'title' => '常州松宇货运', + ]); + + $this->assertSame(1.0, $row['_logistics_qty']); + $this->assertSame(250.0, $row['_logistics_amount']); + $this->assertSame(3.0, $row['_supplier_qty']); + $this->assertSame(750.0, $row['_supplier_amount']); + } +}