From dd03541cfa9fee99cc30b949151adaabf9903c34 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Wed, 21 Oct 2026 21:29:37 -0500 Subject: [PATCH] feat(ledger): allocation share reads and record_acquisition `get_allocation_share(graph_id, entity_id, period)` reads a member's month as the consolidation engine will: its result, the outside owners' pro-rata share, the override if one is set, the share the entries use, and whether the month is closed. `list_allocation_overrides` lists the months an override is set for. `record_acquisition` records a member bought from its owners. Regenerated against robosystems feature/group-acquisition; the residual gains its `cause`. The twin of typescript-client #292. Claude-Session: https://claude.ai/code/session_01VPxBBHkqhwkZ6Eq8EXSBR6 --- .../record_acquisition.py | 310 ++++++++++++++++++ robosystems_client/clients/ledger_client.py | 74 +++++ .../graphql/generated/__init__.py | 20 ++ .../graphql/generated/client.py | 30 ++ .../generated/get_ledger_allocation_share.py | 37 +++ .../list_ledger_allocation_overrides.py | 28 ++ .../graphql/generated/operations.py | 41 +++ .../ledger/GetLedgerAllocationShare.graphql | 8 + .../ListLedgerAllocationOverrides.graphql | 6 + robosystems_client/graphql/schema.graphql | 91 +++++ robosystems_client/models/__init__.py | 16 + .../models/acquisition_allocation.py | 73 +++++ .../models/acquisition_response.py | 155 +++++++++ .../models/consolidation_residual.py | 22 ++ .../models/consolidation_residual_cause.py | 9 + ...operation_envelope_acquisition_response.py | 158 +++++++++ ...on_envelope_acquisition_response_status.py | 10 + .../models/record_acquisition_request.py | 145 ++++++++ tests/test_ledger_client.py | 80 +++++ 19 files changed, 1313 insertions(+) create mode 100644 robosystems_client/api/robo_ledger_ledger_events/record_acquisition.py create mode 100644 robosystems_client/graphql/generated/get_ledger_allocation_share.py create mode 100644 robosystems_client/graphql/generated/list_ledger_allocation_overrides.py create mode 100644 robosystems_client/graphql/operations/ledger/GetLedgerAllocationShare.graphql create mode 100644 robosystems_client/graphql/operations/ledger/ListLedgerAllocationOverrides.graphql create mode 100644 robosystems_client/models/acquisition_allocation.py create mode 100644 robosystems_client/models/acquisition_response.py create mode 100644 robosystems_client/models/consolidation_residual_cause.py create mode 100644 robosystems_client/models/operation_envelope_acquisition_response.py create mode 100644 robosystems_client/models/operation_envelope_acquisition_response_status.py create mode 100644 robosystems_client/models/record_acquisition_request.py diff --git a/robosystems_client/api/robo_ledger_ledger_events/record_acquisition.py b/robosystems_client/api/robo_ledger_ledger_events/record_acquisition.py new file mode 100644 index 0000000..a215e0d --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/record_acquisition.py @@ -0,0 +1,310 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.operation_envelope_acquisition_response import ( + OperationEnvelopeAcquisitionResponse, +) +from ...models.record_acquisition_request import RecordAcquisitionRequest +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: RecordAcquisitionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(idempotency_key, Unset): + headers["Idempotency-Key"] = idempotency_key + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/extensions/roboledger/{graph_id}/operations/record-acquisition".format( + graph_id=quote(str(graph_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | OperationEnvelopeAcquisitionResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeAcquisitionResponse.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 500: + response_500 = ErrorResponse.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | OperationEnvelopeAcquisitionResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: RecordAcquisitionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeAcquisitionResponse]: + """Record Acquisition + + Record a member the parent bought from its outside owners, for a member that joined mid-life + (`joined_on` set) with its balance sheet at acquisition recorded by record-opening-balance. Give the + consideration paid to the sellers in cents and the basis (the purchase agreement, a valuation); + optionally the fair value of the outside owners' interest (`nci_fair_value`, for goodwill on the + whole business) and fair-value step-ups (`allocations`). It writes a draft in the consolidation + entity, dated the join, that its close posts: the excess of the consideration over the parent's + share of the member's equity at acquisition goes to the step-ups and the rest to goodwill. preview- + consolidation-entries then reads the consideration as the parent's investment, so the pre- + acquisition residual clears. A bargain purchase is refused until `confirm_bargain_purchase` is set + after the reassessment ASC 805 asks for. Recording again replaces the draft; a posted acquisition is + reversed first. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (RecordAcquisitionRequest): A member bought from its outside owners: what the parent + paid them, + and how the excess over its share of the member's equity at acquisition is + allocated. The engine never invents a fair value; each comes from the + person, with the basis. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeAcquisitionResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: RecordAcquisitionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeAcquisitionResponse | None: + """Record Acquisition + + Record a member the parent bought from its outside owners, for a member that joined mid-life + (`joined_on` set) with its balance sheet at acquisition recorded by record-opening-balance. Give the + consideration paid to the sellers in cents and the basis (the purchase agreement, a valuation); + optionally the fair value of the outside owners' interest (`nci_fair_value`, for goodwill on the + whole business) and fair-value step-ups (`allocations`). It writes a draft in the consolidation + entity, dated the join, that its close posts: the excess of the consideration over the parent's + share of the member's equity at acquisition goes to the step-ups and the rest to goodwill. preview- + consolidation-entries then reads the consideration as the parent's investment, so the pre- + acquisition residual clears. A bargain purchase is refused until `confirm_bargain_purchase` is set + after the reassessment ASC 805 asks for. Recording again replaces the draft; a posted acquisition is + reversed first. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (RecordAcquisitionRequest): A member bought from its outside owners: what the parent + paid them, + and how the excess over its share of the member's equity at acquisition is + allocated. The engine never invents a fair value; each comes from the + person, with the basis. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeAcquisitionResponse + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: RecordAcquisitionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeAcquisitionResponse]: + """Record Acquisition + + Record a member the parent bought from its outside owners, for a member that joined mid-life + (`joined_on` set) with its balance sheet at acquisition recorded by record-opening-balance. Give the + consideration paid to the sellers in cents and the basis (the purchase agreement, a valuation); + optionally the fair value of the outside owners' interest (`nci_fair_value`, for goodwill on the + whole business) and fair-value step-ups (`allocations`). It writes a draft in the consolidation + entity, dated the join, that its close posts: the excess of the consideration over the parent's + share of the member's equity at acquisition goes to the step-ups and the rest to goodwill. preview- + consolidation-entries then reads the consideration as the parent's investment, so the pre- + acquisition residual clears. A bargain purchase is refused until `confirm_bargain_purchase` is set + after the reassessment ASC 805 asks for. Recording again replaces the draft; a posted acquisition is + reversed first. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (RecordAcquisitionRequest): A member bought from its outside owners: what the parent + paid them, + and how the excess over its share of the member's equity at acquisition is + allocated. The engine never invents a fair value; each comes from the + person, with the basis. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeAcquisitionResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + *, + client: AuthenticatedClient, + body: RecordAcquisitionRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeAcquisitionResponse | None: + """Record Acquisition + + Record a member the parent bought from its outside owners, for a member that joined mid-life + (`joined_on` set) with its balance sheet at acquisition recorded by record-opening-balance. Give the + consideration paid to the sellers in cents and the basis (the purchase agreement, a valuation); + optionally the fair value of the outside owners' interest (`nci_fair_value`, for goodwill on the + whole business) and fair-value step-ups (`allocations`). It writes a draft in the consolidation + entity, dated the join, that its close posts: the excess of the consideration over the parent's + share of the member's equity at acquisition goes to the step-ups and the rest to goodwill. preview- + consolidation-entries then reads the consideration as the parent's investment, so the pre- + acquisition residual clears. A bargain purchase is refused until `confirm_bargain_purchase` is set + after the reassessment ASC 805 asks for. Recording again replaces the draft; a posted acquisition is + reversed first. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (RecordAcquisitionRequest): A member bought from its outside owners: what the parent + paid them, + and how the excess over its share of the member's equity at acquisition is + allocated. The engine never invents a fair value; each comes from the + person, with the basis. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeAcquisitionResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 5eeaf9d..f793003 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -63,6 +63,9 @@ from ..api.robo_ledger_ledger_events.set_allocation_override import ( sync_detailed as op_set_allocation_override, ) +from ..api.robo_ledger_ledger_events.record_acquisition import ( + sync_detailed as op_record_acquisition, +) from ..api.robo_ledger_ledger_events.record_statement_balance import ( sync_detailed as op_record_statement_balance, ) @@ -415,6 +418,18 @@ from ..graphql.generated.list_ledger_event_handlers import ( ListLedgerEventHandlers, ) +from ..graphql.generated.get_ledger_allocation_share import ( + GetLedgerAllocationShare, +) +from ..graphql.generated.get_ledger_allocation_share import ( + GetLedgerAllocationShareAllocationShare as AllocationShare, +) +from ..graphql.generated.list_ledger_allocation_overrides import ( + ListLedgerAllocationOverrides, +) +from ..graphql.generated.list_ledger_allocation_overrides import ( + ListLedgerAllocationOverridesAllocationOverridesOverrides as AllocationOverride, +) from ..graphql.generated.list_ledger_event_handlers import ( ListLedgerEventHandlersEventHandlers as EventHandlersPage, ) @@ -542,6 +557,8 @@ LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, LIST_LEDGER_REPORT_SHARES_GQL, LIST_LEDGER_EVENT_HANDLERS_GQL, + GET_LEDGER_ALLOCATION_SHARE_GQL, + LIST_LEDGER_ALLOCATION_OVERRIDES_GQL, LIST_LEDGER_AGENTS_GQL, LIST_LEDGER_DIMENSIONS_GQL, LIST_LEDGER_ELEMENTS_GQL, @@ -602,6 +619,9 @@ ProposeConsolidationEntriesResponse, ) from ..models.set_allocation_override_request import SetAllocationOverrideRequest +from ..models.acquisition_allocation import AcquisitionAllocation +from ..models.acquisition_response import AcquisitionResponse +from ..models.record_acquisition_request import RecordAcquisitionRequest from ..models.live_financial_statement_request import LiveFinancialStatementRequest from ..models.financial_statement_analysis_response import ( FinancialStatementAnalysisResponse, @@ -2415,6 +2435,60 @@ def set_allocation_override( "Set allocation override", envelope, AllocationOverrideResponse ) + def get_allocation_share( + self, graph_id: str, entity_id: str, period: str + ) -> AllocationShare | None: + """The outside owners' share of a member's result for ``period`` + (``YYYY-MM``), in cents: the pro-rata figure, the override if one is set, + and the share the consolidating entries use (``effective_amount``).""" + data = self._query( + graph_id, + GET_LEDGER_ALLOCATION_SHARE_GQL, + {"entityId": entity_id, "period": period}, + ) + return GetLedgerAllocationShare.model_validate(data).allocation_share + + def list_allocation_overrides( + self, graph_id: str, entity_id: str + ) -> list[AllocationOverride]: + """Every month a member's allocation override is set for, oldest first.""" + data = self._query( + graph_id, LIST_LEDGER_ALLOCATION_OVERRIDES_GQL, {"entityId": entity_id} + ) + listed = ListLedgerAllocationOverrides.model_validate(data).allocation_overrides + return list(listed.overrides) if listed else [] + + def record_acquisition( + self, + graph_id: str, + *, + entity_id: str, + consideration: int, + basis: str, + nci_fair_value: int | None = None, + allocations: list[dict] | None = None, + confirm_bargain_purchase: bool = False, + ) -> AcquisitionResponse: + """Record a member the parent bought from its owners: the + ``consideration`` paid the sellers in cents, with the ``basis``, and + optionally the outside owners' fair value and fair-value step-ups + (``allocations``, each ``{"concept": qname, "amount": cents}``). Writes a + draft in the consolidation entity that its close posts; a bargain + purchase needs ``confirm_bargain_purchase``.""" + body = RecordAcquisitionRequest( + entity_id=entity_id, + consideration=consideration, + basis=basis, + nci_fair_value=nci_fair_value if nci_fair_value is not None else UNSET, + allocations=[AcquisitionAllocation.from_dict(a) for a in allocations or []], + confirm_bargain_purchase=confirm_bargain_purchase, + ) + response = op_record_acquisition( + graph_id=graph_id, body=body, client=self._get_client() + ) + envelope = self._call_op("Record acquisition", response) + return self._typed_result("Record acquisition", envelope, AcquisitionResponse) + # ── Event blocks (generic preview + status transitions) ────────────── def create_event_block( diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index 4eadfff..fe60d8f 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -111,6 +111,11 @@ GetLedgerAgentAgentClassification, GetLedgerAgentAgentClassifications, ) +from .get_ledger_allocation_share import ( + GetLedgerAllocationShare, + GetLedgerAllocationShareAllocationShare, + GetLedgerAllocationShareAllocationShareOverride, +) from .get_ledger_closing_book_structures import ( GetLedgerClosingBookStructures, GetLedgerClosingBookStructuresClosingBookStructures, @@ -296,6 +301,11 @@ ListLedgerAgentsAgentsClassification, ListLedgerAgentsAgentsClassifications, ) +from .list_ledger_allocation_overrides import ( + ListLedgerAllocationOverrides, + ListLedgerAllocationOverridesAllocationOverrides, + ListLedgerAllocationOverridesAllocationOverridesOverrides, +) from .list_ledger_bank_accounts import ( ListLedgerBankAccounts, ListLedgerBankAccountsBankAccounts, @@ -427,6 +437,7 @@ GET_LEDGER_ACCOUNT_ROLLUPS_GQL, GET_LEDGER_ACCOUNT_TREE_GQL, GET_LEDGER_AGENT_GQL, + GET_LEDGER_ALLOCATION_SHARE_GQL, GET_LEDGER_CLOSING_BOOK_STRUCTURES_GQL, GET_LEDGER_ENTITY_GQL, GET_LEDGER_EVENT_BLOCK_GQL, @@ -458,6 +469,7 @@ LIST_INVESTOR_SECURITIES_GQL, LIST_LEDGER_ACCOUNTS_GQL, LIST_LEDGER_AGENTS_GQL, + LIST_LEDGER_ALLOCATION_OVERRIDES_GQL, LIST_LEDGER_BANK_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, LIST_LEDGER_DIMENSIONS_GQL, @@ -534,6 +546,7 @@ "GET_LEDGER_ACCOUNT_ROLLUPS_GQL", "GET_LEDGER_ACCOUNT_TREE_GQL", "GET_LEDGER_AGENT_GQL", + "GET_LEDGER_ALLOCATION_SHARE_GQL", "GET_LEDGER_CLOSING_BOOK_STRUCTURES_GQL", "GET_LEDGER_ENTITY_GQL", "GET_LEDGER_EVENT_BLOCK_GQL", @@ -614,6 +627,9 @@ "GetLedgerAgentAgent", "GetLedgerAgentAgentClassification", "GetLedgerAgentAgentClassifications", + "GetLedgerAllocationShare", + "GetLedgerAllocationShareAllocationShare", + "GetLedgerAllocationShareAllocationShareOverride", "GetLedgerClosingBookStructures", "GetLedgerClosingBookStructuresClosingBookStructures", "GetLedgerClosingBookStructuresClosingBookStructuresCategories", @@ -719,6 +735,7 @@ "LIST_INVESTOR_SECURITIES_GQL", "LIST_LEDGER_ACCOUNTS_GQL", "LIST_LEDGER_AGENTS_GQL", + "LIST_LEDGER_ALLOCATION_OVERRIDES_GQL", "LIST_LEDGER_BANK_ACCOUNTS_GQL", "LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL", "LIST_LEDGER_DIMENSIONS_GQL", @@ -783,6 +800,9 @@ "ListLedgerAgentsAgents", "ListLedgerAgentsAgentsClassification", "ListLedgerAgentsAgentsClassifications", + "ListLedgerAllocationOverrides", + "ListLedgerAllocationOverridesAllocationOverrides", + "ListLedgerAllocationOverridesAllocationOverridesOverrides", "ListLedgerBankAccounts", "ListLedgerBankAccountsBankAccounts", "ListLedgerBankAccountsBankAccountsAccounts", diff --git a/robosystems_client/graphql/generated/client.py b/robosystems_client/graphql/generated/client.py index 0391ef6..89673a2 100644 --- a/robosystems_client/graphql/generated/client.py +++ b/robosystems_client/graphql/generated/client.py @@ -17,6 +17,7 @@ from .get_ledger_account_rollups import GetLedgerAccountRollups from .get_ledger_account_tree import GetLedgerAccountTree from .get_ledger_agent import GetLedgerAgent +from .get_ledger_allocation_share import GetLedgerAllocationShare from .get_ledger_closing_book_structures import GetLedgerClosingBookStructures from .get_ledger_entity import GetLedgerEntity from .get_ledger_event_block import GetLedgerEventBlock @@ -48,6 +49,7 @@ from .list_investor_securities import ListInvestorSecurities from .list_ledger_accounts import ListLedgerAccounts from .list_ledger_agents import ListLedgerAgents +from .list_ledger_allocation_overrides import ListLedgerAllocationOverrides from .list_ledger_bank_accounts import ListLedgerBankAccounts from .list_ledger_blocked_source_graphs import ListLedgerBlockedSourceGraphs from .list_ledger_dimensions import ListLedgerDimensions @@ -86,6 +88,7 @@ GET_LEDGER_ACCOUNT_ROLLUPS_GQL, GET_LEDGER_ACCOUNT_TREE_GQL, GET_LEDGER_AGENT_GQL, + GET_LEDGER_ALLOCATION_SHARE_GQL, GET_LEDGER_CLOSING_BOOK_STRUCTURES_GQL, GET_LEDGER_ENTITY_GQL, GET_LEDGER_EVENT_BLOCK_GQL, @@ -117,6 +120,7 @@ LIST_INVESTOR_SECURITIES_GQL, LIST_LEDGER_ACCOUNTS_GQL, LIST_LEDGER_AGENTS_GQL, + LIST_LEDGER_ALLOCATION_OVERRIDES_GQL, LIST_LEDGER_BANK_ACCOUNTS_GQL, LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL, LIST_LEDGER_DIMENSIONS_GQL, @@ -453,6 +457,19 @@ def get_ledger_agent(self, id: str, **kwargs: Any) -> GetLedgerAgent: data = self.get_data(response) return GetLedgerAgent.model_validate(data) + def get_ledger_allocation_share( + self, entity_id: str, period: str, **kwargs: Any + ) -> GetLedgerAllocationShare: + variables: dict[str, object] = {"entityId": entity_id, "period": period} + response = self.execute( + query=GET_LEDGER_ALLOCATION_SHARE_GQL, + operation_name="GetLedgerAllocationShare", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return GetLedgerAllocationShare.model_validate(data) + def get_ledger_closing_book_structures( self, entity_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any ) -> GetLedgerClosingBookStructures: @@ -830,6 +847,19 @@ def list_ledger_agents( data = self.get_data(response) return ListLedgerAgents.model_validate(data) + def list_ledger_allocation_overrides( + self, entity_id: str, **kwargs: Any + ) -> ListLedgerAllocationOverrides: + variables: dict[str, object] = {"entityId": entity_id} + response = self.execute( + query=LIST_LEDGER_ALLOCATION_OVERRIDES_GQL, + operation_name="ListLedgerAllocationOverrides", + variables=variables, + **kwargs, + ) + data = self.get_data(response) + return ListLedgerAllocationOverrides.model_validate(data) + def list_ledger_bank_accounts( self, entity_id: Union[Optional[str], UnsetType] = UNSET, **kwargs: Any ) -> ListLedgerBankAccounts: diff --git a/robosystems_client/graphql/generated/get_ledger_allocation_share.py b/robosystems_client/graphql/generated/get_ledger_allocation_share.py new file mode 100644 index 0000000..c123d80 --- /dev/null +++ b/robosystems_client/graphql/generated/get_ledger_allocation_share.py @@ -0,0 +1,37 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class GetLedgerAllocationShare(BaseModel): + allocation_share: Optional["GetLedgerAllocationShareAllocationShare"] = Field( + alias="allocationShare" + ) + + +class GetLedgerAllocationShareAllocationShare(BaseModel): + entity_id: str = Field(alias="entityId") + entity_name: str = Field(alias="entityName") + period: str + consolidation_method: str = Field(alias="consolidationMethod") + ownership_pct: Optional[float] = Field(alias="ownershipPct") + outside_pct: float = Field(alias="outsidePct") + month_result: int = Field(alias="monthResult") + pro_rata_amount: int = Field(alias="proRataAmount") + override: Optional["GetLedgerAllocationShareAllocationShareOverride"] + effective_amount: int = Field(alias="effectiveAmount") + closed: bool + + +class GetLedgerAllocationShareAllocationShareOverride(BaseModel): + period: str + amount: Optional[int] + percentage: Optional[float] + basis: Optional[str] + set_by: Optional[str] = Field(alias="setBy") + + +GetLedgerAllocationShare.model_rebuild() +GetLedgerAllocationShareAllocationShare.model_rebuild() diff --git a/robosystems_client/graphql/generated/list_ledger_allocation_overrides.py b/robosystems_client/graphql/generated/list_ledger_allocation_overrides.py new file mode 100644 index 0000000..c5e6a3c --- /dev/null +++ b/robosystems_client/graphql/generated/list_ledger_allocation_overrides.py @@ -0,0 +1,28 @@ +from typing import Optional + +from pydantic import Field + +from .base_model import BaseModel + + +class ListLedgerAllocationOverrides(BaseModel): + allocation_overrides: Optional["ListLedgerAllocationOverridesAllocationOverrides"] = ( + Field(alias="allocationOverrides") + ) + + +class ListLedgerAllocationOverridesAllocationOverrides(BaseModel): + entity_id: str = Field(alias="entityId") + overrides: list["ListLedgerAllocationOverridesAllocationOverridesOverrides"] + + +class ListLedgerAllocationOverridesAllocationOverridesOverrides(BaseModel): + period: str + amount: Optional[int] + percentage: Optional[float] + basis: Optional[str] + set_by: Optional[str] = Field(alias="setBy") + + +ListLedgerAllocationOverrides.model_rebuild() +ListLedgerAllocationOverridesAllocationOverrides.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index bf3253b..94985c1 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -13,6 +13,7 @@ "GET_LEDGER_ACCOUNT_ROLLUPS_GQL", "GET_LEDGER_ACCOUNT_TREE_GQL", "GET_LEDGER_AGENT_GQL", + "GET_LEDGER_ALLOCATION_SHARE_GQL", "GET_LEDGER_CLOSING_BOOK_STRUCTURES_GQL", "GET_LEDGER_ENTITY_GQL", "GET_LEDGER_EVENT_BLOCK_GQL", @@ -44,6 +45,7 @@ "LIST_INVESTOR_SECURITIES_GQL", "LIST_LEDGER_ACCOUNTS_GQL", "LIST_LEDGER_AGENTS_GQL", + "LIST_LEDGER_ALLOCATION_OVERRIDES_GQL", "LIST_LEDGER_BANK_ACCOUNTS_GQL", "LIST_LEDGER_BLOCKED_SOURCE_GRAPHS_GQL", "LIST_LEDGER_DIMENSIONS_GQL", @@ -876,6 +878,30 @@ } """ +GET_LEDGER_ALLOCATION_SHARE_GQL = """ +query GetLedgerAllocationShare($entityId: String!, $period: String!) { + allocationShare(entityId: $entityId, period: $period) { + entityId + entityName + period + consolidationMethod + ownershipPct + outsidePct + monthResult + proRataAmount + override { + period + amount + percentage + basis + setBy + } + effectiveAmount + closed + } +} +""" + GET_LEDGER_CLOSING_BOOK_STRUCTURES_GQL = """ query GetLedgerClosingBookStructures($entityId: String) { closingBookStructures(entityId: $entityId) { @@ -1833,6 +1859,21 @@ } """ +LIST_LEDGER_ALLOCATION_OVERRIDES_GQL = """ +query ListLedgerAllocationOverrides($entityId: String!) { + allocationOverrides(entityId: $entityId) { + entityId + overrides { + period + amount + percentage + basis + setBy + } + } +} +""" + LIST_LEDGER_BANK_ACCOUNTS_GQL = """ query ListLedgerBankAccounts($entityId: String) { bankAccounts(entityId: $entityId) { diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerAllocationShare.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerAllocationShare.graphql new file mode 100644 index 0000000..af70f8f --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/GetLedgerAllocationShare.graphql @@ -0,0 +1,8 @@ +query GetLedgerAllocationShare($entityId: String!, $period: String!) { + allocationShare(entityId: $entityId, period: $period) { + entityId entityName period consolidationMethod ownershipPct outsidePct + monthResult proRataAmount + override { period amount percentage basis setBy } + effectiveAmount closed + } +} diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerAllocationOverrides.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerAllocationOverrides.graphql new file mode 100644 index 0000000..e980fe9 --- /dev/null +++ b/robosystems_client/graphql/operations/ledger/ListLedgerAllocationOverrides.graphql @@ -0,0 +1,6 @@ +query ListLedgerAllocationOverrides($entityId: String!) { + allocationOverrides(entityId: $entityId) { + entityId + overrides { period amount percentage basis setBy } + } +} diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index d624901..cb61c0e 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -814,6 +814,26 @@ type Query { listId: String! ): PublishListDetail + """ + The outside owners' share of a member's result for one month: the + pro-rata figure its ownership gives, the override set with + `set-allocation-override` if any, and the share the consolidating + entries use. + """ + allocationShare( + """The member.""" + entityId: String! + + """The month, as YYYY-MM.""" + period: String! + ): AllocationShare + + """Every month a member's allocation override is set for, oldest first.""" + allocationOverrides( + """The member.""" + entityId: String! + ): AllocationOverrideList + """ The graph's event handlers — the rules that turn a matching event into GL rows — highest priority first, active or not. @@ -4691,6 +4711,77 @@ type PublishListMember { addedAt: DateTime! } +""" +The outside owners' share of a member's result for one month: the +pro-rata figure its ownership gives, the override that replaces it if one +is set, and the share the consolidating entries use. +""" +type AllocationShare { + """The member.""" + entityId: String! + + """The member's name.""" + entityName: String! + + """The month, as YYYY-MM.""" + period: String! + + """How the member is consolidated: full, equity, cost or none.""" + consolidationMethod: String! + + """The parent's ownership, as a percentage.""" + ownershipPct: Float + + """The outside owners' ownership, as a percentage.""" + outsidePct: Float! + + """Cents: the member's result for the month, positive for a profit.""" + monthResult: Int! + + """Cents: the outside owners' percentage of the month's result.""" + proRataAmount: Int! + + """The month's override, when one is set.""" + override: AllocationOverrideRecord + + """ + Cents: the share the consolidating entries use, the override's when set. + """ + effectiveAmount: Int! + + """ + True when the consolidation entity has closed the month, so the override can no longer change. + """ + closed: Boolean! +} + +"""A member's allocation override for one month, as set.""" +type AllocationOverrideRecord { + """The month, as YYYY-MM.""" + period: String! + + """The share, in cents, when set so.""" + amount: Int + + """The share as a percentage of the month's result, when set so.""" + percentage: Float + + """Why it differs from pro rata.""" + basis: String + + """Who set it.""" + setBy: String +} + +"""Every month a member's allocation override is set for.""" +type AllocationOverrideList { + """The member.""" + entityId: String! + + """The overrides, oldest month first.""" + overrides: [AllocationOverrideRecord!]! +} + """A page of event handlers, highest priority first.""" type EventHandlerList { """Handlers, highest priority first.""" diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index b0293f4..a412d12 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -5,6 +5,8 @@ from .accept_suggestions_request import AcceptSuggestionsRequest from .accept_suggestions_response import AcceptSuggestionsResponse from .account_info import AccountInfo +from .acquisition_allocation import AcquisitionAllocation +from .acquisition_response import AcquisitionResponse from .add_graph_member_request import AddGraphMemberRequest from .add_publish_list_members_operation import AddPublishListMembersOperation from .agent_classification import AgentClassification @@ -91,6 +93,7 @@ from .consolidation_proposal_line import ConsolidationProposalLine from .consolidation_reconciling_item import ConsolidationReconcilingItem from .consolidation_residual import ConsolidationResidual +from .consolidation_residual_cause import ConsolidationResidualCause from .consolidation_worksheet_request import ConsolidationWorksheetRequest from .consolidation_worksheet_response import ConsolidationWorksheetResponse from .content_limits import ContentLimits @@ -468,6 +471,12 @@ from .operation_envelope_accept_suggestions_response_status import ( OperationEnvelopeAcceptSuggestionsResponseStatus, ) +from .operation_envelope_acquisition_response import ( + OperationEnvelopeAcquisitionResponse, +) +from .operation_envelope_acquisition_response_status import ( + OperationEnvelopeAcquisitionResponseStatus, +) from .operation_envelope_allocation_override_response import ( OperationEnvelopeAllocationOverrideResponse, ) @@ -933,6 +942,7 @@ ReconcilingItemPlanDefaultDisposition, ) from .reconciling_item_regenerated import ReconcilingItemRegenerated +from .record_acquisition_request import RecordAcquisitionRequest from .record_opening_balance_request import RecordOpeningBalanceRequest from .record_statement_balance_request import RecordStatementBalanceRequest from .recovery_codes_request import RecoveryCodesRequest @@ -1206,6 +1216,8 @@ "AcceptSuggestionsRequest", "AcceptSuggestionsResponse", "AccountInfo", + "AcquisitionAllocation", + "AcquisitionResponse", "AddGraphMemberRequest", "AddPublishListMembersOperation", "AgentClassification", @@ -1282,6 +1294,7 @@ "ConsolidationProposalLine", "ConsolidationReconcilingItem", "ConsolidationResidual", + "ConsolidationResidualCause", "ConsolidationWorksheetRequest", "ConsolidationWorksheetResponse", "ContentLimits", @@ -1579,6 +1592,8 @@ "OperationEnvelope", "OperationEnvelopeAcceptSuggestionsResponse", "OperationEnvelopeAcceptSuggestionsResponseStatus", + "OperationEnvelopeAcquisitionResponse", + "OperationEnvelopeAcquisitionResponseStatus", "OperationEnvelopeAllocationOverrideResponse", "OperationEnvelopeAllocationOverrideResponseStatus", "OperationEnvelopeAssertMetricsResponse", @@ -1806,6 +1821,7 @@ "ReconcilingItemPlan", "ReconcilingItemPlanDefaultDisposition", "ReconcilingItemRegenerated", + "RecordAcquisitionRequest", "RecordOpeningBalanceRequest", "RecordStatementBalanceRequest", "RecoveryCodesRequest", diff --git a/robosystems_client/models/acquisition_allocation.py b/robosystems_client/models/acquisition_allocation.py new file mode 100644 index 0000000..069615b --- /dev/null +++ b/robosystems_client/models/acquisition_allocation.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="AcquisitionAllocation") + + +@_attrs_define +class AcquisitionAllocation: + """Part of an acquisition's excess assigned to an identifiable asset or + liability at fair value instead of goodwill: a step-up. + + Attributes: + concept (str): The rs-gaap concept stepped up, e.g. `rs-gaap:PropertyPlantAndEquipmentNet`; the consolidation + entity needs an account mapped to it. + amount (int): Cents, debit-positive: fair value less book value at acquisition. Positive raises an asset; + negative raises a liability. + """ + + concept: str + amount: int + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + concept = self.concept + + amount = self.amount + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "concept": concept, + "amount": amount, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + concept = d.pop("concept") + + amount = d.pop("amount") + + acquisition_allocation = cls( + concept=concept, + amount=amount, + ) + + acquisition_allocation.additional_properties = d + return acquisition_allocation + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/acquisition_response.py b/robosystems_client/models/acquisition_response.py new file mode 100644 index 0000000..17c821a --- /dev/null +++ b/robosystems_client/models/acquisition_response.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.journal_entry_response import JournalEntryResponse + + +T = TypeVar("T", bound="AcquisitionResponse") + + +@_attrs_define +class AcquisitionResponse: + """A member's acquisition as recorded: the draft in the consolidation + entity its close posts. + + Attributes: + entity_id (str): The member. + entity_name (str): The member's name. + joined_on (datetime.date): When it joined the group. + equity_at_acquisition (int): Cents: the member's equity in its opening balance. + parent_share (int): Cents: the parent's share of that equity. + consideration (int): Cents paid to the sellers. + excess (int): Cents: consideration less the parent's share of the equity. + goodwill (int): Cents of goodwill; negative for a bargain purchase gain. + entry (JournalEntryResponse): The common response shape for journal entry write operations. + noncontrolling_interest (int | Unset): Cents added to the outside owners' interest beyond their share of the + member's book equity. Default: 0. + replaced (bool | Unset): True when it replaced an earlier draft of the acquisition. Default: False. + """ + + entity_id: str + entity_name: str + joined_on: datetime.date + equity_at_acquisition: int + parent_share: int + consideration: int + excess: int + goodwill: int + entry: JournalEntryResponse + noncontrolling_interest: int | Unset = 0 + replaced: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entity_id = self.entity_id + + entity_name = self.entity_name + + joined_on = self.joined_on.isoformat() + + equity_at_acquisition = self.equity_at_acquisition + + parent_share = self.parent_share + + consideration = self.consideration + + excess = self.excess + + goodwill = self.goodwill + + entry = self.entry.to_dict() + + noncontrolling_interest = self.noncontrolling_interest + + replaced = self.replaced + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entity_id": entity_id, + "entity_name": entity_name, + "joined_on": joined_on, + "equity_at_acquisition": equity_at_acquisition, + "parent_share": parent_share, + "consideration": consideration, + "excess": excess, + "goodwill": goodwill, + "entry": entry, + } + ) + if noncontrolling_interest is not UNSET: + field_dict["noncontrolling_interest"] = noncontrolling_interest + if replaced is not UNSET: + field_dict["replaced"] = replaced + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.journal_entry_response import JournalEntryResponse + + d = dict(src_dict) + entity_id = d.pop("entity_id") + + entity_name = d.pop("entity_name") + + joined_on = datetime.date.fromisoformat(d.pop("joined_on")) + + equity_at_acquisition = d.pop("equity_at_acquisition") + + parent_share = d.pop("parent_share") + + consideration = d.pop("consideration") + + excess = d.pop("excess") + + goodwill = d.pop("goodwill") + + entry = JournalEntryResponse.from_dict(d.pop("entry")) + + noncontrolling_interest = d.pop("noncontrolling_interest", UNSET) + + replaced = d.pop("replaced", UNSET) + + acquisition_response = cls( + entity_id=entity_id, + entity_name=entity_name, + joined_on=joined_on, + equity_at_acquisition=equity_at_acquisition, + parent_share=parent_share, + consideration=consideration, + excess=excess, + goodwill=goodwill, + entry=entry, + noncontrolling_interest=noncontrolling_interest, + replaced=replaced, + ) + + acquisition_response.additional_properties = d + return acquisition_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/consolidation_residual.py b/robosystems_client/models/consolidation_residual.py index a71abcb..3e42529 100644 --- a/robosystems_client/models/consolidation_residual.py +++ b/robosystems_client/models/consolidation_residual.py @@ -6,6 +6,9 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..models.consolidation_residual_cause import ConsolidationResidualCause +from ..types import UNSET, Unset + T = TypeVar("T", bound="ConsolidationResidual") @@ -22,12 +25,17 @@ class ConsolidationResidual: amount (int): Cents: the parent's investment balance less its share of the member's capital. Positive when the parent carries more than its share. explanation (str): What the two figures are. + cause (ConsolidationResidualCause | Unset): `pre_acquisition` when the member joined the group mid-life and its + acquisition is not recorded: the parent paid its sellers, not the member, so `record-acquisition` explains the + difference — the consideration, with the excess over the parent's share of the member's equity at acquisition as + goodwill or fair-value step-ups. Default: ConsolidationResidualCause.UNEXPLAINED. """ entity_id: str entity_name: str amount: int explanation: str + cause: ConsolidationResidualCause | Unset = ConsolidationResidualCause.UNEXPLAINED additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -39,6 +47,10 @@ def to_dict(self) -> dict[str, Any]: explanation = self.explanation + cause: str | Unset = UNSET + if not isinstance(self.cause, Unset): + cause = self.cause.value + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -49,6 +61,8 @@ def to_dict(self) -> dict[str, Any]: "explanation": explanation, } ) + if cause is not UNSET: + field_dict["cause"] = cause return field_dict @@ -63,11 +77,19 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: explanation = d.pop("explanation") + _cause = d.pop("cause", UNSET) + cause: ConsolidationResidualCause | Unset + if isinstance(_cause, Unset): + cause = UNSET + else: + cause = ConsolidationResidualCause(_cause) + consolidation_residual = cls( entity_id=entity_id, entity_name=entity_name, amount=amount, explanation=explanation, + cause=cause, ) consolidation_residual.additional_properties = d diff --git a/robosystems_client/models/consolidation_residual_cause.py b/robosystems_client/models/consolidation_residual_cause.py new file mode 100644 index 0000000..6905e2d --- /dev/null +++ b/robosystems_client/models/consolidation_residual_cause.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class ConsolidationResidualCause(str, Enum): + PRE_ACQUISITION = "pre_acquisition" + UNEXPLAINED = "unexplained" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/operation_envelope_acquisition_response.py b/robosystems_client/models/operation_envelope_acquisition_response.py new file mode 100644 index 0000000..5bb88a6 --- /dev/null +++ b/robosystems_client/models/operation_envelope_acquisition_response.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.operation_envelope_acquisition_response_status import ( + OperationEnvelopeAcquisitionResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.acquisition_response import AcquisitionResponse + + +T = TypeVar("T", bound="OperationEnvelopeAcquisitionResponse") + + +@_attrs_define +class OperationEnvelopeAcquisitionResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeAcquisitionResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (AcquisitionResponse | None | Unset): Command-specific result payload + created_by (None | str | Unset): User ID that initiated the operation + idempotent_replay (bool | Unset): True when this envelope came from the idempotency cache — the underlying + command did not execute again. False on fresh executions. Default: False. + """ + + operation: str + operation_id: str + status: OperationEnvelopeAcquisitionResponseStatus + at: str + result: AcquisitionResponse | None | Unset = UNSET + created_by: None | str | Unset = UNSET + idempotent_replay: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.acquisition_response import AcquisitionResponse + + operation = self.operation + + operation_id = self.operation_id + + status = self.status.value + + at = self.at + + result: dict[str, Any] | None | Unset + if isinstance(self.result, Unset): + result = UNSET + elif isinstance(self.result, AcquisitionResponse): + result = self.result.to_dict() + else: + result = self.result + + created_by: None | str | Unset + if isinstance(self.created_by, Unset): + created_by = UNSET + else: + created_by = self.created_by + + idempotent_replay = self.idempotent_replay + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "operation": operation, + "operationId": operation_id, + "status": status, + "at": at, + } + ) + if result is not UNSET: + field_dict["result"] = result + if created_by is not UNSET: + field_dict["createdBy"] = created_by + if idempotent_replay is not UNSET: + field_dict["idempotentReplay"] = idempotent_replay + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.acquisition_response import AcquisitionResponse + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeAcquisitionResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result(data: object) -> AcquisitionResponse | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = AcquisitionResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AcquisitionResponse | None | Unset, data) + + result = _parse_result(d.pop("result", UNSET)) + + def _parse_created_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + created_by = _parse_created_by(d.pop("createdBy", UNSET)) + + idempotent_replay = d.pop("idempotentReplay", UNSET) + + operation_envelope_acquisition_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_acquisition_response.additional_properties = d + return operation_envelope_acquisition_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/operation_envelope_acquisition_response_status.py b/robosystems_client/models/operation_envelope_acquisition_response_status.py new file mode 100644 index 0000000..5171938 --- /dev/null +++ b/robosystems_client/models/operation_envelope_acquisition_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeAcquisitionResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/record_acquisition_request.py b/robosystems_client/models/record_acquisition_request.py new file mode 100644 index 0000000..fab05d2 --- /dev/null +++ b/robosystems_client/models/record_acquisition_request.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.acquisition_allocation import AcquisitionAllocation + + +T = TypeVar("T", bound="RecordAcquisitionRequest") + + +@_attrs_define +class RecordAcquisitionRequest: + """A member bought from its outside owners: what the parent paid them, + and how the excess over its share of the member's equity at acquisition is + allocated. The engine never invents a fair value; each comes from the + person, with the basis. + + Attributes: + entity_id (str): The member acquired, consolidated in full, with `joined_on` set and its opening balance + recorded at acquisition. + consideration (int): Cents the parent paid the sellers for its interest. Its investment line names the seller, + not the member, which is why it is given here. + basis (str): Where the consideration and fair values come from: the purchase agreement, a valuation. + nci_fair_value (int | None | Unset): Cents: the fair value of the outside owners' interest at acquisition, for + goodwill on the whole business (ASC 805). Omitted, the outside owners carry their share of the identifiable net + assets and goodwill is the parent's alone. + allocations (list[AcquisitionAllocation] | Unset): Fair-value step-ups; what is left of the excess is goodwill. + confirm_bargain_purchase (bool | Unset): Required when the parent paid less than its share of the fair value of + what it acquired. ASC 805 has the acquirer reassess before recognizing the gain; set it only after that review. + Default: False. + """ + + entity_id: str + consideration: int + basis: str + nci_fair_value: int | None | Unset = UNSET + allocations: list[AcquisitionAllocation] | Unset = UNSET + confirm_bargain_purchase: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + entity_id = self.entity_id + + consideration = self.consideration + + basis = self.basis + + nci_fair_value: int | None | Unset + if isinstance(self.nci_fair_value, Unset): + nci_fair_value = UNSET + else: + nci_fair_value = self.nci_fair_value + + allocations: list[dict[str, Any]] | Unset = UNSET + if not isinstance(self.allocations, Unset): + allocations = [] + for allocations_item_data in self.allocations: + allocations_item = allocations_item_data.to_dict() + allocations.append(allocations_item) + + confirm_bargain_purchase = self.confirm_bargain_purchase + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "entity_id": entity_id, + "consideration": consideration, + "basis": basis, + } + ) + if nci_fair_value is not UNSET: + field_dict["nci_fair_value"] = nci_fair_value + if allocations is not UNSET: + field_dict["allocations"] = allocations + if confirm_bargain_purchase is not UNSET: + field_dict["confirm_bargain_purchase"] = confirm_bargain_purchase + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.acquisition_allocation import AcquisitionAllocation + + d = dict(src_dict) + entity_id = d.pop("entity_id") + + consideration = d.pop("consideration") + + basis = d.pop("basis") + + def _parse_nci_fair_value(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + nci_fair_value = _parse_nci_fair_value(d.pop("nci_fair_value", UNSET)) + + _allocations = d.pop("allocations", UNSET) + allocations: list[AcquisitionAllocation] | Unset = UNSET + if _allocations is not UNSET: + allocations = [] + for allocations_item_data in _allocations: + allocations_item = AcquisitionAllocation.from_dict(allocations_item_data) + + allocations.append(allocations_item) + + confirm_bargain_purchase = d.pop("confirm_bargain_purchase", UNSET) + + record_acquisition_request = cls( + entity_id=entity_id, + consideration=consideration, + basis=basis, + nci_fair_value=nci_fair_value, + allocations=allocations, + confirm_bargain_purchase=confirm_bargain_purchase, + ) + + record_acquisition_request.additional_properties = d + return record_acquisition_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index cbb1385..a57f6a8 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -1137,6 +1137,34 @@ def test_set_allocation_override(self, mock_op, mock_config, graph_id): "clear": False, } + @patch("robosystems_client.clients.ledger_client.op_record_acquisition") + def test_record_acquisition(self, mock_op, mock_config, graph_id): + envelope = _envelope( + "record-acquisition", + {"entity_id": "ent_cafe", "consideration": 900000, "goodwill": 165000}, + ) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + result = client.record_acquisition( + graph_id, + entity_id="ent_cafe", + consideration=900000, + basis="Purchase agreement", + allocations=[ + {"concept": "rs-gaap:PropertyPlantAndEquipmentNet", "amount": 50000} + ], + ) + assert result["goodwill"] == 165000 + assert mock_op.call_args.kwargs["body"].to_dict() == { + "entity_id": "ent_cafe", + "consideration": 900000, + "basis": "Purchase agreement", + "allocations": [ + {"concept": "rs-gaap:PropertyPlantAndEquipmentNet", "amount": 50000} + ], + "confirm_bargain_purchase": False, + } + # ── Taxonomy / entity linking ─────────────────────────────────────────── @@ -3644,6 +3672,58 @@ def test_list_event_handlers(self, mock_query, mock_config, graph_id): "offset": 0, } + @patch("robosystems_client.clients.ledger_client.LedgerClient._query") + def test_get_allocation_share(self, mock_query, mock_config, graph_id): + mock_query.return_value = { + "allocationShare": { + "entityId": "ent_cafe", + "entityName": "Harbor Café", + "period": "2026-08", + "consolidationMethod": "full", + "ownershipPct": 70.0, + "outsidePct": 30.0, + "monthResult": 182500, + "proRataAmount": 54750, + "override": { + "period": "2026-08", + "amount": 100000, + "percentage": None, + "basis": "Preferred return", + "setBy": "usr_1", + }, + "effectiveAmount": 100000, + "closed": False, + } + } + client = LedgerClient(mock_config) + share = client.get_allocation_share(graph_id, "ent_cafe", "2026-08") + assert share is not None + assert (share.pro_rata_amount, share.effective_amount) == (54750, 100000) + assert share.override is not None and share.override.basis == "Preferred return" + assert mock_query.call_args.args[2] == {"entityId": "ent_cafe", "period": "2026-08"} + + @patch("robosystems_client.clients.ledger_client.LedgerClient._query") + def test_list_allocation_overrides(self, mock_query, mock_config, graph_id): + mock_query.return_value = { + "allocationOverrides": { + "entityId": "ent_cafe", + "overrides": [ + { + "period": "2026-08", + "amount": None, + "percentage": 40.0, + "basis": "Waterfall", + "setBy": "usr_1", + } + ], + } + } + client = LedgerClient(mock_config) + overrides = client.list_allocation_overrides(graph_id, "ent_cafe") + assert [o.period for o in overrides] == ["2026-08"] + mock_query.return_value = {"allocationOverrides": None} + assert client.list_allocation_overrides(graph_id, "ent_cafe") == [] + @patch("robosystems_client.clients.ledger_client.LedgerClient._query") def test_list_report_shares(self, mock_query, mock_config, graph_id): mock_query.return_value = {