feat: 支持账户资金汇总直接编辑每日流水 (#1063)

账户资金汇总新增逐行编辑入口,可直接选择日期打开每日流水编辑;目标日期无流水时创建手动流水,并统一回算账户余额、每日流水及期间汇总。
This commit is contained in:
shanshi 2026-11-10 20:24:54 +08:00
parent 1dd1985de2
commit b959cef955
5 changed files with 285 additions and 63 deletions

View File

@ -1390,12 +1390,12 @@ class FinanceLedger extends Controller
{
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改每日资金流水。');
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
$data = $request->validate([
'flow_date' => ['required', 'date_format:Y-m-d'],
'flow_date' => ['required', 'date_format:Y-m-d', 'after_or_equal:' . $openingDate, 'before_or_equal:today'],
'account_short' => ['required', 'string', 'max:100'],
]);
$flow = $editor->existingFlow((string) $data['flow_date'], trim((string) $data['account_short']));
abort_if($flow === null, 404);
$flow = $editor->editableFlow((string) $data['flow_date'], trim((string) $data['account_short']));
return response()->json([
'ok' => true,

View File

@ -175,6 +175,58 @@ class FinanceLedgerFlowEditor
];
}
/**
* 汇总页可以在尚无每日流水时直接选择日期建立手动流水。
*
* @return array<string,mixed>
*/
public function editableFlow(string $flowDate, string $accountShort): array
{
$date = $this->date($flowDate);
$accountShort = $this->text($accountShort);
$existing = $this->existingFlow($date, $accountShort);
if ($existing !== null) {
return $existing;
}
$account = $this->connection->table('fin_accounts')
->where('account_short', $accountShort)
->orderBy('id')
->first(['account_short', 'company_name']);
if ($account === null) {
throw ValidationException::withMessages([
'account_short' => '账户简称不存在,请从账户总表中选择。',
]);
}
$latest = $this->connection->table('fin_daily_flows')
->where('account_short', $accountShort)
->where('flow_date', '<', $date)
->orderByDesc('flow_date')
->orderByDesc('id')
->first(['closing_balance']);
$openingBalance = $latest === null
? round((float) ($this->connection->table('fin_account_opening')
->where('account_short', $accountShort)
->where('opening_date', '<=', $date)
->orderByDesc('opening_date')
->value('opening_balance') ?? 0), 2)
: round((float) $latest->closing_balance, 2);
return [
'id' => null,
'flow_date' => $date,
'company_name' => (string) ($account->company_name ?? ''),
'account_short' => (string) $account->account_short,
'opening_balance' => $openingBalance,
'inflow' => 0.0,
'outflow' => 0.0,
'closing_balance' => $openingBalance,
'remark' => '新建手动流水',
'manual_override' => false,
'updated_by' => null,
];
}
private function date(mixed $value): string
{
$text = trim((string) $value);

View File

@ -101,6 +101,14 @@
$flowViewQuery,
['tab' => 'flows', 'flows_view' => $view],
));
$flowSummaryEditDate = now()->toDateString();
if (($flowPeriod['to'] ?? null) !== null && $flowSummaryEditDate > $flowPeriod['to']) {
$flowSummaryEditDate = $flowPeriod['to'];
}
if (($flowPeriod['from'] ?? null) !== null && $flowSummaryEditDate < $flowPeriod['from']) {
$flowSummaryEditDate = $flowPeriod['from'];
}
$flowSummaryEditDate = min($flowSummaryEditDate, now()->toDateString());
$ledgerHeaderPreserve = [];
foreach (array_keys($tabs) as $tabKey) {
$ledgerHeaderPreserve[$tabKey] = collect(request()->query())
@ -568,6 +576,9 @@
.ledger-transaction-approval-actions > span { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ledger-row-edit.is-delete { color:var(--vip-danger, #B80000); }
.ledger-row-edit.is-delete:hover, .ledger-row-edit.is-delete:focus-visible { border-color:var(--vip-danger, #B80000); background:#fff5f5; }
.ledger-row-edit--text { display:inline-flex; width:auto; min-width:4.8rem; gap:.2rem; padding:0 .45rem; color:var(--vip-content-accent-dark, #856632); font-size:.68rem; font-weight:600; text-decoration:none; white-space:nowrap; }
#ledger-flow-date-edit:not([readonly]) { border-color:var(--vip-content-accent, #B9954F); background:#fffdf7; }
#ledger-flow-date-edit:not([readonly]):focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
.ledger-manual-badge { display:inline-flex; margin-left:.25rem; padding:.08rem .28rem; border-radius:.25rem; background:#fff8eb; color:var(--vip-warning, #935F07); font-size:.58rem; font-weight:700; vertical-align:middle; }
.ledger-custom-column-head { display:flex; align-items:center; gap:.3rem; min-width:0; }
.ledger-custom-column-head strong { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@ -602,6 +613,7 @@
@media (max-width: 760px) { .ledger-manual-custom-fields { grid-template-columns:minmax(0,1fr); } }
.ledger-flow-equation { grid-column:1 / -1; margin:0; padding:.55rem .65rem; border-radius:.4rem; background:var(--vip-content-accent-soft, #F7F0DE); color:var(--vip-content-muted, #595959); font-size:.66rem; line-height:1.5; }
.ledger-flow-edit-actions { display:flex; align-items:center; gap:.5rem; margin-right:auto; }
.ledger-flow-edit-actions [hidden] { display:none !important; }
.ledger-flow-edit-actions .ledger-btn { color:var(--vip-warning, #935F07); }
.ledger-mono { font-variant-numeric:tabular-nums; }
@media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } .ledger-edit-dialog .ledger-company-form__body { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
@ -854,37 +866,6 @@
</footer>
</form>
</dialog>
<dialog class="ledger-company-dialog ledger-edit-dialog" data-ledger-flow-dialog aria-labelledby="ledger-flow-edit-title">
<form class="ledger-company-form" method="POST" data-ledger-flow-form>
@csrf
<input type="hidden" name="flow_date" data-ledger-flow-date-value>
<input type="hidden" name="account_short" data-ledger-flow-account-value>
<header class="ledger-company-form__header">
<div>
<h3 class="ledger-company-form__title" id="ledger-flow-edit-title">编辑每日资金流水</h3>
<p class="ledger-company-form__help">保存后按手填数据重算该账户后续流水和当前余额;恢复自动计算会删除本日手动覆盖。</p>
</div>
<button class="ledger-company-form__close" type="button" aria-label="关闭流水编辑" data-ledger-flow-close><span class="material-icons-outlined" aria-hidden="true">close</span></button>
</header>
<div class="ledger-company-form__body">
<div class="ledger-company-field"><label for="ledger-flow-date-edit">日期</label><input id="ledger-flow-date-edit" type="text" data-ledger-flow-date readonly></div>
<div class="ledger-company-field ledger-edit-field--wide"><label for="ledger-flow-account-edit">账户简称</label><input id="ledger-flow-account-edit" type="text" data-ledger-flow-account readonly></div>
<div class="ledger-company-field"><label>当前口径</label><input type="text" data-ledger-flow-mode readonly></div>
<div class="ledger-company-field"><label for="ledger-flow-opening">期初余额(元)</label><input id="ledger-flow-opening" name="opening_balance" type="number" step="0.01" required data-ledger-flow-opening></div>
<div class="ledger-company-field"><label for="ledger-flow-inflow">当日入账(元)</label><input id="ledger-flow-inflow" name="inflow" type="number" min="0" step="0.01" required data-ledger-flow-inflow></div>
<div class="ledger-company-field"><label for="ledger-flow-outflow">当日出账(元)</label><input id="ledger-flow-outflow" name="outflow" type="number" min="0" step="0.01" required data-ledger-flow-outflow></div>
<div class="ledger-company-field"><label for="ledger-flow-closing">期末余额(元)</label><input id="ledger-flow-closing" name="closing_balance" type="number" step="0.01" required readonly data-ledger-flow-closing></div>
<p class="ledger-flow-equation">期末余额按“期初余额 + 当日入账 - 当日出账”自动计算,保存时会再次校验。</p>
<div class="ledger-company-field ledger-edit-field--wide"><label for="ledger-flow-remark">备注</label><input id="ledger-flow-remark" name="remark" type="text" maxlength="200" data-ledger-flow-remark></div>
<p class="ledger-company-form__message" role="alert" data-ledger-flow-error hidden></p>
</div>
<footer class="ledger-company-form__footer">
<div class="ledger-flow-edit-actions"><button class="ledger-btn" type="button" data-ledger-flow-reset hidden>恢复自动计算</button></div>
<button class="ledger-btn" type="button" data-ledger-flow-cancel>取消</button>
<button class="ledger-btn primary" type="submit" data-ledger-flow-save><span class="material-icons-outlined" aria-hidden="true">check</span>保存流水</button>
</footer>
</form>
</dialog>
@endif
<div class="ledger-summary">
<div class="ledger-summary__item"><span>账户数</span><b>{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}</b></div>
@ -1114,6 +1095,9 @@
'preserve' => $ledgerHeaderPreserve['flows'],
])
@endforeach
@if ($flowView === 'summary')
<th scope="col">操作</th>
@endif
</tr></thead>
<tbody>
@if ($flowView === 'daily')
@ -1124,9 +1108,9 @@
@endforelse
@else
@forelse ($ledgerFlows as $row)
<tr data-flow-summary-account="{{ $row->account_short }}"><td>{{ $row->account_short }}</td><td>{{ $row->bank_affiliation ?: '—' }}</td><td>{{ $row->company_name }}</td><td class="num">{{ $row->opening_balance === null ? '—' : $money($row->opening_balance) }}</td><td class="num">{{ $row->inflow > 0 ? $money($row->inflow) : '' }}</td><td class="num">{{ $row->outflow > 0 ? $money($row->outflow) : '' }}</td><td class="num">{{ $row->closing_balance === null ? '—' : $money($row->closing_balance) }}</td><td class="num">{{ number_format((int) $row->activity_days) }}</td><td>{{ $row->remark }}</td></tr>
<tr data-flow-summary-account="{{ $row->account_short }}"><td>{{ $row->account_short }}</td><td>{{ $row->bank_affiliation ?: '—' }}</td><td>{{ $row->company_name }}</td><td class="num">{{ $row->opening_balance === null ? '—' : $money($row->opening_balance) }}</td><td class="num">{{ $row->inflow > 0 ? $money($row->inflow) : '' }}</td><td class="num">{{ $row->outflow > 0 ? $money($row->outflow) : '' }}</td><td class="num">{{ $row->closing_balance === null ? '—' : $money($row->closing_balance) }}</td><td class="num">{{ number_format((int) $row->activity_days) }}</td><td>{{ $row->remark }}</td><td>@if ($canEditAccounts)<button class="ledger-row-edit ledger-row-edit--text" type="button" title="编辑 {{ $row->account_short }} 每日流水,默认 {{ $flowSummaryEditDate }},可在弹窗中更改日期" aria-label="编辑 {{ $row->account_short }} 每日流水" data-ledger-flow-edit data-flow-summary-edit data-edit-url="{{ route('business.financial-ledger.flows.edit', ['flow_date' => $flowSummaryEditDate, 'account_short' => $row->account_short]) }}" data-save-url="{{ route('business.financial-ledger.flows.update') }}" data-reset-url="{{ route('business.financial-ledger.flows.reset') }}" data-flow-date="{{ $flowSummaryEditDate }}" data-flow-account-short="{{ $row->account_short }}"><span class="material-icons-outlined" aria-hidden="true">edit</span><span>编辑</span></button>@else<span class="muted">—</span>@endif</td></tr>
@empty
<tr><td colspan="9"><div class="ledger-empty">没有匹配的账户。</div></td></tr>
<tr><td colspan="10"><div class="ledger-empty">没有匹配的账户。</div></td></tr>
@endforelse
@endif
</tbody>
@ -1441,6 +1425,40 @@
</div>
</section>
@if ($canEditAccounts)
<dialog class="ledger-company-dialog ledger-edit-dialog" data-ledger-flow-dialog aria-labelledby="ledger-flow-edit-title">
<form class="ledger-company-form" method="POST" data-ledger-flow-form>
@csrf
<input type="hidden" name="flow_date" data-ledger-flow-date-value>
<input type="hidden" name="account_short" data-ledger-flow-account-value>
<header class="ledger-company-form__header">
<div>
<h3 class="ledger-company-form__title" id="ledger-flow-edit-title" data-ledger-flow-title>编辑每日资金流水</h3>
<p class="ledger-company-form__help" data-ledger-flow-help>保存后按手填数据重算该账户后续流水和当前余额;恢复自动计算会删除本日手动覆盖。</p>
</div>
<button class="ledger-company-form__close" type="button" aria-label="关闭流水编辑" data-ledger-flow-close><span class="material-icons-outlined" aria-hidden="true">close</span></button>
</header>
<div class="ledger-company-form__body">
<div class="ledger-company-field"><label for="ledger-flow-date-edit">日期</label><input id="ledger-flow-date-edit" type="date" min="{{ config('finance_ledger.opening_date', '2026-10-17') }}" max="{{ now()->toDateString() }}" required data-ledger-flow-date readonly></div>
<div class="ledger-company-field ledger-edit-field--wide"><label for="ledger-flow-account-edit">账户简称</label><input id="ledger-flow-account-edit" type="text" data-ledger-flow-account readonly></div>
<div class="ledger-company-field"><label>当前口径</label><input type="text" data-ledger-flow-mode readonly></div>
<div class="ledger-company-field"><label for="ledger-flow-opening">期初余额(元)</label><input id="ledger-flow-opening" name="opening_balance" type="number" step="0.01" required data-ledger-flow-opening></div>
<div class="ledger-company-field"><label for="ledger-flow-inflow">当日入账(元)</label><input id="ledger-flow-inflow" name="inflow" type="number" min="0" step="0.01" required data-ledger-flow-inflow></div>
<div class="ledger-company-field"><label for="ledger-flow-outflow">当日出账(元)</label><input id="ledger-flow-outflow" name="outflow" type="number" min="0" step="0.01" required data-ledger-flow-outflow></div>
<div class="ledger-company-field"><label for="ledger-flow-closing">期末余额(元)</label><input id="ledger-flow-closing" name="closing_balance" type="number" step="0.01" required readonly data-ledger-flow-closing></div>
<p class="ledger-flow-equation">期末余额按“期初余额 + 当日入账 - 当日出账”自动计算,保存时会再次校验。</p>
<div class="ledger-company-field ledger-edit-field--wide"><label for="ledger-flow-remark">备注</label><input id="ledger-flow-remark" name="remark" type="text" maxlength="200" data-ledger-flow-remark></div>
<p class="ledger-company-form__message" role="alert" data-ledger-flow-error hidden></p>
</div>
<footer class="ledger-company-form__footer">
<div class="ledger-flow-edit-actions"><button class="ledger-btn" type="button" data-ledger-flow-reset hidden>恢复自动计算</button></div>
<button class="ledger-btn" type="button" data-ledger-flow-cancel>取消</button>
<button class="ledger-btn primary" type="submit" data-ledger-flow-save><span class="material-icons-outlined" aria-hidden="true">check</span>保存流水</button>
</footer>
</form>
</dialog>
@endif
@if ($canEditTransactions)
<dialog class="ledger-company-dialog ledger-edit-dialog" data-ledger-manual-edit-dialog aria-labelledby="ledger-manual-edit-title">
<form class="ledger-company-form" method="POST" data-ledger-manual-edit-form>
@ -3270,11 +3288,23 @@
const flowEditError = page.querySelector('[data-ledger-flow-error]');
const flowEditSave = page.querySelector('[data-ledger-flow-save]');
const flowEditReset = page.querySelector('[data-ledger-flow-reset]');
const flowEditTitle = page.querySelector('[data-ledger-flow-title]');
const flowEditHelp = page.querySelector('[data-ledger-flow-help]');
const flowDateInput = page.querySelector('[data-ledger-flow-date]');
const flowDateValue = flowEditForm?.querySelector('[data-ledger-flow-date-value]');
const flowAccountValue = flowEditForm?.querySelector('[data-ledger-flow-account-value]');
const flowAccountInput = page.querySelector('[data-ledger-flow-account]');
const flowModeInput = page.querySelector('[data-ledger-flow-mode]');
const flowRemarkInput = page.querySelector('[data-ledger-flow-remark]');
const flowOpening = page.querySelector('[data-ledger-flow-opening]');
const flowInflow = page.querySelector('[data-ledger-flow-inflow]');
const flowOutflow = page.querySelector('[data-ledger-flow-outflow]');
const flowClosing = page.querySelector('[data-ledger-flow-closing]');
let flowResetUrl = '';
let activeFlowEditButton = null;
let flowDateLoadId = 0;
let flowEditLoadedDate = '';
const defaultFlowEditHelp = flowEditHelp?.textContent || '';
const updateFlowClosing = () => {
if (!flowClosing) return;
const opening = Number(flowOpening?.value || 0);
@ -3283,44 +3313,93 @@
flowClosing.value = (opening + inflow - outflow).toFixed(2);
};
[flowOpening, flowInflow, flowOutflow].forEach((input) => input?.addEventListener('input', updateFlowClosing));
const setFlowEditMode = (button) => {
const summaryMode = button.hasAttribute('data-flow-summary-edit');
if (flowDateInput) {
flowDateInput.readOnly = !summaryMode;
flowDateInput.title = summaryMode
? '可修改日期;没有流水的日期会新建手动流水'
: '从按日流水进入时,日期不可修改';
}
if (flowEditTitle) {
flowEditTitle.textContent = summaryMode ? '编辑账户每日流水' : '编辑每日资金流水';
}
if (flowEditHelp) {
flowEditHelp.textContent = summaryMode
? '可选择日期后编辑当日流水;目标日期尚无流水时会新建手动流水,保存后统一回算账户余额和汇总。'
: defaultFlowEditHelp;
}
};
const flowEditRequestUrl = (button, date) => {
const url = new URL(button.dataset.editUrl, window.location.origin);
url.searchParams.set('flow_date', date);
url.searchParams.set('account_short', button.dataset.flowAccountShort || '');
return url.toString();
};
const loadFlowForDate = async (button, date) => {
const requestId = ++flowDateLoadId;
const response = await fetch(flowEditRequestUrl(button, date), {
credentials: 'same-origin',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const firstError = Object.values(payload.errors || {})[0]?.[0];
throw new Error(firstError || payload.message || '流水读取失败');
}
return { requestId, flow: payload.flow || {} };
};
const renderFlowEdit = (button, flow) => {
if (!flowEditForm) return;
flowEditForm.action = button.dataset.saveUrl;
flowResetUrl = button.dataset.resetUrl;
flowEditLoadedDate = flow.flow_date || '';
if (flowDateValue) flowDateValue.value = flow.flow_date || '';
if (flowAccountValue) flowAccountValue.value = flow.account_short || '';
if (flowDateInput) flowDateInput.value = flow.flow_date || '';
if (flowAccountInput) flowAccountInput.value = flow.account_short || '';
if (flowModeInput) flowModeInput.value = flow.manual_override ? '手动覆盖' : '自动计算';
if (flowOpening) flowOpening.value = Number(flow.opening_balance || 0).toFixed(2);
if (flowInflow) flowInflow.value = Number(flow.inflow || 0).toFixed(2);
if (flowOutflow) flowOutflow.value = Number(flow.outflow || 0).toFixed(2);
if (flowRemarkInput) flowRemarkInput.value = flow.remark || '';
updateFlowClosing();
if (flowEditReset) flowEditReset.hidden = !flow.manual_override;
if (flowEditError) {
flowEditError.hidden = true;
flowEditError.textContent = '';
}
};
const closeFlowEditDialog = () => {
flowDateLoadId += 1;
activeFlowEditButton = null;
flowEditLoadedDate = '';
if (flowEditDialog?.open) flowEditDialog.close();
};
page.querySelectorAll('[data-ledger-flow-edit]').forEach((button) => {
button.addEventListener('click', async () => {
if (!flowEditDialog || !flowEditForm) return;
button.disabled = true;
activeFlowEditButton = button;
setFlowEditMode(button);
const requestedDate = button.dataset.flowDate || flowDateInput?.value || '';
if (flowDateInput) flowDateInput.value = requestedDate;
if (flowDateValue) flowDateValue.value = requestedDate;
if (flowEditReset) flowEditReset.hidden = true;
try {
const response = await fetch(button.dataset.editUrl, {
credentials: 'same-origin',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.message || '流水读取失败');
const flow = payload.flow || {};
flowEditForm.action = button.dataset.saveUrl;
flowResetUrl = button.dataset.resetUrl;
flowEditForm.querySelector('[data-ledger-flow-date-value]').value = flow.flow_date || '';
flowEditForm.querySelector('[data-ledger-flow-account-value]').value = flow.account_short || '';
page.querySelector('[data-ledger-flow-date]').value = flow.flow_date || '';
page.querySelector('[data-ledger-flow-account]').value = flow.account_short || '';
page.querySelector('[data-ledger-flow-mode]').value = flow.manual_override ? '手动覆盖' : '自动计算';
flowOpening.value = Number(flow.opening_balance || 0).toFixed(2);
flowInflow.value = Number(flow.inflow || 0).toFixed(2);
flowOutflow.value = Number(flow.outflow || 0).toFixed(2);
page.querySelector('[data-ledger-flow-remark]').value = flow.remark || '';
updateFlowClosing();
if (flowEditReset) flowEditReset.hidden = !flow.manual_override;
if (flowEditError) {
flowEditError.hidden = true;
flowEditError.textContent = '';
}
const result = await loadFlowForDate(button, requestedDate);
if (result.requestId !== flowDateLoadId || activeFlowEditButton !== button) return;
renderFlowEdit(button, result.flow);
if (typeof flowEditDialog.showModal === 'function') {
flowEditDialog.showModal();
} else {
flowEditDialog.setAttribute('open', '');
}
flowOpening.focus();
if (button.hasAttribute('data-flow-summary-edit')) {
flowDateInput?.focus();
} else {
flowOpening?.focus();
}
} catch (error) {
notify(error.message || '流水读取失败', true);
} finally {
@ -3328,6 +3407,39 @@
}
});
});
flowDateInput?.addEventListener('change', async () => {
if (!activeFlowEditButton?.hasAttribute('data-flow-summary-edit')) return;
const requestedDate = flowDateInput.value;
if (!requestedDate) {
if (flowEditError) {
flowEditError.textContent = '请选择流水日期。';
flowEditError.hidden = false;
}
return;
}
flowDateInput.disabled = true;
if (flowEditSave) flowEditSave.disabled = true;
if (flowEditReset) flowEditReset.hidden = true;
try {
const result = await loadFlowForDate(activeFlowEditButton, requestedDate);
if (result.requestId !== flowDateLoadId || !activeFlowEditButton) return;
renderFlowEdit(activeFlowEditButton, result.flow);
} catch (error) {
if (flowEditError) {
flowEditError.textContent = error.message || '流水读取失败';
flowEditError.hidden = false;
}
notify(error.message || '流水读取失败', true);
if (flowEditLoadedDate) {
flowDateInput.value = flowEditLoadedDate;
if (flowDateValue) flowDateValue.value = flowEditLoadedDate;
}
} finally {
flowDateInput.disabled = false;
if (flowEditSave) flowEditSave.disabled = false;
flowDateInput.focus();
}
});
page.querySelector('[data-ledger-flow-close]')?.addEventListener('click', closeFlowEditDialog);
page.querySelector('[data-ledger-flow-cancel]')?.addEventListener('click', closeFlowEditDialog);
flowEditDialog?.addEventListener('click', (event) => {
@ -3335,6 +3447,7 @@
});
flowEditForm?.addEventListener('submit', async (event) => {
event.preventDefault();
if (flowDateValue && flowDateInput) flowDateValue.value = flowDateInput.value;
updateFlowClosing();
if (!flowEditForm.reportValidity()) return;
if (flowEditSave) flowEditSave.disabled = true;

View File

@ -441,6 +441,9 @@ class FinanceLedgerHeaderControlsTest extends FeatureTestCase
$this->assertStringContainsString('<td>账户甲</td><td>—</td><td>甲公司</td>', $accountRow[0]);
$this->assertStringContainsString('>所属银行</span>', $summaryHtml);
$this->assertStringContainsString('<td class="num">100.00</td><td class="num">10.00</td><td class="num"></td><td class="num">110.00</td><td class="num">1</td>', $accountRow[0]);
$this->assertStringContainsString('data-flow-summary-edit', $accountRow[0]);
$this->assertStringContainsString('data-edit-url=', $accountRow[0]);
$this->assertStringContainsString('>编辑</span>', $accountRow[0]);
$this->assertSame(1, preg_match('/<tr data-flow-summary-account="账户丙">.*?<\/tr>/s', $summaryHtml, $emptyAccountRow));
$this->assertStringContainsString('<td>账户丙</td><td>—</td><td>丙公司</td>', $emptyAccountRow[0]);
$this->assertStringContainsString('<td class="num">—</td>', $emptyAccountRow[0]);

View File

@ -76,6 +76,11 @@ class FinanceLedgerManualEditTest extends FeatureTestCase
$this->assertStringContainsString('data-ledger-account-dialog', $html);
$this->assertStringContainsString('data-ledger-flow-edit', $html);
$this->assertStringContainsString('data-ledger-flow-dialog', $html);
$this->assertSame(
1,
preg_match('/<section class="ledger-panel" id="ledger-panel-accounts".*?<\/section>/s', $html, $accountsPanel),
);
$this->assertStringNotContainsString('data-ledger-flow-dialog', $accountsPanel[0]);
}
public function test_account_editor_updates_fields_renames_references_and_writes_audit(): void
@ -194,6 +199,55 @@ class FinanceLedgerManualEditTest extends FeatureTestCase
], 'business_mirror');
}
public function test_summary_editor_creates_a_flow_for_an_empty_day_and_updates_the_summary(): void
{
Carbon::setTestNow('2026-10-20 12:00:00');
$this->loginAs()->get($this->flowEditUrl('2026-10-20'))
->assertOk()
->assertJsonPath('flow.id', null)
->assertJsonPath('flow.flow_date', '2026-10-20')
->assertJsonPath('flow.opening_balance', 95)
->assertJsonPath('flow.inflow', 0)
->assertJsonPath('flow.outflow', 0)
->assertJsonPath('flow.closing_balance', 95);
$this->loginAs()->postJson($this->flowUpdateUrl(), [
'flow_date' => '2026-10-20',
'account_short' => '手动编辑测试账户',
'opening_balance' => 95,
'inflow' => 10,
'outflow' => 0,
'closing_balance' => 105,
'remark' => '汇总页新建流水',
])->assertOk()
->assertJsonPath('flow.manual_override', true)
->assertJsonPath('flow.closing_balance', 105);
$this->assertDatabaseHas('fin_daily_flow_overrides', [
'flow_date' => '2026-10-20',
'account_short' => '手动编辑测试账户',
'inflow' => 10,
'outflow' => 0,
'closing_balance' => 105,
], 'business_mirror');
$summaryHtml = $this->loginAs()->get($this->pageUrl([
'flows_view' => 'summary',
'flows_from' => '2026-10-01',
'flows_to' => '2026-10-31',
]))->assertOk()->getContent();
$this->assertSame(
1,
preg_match('/<tr data-flow-summary-account="手动编辑测试账户">.*?<\/tr>/s', $summaryHtml, $accountRow),
);
$this->assertStringContainsString(
'<td class="num">100.00</td><td class="num">35.00</td><td class="num">30.00</td><td class="num">105.00</td>',
$accountRow[0],
);
$this->assertStringContainsString('data-flow-summary-edit', $accountRow[0]);
}
public function test_manual_edit_validates_equation_duplicate_short_and_permission(): void
{
$this->withExceptionHandling()->loginAs()->postJson($this->flowUpdateUrl(), [
@ -239,13 +293,13 @@ class FinanceLedgerManualEditTest extends FeatureTestCase
])->assertForbidden();
}
private function pageUrl(): string
private function pageUrl(array $query = []): string
{
return route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-operations',
'page' => 'ledger',
]) . '?tab=flows';
]) . '?' . http_build_query(array_merge(['tab' => 'flows'], $query));
}
private function accountEditUrl(): string