469 lines
20 KiB
PHP
469 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Business;
|
|
|
|
use App\Utilities\FinanceLedgerAccountStatus;
|
|
use App\Utilities\FinanceLedgerAccountShouldKeep;
|
|
use App\Utilities\FinanceLedgerAccountType;
|
|
use App\Utilities\FinanceLedgerAccess;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\Rule;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerAccountTemplate;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerCalculator;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerPayableMappingBulk;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerShopMappingBulk;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class FinanceLedger extends Controller
|
|
{
|
|
public function downloadAccountTemplate(FinanceLedgerAccountTemplate $template): StreamedResponse
|
|
{
|
|
return $template->download();
|
|
}
|
|
|
|
public function unavailableBalanceTasks(Request $request)
|
|
{
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
$abnormalStatuses = array_values(array_diff(
|
|
array_keys(FinanceLedgerAccountStatus::OPTIONS),
|
|
[FinanceLedgerAccountStatus::NORMAL],
|
|
));
|
|
$query = $connection->table('fin_accounts')->whereIn(
|
|
'account_short',
|
|
$connection->table('fin_account_statuses')
|
|
->select('account_short')
|
|
->whereIn('status', $abnormalStatuses),
|
|
);
|
|
$keyword = trim((string) $request->query('pending_q', ''));
|
|
|
|
if ($keyword !== '') {
|
|
$query->where(function ($builder) use ($keyword): void {
|
|
foreach (['company_name', 'account_short', 'account_no', 'owner'] as $column) {
|
|
$builder->orWhere($column, 'like', '%' . $keyword . '%');
|
|
}
|
|
});
|
|
}
|
|
|
|
$summaryQuery = clone $query;
|
|
$summary = [
|
|
'count' => (clone $query)->count(),
|
|
'balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2),
|
|
'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2),
|
|
'daily_available' => round((float) (clone $summaryQuery)->sum('daily_available'), 2),
|
|
];
|
|
|
|
$rows = $query
|
|
->orderByDesc('blocked_amount')
|
|
->orderBy('company_name')
|
|
->orderBy('account_short')
|
|
->paginate(50, ['*'], 'pending_page')
|
|
->withQueryString();
|
|
|
|
$accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all();
|
|
$statuses = $accountShorts === []
|
|
? collect()
|
|
: $connection->table('fin_account_statuses')
|
|
->whereIn('account_short', $accountShorts)
|
|
->pluck('status', 'account_short');
|
|
$typeOverrides = $accountShorts === []
|
|
? collect()
|
|
: $connection->table('fin_account_type_overrides')
|
|
->whereIn('account_short', $accountShorts)
|
|
->pluck('account_type', 'account_short');
|
|
$todos = $accountShorts === []
|
|
? collect()
|
|
: $connection->table('fin_account_todos')
|
|
->whereIn('account_short', $accountShorts)
|
|
->get()
|
|
->keyBy('account_short');
|
|
|
|
$rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides, $todos) {
|
|
$row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null);
|
|
if ($typeOverrides->has($row->account_short)) {
|
|
$row->account_type = (string) ($typeOverrides->get($row->account_short) ?? '');
|
|
}
|
|
$todo = $todos->get($row->account_short);
|
|
$row->todo_abnormal_reason = $todo->abnormal_reason ?? null;
|
|
$row->todo_handling_method = $todo->handling_method ?? null;
|
|
$row->todo_owner_name = $todo->owner_name ?? null;
|
|
$row->todo_started_at = $todo->started_at ?? null;
|
|
$row->todo_completed_at = $todo->completed_at ?? null;
|
|
$row->todo_current_progress = $todo->current_progress ?? null;
|
|
|
|
return $row;
|
|
});
|
|
|
|
return view('business.pages.financial-ledger.unavailable-balance-tasks', [
|
|
'rows' => $rows,
|
|
'summary' => $summary,
|
|
'keyword' => $keyword,
|
|
'backUrl' => route('business.pages.show', [
|
|
'section' => 'financial-operations',
|
|
'page' => 'ledger',
|
|
]) . '?tab=accounts',
|
|
'canEdit' => FinanceLedgerAccess::canEditAccountStatus(user()),
|
|
'handlingMethods' => [
|
|
'online' => '线上',
|
|
'offline' => '线下',
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function updateAccountTodo(Request $request)
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改待办事项。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['required', 'string', 'max:100'],
|
|
'abnormal_reason' => ['nullable', 'string', 'max:1000'],
|
|
'handling_method' => ['nullable', 'string', Rule::in(['online', 'offline'])],
|
|
'owner_name' => ['nullable', 'string', 'max:100'],
|
|
'started_at' => ['nullable', 'date'],
|
|
'completed_at' => ['nullable', 'date'],
|
|
'current_progress' => ['nullable', 'string', 'max:100'],
|
|
]);
|
|
|
|
$accountShort = trim((string) $data['account_short']);
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
abort_unless(
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
|
422,
|
|
'账户简称不存在,请刷新待办清单后重试。',
|
|
);
|
|
|
|
$abnormalStatuses = array_values(array_diff(
|
|
array_keys(FinanceLedgerAccountStatus::OPTIONS),
|
|
[FinanceLedgerAccountStatus::NORMAL],
|
|
));
|
|
$status = $connection->table('fin_account_statuses')
|
|
->where('account_short', $accountShort)
|
|
->value('status');
|
|
abort_unless(
|
|
$status !== null && in_array(FinanceLedgerAccountStatus::normalize((string) $status), $abnormalStatuses, true),
|
|
422,
|
|
'账户当前已不是异常状态,无需继续填写待办。',
|
|
);
|
|
|
|
$now = now();
|
|
$values = [
|
|
'abnormal_reason' => $data['abnormal_reason'] ?? null,
|
|
'handling_method' => $data['handling_method'] ?? null,
|
|
'owner_name' => $data['owner_name'] ?? null,
|
|
'started_at' => $data['started_at'] ?? null,
|
|
'completed_at' => $data['completed_at'] ?? null,
|
|
'current_progress' => $data['current_progress'] ?? null,
|
|
'updated_by' => (string) user()->name,
|
|
'updated_at' => $now,
|
|
];
|
|
$todos = $connection->table('fin_account_todos');
|
|
$existing = $todos->where('account_short', $accountShort)->exists();
|
|
if ($existing) {
|
|
$todos->where('account_short', $accountShort)->update($values);
|
|
} else {
|
|
$todos->insert($values + [
|
|
'account_short' => $accountShort,
|
|
'created_at' => $now,
|
|
]);
|
|
}
|
|
|
|
return redirect()->back()->with('success', '待办事项已保存');
|
|
}
|
|
|
|
public function updateMapping(Request $request, int $id, FinanceLedgerCalculator $calculator): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以修改账户映射。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['nullable', 'string', 'max:100'],
|
|
]);
|
|
|
|
$account = trim((string) ($data['account_short'] ?? ''));
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
$row = $connection->table('fin_account_map')->where('id', $id)->first();
|
|
abort_if($row === null, 404);
|
|
|
|
if ($account !== '') {
|
|
$exists = $connection->table('fin_accounts')->where('account_short', $account)->exists();
|
|
abort_unless($exists, 422, '账户简称不存在,请从账户总表中选择。');
|
|
}
|
|
|
|
$old = (string) ($row->account_short ?? '');
|
|
$connection->table('fin_account_map')->where('id', $id)->update([
|
|
'account_short' => $account !== '' ? $account : null,
|
|
'updated_by' => (string) user()->name,
|
|
'updated_at' => now(),
|
|
]);
|
|
$connection->table('fin_account_map_events')->insert([
|
|
'source' => (string) $row->source,
|
|
'source_value' => (string) $row->source_value,
|
|
'old_account_short' => $old !== '' ? $old : null,
|
|
'new_account_short' => $account !== '' ? $account : null,
|
|
'user_id' => (int) user()->id,
|
|
'user_name' => (string) user()->name,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
if ($request->boolean('recalculate', true)) {
|
|
$calculator->recalculate();
|
|
}
|
|
|
|
$mapped = $connection->table('fin_account_map')
|
|
->whereNotNull('account_short')->where('account_short', '<>', '')->count();
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'account_short' => $account,
|
|
'mapped' => $mapped,
|
|
'total' => $connection->table('fin_account_map')->count(),
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function updateAccountStatus(Request $request): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户状态。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['required', 'string', 'max:100'],
|
|
'status' => ['required', 'string', Rule::in(array_keys(FinanceLedgerAccountStatus::OPTIONS))],
|
|
]);
|
|
|
|
$accountShort = trim((string) $data['account_short']);
|
|
abort_if($accountShort === '', 422, '账户简称不能为空。');
|
|
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
abort_unless(
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
|
422,
|
|
'账户简称不存在,请刷新账户总表后重试。',
|
|
);
|
|
|
|
$status = FinanceLedgerAccountStatus::normalize((string) $data['status']);
|
|
$statuses = $connection->table('fin_account_statuses');
|
|
$existing = $statuses->where('account_short', $accountShort)->first();
|
|
$now = now();
|
|
$values = [
|
|
'status' => $status,
|
|
'updated_by' => (string) user()->name,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if ($existing === null) {
|
|
$statuses->insert($values + [
|
|
'account_short' => $accountShort,
|
|
'created_at' => $now,
|
|
]);
|
|
} else {
|
|
$statuses->where('account_short', $accountShort)->update($values);
|
|
}
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'account_short' => $accountShort,
|
|
'status' => $status,
|
|
'label' => FinanceLedgerAccountStatus::label($status),
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function updateAccountType(Request $request): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditAccountType(user()), 403, '只有财务台账管理员可以修改账户类型。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['required', 'string', 'max:100'],
|
|
'account_type' => ['nullable', 'string', 'max:50'],
|
|
]);
|
|
|
|
$accountShort = trim((string) $data['account_short']);
|
|
$accountType = trim((string) ($data['account_type'] ?? ''));
|
|
abort_if($accountShort === '', 422, '账户简称不能为空。');
|
|
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
abort_unless(
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
|
422,
|
|
'账户简称不存在,请刷新账户总表后重试。',
|
|
);
|
|
|
|
if ($accountType !== '') {
|
|
$allowedTypes = FinanceLedgerAccountType::options(
|
|
$connection->table('fin_accounts')
|
|
->whereNotNull('account_type')->where('account_type', '<>', '')
|
|
->distinct()->pluck('account_type')->all(),
|
|
$connection->table('fin_account_type_overrides')
|
|
->whereNotNull('account_type')->where('account_type', '<>', '')
|
|
->distinct()->pluck('account_type')->all(),
|
|
);
|
|
abort_unless(in_array($accountType, $allowedTypes, true), 422, '请选择现有账户类型。');
|
|
}
|
|
|
|
$overrides = $connection->table('fin_account_type_overrides');
|
|
$existing = $overrides->where('account_short', $accountShort)->first();
|
|
$now = now();
|
|
$values = [
|
|
'account_type' => $accountType !== '' ? $accountType : null,
|
|
'updated_by' => (string) user()->name,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if ($existing === null) {
|
|
$overrides->insert($values + [
|
|
'account_short' => $accountShort,
|
|
'created_at' => $now,
|
|
]);
|
|
} else {
|
|
$overrides->where('account_short', $accountShort)->update($values);
|
|
}
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'account_short' => $accountShort,
|
|
'account_type' => $accountType,
|
|
'label' => $accountType !== '' ? $accountType : '未填写',
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function updateAccountPlan(Request $request): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改公司规划。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['required', 'string', 'max:100'],
|
|
'company_plan' => ['nullable', 'string', 'max:50'],
|
|
]);
|
|
$accountShort = trim((string) $data['account_short']);
|
|
$plan = trim((string) ($data['company_plan'] ?? ''));
|
|
if ($accountShort === '') {
|
|
abort(422, '账户简称不能为空。');
|
|
}
|
|
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
abort_unless(
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
|
422,
|
|
'账户简称不存在,请刷新账户总表后重试。',
|
|
);
|
|
$allowedPlans = $connection->table('fin_accounts')
|
|
->whereNotNull('company_plan')
|
|
->where('company_plan', '<>', '')
|
|
->distinct()
|
|
->pluck('company_plan')
|
|
->map(static fn (mixed $value): string => trim((string) $value))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
abort_unless($plan === '' || in_array($plan, $allowedPlans, true), 422, '请选择现有公司规划。');
|
|
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->update([
|
|
'company_plan' => $plan !== '' ? $plan : null,
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'account_short' => $accountShort,
|
|
'company_plan' => $plan,
|
|
'label' => $plan !== '' ? $plan : '未填写',
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function updateAccountKeep(Request $request): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户是否保留。');
|
|
|
|
$data = $request->validate([
|
|
'account_short' => ['required', 'string', 'max:100'],
|
|
'account_should_keep' => ['nullable', 'string', Rule::in(FinanceLedgerAccountShouldKeep::OPTIONS)],
|
|
]);
|
|
$accountShort = trim((string) $data['account_short']);
|
|
$keep = FinanceLedgerAccountShouldKeep::normalize((string) ($data['account_should_keep'] ?? ''));
|
|
if ($accountShort === '') {
|
|
abort(422, '账户简称不能为空。');
|
|
}
|
|
|
|
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
abort_unless(
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
|
422,
|
|
'账户简称不存在,请刷新账户总表后重试。',
|
|
);
|
|
|
|
$connection->table('fin_accounts')->where('account_short', $accountShort)->update([
|
|
'account_should_keep' => $keep !== '' ? $keep : null,
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'account_short' => $accountShort,
|
|
'account_should_keep' => $keep,
|
|
'label' => $keep !== '' ? $keep : '未填写',
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function previewShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。');
|
|
|
|
$preview = $bulk->preview();
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'total' => $preview['total'],
|
|
'matched' => $preview['matched'],
|
|
'unmatched' => $preview['unmatched'],
|
|
'sample' => array_slice($preview['candidates'], 0, 10),
|
|
'unmatched_sample' => array_slice($preview['unmatched_values'], 0, 10),
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function applyShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。');
|
|
|
|
$result = $bulk->apply(user());
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'total' => $result['total'],
|
|
'mapped' => $result['mapped'],
|
|
'unmatched' => $result['unmatched'],
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function previewPayableMapping(FinanceLedgerPayableMappingBulk $bulk): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。');
|
|
|
|
$preview = $bulk->preview();
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'total' => $preview['total'],
|
|
'eligible' => $preview['eligible'],
|
|
'ambiguous' => $preview['ambiguous'],
|
|
'missing' => $preview['missing'],
|
|
'sample' => array_slice($preview['candidates'], 0, 10),
|
|
'ambiguous_sample' => array_slice($preview['ambiguous_values'], 0, 10),
|
|
'missing_sample' => array_slice($preview['missing_values'], 0, 10),
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
|
|
public function applyPayableMapping(FinanceLedgerPayableMappingBulk $bulk): JsonResponse
|
|
{
|
|
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。');
|
|
|
|
$result = $bulk->apply(user());
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'total' => $result['total'],
|
|
'mapped' => $result['mapped'],
|
|
'ambiguous' => $result['ambiguous'],
|
|
'missing' => $result['missing'],
|
|
], 200, ['Cache-Control' => 'private, no-store']);
|
|
}
|
|
}
|