P05-02: 全費目と資金区分:表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税の帳簿への接続(原 P05-02 の完了の条件)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://l14tq0avw0ux
|
||||
@@ -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:
|
||||
|
||||
@@ -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"])):
|
||||
|
||||
Reference in New Issue
Block a user