财务台账:增加当前余额并按账户状态同步金额 #437
@ -99,7 +99,8 @@ class FinanceLedger extends Controller
|
||||
$summaryQuery = clone $query;
|
||||
$summary = [
|
||||
'count' => (clone $query)->count(),
|
||||
'balance' => round((float) (clone $summaryQuery)->sum('accounts.current_balance'), 2),
|
||||
'current_balance' => round((float) (clone $summaryQuery)->sum('accounts.current_balance'), 2),
|
||||
'balance' => round((float) (clone $summaryQuery)->sum('accounts.daily_available'), 2),
|
||||
'blocked' => round((float) (clone $summaryQuery)->sum('accounts.blocked_amount'), 2),
|
||||
'daily_available' => round((float) (clone $summaryQuery)->sum('accounts.daily_available'), 2),
|
||||
];
|
||||
@ -187,7 +188,7 @@ class FinanceLedger extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateAccountTodo(Request $request)
|
||||
public function updateAccountTodo(Request $request, FinanceLedgerCalculator $calculator)
|
||||
{
|
||||
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改待办事项。');
|
||||
|
||||
@ -240,7 +241,7 @@ class FinanceLedger extends Controller
|
||||
'updated_by' => (string) user()->name,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$connection->transaction(function () use ($connection, $accountShort, $values, $now, $completionStatus): void {
|
||||
$connection->transaction(function () use ($connection, $accountShort, $values, $now, $completionStatus, $calculator): void {
|
||||
$todos = $connection->table('fin_account_todos');
|
||||
$existing = $todos->where('account_short', $accountShort)->exists();
|
||||
if ($existing) {
|
||||
@ -260,6 +261,7 @@ class FinanceLedger extends Controller
|
||||
'updated_by' => (string) user()->name,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$calculator->refreshAccountAmounts($accountShort);
|
||||
}
|
||||
});
|
||||
|
||||
@ -320,7 +322,7 @@ class FinanceLedger extends Controller
|
||||
], 200, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function updateAccountStatus(Request $request): JsonResponse
|
||||
public function updateAccountStatus(Request $request, FinanceLedgerCalculator $calculator): JsonResponse
|
||||
{
|
||||
abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户状态。');
|
||||
|
||||
@ -349,14 +351,18 @@ class FinanceLedger extends Controller
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if ($existing === null) {
|
||||
$statuses->insert($values + [
|
||||
'account_short' => $accountShort,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$statuses->where('account_short', $accountShort)->update($values);
|
||||
}
|
||||
$connection->transaction(function () use ($statuses, $existing, $values, $accountShort, $now, $calculator): void {
|
||||
if ($existing === null) {
|
||||
$statuses->insert($values + [
|
||||
'account_short' => $accountShort,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$statuses->where('account_short', $accountShort)->update($values);
|
||||
}
|
||||
|
||||
$calculator->refreshAccountAmounts($accountShort);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
|
||||
@ -8,7 +8,7 @@ CAIWU_BATCH_CALLER_DIR="$PWD"
|
||||
export CAIWU_BATCH_CALLER_DIR
|
||||
case "${1:-}" in
|
||||
*:import|*:import-*|*:import:*|product:monthly-weighted-average:base) is_import=1 ;;
|
||||
jushuitan:sync-shipping|store-receipt:order-match:sync|store-receipt:product-cost:sync|store-receipt:auto-resync|order-data:warm-shop-candidates|business-mirror:index-work-order-waybills|business-mirror:refresh-paid-sales|account-bills:exports:run) ;;
|
||||
jushuitan:sync-shipping|store-receipt:order-match:sync|store-receipt:product-cost:sync|store-receipt:auto-resync|order-data:warm-shop-candidates|business-mirror:index-work-order-waybills|business-mirror:refresh-paid-sales|account-bills:exports:run|finance-ledger:recalculate) ;;
|
||||
*) echo 'Unsupported finance batch command' >&2; exit 64 ;;
|
||||
esac
|
||||
cd "$REMOTE_DIR"
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use App\Utilities\FinanceLedgerAccountStatus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
@ -10,7 +11,8 @@ use Illuminate\Support\Facades\DB;
|
||||
* 计算口径固定在服务端,页面只读结果:
|
||||
* 期末 = 期初 + 当日入账 - 当日出账
|
||||
* 账户当前余额 = 最新一条流水期末
|
||||
* 每日可出金额 = 当前余额 - 卡住金额
|
||||
* 正常账户:可用金额 = 当前余额,不可用金额 = 0
|
||||
* 异常账户:可用金额 = 0,不可用金额 = 当前余额
|
||||
* 店铺余额 = 店铺所属公司名下账户当前余额合计(待财务确认)
|
||||
*/
|
||||
class FinanceLedgerCalculator
|
||||
@ -150,11 +152,12 @@ class FinanceLedgerCalculator
|
||||
->where('account_short', $account)
|
||||
->update([
|
||||
'current_balance' => round((float) $balance, 2),
|
||||
'daily_available' => $connection->raw('ROUND(current_balance - blocked_amount, 2)'),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->refreshAccountAmounts();
|
||||
|
||||
$companies = $connection->table('fin_accounts')
|
||||
->selectRaw('company_name, SUM(current_balance) AS balance')
|
||||
->groupBy('company_name')
|
||||
@ -175,6 +178,58 @@ class FinanceLedgerCalculator
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按账户状态回填可用金额和不可用金额。
|
||||
*
|
||||
* 正常账户:可用金额 = 当前余额,不可用金额 = 0;
|
||||
* 异常账户:可用金额 = 0,不可用金额 = 当前余额。
|
||||
*/
|
||||
public function refreshAccountAmounts(?string $accountShort = null): int
|
||||
{
|
||||
$connection = $this->connection();
|
||||
if ($accountShort !== null) {
|
||||
$status = $connection->table('fin_account_statuses')
|
||||
->where('account_short', $accountShort)
|
||||
->value('status');
|
||||
$normal = $status === null
|
||||
|| trim((string) $status) === ''
|
||||
|| $status === FinanceLedgerAccountStatus::NORMAL;
|
||||
|
||||
return $connection->table('fin_accounts')
|
||||
->where('account_short', $accountShort)
|
||||
->update([
|
||||
'daily_available' => $normal ? $connection->raw('current_balance') : 0,
|
||||
'blocked_amount' => $normal ? 0 : $connection->raw('current_balance'),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$abnormalAccounts = $connection->table('fin_account_statuses')
|
||||
->where('status', '<>', FinanceLedgerAccountStatus::NORMAL)
|
||||
->pluck('account_short')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$normalCount = $connection->table('fin_accounts')->update([
|
||||
'daily_available' => $connection->raw('current_balance'),
|
||||
'blocked_amount' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if ($abnormalAccounts === []) {
|
||||
return $normalCount;
|
||||
}
|
||||
|
||||
return $normalCount + $connection->table('fin_accounts')
|
||||
->whereIn('account_short', $abnormalAccounts)
|
||||
->update([
|
||||
'daily_available' => 0,
|
||||
'blocked_amount' => $connection->raw('current_balance'),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function connection()
|
||||
{
|
||||
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
|
||||
@ -150,7 +150,8 @@ class FinanceLedgerPageData
|
||||
$summaryQuery = clone $query;
|
||||
$summary = [
|
||||
'count' => (clone $query)->count(),
|
||||
'balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2),
|
||||
'current_balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2),
|
||||
'balance' => round((float) (clone $summaryQuery)->sum('daily_available'), 2),
|
||||
'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2),
|
||||
];
|
||||
|
||||
|
||||
@ -84,8 +84,8 @@
|
||||
.ledger-mapping-bulk-actions { display:flex; flex-wrap:wrap; gap:.45rem; }
|
||||
.ledger-mapping-bulk .ledger-btn { height:1.95rem; }
|
||||
.ledger-download-status { flex-basis:100%; color:var(--vip-success, #53793E); font-size:.68rem; font-weight:600; line-height:1.4; text-align:right; }
|
||||
.ledger-summary { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:.65rem; margin-bottom:.85rem; }
|
||||
.ledger-summary__item { display:flex; flex-direction:column; justify-content:center; gap:.2rem; min-width:0; min-height:3.4rem; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); }
|
||||
.ledger-summary { display:flex; flex-wrap:wrap; gap:.65rem; margin-bottom:.85rem; }
|
||||
.ledger-summary__item { display:flex; flex:0 0 14rem; flex-direction:column; justify-content:center; gap:.2rem; width:14rem; min-width:14rem; height:3.4rem; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); }
|
||||
.ledger-summary__item--available { background:#f3f7ee; }
|
||||
.ledger-summary__item--blocked { background:#fff5f5; }
|
||||
.ledger-summary__item--link { color:inherit; text-decoration:none; transition:border-color .15s ease, box-shadow .15s ease; }
|
||||
@ -170,8 +170,7 @@
|
||||
.ledger-toast.is-error { border-color:var(--vip-danger, #B80000); color:var(--vip-danger, #B80000); }
|
||||
.ledger-source-tag { display:inline-flex; align-items:center; padding:.1rem .4rem; border-radius:.25rem; background:#f4f1eb; color:var(--vip-content-muted, #595959); font-size:.68rem; }
|
||||
.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-summary { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 560px) { .ledger-summary { grid-template-columns:1fr; } }
|
||||
@media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } }
|
||||
</style>
|
||||
|
||||
<div class="ledger-hero">
|
||||
@ -217,7 +216,7 @@
|
||||
<input type="checkbox" role="switch" aria-label="编辑模式" aria-checked="false" data-ledger-edit-mode-toggle @disabled(! $canEditAccounts)>
|
||||
<span class="ledger-edit-switch__track" aria-hidden="true"><span class="ledger-edit-switch__thumb"></span></span>
|
||||
</label>
|
||||
<div class="ledger-column-picker" data-ledger-column-picker data-column-storage-key="caiwu.financial-ledger.accounts.preferences.v1.{{ auth()->id() }}.{{ company_id() }}">
|
||||
<div class="ledger-column-picker" data-ledger-column-picker data-column-storage-key="caiwu.financial-ledger.accounts.preferences.v2.{{ auth()->id() }}.{{ company_id() }}">
|
||||
<button class="ledger-btn" type="button" data-ledger-column-toggle aria-expanded="false"><span class="material-icons-outlined" aria-hidden="true">view_column</span>列设置</button>
|
||||
<div class="ledger-column-panel" data-ledger-column-panel role="group" aria-label="选择账户总表展示列" hidden>
|
||||
<p class="ledger-column-help">拖动字段可调整列顺序;拖动表头右侧边界可调整列宽,双击边界恢复默认。</p>
|
||||
@ -230,13 +229,13 @@
|
||||
</form>
|
||||
<div class="ledger-summary">
|
||||
<div class="ledger-summary__item"><span>账户数</span><b>{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}</b></div>
|
||||
<div class="ledger-summary__item ledger-summary__item--available"><span>可用金额合计</span><b>{{ $money($ledgerAccountSummary['balance'] ?? 0) }}</b></div>
|
||||
<a class="ledger-summary__item ledger-summary__item--blocked ledger-summary__item--link" href="{{ route('business.financial-ledger.unavailable-balances') }}" aria-label="查看账户异常待办"><span>不可用金额合计</span><b>{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}</b></a>
|
||||
<div class="ledger-summary__item"><span>每日可出金额口径</span><b>可用金额 − 不可用金额</b></div>
|
||||
<div class="ledger-summary__item ledger-summary__item--available" data-ledger-summary-available><span>可用金额合计</span><b>{{ $money($ledgerAccountSummary['balance'] ?? 0) }}</b></div>
|
||||
<a class="ledger-summary__item ledger-summary__item--blocked ledger-summary__item--link" data-ledger-summary-blocked href="{{ route('business.financial-ledger.unavailable-balances') }}" aria-label="查看账户异常待办"><span>不可用金额合计</span><b>{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}</b></a>
|
||||
<div class="ledger-summary__item"><span>金额口径</span><b>正常计可用,异常计不可用</b></div>
|
||||
</div>
|
||||
<div class="ledger-table-wrap ledger-accounts-table-wrap">
|
||||
<table class="ledger-table ledger-accounts-table">
|
||||
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额</th><th>日限额</th><th>月限额</th><th>年限额</th><th class="num">可用金额</th><th class="num">不可用金额</th><th class="num">每日可出</th><th class="num">预计出清天数</th><th>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
|
||||
<thead><tr><th>公司名称</th><th>账户简称</th><th>账户类型</th><th class="ledger-status-column">账户状态</th><th>地区</th><th>账号</th><th>开户行</th><th>开户城市/网点</th><th class="ledger-plan-column">公司规划</th><th class="ledger-keep-column">账户是否保留</th><th>后4位</th><th>单笔限额</th><th>日限额</th><th>月限额</th><th>年限额</th><th class="num">当前余额</th><th class="num">可用金额</th><th class="num">不可用金额</th><th class="num">每日可出</th><th class="num">预计出清天数</th><th>提额目标</th><th>提额状态</th><th>提额动作</th><th>已做动作</th><th>下一步</th><th>备注</th><th>责任人</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse ($ledgerAccounts as $row)
|
||||
<tr>
|
||||
@ -290,10 +289,10 @@
|
||||
</select>
|
||||
@endif
|
||||
</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->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" data-account-current-balance="{{ (float) $row->current_balance }}">{{ $money($row->current_balance) }}</td><td class="num" data-account-available-amount>{{ $money($row->daily_available) }}</td><td class="num" data-account-blocked-amount>{{ $money($row->blocked_amount) }}</td><td class="num" data-account-daily-available-amount>{{ $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="26"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
|
||||
<tr><td colspan="27"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -968,6 +967,41 @@
|
||||
item.closest('td')?.setAttribute('data-account-should-keep', value);
|
||||
}
|
||||
});
|
||||
if (field === 'status') {
|
||||
const row = select.closest('tr');
|
||||
const currentCell = row?.querySelector('[data-account-current-balance]');
|
||||
const currentAmount = Number(currentCell?.dataset.accountCurrentBalance || 0);
|
||||
const currentText = currentCell?.textContent?.trim() || '0.00';
|
||||
const nextNormal = value === 'normal';
|
||||
const previousNormal = previous === 'normal';
|
||||
const availableCell = row?.querySelector('[data-account-available-amount]');
|
||||
const blockedCell = row?.querySelector('[data-account-blocked-amount]');
|
||||
const dailyAvailableCell = row?.querySelector('[data-account-daily-available-amount]');
|
||||
if (availableCell) availableCell.textContent = nextNormal ? currentText : '0.00';
|
||||
if (blockedCell) blockedCell.textContent = nextNormal ? '0.00' : currentText;
|
||||
if (dailyAvailableCell) dailyAvailableCell.textContent = nextNormal ? currentText : '0.00';
|
||||
|
||||
if (nextNormal !== previousNormal) {
|
||||
const formatSummaryAmount = (amount) => amount.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
const adjustSummary = (card, delta) => {
|
||||
const valueNode = card?.querySelector('b');
|
||||
if (!valueNode) return;
|
||||
const current = Number(valueNode.textContent.replace(/,/g, '')) || 0;
|
||||
valueNode.textContent = formatSummaryAmount(Math.max(0, current + delta));
|
||||
};
|
||||
adjustSummary(
|
||||
page.querySelector('[data-ledger-summary-available]'),
|
||||
nextNormal ? currentAmount : -currentAmount,
|
||||
);
|
||||
adjustSummary(
|
||||
page.querySelector('[data-ledger-summary-blocked]'),
|
||||
nextNormal ? -currentAmount : currentAmount,
|
||||
);
|
||||
}
|
||||
}
|
||||
notify(successLabel + ':' + (payload.label || ''));
|
||||
} catch (error) {
|
||||
select.value = previous;
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
.pending-balance-hero p { margin:0; color:var(--vip-content-muted, #595959); line-height:1.55; }
|
||||
.pending-balance-back { display:inline-flex; align-items:center; gap:.35rem; color:var(--vip-content-accent-dark, #856632); font-weight:600; text-decoration:none; }
|
||||
.pending-balance-back:hover { color:#6A5129; }
|
||||
.pending-balance-summary { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:.65rem; margin-bottom:.9rem; }
|
||||
.pending-balance-card { min-width:0; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); }
|
||||
.pending-balance-summary { display:flex; flex-wrap:wrap; gap:.65rem; margin-bottom:.9rem; }
|
||||
.pending-balance-card { display:flex; flex:0 0 14rem; flex-direction:column; justify-content:center; width:14rem; min-width:14rem; height:3.4rem; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); }
|
||||
.pending-balance-card.is-available { background:#f3f7ee; }
|
||||
.pending-balance-card.is-blocked { background:#fff5f5; }
|
||||
.pending-balance-card span { display:block; overflow:hidden; color:var(--vip-content-muted, #595959); font-size:.68rem; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }
|
||||
@ -42,6 +42,8 @@
|
||||
.pending-balance-table td:last-child { border-right:0; }
|
||||
.pending-balance-table tbody tr:last-child td { border-bottom:0; }
|
||||
.pending-balance-table tbody tr:hover td { background:#fcfaf4; }
|
||||
.pending-balance-table tfoot td { position:sticky; bottom:0; z-index:5; border-top:1px solid var(--vip-border, #D8D2C7); background:var(--vip-content-accent-soft, #F7F0DE); font-weight:700; }
|
||||
.pending-balance-table tfoot .num { text-align:right; }
|
||||
.pending-balance-table .num { font-variant-numeric:tabular-nums; text-align:right; }
|
||||
.pending-balance-sort { display:inline-flex; align-items:center; justify-content:center; gap:.2rem; color:inherit; text-decoration:none; }
|
||||
.pending-balance-sort:hover { color:var(--vip-content-accent-dark, #856632); }
|
||||
@ -81,9 +83,8 @@
|
||||
.ledger-page-box__current { min-width:2.2rem; padding:0 .15rem; color:var(--vip-content-ink, #171717); font-size:.7rem; font-variant-numeric:tabular-nums; font-weight:700; text-align:center; white-space:nowrap; }
|
||||
.pending-balance-page__status { color:var(--vip-content-muted, #595959); font-size:.68rem; }
|
||||
@media (max-width:1100px) { .pending-balance-filter-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width:900px) { .pending-balance-summary { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width:700px) { .pending-balance-filter-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width:560px) { .pending-balance-page { margin:1rem .75rem 1.5rem; } .pending-balance-summary, .pending-balance-filter-grid { grid-template-columns:1fr; } }
|
||||
@media (max-width:560px) { .pending-balance-page { margin:1rem .75rem 1.5rem; } .pending-balance-filter-grid { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
<section class="pending-balance-page" aria-labelledby="pending-balance-title">
|
||||
@ -202,6 +203,7 @@
|
||||
<th data-pending-column-key="account_short">账户简称</th>
|
||||
<th data-pending-column-key="account_type">账户类型</th>
|
||||
<th data-pending-column-key="account_status">账户状态</th>
|
||||
<th data-pending-column-key="current_balance">当前余额</th>
|
||||
<th data-pending-column-key="account_no">账号</th>
|
||||
<th data-pending-column-key="bank_name">开户行</th>
|
||||
<th data-pending-column-key="bank_city_branch">开户城市/网点</th>
|
||||
@ -237,6 +239,7 @@
|
||||
<td>{{ $row->account_short ?: '未填写' }}</td>
|
||||
<td>{{ $row->account_type ?: '未填写' }}</td>
|
||||
<td><span class="pending-balance-status is-{{ $row->account_status }}">{{ \App\Utilities\FinanceLedgerAccountStatus::label($row->account_status) }}</span></td>
|
||||
<td class="num" data-pending-current-balance="{{ $money($row->current_balance) }}">{{ $money($row->current_balance) }}</td>
|
||||
<td>{{ $row->account_no ?: '未填写' }}</td>
|
||||
<td>{{ $row->bank_name ?: '未填写' }}</td>
|
||||
<td>{{ $row->bank_city_branch ?: '未填写' }}</td>
|
||||
@ -271,9 +274,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="14" class="pending-balance-empty">当前没有账户异常待办</td></tr>
|
||||
<tr><td colspan="15" class="pending-balance-empty">当前没有账户异常待办</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4">当前筛选合计</td>
|
||||
<td class="num" data-pending-current-balance-total="{{ $money($summary['current_balance'] ?? 0) }}">{{ $money($summary['current_balance'] ?? 0) }}</td>
|
||||
<td colspan="10" aria-label="其余列无金额合计"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -316,6 +326,7 @@
|
||||
account_short: 92,
|
||||
account_type: 72,
|
||||
account_status: 78,
|
||||
current_balance: 112,
|
||||
account_no: 180,
|
||||
bank_name: 250,
|
||||
bank_city_branch: 180,
|
||||
|
||||
@ -23,6 +23,9 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
'account_no' => '6222000000000001',
|
||||
'bank_name' => '测试银行',
|
||||
'company_plan' => '保留',
|
||||
'current_balance' => 321.45,
|
||||
'blocked_amount' => 0,
|
||||
'daily_available' => 321.45,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -123,7 +126,12 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('不可用金额合计')
|
||||
->assertSee('href="' . $this->unavailableBalancesUrl() . '"', false)
|
||||
->assertSee('查看账户异常待办')
|
||||
->assertSee('可用金额 − 不可用金额')
|
||||
->assertSee('正常计可用,异常计不可用')
|
||||
->assertSee('<th class="num">当前余额</th><th class="num">可用金额</th><th class="num">不可用金额</th>', false)
|
||||
->assertSee('data-ledger-summary-available', false)
|
||||
->assertSee('data-account-current-balance="321.45"', false)
|
||||
->assertSee('data-account-available-amount', false)
|
||||
->assertSee('data-account-blocked-amount', false)
|
||||
->assertSee('未填写')
|
||||
->assertDontSee('data-account-filter-state', false)
|
||||
->assertSee('value="normal" selected', false);
|
||||
@ -141,8 +149,8 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
'bank_name' => '待办测试银行',
|
||||
'bank_city_branch' => '上海 / 待办测试支行',
|
||||
'current_balance' => 100,
|
||||
'blocked_amount' => 25.5,
|
||||
'daily_available' => 74.5,
|
||||
'blocked_amount' => 100,
|
||||
'daily_available' => 0,
|
||||
'owner' => '待办负责人',
|
||||
'next_action' => '核实冻结原因',
|
||||
'remark' => '独立待办备注',
|
||||
@ -158,8 +166,8 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
'company_name' => '正常状态测试公司',
|
||||
'account_short' => '正常状态测试账户',
|
||||
'current_balance' => 100,
|
||||
'blocked_amount' => 50,
|
||||
'daily_available' => 50,
|
||||
'blocked_amount' => 0,
|
||||
'daily_available' => 100,
|
||||
],
|
||||
[
|
||||
'company_name' => '待办测试公司二',
|
||||
@ -168,9 +176,9 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
'account_type' => '个人卡',
|
||||
'bank_name' => '待办测试银行二',
|
||||
'bank_city_branch' => '杭州 / 待办测试支行',
|
||||
'current_balance' => 100,
|
||||
'blocked_amount' => 10,
|
||||
'daily_available' => 90,
|
||||
'current_balance' => 250,
|
||||
'blocked_amount' => 250,
|
||||
'daily_available' => 0,
|
||||
],
|
||||
];
|
||||
foreach ($accounts as $account) {
|
||||
@ -206,7 +214,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('待办测试公司')
|
||||
->assertSee('data-pending-account="待办测试账户"', false)
|
||||
->assertSee('不可用金额合计')
|
||||
->assertSee('35.50')
|
||||
->assertSee('350.00')
|
||||
->assertSee('限额')
|
||||
->assertSee('网商银行')
|
||||
->assertSee('6222000000000099')
|
||||
@ -216,6 +224,10 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('办理方式')
|
||||
->assertSee('当前进度')
|
||||
->assertSee('是否完成')
|
||||
->assertSee('<th data-pending-column-key="current_balance">当前余额</th>', false)
|
||||
->assertSee('data-pending-current-balance="100.00"', false)
|
||||
->assertSee('data-pending-current-balance-total="350.00"', false)
|
||||
->assertSee('当前筛选合计')
|
||||
->assertSee('name="pending_short"', false)
|
||||
->assertSee('name="pending_type"', false)
|
||||
->assertSee('name="pending_status"', false)
|
||||
@ -288,8 +300,17 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
$this->loginAs()->get($filteredUrl)
|
||||
->assertOk()
|
||||
->assertSee('data-pending-account="待办测试账户"', false)
|
||||
->assertSee('data-pending-current-balance-total="100.00"', false)
|
||||
->assertDontSee('data-pending-account="待办测试账户二"', false);
|
||||
|
||||
$connection->table('fin_accounts')
|
||||
->where('account_short', '待办测试账户')
|
||||
->update(['current_balance' => 180]);
|
||||
$this->loginAs()->get($filteredUrl)
|
||||
->assertOk()
|
||||
->assertSee('data-pending-current-balance="180.00"', false)
|
||||
->assertSee('data-pending-current-balance-total="180.00"', false);
|
||||
|
||||
$sortResponse = $this->loginAs()->get(
|
||||
$this->unavailableBalancesUrl() . '?pending_sort=started_at&pending_direction=desc'
|
||||
)->assertOk()->assertSee('aria-sort="descending"', false);
|
||||
@ -314,6 +335,12 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
'account_short' => '待办测试账户',
|
||||
'status' => 'normal',
|
||||
], 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'account_short' => '待办测试账户',
|
||||
'current_balance' => 180,
|
||||
'daily_available' => 180,
|
||||
'blocked_amount' => 0,
|
||||
], 'business_mirror');
|
||||
|
||||
$this->loginAs()->get($this->unavailableBalancesUrl())
|
||||
->assertOk()
|
||||
@ -511,11 +538,23 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'limited')
|
||||
->assertJsonPath('label', '限额');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'account_short' => '测试账户',
|
||||
'current_balance' => 321.45,
|
||||
'daily_available' => 0,
|
||||
'blocked_amount' => 321.45,
|
||||
], 'business_mirror');
|
||||
|
||||
$this->loginAs()->postJson($this->statusUrl(), [
|
||||
'account_short' => '测试账户',
|
||||
'status' => 'normal',
|
||||
])->assertOk()->assertJsonPath('label', '正常');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'account_short' => '测试账户',
|
||||
'current_balance' => 321.45,
|
||||
'daily_available' => 321.45,
|
||||
'blocked_amount' => 0,
|
||||
], 'business_mirror');
|
||||
|
||||
$this->assertDatabaseCount('fin_account_statuses', 1, 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_account_statuses', [
|
||||
|
||||
@ -73,4 +73,24 @@ class FinanceBatchIsolationTest extends TestCase
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_finance_ledger_recalculation_runs_in_the_isolated_batch_profile(): void
|
||||
{
|
||||
$directory = sys_get_temp_dir() . '/caiwu-ledger-batch-' . bin2hex(random_bytes(6));
|
||||
mkdir($directory);
|
||||
try {
|
||||
$process = $this->runBatch($directory, ['finance-ledger:recalculate']);
|
||||
$this->assertTrue($process->isSuccessful(), $process->getErrorOutput());
|
||||
$arguments = file($directory . '/called', FILE_IGNORE_NEW_LINES);
|
||||
$this->assertContains('batch', $arguments);
|
||||
$this->assertSame('finance-ledger:recalculate', end($arguments));
|
||||
$this->assertContains('--no-deps', $arguments);
|
||||
$this->assertSame('image=123456789abc', $arguments[0]);
|
||||
} finally {
|
||||
foreach (glob($directory . '/*') as $path) {
|
||||
unlink($path);
|
||||
}
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,13 @@ class FinanceLedgerCalculatorTest extends TestCase
|
||||
$table->decimal('opening_balance', 18, 2)->default(0);
|
||||
$table->timestamp('updated_at')->nullable();
|
||||
});
|
||||
$schema->create('fin_account_statuses', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->string('account_short')->unique();
|
||||
$table->string('status');
|
||||
$table->string('updated_by')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
$schema->create('fin_transactions', function (Blueprint $table): void {
|
||||
$table->increments('id');
|
||||
$table->date('txn_date');
|
||||
@ -83,6 +90,10 @@ class FinanceLedgerCalculatorTest extends TestCase
|
||||
['account_short' => '账户A', 'opening_date' => '2026-10-17', 'opening_balance' => 1000],
|
||||
['account_short' => '账户B', 'opening_date' => '2026-10-17', 'opening_balance' => 500],
|
||||
]);
|
||||
$db->table('fin_account_statuses')->insert([
|
||||
['account_short' => '账户A', 'status' => 'normal', 'updated_by' => 'Test User'],
|
||||
['account_short' => '账户B', 'status' => 'limited', 'updated_by' => 'Test User'],
|
||||
]);
|
||||
$db->table('fin_transactions')->insert([
|
||||
['txn_date' => '2026-10-17', 'account_short' => '', 'source' => 'shop', 'source_value' => '店铺A', 'income' => 100, 'expense' => 0],
|
||||
['txn_date' => '2026-10-18', 'account_short' => '账户A', 'source' => 'shop', 'source_value' => '店铺A', 'income' => 0, 'expense' => 50],
|
||||
@ -118,9 +129,30 @@ class FinanceLedgerCalculatorTest extends TestCase
|
||||
$this->assertEqualsWithDelta(1050.00, (float) $rows[1]->closing_balance, 0.001);
|
||||
$this->assertEqualsWithDelta(500.00, (float) $rows[2]->closing_balance, 0.001);
|
||||
|
||||
$accounts = DB::connection('business_mirror')->table('fin_accounts')->pluck('current_balance', 'account_short');
|
||||
$this->assertEqualsWithDelta(1050.00, (float) $accounts['账户A'], 0.001);
|
||||
$this->assertEqualsWithDelta(500.00, (float) $accounts['账户B'], 0.001);
|
||||
$accounts = DB::connection('business_mirror')->table('fin_accounts')->get()->keyBy('account_short');
|
||||
$this->assertEqualsWithDelta(1050.00, (float) $accounts['账户A']->current_balance, 0.001);
|
||||
$this->assertEqualsWithDelta(1050.00, (float) $accounts['账户A']->daily_available, 0.001);
|
||||
$this->assertEqualsWithDelta(0.00, (float) $accounts['账户A']->blocked_amount, 0.001);
|
||||
$this->assertEqualsWithDelta(500.00, (float) $accounts['账户B']->current_balance, 0.001);
|
||||
$this->assertEqualsWithDelta(0.00, (float) $accounts['账户B']->daily_available, 0.001);
|
||||
$this->assertEqualsWithDelta(500.00, (float) $accounts['账户B']->blocked_amount, 0.001);
|
||||
$this->assertEqualsWithDelta(1050.00, (float) DB::connection('business_mirror')->table('fin_shops')->value('shop_balance'), 0.001);
|
||||
}
|
||||
|
||||
public function test_account_amounts_follow_status_changes(): void
|
||||
{
|
||||
$calculator = app(FinanceLedgerCalculator::class);
|
||||
$calculator->recalculate();
|
||||
|
||||
DB::connection('business_mirror')->table('fin_account_statuses')
|
||||
->where('account_short', '账户B')
|
||||
->update(['status' => 'normal', 'updated_by' => 'Test User']);
|
||||
$calculator->refreshAccountAmounts('账户B');
|
||||
|
||||
$account = DB::connection('business_mirror')->table('fin_accounts')
|
||||
->where('account_short', '账户B')
|
||||
->first();
|
||||
$this->assertEqualsWithDelta(500.00, (float) $account->daily_available, 0.001);
|
||||
$this->assertEqualsWithDelta(0.00, (float) $account->blocked_amount, 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user