feat: 支持维护财务台账账户类型
This commit is contained in:
parent
76a940fe97
commit
8b76e100cf
@ -107,4 +107,62 @@ class FinanceLedger extends Controller
|
||||
'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 = array_values(array_unique(array_merge(
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,10 +30,15 @@ class FinanceLedgerAccess
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户状态与账户映射使用同一组财务台账编辑角色。
|
||||
* 账户状态、账户类型与账户映射使用同一组财务台账编辑角色。
|
||||
*/
|
||||
public static function canEditAccountStatus(?User $user = null): bool
|
||||
{
|
||||
return self::canEditMapping($user);
|
||||
}
|
||||
|
||||
public static function canEditAccountType(?User $user = null): bool
|
||||
{
|
||||
return self::canEditMapping($user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 财务台账账户类型人工维护值。
|
||||
*
|
||||
* 基础表 fin_accounts 在导入时会被全量覆盖,因此人工调整后的类型单独保存。
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
private function connection(): string
|
||||
{
|
||||
return (string) config('upstream_mirrors.connection', 'business_mirror');
|
||||
}
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$schema = Schema::connection($this->connection());
|
||||
|
||||
if (! $schema->hasTable('fin_account_type_overrides')) {
|
||||
$schema->create('fin_account_type_overrides', function (Blueprint $table): void {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('account_short', 100)->unique('fin_account_type_overrides_account_unique');
|
||||
$table->string('account_type', 50)->nullable();
|
||||
$table->string('updated_by', 100)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['account_type'], 'fin_account_type_overrides_type_index');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection())->dropIfExists('fin_account_type_overrides');
|
||||
}
|
||||
};
|
||||
@ -17,9 +17,11 @@
|
||||
| `fin_account_map` | 来源值 → 账户简称 |
|
||||
| `fin_account_map_events` | 映射修改审计 |
|
||||
| `fin_account_statuses` | 账户状态维护(正常、网银异常、限额、工商注销) |
|
||||
| `fin_account_type_overrides` | 账户类型人工维护值 |
|
||||
|
||||
迁移:`database/migrations/2026_10_19_160000_create_finance_ledger_tables.php`、
|
||||
`database/migrations/2026_10_20_120000_create_finance_ledger_account_statuses.php`。
|
||||
`database/migrations/2026_10_20_120000_create_finance_ledger_account_statuses.php`、
|
||||
`database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php`。
|
||||
|
||||
## 本地导入
|
||||
|
||||
@ -61,11 +63,12 @@ docker compose -f docker-compose.local.yml exec -u www-data app \
|
||||
|
||||
- 路由:`/workspace/financial-ledger/ledger`
|
||||
- 页面:账户总表、店铺台账、每日资金流水、动账明细、账户映射、数据来源
|
||||
- 写权限:账户映射和账户状态修改仅系统管理员及配置的财务台账管理员角色;其他账号只读
|
||||
- 写权限:账户映射、账户状态和账户类型修改仅系统管理员及配置的财务台账管理员角色;其他账号只读
|
||||
- 调度:`finance-ledger:sync-rpt` 每天 02:30(Asia/Shanghai)
|
||||
|
||||
账户状态独立保存在 `fin_account_statuses`,重新导入账户基础数据不会覆盖;
|
||||
页面修改使用异步保存,不触发整页刷新。
|
||||
账户状态独立保存在 `fin_account_statuses`,账户类型人工维护值独立保存在
|
||||
`fin_account_type_overrides`,重新导入账户基础数据均不会覆盖;页面修改使用
|
||||
异步保存,不触发整页刷新。
|
||||
|
||||
生产宿主机 `/etc/cron.d` 仍需增加每日调用入口;本项目当前没有运行
|
||||
`schedule:run` 的容器,调度登记不能替代宿主机 cron。
|
||||
|
||||
@ -59,6 +59,7 @@ class FinanceLedgerPageData
|
||||
'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS,
|
||||
'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()),
|
||||
'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()),
|
||||
'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()),
|
||||
];
|
||||
}
|
||||
|
||||
@ -102,20 +103,46 @@ class FinanceLedgerPageData
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['accounts_region' => 'region', 'accounts_type' => 'account_type'] as $parameter => $column) {
|
||||
$value = trim((string) request($parameter, ''));
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if ($value === self::ACCOUNT_FILTER_EMPTY) {
|
||||
$query->where(function (Builder $builder) use ($column): void {
|
||||
$builder->whereNull($column)->orWhere($column, '');
|
||||
$region = trim((string) request('accounts_region', ''));
|
||||
if ($region !== '') {
|
||||
if ($region === self::ACCOUNT_FILTER_EMPTY) {
|
||||
$query->where(function (Builder $builder): void {
|
||||
$builder->whereNull('region')->orWhere('region', '');
|
||||
});
|
||||
} else {
|
||||
$query->where($column, $value);
|
||||
$query->where('region', $region);
|
||||
}
|
||||
}
|
||||
|
||||
$accountType = trim((string) request('accounts_type', ''));
|
||||
if ($accountType !== '') {
|
||||
$query->where(function (Builder $effectiveType) use ($accountType): void {
|
||||
$effectiveType->where(function (Builder $baseType) use ($accountType): void {
|
||||
$baseType->whereNotIn(
|
||||
'account_short',
|
||||
$this->db()->table('fin_account_type_overrides')->select('account_short'),
|
||||
);
|
||||
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
||||
$baseType->where(function (Builder $blank): void {
|
||||
$blank->whereNull('account_type')->orWhere('account_type', '');
|
||||
});
|
||||
} else {
|
||||
$baseType->where('account_type', $accountType);
|
||||
}
|
||||
});
|
||||
|
||||
$overrideType = $this->db()->table('fin_account_type_overrides')->select('account_short');
|
||||
if ($accountType === self::ACCOUNT_FILTER_EMPTY) {
|
||||
$overrideType->where(function (Builder $blank): void {
|
||||
$blank->whereNull('account_type')->orWhere('account_type', '');
|
||||
});
|
||||
} else {
|
||||
$overrideType->where('account_type', $accountType);
|
||||
}
|
||||
$effectiveType->orWhereIn('account_short', $overrideType);
|
||||
});
|
||||
}
|
||||
|
||||
$summaryQuery = clone $query;
|
||||
$summary = [
|
||||
'count' => (clone $query)->count(),
|
||||
@ -135,8 +162,16 @@ class FinanceLedgerPageData
|
||||
: $this->db()->table('fin_account_statuses')
|
||||
->whereIn('account_short', $accountShorts)
|
||||
->pluck('status', 'account_short');
|
||||
$rows->getCollection()->transform(static function ($row) use ($statuses) {
|
||||
$typeOverrides = $accountShorts === []
|
||||
? collect()
|
||||
: $this->db()->table('fin_account_type_overrides')
|
||||
->whereIn('account_short', $accountShorts)
|
||||
->pluck('account_type', 'account_short');
|
||||
$rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides) {
|
||||
$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) ?? '');
|
||||
}
|
||||
|
||||
return $row;
|
||||
});
|
||||
@ -319,13 +354,20 @@ class FinanceLedgerPageData
|
||||
|
||||
private function accountFilterOptions(): array
|
||||
{
|
||||
$baseTypes = $this->db()->table('fin_accounts')
|
||||
->whereNotNull('account_type')->where('account_type', '<>', '')
|
||||
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
||||
$overrideTypes = $this->db()->table('fin_account_type_overrides')
|
||||
->whereNotNull('account_type')->where('account_type', '<>', '')
|
||||
->distinct()->orderBy('account_type')->pluck('account_type')->all();
|
||||
$types = array_values(array_unique(array_merge($baseTypes, $overrideTypes)));
|
||||
sort($types, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return [
|
||||
'regions' => $this->db()->table('fin_accounts')
|
||||
->whereNotNull('region')->where('region', '<>', '')
|
||||
->distinct()->orderBy('region')->pluck('region')->all(),
|
||||
'types' => $this->db()->table('fin_accounts')
|
||||
->whereNotNull('account_type')->where('account_type', '<>', '')
|
||||
->distinct()->orderBy('account_type')->pluck('account_type')->all(),
|
||||
'types' => $types,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,10 @@
|
||||
$activeTab = $ledgerTab ?? 'accounts';
|
||||
$canEdit = (bool) ($ledgerCanEditMapping ?? false);
|
||||
$canEditAccountStatus = (bool) ($ledgerCanEditAccountStatus ?? false);
|
||||
$canEditAccountType = (bool) ($ledgerCanEditAccountType ?? false);
|
||||
$accountStatusOptions = $ledgerAccountStatusOptions ?? \App\Utilities\FinanceLedgerAccountStatus::OPTIONS;
|
||||
$accountFilterOptions = $ledgerAccountFilterOptions ?? ['regions' => [], 'types' => []];
|
||||
$accountTypeOptions = $accountFilterOptions['types'] ?? [];
|
||||
$money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2);
|
||||
$date = static fn ($value): string => $value ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d') : '';
|
||||
$tabUrl = static fn (string $tab): string => route('business.pages.show', ['section' => 'financial-ledger', 'page' => 'ledger', 'tab' => $tab]);
|
||||
@ -69,14 +71,16 @@
|
||||
.ledger-map-input.is-saving { border-color:var(--vip-content-accent, #B9954F); background:#fffdf6; }
|
||||
.ledger-map-input.is-error { border-color:var(--vip-danger, #B80000); background:#fff5f5; }
|
||||
.ledger-table .ledger-status-column { width:4.6rem; min-width:4.6rem; padding-right:.2rem; padding-left:.2rem; text-align:center; }
|
||||
.ledger-status-select { display:block; width:4.15rem; min-width:4.15rem; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; }
|
||||
.ledger-status-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
|
||||
.ledger-status-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); }
|
||||
.ledger-status-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); }
|
||||
.ledger-status-select[data-status="limited"] { border-color:rgba(147,95,7,.36); background:#fff8eb; color:var(--vip-warning, #935F07); }
|
||||
.ledger-status-select[data-status="business_deregistered"] { border-color:rgba(89,89,89,.32); background:#f3f1ec; color:var(--vip-content-muted, #595959); }
|
||||
.ledger-status-select.is-saving { opacity:.62; cursor:wait; }
|
||||
.ledger-status-select.is-error { border-color:var(--vip-danger, #B80000); box-shadow:0 0 0 3px rgba(184,0,0,.12); }
|
||||
.ledger-inline-select { display:block; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; }
|
||||
.ledger-status-select { width:4.15rem; min-width:4.15rem; }
|
||||
.ledger-type-select { width:5.2rem; min-width:5.2rem; }
|
||||
.ledger-inline-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
|
||||
.ledger-inline-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); }
|
||||
.ledger-inline-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); }
|
||||
.ledger-inline-select[data-status="limited"] { border-color:rgba(147,95,7,.36); background:#fff8eb; color:var(--vip-warning, #935F07); }
|
||||
.ledger-inline-select[data-status="business_deregistered"] { border-color:rgba(89,89,89,.32); background:#f3f1ec; color:var(--vip-content-muted, #595959); }
|
||||
.ledger-inline-select.is-saving { opacity:.62; cursor:wait; }
|
||||
.ledger-inline-select.is-error { border-color:var(--vip-danger, #B80000); box-shadow:0 0 0 3px rgba(184,0,0,.12); }
|
||||
.ledger-status-badge { display:inline-flex; align-items:center; justify-content:center; min-height:1.65rem; padding:.1rem .3rem; border:1px solid transparent; border-radius:999px; font-size:.68rem; font-weight:700; }
|
||||
.ledger-status-badge[data-status="normal"] { border-color:rgba(83,121,62,.28); background:#f3f7ee; color:var(--vip-success, #53793E); }
|
||||
.ledger-status-badge[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.24); background:#fff5f5; color:var(--vip-danger, #B80000); }
|
||||
@ -143,7 +147,7 @@
|
||||
<td>{{ $row->account_short }}</td>
|
||||
<td class="ledger-status-column">
|
||||
@if ($canEditAccountStatus)
|
||||
<select class="ledger-status-select" data-account-status data-account-short="{{ $row->account_short }}" data-status="{{ $row->account_status }}" data-url="{{ route('business.financial-ledger.accounts.status') }}" aria-label="账户 {{ $row->account_short }} 状态">
|
||||
<select class="ledger-inline-select ledger-status-select" data-account-status data-account-short="{{ $row->account_short }}" data-status="{{ $row->account_status }}" data-url="{{ route('business.financial-ledger.accounts.status') }}" aria-label="账户 {{ $row->account_short }} 状态">
|
||||
@foreach ($accountStatusOptions as $statusCode => $statusLabel)
|
||||
<option value="{{ $statusCode }}" @selected($row->account_status === $statusCode)>{{ $statusLabel }}</option>
|
||||
@endforeach
|
||||
@ -152,7 +156,20 @@
|
||||
<span class="ledger-status-badge" data-status="{{ $row->account_status }}">{{ $accountStatusOptions[$row->account_status] ?? '正常' }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $row->region }}</td><td>{{ $row->account_no }}</td><td>{{ $row->bank_name }}</td><td>{{ $row->bank_city_branch }}</td><td>{{ $row->account_type }}</td><td>{{ $row->company_plan }}</td><td>{{ $row->account_last4 }}</td><td>{{ $row->single_limit }}</td><td>{{ $row->daily_limit }}</td><td>{{ $row->monthly_limit }}</td><td>{{ $row->yearly_limit }}</td><td class="num">{{ $money($row->current_balance) }}</td><td class="num">{{ $money($row->blocked_amount) }}</td><td class="num">{{ $money($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td>{{ $row->raise_target }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
|
||||
<td>{{ $row->region }}</td><td>{{ $row->account_no }}</td><td>{{ $row->bank_name }}</td><td>{{ $row->bank_city_branch }}</td>
|
||||
<td>
|
||||
@if ($canEditAccountType)
|
||||
<select class="ledger-inline-select ledger-type-select" data-account-type data-account-short="{{ $row->account_short }}" data-url="{{ route('business.financial-ledger.accounts.type') }}" aria-label="账户 {{ $row->account_short }} 类型">
|
||||
<option value="" @selected(($row->account_type ?? '') === '')>未填写</option>
|
||||
@foreach ($accountTypeOptions as $accountType)
|
||||
<option value="{{ $accountType }}" @selected($row->account_type === $accountType)>{{ $accountType }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
{{ $row->account_type ?: '未填写' }}
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $row->company_plan }}</td><td>{{ $row->account_last4 }}</td><td>{{ $row->single_limit }}</td><td>{{ $row->daily_limit }}</td><td>{{ $row->monthly_limit }}</td><td>{{ $row->yearly_limit }}</td><td class="num">{{ $money($row->current_balance) }}</td><td class="num">{{ $money($row->blocked_amount) }}</td><td class="num">{{ $money($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td>{{ $row->raise_target }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="25"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
|
||||
@ -346,48 +363,52 @@
|
||||
});
|
||||
});
|
||||
|
||||
const statusSelects = Array.from(page.querySelectorAll('[data-account-status]'));
|
||||
statusSelects.forEach((select) => {
|
||||
select.dataset.previousStatus = select.value;
|
||||
select.addEventListener('change', async () => {
|
||||
const value = select.value;
|
||||
const previous = select.dataset.previousStatus || value;
|
||||
const related = statusSelects.filter((item) => item.dataset.accountShort === select.dataset.accountShort);
|
||||
related.forEach((item) => {
|
||||
item.classList.add('is-saving');
|
||||
item.classList.remove('is-error');
|
||||
});
|
||||
try {
|
||||
const response = await fetch(select.dataset.url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
account_short: select.dataset.accountShort,
|
||||
status: value,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.message || '保存失败');
|
||||
const bindInlineAccountSelect = (selector, field, successLabel) => {
|
||||
const selects = Array.from(page.querySelectorAll(selector));
|
||||
selects.forEach((select) => {
|
||||
select.dataset.previousValue = select.value;
|
||||
select.addEventListener('change', async () => {
|
||||
const value = select.value;
|
||||
const previous = select.dataset.previousValue ?? value;
|
||||
const related = selects.filter((item) => item.dataset.accountShort === select.dataset.accountShort);
|
||||
related.forEach((item) => {
|
||||
item.value = value;
|
||||
item.dataset.status = value;
|
||||
item.dataset.previousStatus = value;
|
||||
item.classList.add('is-saving');
|
||||
item.classList.remove('is-error');
|
||||
});
|
||||
notify('账户状态已保存:' + (payload.label || ''));
|
||||
} catch (error) {
|
||||
select.value = previous;
|
||||
select.dataset.status = previous;
|
||||
select.classList.add('is-error');
|
||||
notify(error.message || '账户状态保存失败', true);
|
||||
} finally {
|
||||
related.forEach((item) => item.classList.remove('is-saving'));
|
||||
}
|
||||
try {
|
||||
const response = await fetch(select.dataset.url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
account_short: select.dataset.accountShort,
|
||||
[field]: value,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.message || '保存失败');
|
||||
related.forEach((item) => {
|
||||
item.value = value;
|
||||
item.dataset.previousValue = value;
|
||||
if (field === 'status') item.dataset.status = value;
|
||||
});
|
||||
notify(successLabel + ':' + (payload.label || ''));
|
||||
} catch (error) {
|
||||
select.value = previous;
|
||||
if (field === 'status') select.dataset.status = previous;
|
||||
select.classList.add('is-error');
|
||||
notify(error.message || successLabel + '失败', true);
|
||||
} finally {
|
||||
related.forEach((item) => item.classList.remove('is-saving'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
bindInlineAccountSelect('[data-account-status]', 'status', '账户状态已保存');
|
||||
bindInlineAccountSelect('[data-account-type]', 'account_type', '账户类型已保存');
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -641,6 +641,10 @@ Route::post('workspace/financial-ledger/accounts/status', [\App\Http\Controllers
|
||||
->middleware('business.page:financial-ledger,ledger')
|
||||
->name('business.financial-ledger.accounts.status');
|
||||
|
||||
Route::post('workspace/financial-ledger/accounts/type', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountType'])
|
||||
->middleware('business.page:financial-ledger,ledger')
|
||||
->name('business.financial-ledger.accounts.type');
|
||||
|
||||
Route::get('workspace/{section}/{page?}/{tab?}', 'Business\Pages@show')
|
||||
->name('business.pages.show');
|
||||
|
||||
|
||||
@ -40,6 +40,13 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function typeUrl(): string
|
||||
{
|
||||
return route('business.financial-ledger.accounts.type', [
|
||||
'company_id' => $this->company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_account_table_renders_status_selector_with_four_options(): void
|
||||
{
|
||||
$this->loginAs()->get($this->pageUrl())
|
||||
@ -54,6 +61,8 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('name="accounts_status"', false)
|
||||
->assertSee('name="accounts_region"', false)
|
||||
->assertSee('name="accounts_type"', false)
|
||||
->assertSee('data-account-type', false)
|
||||
->assertSee('ledger-type-select', false)
|
||||
->assertSee('未填写')
|
||||
->assertSee('value="normal" selected', false);
|
||||
}
|
||||
@ -119,6 +128,74 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('data-account-short="测试账户"', false);
|
||||
}
|
||||
|
||||
public function test_account_type_can_be_saved_and_filtered_by_effective_value(): void
|
||||
{
|
||||
$connection = DB::connection('business_mirror');
|
||||
$connection->table('fin_accounts')->insert([
|
||||
[
|
||||
'company_name' => '类型测试公司甲',
|
||||
'account_short' => '类型账户甲',
|
||||
'account_type' => '公户',
|
||||
],
|
||||
[
|
||||
'company_name' => '类型测试公司乙',
|
||||
'account_short' => '类型账户乙',
|
||||
'account_type' => '个人卡',
|
||||
],
|
||||
[
|
||||
'company_name' => '类型测试公司丙',
|
||||
'account_short' => '类型账户丙',
|
||||
'account_type' => '支付宝',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginAs()->postJson($this->typeUrl(), [
|
||||
'account_short' => '类型账户甲',
|
||||
'account_type' => '支付宝',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('account_type', '支付宝')
|
||||
->assertJsonPath('label', '支付宝');
|
||||
|
||||
$this->loginAs()->postJson($this->typeUrl(), [
|
||||
'account_short' => '类型账户甲',
|
||||
'account_type' => '个人卡',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('fin_account_type_overrides', 1, 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_account_type_overrides', [
|
||||
'account_short' => '类型账户甲',
|
||||
'account_type' => '个人卡',
|
||||
'updated_by' => 'Test User',
|
||||
], 'business_mirror');
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'company_id' => $this->company->id,
|
||||
'section' => 'financial-ledger',
|
||||
'page' => 'ledger',
|
||||
'accounts_type' => '个人卡',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('data-account-short="类型账户甲"', false)
|
||||
->assertSee('data-account-short="类型账户乙"', false)
|
||||
->assertDontSee('data-account-short="类型账户丙"', false)
|
||||
->assertSee('value="个人卡" selected', false);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'company_id' => $this->company->id,
|
||||
'section' => 'financial-ledger',
|
||||
'page' => 'ledger',
|
||||
'accounts_type' => '公户',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertDontSee('data-account-short="类型账户甲"', false);
|
||||
|
||||
$this->withExceptionHandling()->loginAs()->postJson($this->typeUrl(), [
|
||||
'account_short' => '类型账户甲',
|
||||
'account_type' => '不存在的类型',
|
||||
])->assertUnprocessable()->assertJsonPath('message', '请选择现有账户类型。');
|
||||
}
|
||||
|
||||
public function test_account_status_can_be_saved_and_updated_without_duplicate_rows(): void
|
||||
{
|
||||
$payload = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user