224 lines
8.8 KiB
PHP
224 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Business;
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Modules\EcommerceFinance\Services\SupplierDingTalkNotifier;
|
|
use Modules\EcommerceFinance\Services\SupplierReturnCsvExporter;
|
|
use Modules\EcommerceFinance\Services\SupplierReturnConfirmation;
|
|
use Modules\EcommerceFinance\Services\SupplierStatementMatcher;
|
|
use Modules\EcommerceFinance\Services\SupplierStatementStore;
|
|
use Modules\EcommerceFinance\Services\SupplierStatementTemplate;
|
|
use Modules\EcommerceFinance\View\SupplierReconciliationPageData;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
use Throwable;
|
|
|
|
/**
|
|
* 供应商对账:对账单模板下载 + 厂家对账单上传(严格按模板校验)。
|
|
*/
|
|
class SupplierReconciliation extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly SupplierStatementTemplate $template,
|
|
private readonly SupplierStatementStore $statements,
|
|
private readonly SupplierReturnConfirmation $returnConfirmations,
|
|
private readonly SupplierReconciliationPageData $pageData,
|
|
private readonly SupplierReturnCsvExporter $returnCsvExporter,
|
|
private readonly SupplierDingTalkNotifier $dingTalkNotifier,
|
|
) {
|
|
$this->middleware('auth');
|
|
$this->middleware('company.identify');
|
|
}
|
|
|
|
public function template(): BinaryFileResponse
|
|
{
|
|
$path = $this->template->path();
|
|
abort_unless(is_file($path), 404, '对账单模板不存在。');
|
|
|
|
return response()->download($path, $this->template->filename());
|
|
}
|
|
|
|
public function upload(Request $request): JsonResponse|RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:51200'],
|
|
'supplier' => ['required', 'string', 'max:191'],
|
|
]);
|
|
|
|
$supplier = trim((string) $request->input('supplier'));
|
|
if ($supplier === '') {
|
|
abort(422, '请选择厂家。');
|
|
}
|
|
|
|
$records = $this->template->parse($request->file('import'), $supplier);
|
|
$rows = $this->statements->save(
|
|
$supplier,
|
|
$records,
|
|
(string) $request->file('import')->getClientOriginalName(),
|
|
(int) (company_id() ?: 1),
|
|
user_id() ? (int) user_id() : null,
|
|
);
|
|
|
|
$message = "对账单已保存:{$supplier} 共 {$rows} 行。";
|
|
|
|
return $request->expectsJson()
|
|
? response()->json(['message' => $message, 'rows' => $rows])
|
|
: back()->with('success', $message);
|
|
}
|
|
|
|
public function confirmReturn(Request $request): RedirectResponse
|
|
{
|
|
return $this->markReturn($request, SupplierReturnConfirmation::STATUS_CONFIRMED, '已确认该行退货扣减。');
|
|
}
|
|
|
|
public function disputeReturn(Request $request): RedirectResponse
|
|
{
|
|
return $this->markReturn($request, SupplierReturnConfirmation::STATUS_DISPUTED, '已提交异议,复核通过前该行暂不扣减。');
|
|
}
|
|
|
|
public function confirmAllReturns(Request $request): RedirectResponse
|
|
{
|
|
[$supplier, $companyId] = $this->returnTarget($request);
|
|
$keys = array_values(array_map(
|
|
static fn (array $row): string => (string) ($row['_action_key'] ?? ''),
|
|
$this->pageData->returnRowsForFilters($supplier, $companyId, $request->all()),
|
|
));
|
|
$confirmed = $this->returnConfirmations->confirmAll(
|
|
$supplier,
|
|
$keys,
|
|
$companyId,
|
|
user_id() ? (int) user_id() : null,
|
|
);
|
|
|
|
return back()->with('success', "已全部确认:{$confirmed} 行退货扣减生效(异议行保持暂不扣减)。");
|
|
}
|
|
|
|
public function exportReturns(Request $request): StreamedResponse
|
|
{
|
|
[$supplier, $companyId] = $this->returnTarget($request);
|
|
$rows = $this->pageData->returnRowsForFilters($supplier, $companyId, $request->query());
|
|
|
|
return $this->returnCsvExporter->download($rows, $supplier);
|
|
}
|
|
|
|
public function saveDingTalkRobot(Request $request): RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'supplier' => ['required', 'string', 'max:191'],
|
|
'dingtalk_webhook' => ['nullable', 'string', 'max:2048'],
|
|
'dingtalk_secret' => ['nullable', 'string', 'max:256'],
|
|
'dingtalk_at_all' => ['nullable', 'boolean'],
|
|
]);
|
|
[$supplier] = $this->returnTarget($request);
|
|
$this->dingTalkNotifier->save(
|
|
$supplier,
|
|
(string) $request->input('dingtalk_webhook', ''),
|
|
(string) $request->input('dingtalk_secret', ''),
|
|
$request->boolean('dingtalk_at_all'),
|
|
);
|
|
|
|
return back()->with('success', "{$supplier} 的钉钉群机器人已保存。");
|
|
}
|
|
|
|
public function pushDifferences(Request $request): RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'supplier' => ['required', 'string', 'max:191'],
|
|
'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'],
|
|
'reconciler' => ['nullable', 'string', 'max:191'],
|
|
'scope' => ['required', 'in:all,row,selected'],
|
|
'barcode' => ['nullable', 'string', 'max:191'],
|
|
'difference_type' => ['nullable', 'string', 'max:32'],
|
|
'unit_price' => ['nullable', 'numeric'],
|
|
'selected_rows' => ['nullable', 'array', 'max:600'],
|
|
'selected_rows.*' => ['string', 'max:2048'],
|
|
]);
|
|
[$supplier, $companyId] = $this->returnTarget($request);
|
|
$detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->all());
|
|
$differences = array_values(array_filter(
|
|
$detail['rows'],
|
|
static fn (array $row): bool => ($row['type'] ?? '') !== SupplierStatementMatcher::TYPE_MATCH,
|
|
));
|
|
|
|
if ($request->input('scope') === 'row') {
|
|
$barcode = trim((string) $request->input('barcode'));
|
|
$type = trim((string) $request->input('difference_type'));
|
|
$unitPrice = (float) $request->input('unit_price');
|
|
$differences = array_values(array_filter(
|
|
$differences,
|
|
static fn (array $row): bool => (string) ($row['barcode'] ?? '') === $barcode
|
|
&& (string) ($row['type'] ?? '') === $type
|
|
&& abs((float) ($row['unitPrice'] ?? 0) - $unitPrice) < 0.005,
|
|
));
|
|
} elseif ($request->input('scope') === 'selected') {
|
|
$selected = [];
|
|
foreach ((array) $request->input('selected_rows', []) as $token) {
|
|
$row = json_decode((string) $token, true);
|
|
if (is_array($row)) {
|
|
$selected[$this->differenceRowKey($row)] = true;
|
|
}
|
|
}
|
|
$differences = array_values(array_filter(
|
|
$differences,
|
|
fn (array $row): bool => isset($selected[$this->differenceRowKey($row)]),
|
|
));
|
|
}
|
|
|
|
if ($differences === []) {
|
|
return back()->withErrors(['dingtalk_push' => '当前筛选没有可推送的差异。']);
|
|
}
|
|
|
|
try {
|
|
$result = $this->dingTalkNotifier->push($supplier, $differences, [
|
|
'month' => (string) $request->input('month', ''),
|
|
'reconciler' => (string) $request->input('reconciler', ''),
|
|
]);
|
|
} catch (Throwable $exception) {
|
|
return back()->withErrors(['dingtalk_push' => '推送失败:' . $exception->getMessage()]);
|
|
}
|
|
|
|
return back()->with(
|
|
'success',
|
|
"已推送 {$result['rows']} 条差异到 {$supplier} 钉钉群({$result['messages']} 条消息)。",
|
|
);
|
|
}
|
|
|
|
private function markReturn(Request $request, string $status, string $message): RedirectResponse
|
|
{
|
|
$request->validate(['key' => ['required', 'string', 'max:191']]);
|
|
[$supplier, $companyId] = $this->returnTarget($request);
|
|
$this->returnConfirmations->mark(
|
|
$supplier,
|
|
(string) $request->input('key'),
|
|
$status,
|
|
$companyId,
|
|
user_id() ? (int) user_id() : null,
|
|
);
|
|
|
|
return back()->with('success', $message);
|
|
}
|
|
|
|
/** @return array{0:string,1:int} */
|
|
private function returnTarget(Request $request): array
|
|
{
|
|
$request->validate(['supplier' => ['required', 'string', 'max:191']]);
|
|
|
|
return [trim((string) $request->input('supplier')), (int) (company_id() ?: 1)];
|
|
}
|
|
|
|
/** @param array<string,mixed> $row */
|
|
private function differenceRowKey(array $row): string
|
|
{
|
|
return implode("\0", [
|
|
trim((string) ($row['barcode'] ?? '')),
|
|
trim((string) ($row['type'] ?? '')),
|
|
number_format((float) ($row['unitPrice'] ?? 0), 4, '.', ''),
|
|
]);
|
|
}
|
|
|
|
}
|