P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件)

This commit is contained in:
Agent Control Runner
2026-10-23 16:02:54 +09:00
parent 613b9886da
commit 7808ca76db
20 changed files with 1906 additions and 17 deletions
@@ -65,6 +65,33 @@ func settle(period: Dictionary, economy: EconomyService) -> Dictionary:
return {&"ok": true, &"replayed": false, &"receipt": receipt.duplicate(true), &"transaction_id": transaction_id}
## Whole Gold that settling these closed periods would owe (arrears included),
## planned with no cash; -1 if a period is invalid or already settled. Nothing changes.
func preview_debt(periods: Array) -> int:
var arrears := _arrears.duplicate()
var fractions := _fractions.duplicate()
var last_end := int(_receipts[-1].period.period_end) if not _receipts.is_empty() else -1
var candidate := FacilityUpkeepSettlement.new()
for raw: Variant in periods:
if not raw is Dictionary:
return -1
var canonical := _parse_period(raw)
if canonical.is_empty() or (last_end >= 0 and canonical.period_start != last_end):
return -1
candidate._arrears = arrears
candidate._fractions = fractions
var plan := candidate._plan(canonical, 0)
if not plan[&"ok"]:
return -1
arrears = plan.arrears
fractions = plan.fractions
last_end = canonical.period_end
var debt := 0
for amount: int in arrears.values():
debt += amount
return debt
func get_arrears_through(minute: int) -> int:
var low := 0
var high := _receipts.size()
+86
View File
@@ -0,0 +1,86 @@
class_name FinancePolicy
extends RefCounted
## Front (表金) and hidden (裏金) funds, the World Tree rank tax and dark bank interest.
## Values are the user-approved initial test values (decisions A09-A12, 2026-10-23).
## The front fund keeps the existing PLAYER_CASH account; saves made before the split
## carry no hidden fund account and are read with every Gold as front funds (A12).
const FRONT_CASH: StringName = &"PLAYER_CASH"
const BACK_CASH: StringName = &"BACK_CASH"
const RANK_TAX_EXPENSE: StringName = &"RANK_TAX_EXPENSE"
const RANK_TAX_PAYABLE: StringName = &"RANK_TAX_PAYABLE"
const LOAN_INTEREST_EXPENSE: StringName = &"LOAN_INTEREST_EXPENSE"
const LOAN_INTEREST_PAYABLE: StringName = &"LOAN_INTEREST_PAYABLE"
const LOAN_LIABILITY_PREFIX := "LOAN_LIABILITY:"
## A10: 10% of the closed day's sales (front + hidden), whole Gold rounded down.
const RANK_TAX_PERCENT := 10
## A11: 1% of the outstanding principal per day, whole Gold rounded down.
const LOAN_INTEREST_PERCENT_PER_DAY := 1
## Services of facilities with one of these tags earn hidden funds (原本: 成人施設、
## カジノ、脅迫金、ユニット売却は裏の収益). Every other service earns front funds.
const BACK_FUND_TAGS: Array[StringName] = [&"restricted_service", &"adult_only"]
## Ledger categories. Accruals are expenses of the closed day; payments, top-ups and
## the payable balances are not income or expense.
const TOP_UP_CATEGORY: StringName = &"BACK_FUND_TOP_UP"
const RANK_TAX_CATEGORY: StringName = &"RANK_TAX"
const RANK_TAX_PAYMENT_CATEGORY: StringName = &"RANK_TAX_PAYMENT"
const LOAN_INTEREST_CATEGORY: StringName = &"LOAN_INTEREST"
const LOAN_INTEREST_PAYMENT_CATEGORY: StringName = &"LOAN_INTEREST_PAYMENT"
const NON_PROFIT_CATEGORIES: Array[StringName] = [TOP_UP_CATEGORY, RANK_TAX_PAYMENT_CATEGORY, LOAN_INTEREST_PAYMENT_CATEGORY]
static func rank_tax(sales: int) -> int:
return maxi(sales, 0) / 100 * RANK_TAX_PERCENT + maxi(sales, 0) % 100 * RANK_TAX_PERCENT / 100
static func loan_interest(outstanding_principal: int) -> int:
return maxi(outstanding_principal, 0) / 100 * LOAN_INTEREST_PERCENT_PER_DAY \
+ maxi(outstanding_principal, 0) % 100 * LOAN_INTEREST_PERCENT_PER_DAY / 100
static func is_back_fund_service(tags: Array) -> bool:
for tag: StringName in BACK_FUND_TAGS:
if tag in tags:
return true
return false
## Accounts of the split funds. A save without BACK_CASH predates the split and must
## not hold any of the other accounts either; they are then created empty (A12).
## Any other partial or incompatible set is rejected.
static func prepare_accounts(economy: EconomyService) -> bool:
if economy == null or not economy.has_account(FRONT_CASH) or economy.account_allows_negative(FRONT_CASH):
return false
var expected := {
BACK_CASH: false, RANK_TAX_EXPENSE: false, LOAN_INTEREST_EXPENSE: false,
RANK_TAX_PAYABLE: true, LOAN_INTEREST_PAYABLE: true,
}
if not economy.has_account(BACK_CASH):
for account_id: StringName in expected:
if economy.has_account(account_id):
return false
for account_id: StringName in expected:
if not economy.register_account(account_id, bool(expected[account_id])):
return false
return true
for account_id: StringName in expected:
if not economy.has_account(account_id) \
or economy.account_allows_negative(account_id) != bool(expected[account_id]):
return false
return true
static func outstanding_principal(economy: EconomyService) -> int:
var total := 0
for account: Dictionary in economy.get_accounts():
if String(account[&"account_id"]).begins_with(LOAN_LIABILITY_PREFIX):
total += -int(account[&"balance"])
return total
static func available_funds(economy: EconomyService) -> int:
return economy.get_balance(FRONT_CASH) + economy.get_balance(BACK_CASH)
+1
View File
@@ -0,0 +1 @@
uid://l14tq0avw0ux
+13 -4
View File
@@ -18,7 +18,8 @@ func preview_purchase(runtime: Variant, warehouse_id: StringName, item_id: Strin
or not bool(item.get(&"purchasable", false)) or quantity <= 0 or quantity > 10000: return quote
var frozen := PurchaseQuoteV2.make(item_id, quantity, int(item[&"unit_price"]))
if frozen.is_empty(): return quote
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH")
# A09: purchases may use the hidden fund for what the front fund cannot pay.
var cash: int = FinancePolicy.available_funds(runtime.economy_service)
var space: int = maxi(SupplyCatalog.storage_capacity(facility.definition_id) - SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id) - _incoming_weight(runtime, facility.local_inventory_id), 0)
@warning_ignore("integer_division")
var slots := space / int(item[&"unit_weight"])
@@ -40,7 +41,7 @@ func preview_purchase(runtime: Variant, warehouse_id: StringName, item_id: Strin
func _preview_purchase_v1(runtime: Variant, warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary:
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH")
var cash: int = FinancePolicy.available_funds(runtime.economy_service)
var item := SupplyCatalog.get_item(item_id)
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
var quote := {&"ok": false, &"reason": &"", &"total_cost": 0, &"cash_after": cash, &"quantity_after": 0, &"max_quantity": 0}
@@ -99,6 +100,13 @@ func _purchase_paid(runtime: Variant, warehouse_id: StringName, item_id: StringN
var memo: StringName = SupplyAutoPurchasePlans.receipt_memo(item_id, quantity, int(item[&"unit_price"]), int(quote[&"total_cost"])) if record_auto_attempt else &"supply_purchase"
if quote.has(&"purchase_quote"):
memo = PurchaseQuoteV2.receipt_memo(quote[&"purchase_quote"])
# A09: the hidden fund covers what the front fund cannot pay, in its own posting
# before the single-entry purchase (its receipt shape is unchanged).
var top_up: CommandResult = runtime.top_up_front_fund(int(quote[&"total_cost"]), minute, StringName("finance.back_top_up.purchase." + String(ledger_key)))
if not top_up.is_success():
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"PAYMENT_FAILED")
var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, ledger_key, memo, &"", false)
if not paid.is_success():
runtime.inventory_service.restore_snapshot(inventory_before)
@@ -114,7 +122,8 @@ func _purchase_paid(runtime: Variant, warehouse_id: StringName, item_id: StringN
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
return _purchase_failure(command_id, &"PAYMENT_FAILED")
if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])):
if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])) \
or (int(top_up.details.get(&"amount", 0)) > 0 and not runtime.economy_service.commit_transaction_notification(StringName(top_up.details[&"transaction_id"]))):
runtime.inventory_service.restore_snapshot(inventory_before)
runtime.economy_service.restore_snapshot(economy_before)
if record_auto_attempt:
@@ -274,7 +283,7 @@ func get_report(runtime: Variant) -> Dictionary:
row[&"reason_codes"].append(&"NO_INPUT")
destinations.append(row)
var bridge_report: Dictionary = runtime.supply_logistics.get_report()
return {&"cash": runtime.economy_service.get_balance(&"PLAYER_CASH"), &"catalog": SupplyCatalog.get_purchase_items(), &"warehouses": warehouses, &"kitchens": kitchens, &"destinations": destinations, &"deliveries": bridge_report.get(&"deliveries", [])}
return {&"cash": runtime.economy_service.get_balance(&"PLAYER_CASH"), &"back_cash": runtime.economy_service.get_balance(FinancePolicy.BACK_CASH), &"catalog": SupplyCatalog.get_purchase_items(), &"warehouses": warehouses, &"kitchens": kitchens, &"destinations": destinations, &"deliveries": bridge_report.get(&"deliveries", [])}
func ensure_deliveries(runtime: Variant, minute: int) -> void:
+22
View File
@@ -76,9 +76,17 @@ func get_report() -> Dictionary:
var construction_outflow := 0
var wage_outflow := 0
var upkeep_outflow := 0
var back_inflow := 0
var back_top_up := 0
var rank_tax_outflow := 0
var loan_interest_outflow := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION":
continue
if entry.debit_account == FinancePolicy.BACK_CASH:
back_inflow += entry.amount
if entry.credit_account == FinancePolicy.BACK_CASH and entry.category == FinancePolicy.TOP_UP_CATEGORY:
back_top_up += entry.amount
if entry.debit_account == &"PLAYER_CASH":
inflow += entry.amount
if entry.credit_account == &"PLAYER_CASH":
@@ -89,6 +97,10 @@ func get_report() -> Dictionary:
upkeep_outflow += entry.amount
elif entry.category == &"WAGE":
wage_outflow += entry.amount
elif entry.category == FinancePolicy.RANK_TAX_PAYMENT_CATEGORY:
rank_tax_outflow += entry.amount
elif entry.category == FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY:
loan_interest_outflow += entry.amount
var summaries: Array[Dictionary] = []
if _runtime.operations_coordinator != null:
summaries = _runtime.operations_coordinator.get_day_summaries()
@@ -105,6 +117,11 @@ func get_report() -> Dictionary:
&"construction_cash_out": construction_outflow, &"wage_cash_out": wage_outflow,
&"upkeep_cash_out": upkeep_outflow,
&"upkeep_arrears": _runtime.upkeep_settlement.get_arrears_total(),
&"back_cash": _runtime.economy_service.get_balance(FinancePolicy.BACK_CASH),
&"back_cash_in": back_inflow, &"back_top_up": back_top_up,
&"rank_tax_cash_out": rank_tax_outflow, &"loan_interest_cash_out": loan_interest_outflow,
&"finance_arrears": -_runtime.economy_service.get_balance(FinancePolicy.RANK_TAX_PAYABLE)
- _runtime.economy_service.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE),
&"closed_days": summaries, &"selected_close": selected.duplicate(true),
}
@@ -117,6 +134,8 @@ func refresh() -> void:
_refresh_repayment_controls()
var report := get_report()
_cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG / 維持費支払 %dG\n未払い維持費 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"upkeep_cash_out"], report[&"upkeep_arrears"], report[&"cash_net"]]
# 表金は上の手持ち。裏金は裏のサービスの売上で、表金で払えない分だけを補う(A09)。
_cash_label.text += "\n裏金 %dG(本日の入金 +%dG / 表金へ補填 −%dG)\n本日の税 %dG / 利息 %dG(未払い %dG)" % [report[&"back_cash"], report[&"back_cash_in"], report[&"back_top_up"], report[&"rank_tax_cash_out"], report[&"loan_interest_cash_out"], report[&"finance_arrears"]]
var summary: Dictionary = report[&"selected_close"]
if summary.is_empty():
if not _day_picker.disabled or _day_picker.item_count == 0:
@@ -143,6 +162,9 @@ func refresh() -> void:
for key: StringName in [&"maintenance", &"utilities", &"taxes_and_interest", &"fines"]:
other += int(totals.get(key, 0))
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
var funds: Dictionary = (summary.get(&"settlement", {}) as Dictionary).get(&"funds", {})
if not funds.is_empty():
_closed_label.text += "\nうち表の売上 %dG / 裏の売上 %dG\n世界樹ランク税 %dG(支払 %dG)/ 利息 %dG(支払 %dG)\n裏金から表金へ補填 %dG / 未払い 税 %dG・利息 %dG" % [int(funds.get(&"revenue_front", 0)), int(funds.get(&"revenue_back", 0)), int(funds.get(&"rank_tax", 0)), int(funds.get(&"rank_tax_paid", 0)), int(funds.get(&"loan_interest", 0)), int(funds.get(&"loan_interest_paid", 0)), int(funds.get(&"back_top_up", 0)), int(funds.get(&"rank_tax_arrears", 0)), int(funds.get(&"loan_interest_arrears", 0))]
if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"):
_closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))]
if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])):