财务台账支持维护公司资料 #592
@ -48,6 +48,13 @@ class FinanceLedger extends Controller
|
||||
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
|
||||
$data = $request->validate([
|
||||
'company_name' => ['required', 'string', 'max:100'],
|
||||
'company_register_place' => ['nullable', 'string', 'max:200'],
|
||||
'company_business_type' => ['nullable', 'string', 'max:50'],
|
||||
'company_tax_type' => ['nullable', 'string', 'max:50'],
|
||||
'company_status' => ['nullable', 'string', 'max:20'],
|
||||
'company_basic_bank' => ['nullable', 'string', 'max:200'],
|
||||
'company_basic_account_last4' => ['nullable', 'string', 'max:50'],
|
||||
'company_remark' => ['nullable', 'string', 'max:500'],
|
||||
'account_short' => ['required', 'string', 'max:100'],
|
||||
'account_type' => ['nullable', 'string', 'max:50'],
|
||||
'status' => ['required', 'string', Rule::in(array_keys(FinanceLedgerAccountStatus::OPTIONS))],
|
||||
@ -90,9 +97,11 @@ class FinanceLedger extends Controller
|
||||
|
||||
$result = $creator->create($data);
|
||||
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
$message = $result['company_created']
|
||||
? '已新增公司并建立首个账户。'
|
||||
: '已新增账户并关联现有公司。';
|
||||
$message = match (true) {
|
||||
$result['company_created'] => '已新增公司并建立首个账户。',
|
||||
$result['company_updated'] => '已新增账户并更新公司资料。',
|
||||
default => '已新增账户并关联现有公司。',
|
||||
};
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
|
||||
@ -64,7 +64,7 @@ docker compose -f docker-compose.local.yml exec -u www-data app \
|
||||
- 路由:`/workspace/financial-operations/ledger`(旧地址会自动跳转)
|
||||
- 页面:账户总表、店铺台账、每日资金流水、动账明细、账户映射、数据来源
|
||||
- 账户总表支持导出空白 Excel 录入模板,包含页面列、状态和账户类型选项及填写说明
|
||||
- 账户总表编辑模式支持直接新增公司和首个账户:公司不存在时同步写入公司台账,账户简称必须唯一,可同时登记期初日期与期初余额
|
||||
- 账户总表编辑模式支持直接新增公司和首个账户:公司不存在时同步写入公司台账,可维护注册地、企业类型、税务类型、公司状态、基本户信息和备注;账户简称必须唯一,可同时登记期初日期与期初余额
|
||||
- 写权限:账户映射、账户状态和账户类型修改仅系统管理员及配置的财务台账管理员角色;其他账号只读
|
||||
- 调度:`finance-ledger:sync-rpt --days=7 --merge` 每天 02:30(Asia/Shanghai)
|
||||
|
||||
|
||||
@ -21,6 +21,15 @@ class FinanceLedgerAccountCreator
|
||||
$accountNo = $this->text($data['account_no'] ?? '');
|
||||
$accountType = $this->text($data['account_type'] ?? '');
|
||||
$companyPlan = $this->text($data['company_plan'] ?? '');
|
||||
$companyFields = [
|
||||
'register_place' => $this->nullableText($data['company_register_place'] ?? null),
|
||||
'business_type' => $this->nullableText($data['company_business_type'] ?? null),
|
||||
'tax_type' => $this->nullableText($data['company_tax_type'] ?? null),
|
||||
'status' => $this->nullableText($data['company_status'] ?? null),
|
||||
'basic_bank' => $this->nullableText($data['company_basic_bank'] ?? null),
|
||||
'basic_account_last4' => $this->nullableText($data['company_basic_account_last4'] ?? null),
|
||||
'remark' => $this->nullableText($data['company_remark'] ?? null),
|
||||
];
|
||||
$openingDate = (string) ($data['opening_date'] ?? now()->toDateString());
|
||||
$openingBalance = round((float) ($data['opening_balance'] ?? 0), 2);
|
||||
$status = FinanceLedgerAccountStatus::normalize((string) ($data['status'] ?? ''));
|
||||
@ -38,6 +47,7 @@ class FinanceLedgerAccountCreator
|
||||
$accountNo,
|
||||
$accountType,
|
||||
$companyPlan,
|
||||
$companyFields,
|
||||
$openingDate,
|
||||
$openingBalance,
|
||||
$status,
|
||||
@ -55,12 +65,28 @@ class FinanceLedgerAccountCreator
|
||||
|
||||
$company = $connection->table('fin_companies')->where('name', $companyName)->first(['id']);
|
||||
$companyCreated = $company === null;
|
||||
$companyUpdated = false;
|
||||
if ($companyCreated) {
|
||||
$connection->table('fin_companies')->insert([
|
||||
'name' => $companyName,
|
||||
...$companyFields,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$providedCompanyFields = array_filter(
|
||||
$companyFields,
|
||||
static fn (?string $value): bool => $value !== null,
|
||||
);
|
||||
if ($providedCompanyFields !== []) {
|
||||
$connection->table('fin_companies')
|
||||
->where('id', $company->id)
|
||||
->update([
|
||||
...$providedCompanyFields,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$companyUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
$accountId = $connection->table('fin_accounts')->insertGetId([
|
||||
@ -118,6 +144,7 @@ class FinanceLedgerAccountCreator
|
||||
'company_name' => $companyName,
|
||||
'account_short' => $accountShort,
|
||||
'company_created' => $companyCreated,
|
||||
'company_updated' => $companyUpdated,
|
||||
'status' => $status,
|
||||
'opening_date' => $openingDate,
|
||||
'opening_balance' => $openingBalance,
|
||||
|
||||
@ -211,13 +211,20 @@
|
||||
.ledger-company-form__close { display:inline-grid; flex:0 0 auto; place-items:center; width:1.8rem; height:1.8rem; padding:0; border:1px solid transparent; border-radius:.35rem; background:transparent; color:var(--vip-content-muted, #595959); cursor:pointer; }
|
||||
.ledger-company-form__close:hover { border-color:var(--vip-border, #D8D2C7); color:var(--vip-content-accent-dark, #856632); }
|
||||
.ledger-company-form__body { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:.8rem; padding:1rem 1.1rem; }
|
||||
.ledger-company-form__section { grid-column:1 / -1; display:flex; align-items:baseline; gap:.55rem; padding-top:.75rem; border-top:1px solid #e8e3da; color:var(--vip-content-ink, #171717); font-size:.76rem; font-weight:700; }
|
||||
.ledger-company-form__section:first-child { padding-top:0; border-top:0; }
|
||||
.ledger-company-form__section span:last-child { color:var(--vip-content-muted, #595959); font-size:.65rem; font-weight:400; }
|
||||
.ledger-company-field { display:grid; min-width:0; gap:.3rem; }
|
||||
.ledger-company-field--wide { grid-column:span 2; }
|
||||
.ledger-company-field--full { grid-column:1 / -1; }
|
||||
.ledger-company-field label { color:var(--vip-content-muted, #595959); font-size:.7rem; font-weight:600; }
|
||||
.ledger-company-field input,
|
||||
.ledger-company-field select { width:100%; min-width:0; height:2.15rem; padding:0 .58rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; }
|
||||
.ledger-company-field select,
|
||||
.ledger-company-field textarea { width:100%; min-width:0; min-height:2.15rem; padding:0 .58rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; }
|
||||
.ledger-company-field textarea { min-height:3.4rem; padding-top:.45rem; padding-bottom:.45rem; resize:vertical; }
|
||||
.ledger-company-field input:focus,
|
||||
.ledger-company-field select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
|
||||
.ledger-company-field select:focus,
|
||||
.ledger-company-field textarea:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
|
||||
.ledger-company-field__hint { color:var(--vip-content-muted, #595959); font-size:.65rem; line-height:1.45; }
|
||||
.ledger-company-form__message { grid-column:1 / -1; margin:0; padding:.55rem .65rem; border:1px solid rgba(184,0,0,.28); border-radius:.4rem; background:#fff5f5; color:var(--vip-danger, #B80000); font-size:.7rem; line-height:1.5; }
|
||||
.ledger-company-form__message[hidden] { display:none !important; }
|
||||
@ -479,18 +486,54 @@
|
||||
<header class="ledger-company-form__header">
|
||||
<div>
|
||||
<h3 class="ledger-company-form__title" id="ledger-company-dialog-title">新增公司/账户</h3>
|
||||
<p class="ledger-company-form__help">公司不存在时自动加入公司台账;账户简称必须唯一。期初余额会作为该账户的起始资金并进入每日流水。</p>
|
||||
<p class="ledger-company-form__help">公司不存在时自动加入公司台账,也可补充已有公司的资料。账户简称必须唯一,期初余额会作为该账户的起始资金并进入每日流水。</p>
|
||||
</div>
|
||||
<button class="ledger-company-form__close" type="button" aria-label="关闭新增公司/账户" data-ledger-company-close>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</header>
|
||||
<div class="ledger-company-form__body">
|
||||
<div class="ledger-company-form__section">
|
||||
<span>公司资料</span>
|
||||
<span>除公司名称外均可留空;已有公司只会更新本次填写的字段。</span>
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-name">公司名称 <span aria-hidden="true">*</span></label>
|
||||
<input id="ledger-company-name" name="company_name" type="text" list="ledger-company-options" maxlength="100" required autocomplete="off" placeholder="选择或输入公司名称" data-ledger-company-first-field>
|
||||
<span class="ledger-company-field__hint">输入公司台账中不存在的名称时会自动新增公司。</span>
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-register-place">注册地</label>
|
||||
<input id="ledger-company-register-place" name="company_register_place" type="text" maxlength="200" autocomplete="off" placeholder="如 上海市浦东新区">
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-business-type">企业类型</label>
|
||||
<input id="ledger-company-business-type" name="company_business_type" type="text" maxlength="50" autocomplete="off" placeholder="如 有限责任公司">
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-tax-type">税务类型</label>
|
||||
<input id="ledger-company-tax-type" name="company_tax_type" type="text" maxlength="50" autocomplete="off" placeholder="如 一般纳税人">
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-company-status">公司状态</label>
|
||||
<input id="ledger-company-company-status" name="company_status" type="text" maxlength="20" autocomplete="off" placeholder="如 正常、注销">
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-basic-last4">基本户后4位</label>
|
||||
<input id="ledger-company-basic-last4" name="company_basic_account_last4" type="text" maxlength="50" autocomplete="off" placeholder="如 1234">
|
||||
</div>
|
||||
<div class="ledger-company-field ledger-company-field--full">
|
||||
<label for="ledger-company-basic-bank">基本户开户行</label>
|
||||
<input id="ledger-company-basic-bank" name="company_basic_bank" type="text" maxlength="200" autocomplete="off" placeholder="公司基本户开户行">
|
||||
</div>
|
||||
<div class="ledger-company-field ledger-company-field--full">
|
||||
<label for="ledger-company-remark">公司备注</label>
|
||||
<textarea id="ledger-company-remark" name="company_remark" rows="2" maxlength="500" placeholder="公司层面的补充说明"></textarea>
|
||||
</div>
|
||||
<div class="ledger-company-form__section">
|
||||
<span>账户资料</span>
|
||||
<span>首个账户会自动关联上方公司。</span>
|
||||
</div>
|
||||
<div class="ledger-company-field">
|
||||
<label for="ledger-company-account-short">账户简称 <span aria-hidden="true">*</span></label>
|
||||
<input id="ledger-company-account-short" name="account_short" type="text" maxlength="100" required autocomplete="off" placeholder="如 上海甲公司农行">
|
||||
@ -555,7 +598,7 @@
|
||||
<button class="ledger-btn" type="button" data-ledger-company-cancel>取消</button>
|
||||
<button class="ledger-btn primary" type="submit" data-ledger-company-save>
|
||||
<span class="material-icons-outlined" aria-hidden="true">check</span>
|
||||
保存公司/账户
|
||||
保存公司及账户
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@ -141,6 +141,13 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
->assertSee('data-ledger-company-dialog', false)
|
||||
->assertSee('新增公司/账户')
|
||||
->assertSee('name="company_name"', false)
|
||||
->assertSee('name="company_register_place"', false)
|
||||
->assertSee('name="company_business_type"', false)
|
||||
->assertSee('name="company_tax_type"', false)
|
||||
->assertSee('name="company_status"', false)
|
||||
->assertSee('name="company_basic_bank"', false)
|
||||
->assertSee('name="company_basic_account_last4"', false)
|
||||
->assertSee('name="company_remark"', false)
|
||||
->assertSee('name="opening_balance"', false)
|
||||
->assertSee('id="ledger-company-options"', false)
|
||||
->assertSee('aria-checked="false"', false)
|
||||
@ -753,6 +760,13 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
{
|
||||
$this->loginAs()->postJson($this->createAccountUrl(), [
|
||||
'company_name' => '新设财务公司',
|
||||
'company_register_place' => '上海市浦东新区',
|
||||
'company_business_type' => '有限责任公司',
|
||||
'company_tax_type' => '一般纳税人',
|
||||
'company_status' => '正常',
|
||||
'company_basic_bank' => '中国农业银行上海浦东支行',
|
||||
'company_basic_account_last4' => '1234',
|
||||
'company_remark' => '新设公司财务备注',
|
||||
'account_short' => '新设财务公司农行',
|
||||
'account_type' => '公户',
|
||||
'status' => 'limited',
|
||||
@ -774,6 +788,13 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
|
||||
$this->assertDatabaseHas('fin_companies', [
|
||||
'name' => '新设财务公司',
|
||||
'register_place' => '上海市浦东新区',
|
||||
'business_type' => '有限责任公司',
|
||||
'tax_type' => '一般纳税人',
|
||||
'status' => '正常',
|
||||
'basic_bank' => '中国农业银行上海浦东支行',
|
||||
'basic_account_last4' => '1234',
|
||||
'remark' => '新设公司财务备注',
|
||||
], 'business_mirror');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'company_name' => '新设财务公司',
|
||||
@ -823,6 +844,49 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
$this->assertDatabaseCount('fin_companies', 1, 'business_mirror');
|
||||
}
|
||||
|
||||
public function test_existing_company_profile_can_be_updated_with_a_new_account(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('fin_companies')->insert([
|
||||
'name' => '已有资料公司',
|
||||
'register_place' => '江苏省苏州市',
|
||||
'business_type' => '个体工商户',
|
||||
'tax_type' => '小规模纳税人',
|
||||
'status' => '存续',
|
||||
'remark' => '原公司备注',
|
||||
]);
|
||||
|
||||
$this->loginAs()->postJson($this->createAccountUrl(), [
|
||||
'company_name' => '已有资料公司',
|
||||
'company_business_type' => '有限责任公司',
|
||||
'company_tax_type' => '一般纳税人',
|
||||
'company_status' => '正常',
|
||||
'company_basic_bank' => '中国工商银行苏州分行',
|
||||
'company_basic_account_last4' => '8899',
|
||||
'company_remark' => '更新后的公司备注',
|
||||
'account_short' => '已有资料公司工行',
|
||||
'account_type' => '公户',
|
||||
'status' => 'normal',
|
||||
'account_no' => '1102000000008899',
|
||||
'opening_date' => '2026-10-20',
|
||||
'opening_balance' => 0,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('account.company_created', false)
|
||||
->assertJsonPath('account.company_updated', true)
|
||||
->assertJsonPath('message', '已新增账户并更新公司资料。');
|
||||
|
||||
$this->assertDatabaseHas('fin_companies', [
|
||||
'name' => '已有资料公司',
|
||||
'register_place' => '江苏省苏州市',
|
||||
'business_type' => '有限责任公司',
|
||||
'tax_type' => '一般纳税人',
|
||||
'status' => '正常',
|
||||
'basic_bank' => '中国工商银行苏州分行',
|
||||
'basic_account_last4' => '8899',
|
||||
'remark' => '更新后的公司备注',
|
||||
], 'business_mirror');
|
||||
}
|
||||
|
||||
public function test_invalid_status_or_account_is_rejected(): void
|
||||
{
|
||||
$this->withExceptionHandling()->loginAs()->postJson($this->statusUrl(), [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user