parent
32cd0d0a15
commit
db08b2c264
@ -6,6 +6,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\EcommerceFinance\Services\SupplierComparisonCsvExporter;
|
||||
use Modules\EcommerceFinance\Services\SupplierDingTalkNotifier;
|
||||
use Modules\EcommerceFinance\Services\SupplierReturnCsvExporter;
|
||||
use Modules\EcommerceFinance\Services\SupplierReturnConfirmation;
|
||||
@ -29,6 +30,7 @@ class SupplierReconciliation extends Controller
|
||||
private readonly SupplierReconciliationPageData $pageData,
|
||||
private readonly SupplierReturnCsvExporter $returnCsvExporter,
|
||||
private readonly SupplierDingTalkNotifier $dingTalkNotifier,
|
||||
private readonly SupplierComparisonCsvExporter $comparisonCsvExporter,
|
||||
) {
|
||||
$this->middleware('auth');
|
||||
$this->middleware('company.identify');
|
||||
@ -105,6 +107,23 @@ class SupplierReconciliation extends Controller
|
||||
return $this->returnCsvExporter->download($rows, $supplier);
|
||||
}
|
||||
|
||||
public function exportComparison(Request $request): StreamedResponse
|
||||
{
|
||||
$request->validate([
|
||||
'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'],
|
||||
'reconciler' => ['nullable', 'string', 'max:191'],
|
||||
]);
|
||||
[$supplier, $companyId] = $this->returnTarget($request);
|
||||
$detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->query());
|
||||
|
||||
return $this->comparisonCsvExporter->download(
|
||||
$detail,
|
||||
$supplier,
|
||||
trim((string) $request->query('month', '')),
|
||||
trim((string) $request->query('reconciler', '')),
|
||||
);
|
||||
}
|
||||
|
||||
public function saveDingTalkRobot(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/** 财务端货款对账比对汇总 CSV。 */
|
||||
class SupplierComparisonCsvExporter
|
||||
{
|
||||
private const HEADERS = [
|
||||
'采购单号', '日期', '条码', '厂家条码', '比对结果',
|
||||
'我方数量', '我方单价', '我方金额',
|
||||
'厂家数量', '厂家单价', '厂家金额',
|
||||
'数量差异', '金额差异', '结算口径', '结算金额',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $detail
|
||||
*/
|
||||
public function download(
|
||||
array $detail,
|
||||
string $supplier,
|
||||
string $month,
|
||||
string $reconciler,
|
||||
): StreamedResponse {
|
||||
$safeSupplier = preg_replace('/[\\\\\\/:*?"<>|]+/u', '_', $supplier) ?: '厂家';
|
||||
$period = $month !== '' ? preg_replace('/[^0-9-]+/u', '', $month) : '全部月份';
|
||||
$filename = $safeSupplier . '_货款对账比对_' . $period . '_' . now()->format('Ymd_His') . '.csv';
|
||||
$summary = (array) ($detail['summary'] ?? []);
|
||||
$rows = (array) ($detail['rows'] ?? []);
|
||||
|
||||
return response()->streamDownload(function () use ($detail, $rows, $summary, $supplier, $month, $reconciler): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
if ($csv === false) {
|
||||
throw new RuntimeException('无法创建导出文件。');
|
||||
}
|
||||
|
||||
fwrite($csv, "\xEF\xBB\xBF");
|
||||
$this->write($csv, ['供应商', $supplier, '月份', $month !== '' ? $month : '全部月份', '对账人', $reconciler !== '' ? $reconciler : '全部']);
|
||||
$this->write($csv, [
|
||||
'我方数据行数', (int) ($detail['mineRows'] ?? 0),
|
||||
'厂家数据行数', (int) ($detail['theirsRows'] ?? 0),
|
||||
'比对结果条数', count($rows),
|
||||
]);
|
||||
$this->write($csv, [
|
||||
'双方一致', (int) ($summary['match'] ?? 0),
|
||||
'厂家多', (int) ($summary['supplierExtra'] ?? 0),
|
||||
'数量/金额不一致', (int) ($summary['mismatch'] ?? 0),
|
||||
'厂家少', (int) ($summary['supplierMissing'] ?? 0),
|
||||
]);
|
||||
$this->write($csv, [
|
||||
'我方金额合计', $this->money($summary['mineAmount'] ?? 0),
|
||||
'厂家金额合计', $this->money($summary['theirsAmount'] ?? 0),
|
||||
'厂家多算金额', $this->money($summary['supplierOverAmount'] ?? 0),
|
||||
'按口径结算合计', $this->money($summary['settleAmount'] ?? 0),
|
||||
'差异金额', $this->money($summary['differenceAmount'] ?? 0),
|
||||
]);
|
||||
$this->write($csv, []);
|
||||
$this->write($csv, self::HEADERS);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$this->write($csv, [
|
||||
$row['purchaseNo'] ?? '',
|
||||
$row['date'] ?? '',
|
||||
$row['barcode'] ?? '',
|
||||
$row['supplierBarcode'] ?? '',
|
||||
$row['type'] ?? '',
|
||||
$this->number($row['mineQty'] ?? 0),
|
||||
$this->money($row['mineUnitPrice'] ?? 0),
|
||||
$this->money($row['mineAmount'] ?? 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['note'] ?? '',
|
||||
$this->money($row['settleAmount'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($csv);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param resource $csv @param list<mixed> $values */
|
||||
private function write($csv, array $values): void
|
||||
{
|
||||
fputcsv($csv, array_map(fn (mixed $value): string => $this->csvCell($value), $values));
|
||||
}
|
||||
|
||||
private function quantityDifference(mixed $value): string
|
||||
{
|
||||
$difference = (float) $value;
|
||||
if (abs($difference) < 0.005) {
|
||||
return '一致';
|
||||
}
|
||||
|
||||
return '厂家' . ($difference > 0 ? '多 ' : '少 ') . $this->number(abs($difference));
|
||||
}
|
||||
|
||||
private function amountDifference(mixed $value): string
|
||||
{
|
||||
$difference = (float) $value;
|
||||
if (abs($difference) < 0.005) {
|
||||
return '一致';
|
||||
}
|
||||
|
||||
return '厂家' . ($difference > 0 ? '多 ¥' : '少 ¥') . $this->money(abs($difference));
|
||||
}
|
||||
|
||||
private function number(mixed $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
private function money(mixed $value): string
|
||||
{
|
||||
return number_format((float) $value, 2, '.', '');
|
||||
}
|
||||
|
||||
private function csvCell(mixed $value): string
|
||||
{
|
||||
$text = trim((string) $value);
|
||||
if (is_numeric(str_replace(',', '', $text))) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return preg_match('/^[=+\-@\t\r]/u', $text) === 1 ? "\t" . $text : $text;
|
||||
}
|
||||
}
|
||||
@ -140,6 +140,12 @@
|
||||
</span>
|
||||
<span style="flex:1 1 auto"></span>
|
||||
<a href="{{ $listUrl }}" class="sr-button">← 返回供应商列表</a>
|
||||
<a href="{{ route('business.supplier-reconciliation.comparison.export', array_filter([
|
||||
'supplier' => $selectedSupplier,
|
||||
'month' => $month,
|
||||
'reconciler' => $reconciler,
|
||||
], static fn (mixed $value): bool => $value !== '' && $value !== null)) }}"
|
||||
class="sr-button">导出比对汇总</a>
|
||||
<a href="{{ $statementTemplateUrl }}" class="sr-button">下载对账单模板</a>
|
||||
@if ($differenceCount > 0)
|
||||
<form method="POST"
|
||||
|
||||
@ -409,6 +409,10 @@ Route::get('workspace/supplier-reconciliation/returns/export', [SupplierReconcil
|
||||
->middleware('business.page:supplier-reconciliation,supplier-data')
|
||||
->name('business.supplier-reconciliation.returns.export');
|
||||
|
||||
Route::get('workspace/supplier-reconciliation/comparison/export', [SupplierReconciliationActions::class, 'exportComparison'])
|
||||
->middleware('business.page:supplier-reconciliation,finance-data')
|
||||
->name('business.supplier-reconciliation.comparison.export');
|
||||
|
||||
// 退货扣减确认:逐行确认/异议 + 一键全部确认
|
||||
Route::post('workspace/supplier-reconciliation/returns/confirm', [SupplierReconciliationActions::class, 'confirmReturn'])
|
||||
->middleware('business.page:supplier-reconciliation,supplier-data')
|
||||
|
||||
@ -115,6 +115,7 @@ class SupplierReconciliationTest extends FeatureTestCase
|
||||
->assertSeeText('厂家多 1.00')
|
||||
->assertSeeText('厂家多 ¥128.00')
|
||||
->assertSeeText('编号-ZSJ-1N1097V01')
|
||||
->assertSeeText('导出比对汇总')
|
||||
->assertSee('sr-comparison-scroll', false)
|
||||
->assertSeeText('每页 15 条')
|
||||
->assertSeeText('ZSJ-3')
|
||||
@ -210,6 +211,59 @@ class SupplierReconciliationTest extends FeatureTestCase
|
||||
->assertDontSeeText('A-01');
|
||||
}
|
||||
|
||||
public function test_finance_comparison_export_includes_summary_and_all_filtered_rows(): void
|
||||
{
|
||||
$companyId = (int) $this->company->id;
|
||||
$store = app(SupplierPayableOverrideStore::class);
|
||||
foreach ([
|
||||
'mine:1' => ['purchaseNo' => 'PO-1', 'skuCode' => 'A-1', 'qty' => '2', 'unitPrice' => '100', 'amount' => '200', 'reconciler' => '张三'],
|
||||
'mine:2' => ['purchaseNo' => 'PO-2', 'skuCode' => 'B-1', 'qty' => '1', 'unitPrice' => '50', 'amount' => '50', 'reconciler' => '张三'],
|
||||
'mine:other' => ['purchaseNo' => 'PO-OTHER', 'skuCode' => 'C-1', 'qty' => '1', 'unitPrice' => '30', 'amount' => '30', 'reconciler' => '李四'],
|
||||
] as $key => $row) {
|
||||
$store->save('final:supplier:payment-reconciliation', $key, $row + [
|
||||
'summary_month' => $key === 'mine:other' ? '2026-10' : '2026-09',
|
||||
'date' => '2026-09-30',
|
||||
'supplier' => '晨丰',
|
||||
], $companyId, null);
|
||||
}
|
||||
$this->seedStatement('晨丰', [
|
||||
array_merge($this->statementRow('A-1N1097V01', '100.00', '2', '200.00'), ['purchaseNo' => 'PO-1']),
|
||||
array_merge($this->statementRow('B-1N1097V01', '50.00', '2', '100.00'), ['purchaseNo' => 'PO-2']),
|
||||
array_merge($this->statementRow('D-1N1097V01', '80.00', '1', '80.00'), ['purchaseNo' => 'PO-D']),
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.supplier-reconciliation.comparison.export', [
|
||||
'supplier' => '晨丰',
|
||||
'month' => '2026-09',
|
||||
'reconciler' => '张三',
|
||||
]));
|
||||
$content = $response->streamedContent();
|
||||
|
||||
$response->assertOk()
|
||||
->assertDownload()
|
||||
->assertHeader('content-type', 'text/csv; charset=UTF-8');
|
||||
|
||||
foreach ([
|
||||
'供应商,晨丰',
|
||||
'月份,2026-09',
|
||||
'对账人,张三',
|
||||
'双方一致,1',
|
||||
'厂家多,1',
|
||||
'数量/金额不一致,1',
|
||||
'厂家少,0',
|
||||
'采购单号,日期,条码,厂家条码,比对结果',
|
||||
'PO-1',
|
||||
'PO-2',
|
||||
'PO-D',
|
||||
'厂家多 1',
|
||||
'厂家多 ¥50.00',
|
||||
] as $expected) {
|
||||
$this->assertStringContainsString($expected, $content);
|
||||
}
|
||||
|
||||
$this->assertStringNotContainsString('PO-OTHER', $content);
|
||||
}
|
||||
|
||||
public function test_supplier_page_shows_uploads_and_return_rows(): void
|
||||
{
|
||||
$this->seedStatement('晨丰', [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user