feat: 支持账户资金汇总直接编辑每日流水 (#1063)
账户资金汇总新增逐行编辑入口,可直接选择日期打开每日流水编辑;目标日期无流水时创建手动流水,并统一回算账户余额、每日流水及期间汇总。
This commit is contained in:
parent
1dd1985de2
commit
b959cef955
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user