feat(财务台账): 支持逐行修改动账账户关联 #456
@ -673,6 +673,145 @@ class FinanceLedger extends Controller
|
||||
], $inserted ? 201 : 200, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function updateTransactionAccount(Request $request, int $id, FinanceLedgerCalculator $calculator): JsonResponse
|
||||
{
|
||||
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以修改动账账户关联。');
|
||||
|
||||
$data = $request->validate([
|
||||
'account_short' => ['required', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
$accountShort = trim((string) $data['account_short']);
|
||||
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
$account = $connection->table('fin_accounts')
|
||||
->where('account_short', $accountShort)
|
||||
->orderBy('id')
|
||||
->first(['account_short', 'company_name']);
|
||||
abort_if($account === null, 422, '账户简称不存在,请从账户总表中选择。');
|
||||
|
||||
$result = $connection->transaction(function () use (
|
||||
$connection,
|
||||
$calculator,
|
||||
$id,
|
||||
$accountShort,
|
||||
): array {
|
||||
$transaction = $connection->table('fin_transactions')
|
||||
->where('id', $id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
abort_if($transaction === null, 404);
|
||||
|
||||
$source = trim((string) $transaction->source);
|
||||
$sourceValue = trim((string) ($transaction->source_value ?? ''));
|
||||
$directTransaction = $source === FinanceLedgerTransactionWriter::MANUAL_SOURCE || $sourceValue === '';
|
||||
$groupKey = $directTransaction
|
||||
? 'transaction:' . $id
|
||||
: hash('sha256', $source . '|' . $sourceValue);
|
||||
$mapping = null;
|
||||
$old = trim((string) ($transaction->account_short ?? ''));
|
||||
|
||||
if (! $directTransaction) {
|
||||
$mapping = $connection->table('fin_account_map')
|
||||
->where('source', $source)
|
||||
->where('source_value', $sourceValue)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$mappedAccount = trim((string) ($mapping->account_short ?? ''));
|
||||
if ($mappedAccount !== '') {
|
||||
$old = $mappedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
if ($old === $accountShort) {
|
||||
return [
|
||||
'changed' => false,
|
||||
'scope' => $directTransaction ? 'transaction' : 'mapping',
|
||||
'group_key' => $groupKey,
|
||||
'txn_date' => (string) $transaction->txn_date,
|
||||
];
|
||||
}
|
||||
|
||||
$now = now();
|
||||
if ($directTransaction) {
|
||||
$connection->table('fin_transactions')->where('id', $id)->update([
|
||||
'account_short' => $accountShort,
|
||||
'updated_by' => (string) user()->name,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
if ($mapping === null) {
|
||||
$stats = $connection->table('fin_transactions')
|
||||
->where('source', $source)
|
||||
->where('source_value', $sourceValue)
|
||||
->selectRaw('COUNT(*) AS txn_count, SUM(income) AS income_sum, SUM(expense) AS expense_sum')
|
||||
->first();
|
||||
$connection->table('fin_account_map')->insert([
|
||||
'source' => $source,
|
||||
'source_value' => $sourceValue,
|
||||
'account_short' => $accountShort,
|
||||
'txn_count' => (int) ($stats->txn_count ?? 0),
|
||||
'income_sum' => round((float) ($stats->income_sum ?? 0), 2),
|
||||
'expense_sum' => round((float) ($stats->expense_sum ?? 0), 2),
|
||||
'updated_by' => (string) user()->name,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$connection->table('fin_account_map')->where('id', $mapping->id)->update([
|
||||
'account_short' => $accountShort,
|
||||
'updated_by' => (string) user()->name,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->table('fin_account_map_events')->insert([
|
||||
'source' => $source,
|
||||
'source_value' => $sourceValue,
|
||||
'old_account_short' => $old !== '' ? $old : null,
|
||||
'new_account_short' => $accountShort,
|
||||
'user_id' => (int) user()->id,
|
||||
'user_name' => (string) user()->name,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
$calculator->recalculate();
|
||||
|
||||
return [
|
||||
'changed' => true,
|
||||
'scope' => $directTransaction ? 'transaction' : 'mapping',
|
||||
'group_key' => $groupKey,
|
||||
'txn_date' => (string) $transaction->txn_date,
|
||||
];
|
||||
});
|
||||
|
||||
$flow = $connection->table('fin_daily_flows')
|
||||
->where('account_short', $accountShort)
|
||||
->whereDate('flow_date', (string) $result['txn_date'])
|
||||
->first(['flow_date', 'opening_balance', 'inflow', 'outflow', 'closing_balance']);
|
||||
$currentBalance = $connection->table('fin_accounts')
|
||||
->where('account_short', $accountShort)
|
||||
->orderBy('id')
|
||||
->value('current_balance');
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'changed' => (bool) $result['changed'],
|
||||
'scope' => (string) $result['scope'],
|
||||
'group_key' => (string) $result['group_key'],
|
||||
'account_short' => $accountShort,
|
||||
'company_name' => (string) ($account->company_name ?? ''),
|
||||
'current_balance' => round((float) ($currentBalance ?? 0), 2),
|
||||
'daily_flow' => $flow === null ? null : [
|
||||
'flow_date' => (string) $flow->flow_date,
|
||||
'opening_balance' => round((float) $flow->opening_balance, 2),
|
||||
'inflow' => round((float) $flow->inflow, 2),
|
||||
'outflow' => round((float) $flow->outflow, 2),
|
||||
'closing_balance' => round((float) $flow->closing_balance, 2),
|
||||
],
|
||||
], 200, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function updateAccountStatus(Request $request, FinanceLedgerCalculator $calculator): JsonResponse
|
||||
{
|
||||
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户状态。');
|
||||
|
||||
@ -278,6 +278,12 @@
|
||||
.ledger-manual-date__button:hover { background:#efe4c8; }
|
||||
.ledger-manual-date__button:focus-visible { position:relative; z-index:2; outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.18); }
|
||||
.ledger-manual-date__button .material-icons-outlined { font-size:1rem; }
|
||||
.ledger-transaction-account-input { width:100%; min-width:0; height:1.65rem; padding:0 .38rem; border:1px solid transparent; border-radius:.3rem; background:transparent; color:var(--vip-content-ink, #171717); font:inherit; font-size:.7rem; }
|
||||
.ledger-transaction-account-input:hover { border-color:var(--vip-border, #D8D2C7); background:#fff; }
|
||||
.ledger-transaction-account-input:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; background:#fff; box-shadow:0 0 0 3px rgba(133,102,50,.14); }
|
||||
.ledger-transaction-account-input.is-saving { border-color:var(--vip-content-accent, #B9954F); background:#fffdf6; }
|
||||
.ledger-transaction-account-input.is-error { border-color:var(--vip-danger, #B80000); background:#fff5f5; }
|
||||
.ledger-transaction-account-input:disabled { cursor:wait; opacity:.65; }
|
||||
.ledger-manual-actions { display:flex; align-items:center; gap:.25rem; }
|
||||
.ledger-manual-action { display:inline-grid; place-items:center; width:1.75rem; height:1.75rem; padding:0; border:1px solid var(--vip-border, #D8D2C7); border-radius:.35rem; background:#fff; color:var(--vip-content-muted, #595959); cursor:pointer; }
|
||||
.ledger-manual-action.is-save { border-color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-dark, #856632); color:#fff; }
|
||||
@ -547,7 +553,7 @@
|
||||
<input type="hidden" name="tab" value="flows">
|
||||
<div class="ledger-field"><label for="flows-date">日期</label><input id="flows-date" type="date" name="flows_date" value="{{ request('flows_date') }}"></div>
|
||||
<div class="ledger-field"><label for="flows-company">公司</label><input id="flows-company" name="flows_company" value="{{ request('flows_company') }}" placeholder="公司名称"></div>
|
||||
<div class="ledger-field"><label for="flows-account">账户简称</label><input id="flows-account" name="flows_account" value="{{ request('flows_account') }}" placeholder="账户简称"></div>
|
||||
<div class="ledger-field"><label for="flows-account">账户简称</label><input id="flows-account" name="flows_account" list="ledger-account-list" value="{{ request('flows_account') }}" placeholder="输入或选择"></div>
|
||||
<div class="ledger-actions"><button class="ledger-btn primary" type="submit"><span class="material-icons-outlined">search</span>查询</button><a class="ledger-btn" href="{{ $tabUrl('flows') }}"><span class="material-icons-outlined">refresh</span>重置</a></div>
|
||||
</form>
|
||||
<div class="ledger-summary"><div class="ledger-summary__item"><span>流水条数</span><b>{{ number_format((int) ($ledgerFlowSummary['count'] ?? 0)) }}</b></div><div class="ledger-summary__item"><span>期初余额({{ $ledgerOpeningDate }})</span><b>{{ $money($meta['opening_sum'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>当期入账</span><b>{{ $money($ledgerFlowSummary['inflow'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>当期出账</span><b>{{ $money($ledgerFlowSummary['outflow'] ?? 0) }}</b></div><div class="ledger-summary__item"><span>期末余额合计</span><b>{{ $money($ledgerFlowSummary['closing'] ?? 0) }}</b></div></div>
|
||||
@ -577,11 +583,11 @@
|
||||
</section>
|
||||
|
||||
{{-- 动账明细 --}}
|
||||
<section class="ledger-panel" id="ledger-panel-transactions" role="tabpanel" data-ledger-panel="transactions" @if ($activeTab !== 'transactions') hidden @endif>
|
||||
<section class="ledger-panel" id="ledger-panel-transactions" role="tabpanel" data-ledger-panel="transactions" data-transaction-account-url-template="{{ route('business.financial-ledger.transactions.account.update', ['id' => '__TRANSACTION__']) }}" @if ($activeTab !== 'transactions') hidden @endif>
|
||||
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('transactions') }}">
|
||||
<input type="hidden" name="tab" value="transactions">
|
||||
<div class="ledger-field" style="min-width:15rem"><label for="txns-q">关键词</label><input id="txns-q" name="txns_q" value="{{ request('txns_q') }}" placeholder="账户、明细、审批编号"></div>
|
||||
<div class="ledger-field"><label for="txns-account">账户简称</label><input id="txns-account" name="txns_account" value="{{ request('txns_account') }}" placeholder="含未匹配"></div>
|
||||
<div class="ledger-field"><label for="txns-account">账户简称</label><input id="txns-account" name="txns_account" list="ledger-account-list" value="{{ request('txns_account') }}" placeholder="输入或选择(含未匹配)"></div>
|
||||
<div class="ledger-field"><label for="txns-source">来源</label><select id="txns-source" name="txns_source"><option value="">全部</option>@foreach ($sourceLabels as $key => $label)<option value="{{ $key }}" @selected(request('txns_source') === $key)>{{ $label }}</option>@endforeach</select></div>
|
||||
<div class="ledger-field"><label for="txns-from">日期起</label><input id="txns-from" type="date" name="txns_from" value="{{ request('txns_from') }}"></div>
|
||||
<div class="ledger-field"><label for="txns-to">日期止</label><input id="txns-to" type="date" name="txns_to" value="{{ request('txns_to') }}"></div>
|
||||
@ -629,11 +635,6 @@
|
||||
</td>
|
||||
<td>
|
||||
<input id="ledger-manual-account" class="ledger-manual-input" form="ledger-manual-transaction-form" type="text" name="account_short" list="ledger-manual-account-options" maxlength="100" placeholder="输入账户简称" autocomplete="off" required data-ledger-manual-account>
|
||||
<datalist id="ledger-manual-account-options">
|
||||
@foreach ($manualTransactionAccountOptions as $option)
|
||||
<option value="{{ $option['value'] }}" label="{{ $option['label'] }}"></option>
|
||||
@endforeach
|
||||
</datalist>
|
||||
</td>
|
||||
<td><input class="ledger-manual-input" form="ledger-manual-transaction-form" type="text" name="detail" maxlength="4000" placeholder="填写明细" required></td>
|
||||
<td><span class="ledger-source-tag">手工录入</span></td>
|
||||
@ -651,7 +652,13 @@
|
||||
@forelse ($ledgerTransactions as $row)
|
||||
<tr class="ledger-transaction-row" data-ledger-transaction-row data-transaction-id="{{ $row->id }}" data-source="{{ $row->source }}">
|
||||
<td>{{ $date($row->txn_date) }}</td>
|
||||
<td class="{{ $row->effective_account_short === '未匹配' ? 'danger' : '' }}">{{ $row->effective_account_short }}</td>
|
||||
<td>
|
||||
@if ($canEdit)
|
||||
<input class="ledger-transaction-account-input" type="text" list="ledger-manual-account-options" value="{{ $row->effective_account_short === '未匹配' ? '' : $row->effective_account_short }}" placeholder="选择账户简称" autocomplete="off" aria-label="修改动账明细账户简称" title="输入并选择账户总表简称" data-ledger-transaction-account data-url="{{ route('business.financial-ledger.transactions.account.update', ['id' => $row->id]) }}" data-previous-account="{{ $row->effective_account_short === '未匹配' ? '' : $row->effective_account_short }}" data-account-group="{{ $row->source === 'manual' || blank($row->source_value) ? 'transaction:' . $row->id : hash('sha256', (string) $row->source . '|' . (string) $row->source_value) }}">
|
||||
@else
|
||||
<span class="{{ $row->effective_account_short === '未匹配' ? 'danger' : '' }}">{{ $row->effective_account_short }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="wrap">{{ $row->detail }}</td>
|
||||
<td><span class="ledger-source-tag">{{ $sourceLabels[$row->source] ?? $row->source }}</span></td>
|
||||
<td class="num">{{ $row->income > 0 ? $money($row->income) : '' }}</td>
|
||||
@ -669,6 +676,11 @@
|
||||
'paginationLabel' => '动账明细分页',
|
||||
])
|
||||
</div>
|
||||
<datalist id="ledger-manual-account-options">
|
||||
@foreach ($manualTransactionAccountOptions as $option)
|
||||
<option value="{{ $option['value'] }}" label="{{ $option['label'] }}"></option>
|
||||
@endforeach
|
||||
</datalist>
|
||||
</section>
|
||||
|
||||
{{-- 账户映射 --}}
|
||||
@ -784,7 +796,7 @@
|
||||
};
|
||||
tabs.forEach((tab) => tab.addEventListener('click', () => {
|
||||
const name = tab.dataset.ledgerTab;
|
||||
if (ledgerDependentDataStale && ['accounts', 'shops', 'flows', 'sources'].includes(name)) {
|
||||
if (ledgerDependentDataStale && ['accounts', 'shops', 'flows', 'mapping', 'sources'].includes(name)) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('tab', name);
|
||||
window.location.assign(url.toString());
|
||||
@ -829,6 +841,119 @@
|
||||
const manualTransactionTrigger = page.querySelector('[data-ledger-manual-trigger]');
|
||||
const manualTransactionDraft = page.querySelector('[data-ledger-manual-draft]');
|
||||
const manualTransactionForm = page.querySelector('[data-ledger-manual-form]');
|
||||
const transactionAccountUrlTemplate = page.querySelector('#ledger-panel-transactions')?.dataset.transactionAccountUrlTemplate || '';
|
||||
const ledgerAccountOptions = page.querySelector('#ledger-manual-account-options');
|
||||
const ledgerAccountShortValues = new Set(
|
||||
Array.from(ledgerAccountOptions?.options || []).map((option) => option.value)
|
||||
);
|
||||
const ledgerCsrfToken = manualTransactionForm?.querySelector('input[name="_token"]')?.value || '';
|
||||
|
||||
const createTransactionAccountInput = (transactionId, accountShort, accountGroup) => {
|
||||
const input = document.createElement('input');
|
||||
input.className = 'ledger-transaction-account-input';
|
||||
input.type = 'text';
|
||||
input.setAttribute('list', 'ledger-manual-account-options');
|
||||
input.value = accountShort || '';
|
||||
input.placeholder = '选择账户简称';
|
||||
input.autocomplete = 'off';
|
||||
input.setAttribute('aria-label', '修改动账明细账户简称');
|
||||
input.title = '输入并选择账户总表简称';
|
||||
input.dataset.ledgerTransactionAccount = '';
|
||||
input.dataset.url = transactionAccountUrlTemplate.replace('__TRANSACTION__', encodeURIComponent(String(transactionId)));
|
||||
input.dataset.previousAccount = accountShort || '';
|
||||
input.dataset.accountGroup = accountGroup || '';
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
const bindTransactionAccountInput = (input) => {
|
||||
if (!input || input.dataset.accountBound === '1') return;
|
||||
input.dataset.accountBound = '1';
|
||||
const previous = () => {
|
||||
input.value = input.dataset.previousAccount || '';
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('is-error');
|
||||
};
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('is-error');
|
||||
});
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
input.blur();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
previous();
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
input.addEventListener('change', async () => {
|
||||
const accountShort = input.value.trim();
|
||||
if (!ledgerAccountShortValues.has(accountShort)) {
|
||||
input.setCustomValidity('请选择账户总表中的账户简称');
|
||||
input.classList.add('is-error');
|
||||
input.reportValidity();
|
||||
notify('请选择账户总表中的账户简称', true);
|
||||
return;
|
||||
}
|
||||
if (accountShort === (input.dataset.previousAccount || '')) {
|
||||
input.value = accountShort;
|
||||
input.setCustomValidity('');
|
||||
return;
|
||||
}
|
||||
|
||||
input.disabled = true;
|
||||
input.classList.remove('is-error');
|
||||
input.classList.add('is-saving');
|
||||
try {
|
||||
const response = await fetch(input.dataset.url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'X-CSRF-TOKEN': ledgerCsrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: new URLSearchParams({ account_short: accountShort }),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const firstError = Object.values(payload.errors || {})[0]?.[0];
|
||||
throw new Error(firstError || payload.message || '账户简称保存失败');
|
||||
}
|
||||
|
||||
const targets = payload.scope === 'mapping'
|
||||
? Array.from(page.querySelectorAll('[data-ledger-transaction-account]')).filter((item) => (
|
||||
(item.dataset.accountGroup || '') === payload.group_key
|
||||
))
|
||||
: [input];
|
||||
targets.forEach((item) => {
|
||||
item.value = payload.account_short;
|
||||
item.dataset.previousAccount = payload.account_short;
|
||||
item.setCustomValidity('');
|
||||
item.classList.remove('is-error');
|
||||
});
|
||||
ledgerDependentDataStale = true;
|
||||
const balance = payload.daily_flow
|
||||
? ',当日期末余额 ' + new Intl.NumberFormat('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(payload.daily_flow.closing_balance)
|
||||
: '';
|
||||
notify('账户简称已更新为 ' + payload.account_short + balance);
|
||||
} catch (error) {
|
||||
previous();
|
||||
notify(error.message || '账户简称保存失败', true);
|
||||
} finally {
|
||||
input.disabled = false;
|
||||
input.classList.remove('is-saving');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (manualTransactionTrigger && manualTransactionDraft && manualTransactionForm) {
|
||||
const triggerRow = page.querySelector('[data-ledger-manual-trigger-row]');
|
||||
const tokenInput = manualTransactionForm.querySelector('[data-ledger-manual-token]');
|
||||
@ -837,14 +962,10 @@
|
||||
const dateInput = page.querySelector('[data-ledger-manual-date]');
|
||||
const datePickerButton = page.querySelector('[data-ledger-manual-date-picker]');
|
||||
const accountInput = page.querySelector('[data-ledger-manual-account]');
|
||||
const accountOptions = page.querySelector('#ledger-manual-account-options');
|
||||
const accountShortValues = new Set(
|
||||
Array.from(accountOptions?.options || []).map((option) => option.value)
|
||||
);
|
||||
const validateManualAccount = () => {
|
||||
if (!accountInput) return false;
|
||||
const accountShort = accountInput.value.trim();
|
||||
const invalid = accountShort !== '' && !accountShortValues.has(accountShort);
|
||||
const invalid = accountShort !== '' && !ledgerAccountShortValues.has(accountShort);
|
||||
accountInput.setCustomValidity(invalid ? '请选择账户总表中的账户简称' : '');
|
||||
return !invalid;
|
||||
};
|
||||
@ -906,7 +1027,10 @@
|
||||
};
|
||||
|
||||
addCell(row.txn_date);
|
||||
addCell(row.account_short);
|
||||
const accountCell = addCell();
|
||||
const accountEditor = createTransactionAccountInput(row.id, row.account_short, 'transaction:' + row.id);
|
||||
accountCell.appendChild(accountEditor);
|
||||
bindTransactionAccountInput(accountEditor);
|
||||
addCell(row.detail).classList.add('wrap');
|
||||
const sourceCell = addCell();
|
||||
const sourceTag = document.createElement('span');
|
||||
@ -946,7 +1070,7 @@
|
||||
manualTransactionForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const accountShort = accountInput?.value.trim() || '';
|
||||
if (!accountShortValues.has(accountShort)) {
|
||||
if (!ledgerAccountShortValues.has(accountShort)) {
|
||||
accountInput?.setCustomValidity('请选择账户总表中的账户简称');
|
||||
accountInput?.reportValidity();
|
||||
return;
|
||||
@ -1005,6 +1129,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
page.querySelectorAll('[data-ledger-transaction-account]').forEach(bindTransactionAccountInput);
|
||||
|
||||
const accountTemplateDownload = page.querySelector('[data-account-template-download]');
|
||||
accountTemplateDownload?.addEventListener('click', () => {
|
||||
const status = page.querySelector('[data-account-template-download-status]');
|
||||
|
||||
@ -669,6 +669,11 @@ Route::post('workspace/financial-ledger/transactions/manual', [\App\Http\Control
|
||||
->middleware('business.page:financial-operations,ledger')
|
||||
->name('business.financial-ledger.transactions.manual');
|
||||
|
||||
Route::post('workspace/financial-ledger/transactions/{id}/account', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateTransactionAccount'])
|
||||
->whereNumber('id')
|
||||
->middleware('business.page:financial-operations,ledger')
|
||||
->name('business.financial-ledger.transactions.account.update');
|
||||
|
||||
Route::post('workspace/financial-ledger/accounts/status', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountStatus'])
|
||||
->middleware('business.page:financial-operations,ledger')
|
||||
->name('business.financial-ledger.accounts.status');
|
||||
|
||||
@ -55,6 +55,14 @@ class FinanceLedgerManualTransactionTest extends FeatureTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function transactionAccountUrl(int $transactionId): string
|
||||
{
|
||||
return route('business.financial-ledger.transactions.account.update', [
|
||||
'company_id' => $this->company->id,
|
||||
'id' => $transactionId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function payload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
@ -204,6 +212,97 @@ class FinanceLedgerManualTransactionTest extends FeatureTestCase
|
||||
->assertJsonPath('total_transactions', 1);
|
||||
}
|
||||
|
||||
public function test_transaction_row_can_reassign_the_source_account_mapping(): void
|
||||
{
|
||||
$connection = DB::connection('business_mirror');
|
||||
$transactionId = $connection->table('fin_transactions')->insertGetId([
|
||||
'txn_date' => '2026-10-18',
|
||||
'account_short' => null,
|
||||
'detail' => '待修正来源账户',
|
||||
'income' => 12,
|
||||
'expense' => 0,
|
||||
'approval_no' => 'MAP-ROW-1',
|
||||
'source' => 'dingtalk',
|
||||
'source_value' => 'SOURCE-MAP-ROW-1',
|
||||
'process_type' => '报销',
|
||||
'row_key' => 'manual-account-map-row-1',
|
||||
]);
|
||||
$connection->table('fin_account_map')->insert([
|
||||
'source' => 'dingtalk',
|
||||
'source_value' => 'SOURCE-MAP-ROW-1',
|
||||
'account_short' => null,
|
||||
'txn_count' => 1,
|
||||
'income_sum' => 12,
|
||||
'expense_sum' => 0,
|
||||
]);
|
||||
|
||||
$this->loginAs()->get($this->pageUrl())
|
||||
->assertOk()
|
||||
->assertSee('data-ledger-transaction-account', false)
|
||||
->assertSee($this->transactionAccountUrl($transactionId), false);
|
||||
|
||||
$this->loginAs()->postJson($this->transactionAccountUrl($transactionId), [
|
||||
'account_short' => '手工账户乙',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('changed', true)
|
||||
->assertJsonPath('scope', 'mapping')
|
||||
->assertJsonPath('account_short', '手工账户乙')
|
||||
->assertJsonPath('daily_flow.closing_balance', 12)
|
||||
->assertJsonPath('current_balance', 12);
|
||||
|
||||
$this->assertDatabaseHas('fin_account_map', [
|
||||
'source' => 'dingtalk',
|
||||
'source_value' => 'SOURCE-MAP-ROW-1',
|
||||
'account_short' => '手工账户乙',
|
||||
'updated_by' => 'Test User',
|
||||
], 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_account_map_events', [
|
||||
'source' => 'dingtalk',
|
||||
'source_value' => 'SOURCE-MAP-ROW-1',
|
||||
'old_account_short' => null,
|
||||
'new_account_short' => '手工账户乙',
|
||||
'user_name' => 'Test User',
|
||||
], 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_daily_flows', [
|
||||
'flow_date' => '2026-10-18',
|
||||
'account_short' => '手工账户乙',
|
||||
'inflow' => 12,
|
||||
'closing_balance' => 12,
|
||||
], 'business_mirror');
|
||||
}
|
||||
|
||||
public function test_manual_transaction_row_can_change_its_own_account(): void
|
||||
{
|
||||
$this->loginAs()->postJson($this->storeUrl(), $this->payload())->assertCreated();
|
||||
$transactionId = (int) DB::connection('business_mirror')->table('fin_transactions')
|
||||
->where('source', FinanceLedgerTransactionWriter::MANUAL_SOURCE)
|
||||
->value('id');
|
||||
|
||||
$this->loginAs()->postJson($this->transactionAccountUrl($transactionId), [
|
||||
'account_short' => '手工账户乙',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('changed', true)
|
||||
->assertJsonPath('scope', 'transaction')
|
||||
->assertJsonPath('account_short', '手工账户乙')
|
||||
->assertJsonPath('daily_flow.closing_balance', 25.5)
|
||||
->assertJsonPath('current_balance', 25.5);
|
||||
|
||||
$this->assertDatabaseHas('fin_transactions', [
|
||||
'id' => $transactionId,
|
||||
'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE,
|
||||
'account_short' => '手工账户乙',
|
||||
'updated_by' => 'Test User',
|
||||
], 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_daily_flows', [
|
||||
'flow_date' => '2026-10-18',
|
||||
'account_short' => '手工账户乙',
|
||||
'inflow' => 25.5,
|
||||
'closing_balance' => 25.5,
|
||||
], 'business_mirror');
|
||||
}
|
||||
|
||||
public function test_manual_submit_validates_amount_account_date_and_permission(): void
|
||||
{
|
||||
$this->withExceptionHandling()->loginAs()->postJson($this->storeUrl(), $this->payload([
|
||||
|
||||
@ -74,6 +74,7 @@ class FinanceLedgerNavigationTest extends FeatureTestCase
|
||||
'business.financial-ledger.accounts.status',
|
||||
'business.financial-ledger.accounts.type',
|
||||
'business.financial-ledger.accounts.template',
|
||||
'business.financial-ledger.transactions.account.update',
|
||||
'business.financial-ledger.mapping.shop.preview',
|
||||
'business.financial-ledger.mapping.shop.apply',
|
||||
'business.financial-ledger.mapping.payable.preview',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user